Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions examples/exapp2
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,34 @@ helps['abc last'] = """
text: {cli_name} abc last --number 3
""".format(cli_name=cli_name)

helps['ga'] = """
type: group
short-summary: A general available command group
"""

helps['pre'] = """
type: group
short-summary: A preview command group
"""

helps['exp'] = """
type: group
short-summary: An experimental command group
"""

def a_test_command_handler():
return [{'a': 1, 'b': 1234}, {'a': 3, 'b': 4}]

def abc_show_command_handler():
"""
Show a JSON mapping of letters to their ASCII values
"""
import string
lower = {}
for ch in string.ascii_lowercase:
lower[ch] = ord(ch)
upper = {}
for ch in string.ascii_uppercase:
upper[ch] = ord(ch)
return {"lowercase": lower, "uppercase": upper}


def abc_list_command_handler():
Expand All @@ -64,7 +89,7 @@ def abc_last_command_handler(number=5):
return list(string.ascii_lowercase)[-number:]


def num_range_command_handler(start=0, end=5):
def range_command_handler(start=0, end=5):
"""
Get a list of natural numbers from start to end
:param start: the lower bound
Expand All @@ -76,7 +101,7 @@ def num_range_command_handler(start=0, end=5):

def sample_json_handler():
"""
Get a sample JSON dict
Get a sample JSON string
"""
# https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list#examples
result = {
Expand All @@ -99,8 +124,13 @@ def sample_json_handler():
}
return result

def hello_command_handler(myarg=None, abc=None):
return ['hello', 'world', myarg, abc]

def hello_command_handler(greetings=None):
"""
Say "Hello World!" and my warm greetings
:param greetings: My warm greetings
"""
return ['Hello World!', greetings]


WELCOME_MESSAGE = r"""
Expand All @@ -127,30 +157,28 @@ class MyCLIHelp(CLIHelp):
class MyCommandsLoader(CLICommandsLoader):

def load_command_table(self, args):
with CommandGroup(self, 'hello', '__main__#{}') as g:
g.command('world', 'hello_command_handler', confirmation=True)
with CommandGroup(self, '', '__main__#{}') as g:
g.command('hello', 'hello_command_handler', confirmation=True)
g.command('sample-json', 'sample_json_handler')
with CommandGroup(self, 'abc', '__main__#{}') as g:
g.command('list', 'abc_list_command_handler')
g.command('show', 'a_test_command_handler')
g.command('get', 'a_test_command_handler', deprecate_info=g.deprecate(redirect='show', hide='1.0.0'))
g.command('show', 'abc_show_command_handler')
g.command('get', 'abc_show_command_handler', deprecate_info=g.deprecate(redirect='show', hide='1.0.0'))
g.command('first', 'abc_first_command_handler', is_preview=True)
g.command('last', 'abc_last_command_handler', )
g.command('last', 'abc_last_command_handler', is_experimental=True)
with CommandGroup(self, 'ga', '__main__#{}') as g:
g.command('range', 'num_range_command_handler')
g.command('range', 'range_command_handler')
with CommandGroup(self, 'pre', '__main__#{}', is_preview=True) as g:
g.command('range', 'num_range_command_handler')
with CommandGroup(self, 'exp', '__main__#{}', ) as g:
g.command('range', 'num_range_command_handler')
g.command('first', 'abc_first_command_handler', is_preview=True)
g.command('range', 'range_command_handler')
with CommandGroup(self, 'exp', '__main__#{}', is_experimental=True) as g:
g.command('range', 'range_command_handler')
return super(MyCommandsLoader, self).load_command_table(args)

def load_arguments(self, command):
with ArgumentsContext(self, 'hello world') as ac:
ac.argument('myarg', type=int, default=100)
with ArgumentsContext(self, 'ga range') as ac:
ac.argument('start', type=int, is_preview=True)
ac.argument('end', type=int, )
ac.argument('end', type=int, is_experimental=True)
super(MyCommandsLoader, self).load_arguments(command)


Expand Down
77 changes: 70 additions & 7 deletions knack/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

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

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

class CLICommandArgument(object):

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

