Skip to content

Commit

Permalink
Merge dev to master (#181)
Browse files Browse the repository at this point in the history
* Allow disabling color (#171)

* Support --only-show-errors (#179)

* Add experimental tag (#180)
  • Loading branch information
jiasli committed Mar 20, 2020
1 parent bce1c0f commit 5f0bcc2
Show file tree
Hide file tree
Showing 19 changed files with 868 additions and 123 deletions.
1 change: 1 addition & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Logging
- By default, log messages Warning and above are shown to the user.
- `--verbose` - This flag changes the logging level to Info and above.
- `--debug` - This flag changes the logging level to Debug and above.
- `--only-show-errors` - This flag changes the logging level to Error only, suppressing Warning.

* All log messages go to STDERR (not STDOUT)
* Log to Error or Warning for user messages instead of using the `print()` function
Expand Down
125 changes: 114 additions & 11 deletions examples/exapp2
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,110 @@ helps['abc list'] = """
text: {cli_name} abc list
""".format(cli_name=cli_name)

helps['abc first'] = """
type: command
short-summary: List the first several letters in the alphabet.
examples:
- name: Show the list of abc
text: {cli_name} abc first --number 3
""".format(cli_name=cli_name)

helps['abc last'] = """
type: command
short-summary: List the last several letters in the alphabet.
examples:
- name: Show the list of xyz
text: {cli_name} abc last --number 3
""".format(cli_name=cli_name)

def a_test_command_handler():
return [{'a': 1, 'b': 1234}, {'a': 3, 'b': 4}]
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 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():
import string
return list(string.ascii_lowercase)


def hello_command_handler(myarg=None, abc=None):
return ['hello', 'world', myarg, abc]
def abc_first_command_handler(number=5):
import string
return list(string.ascii_lowercase)[0:number]


def abc_last_command_handler(number=5):
import string
return list(string.ascii_lowercase)[-number:]


def range_command_handler(start=0, end=5):
"""
Get a list of natural numbers from start to end
:param start: the lower bound
:param end: the higher bound
:return:
"""
return list(range(int(start), int(end) + 1))


def sample_json_handler():
"""
Get a sample JSON string
"""
# https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list#examples
result = {
"id": "/subscriptions/291bba3f-e0a5-47bc-a099-3bdcb2a50a05",
"subscriptionId": "291bba3f-e0a5-47bc-a099-3bdcb2a50a05",
"tenantId": "31c75423-32d6-4322-88b7-c478bdde4858",
"displayName": "Example Subscription",
"state": "Enabled",
"subscriptionPolicies": {
"locationPlacementId": "Internal_2014-09-01",
"quotaId": "Internal_2014-09-01",
"spendingLimit": "Off"
},
"authorizationSource": "RoleBased",
"managedByTenants": [
{
"tenantId": "8f70baf1-1f6e-46a2-a1ff-238dac1ebfb7"
}
]
}
return result


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 @@ -65,17 +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='0.1.0'))
g.command('list', 'abc_list_command_handler')
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', is_experimental=True)
with CommandGroup(self, 'ga', '__main__#{}') as g:
g.command('range', 'range_command_handler')
with CommandGroup(self, 'pre', '__main__#{}', is_preview=True) as g:
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, is_experimental=True)
super(MyCommandsLoader, self).load_arguments(command)


Expand Down
78 changes: 71 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 @@ -265,6 +267,7 @@ def _get_preview_arg_message(self):
object_type = 'positional argument'

preview_info = PreviewItem(
cli_ctx=self.command_loader.cli_ctx,
target=target,
object_type=object_type,
message_func=_get_preview_arg_message
Expand All @@ -273,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 @@ -304,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 @@ -315,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 @@ -329,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 @@ -356,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 @@ -382,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 @@ -399,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
12 changes: 11 additions & 1 deletion knack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def __init__(self,
self.output = self.output_cls(cli_ctx=self)
self.result = None
self.query = query_cls(cli_ctx=self)
self.only_show_errors = self.config.get('core', 'only_show_errors', fallback=False)
self.enable_color = not self.config.get('core', 'no_color', fallback=False)

@staticmethod
def _should_show_version(args):
Expand Down Expand Up @@ -187,6 +189,13 @@ def invoke(self, args, initial_invocation_data=None, out_file=None):
raise TypeError('args should be a list or tuple.')
exit_code = 0
try:
if self.enable_color:
import colorama
colorama.init()
if self.out_file == sys.__stdout__:
# point out_file to the new sys.stdout which is overwritten by colorama
self.out_file = sys.stdout

args = self.completion.get_completion_args() or args
out_file = out_file or self.out_file

Expand Down Expand Up @@ -218,6 +227,7 @@ def invoke(self, args, initial_invocation_data=None, out_file=None):
exit_code = self.exception_handler(ex)
self.result = CommandResultItem(None, error=ex)
finally:
pass
if self.enable_color:
colorama.deinit()
self.result.exit_code = exit_code
return exit_code

0 comments on commit 5f0bcc2

Please sign in to comment.