Skip to content

Commit

Permalink
Copybara generated commit for Python Fire.
Browse files Browse the repository at this point in the history
  - Formatting for completion.
  - Preserving order of keys when printing result which is an OrderedDict.
  - Allow use of --help without --.

PiperOrigin-RevId: 201601732
Change-Id: Icba7e663634f5153283095d8b177d834af480b15
Reviewed-on: https://team-review.git.corp.google.com/278010
Reviewed-by: Joe Chen <[email protected]>
  • Loading branch information
joejoevictor committed Jun 21, 2018
1 parent 83a8036 commit 021d627
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 28 deletions.
8 changes: 4 additions & 4 deletions fire/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ def _FishScript(name, commands, default_options=None):
return 1
end
"""
subcommand_template = "complete -c {name} -n " \
"'__fish_using_command {start}' -f -a {subcommand}\n"
flag_template = "complete -c {name} -n " \
"'__fish_using_command {start}' -l {option}\n"
subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
"-f -a {subcommand}\n")
flag_template = ("complete -c {name} -n "
"'__fish_using_command {start}' -l {option}\n")
for start in options_map:
for option in sorted(options_map[start]):
if option.startswith('--'):
Expand Down
73 changes: 57 additions & 16 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,41 @@ def __init__(self, code, component_trace):
self.trace = component_trace


def _IsHelpShortcut(component_trace, remaining_args):
"""Determines if the user is trying to access help without '--' separator.
For example, mycmd.py --help instead of mycmd.py -- --help.
Args:
component_trace: (FireTrace) The trace for the Fire command.
remaining_args: List of remaining args that haven't been consumed yet.
Returns:
True if help is requested, False otherwise.
"""
show_help = False
if remaining_args:
target = remaining_args[0]
if target == '-h':
show_help = True
elif target == '--help':
# Check if --help would be consumed as a keyword argument, or is a member.
component = component_trace.GetResult()
if inspect.isclass(component) or inspect.isroutine(component):
fn_spec = inspectutils.GetFullArgSpec(component)
_, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
show_help = target in remaining_kwargs
else:
members = dict(inspect.getmembers(component))
show_help = target not in members

if show_help:
component_trace.show_help = True
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
print('INFO: Showing help with the command {cmd}.\n'.format(
cmd=pipes.quote(command)), file=sys.stderr)
return show_help


def _PrintResult(component_trace, verbose=False):
"""Prints the result of the Fire call to stdout in a human readable way."""
# TODO: Design human readable deserializable serialization method
Expand Down Expand Up @@ -231,20 +266,25 @@ def _DictAsString(result, verbose=False):
Returns:
A string representing the dict
"""
result = {key: value for key, value in result.items()
if _ComponentVisible(key, verbose)}

if not result:
# We need to do 2 iterations over the items in the result dict
# 1) Getting visible items and the longest key for output formatting
# 2) Actually construct the output lines
result_visible = {key: value for key, value in result.items()
if _ComponentVisible(key, verbose)}

if not result_visible:
return '{}'

longest_key = max(len(str(key)) for key in result.keys())
longest_key = max(len(str(key)) for key in result_visible.keys())
format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1)

lines = []
for key, value in result.items():
line = format_string.format(key=str(key) + ':',
value=_OneLineResult(value))
lines.append(line)
if _ComponentVisible(key, verbose):
line = format_string.format(key=str(key) + ':',
value=_OneLineResult(value))
lines.append(line)
return '\n'.join(lines)


Expand Down Expand Up @@ -344,6 +384,10 @@ def _Fire(component, args, context, name=None):
# there's a separator after it, and instead process the current component.
break

if _IsHelpShortcut(component_trace, remaining_args):
remaining_args = []
break

saved_args = []
used_separator = False
if separator in remaining_args:
Expand Down Expand Up @@ -556,7 +600,6 @@ def _MakeParseFn(fn):
the leftover args from the arguments to the parse function.
"""
fn_spec = inspectutils.GetFullArgSpec(fn)
all_args = fn_spec.args + fn_spec.kwonlyargs
metadata = decorators.GetMetadata(fn)

# Note: num_required_args is the number of positional arguments without
Expand All @@ -566,8 +609,7 @@ def _MakeParseFn(fn):

def _ParseFn(args):
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(
args, all_args, fn_spec.varkw)
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)

# Note: _ParseArgs modifies kwargs.
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
Expand Down Expand Up @@ -663,7 +705,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
return parsed_args, kwargs, remaining_args, capacity


def _ParseKeywordArgs(args, fn_args, fn_keywords):
def _ParseKeywordArgs(args, fn_spec):
"""Parses the supplied arguments for keyword arguments.
Given a list of arguments, finds occurences of --name value, and uses 'name'
Expand All @@ -677,11 +719,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
_ParseArgs, which converts them to the appropriate type.
Args:
args: A list of arguments
fn_args: A list of argument names that the target function accepts,
including positional and named arguments, but not the varargs or kwargs
names.
fn_keywords: The argument name for **kwargs, or None if **kwargs not used
args: A list of arguments.
fn_spec: The inspectutils.FullArgSpec describing the given callable.
Returns:
kwargs: A dictionary mapping keywords to values.
remaining_kwargs: A list of the unused kwargs from the original args.
Expand All @@ -690,6 +729,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
kwargs = {}
remaining_kwargs = []
remaining_args = []
fn_keywords = fn_spec.varkw
fn_args = fn_spec.args + fn_spec.kwonlyargs

