Hello community, here is the log from the commit of package python-knack for openSUSE:Factory checked in at 2018-10-04 19:03:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-knack (Old) and /work/SRC/openSUSE:Factory/.python-knack.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-knack" Thu Oct 4 19:03:18 2018 rev:3 rq:639951 version:0.4.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-knack/python-knack.changes 2018-05-13 16:04:35.243377470 +0200 +++ /work/SRC/openSUSE:Factory/.python-knack.new/python-knack.changes 2018-10-04 19:03:21.231085978 +0200 @@ -1,0 +2,29 @@ +Thu Oct 4 10:28:24 UTC 2018 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- New upstream release + + Version 0.4.3 + + Fixes issue where values were sometimes ignored when using + deprecated options regardless of which option was given. +- from version 0.4.2 + + Bug fixes: [output]: disable number parse on table mode PR #88 +- from version 0.4.1 + + Version 0.4.0 introduced deprecation to Knack. This + release fixes a bug related to that. + * Ensures that the action kwarg is only set if the item is + deprecated. Previously it would set it to "None" which + would then override a pre-existing action like store_true. + + Version 0.4.0 also added the concept of the command group table + to the CommandsLoader class. This release corrects an issue + related to that: + * The command group table would only be filled by calls to create + CommandGroup classes. This resulted in some gaps in the command + group table. +- from version 0.4.0 + + Add mechanism to deprecate commands, command groups, + arguments and argument options. + + Improve help display support for Unicode. +- from version 0.3.3 + + expose a callback to let client side perform extra logics (#80) + + output: don't skip false value on auto-tabulating (#83) + +------------------------------------------------------------------- Old: ---- knack-0.3.2.tar.gz New: ---- knack-0.4.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-knack.spec ++++++ --- /var/tmp/diff_new_pack.e9K49q/_old 2018-10-04 19:03:21.611085578 +0200 +++ /var/tmp/diff_new_pack.e9K49q/_new 2018-10-04 19:03:21.611085578 +0200 @@ -12,13 +12,13 @@ # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. -# Please submit bugfixes or comments via http://bugs.opensuse.org/ +# Please submit bugfixes or comments via https://bugs.opensuse.org/ # %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-knack -Version: 0.3.2 +Version: 0.4.3 Release: 0 Summary: A Command-Line Interface framework License: MIT ++++++ knack-0.3.2.tar.gz -> knack-0.4.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/PKG-INFO new/knack-0.4.3/PKG-INFO --- old/knack-0.3.2/PKG-INFO 2018-03-16 17:00:24.000000000 +0100 +++ new/knack-0.4.3/PKG-INFO 2018-09-06 20:10:13.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 1.1 Name: knack -Version: 0.3.2 +Version: 0.4.3 Summary: A Command-Line Interface framework Home-page: https://github.com/microsoft/knack Author: Microsoft Corporation @@ -149,4 +149,3 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: License :: OSI Approved :: MIT License -Provides-Extra: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/__init__.py new/knack-0.4.3/knack/__init__.py --- old/knack-0.3.2/knack/__init__.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/__init__.py 2018-09-06 20:09:13.000000000 +0200 @@ -3,9 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from .cli import CLI -from .commands import CLICommandsLoader, CLICommand -from .arguments import ArgumentsContext -from .help import CLIHelp +import sys + +from knack.cli import CLI +from knack.commands import CLICommandsLoader, CLICommand +from knack.arguments import ArgumentsContext +from knack.help import CLIHelp __all__ = ['CLI', 'CLICommandsLoader', 'CLICommand', 'CLIHelp', 'ArgumentsContext'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/arguments.py new/knack-0.4.3/knack/arguments.py --- old/knack-0.3.2/knack/arguments.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/arguments.py 2018-09-06 20:09:13.000000000 +0200 @@ -6,6 +6,7 @@ import argparse from collections import defaultdict +from .deprecation import Deprecated from .log import get_logger logger = get_logger(__name__) @@ -39,7 +40,7 @@ class CLICommandArgument(object): # pylint: disable=too-few-public-methods - NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group'] + NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group', 'deprecate_info'] def __init__(self, dest=None, argtype=None, **kwargs): """An argument that has a specific destination parameter. @@ -100,8 +101,7 @@ :type argtype: knack.arguments.CLIArgumentType :param kwargs: see knack.arguments.CLIArgumentType """ - argument = CLIArgumentType(overrides=argtype, - **kwargs) + argument = CLIArgumentType(overrides=argtype, **kwargs) self.arguments[scope][dest] = argument def get_cli_argument(self, command, name): @@ -143,6 +143,88 @@ def __exit__(self, exc_type, exc_val, exc_tb): pass + def _get_parent_class(self, **kwargs): + # wrap any existing action + action = kwargs.get('action', None) + parent_class = argparse.Action + if isinstance(action, argparse.Action): + parent_class = action + elif isinstance(action, str): + parent_class = self.command_loader.cli_ctx.invocation.parser._registries['action'][action] # pylint: disable=protected-access + return parent_class + + def _handle_deprecations(self, argument_dest, **kwargs): + + def _handle_argument_deprecation(deprecate_info): + + parent_class = self._get_parent_class(**kwargs) + + class DeprecatedArgumentAction(parent_class): + + def __call__(self, parser, namespace, values, option_string=None): + if not hasattr(namespace, '_argument_deprecations'): + setattr(namespace, '_argument_deprecations', [deprecate_info]) + else: + namespace._argument_deprecations.append(deprecate_info) # pylint: disable=protected-access + try: + super(DeprecatedArgumentAction, self).__call__(parser, namespace, values, option_string) + except NotImplementedError: + pass + + return DeprecatedArgumentAction + + def _handle_option_deprecation(deprecated_options): + + if not isinstance(deprecated_options, list): + deprecated_options = [deprecated_options] + + parent_class = self._get_parent_class(**kwargs) + + class DeprecatedOptionAction(parent_class): + + def __call__(self, parser, namespace, values, option_string=None): + deprecated_opt = next((x for x in deprecated_options if option_string == x.target), None) + if deprecated_opt: + if not hasattr(namespace, '_argument_deprecations'): + setattr(namespace, '_argument_deprecations', [deprecated_opt]) + else: + namespace._argument_deprecations.append(deprecated_opt) # pylint: disable=protected-access + try: + super(DeprecatedOptionAction, self).__call__(parser, namespace, values, option_string) + except NotImplementedError: + setattr(namespace, self.dest, values) + + return DeprecatedOptionAction + + action = kwargs.get('action', None) + + deprecate_info = kwargs.get('deprecate_info', None) + if deprecate_info: + deprecate_info.target = deprecate_info.target or argument_dest + action = _handle_argument_deprecation(deprecate_info) + deprecated_opts = [x for x in kwargs.get('options_list', []) if isinstance(x, Deprecated)] + if deprecated_opts: + action = _handle_option_deprecation(deprecated_opts) + return action + + def deprecate(self, **kwargs): + + def _get_deprecated_arg_message(self): + msg = "{} '{}' has been deprecated and will be removed ".format( + self.object_type, self.target).capitalize() + if self.expiration: + msg += "in version '{}'.".format(self.expiration) + else: + msg += 'in a future release.' + if self.redirect: + msg += " Use '{}' instead.".format(self.redirect) + return msg + + target = kwargs.get('target', '') + kwargs['object_type'] = 'option' if target.startswith('-') else 'argument' + kwargs['message_func'] = _get_deprecated_arg_message + return Deprecated(self.command_loader.cli_ctx, **kwargs) + def argument(self, argument_dest, arg_type=None, **kwargs): """ Register an argument for the given command scope using a knack.arguments.CLIArgumentType @@ -153,19 +235,22 @@ :param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`, `type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md. """ + deprecate_action = self._handle_deprecations(argument_dest, **kwargs) + if deprecate_action: + kwargs['action'] = deprecate_action self.command_loader.argument_registry.register_cli_argument(self.command_scope, argument_dest, arg_type, **kwargs) - def ignore(self, argument_dest): + def ignore(self, argument_dest, **kwargs): """ Register an argument with type knack.arguments.ignore_type (hidden/ignored) :param argument_dest: The destination argument to apply the ignore type to :type argument_dest: str """ dest_option = ['--__{}'.format(argument_dest.upper())] - self.argument(argument_dest, arg_type=ignore_type, options_list=dest_option) + self.argument(argument_dest, arg_type=ignore_type, options_list=dest_option, **kwargs) def extra(self, argument_dest, **kwargs): """Register extra parameters for the given command. Typically used to augment auto-command built @@ -176,6 +261,9 @@ :param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`, `type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md. """ + deprecate_action = self._handle_deprecations(argument_dest, **kwargs) + if deprecate_action: + kwargs['action'] = deprecate_action 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.3.2/knack/commands.py new/knack-0.4.3/knack/commands.py --- old/knack-0.3.2/knack/commands.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/commands.py 2018-09-06 20:09:13.000000000 +0200 @@ -4,12 +4,12 @@ # -------------------------------------------------------------------------------------------- import types -import copy from collections import OrderedDict, defaultdict from importlib import import_module import six +from .deprecation import Deprecated from .prompting import prompt_y_n, NoTTYException from .util import CLIError, CtxTypeError from .arguments import ArgumentRegistry, CLICommandArgument @@ -93,12 +93,8 @@ return self(**kwargs) def __call__(self, *args, **kwargs): + cmd_args = args[0] - if self.deprecate_info is not None: - text = 'This command is deprecating and will be removed in future releases.' - if self.deprecate_info: - text += " Use '{}' instead.".format(self.deprecate_info) - logger.warning(text) confirm = self.confirmation and not cmd_args.pop('yes', None) \ and not self.cli_ctx.config.getboolean('core', 'disable_confirm_prompt', fallback=False) @@ -142,10 +138,23 @@ self.excluded_command_handler_args = excluded_command_handler_args # A command table is a dictionary of name -> CLICommand instances self.command_table = dict() + # A command group table is a dictionary of names -> CommandGroup instances + self.command_group_table = dict() # An argument registry stores all arguments for commands self.argument_registry = ArgumentRegistry() self.extra_argument_registry = defaultdict(lambda: {}) + def _populate_command_group_table_with_subgroups(self, name): + if not name: + return + + # ensure all subgroups have some entry in the command group table + name_components = name.split() + for i, _ in enumerate(name_components): + subgroup_name = ' '.join(name_components[:i + 1]) + if subgroup_name not in self.command_group_table: + self.command_group_table[subgroup_name] = {} + def load_command_table(self, args): # pylint: disable=unused-argument """ Load commands into the command table @@ -221,8 +230,13 @@ except (ValueError, AttributeError): raise ValueError("The operation '{}' is invalid.".format(operation)) + def deprecate(self, **kwargs): + kwargs['object_type'] = 'command group' + return Deprecated(self.cli_ctx, **kwargs) + class CommandGroup(object): + def __init__(self, command_loader, group_name, operations_tmpl, **kwargs): """ Context manager for registering commands that share common properties. @@ -240,6 +254,11 @@ self.group_name = group_name self.operations_tmpl = operations_tmpl self.group_kwargs = kwargs + 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 + command_loader._populate_command_group_table_with_subgroups(group_name) # pylint: disable=protected-access + self.command_loader.command_group_table[group_name] = self def __enter__(self): return self @@ -258,10 +277,20 @@ Possible values: `client_factory`, `arguments_loader`, `description_loader`, `description`, `formatter_class`, `table_transformer`, `deprecate_info`, `validator`, `confirmation`. """ + import copy + command_name = '{} {}'.format(self.group_name, name) if self.group_name else name command_kwargs = copy.deepcopy(self.group_kwargs) command_kwargs.update(kwargs) + # don't inherit deprecation info from command group + command_kwargs['deprecate_info'] = kwargs.get('deprecate_info', None) + + 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( command_name, self.operations_tmpl.format(handler_name), **command_kwargs) + + def deprecate(self, **kwargs): + kwargs['object_type'] = 'command' + return Deprecated(self.command_loader.cli_ctx, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/config.py new/knack-0.4.3/knack/config.py --- old/knack-0.3.2/knack/config.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/config.py 2018-09-06 20:09:13.000000000 +0200 @@ -33,6 +33,7 @@ :type config_env_var_prefix: str """ config_dir = config_dir or CLIConfig._DEFAULT_CONFIG_DIR + ensure_dir(config_dir) config_env_var_prefix = config_env_var_prefix or CLIConfig._DEFAULT_CONFIG_ENV_VAR_PREFIX self.config_parser = get_config_parser() env_var_prefix = '{}_'.format(config_env_var_prefix.upper()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/deprecation.py new/knack-0.4.3/knack/deprecation.py --- old/knack-0.3.2/knack/deprecation.py 1970-01-01 01:00:00.000000000 +0100 +++ new/knack-0.4.3/knack/deprecation.py 2018-09-06 20:09:13.000000000 +0200 @@ -0,0 +1,181 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from six import string_types as STRING_TYPES + + +DEFAULT_DEPRECATED_TAG = '[Deprecated]' + + +def resolve_deprecate_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) + + deprecate_info = None + try: + command = _get_command(name) + deprecate_info = getattr(command, 'deprecate_info', None) + except KeyError: + command_group = _get_command_group(name) + group_kwargs = getattr(command_group, 'group_kwargs', None) + if group_kwargs: + deprecate_info = group_kwargs.get('deprecate_info', None) + 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): + + @staticmethod + def ensure_new_style_deprecation(cli_ctx, kwargs, object_type): + """ Helper method to make the previous string-based deprecate_info kwarg + work with the new style. """ + deprecate_info = kwargs.get('deprecate_info', None) + if isinstance(deprecate_info, Deprecated): + deprecate_info.object_type = object_type + elif isinstance(deprecate_info, STRING_TYPES): + deprecate_info = Deprecated(cli_ctx, redirect=deprecate_info, object_type=object_type) + kwargs['deprecate_info'] = deprecate_info + 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): + """ Create a collection of deprecation metadata. + + :param cli_ctx: The CLI context associated with the deprecated item. + :type cli_ctx: knack.cli.CLI + :param object_type: A label describing the type of object being deprecated. + :type: object_type: str + :param target: The name of the object being deprecated. + :type target: str + :param redirect: The alternative to redirect users to in lieu of the deprecated item. If omitted it, there is + no alternative. + :type redirect: str + :param hide: A boolean or CLI version at or above-which the deprecated item will no longer appear + in help text, but will continue to work. Warnings will be displayed if the deprecated + item is used. + :type hide: bool OR str + :param expiration: The CLI version at or above-which the deprecated item will no longer work. + :type expiration: str + :param tag_func: Callable which returns the desired unformatted tag string for the deprecated item. + Omit to use the default. + :type tag_func: callable + :param message_func: Callable which returns the desired unformatted message string for the deprecated item. + 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: + msg += "in version '{}'.".format(self.expiration) + else: + msg += 'in a future release.' + if self.redirect: + 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 + + 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 _version_less_than_or_equal_to(self, v1, v2): + """ Returns true if v1 <= v2. """ + # pylint: disable=no-name-in-module, import-error + from distutils.version import LooseVersion + return LooseVersion(v1) <= LooseVersion(v2) + + def expired(self): + if self.expiration: + cli_version = self.cli_ctx.get_cli_version() + return self._version_less_than_or_equal_to(self.expiration, cli_version) + return False + + def hidden(self): + hidden = False + if isinstance(self.hide, bool): + hidden = self.hide + elif isinstance(self.hide, STRING_TYPES): + cli_version = self.cli_ctx.get_cli_version() + hidden = self._version_less_than_or_equal_to(self.hide, cli_version) + return hidden + + 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): + + def __init__(self, **kwargs): + + def get_implicit_deprecation_message(self): + msg = "This {} is implicitly deprecated because command group '{}' is deprecated " \ + "and will be removed ".format(self.object_type, self.target) + if self.expiration: + msg += "in version '{}'.".format(self.expiration) + else: + msg += 'in a future release.' + if self.redirect: + msg += " Use '{}' instead.".format(self.redirect) + return msg + + kwargs.update({ + 'tag_func': lambda _: '', + 'message_func': get_implicit_deprecation_message + }) + super(ImplicitDeprecated, self).__init__(**kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/help.py new/knack-0.4.3/knack/help.py --- old/knack-0.3.2/knack/help.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/help.py 2018-09-06 20:09:13.000000000 +0200 @@ -3,24 +3,45 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from __future__ import print_function +from __future__ import print_function, unicode_literals import argparse import sys import textwrap +from .deprecation import ImplicitDeprecated, resolve_deprecate_info +from .log import get_logger from .util import CtxTypeError from .help_files import _load_help_file -FIRST_LINE_PREFIX = ': ' +logger = get_logger(__name__) -def _get_column_indent(text, max_name_length): - return ' ' * (max_name_length - len(text)) +FIRST_LINE_PREFIX = ' : ' +REQUIRED_TAG = '[Required]' + + +def _get_preview_tag(): + import colorama + PREVIEW_TAG = colorama.Fore.CYAN + '[Preview]' + 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) + return max_length + (indent * 4) + len(FIRST_LINE_PREFIX) - 1 + + +def _get_padding_len(max_len, layout): + if layout['tags']: + pad_len = max_len - layout['line_len'] + 1 + else: + pad_len = max_len - layout['line_len'] + return pad_len + + +def _get_line_len(name, tags_len): + return len(name) + tags_len + (2 if tags_len else 1) def _print_indent(s, indent=0, subsequent_spaces=-1): @@ -95,6 +116,7 @@ self._long_summary = self._normalize_text(value) +# pylint: disable=too-many-instance-attributes class HelpFile(HelpObject): @staticmethod @@ -105,8 +127,9 @@ except Exception: # pylint: disable=broad-except return text - def __init__(self, delimiters): + def __init__(self, help_ctx, delimiters): super(HelpFile, self).__init__() + self.help_ctx = help_ctx self.delimiters = delimiters self.name = delimiters.split()[-1] if delimiters else delimiters self.command = delimiters @@ -114,6 +137,27 @@ self.short_summary = '' self.long_summary = '' self.examples = [] + self.deprecate_info = None + self.preview_info = None + + direct_deprecate_info = resolve_deprecate_info(help_ctx.cli_ctx, delimiters) + if direct_deprecate_info: + self.deprecate_info = direct_deprecate_info + + # search for implicit deprecation + path_comps = delimiters.split()[:-1] + implicit_deprecate_info = None + while path_comps and not implicit_deprecate_info: + implicit_deprecate_info = resolve_deprecate_info(help_ctx.cli_ctx, ' '.join(path_comps)) + del path_comps[-1] + + if implicit_deprecate_info: + deprecate_kwargs = implicit_deprecate_info.__dict__.copy() + deprecate_kwargs['object_type'] = 'command' if delimiters in \ + help_ctx.cli_ctx.invocation.commands_loader.command_table else 'command group' + del deprecate_kwargs['_get_tag'] + del deprecate_kwargs['_get_message'] + self.deprecate_info = ImplicitDeprecated(**deprecate_kwargs) def load(self, options): description = getattr(options, 'description', None) @@ -160,39 +204,73 @@ class GroupHelpFile(HelpFile): - def __init__(self, delimiters, parser): - super(GroupHelpFile, self).__init__(delimiters) + def __init__(self, help_ctx, delimiters, parser): + + 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): for options in parser.choices.values(): delimiters = ' '.join(options.prog.split()[1:]) - child = (GroupHelpFile(delimiters, options) if options.is_group() - else HelpFile(delimiters)) + child = (help_ctx.group_help_cls(self.help_ctx, delimiters, options) if options.is_group() + else help_ctx.help_cls(self.help_ctx, delimiters)) child.load(options) + try: + # don't hide implicitly deprecated commands + if not isinstance(child.deprecate_info, ImplicitDeprecated) and \ + not child.deprecate_info.show_in_help(): + continue + except AttributeError: + pass self.children.append(child) class CommandHelpFile(HelpFile): - def __init__(self, delimiters, parser): - super(CommandHelpFile, self).__init__(delimiters) + def __init__(self, help_ctx, delimiters, parser): + + super(CommandHelpFile, self).__init__(help_ctx, delimiters) self.type = 'command' self.parameters = [] for action in [a for a in parser._actions if a.help != argparse.SUPPRESS]: # pylint: disable=protected-access - self.parameters.append(HelpParameter(' '.join(sorted(action.option_strings)), - action.help, - required=action.required, - choices=action.choices, - default=action.default, - group_name=action.container.description)) + self._add_parameter_help(action) help_param = next(p for p in self.parameters if p.name == '--help -h') help_param.group_name = 'Global Arguments' + def _add_parameter_help(self, param): + param_kwargs = { + 'description': param.help, + 'choices': param.choices, + 'required': param.required, + 'default': param.default, + 'group_name': param.container.description + } + normal_options = [] + deprecated_options = [] + for item in param.option_strings: + deprecated_info = getattr(item, 'deprecate_info', None) + if deprecated_info: + if deprecated_info.show_in_help(): + deprecated_options.append(item) + else: + normal_options.append(item) + if deprecated_options: + param_kwargs.update({ + 'name_source': deprecated_options, + 'deprecate_info': deprecated_options[0].deprecate_info + }) + self.parameters.append(HelpParameter(**param_kwargs)) + param_kwargs.update({ + 'name_source': normal_options, + 'deprecate_info': getattr(param, 'deprecate_info', None) + }) + self.parameters.append(HelpParameter(**param_kwargs)) + def _load_from_data(self, data): super(CommandHelpFile, self)._load_from_data(data) @@ -212,10 +290,11 @@ class HelpParameter(HelpObject): # pylint: disable=too-many-instance-attributes - def __init__(self, param_name, description, required, choices=None, - default=None, group_name=None): + def __init__(self, name_source, description, required, choices=None, + default=None, group_name=None, deprecate_info=None): super(HelpParameter, self).__init__() - self.name = param_name + self.name_source = name_source + self.name = ' '.join(sorted(name_source)) self.required = required self.type = 'string' self.short_summary = description @@ -224,10 +303,11 @@ self.choices = choices self.default = default self.group_name = group_name + self.deprecate_info = deprecate_info def update_from_data(self, data): if self.name != data.get('name'): - raise HelpAuthoringException("mismatched name {0} vs. {1}" + raise HelpAuthoringException(u"mismatched name {0} vs. {1}" .format(self.name, data.get('name'))) @@ -253,155 +333,254 @@ class CLIHelp(object): - @staticmethod - def _print_header(cli_name, help_file): + # pylint: disable=no-self-use + def _print_header(self, cli_name, help_file): indent = 0 _print_indent('') _print_indent('Command' if help_file.type == 'command' else 'Group', indent) indent += 1 - _print_indent('{0}{1}'.format(cli_name + ' ' + help_file.command, - FIRST_LINE_PREFIX + help_file.short_summary - if help_file.short_summary - else ''), - indent) + LINE_FORMAT = u'{cli}{name}{separator}{summary}' + line = LINE_FORMAT.format( + cli=cli_name, + name=' ' + help_file.command if help_file.command else '', + separator=FIRST_LINE_PREFIX if help_file.short_summary else '', + summary=help_file.short_summary if help_file.short_summary else '' + ) + _print_indent(line, indent) + + def _build_long_summary(item): + lines = [] + if item.long_summary: + lines.append(item.long_summary) + if item.deprecate_info: + lines.append(str(item.deprecate_info.message)) + return ' '.join(lines) indent += 1 - if help_file.long_summary: - _print_indent('{0}'.format(help_file.long_summary.rstrip()), indent) - _print_indent('') + long_sum = _build_long_summary(help_file) + _print_indent(long_sum, indent) - @staticmethod - def _print_groups(help_file): + def _print_groups(self, help_file): + + LINE_FORMAT = u'{name}{padding}{tags}{separator}{summary}' + indent = 1 + + self.max_line_len = 0 + + def _build_tags_string(item): + PREVIEW_TAG, PREVIEW_TAG_LEN = _get_preview_tag() + 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_len = sum([ + len(deprecated), + PREVIEW_TAG_LEN if preview else 0, + len(required), + tags.count(' ') + ]) + if not tags_len: + tags = '' + return tags, tags_len - def _print_items(items): + def _layout_items(items): + + layouts = [] for c in sorted(items, key=lambda h: h.name): - column_indent = _get_column_indent(c.name, max_name_length) - summary = FIRST_LINE_PREFIX + c.short_summary if c.short_summary else '' - summary = summary.replace('\n', ' ') - hanging_indent = max_name_length + indent * 4 + 2 - _print_indent( - '{0}{1}{2}'.format(c.name, column_indent, summary), indent, hanging_indent) + tags, tags_len = _build_tags_string(c) + line_len = _get_line_len(c.name, tags_len) + layout = { + 'name': c.name, + 'tags': tags, + 'separator': FIRST_LINE_PREFIX if c.short_summary else '', + 'summary': c.short_summary or '', + 'line_len': line_len + } + layout['summary'] = layout['summary'].replace('\n', ' ') + if line_len > self.max_line_len: + self.max_line_len = line_len + layouts.append(layout) + return layouts + + def _print_items(layouts): + for layout in layouts: + layout['padding'] = ' ' * _get_padding_len(self.max_line_len, layout) + _print_indent(LINE_FORMAT.format(**layout), indent, _get_hanging_indent(self.max_line_len, indent)) _print_indent('') - indent = 1 - max_name_length = max(len(c.name) for c in help_file.children) \ - if help_file.children \ - else 0 - subgroups = [c for c in help_file.children if isinstance(c, GroupHelpFile)] - subcommands = [c for c in help_file.children if c not in subgroups] + groups = [c for c in help_file.children if isinstance(c, self.group_help_cls)] + group_layouts = _layout_items(groups) + + commands = [c for c in help_file.children if c not in groups] + command_layouts = _layout_items(commands) - if subgroups: + if groups: _print_indent('Subgroups:') - _print_items(subgroups) + _print_items(group_layouts) - if subcommands: + if commands: _print_indent('Commands:') - _print_items(subcommands) + _print_items(command_layouts) @staticmethod def _get_choices_defaults_sources_str(p): - choice_str = ' Allowed values: {0}.'.format(', '.join(sorted([str(x) for x in p.choices]))) \ + choice_str = u' Allowed values: {0}.'.format(', '.join(sorted([str(x) for x in p.choices]))) \ if p.choices else '' - default_str = ' Default: {0}.'.format(p.default) \ + default_str = u' Default: {0}.'.format(p.default) \ if p.default and p.default != argparse.SUPPRESS else '' - value_sources_str = ' Values from: {0}.'.format(', '.join(p.value_sources)) \ + value_sources_str = u' Values from: {0}.'.format(', '.join(p.value_sources)) \ if p.value_sources else '' - return '{0}{1}{2}'.format(choice_str, default_str, value_sources_str) + return u'{0}{1}{2}'.format(choice_str, default_str, value_sources_str) @staticmethod def print_description_list(help_files): indent = 1 - max_name_length = max(len(f.name) for f in help_files) if help_files else 0 + max_length = max(len(f.name) for f in help_files) if help_files else 0 for help_file in sorted(help_files, key=lambda h: h.name): - _print_indent('{0}{1}{2}'.format(help_file.name, - _get_column_indent(help_file.name, max_name_length), - FIRST_LINE_PREFIX + help_file.short_summary - if help_file.short_summary - else ''), + column_indent = max_length - len(help_file.name) + _print_indent(u'{0}{1}{2}'.format(help_file.name, + ' ' * column_indent, + FIRST_LINE_PREFIX + help_file.short_summary + if help_file.short_summary + else ''), indent, - _get_hanging_indent(max_name_length, indent)) + _get_hanging_indent(max_length, indent)) @staticmethod def _print_examples(help_file): indent = 0 - print('') _print_indent('Examples', indent) for e in help_file.examples: indent = 1 - _print_indent('{0}'.format(e.name), indent) + _print_indent(u'{0}'.format(e.name), indent) indent = 2 - _print_indent('{0}'.format(e.text), indent) + _print_indent(u'{0}'.format(e.text), indent) print('') - @classmethod - def _print_arguments(cls, help_file): + def _print_arguments(self, help_file): + + LINE_FORMAT = u'{name}{padding}{tags}{separator}{short_summary}' indent = 1 + self.max_line_len = 0 + if not help_file.parameters: _print_indent('None', indent) _print_indent('') return None - if not help_file.parameters: - _print_indent('none', indent) - required_tag = ' [Required]' - max_name_length = max(len(p.name) + (len(required_tag) if p.required else 0) - for p in help_file.parameters) - last_group_name = None - - group_registry = ArgumentGroupRegistry( - [p.group_name for p in help_file.parameters if p.group_name]) - - def _get_parameter_key(parameter): - return '{}{}{}'.format(group_registry.get_group_priority(parameter.group_name), - str(not parameter.required), - parameter.name) + def _build_tags_string(item): + PREVIEW_TAG, PREVIEW_TAG_LEN = _get_preview_tag() + 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_len = sum([ + len(deprecated), + PREVIEW_TAG_LEN if preview else 0, + len(required), + tags.count(' ') + ]) + if not tags_len: + tags = '' + return tags, tags_len + + def _layout_items(items): + + layouts = [] + for c in sorted(items, key=_get_parameter_key): + + deprecate_info = getattr(c, 'deprecate_info', None) + if deprecate_info and not deprecate_info.show_in_help(): + continue + + tags, tags_len = _build_tags_string(c) + short_summary = _build_short_summary(c) + long_summary = _build_long_summary(c) + line_len = _get_line_len(c.name, tags_len) + layout = { + 'name': c.name, + 'tags': tags, + 'separator': FIRST_LINE_PREFIX if short_summary else '', + 'short_summary': short_summary, + 'long_summary': long_summary, + 'group_name': c.group_name, + 'line_len': line_len + } + if line_len > self.max_line_len: + self.max_line_len = line_len + layouts.append(layout) + return layouts + + def _print_items(layouts): + last_group_name = '' + + for layout in layouts: + indent = 1 + if layout['group_name'] != last_group_name: + if layout['group_name']: + print('') + print(layout['group_name']) + last_group_name = layout['group_name'] + + layout['padding'] = ' ' * _get_padding_len(self.max_line_len, layout) + _print_indent(LINE_FORMAT.format(**layout), indent, _get_hanging_indent(self.max_line_len, indent)) + + indent = 2 + long_summary = layout.get('long_summary', None) + if long_summary: + _print_indent(long_summary, indent) - for p in sorted(help_file.parameters, key=_get_parameter_key): - indent = 1 - required_text = required_tag if p.required else '' + _print_indent('') - short_summary = p.short_summary if p.short_summary else '' + def _build_short_summary(item): + short_summary = item.short_summary possible_values_index = short_summary.find(' Possible values include') short_summary = short_summary[0:possible_values_index if possible_values_index >= 0 else len(short_summary)] - short_summary += cls._get_choices_defaults_sources_str(p) + short_summary += self._get_choices_defaults_sources_str(item) short_summary = short_summary.strip() + return short_summary - if p.group_name != last_group_name: - if p.group_name: - print('') - print(p.group_name) - last_group_name = p.group_name - _print_indent( - '{0}{1}{2}{3}'.format( - p.name, - _get_column_indent(p.name + required_text, max_name_length), - required_text, - FIRST_LINE_PREFIX + short_summary if short_summary else '' - ), - indent, - _get_hanging_indent(max_name_length, indent) - ) + def _build_long_summary(item): + lines = [] + if item.long_summary: + lines.append(item.long_summary) + deprecate_info = getattr(item, 'deprecate_info', None) + if deprecate_info: + lines.append(str(item.deprecate_info.message)) + return ' '.join(lines) - indent = 2 - if p.long_summary: - _print_indent('{0}'.format(p.long_summary.rstrip()), indent) + group_registry = ArgumentGroupRegistry([p.group_name for p in help_file.parameters if p.group_name]) + + def _get_parameter_key(parameter): + return u'{}{}{}'.format(group_registry.get_group_priority(parameter.group_name), + str(not parameter.required), + parameter.name) + + parameter_layouts = _layout_items(help_file.parameters) + _print_items(parameter_layouts) return indent - @classmethod - def _print_detailed_help(cls, cli_name, help_file): - cls._print_header(cli_name, help_file) + def _print_detailed_help(self, cli_name, help_file): + self._print_header(cli_name, help_file) + if help_file.long_summary or getattr(help_file, 'deprecate_info', None): + _print_indent('') + if help_file.type == 'command': _print_indent('Arguments') - cls._print_arguments(help_file) + self._print_arguments(help_file) elif help_file.type == 'group': - cls._print_groups(help_file) + self._print_groups(help_file) if help_file.examples: - cls._print_examples(help_file) + self._print_examples(help_file) - def __init__(self, cli_ctx=None, privacy_statement='', welcome_message=''): + def __init__(self, cli_ctx=None, privacy_statement='', welcome_message='', + group_help_cls=GroupHelpFile, command_help_cls=CommandHelpFile, + help_cls=HelpFile): """ Manages the generation and production of help in the CLI :param cli_ctx: CLI Context @@ -410,6 +589,12 @@ :type privacy_statement: str :param welcome_message: A welcome message for the CLI :type welcome_message: str + :param group_help_cls: Class to use for formatting group help. + :type group_help_cls: HelpFile + :param command_help_cls: Class to use for formatting command help. + :type command_help_cls: HelpFile + :param command_help_cls: Class to use for formatting generic help. + :type command_help_cls: HelpFile """ from .cli import CLI if cli_ctx is not None and not isinstance(cli_ctx, CLI): @@ -417,6 +602,10 @@ self.cli_ctx = cli_ctx self.privacy_statement = privacy_statement self.welcome_message = welcome_message + self.max_line_len = 0 + self.group_help_cls = group_help_cls + self.command_help_cls = command_help_cls + self.help_cls = help_cls def show_privacy_statement(self): ran_before = self.cli_ctx.config.getboolean('core', 'first_run', fallback=False) @@ -431,16 +620,16 @@ def show_welcome(self, parser): self.show_privacy_statement() self.show_welcome_message() - help_file = GroupHelpFile('', parser) + help_file = self.group_help_cls(self, '', parser) self.print_description_list(help_file.children) - @classmethod - def show_help(cls, cli_name, nouns, parser, is_group): + def show_help(self, cli_name, nouns, parser, is_group): + import colorama + colorama.init(autoreset=True) delimiters = ' '.join(nouns) - help_file = CommandHelpFile(delimiters, parser) \ - if not is_group \ - else GroupHelpFile(delimiters, parser) + help_file = self.command_help_cls(self, delimiters, parser) if not is_group \ + else self.group_help_cls(self, delimiters, parser) help_file.load(parser) if not nouns: help_file.command = '' - cls._print_detailed_help(cli_name, help_file) + self._print_detailed_help(cli_name, help_file) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/introspection.py new/knack-0.4.3/knack/introspection.py --- old/knack-0.3.2/knack/introspection.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/introspection.py 2018-09-06 20:09:13.000000000 +0200 @@ -74,7 +74,7 @@ sig = inspect.signature(operation) args = sig.parameters except AttributeError: - sig = inspect.getargspec(operation) # pylint: disable=deprecated-method + sig = inspect.getargspec(operation) # pylint: disable=deprecated-method, useless-suppression args = sig.args arg_docstring_help = option_descriptions(operation) @@ -84,7 +84,7 @@ try: # this works in python3 default = args[arg_name].default - required = default == inspect.Parameter.empty # pylint: disable=no-member + required = default == inspect.Parameter.empty # pylint: disable=no-member, useless-suppression except TypeError: arg_defaults = (dict(zip(sig.args[-len(sig.defaults):], sig.defaults)) if sig.defaults @@ -96,7 +96,7 @@ try: default = (default - if default != inspect._empty # pylint: disable=protected-access, no-member + if default != inspect._empty # pylint: disable=protected-access else None) except AttributeError: pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/invocation.py new/knack-0.4.3/knack/invocation.py --- old/knack-0.3.2/knack/invocation.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/invocation.py 2018-09-06 20:09:13.000000000 +0200 @@ -3,10 +3,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from __future__ import print_function + import sys from collections import defaultdict +from .deprecation import ImplicitDeprecated, resolve_deprecate_info from .util import CLIError, CtxTypeError, CommandResultItem, todict from .parser import CLICommandParser from .commands import CLICommandsLoader @@ -111,12 +114,15 @@ :return: The command result :rtype: knack.util.CommandResultItem """ + import colorama + self.cli_ctx.raise_event(EVENT_INVOKER_PRE_CMD_TBL_CREATE, args=args) cmd_tbl = self.commands_loader.load_command_table(args) command = self._rudimentary_get_command(args) self.commands_loader.load_arguments(command) + self.cli_ctx.raise_event(EVENT_INVOKER_POST_CMD_TBL_CREATE, cmd_tbl=cmd_tbl) - self.parser.load_command_table(cmd_tbl) + self.parser.load_command_table(self.commands_loader) self.cli_ctx.raise_event(EVENT_INVOKER_CMD_TBL_LOADED, parser=self.parser) if not args: self.cli_ctx.completion.enable_autocomplete(self.parser) @@ -139,6 +145,30 @@ params = self._filter_params(parsed_args) + cmd = parsed_args.func + deprecations = getattr(parsed_args, '_argument_deprecations', []) + if cmd.deprecate_info: + deprecations.append(cmd.deprecate_info) + + # search for implicit deprecation + path_comps = cmd.name.split()[:-1] + implicit_deprecate_info = None + while path_comps and not implicit_deprecate_info: + implicit_deprecate_info = resolve_deprecate_info(self.cli_ctx, ' '.join(path_comps)) + del path_comps[-1] + + if implicit_deprecate_info: + deprecate_kwargs = implicit_deprecate_info.__dict__.copy() + deprecate_kwargs['object_type'] = 'command' + del deprecate_kwargs['_get_tag'] + del deprecate_kwargs['_get_message'] + deprecations.append(ImplicitDeprecated(**deprecate_kwargs)) + + colorama.init() + for d in deprecations: + print(d.message, file=sys.stderr) + colorama.deinit() + cmd_result = parsed_args.func(params) cmd_result = todict(cmd_result) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/output.py new/knack-0.4.3/knack/output.py --- old/knack-0.3.2/knack/output.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/output.py 2018-09-06 20:09:13.000000000 +0200 @@ -165,7 +165,7 @@ for k in keys: if k in _TableOutput.SKIP_KEYS: continue - if item[k] and not isinstance(item[k], (list, dict, set)): + if item[k] is not None and not isinstance(item[k], (list, dict, set)): new_entry[_TableOutput._capitalize_first_char(k)] = item[k] except AttributeError: # handles odd cases where a string/bool/etc. is returned @@ -187,7 +187,8 @@ def dump(self, data): from tabulate import tabulate table_data = self._auto_table(data) - table_str = tabulate(table_data, headers="keys", tablefmt="simple") if table_data else '' + table_str = tabulate(table_data, headers="keys", tablefmt="simple", + disable_numparse=True) if table_data else '' if table_str == '\n': raise ValueError('Unable to extract fields for table.') return table_str + '\n' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/parser.py new/knack-0.4.3/knack/parser.py --- old/knack-0.3.2/knack/parser.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/parser.py 2018-09-06 20:09:13.000000000 +0200 @@ -5,6 +5,7 @@ import argparse +from .deprecation import Deprecated from .events import EVENT_PARSER_GLOBAL_CREATE from .util import CtxTypeError @@ -38,9 +39,24 @@ @staticmethod def _add_argument(obj, arg): """ Only pass valid argparse kwargs to argparse.ArgumentParser.add_argument """ - options_list = arg.options_list argparse_options = {name: value for name, value in arg.options.items() if name in ARGPARSE_SUPPORTED_KWARGS} - return obj.add_argument(*options_list, **argparse_options) + scrubbed_options_list = [] + for item in arg.options_list: + if isinstance(item, Deprecated): + # don't add expired options to the parser + if item.expired(): + continue + + class _DeprecatedOption(str): + def __new__(cls, *args, **kwargs): + instance = str.__new__(cls, *args, **kwargs) + return instance + + option = _DeprecatedOption(item.target) + setattr(option, 'deprecate_info', item) + item = option + scrubbed_options_list.append(item) + return obj.add_argument(*scrubbed_options_list, **argparse_options) def __init__(self, cli_ctx=None, cli_help=None, **kwargs): """ Create the argument parser @@ -63,12 +79,14 @@ self._description = kwargs.pop('description', None) super(CLICommandParser, self).__init__(**kwargs) - def load_command_table(self, cmd_tbl): + def load_command_table(self, command_loader): """ Process the command table and load it into the parser :param cmd_tbl: A dictionary containing the commands :type cmd_tbl: dict """ + cmd_tbl = command_loader.command_table + grp_tbl = command_loader.command_group_table if not cmd_tbl: raise ValueError('The command table is empty. At least one command is required.') # If we haven't already added a subparser, we @@ -77,12 +95,16 @@ sp = self.add_subparsers(dest='_command') sp.required = True self.subparsers = {(): sp} + for command_name, metadata in cmd_tbl.items(): - subparser = self._get_subparser(command_name.split()) + subparser = self._get_subparser(command_name.split(), grp_tbl) command_verb = command_name.split()[-1] # To work around http://bugs.python.org/issue9253, we artificially add any new # parsers we add to the "choices" section of the subparser. - subparser.choices[command_verb] = command_verb + subparser = self._get_subparser(command_name.split(), grp_tbl) + deprecate_info = metadata.deprecate_info + if not subparser or (deprecate_info and deprecate_info.expired()): + continue # inject command_module designer's help formatter -- default is HelpFormatter fc = metadata.formatter_class or argparse.HelpFormatter @@ -93,11 +115,17 @@ help_file=metadata.help, formatter_class=fc, cli_help=self.cli_help) - + command_parser.cli_ctx = self.cli_ctx command_validator = metadata.validator argument_validators = [] argument_groups = {} for arg in metadata.arguments.values(): + + # don't add deprecated arguments to the parser + deprecate_info = arg.type.settings.get('deprecate_info', None) + if deprecate_info and deprecate_info.expired(): + continue + if arg.validator: argument_validators.append(arg.validator) if arg.arg_group: @@ -112,7 +140,7 @@ else: param = CLICommandParser._add_argument(command_parser, arg) param.completer = arg.completer - + param.deprecate_info = arg.deprecate_info command_parser.set_defaults( func=metadata, command=command_name, @@ -120,12 +148,14 @@ _argument_validators=argument_validators, _parser=command_parser) - def _get_subparser(self, path): + def _get_subparser(self, path, group_table=None): """For each part of the path, walk down the tree of subparsers, creating new ones if one doesn't already exist. """ + group_table = group_table or {} for length in range(0, len(path)): - parent_subparser = self.subparsers.get(tuple(path[0:length]), None) + parent_path = path[:length] + parent_subparser = self.subparsers.get(tuple(parent_path), None) if not parent_subparser: # No subparser exists for the given subpath - create and register # a new subparser. @@ -135,13 +165,25 @@ # with ensuring that a subparser for cmd exists, then for subcmd1, # subcmd2 and so on), we know we can always back up one step and # add a subparser if one doesn't exist - grandparent_subparser = self.subparsers[tuple(path[0:length - 1])] - new_parser = grandparent_subparser.add_parser(path[length - 1], cli_help=self.cli_help) + command_group = group_table.get(' '.join(parent_path)) + if command_group: + deprecate_info = command_group.group_kwargs.get('deprecate_info', None) + if deprecate_info and deprecate_info.expired(): + continue + grandparent_path = path[:length - 1] + grandparent_subparser = self.subparsers[tuple(grandparent_path)] + new_path = path[length - 1] + new_parser = grandparent_subparser.add_parser(new_path, cli_help=self.cli_help) # Due to http://bugs.python.org/issue9253, we have to give the subparser # a destination and set it to required in order to get a meaningful error parent_subparser = new_parser.add_subparsers(dest='_subcommand') + command_group = group_table.get(' '.join(parent_path), None) + deprecate_info = None + if command_group: + deprecate_info = command_group.group_kwargs.get('deprecate_info', None) parent_subparser.required = True + parent_subparser.deprecate_info = deprecate_info self.subparsers[tuple(path[0:length])] = parent_subparser return parent_subparser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/prompting.py new/knack-0.4.3/knack/prompting.py --- old/knack-0.3.2/knack/prompting.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/prompting.py 2018-09-06 20:09:13.000000000 +0200 @@ -77,7 +77,7 @@ return _prompt_bool(msg, 't', 'f', default=default, help_string=help_string) -def _prompt_bool(msg, true_str, false_str, default=None, help_string=None): # pylint: disable=inconsistent-return-statements +def _prompt_bool(msg, true_str, false_str, default=None, help_string=None): verify_is_a_tty() if default not in [None, true_str, false_str]: raise ValueError("Valid values for default are {}, {} or None".format(true_str, false_str)) @@ -96,7 +96,7 @@ return default == y.lower() -def prompt_choice_list(msg, a_list, default=1, help_string=None): # pylint: disable=inconsistent-return-statements +def prompt_choice_list(msg, a_list, default=1, help_string=None): """Prompt user to select from a list of possible choices. :param msg:A message displayed to the user before the choice list diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/testsdk/base.py new/knack-0.4.3/knack/testsdk/base.py --- old/knack-0.3.2/knack/testsdk/base.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/testsdk/base.py 2018-09-06 20:09:13.000000000 +0200 @@ -179,7 +179,7 @@ @classmethod def _custom_request_query_matcher(cls, r1, r2): """ Ensure method, path, and query parameters match. """ - from six.moves.urllib_parse import urlparse, parse_qs # pylint: disable=import-error + from six.moves.urllib_parse import urlparse, parse_qs # pylint: disable=relative-import, useless-suppression url1 = urlparse(r1.uri) url2 = urlparse(r2.uri) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack/util.py new/knack-0.4.3/knack/util.py --- old/knack-0.3.2/knack/util.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/knack/util.py 2018-09-06 20:09:13.000000000 +0200 @@ -54,12 +54,16 @@ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() -def todict(obj): # pylint: disable=too-many-return-statements - +def todict(obj, post_processor=None): # pylint: disable=too-many-return-statements + """ + Convert an object to a dictionary. Use 'post_processor(original_obj, dictionary)' to update the + dictionary in the process + """ if isinstance(obj, dict): - return {k: todict(v) for (k, v) in obj.items()} + result = {k: todict(v, post_processor) for (k, v) in obj.items()} + return post_processor(obj, result) if post_processor else result elif isinstance(obj, list): - return [todict(a) for a in obj] + return [todict(a, post_processor) for a in obj] elif isinstance(obj, Enum): return obj.value elif isinstance(obj, (date, time, datetime)): @@ -67,9 +71,10 @@ elif isinstance(obj, timedelta): return str(obj) elif hasattr(obj, '_asdict'): - return todict(obj._asdict()) + return todict(obj._asdict(), post_processor) elif hasattr(obj, '__dict__'): - return dict([(to_camel_case(k), todict(v)) - for k, v in obj.__dict__.items() - if not callable(v) and not k.startswith('_')]) + result = dict([(to_camel_case(k), todict(v, post_processor)) + for k, v in obj.__dict__.items() + if not callable(v) and not k.startswith('_')]) + return post_processor(obj, result) if post_processor else result return obj diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack.egg-info/PKG-INFO new/knack-0.4.3/knack.egg-info/PKG-INFO --- old/knack-0.3.2/knack.egg-info/PKG-INFO 2018-03-16 17:00:24.000000000 +0100 +++ new/knack-0.4.3/knack.egg-info/PKG-INFO 2018-09-06 20:10:13.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 1.1 Name: knack -Version: 0.3.2 +Version: 0.4.3 Summary: A Command-Line Interface framework Home-page: https://github.com/microsoft/knack Author: Microsoft Corporation @@ -149,4 +149,3 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: License :: OSI Approved :: MIT License -Provides-Extra: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/knack.egg-info/SOURCES.txt new/knack-0.4.3/knack.egg-info/SOURCES.txt --- old/knack-0.3.2/knack.egg-info/SOURCES.txt 2018-03-16 17:00:24.000000000 +0100 +++ new/knack-0.4.3/knack.egg-info/SOURCES.txt 2018-09-06 20:10:13.000000000 +0200 @@ -9,6 +9,7 @@ knack/commands.py knack/completion.py knack/config.py +knack/deprecation.py knack/events.py knack/help.py knack/help_files.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/knack-0.3.2/setup.py new/knack-0.4.3/setup.py --- old/knack-0.3.2/setup.py 2018-03-16 16:59:17.000000000 +0100 +++ new/knack-0.4.3/setup.py 2018-09-06 20:09:13.000000000 +0200 @@ -9,7 +9,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = '0.3.2' +VERSION = '0.4.3' DEPENDENCIES = [ 'argcomplete',