Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge dev to master #181

Merged
merged 3 commits into from
Mar 20, 2020
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
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
Loading