def __init__(self, dest=None, argtype=None, **kwargs):
"""An argument that has a specific destination parameter.
Expand Down Expand Up @@ -274,6 +276,56 @@ def _get_preview_arg_message(self):
kwargs['action'] = _handle_argument_preview(preview_info)
return kwargs

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

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

def _handle_argument_experimental(experimental_info):

parent_class = self._get_parent_class(**kwargs)

class ExperimentalArgumentAction(parent_class):

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

return ExperimentalArgumentAction

def _get_experimental_arg_message(self):
return "{} '{}' is experimental and not covered by customer support. " \
"Please use with discretion.".format(self.object_type.capitalize(), self.target)

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

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

experimental_info = ExperimentalItem(
self.command_loader.cli_ctx,
target=target,
object_type=object_type,
message_func=_get_experimental_arg_message
)
kwargs['experimental_info'] = experimental_info
kwargs['action'] = _handle_argument_experimental(experimental_info)
return kwargs

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

Expand Down Expand Up @@ -305,8 +357,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`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `is_experimental`,
`deprecate_info`. See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -316,7 +368,16 @@ def argument(self, argument_dest, arg_type=None, **kwargs):
if deprecate_action:
kwargs['action'] = deprecate_action

is_preview = kwargs.get('is_preview', False)
is_experimental = kwargs.get('is_experimental', False)

if is_preview and is_experimental:
from .commands import PREVIEW_EXPERIMENTAL_CONFLICT_ERROR
raise CLIError(PREVIEW_EXPERIMENTAL_CONFLICT_ERROR.format('argument', argument_dest))

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

self.command_loader.argument_registry.register_cli_argument(self.command_scope,
argument_dest,
arg_type,
Expand All @@ -330,8 +391,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`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `is_experimental`,
`deprecate_info`. See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -357,6 +418,7 @@ def positional(self, argument_dest, arg_type=None, **kwargs):
kwargs['action'] = deprecate_action

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

self.command_loader.argument_registry.register_cli_argument(self.command_scope,
argument_dest,
Expand All @@ -383,8 +445,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`, `is_preview`, `deprecate_info`.
See /docs/arguments.md.
`type`, `choices`, `required`, `help`, `metavar`, `is_preview`, `is_experimental`,
`deprecate_info`. See /docs/arguments.md.
"""
self._check_stale()
if not self._applicable():
Expand All @@ -400,6 +462,7 @@ def extra(self, argument_dest, **kwargs):
kwargs['action'] = deprecate_action

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

self.command_loader.extra_argument_registry[self.command_scope][argument_dest] = CLICommandArgument(
argument_dest, **kwargs)
Expand Down
46 changes: 37 additions & 9 deletions knack/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .deprecation import Deprecated
from .preview import PreviewItem
from .experimental import ExperimentalItem
from .prompting import prompt_y_n, NoTTYException
from .util import CLIError, CtxTypeError
from .arguments import ArgumentRegistry, CLICommandArgument
Expand All @@ -23,13 +24,17 @@
logger = get_logger(__name__)


PREVIEW_EXPERIMENTAL_CONFLICT_ERROR = "Failed to register {} '{}', " \
"is_preview and is_experimental can't be true at the same time"


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, preview_info=None,
**kwargs):
experimental_info=None, **kwargs):
""" The command object that goes into the command table.

:param cli_ctx: CLI Context
Expand All @@ -52,6 +57,8 @@ def __init__(self, cli_ctx, name, handler, description=None, table_transformer=N
:type deprecate_info: str
:param preview_info: Indicates a command is in preview
:type preview_info: bool
:param experimental_info: Indicates a command is experimental
:type experimental_info: bool
:param validator: The command validator
:param confirmation: User confirmation required for command
:type confirmation: bool, str, callable
Expand All @@ -71,6 +78,7 @@ def __init__(self, cli_ctx, name, handler, description=None, table_transformer=N
self.formatter_class = formatter_class
self.deprecate_info = deprecate_info
self.preview_info = preview_info
self.experimental_info = experimental_info
self.confirmation = confirmation
self.validator = validator

Expand Down Expand Up @@ -300,12 +308,23 @@ 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):

is_preview = kwargs.get('is_preview', False)
is_experimental = kwargs.get('is_experimental', False)
if is_preview and is_experimental:
raise CLIError(PREVIEW_EXPERIMENTAL_CONFLICT_ERROR.format("command group", group_name))
if is_preview:
kwargs['preview_info'] = PreviewItem(
cli_ctx=self.command_loader.cli_ctx,
target=group_name,
object_type='command group'
)
if is_experimental:
kwargs['experimental_info'] = ExperimentalItem(
cli_ctx=self.command_loader.cli_ctx,
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 @@ -325,20 +344,29 @@ def command(self, name, handler_name, **kwargs):
: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`,
`is_preview`.
`is_preview`, `is_experimental`.
"""
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

# don't inherit deprecation, preview and experimental info from command group
# https://github.com/Azure/azure-cli/blob/683b9709b67c4c9e8df92f9fbd53cbf83b6973d3/src/azure-cli-core/azure/cli/core/commands/__init__.py#L1155
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'
)

is_preview = kwargs.get('is_preview', False)
is_experimental = kwargs.get('is_experimental', False)
if is_preview and is_experimental:
raise CLIError(PREVIEW_EXPERIMENTAL_CONFLICT_ERROR.format("command", self.group_name + " " + name))

command_kwargs['preview_info'] = None
if is_preview:
command_kwargs['preview_info'] = PreviewItem(self.command_loader.cli_ctx, object_type='command')
command_kwargs['experimental_info'] = None
if is_experimental:
command_kwargs['experimental_info'] = ExperimentalItem(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