Skip to content

Commit

Permalink
Command/Argument Preview Mechanism (#152)
Browse files Browse the repository at this point in the history
* Initial work.

* Preview mechanisms.

* Add test.

* Review UX. Add HISTORY file.

* Create TagDecorator class to contain the core logic for [Preview] and [Deprecated]

* Minor change to trigger CI... :-/
  • Loading branch information
tjprescott authored May 22, 2019
1 parent 231ee81 commit 7993fa2
Show file tree
Hide file tree
Showing 11 changed files with 606 additions and 80 deletions.
105 changes: 105 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
.. :changelog:
Release History
===============

0.6.2
+++++
* Adds ability to declare that command groups, commands, and arguments are in a preview status and therefore might change or be removed. This is done by passing the kwarg `is_preview=True`.
* Adds a generic `TagDecorator` class to `knack.util` that allows you to create your own colorized tags like `[Preview]` and `[Deprecated]`.

0.6.1
+++++
* Always read from local for configured_default

0.6.0
+++++
* Support local context chained config file

0.5.4
+++++
* Allows the loading of text files using @filename syntax.
* Adds the argument kwarg configured_default to support setting argument defaults via the config file's [defaults] section or an environment variable.

0.5.3
+++++
* Removes an incorrect check when adding arguments.

0.5.2
+++++
* Updates usages of yaml.load to use yaml.safe_load.

0.5.1
+++++
* Fix issue with some scenarios (no args and --version)

0.5.0
+++++
* Adds support for positional arguments with the .positional helper method on ArgumentsContext.
* Removes the necessity for the type field in help.py. This information can be inferred from the class, so specifying it causes unnecessary crashes.
* Adds support for examining the result of a command after a call to invoke. The raw object, error (if any) an exit code are accessible.
* Adds support for accessing the command instance from inside custom commands by putting the special argument cmd in the signature.
* Fixes an issue with the default config directory. It use to be .cli and is now based on the CLI name.
* Fixes regression in knack 0.4.5 in behavior when cli_name --verbose/debug is used. Displays the welcome message as intended.
* Adds ability to specify line width for help text display.

0.4.5
+++++
* Preserves logging verbosity and output format on the namespace for use by validators.

0.4.4
+++++
* Adds ability to set config file name.
* Fixes bug with argument deprecations.

0.4.3
+++++
* Fixes issue where values were sometimes ignored when using deprecated options regardless of which option was given.

0.4.2
+++++
* Bug fix: disable number parse on table mode PR #88

0.4.1
+++++
* Fixes bug with deprecation mechanism.
* Fixes an issue where the command group table would only be filled by calls to create CommandGroup classes. This resulted in some gaps in the command group table.

0.4.0
+++++
* Add mechanism to deprecate commands, command groups, arguments and argument options.
* Improve help display support for Unicode.

0.3.3
+++++
* expose a callback to let client side perform extra logics (#80)
* output: don't skip false value on auto-tabulating (#83)

0.3.2
+++++
* ArgumentsContext.ignore() should use hidden options_list (#76)
* Consolidate exception handling (#66)

0.3.1
+++++
* Performance optimization - Delay import of platform and colorama (#47)
* CLIError: Inherit from Exception directly (#65)
* Explicitly state which packages to include (so exclude 'tests') (#68)

0.2.0
+++++
* Support command level and argument level validators.
* knack.commands.CLICommandsLoader now accepts a command_cls argument so you can provide your own CLICommand class.
* logging: make determine_verbose_level private method.
* Allow overriding of NAMED_ARGUMENTS
* Only pass valid argparse kwargs to argparse.ArgumentParser.add_argument and ignore the rest
* logging: make determine_verbose_level private method
* Remove cli_command, register_cli_argument, register_extra_cli_argument as ways to register commands and arguments.

0.1.1
+++++
* Add more types of command and argument loaders.

0.1.0
+++++
* Initial release
71 changes: 66 additions & 5 deletions knack/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections import defaultdict

from .deprecation import Deprecated
from .preview import PreviewItem
from .log import get_logger
from .util import CLIError

Expand Down Expand Up @@ -42,7 +43,7 @@ def update(self, other=None, **kwargs):

class CLICommandArgument(object):

NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group', 'deprecate_info']
NAMED_ARGUMENTS = ['options_list', 'validator', 'completer', 'arg_group', 'deprecate_info', 'preview_info']

def __init__(self, dest=None, argtype=None, **kwargs):
"""An argument that has a specific destination parameter.
Expand Down Expand Up @@ -221,6 +222,55 @@ def __call__(self, parser, namespace, values, option_string=None):
action = _handle_option_deprecation(deprecated_opts)
return action

def _handle_previews(self, argument_dest, **kwargs):

if not kwargs.get('is_preview', False):
return kwargs

def _handle_argument_preview(preview_info):

parent_class = self._get_parent_class(**kwargs)

class PreviewArgumentAction(parent_class):

def __call__(self, parser, namespace, values, option_string=None):
if not hasattr(namespace, '_argument_previews'):
setattr(namespace, '_argument_previews', [preview_info])
else:
namespace._argument_previews.append(preview_info) # pylint: disable=protected-access
try:
super(PreviewArgumentAction, self).__call__(parser, namespace, values, option_string)
except NotImplementedError:
setattr(namespace, self.dest, values)

return PreviewArgumentAction

def _get_preview_arg_message(self):
return "{} '{}' is in preview. It may be changed/removed in a future release.".format(
self.object_type.capitalize(), self.target)

options_list = kwargs.get('options_list', None)
object_type = 'argument'

if options_list is None:
# convert argument dest
target = '--{}'.format(argument_dest.replace('_', '-'))
elif options_list:
target = sorted(options_list, key=len)[0]
else:
# positional argument
target = kwargs.get('metavar', '<{}>'.format(argument_dest.upper()))
object_type = 'positional argument'

preview_info = PreviewItem(
target=target,
object_type=object_type,
message_func=_get_preview_arg_message
)
kwargs['preview_info'] = preview_info
kwargs['action'] = _handle_argument_preview(preview_info)
return kwargs

# pylint: disable=inconsistent-return-statements
def deprecate(self, **kwargs):

Expand Down Expand Up @@ -252,7 +302,8 @@ def argument(self, argument_dest, arg_type=None, **kwargs):
:param arg_type: Predefined CLIArgumentType definition to register, as modified by any provided kwargs.
:type arg_type: knack.arguments.CLIArgumentType
:param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`,
`type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -261,6 +312,8 @@ def argument(self, argument_dest, arg_type=None, **kwargs):
deprecate_action = self._handle_deprecations(argument_dest, **kwargs)
if deprecate_action:
kwargs['action'] = deprecate_action

kwargs = self._handle_previews(argument_dest, **kwargs)
self.command_loader.argument_registry.register_cli_argument(self.command_scope,
argument_dest,
arg_type,
Expand All @@ -274,7 +327,8 @@ def positional(self, argument_dest, arg_type=None, **kwargs):
:param arg_type: Predefined CLIArgumentType definition to register, as modified by any provided kwargs.
:type arg_type: knack.arguments.CLIArgumentType
:param kwargs: Possible values: `validator`, `completer`, `nargs`, `action`, `const`, `default`,
`type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -293,11 +347,14 @@ def positional(self, argument_dest, arg_type=None, **kwargs):
raise CLIError("command authoring error: commands may have, at most, one positional argument. '{}' already "
"has positional argument: {}.".format(self.command_scope, ' '.join(positional_args.keys())))

kwargs['options_list'] = []

deprecate_action = self._handle_deprecations(argument_dest, **kwargs)
if deprecate_action:
kwargs['action'] = deprecate_action

kwargs['options_list'] = []
kwargs = self._handle_previews(argument_dest, **kwargs)

self.command_loader.argument_registry.register_cli_argument(self.command_scope,
argument_dest,
arg_type,
Expand All @@ -323,7 +380,8 @@ def extra(self, argument_dest, **kwargs):
:param argument_dest: The destination argument to add this argument type to
:type argument_dest: str
:param kwargs: Possible values: `options_list`, `validator`, `completer`, `nargs`, `action`, `const`, `default`,
`type`, `choices`, `required`, `help`, `metavar`. See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -337,6 +395,9 @@ def extra(self, argument_dest, **kwargs):
deprecate_action = self._handle_deprecations(argument_dest, **kwargs)
if deprecate_action:
kwargs['action'] = deprecate_action

kwargs = self._handle_previews(argument_dest, **kwargs)

self.command_loader.extra_argument_registry[self.command_scope][argument_dest] = CLICommandArgument(
argument_dest, **kwargs)

Expand Down
20 changes: 18 additions & 2 deletions knack/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import six

from .deprecation import Deprecated
from .preview import PreviewItem
from .prompting import prompt_y_n, NoTTYException
from .util import CLIError, CtxTypeError
from .arguments import ArgumentRegistry, CLICommandArgument
Expand All @@ -27,7 +28,8 @@ class CLICommand(object): # pylint:disable=too-many-instance-attributes
# pylint: disable=unused-argument
def __init__(self, cli_ctx, name, handler, description=None, table_transformer=None,
arguments_loader=None, description_loader=None,
formatter_class=None, deprecate_info=None, validator=None, confirmation=None, **kwargs):
formatter_class=None, deprecate_info=None, validator=None, confirmation=None, preview_info=None,
**kwargs):
""" The command object that goes into the command table.
:param cli_ctx: CLI Context
Expand All @@ -48,6 +50,8 @@ def __init__(self, cli_ctx, name, handler, description=None, table_transformer=N
:type formatter_class: class
:param deprecate_info: Deprecation message to display when this command is invoked
:type deprecate_info: str
:param preview_info: Indicates a command is in preview
:type preview_info: bool
:param validator: The command validator
:param confirmation: User confirmation required for command
:type confirmation: bool, str, callable
Expand All @@ -66,6 +70,7 @@ def __init__(self, cli_ctx, name, handler, description=None, table_transformer=N
self.table_transformer = table_transformer
self.formatter_class = formatter_class
self.deprecate_info = deprecate_info
self.preview_info = preview_info
self.confirmation = confirmation
self.validator = validator

Expand Down Expand Up @@ -295,6 +300,11 @@ def __init__(self, command_loader, group_name, operations_tmpl, **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
if kwargs.get('is_preview', False):
kwargs['preview_info'] = PreviewItem(
target=group_name,
object_type='command group'
)
command_loader._populate_command_group_table_with_subgroups(group_name) # pylint: disable=protected-access
self.command_loader.command_group_table[group_name] = self

Expand All @@ -313,7 +323,8 @@ def command(self, name, handler_name, **kwargs):
:type handler_name: str
:param kwargs: Kwargs to apply to the command.
Possible values: `client_factory`, `arguments_loader`, `description_loader`, `description`,
`formatter_class`, `table_transformer`, `deprecate_info`, `validator`, `confirmation`.
`formatter_class`, `table_transformer`, `deprecate_info`, `validator`, `confirmation`,
`is_preview`.
"""
import copy

Expand All @@ -322,6 +333,11 @@ def command(self, name, handler_name, **kwargs):
command_kwargs.update(kwargs)
# don't inherit deprecation info from command group
command_kwargs['deprecate_info'] = kwargs.get('deprecate_info', None)
if kwargs.get('is_preview', False):
command_kwargs['preview_info'] = PreviewItem(
self.command_loader.cli_ctx,
object_type='command'
)

self.command_loader._populate_command_group_table_with_subgroups(' '.join(command_name.split()[:-1])) # pylint: disable=protected-access
self.command_loader.command_table[command_name] = self.command_loader.create_command(
Expand Down
Loading

0 comments on commit 7993fa2

Please sign in to comment.