Skip to content

Commit 021d627

Browse files
committed
Copybara generated commit for Python Fire.
- 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 <zuhaochen@google.com>
1 parent 83a8036 commit 021d627

5 files changed

Lines changed: 137 additions & 28 deletions

File tree

fire/completion.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,10 @@ def _FishScript(name, commands, default_options=None):
133133
return 1
134134
end
135135
"""
136-
subcommand_template = "complete -c {name} -n " \
137-
"'__fish_using_command {start}' -f -a {subcommand}\n"
138-
flag_template = "complete -c {name} -n " \
139-
"'__fish_using_command {start}' -l {option}\n"
136+
subcommand_template = ("complete -c {name} -n '__fish_using_command {start}' "
137+
"-f -a {subcommand}\n")
138+
flag_template = ("complete -c {name} -n "
139+
"'__fish_using_command {start}' -l {option}\n")
140140
for start in options_map:
141141
for option in sorted(options_map[start]):
142142
if option.startswith('--'):

fire/core.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,41 @@ def __init__(self, code, component_trace):
200200
self.trace = component_trace
201201

202202

203+
def _IsHelpShortcut(component_trace, remaining_args):
204+
"""Determines if the user is trying to access help without '--' separator.
205+
206+
For example, mycmd.py --help instead of mycmd.py -- --help.
207+
208+
Args:
209+
component_trace: (FireTrace) The trace for the Fire command.
210+
remaining_args: List of remaining args that haven't been consumed yet.
211+
Returns:
212+
True if help is requested, False otherwise.
213+
"""
214+
show_help = False
215+
if remaining_args:
216+
target = remaining_args[0]
217+
if target == '-h':
218+
show_help = True
219+
elif target == '--help':
220+
# Check if --help would be consumed as a keyword argument, or is a member.
221+
component = component_trace.GetResult()
222+
if inspect.isclass(component) or inspect.isroutine(component):
223+
fn_spec = inspectutils.GetFullArgSpec(component)
224+
_, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
225+
show_help = target in remaining_kwargs
226+
else:
227+
members = dict(inspect.getmembers(component))
228+
show_help = target not in members
229+
230+
if show_help:
231+
component_trace.show_help = True
232+
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
233+
print('INFO: Showing help with the command {cmd}.\n'.format(
234+
cmd=pipes.quote(command)), file=sys.stderr)
235+
return show_help
236+
237+
203238
def _PrintResult(component_trace, verbose=False):
204239
"""Prints the result of the Fire call to stdout in a human readable way."""
205240
# TODO: Design human readable deserializable serialization method
@@ -231,20 +266,25 @@ def _DictAsString(result, verbose=False):
231266
Returns:
232267
A string representing the dict
233268
"""
234-
result = {key: value for key, value in result.items()
235-
if _ComponentVisible(key, verbose)}
236269

237-
if not result:
270+
# We need to do 2 iterations over the items in the result dict
271+
# 1) Getting visible items and the longest key for output formatting
272+
# 2) Actually construct the output lines
273+
result_visible = {key: value for key, value in result.items()
274+
if _ComponentVisible(key, verbose)}
275+
276+
if not result_visible:
238277
return '{}'
239278

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

243282
lines = []
244283
for key, value in result.items():
245-
line = format_string.format(key=str(key) + ':',
246-
value=_OneLineResult(value))
247-
lines.append(line)
284+
if _ComponentVisible(key, verbose):
285+
line = format_string.format(key=str(key) + ':',
286+
value=_OneLineResult(value))
287+
lines.append(line)
248288
return '\n'.join(lines)
249289

250290

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

387+
if _IsHelpShortcut(component_trace, remaining_args):
388+
remaining_args = []
389+
break
390+
347391
saved_args = []
348392
used_separator = False
349393
if separator in remaining_args:
@@ -556,7 +600,6 @@ def _MakeParseFn(fn):
556600
the leftover args from the arguments to the parse function.
557601
"""
558602
fn_spec = inspectutils.GetFullArgSpec(fn)
559-
all_args = fn_spec.args + fn_spec.kwonlyargs
560603
metadata = decorators.GetMetadata(fn)
561604

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

567610
def _ParseFn(args):
568611
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
569-
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(
570-
args, all_args, fn_spec.varkw)
612+
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)
571613

572614
# Note: _ParseArgs modifies kwargs.
573615
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
@@ -663,7 +705,7 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
663705
return parsed_args, kwargs, remaining_args, capacity
664706

665707

666-
def _ParseKeywordArgs(args, fn_args, fn_keywords):
708+
def _ParseKeywordArgs(args, fn_spec):
667709
"""Parses the supplied arguments for keyword arguments.
668710
669711
Given a list of arguments, finds occurences of --name value, and uses 'name'
@@ -677,11 +719,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
677719
_ParseArgs, which converts them to the appropriate type.
678720
679721
Args:
680-
args: A list of arguments
681-
fn_args: A list of argument names that the target function accepts,
682-
including positional and named arguments, but not the varargs or kwargs
683-
names.
684-
fn_keywords: The argument name for **kwargs, or None if **kwargs not used
722+
args: A list of arguments.
723+
fn_spec: The inspectutils.FullArgSpec describing the given callable.
685724
Returns:
686725
kwargs: A dictionary mapping keywords to values.
687726
remaining_kwargs: A list of the unused kwargs from the original args.
@@ -690,6 +729,8 @@ def _ParseKeywordArgs(args, fn_args, fn_keywords):
690729
kwargs = {}
691730
remaining_kwargs = []
692731
remaining_args = []
732+
fn_keywords = fn_spec.varkw
733+
fn_args = fn_spec.args + fn_spec.kwonlyargs
693734

