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',


Reply via email to