Source code for versionah.cmdline

#
"""cmdline - Command line functionality for versionah."""
# Copyright © 2014-2018  James Rowe <jnrowe@gmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of versionah.
#
# versionah is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# versionah is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# versionah.  If not, see <http://www.gnu.org/licenses/>.

import datetime
import os
import re

import click
import jinja2
from jnrbase.colourise import psuccess
from jnrbase.iso_8601 import parse_datetime
from jnrbase.template import FILTERS
from jnrbase.xdg_basedir import get_data_dirs, user_data

from . import _version
from .models import (MONTHS, VALID_DATE, VALID_PACKAGE, VALID_VERSION,
                     VERSION_COMPS, Version, split_version)


[docs]class ReMatchParamType(click.ParamType): """Regular expression based parameter matcher.""" def __init__(self): """Initialise a new `ReMatchParamType` object.""" super(ReMatchParamType, self).__init__() # Set name to "<value>ParamType" self.name = self.__class__.__qualname__[:-9].lower()
[docs] def convert(self, value, param, ctx): """Check given name is valid. Args: value (str): Value given to flag param (click.Argument): Parameter being processed ctx (click.Context): Current command context Returns: str: Valid value """ if not getattr(self, 'matcher', None): raise NotImplementedError('No matcher provided') if not re.fullmatch(self.matcher, value): self.fail(repr(value)) return value
[docs]class NameParamType(ReMatchParamType): """Name parameter handler.""" matcher = VALID_PACKAGE
[docs]class VersionParamType(ReMatchParamType): """Version parameter handler.""" matcher = VALID_VERSION
[docs]class CliVersion(Version): """Specialisation of models.Version for command line usage.""" mk_data_dir = lambda s: os.path.join(s, 'templates') # NOQA: E731 pkg_data_dirs = [mk_data_dir(s) for s in get_data_dirs('versionah')] pkg_data_dirs.insert(0, mk_data_dir(user_data('versionah'))) env = jinja2.Environment( autoescape=jinja2.select_autoescape(['html', 'xml']), loader=jinja2.ChoiceLoader([jinja2.FileSystemLoader(s) for s in pkg_data_dirs]) ) env.loader.loaders.append(jinja2.PackageLoader('versionah', 'templates')) env.filters.update(FILTERS) filetypes = [s.split('.')[0] for s in env.list_templates()]
[docs] @staticmethod def display_types(): """Supported representation types. Returns: list[str]: Method names for representation types """ return [s[3:] for s in dir(CliVersion) if s.startswith('as_')]
[docs] def display(self, display_format): """Display a version string. Args: display_format (str): Format to display version string in Returns: str: Formatted version string """ return getattr(self, 'as_{}'.format(display_format))()
[docs] @staticmethod def read(filename): """Read a version file. Args: filename (str): Version file to read Returns: CliVersion: New `CliVersion` object representing file Raises: OSError: When ``filename`` doesn't exist ValueError: Unparsable version data """ with open(filename) as f: data = f.read().strip() match = re.search(r'This is ({}),? [vV]ersion ({}) \(({})\)'.format( VALID_PACKAGE, VALID_VERSION, VALID_DATE ), data) if not match: raise ValueError( 'No valid version identifier in {!r}'.format(filename)) name, version_str, date_str = match.groups() components = split_version(version_str) try: parsed = parse_datetime(date_str) except ValueError: parsed = datetime.datetime.strptime(date_str, '%d-%b-%Y') return CliVersion(components, name, parsed.date())
[docs] def write(self, filename, file_type, *, shtool=False): """Write a version file. Args: filename (str): Version file to write file_type (str): File type to write shtool (bool): Write shtool_ compatible files .. _shtool: http://www.gnu.org/software/shtool/shtool.html """ data = vars(self) data.update({ 'now': datetime.datetime.now(), 'utcnow': datetime.datetime.utcnow(), 'filename': filename, 'dateobj': self.date, 'resolution': self._resolution, }) if shtool: # %d-%b-%Y, if %b wasn't locale dependent shtool_date = '{:02d}-{}-{}'.format( self.date.day, MONTHS[self.date.month - 1], self.date.year ) data['magic'] = 'This is {}, Version {} ({})'.format( self.name, self.as_dotted(), shtool_date ) else: data['magic'] = 'This is {} version {} ({})'.format( self.name, self.as_dotted(), self.as_date() ) data.update({k: v for k, v in zip(VERSION_COMPS, self.components)}) data.update({k: getattr(self, 'as_{}'.format(k))() for k in self.display_types()}) template = self.env.get_template('{}.jinja'.format(file_type)) with open(filename, 'w') as f: f.write(template.render(data))
[docs]def guess_type(filename): """Guess output type from filename. Args: filename (str): File to operate on """ suffix = os.path.splitext(filename)[1][1:] if suffix in CliVersion.filetypes: file_type = suffix else: file_type = 'text' return file_type
@click.group(help='A tool to manage project version files.', epilog='Please report bugs at ' 'https://github.com/JNRowe/versionah/issues', context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(_version.dotted) def cli(): """Main command entry point.""" pass @cli.command() @click.option('-d', '--display', 'display_format', default='dotted', type=click.Choice(CliVersion.display_types()), help='Display format for output.') @click.option('-t', '--type', 'file_type', multiple=True, type=click.Choice(CliVersion.filetypes), help='Define the file type used for version file.') @click.option('--shtool/--no-shtool', help='Write shtool compatible output.') @click.argument('filename', type=click.Path(exists=True, dir_okay=False, writable=True), nargs=-1, required=True) @click.argument('bump', type=click.Choice(['major', 'minor', 'micro', 'patch'])) def bump(display_format, file_type, shtool, filename, bump): """Bump version in existing file. \f Args: display_format (str): Format to display output in filename (tuple[str]): File to operate on file_type (tuple[str]): File type to produce shtool (bool): Write shtool_ compatible files bump (str): Component to bump .. _shtool: http://www.gnu.org/software/shtool/shtool.html """ if file_type and len(file_type) != len(filename): raise click.BadParameter('Number of --type options and filename args ' 'must match!') multi = len(filename) != 1 for ftype, fname in zip(file_type + (None, ) * len(filename), filename): if not ftype: ftype = guess_type(fname) version = CliVersion.read(fname) version.bump(bump) version.write(fname, ftype, shtool=shtool) if multi: click.echo('{}: '.format(fname), nl=False) psuccess(version.display(display_format)) @cli.command(name='set') @click.option('-d', '--display', 'display_format', default='dotted', type=click.Choice(CliVersion.display_types()), help='Display format for output.') @click.option('-t', '--type', 'file_type', multiple=True, type=click.Choice(CliVersion.filetypes), help='Define the file type used for version file.') @click.option('--shtool/--no-shtool', help='Write shtool compatible output.') @click.option('-n', '--name', default=os.path.basename(os.getenv('PWD')), type=NameParamType(), help='Package name for version(default from $PWD).') @click.argument('filename', type=click.Path(dir_okay=False, writable=True), nargs=-1, required=True) @click.argument('version_str', type=VersionParamType()) def set_version(display_format, file_type, shtool, name, filename, version_str): """Set version in given file. \f Args: display_format (str): Format to display output in filename (tuple[str]): File to operate on file_type (tuple[str]): File type to produce shtool (bool): Write shtool_ compatible files name (str): Project name used in output version_str (str): Initial version string .. _shtool: http://www.gnu.org/software/shtool/shtool.html """ if file_type and len(file_type) != len(filename): raise click.BadParameter('Number of --type options and filename args ' 'must match!') multi = len(filename) != 1 for ftype, fname in zip(file_type + (None, ) * len(filename), filename): if not ftype: ftype = guess_type(fname) version = CliVersion(version_str, name) version.write(fname, ftype, shtool=shtool) if multi: click.echo('{}: '.format(fname), nl=False) psuccess(version.display(display_format)) @cli.command() @click.option('-d', '--display', 'display_format', default='dotted', type=click.Choice(CliVersion.display_types()), help='Display format for output.') @click.argument('filename', type=click.Path(exists=True, dir_okay=False), nargs=-1, required=True) def display(display_format, filename): """Display version in given file. \f Args: display_format (str): Format to display output in filename (tuple[str]): File to operate on """ multi = len(filename) != 1 for fname in filename: version = CliVersion.read(fname) if multi: click.echo('{}: '.format(fname), nl=False) psuccess(version.display(display_format))