commit python-knack for openSUSE:Factory
Hello community, here is the log from the commit of package python-knack for openSUSE:Factory checked in at 2019-05-27 08:38:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-knack (Old) and /work/SRC/openSUSE:Factory/.python-knack.new.5148 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-knack" Mon May 27 08:38:23 2019 rev:8 rq:705262 version:0.6.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-knack/python-knack.changes 2019-05-22 11:16:43.162502081 +0200 +++ /work/SRC/openSUSE:Factory/.python-knack.new.5148/python-knack.changes 2019-05-27 08:38:24.555070416 +0200 @@ -1,0 +2,12 @@ +Fri May 24 12:36:58 UTC 2019 - pgajdos@suse.com + +- version update to 0.6.2 + * Adds ability to declare that command groups, commands, and arguments + are in a preview status and therefore might change or be removed. + This is done by passing the kwarg `is_preview=True`. + * Adds a generic `StatusTag` class to `knack.util` that allows you + to create your own colorized tags like `[Preview]` and `[Deprecated]`. + * When an incorrect command name is entered, Knack will now attempt + to suggest the closest alternative. + +------------------------------------------------------------------- Old: ---- v0.6.1.tar.gz New: ---- v0.6.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-knack.spec ++++++ --- /var/tmp/diff_new_pack.6iAYgT/_old 2019-05-27 08:38:25.147070185 +0200 +++ /var/tmp/diff_new_pack.6iAYgT/_new 2019-05-27 08:38:25.151070184 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-knack -Version: 0.6.1 +Version: 0.6.2 Release: 0 Summary: A Command-Line Interface framework License: MIT ++++++ v0.6.1.tar.gz -> v0.6.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/HISTORY.rst new/knack-0.6.2/HISTORY.rst --- old/knack-0.6.1/HISTORY.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/knack-0.6.2/HISTORY.rst 2019-05-23 00:19:57.000000000 +0200 @@ -0,0 +1,106 @@ +.. :changelog: + +Release History +=============== + +0.6.2 ++++++ +* Adds ability to declare that command groups, commands, and arguments are in a preview status and therefore might change or be removed. This is done by passing the kwarg `is_preview=True`. +* Adds a generic `StatusTag` class to `knack.util` that allows you to create your own colorized tags like `[Preview]` and `[Deprecated]`. +* When an incorrect command name is entered, Knack will now attempt to suggest the closest alternative. + +0.6.1 ++++++ +* Always read from local for configured_default + +0.6.0 ++++++ +* Support local context chained config file + +0.5.4 ++++++ +* Allows the loading of text files using @filename syntax. +* Adds the argument kwarg configured_default to support setting argument defaults via the config file's [defaults] section or an environment variable. + +0.5.3 ++++++ +* Removes an incorrect check when adding arguments. + +0.5.2 ++++++ +* Updates usages of yaml.load to use yaml.safe_load. + +0.5.1 ++++++ +* Fix issue with some scenarios (no args and --version) + +0.5.0 ++++++ +* Adds support for positional arguments with the .positional helper method on ArgumentsContext. +* Removes the necessity for the type field in help.py. This information can be inferred from the class, so specifying it causes unnecessary crashes. +* Adds support for examining the result of a command after a call to invoke. The raw object, error (if any) an exit code are accessible. +* Adds support for accessing the command instance from inside custom commands by putting the special argument cmd in the signature. +* Fixes an issue with the default config directory. It use to be .cli and is now based on the CLI name. +* Fixes regression in knack 0.4.5 in behavior when cli_name --verbose/debug is used. Displays the welcome message as intended. +* Adds ability to specify line width for help text display. + +0.4.5 ++++++ +* Preserves logging verbosity and output format on the namespace for use by validators. + +0.4.4 ++++++ +* Adds ability to set config file name. +* Fixes bug with argument deprecations. + +0.4.3 ++++++ +* Fixes issue where values were sometimes ignored when using deprecated options regardless of which option was given. + +0.4.2 ++++++ +* Bug fix: disable number parse on table mode PR #88 + +0.4.1 ++++++ +* Fixes bug with deprecation mechanism. +* Fixes an issue where the command group table would only be filled by calls to create CommandGroup classes. This resulted in some gaps in the command group table. + +0.4.0 ++++++ +* Add mechanism to deprecate commands, command groups, arguments and argument options. +* Improve help display support for Unicode. + +0.3.3 ++++++ +* expose a callback to let client side perform extra logics (#80) +* output: don't skip false value on auto-tabulating (#83) + +0.3.2 ++++++ +* ArgumentsContext.ignore() should use hidden options_list (#76) +* Consolidate exception handling (#66) + +0.3.1 ++++++ +* Performance optimization - Delay import of platform and colorama (#47) +* CLIError: Inherit from Exception directly (#65) +* Explicitly state which packages to include (so exclude 'tests') (#68) + +0.2.0 ++++++ +* Support command level and argument level validators. +* knack.commands.CLICommandsLoader now accepts a command_cls argument so you can provide your own CLICommand class. +* logging: make determine_verbose_level private method. +* Allow overriding of NAMED_ARGUMENTS +* Only pass valid argparse kwargs to argparse.ArgumentParser.add_argument and ignore the rest +* logging: make determine_verbose_level private method +* Remove cli_command, register_cli_argument, register_extra_cli_argument as ways to register commands and arguments. + +0.1.1 ++++++ +* Add more types of command and argument loaders. + +0.1.0 ++++++ +* Initial release diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/MANIFEST.in new/knack-0.6.2/MANIFEST.in --- old/knack-0.6.1/MANIFEST.in 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/MANIFEST.in 2019-05-23 00:19:57.000000000 +0200 @@ -1 +1,3 @@ include *.rst +include LICENSE +recursive-include tests * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/docs/cli.md new/knack-0.6.2/docs/cli.md --- old/knack-0.6.1/docs/cli.md 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/docs/cli.md 2019-05-23 00:19:57.000000000 +0200 @@ -9,7 +9,7 @@ For example: `cli_name` - Name of CLI. Typically the executable name. -`config_dir` - Path to config dir. e.g. `os.path.join('~', '.myconfig')` +`config_dir` - Path to config dir. e.g. `os.path.expanduser(os.path.join('~', '.myconfig'))` `config_env_var_prefix` - A prefix for environment variables used in config e.g. `CLI_`. Use the `invoke()` method to invoke commands. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/examples/exapp new/knack-0.6.2/examples/exapp --- old/knack-0.6.1/examples/exapp 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/examples/exapp 2019-05-23 00:19:57.000000000 +0200 @@ -79,7 +79,7 @@ mycli = CLI(cli_name=cli_name, - config_dir=os.path.join('~', '.{}'.format(cli_name)), + config_dir=os.path.expanduser(os.path.join('~', '.{}'.format(cli_name))), config_env_var_prefix=cli_name, commands_loader_cls=MyCommandsLoader, help_cls=MyCLIHelp) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/examples/exapp2 new/knack-0.6.2/examples/exapp2 --- old/knack-0.6.1/examples/exapp2 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/examples/exapp2 2019-05-23 00:19:57.000000000 +0200 @@ -86,7 +86,7 @@ mycli = MyCLI(cli_name=cli_name, - config_dir=os.path.join('~', '.{}'.format(cli_name)), + config_dir=os.path.expanduser(os.path.join('~', '.{}'.format(cli_name))), config_env_var_prefix=cli_name, commands_loader_cls=MyCommandsLoader, help_cls=MyCLIHelp) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/examples/test_exapp new/knack-0.6.2/examples/test_exapp --- old/knack-0.6.1/examples/test_exapp 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/examples/test_exapp 2019-05-23 00:19:57.000000000 +0200 @@ -47,7 +47,7 @@ name = 'exapp4' mycli = CLI(cli_name=name, - config_dir=os.path.join('~', '.{}'.format(name)), + config_dir=os.path.expanduser(os.path.join('~', '.{}'.format(name))), config_env_var_prefix=name, commands_loader_cls=MyCommandsLoader) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/arguments.py new/knack-0.6.2/knack/arguments.py --- old/knack-0.6.1/knack/arguments.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/arguments.py 2019-05-23 00:19:57.000000000 +0200 @@ -7,6 +7,7 @@ from collections import defaultdict from .deprecation import Deprecated +from .preview import PreviewItem from .log import get_logger from .util import CLIError @@ -42,7 +43,7 @@ class CLICommandArgument(object): - NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group', 'deprecate_info'] + NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group', 'deprecate_info', 'preview_info'] def __init__(self, dest=None, argtype=None, **kwargs): """An argument that has a specific destination parameter. @@ -221,6 +222,55 @@ action = _handle_option_deprecation(deprecated_opts) return action + def _handle_previews(self, argument_dest, **kwargs): + + if not kwargs.get('is_preview', False): + return kwargs + + def _handle_argument_preview(preview_info): + + parent_class = self._get_parent_class(**kwargs) + + class PreviewArgumentAction(parent_class): + + def __call__(self, parser, namespace, values, option_string=None): + if not hasattr(namespace, '_argument_previews'): + setattr(namespace, '_argument_previews', [preview_info]) + else: + namespace._argument_previews.append(preview_info) # pylint: disable=protected-access + try: + super(PreviewArgumentAction, self).__call__(parser, namespace, values, option_string) + except NotImplementedError: + setattr(namespace, self.dest, values) + + return PreviewArgumentAction + + def _get_preview_arg_message(self): + return "{} '{}' is in preview. It may be changed/removed in a future release.".format( + self.object_type.capitalize(), self.target) + + options_list = kwargs.get('options_list', None) + object_type = 'argument' + + if options_list is None: + # convert argument dest + target = '--{}'.format(argument_dest.replace('_', '-')) + elif options_list: + target = sorted(options_list, key=len)[-1] + else: + # positional argument + target = kwargs.get('metavar', '<{}>'.format(argument_dest.upper())) + object_type = 'positional argument' + + preview_info = PreviewItem( + target=target, + object_type=object_type, + message_func=_get_preview_arg_message + ) + kwargs['preview_info'] = preview_info + kwargs['action'] = _handle_argument_preview(preview_info) + return kwargs + # pylint: disable=inconsistent-return-statements def deprecate(self, **kwargs): @@ -252,7 +302,8 @@ :param arg_type: Predefined CLIArgumentType definition to register, as modified by any provided kwargs. :type arg_type: knack.arguments.CLIArgumentType :param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`, - `type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md. + `type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`. + See /docs/arguments.md. """ self._check_stale() if not self._applicable(): @@ -261,6 +312,8 @@ deprecate_action = self._handle_deprecations(argument_dest, **kwargs) if deprecate_action: kwargs['action'] = deprecate_action + + kwargs = self._handle_previews(argument_dest, **kwargs) self.command_loader.argument_registry.register_cli_argument(self.command_scope, argument_dest, arg_type, @@ -274,7 +327,8 @@ :param arg_type: Predefined CLIArgumentType definition to register, as modified by any provided kwargs. :type arg_type: knack.arguments.CLIArgumentType :param kwargs: Possible values: `validator`, `completer`, `nargs`, `action`, `const`, `default`, - `type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md. + `type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`. + See /docs/arguments.md. """ self._check_stale() if not self._applicable(): @@ -293,11 +347,14 @@ raise CLIError("command authoring error: commands may have, at most, one positional argument. '{}' already " "has positional argument: {}.".format(self.command_scope, ' '.join(positional_args.keys()))) + kwargs['options_list'] = [] + deprecate_action = self._handle_deprecations(argument_dest, **kwargs) if deprecate_action: kwargs['action'] = deprecate_action - kwargs['options_list'] = [] + kwargs = self._handle_previews(argument_dest, **kwargs) + self.command_loader.argument_registry.register_cli_argument(self.command_scope, argument_dest, arg_type, @@ -323,7 +380,8 @@ :param argument_dest: The destination argument to add this argument type to :type argument_dest: str :param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`, - `type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md. + `type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`. + See /docs/arguments.md. """ self._check_stale() if not self._applicable(): @@ -337,6 +395,9 @@ deprecate_action = self._handle_deprecations(argument_dest, **kwargs) if deprecate_action: kwargs['action'] = deprecate_action + + kwargs = self._handle_previews(argument_dest, **kwargs) + self.command_loader.extra_argument_registry[self.command_scope][argument_dest] = CLICommandArgument( argument_dest, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/commands.py new/knack-0.6.2/knack/commands.py --- old/knack-0.6.1/knack/commands.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/commands.py 2019-05-23 00:19:57.000000000 +0200 @@ -10,6 +10,7 @@ import six from .deprecation import Deprecated +from .preview import PreviewItem from .prompting import prompt_y_n, NoTTYException from .util import CLIError, CtxTypeError from .arguments import ArgumentRegistry, CLICommandArgument @@ -27,7 +28,8 @@ # pylint: disable=unused-argument def __init__(self, cli_ctx, name, handler, description=None, table_transformer=None, arguments_loader=None, description_loader=None, - formatter_class=None, deprecate_info=None, validator=None, confirmation=None, **kwargs): + formatter_class=None, deprecate_info=None, validator=None, confirmation=None, preview_info=None, + **kwargs): """ The command object that goes into the command table. :param cli_ctx: CLI Context @@ -48,6 +50,8 @@ :type formatter_class: class :param deprecate_info: Deprecation message to display when this command is invoked :type deprecate_info: str + :param preview_info: Indicates a command is in preview + :type preview_info: bool :param validator: The command validator :param confirmation: User confirmation required for command :type confirmation: bool, str, callable @@ -66,6 +70,7 @@ self.table_transformer = table_transformer self.formatter_class = formatter_class self.deprecate_info = deprecate_info + self.preview_info = preview_info self.confirmation = confirmation self.validator = validator @@ -295,6 +300,11 @@ Deprecated.ensure_new_style_deprecation(self.command_loader.cli_ctx, self.group_kwargs, 'command group') if kwargs['deprecate_info']: kwargs['deprecate_info'].target = group_name + if kwargs.get('is_preview', False): + kwargs['preview_info'] = PreviewItem( + target=group_name, + object_type='command group' + ) command_loader._populate_command_group_table_with_subgroups(group_name) # pylint: disable=protected-access self.command_loader.command_group_table[group_name] = self @@ -313,7 +323,8 @@ :type handler_name: str :param kwargs: Kwargs to apply to the command. Possible values: `client_factory`, `arguments_loader`, `description_loader`, `description`, - `formatter_class`, `table_transformer`, `deprecate_info`, `validator`, `confirmation`. + `formatter_class`, `table_transformer`, `deprecate_info`, `validator`, `confirmation`, + `is_preview`. """ import copy @@ -322,6 +333,11 @@ command_kwargs.update(kwargs) # don't inherit deprecation info from command group command_kwargs['deprecate_info'] = kwargs.get('deprecate_info', None) + if kwargs.get('is_preview', False): + command_kwargs['preview_info'] = PreviewItem( + self.command_loader.cli_ctx, + object_type='command' + ) self.command_loader._populate_command_group_table_with_subgroups(' '.join(command_name.split()[:-1])) # pylint: disable=protected-access self.command_loader.command_table[command_name] = self.command_loader.create_command( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/config.py new/knack-0.6.2/knack/config.py --- old/knack-0.6.1/knack/config.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/config.py 2019-05-23 00:19:57.000000000 +0200 @@ -21,7 +21,7 @@ '0': False, 'no': False, 'false': False, 'off': False} _DEFAULT_CONFIG_ENV_VAR_PREFIX = 'CLI' - _DEFAULT_CONFIG_DIR = os.path.join('~', '.{}'.format('cli')) + _DEFAULT_CONFIG_DIR = os.path.expanduser(os.path.join('~', '.{}'.format('cli'))) _DEFAULT_CONFIG_FILE_NAME = 'config' _CONFIG_DEFAULTS_SECTION = 'defaults' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/deprecation.py new/knack-0.6.2/knack/deprecation.py --- old/knack-0.6.1/knack/deprecation.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/deprecation.py 2019-05-23 00:19:57.000000000 +0200 @@ -5,6 +5,7 @@ from six import string_types as STRING_TYPES +from .util import StatusTag DEFAULT_DEPRECATED_TAG = '[Deprecated]' @@ -29,25 +30,8 @@ return deprecate_info -class ColorizedString(object): - - def __init__(self, message, color): - import colorama - self._message = message - self._color = getattr(colorama.Fore, color.upper(), None) - - def __len__(self): - return len(self._message) - - def __str__(self): - import colorama - if not self._color: - return self._message - return self._color + self._message + colorama.Fore.RESET - - # pylint: disable=too-many-instance-attributes -class Deprecated(object): +class Deprecated(StatusTag): @staticmethod def ensure_new_style_deprecation(cli_ctx, kwargs, object_type): @@ -62,7 +46,7 @@ return deprecate_info def __init__(self, cli_ctx=None, object_type='', target=None, redirect=None, hide=False, expiration=None, - tag_func=None, message_func=None): + tag_func=None, message_func=None, **kwargs): """ Create a collection of deprecation metadata. :param cli_ctx: The CLI context associated with the deprecated item. @@ -87,13 +71,6 @@ Omit to use the default. :type message_func: callable """ - self.cli_ctx = cli_ctx - self.object_type = object_type - self.target = target - self.redirect = redirect - self.hide = hide - self.expiration = expiration - def _default_get_message(self): msg = "This {} has been deprecated and will be removed ".format(self.object_type) if self.expiration: @@ -104,24 +81,18 @@ msg += " Use '{}' instead.".format(self.redirect) return msg - self._get_tag = tag_func or (lambda _: DEFAULT_DEPRECATED_TAG) - self._get_message = message_func or _default_get_message - - def __deepcopy__(self, memo): - import copy + self.redirect = redirect + self.hide = hide + self.expiration = expiration - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - try: - setattr(result, k, copy.deepcopy(v, memo)) - except TypeError: - if k == 'cli_ctx': - setattr(result, k, self.cli_ctx) - else: - raise - return result + super(Deprecated, self).__init__( + cli_ctx=cli_ctx, + object_type=object_type, + target=target, + color='yellow', + tag_func=tag_func or (lambda _: DEFAULT_DEPRECATED_TAG), + message_func=message_func or _default_get_message + ) # pylint: disable=no-self-use def _version_less_than_or_equal_to(self, v1, v2): @@ -148,16 +119,6 @@ def show_in_help(self): return not self.hidden() and not self.expired() - @property - def tag(self): - """ Returns a tag object. """ - return ColorizedString(self._get_tag(self), 'yellow') - - @property - def message(self): - """ Returns a tuple with the formatted message string and the message length. """ - return ColorizedString(self._get_message(self), 'yellow') - class ImplicitDeprecated(Deprecated): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/help.py new/knack-0.6.2/knack/help.py --- old/knack-0.6.1/knack/help.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/help.py 2019-05-23 00:19:57.000000000 +0200 @@ -10,6 +10,7 @@ from .deprecation import ImplicitDeprecated, resolve_deprecate_info from .log import get_logger +from .preview import ImplicitPreviewItem, resolve_preview_info from .util import CtxTypeError from .help_files import _load_help_file @@ -21,13 +22,6 @@ REQUIRED_TAG = '[Required]' -def _get_preview_tag(): - import colorama - PREVIEW_TAG = '{}[Preview]{}'.format(colorama.Fore.CYAN, colorama.Fore.RESET) - PREVIEW_TAG_LEN = len(PREVIEW_TAG) - 2 * len(colorama.Fore.RESET) - return (PREVIEW_TAG, PREVIEW_TAG_LEN) - - def _get_hanging_indent(max_length, indent): return max_length + (indent * 4) + len(FIRST_LINE_PREFIX) - 1 @@ -159,6 +153,26 @@ del deprecate_kwargs['_get_message'] self.deprecate_info = ImplicitDeprecated(**deprecate_kwargs) + # resolve preview info + direct_preview_info = resolve_preview_info(help_ctx.cli_ctx, delimiters) + if direct_preview_info: + self.preview_info = direct_preview_info + + # search for implicit preview + path_comps = delimiters.split()[:-1] + implicit_preview_info = None + while path_comps and not implicit_preview_info: + implicit_preview_info = resolve_preview_info(help_ctx.cli_ctx, ' '.join(path_comps)) + del path_comps[-1] + + if implicit_preview_info: + preview_kwargs = implicit_preview_info.__dict__.copy() + if delimiters in help_ctx.cli_ctx.invocation.commands_loader.command_table: + preview_kwargs['object_type'] = 'command' + else: + preview_kwargs['object_type'] = 'command group' + self.preview_info = ImplicitPreviewItem(**preview_kwargs) + def load(self, options): description = getattr(options, 'description', None) try: @@ -208,7 +222,6 @@ super(GroupHelpFile, self).__init__(help_ctx, delimiters) self.type = 'group' - self.preview_info = getattr(parser, 'preview_info', None) self.children = [] if getattr(parser, 'choices', None): @@ -244,6 +257,7 @@ param_kwargs = { 'name_source': [action.metavar or action.dest], 'deprecate_info': getattr(action, 'deprecate_info', None), + 'preview_info': getattr(action, 'preview_info', None), 'description': action.help, 'choices': action.choices, 'required': False, @@ -280,7 +294,8 @@ self.parameters.append(HelpParameter(**param_kwargs)) param_kwargs.update({ 'name_source': normal_options, - 'deprecate_info': getattr(param, 'deprecate_info', None) + 'deprecate_info': getattr(param, 'deprecate_info', None), + 'preview_info': getattr(param, 'preview_info', None) }) self.parameters.append(HelpParameter(**param_kwargs)) @@ -304,7 +319,7 @@ class HelpParameter(HelpObject): # pylint: disable=too-many-instance-attributes def __init__(self, name_source, description, required, choices=None, - default=None, group_name=None, deprecate_info=None): + default=None, group_name=None, deprecate_info=None, preview_info=None): super(HelpParameter, self).__init__() self.name_source = name_source self.name = ' '.join(sorted(name_source)) @@ -317,6 +332,7 @@ self.default = default self.group_name = group_name self.deprecate_info = deprecate_info + self.preview_info = preview_info def update_from_data(self, data): if self.name != data.get('name'): @@ -367,6 +383,8 @@ lines.append(item.long_summary) if item.deprecate_info: lines.append(str(item.deprecate_info.message)) + if item.preview_info: + lines.append(str(item.preview_info.message)) return ' '.join(lines) indent += 1 @@ -381,15 +399,18 @@ self.max_line_len = 0 def _build_tags_string(item): - PREVIEW_TAG, PREVIEW_TAG_LEN = _get_preview_tag() + + preview_info = getattr(item, 'preview_info', None) + preview = preview_info.tag if preview_info else '' + deprecate_info = getattr(item, 'deprecate_info', None) deprecated = deprecate_info.tag if deprecate_info else '' - preview = PREVIEW_TAG if getattr(item, 'preview_info', None) else '' + required = REQUIRED_TAG if getattr(item, 'required', None) else '' - tags = ' '.join([x for x in [str(deprecated), preview, required] if x]) + tags = ' '.join([x for x in [str(deprecated), str(preview), required] if x]) tags_len = sum([ len(deprecated), - PREVIEW_TAG_LEN if preview else 0, + len(preview), len(required), tags.count(' ') ]) @@ -488,15 +509,18 @@ return None def _build_tags_string(item): - PREVIEW_TAG, PREVIEW_TAG_LEN = _get_preview_tag() + + preview_info = getattr(item, 'preview_info', None) + preview = preview_info.tag if preview_info else '' + deprecate_info = getattr(item, 'deprecate_info', None) deprecated = deprecate_info.tag if deprecate_info else '' - preview = PREVIEW_TAG if getattr(item, 'preview_info', None) else '' + required = REQUIRED_TAG if getattr(item, 'required', None) else '' - tags = ' '.join([x for x in [str(deprecated), preview, required] if x]) + tags = ' '.join([x for x in [str(deprecated), str(preview), required] if x]) tags_len = sum([ len(deprecated), - PREVIEW_TAG_LEN if preview else 0, + len(preview), len(required), tags.count(' ') ]) @@ -573,6 +597,9 @@ deprecate_info = getattr(item, 'deprecate_info', None) if deprecate_info: lines.append(str(item.deprecate_info.message)) + preview_info = getattr(item, 'preview_info', None) + if preview_info: + lines.append(str(item.preview_info.message)) return ' '.join(lines) group_registry = ArgumentGroupRegistry([p.group_name for p in help_file.parameters if p.group_name]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/invocation.py new/knack-0.6.2/knack/invocation.py --- old/knack-0.6.1/knack/invocation.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/invocation.py 2019-05-23 00:19:57.000000000 +0200 @@ -10,6 +10,7 @@ from collections import defaultdict from .deprecation import ImplicitDeprecated, resolve_deprecate_info +from .preview import ImplicitPreviewItem, resolve_preview_info from .util import CLIError, CtxTypeError, CommandResultItem, todict from .parser import CLICommandParser from .commands import CLICommandsLoader @@ -117,6 +118,7 @@ err = sys.exc_info()[1] getattr(parsed_ns, '_parser', self.parser).validation_error(str(err)) + # pylint: disable=too-many-statements def execute(self, args): """ Executes the command invocation @@ -164,6 +166,10 @@ if cmd.deprecate_info: deprecations.append(cmd.deprecate_info) + previews = getattr(parsed_args, '_argument_previews', []) + if cmd.preview_info: + previews.append(cmd.preview_info) + params = self._filter_params(parsed_args) # search for implicit deprecation @@ -180,9 +186,23 @@ del deprecate_kwargs['_get_message'] deprecations.append(ImplicitDeprecated(**deprecate_kwargs)) + # search for implicit preview + path_comps = cmd.name.split()[:-1] + implicit_preview_info = None + while path_comps and not implicit_preview_info: + implicit_preview_info = resolve_preview_info(self.cli_ctx, ' '.join(path_comps)) + del path_comps[-1] + + if implicit_preview_info: + preview_kwargs = implicit_preview_info.__dict__.copy() + preview_kwargs['object_type'] = 'command' + previews.append(ImplicitPreviewItem(**preview_kwargs)) + colorama.init() for d in deprecations: print(d.message, file=sys.stderr) + for p in previews: + print(p.message, file=sys.stderr) colorama.deinit() cmd_result = parsed_args.func(params) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/parser.py new/knack-0.6.2/knack/parser.py --- old/knack-0.6.1/knack/parser.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/parser.py 2019-05-23 00:19:57.000000000 +0200 @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from __future__ import print_function + import argparse from .deprecation import Deprecated @@ -170,6 +172,7 @@ param = CLICommandParser._add_argument(command_parser, arg) param.completer = arg.completer param.deprecate_info = arg.deprecate_info + param.preview_info = arg.preview_info command_parser.set_defaults( func=metadata, command=command_name, @@ -254,3 +257,27 @@ """ self._expand_prefixed_files(args) return super(CLICommandParser, self).parse_args(args) + + def _check_value(self, action, value): + # Override to customize the error message when a argument is not among the available choices + # converted value must be one of the choices (if specified) + import difflib + import sys + + if action.choices is not None and value not in action.choices: + # parser has no `command_source`, value is part of command itself + error_msg = "{prog}: '{value}' is not in the '{prog}' command group. See '{prog} --help'.".format( + prog=self.prog, value=value) + logger.error(error_msg) + candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) + if candidates: + print_args = { + 's': 's' if len(candidates) > 1 else '', + 'verb': 'are' if len(candidates) > 1 else 'is', + 'value': value + } + suggestion_msg = "\nThe most similar choice{s} to '{value}' {verb}:\n".format(**print_args) + suggestion_msg += '\n'.join(['\t' + candidate for candidate in candidates]) + print(suggestion_msg, file=sys.stderr) + + self.exit(2) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/preview.py new/knack-0.6.2/knack/preview.py --- old/knack-0.6.1/knack/preview.py 1970-01-01 01:00:00.000000000 +0100 +++ new/knack-0.6.2/knack/preview.py 2019-05-23 00:19:57.000000000 +0200 @@ -0,0 +1,77 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from .util import StatusTag + +_PREVIEW_TAG = '[Preview]' +_preview_kwarg = 'preview_info' + + +def resolve_preview_info(cli_ctx, name): + + def _get_command(name): + return cli_ctx.invocation.commands_loader.command_table[name] + + def _get_command_group(name): + return cli_ctx.invocation.commands_loader.command_group_table.get(name, None) + + preview_info = None + try: + command = _get_command(name) + preview_info = getattr(command, _preview_kwarg, None) + except KeyError: + command_group = _get_command_group(name) + group_kwargs = getattr(command_group, 'group_kwargs', None) + if group_kwargs: + preview_info = group_kwargs.get(_preview_kwarg, None) + return preview_info + + +# pylint: disable=too-many-instance-attributes +class PreviewItem(StatusTag): + + def __init__(self, cli_ctx=None, object_type='', target=None, tag_func=None, message_func=None, **kwargs): + """ Create a collection of preview metadata. + + :param cli_ctx: The CLI context associated with the preview item. + :type cli_ctx: knack.cli.CLI + :param object_type: A label describing the type of object in preview. + :type: object_type: str + :param target: The name of the object in preview. + :type target: str + :param tag_func: Callable which returns the desired unformatted tag string for the preview item. + Omit to use the default. + :type tag_func: callable + :param message_func: Callable which returns the desired unformatted message string for the preview item. + Omit to use the default. + :type message_func: callable + """ + + def _default_get_message(self): + return "This {} is in preview. It may be changed/removed in a future release.".format(self.object_type) + + super(PreviewItem, self).__init__( + cli_ctx=cli_ctx, + object_type=object_type, + target=target, + color='cyan', + tag_func=tag_func or (lambda _: _PREVIEW_TAG), + message_func=message_func or _default_get_message + ) + + +class ImplicitPreviewItem(PreviewItem): + + def __init__(self, **kwargs): + + def get_implicit_preview_message(self): + return "Command group '{}' is in preview. It may be changed/removed " \ + "in a future release.".format(self.target) + + kwargs.update({ + 'tag_func': lambda _: '', + 'message_func': get_implicit_preview_message + }) + super(ImplicitPreviewItem, self).__init__(**kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/knack/util.py new/knack-0.6.2/knack/util.py --- old/knack-0.6.1/knack/util.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/knack/util.py 2019-05-23 00:19:57.000000000 +0200 @@ -35,6 +35,68 @@ obj.__class__.__name__)) +class ColorizedString(object): + + def __init__(self, message, color): + import colorama + self._message = message + self._color = getattr(colorama.Fore, color.upper(), None) + + def __len__(self): + return len(self._message) + + def __str__(self): + import colorama + if not self._color: + return self._message + return self._color + self._message + colorama.Fore.RESET + + +class StatusTag(object): + + # pylint: disable=unused-argument + def __init__(self, cli_ctx, object_type, target, tag_func, message_func, color, **kwargs): + self.cli_ctx = cli_ctx + self.object_type = object_type + self.target = target + self._color = color + self._get_tag = tag_func + self._get_message = message_func + + def __deepcopy__(self, memo): + import copy + + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + try: + setattr(result, k, copy.deepcopy(v, memo)) + except TypeError: + if k == 'cli_ctx': + setattr(result, k, self.cli_ctx) + else: + raise + return result + + # pylint: disable=no-self-use + def hidden(self): + return False + + def show_in_help(self): + return not self.hidden() + + @property + def tag(self): + """ Returns a tag object. """ + return ColorizedString(self._get_tag(self), self._color) + + @property + def message(self): + """ Returns a tuple with the formatted message string and the message length. """ + return ColorizedString(self._get_message(self), self._color) + + def ensure_dir(d): """ Create a directory if it doesn't exist """ if not os.path.isdir(d): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/setup.py new/knack-0.6.2/setup.py --- old/knack-0.6.1/setup.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/setup.py 2019-05-23 00:19:57.000000000 +0200 @@ -9,7 +9,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = '0.6.1' +VERSION = '0.6.2' DEPENDENCIES = [ 'argcomplete', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/tests/test_cli_scenarios.py new/knack-0.6.2/tests/test_cli_scenarios.py --- old/knack-0.6.1/tests/test_cli_scenarios.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/tests/test_cli_scenarios.py 2019-05-23 00:19:57.000000000 +0200 @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import os +from collections import OrderedDict import unittest try: import mock @@ -11,7 +12,6 @@ from unittest import mock import mock -from collections import OrderedDict from six import StringIO from knack import CLI @@ -87,7 +87,7 @@ self.command_table['abc list'] = CLICommand(self.cli_ctx, 'abc list', a_test_command_handler) return OrderedDict(self.command_table) - mycli = CLI(cli_name='exapp1', config_dir=os.path.join('~', '.exapp1'), commands_loader_cls=MyCommandsLoader) + mycli = CLI(cli_name='exapp1', config_dir=os.path.expanduser(os.path.join('~', '.exapp1')), commands_loader_cls=MyCommandsLoader) expected_output = """[ { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/tests/test_command_with_configured_defaults.py new/knack-0.6.2/tests/test_command_with_configured_defaults.py --- old/knack-0.6.1/tests/test_command_with_configured_defaults.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/tests/test_command_with_configured_defaults.py 2019-05-23 00:19:57.000000000 +0200 @@ -5,17 +5,15 @@ from __future__ import print_function import os import logging +import sys import unittest try: import mock except ImportError: from unittest import mock -from six import StringIO -import sys from knack.arguments import ArgumentsContext -from knack.commands import CLICommandsLoader, CLICommand, CommandGroup -from knack.config import CLIConfig +from knack.commands import CLICommandsLoader, CommandGroup from tests.util import DummyCLI, redirect_io diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/tests/test_deprecation.py new/knack-0.6.2/tests/test_deprecation.py --- old/knack-0.6.1/tests/test_deprecation.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/tests/test_deprecation.py 2019-05-23 00:19:57.000000000 +0200 @@ -28,6 +28,7 @@ pass +# pylint: disable=line-too-long class TestCommandDeprecation(unittest.TestCase): def setUp(self): @@ -132,7 +133,8 @@ with self.assertRaises(SystemExit): self.cli_ctx.invoke('cmd5 -h'.split()) actual = self.io.getvalue() - self.assertTrue(u'invalid choice' in actual and u'cmd5' in actual) + expected = """The most similar choices to 'cmd5'""" + self.assertIn(expected, actual) class TestCommandGroupDeprecation(unittest.TestCase): @@ -231,7 +233,8 @@ with self.assertRaises(SystemExit): self.cli_ctx.invoke('group5 -h'.split()) actual = self.io.getvalue() - self.assertTrue(u'invalid choice' in actual and u'group5' in actual) + expected = """The most similar choices to 'group5'""" + self.assertIn(expected, actual) @redirect_io def test_deprecate_command_implicitly(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/tests/test_introspection.py new/knack-0.6.2/tests/test_introspection.py --- old/knack-0.6.1/tests/test_introspection.py 2019-04-26 02:11:43.000000000 +0200 +++ new/knack-0.6.2/tests/test_introspection.py 2019-05-23 00:19:57.000000000 +0200 @@ -3,12 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os -import stat import unittest -import tempfile -import mock -from six.moves import configparser from knack.introspection import extract_full_summary_from_signature, option_descriptions, extract_args_from_signature diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.6.1/tests/test_preview.py new/knack-0.6.2/tests/test_preview.py --- old/knack-0.6.1/tests/test_preview.py 1970-01-01 01:00:00.000000000 +0100 +++ new/knack-0.6.2/tests/test_preview.py 2019-05-23 00:19:57.000000000 +0200 @@ -0,0 +1,196 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from __future__ import unicode_literals + +import unittest +try: + import mock +except ImportError: + from unittest import mock +from threading import Lock + +from knack.arguments import ArgumentsContext +from knack.commands import CLICommand, CLICommandsLoader, CommandGroup + +from tests.util import DummyCLI, redirect_io + + +def example_handler(arg1, arg2=None, arg3=None): + """ Short summary here. Long summary here. Still long summary. """ + pass + + +def example_arg_handler(arg1, opt1, arg2=None, opt2=None, arg3=None, + opt3=None, arg4=None, opt4=None, arg5=None, opt5=None): + pass + + +class TestCommandPreview(unittest.TestCase): + + def setUp(self): + + from knack.help_files import helps + + class PreviewTestCommandLoader(CLICommandsLoader): + def load_command_table(self, args): + super(PreviewTestCommandLoader, self).load_command_table(args) + with CommandGroup(self, '', '{}#{{}}'.format(__name__)) as g: + g.command('cmd1', 'example_handler', is_preview=True) + + with CommandGroup(self, 'grp1', '{}#{{}}'.format(__name__), is_preview=True) as g: + g.command('cmd1', 'example_handler') + + return self.command_table + + def load_arguments(self, command): + with ArgumentsContext(self, '') as c: + c.argument('arg1', options_list=['--arg', '-a'], required=False, type=int, choices=[1, 2, 3]) + c.argument('arg2', options_list=['-b'], required=True, choices=['a', 'b', 'c']) + + super(PreviewTestCommandLoader, self).load_arguments(command) + + helps['grp1'] = """ + type: group + short-summary: A group. +""" + self.cli_ctx = DummyCLI(commands_loader_cls=PreviewTestCommandLoader) + + @redirect_io + def test_preview_command_group_help(self): + """ Ensure preview commands appear correctly in group help view. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('-h'.split()) + actual = self.io.getvalue() + expected = u""" +Group + {} + +Subgroups: + grp1 [Preview] : A group. + +Commands: + cmd1 [Preview] : Short summary here. + +""".format(self.cli_ctx.name) + self.assertEqual(expected, actual) + + @redirect_io + def test_preview_command_plain_execute(self): + """ Ensure general warning displayed when running preview command. """ + self.cli_ctx.invoke('cmd1 -b b'.split()) + actual = self.io.getvalue() + expected = "This command is in preview. It may be changed/removed in a future release." + self.assertIn(expected, actual) + + +class TestCommandGroupPreview(unittest.TestCase): + + def setUp(self): + + from knack.help_files import helps + + class PreviewTestCommandLoader(CLICommandsLoader): + def load_command_table(self, args): + super(PreviewTestCommandLoader, self).load_command_table(args) + + with CommandGroup(self, 'group1', '{}#{{}}'.format(__name__), is_preview=True) as g: + g.command('cmd1', 'example_handler') + + return self.command_table + + def load_arguments(self, command): + with ArgumentsContext(self, '') as c: + c.argument('arg1', options_list=['--arg', '-a'], required=False, type=int, choices=[1, 2, 3]) + c.argument('arg2', options_list=['-b'], required=True, choices=['a', 'b', 'c']) + + super(PreviewTestCommandLoader, self).load_arguments(command) + + helps['group1'] = """ + type: group + short-summary: A group. +""" + self.cli_ctx = DummyCLI(commands_loader_cls=PreviewTestCommandLoader) + + @redirect_io + def test_preview_command_group_help_plain(self): + """ Ensure help warnings appear for preview command group help. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('group1 -h'.split()) + actual = self.io.getvalue() + expected = """ +Group + cli group1 : A group. + This command group is in preview. It may be changed/removed in a future release. +Commands: + cmd1 : Short summary here. + +""".format(self.cli_ctx.name) + self.assertEqual(expected, actual) + + @redirect_io + def test_preview_command_implicitly(self): + """ Ensure help warning displayed for command in preview because of a preview parent group. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('group1 cmd1 -h'.split()) + actual = self.io.getvalue() + expected = """ +Command + {} group1 cmd1 : Short summary here. + Long summary here. Still long summary. Command group 'group1' is in preview. It may be + changed/removed in a future release. +""".format(self.cli_ctx.name) + self.assertIn(expected, actual) + + +class TestArgumentPreview(unittest.TestCase): + + def setUp(self): + + from knack.help_files import helps + + class PreviewTestCommandLoader(CLICommandsLoader): + def load_command_table(self, args): + super(PreviewTestCommandLoader, self).load_command_table(args) + with CommandGroup(self, '', '{}#{{}}'.format(__name__)) as g: + g.command('arg-test', 'example_arg_handler') + return self.command_table + + def load_arguments(self, command): + with ArgumentsContext(self, 'arg-test') as c: + c.argument('arg1', help='Arg1', is_preview=True) + + super(PreviewTestCommandLoader, self).load_arguments(command) + + helps['grp1'] = """ + type: group + short-summary: A group. +""" + self.cli_ctx = DummyCLI(commands_loader_cls=PreviewTestCommandLoader) + + @redirect_io + def test_preview_arguments_command_help(self): + """ Ensure preview arguments appear correctly in command help view. """ + with self.assertRaises(SystemExit): + self.cli_ctx.invoke('arg-test -h'.split()) + actual = self.io.getvalue() + expected = """ +Arguments + --arg1 [Preview] [Required] : Arg1. + Argument '--arg1' is in preview. It may be changed/removed in a future release. +""".format(self.cli_ctx.name) + self.assertIn(expected, actual) + + @redirect_io + def test_preview_arguments_execute(self): + """ Ensure deprecated arguments can be used. """ + self.cli_ctx.invoke('arg-test --arg1 foo --opt1 bar'.split()) + actual = self.io.getvalue() + expected = "Argument '--arg1' is in preview. It may be changed/removed in a future release." + self.assertIn(expected, actual) + + +if __name__ == '__main__': + unittest.main()
participants (1)
-
root