Skip to content

Commit 3d74ae8

Browse files
dbiebercopybara-github
authored andcommitted
Show both call syntax and members for callable objects in help and usage screens.
PiperOrigin-RevId: 260032359 Change-Id: I91963f1eb1fa570c15a1accd4588f53e1a77cabe
1 parent dbc8022 commit 3d74ae8

File tree

5 files changed

+109
-118
lines changed

5 files changed

+109
-118
lines changed

fire/completion.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def _FishScript(name, commands, default_options=None):
281281
)
282282

283283

284-
def MemberVisible(name, member, verbose):
284+
def MemberVisible(component, name, member, verbose):
285285
"""Returns whether a member should be included in auto-completion or help.
286286
287287
Determines whether a member of an object with the specified name should be
@@ -294,6 +294,7 @@ def MemberVisible(name, member, verbose):
294294
When not in verbose mode, some modules and functions are excluded as well.
295295
296296
Args:
297+
component: The component containing the member.
297298
name: The name of the member.
298299
member: The member itself.
299300
verbose: Whether to include private members.
@@ -309,6 +310,13 @@ def MemberVisible(name, member, verbose):
309310
if inspect.ismodule(member) and member is six:
310311
# TODO(dbieber): Determine more generally which modules to hide.
311312
return False
313+
if (six.PY2 and inspect.isfunction(component)
314+
and name in ('func_closure', 'func_code', 'func_defaults',
315+
'func_dict', 'func_doc', 'func_globals', 'func_name')):
316+
return False
317+
if (six.PY2 and inspect.ismethod(component)
318+
and name in ('im_class', 'im_func', 'im_self')):
319+
return False
312320
if isinstance(name, six.string_types):
313321
return not name.startswith('_')
314322
return True # Default to including the member
@@ -334,7 +342,7 @@ def _Members(component, verbose=False):
334342
return [
335343
(member_name, member)
336344
for member_name, member in members
337-
if MemberVisible(member_name, member, verbose)
345+
if MemberVisible(component, member_name, member, verbose)
338346
]
339347

340348

fire/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def _DictAsString(result, verbose=False):
303303
# 1) Getting visible items and the longest key for output formatting
304304
# 2) Actually construct the output lines
305305
result_visible = {key: value for key, value in result.items()
306-
if completion.MemberVisible(key, value, verbose)}
306+
if completion.MemberVisible(result, key, value, verbose)}
307307

308308
if not result_visible:
309309
return '{}'
@@ -313,7 +313,7 @@ def _DictAsString(result, verbose=False):
313313

314314
lines = []
315315
for key, value in result.items():
316-
if completion.MemberVisible(key, value, verbose):
316+
if completion.MemberVisible(result, key, value, verbose):
317317
line = format_string.format(key=str(key) + ':',
318318
value=_OneLineResult(value))
319319
lines.append(line)

fire/helptext.py

Lines changed: 89 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,14 @@ def HelpText(component, trace=None, verbose=False):
7070
description_section = _DescriptionSection(component, info)
7171
# TODO(dbieber): Add returns and raises sections for functions.
7272

73-
if inspect.isroutine(component) or inspect.isclass(component):
74-
# For functions (ARGUMENTS / POSITIONAL ARGUMENTS, FLAGS)
73+
if callable(component):
7574
args_and_flags_sections, notes_sections = _ArgsAndFlagsSections(
7675
info, spec, metadata)
77-
usage_details_sections = []
7876
else:
79-
# For objects (GROUPS, COMMANDS, VALUES, INDEXES)
80-
# TODO(dbieber): Show callable function usage in help text.
8177
args_and_flags_sections = []
8278
notes_sections = []
83-
usage_details_sections = _UsageDetailsSections(component,
84-
actions_grouped_by_kind)
79+
usage_details_sections = _UsageDetailsSections(component,
80+
actions_grouped_by_kind)
8581

8682
sections = (
8783
[name_section, synopsis_section, description_section]
@@ -119,21 +115,19 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata,
119115
"""The "Synopsis" section of the help string."""
120116
current_command = _GetCurrentCommand(trace=trace, include_separators=True)
121117

122-
# TODO(dbieber): Support callable functions.
123-
if inspect.isroutine(component) or inspect.isclass(component):
124-
# For function:
125-
args_and_flags = _GetArgsAndFlagsString(spec, metadata)
126-
synopsis_section_template = '{current_command} {args_and_flags}'
127-
text = synopsis_section_template.format(
128-
current_command=current_command, args_and_flags=args_and_flags)
118+
possible_actions = _GetPossibleActions(actions_grouped_by_kind)
129119

130-
else:
131-
# For object:
132-
possible_actions_string = _GetPossibleActionsString(actions_grouped_by_kind)
133-
synopsis_template = '{current_command} {possible_actions}'
134-
text = synopsis_template.format(
135-
current_command=current_command,
136-
possible_actions=possible_actions_string)
120+
continuations = []
121+
if possible_actions:
122+
continuations.append(_GetPossibleActionsString(possible_actions))
123+
if callable(component):
124+
continuations.append(_GetArgsAndFlagsString(spec, metadata))
125+
continuation = ' | '.join(continuations)
126+
127+
synopsis_template = '{current_command} {continuation}'
128+
text = synopsis_template.format(
129+
current_command=current_command,
130+
continuation=continuation)
137131

138132
return ('SYNOPSIS', text)
139133

@@ -225,22 +219,17 @@ def _UsageDetailsSections(component, actions_grouped_by_kind):
225219
"""The usage details sections of the help string."""
226220
groups, commands, values, indexes = actions_grouped_by_kind
227221

228-
usage_details_sections = []
222+
sections = []
223+
if groups.members:
224+
sections.append(_MakeUsageDetailsSection(groups))
225+
if commands.members:
226+
sections.append(_MakeUsageDetailsSection(commands))
227+
if values.members:
228+
sections.append(_ValuesUsageDetailsSection(component, values))
229+
if indexes.members:
230+
sections.append(('INDEXES', _NewChoicesSection('INDEX', indexes.names)))
229231

230-
if groups:
231-
usage_details_section = _GroupUsageDetailsSection(groups)
232-
usage_details_sections.append(usage_details_section)
233-
if commands:
234-
usage_details_section = _CommandUsageDetailsSection(commands)
235-
usage_details_sections.append(usage_details_section)
236-
if values:
237-
usage_details_section = _ValuesUsageDetailsSection(component, values)
238-
usage_details_sections.append(usage_details_section)
239-
if indexes:
240-
usage_details_sections.append(
241-
('INDEXES', _NewChoicesSection('INDEX', [indexes])))
242-
243-
return usage_details_sections
232+
return sections
244233

245234

246235
def _GetSummary(info):
@@ -302,51 +291,46 @@ def _GetArgsAndFlagsString(spec, metadata):
302291
return ' '.join(arg_and_flag_strings)
303292

304293

305-
def _GetPossibleActionsString(actions_grouped_by_kind):
306-
"""A help screen string listing the possible action kinds available."""
307-
groups, commands, values, indexes = actions_grouped_by_kind
308-
294+
def _GetPossibleActions(actions_grouped_by_kind):
295+
"""The list of possible action kinds."""
309296
possible_actions = []
310-
if groups:
311-
possible_actions.append('GROUP')
312-
if commands:
313-
possible_actions.append('COMMAND')
314-
if values:
315-
possible_actions.append('VALUE')
316-
if indexes:
317-
possible_actions.append('INDEX')
297+
for action_group in actions_grouped_by_kind:
298+
if action_group.members:
299+
possible_actions.append(action_group.name)
300+
return possible_actions
301+
318302

319-
possible_actions_string = ' | '.join(
320-
formatting.Underline(action) for action in possible_actions)
321-
return possible_actions_string
303+
def _GetPossibleActionsString(possible_actions):
304+
"""A help screen string listing the possible action kinds available."""
305+
return ' | '.join(formatting.Underline(action.upper())
306+
for action in possible_actions)
322307

323308

324309
def _GetActionsGroupedByKind(component, verbose=False):
325310
"""Gets lists of available actions, grouped by action kind."""
326-
groups = []
327-
commands = []
328-
values = []
311+
groups = ActionGroup(name='group', plural='groups')
312+
commands = ActionGroup(name='command', plural='commands')
313+
values = ActionGroup(name='value', plural='values')
314+
indexes = ActionGroup(name='index', plural='indexes')
329315

330316
members = completion._Members(component, verbose) # pylint: disable=protected-access
331317
for member_name, member in members:
332318
member_name = str(member_name)
333319
if value_types.IsGroup(member):
334-
groups.append((member_name, member))
320+
groups.Add(name=member_name, member=member)
335321
if value_types.IsCommand(member):
336-
commands.append((member_name, member))
322+
commands.Add(name=member_name, member=member)
337323
if value_types.IsValue(member):
338-
values.append((member_name, member))
324+
values.Add(name=member_name, member=member)
339325

340-
indexes = None
341326
if isinstance(component, (list, tuple)) and component:
342327
component_len = len(component)
343-
# WARNING: Note that indexes is a string, whereas the rest are lists.
344328
if component_len < 10:
345-
indexes = ', '.join(str(x) for x in range(component_len))
329+
indexes.Add(name=', '.join(str(x) for x in range(component_len)))
346330
else:
347-
indexes = '0..{max}'.format(max=component_len-1)
331+
indexes.Add(name='0..{max}'.format(max=component_len-1))
348332

349-
return groups, commands, values, indexes
333+
return [groups, commands, values, indexes]
350334

351335

352336
def _GetCurrentCommand(trace=None, include_separators=True):
@@ -420,38 +404,28 @@ def _GetArgDescription(name, docstring_info):
420404
return None
421405

422406

423-
def _GroupUsageDetailsSection(groups):
424-
"""Creates a section tuple for the groups section of the usage details."""
425-
group_item_strings = []
426-
for group_name, group in groups:
427-
group_info = inspectutils.Info(group)
428-
group_item = group_name
429-
if 'docstring_info' in group_info:
430-
group_docstring_info = group_info['docstring_info']
431-
if group_docstring_info:
432-
group_item = _CreateItem(group_name, group_docstring_info.summary)
433-
group_item_strings.append(group_item)
434-
return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings))
435-
436-
437-
def _CommandUsageDetailsSection(commands):
438-
"""Creates a section tuple for the commands section of the usage details."""
439-
command_item_strings = []
440-
for command_name, command in commands:
441-
command_info = inspectutils.Info(command)
442-
command_item = command_name
443-
if 'docstring_info' in command_info:
444-
command_docstring_info = command_info['docstring_info']
445-
if command_docstring_info:
446-
command_item = _CreateItem(command_name, command_docstring_info.summary)
447-
command_item_strings.append(command_item)
448-
return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings))
407+
def _MakeUsageDetailsSection(action_group):
408+
"""Creates a usage details section for the provided action group."""
409+
item_strings = []
410+
for name, member in action_group.GetItems():
411+
info = inspectutils.Info(member)
412+
item = name
413+
docstring_info = info.get('docstring_info')
414+
if (docstring_info
415+
and not custom_descriptions.NeedsCustomDescription(member)):
416+
summary = docstring_info.summary
417+
else:
418+
summary = None
419+
item = _CreateItem(name, summary)
420+
item_strings.append(item)
421+
return (action_group.plural.upper(),
422+
_NewChoicesSection(action_group.name.upper(), item_strings))
449423

450424

451425
def _ValuesUsageDetailsSection(component, values):
452426
"""Creates a section tuple for the values section of the usage details."""
453427
value_item_strings = []
454-
for value_name, value in values:
428+
for value_name, value in values.GetItems():
455429
del value
456430
init_info = inspectutils.Info(component.__class__.__init__)
457431
value_item = None
@@ -590,34 +564,17 @@ def UsageTextForObject(component, trace=None, verbose=False):
590564
command = ''
591565

592566
actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
593-
groups, commands, values, indexes = actions_grouped_by_kind
594567

595568
possible_actions = []
596569
availability_lines = []
597-
if groups:
598-
possible_actions.append('group')
599-
groups_text = _CreateAvailabilityLine(
600-
header='available groups:',
601-
items=[name for name, _ in groups])
602-
availability_lines.append(groups_text)
603-
if commands:
604-
possible_actions.append('command')
605-
commands_text = _CreateAvailabilityLine(
606-
header='available commands:',
607-
items=[name for name, _ in commands])
608-
availability_lines.append(commands_text)
609-
if values:
610-
possible_actions.append('value')
611-
values_text = _CreateAvailabilityLine(
612-
header='available values:',
613-
items=[name for name, _ in values])
614-
availability_lines.append(values_text)
615-
if indexes:
616-
possible_actions.append('index')
617-
indexes_text = _CreateAvailabilityLine(
618-
header='available indexes:',
619-
items=indexes)
620-
availability_lines.append(indexes_text)
570+
for action_group in actions_grouped_by_kind:
571+
if action_group.members:
572+
possible_actions.append(action_group.name)
573+
availability_line = _CreateAvailabilityLine(
574+
header='available {plural}:'.format(plural=action_group.plural),
575+
items=action_group.names
576+
)
577+
availability_lines.append(availability_line)
621578

622579
if possible_actions:
623580
possible_actions_string = ' <{actions}>'.format(
@@ -641,3 +598,21 @@ def _CreateAvailabilityLine(header, items,
641598
indented_items_text = formatting.Indent(items_text, spaces=items_indent)
642599
indented_header = formatting.Indent(header, spaces=header_indent)
643600
return indented_header + indented_items_text[len(indented_header):] + '\n'
601+
602+
603+
class ActionGroup(object):
604+
"""A group of actions of the same kind."""
605+
606+
def __init__(self, name, plural):
607+
self.name = name
608+
self.plural = plural
609+
self.names = []
610+
self.members = []
611+
612+
def Add(self, name, member=None):
613+
self.names.append(name)
614+
self.members.append(member)
615+
616+
def GetItems(self):
617+
return zip(self.names, self.members)
618+

fire/inspectutils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ class with an __init__ method.
8080
elif inspect.isbuiltin(fn):
8181
# If the function is a bound builtin, we skip the `self` argument.
8282
skip_arg = fn.__self__ is not None
83+
elif not inspect.isfunction(fn):
84+
# The purpose of this else clause is to set skip_arg for callable objects.
85+
skip_arg = True
8386
return fn, skip_arg
8487

8588

fire/test_components.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,14 @@ def matching_names(self):
306306
class CallableWithPositionalArgs(object):
307307
"""Test class for supporting callable."""
308308

309+
TEST = 1
310+
309311
def __call__(self, x, y):
310312
return x + y
311313

314+
def foo(self, x):
315+
return x + 1
316+
312317

313318
NamedTuplePoint = collections.namedtuple('NamedTuplePoint', ['x', 'y'])
314319

0 commit comments

Comments
 (0)