694735
if not args:
695736
return kwargs, remaining_kwargs, remaining_args

fire/core_test.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,43 @@ def testInteractiveModeVariablesWithName(self, mock_embed):
7272
self.assertEqual(variables['D'], tc.WithDefaults)
7373
self.assertIsInstance(variables['trace'], trace.FireTrace)
7474

75-
def testImproperUseOfHelp(self):
76-
# This should produce a warning explaining the proper use of help.
77-
with self.assertRaisesFireExit(2, 'The proper way to show help.*Usage:'):
78-
core.Fire(tc.TypedProperties, command=['alpha', '--help'])
79-
80-
def testProperUseOfHelp(self):
75+
# TODO: Use parameterized tests to break up repetitive tests.
76+
def testHelpWithClass(self):
77+
with self.assertRaisesFireExit(0, 'Usage:.*ARG1'):
78+
core.Fire(tc.InstanceVars, command=['--', '--help'])
79+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
80+
core.Fire(tc.InstanceVars, command=['--help'])
81+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*ARG1'):
82+
core.Fire(tc.InstanceVars, command=['-h'])
83+
84+
def testHelpWithMember(self):
8185
with self.assertRaisesFireExit(0, 'Usage:.*upper'):
8286
core.Fire(tc.TypedProperties, command=['gamma', '--', '--help'])
87+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
88+
core.Fire(tc.TypedProperties, command=['gamma', '--help'])
89+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*upper'):
90+
core.Fire(tc.TypedProperties, command=['gamma', '-h'])
91+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*delta'):
92+
core.Fire(tc.TypedProperties, command=['delta', '--help'])
93+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*echo'):
94+
core.Fire(tc.TypedProperties, command=['echo', '--help'])
95+
96+
def testHelpOnErrorInConstructor(self):
97+
with self.assertRaisesFireExit(0, 'Usage:.*[VALUE]'):
98+
core.Fire(tc.ErrorInConstructor, command=['--', '--help'])
99+
with self.assertRaisesFireExit(0, 'INFO:.*Usage:.*[VALUE]'):
100+
core.Fire(tc.ErrorInConstructor, command=['--help'])
101+
102+
def testHelpWithNamespaceCollision(self):
103+
# Tests cases when calling the help shortcut should not show help.
104+
with self.assertOutputMatches(stdout='Docstring.*', stderr=None):
105+
core.Fire(tc.WithHelpArg, command=['--help', 'False'])
106+
with self.assertOutputMatches(stdout='help in a dict', stderr=None):
107+
core.Fire(tc.WithHelpArg, command=['dictionary', '__help'])
108+
with self.assertOutputMatches(stdout='{}', stderr=None):
109+
core.Fire(tc.WithHelpArg, command=['dictionary', '--help'])
110+
with self.assertOutputMatches(stdout='False', stderr=None):
111+
core.Fire(tc.function_with_help, command=['False'])
83112

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

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

109144
if __name__ == '__main__':
110145
testutils.main()

fire/parser_fuzz_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def testDefaultParseValueFuzz(self, value):
6868
raise
6969

7070
try:
71-
uvalue = six.u(value)
72-
uresult = six.u(result)
71+
uvalue = unicode(value)
72+
uresult = unicode(result)
7373
except UnicodeDecodeError:
7474
# This is not what we're testing.
7575
return

fire/test_components.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from __future__ import division
1919
from __future__ import print_function
2020

21+
import collections
22+
2123
import six
2224

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

3234

35+
def function_with_help(help=True): # pylint: disable=redefined-builtin
36+
return help
37+
38+
3339
class Empty(object):
3440
pass
3541

@@ -44,6 +50,21 @@ def __init__(self):
4450
pass
4551

4652

53+
class ErrorInConstructor(object):
54+
55+
def __init__(self, value='value'):
56+
self.value = value
57+
raise ValueError('Error in constructor')
58+
59+
60+
class WithHelpArg(object):
61+
"""Test class for testing when class has a help= arg."""
62+
63+
def __init__(self, help=True): # pylint: disable=redefined-builtin
64+
self.has_help = help
65+
self.dictionary = {'__help': 'help in a dict'}
66+
67+
4768
class NoDefaults(object):
4869

4970
def double(self, count):
@@ -215,3 +236,15 @@ def create(self):
215236
x = {}
216237
x['y'] = x
217238
return x
239+
240+
241+
class OrderedDictionary(object):
242+
243+
def empty(self):
244+
return collections.OrderedDict()
245+
246+
def non_empty(self):
247+
ordered_dict = collections.OrderedDict()
248+
ordered_dict['A'] = 'A'
249+
ordered_dict[2] = 2
250+
return ordered_dict

0 commit comments

Comments
 (0)