if not args:
return kwargs, remaining_kwargs, remaining_args
Expand Down
47 changes: 41 additions & 6 deletions fire/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,43 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
self.assertEqual(variables['D'], tc.WithDefaults)
self.assertIsInstance(variables['trace'], trace.FireTrace)

def testImproperUseOfHelp(self):
# This should produce a warning explaining the proper use of help.
with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
core.Fire(tc.TypedProperties, command=['alpha', '--help'])

def testProperUseOfHelp(self):
# TODO: Use parameterized tests to break up repetitive tests.
def testHelpWithClass(self):
with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
core.Fire(tc.InstanceVars, command=['-h'])

def testHelpWithMember(self):
with self.assertRaisesFireExit(0, 'Usage:.*upper'):
core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
core.Fire(tc.TypedProperties, command=['gamma', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
core.Fire(tc.TypedProperties, command=['gamma', '-h'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
core.Fire(tc.TypedProperties, command=['delta', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
core.Fire(tc.TypedProperties, command=['echo', '--help'])

def testHelpOnErrorInConstructor(self):
with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
core.Fire(tc.ErrorInConstructor, command=['--help'])

def testHelpWithNamespaceCollision(self):
# Tests cases when calling the help shortcut should not show help.
with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
core.Fire(tc.WithHelpArg, command=['--help', 'False'])
with self.assertOutputMatches(stdout='help in a dict', stderr=None):
core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
with self.assertOutputMatches(stdout='False', stderr=None):
core.Fire(tc.function_with_help, command=['False'])

def testInvalidParameterRaisesFireExit(self):
with self.assertRaisesFireExit(2, 'runmisspelled'):
Expand All @@ -105,6 +134,12 @@ def testPrintEmptyDict(self):
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.EmptyDictOutput, command=['nothing_printable'])

def testPrintDict(self):
with self.assertOutputMatches(stdout=r'A:\s+A\s+2:\s+2\s+', stderr=None):
core.Fire(tc.OrderedDictionary, command=['non_empty'])
with self.assertOutputMatches(stdout='{}'):
core.Fire(tc.OrderedDictionary, command=['empty'])


if __name__ == '__main__':
testutils.main()
4 changes: 2 additions & 2 deletions fire/parser_fuzz_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def testDefaultParseValueFuzz(self, value):
raise

try:
uvalue = six.u(value)
uresult = six.u(result)
uvalue = unicode(value)
uresult = unicode(result)
except UnicodeDecodeError:
# This is not what we're testing.
return
Expand Down
33 changes: 33 additions & 0 deletions fire/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from __future__ import division
from __future__ import print_function

import collections

import six

if six.PY3:
Expand All @@ -30,6 +32,10 @@ def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=ke
identity.__annotations__ = {'arg2': int, 'arg4': int}


def function_with_help(help=True): # pylint: disable=redefined-builtin
return help


class Empty(object):
pass

Expand All @@ -44,6 +50,21 @@ def __init__(self):
pass


class ErrorInConstructor(object):

def __init__(self, value='value'):
self.value = value
raise ValueError('Error in constructor')


class WithHelpArg(object):
"""Test class for testing when class has a help= arg."""

def __init__(self, help=True): # pylint: disable=redefined-builtin
self.has_help = help
self.dictionary = {'__help': 'help in a dict'}


class NoDefaults(object):

def double(self, count):
Expand Down Expand Up @@ -215,3 +236,15 @@ def create(self):
x = {}
x['y'] = x
return x


class OrderedDictionary(object):

def empty(self):
return collections.OrderedDict()

def non_empty(self):
ordered_dict = collections.OrderedDict()
ordered_dict['A'] = 'A'
ordered_dict[2] = 2
return ordered_dict

0 comments on commit 021d627

Please sign in to comment.