Skip to content

Commit 8de8836

Browse files
dbiebercopybara-github
authored andcommitted
Support for callable objects in usage test. Refactor of usage text.
PiperOrigin-RevId: 260041306 Change-Id: Id14ddd3a935627a068ff4f4e3523e9cd9d90ce89
1 parent ba22c78 commit 8de8836

File tree

5 files changed

+160
-121
lines changed

5 files changed

+160
-121
lines changed

fire/completion.py

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

283283

284-
def MemberVisible(component, name, member, verbose):
284+
def GetClassAttrsDict(component):
285+
"""Gets the attributes of the component class, as a dict with name keys."""
286+
if not inspect.isclass(component):
287+
return None
288+
class_attrs_list = inspect.classify_class_attrs(component)
289+
return {
290+
class_attr.name: class_attr
291+
for class_attr in class_attrs_list
292+
}
293+
294+
295+
def MemberVisible(component, name, member, class_attrs=None, verbose=False):
285296
"""Returns whether a member should be included in auto-completion or help.
286297
287298
Determines whether a member of an object with the specified name should be
@@ -297,6 +308,8 @@ def MemberVisible(component, name, member, verbose):
297308
component: The component containing the member.
298309
name: The name of the member.
299310
member: The member itself.
311+
class_attrs: (optional) If component is a class, provide this as:
312+
GetClassAttrsDict(component). If not provided, it will be computed.
300313
verbose: Whether to include private members.
301314
Returns
302315
A boolean value indicating whether the member should be included.
@@ -310,6 +323,13 @@ def MemberVisible(component, name, member, verbose):
310323
if inspect.ismodule(member) and member is six:
311324
# TODO(dbieber): Determine more generally which modules to hide.
312325
return False
326+
if inspect.isclass(component):
327+
# If class_attrs has not been provided, compute it.
328+
if class_attrs is None:
329+
class_attrs = GetClassAttrsDict(class_attrs)
330+
class_attr = class_attrs.get(name)
331+
if class_attr and class_attr.kind == 'method':
332+
return False
313333
if (six.PY2 and inspect.isfunction(component)
314334
and name in ('func_closure', 'func_code', 'func_defaults',
315335
'func_dict', 'func_doc', 'func_globals', 'func_name')):
@@ -322,14 +342,20 @@ def MemberVisible(component, name, member, verbose):
322342
return True # Default to including the member
323343

324344

325-
def _Members(component, verbose=False):
345+
def VisibleMembers(component, class_attrs=None, verbose=False):
326346
"""Returns a list of the members of the given component.
327347
328348
If verbose is True, then members starting with _ (normally ignored) are
329349
included.
330350
331351
Args:
332352
component: The component whose members to list.
353+
class_attrs: (optional) If component is a class, you may provide this as:
354+
GetClassAttrsDict(component). If not provided, it will be computed.
355+
If provided, this determines how class members will be treated for
356+
visibility. In particular, methods are generally hidden for
357+
non-instantiated classes, but if you wish them to be shown (e.g. for
358+
completion scripts) then pass in a different class_attr for them.
333359
verbose: Whether to include private members.
334360
Returns:
335361
A list of tuples (member_name, member) of all members of the component.
@@ -339,10 +365,13 @@ def _Members(component, verbose=False):
339365
else:
340366
members = inspect.getmembers(component)
341367

368+
# If class_attrs has not been provided, compute it.
369+
if class_attrs is None:
370+
class_attrs = GetClassAttrsDict(component)
342371
return [
343-
(member_name, member)
344-
for member_name, member in members
345-
if MemberVisible(component, member_name, member, verbose)
372+
(member_name, member) for member_name, member in members
373+
if MemberVisible(component, member_name, member, class_attrs=class_attrs,
374+
verbose=verbose)
346375
]
347376

348377

@@ -386,7 +415,7 @@ def Completions(component, verbose=False):
386415

387416
return [
388417
_FormatForCommand(member_name)
389-
for member_name, unused_member in _Members(component, verbose)
418+
for member_name, _ in VisibleMembers(component, verbose=verbose)
390419
]
391420

392421

@@ -427,15 +456,17 @@ def _Commands(component, depth=3):
427456
Only traverses the member DAG up to a depth of depth.
428457
"""
429458
if inspect.isroutine(component) or inspect.isclass(component):
430-
for completion in Completions(component):
459+
for completion in Completions(component, verbose=False):
431460
yield (completion,)
432461
if inspect.isroutine(component):
433462
return # Don't descend into routines.
434463

435464
if depth < 1:
436465
return
437466

438-
for member_name, member in _Members(component):
467+
# By setting class_attrs={} we don't hide methods in completion.
468+
for member_name, member in VisibleMembers(component, class_attrs={},
469+
verbose=False):
439470
# TODO(dbieber): Also skip components we've already seen.
440471
member_name = _FormatForCommand(member_name)
441472

fire/core.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,12 @@ def _DictAsString(result, verbose=False):
306306
# We need to do 2 iterations over the items in the result dict
307307
# 1) Getting visible items and the longest key for output formatting
308308
# 2) Actually construct the output lines
309-
result_visible = {key: value for key, value in result.items()
310-
if completion.MemberVisible(result, key, value, verbose)}
309+
class_attrs = completion.GetClassAttrsDict(result)
310+
result_visible = {
311+
key: value for key, value in result.items()
312+
if completion.MemberVisible(result, key, value,
313+
class_attrs=class_attrs, verbose=verbose)
314+
}
311315

312316
if not result_visible:
313317
return '{}'
@@ -317,7 +321,8 @@ def _DictAsString(result, verbose=False):
317321

318322
lines = []
319323
for key, value in result.items():
320-
if completion.MemberVisible(result, key, value, verbose):
324+
if completion.MemberVisible(result, key, value, class_attrs=class_attrs,
325+
verbose=verbose):
321326
line = format_string.format(key=str(key) + ':',
322327
value=_OneLineResult(value))
323328
lines.append(line)

fire/helptext.py

Lines changed: 81 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
from __future__ import division
3434
from __future__ import print_function
3535

36-
import inspect
37-
3836
from fire import completion
3937
from fire import custom_descriptions
4038
from fire import decorators
@@ -319,7 +317,7 @@ def _GetActionsGroupedByKind(component, verbose=False):
319317
values = ActionGroup(name='value', plural='values')
320318
indexes = ActionGroup(name='index', plural='indexes')
321319

322-
members = completion._Members(component, verbose) # pylint: disable=protected-access
320+
members = completion.VisibleMembers(component, verbose=verbose)
323321
for member_name, member in members:
324322
member_name = str(member_name)
325323
if value_types.IsGroup(member):
@@ -456,14 +454,7 @@ def _NewChoicesSection(name, choices):
456454

457455

458456
def UsageText(component, trace=None, verbose=False):
459-
if inspect.isroutine(component) or inspect.isclass(component):
460-
return UsageTextForFunction(component, trace, verbose)
461-
else:
462-
return UsageTextForObject(component, trace, verbose)
463-
464-
465-
def UsageTextForFunction(component, trace=None, verbose=False):
466-
"""Returns usage text for function objects.
457+
"""Returns usage text for the given component.
467458
468459
Args:
469460
component: The component to determine the usage text for.
@@ -473,13 +464,12 @@ def UsageTextForFunction(component, trace=None, verbose=False):
473464
Returns:
474465
String suitable for display in an error screen.
475466
"""
476-
del verbose # Unused.
477-
478-
output_template = """Usage: {current_command} {args_and_flags}
467+
output_template = """Usage: {continued_command}
479468
{availability_lines}
480469
For detailed information on this command, run:
481-
{current_command}{hyphen_hyphen} --help"""
470+
{help_command}"""
482471

472+
# Get the command so far:
483473
if trace:
484474
command = trace.GetCommand()
485475
needs_separating_hyphen_hyphen = trace.NeedsSeparatingHyphenHyphen()
@@ -490,13 +480,67 @@ def UsageTextForFunction(component, trace=None, verbose=False):
490480
if not command:
491481
command = ''
492482

483+
# Build the continuations for the command:
484+
continued_command = command
485+
493486
spec = inspectutils.GetFullArgSpec(component)
487+
metadata = decorators.GetMetadata(component)
488+
489+
# Usage for objects.
490+
actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
491+
possible_actions = _GetPossibleActions(actions_grouped_by_kind)
492+
493+
continuations = []
494+
if possible_actions:
495+
continuations.append(_GetPossibleActionsUsageString(possible_actions))
496+
497+
availability_lines = _UsageAvailabilityLines(actions_grouped_by_kind)
498+
499+
if callable(component):
500+
callable_items = _GetCallableUsageItems(spec, metadata)
501+
continuations.append(' '.join(callable_items))
502+
availability_lines.extend(_GetCallableAvailabilityLines(spec))
503+
504+
if continuations:
505+
continued_command += ' ' + ' | '.join(continuations)
506+
help_command = (
507+
command
508+
+ (' -- ' if needs_separating_hyphen_hyphen else ' ')
509+
+ '--help'
510+
)
511+
512+
return output_template.format(
513+
continued_command=continued_command,
514+
availability_lines=''.join(availability_lines),
515+
help_command=help_command)
516+
517+
518+
def _GetPossibleActionsUsageString(possible_actions):
519+
if possible_actions:
520+
return '<{actions}>'.format(actions='|'.join(possible_actions))
521+
return None
522+
523+
524+
def _UsageAvailabilityLines(actions_grouped_by_kind):
525+
availability_lines = []
526+
for action_group in actions_grouped_by_kind:
527+
if action_group.members:
528+
availability_line = _CreateAvailabilityLine(
529+
header='available {plural}:'.format(plural=action_group.plural),
530+
items=action_group.names
531+
)
532+
availability_lines.append(availability_line)
533+
return availability_lines
534+
535+
536+
def _GetCallableUsageItems(spec, metadata):
537+
"""A list of elements that comprise the usage summary for a callable."""
494538
args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)]
495539
args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
496540

497541
# Check if positional args are allowed. If not, show flag syntax for args.
498-
metadata = decorators.GetMetadata(component)
499542
accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS)
543+
500544
if not accepts_positional_args:
501545
items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper())
502546
for arg in args_with_no_defaults]
@@ -507,93 +551,38 @@ def UsageTextForFunction(component, trace=None, verbose=False):
507551
if args_with_defaults or spec.kwonlyargs or spec.varkw:
508552
items.append('<flags>')
509553

554+
if spec.varargs:
555+
items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
556+
557+
return items
558+
559+
560+
def _GetCallableAvailabilityLines(spec):
561+
"""The list of availability lines for a callable for use in a usage string."""
562+
args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):]
563+
564+
# TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args.
510565
optional_flags = [('--' + flag) for flag in args_with_defaults]
511566
required_flags = [('--' + flag) for flag in spec.kwonlyargs]
512567

513568
# Flags section:
514569
availability_lines = []
515570
if optional_flags:
516571
availability_lines.append(
517-
_CreateAvailabilityLine(header='Optional flags:', items=optional_flags,
518-
header_indent=0))
572+
_CreateAvailabilityLine(header='optional flags:', items=optional_flags,
573+
header_indent=2))
519574
if required_flags:
520575
availability_lines.append(
521-
_CreateAvailabilityLine(header='Required flags:', items=required_flags,
522-
header_indent=0))
576+
_CreateAvailabilityLine(header='required flags:', items=required_flags,
577+
header_indent=2))
523578
if spec.varkw:
524-
additional_flags = ('Additional flags are accepted.'
579+
additional_flags = ('additional flags are accepted'
525580
if optional_flags or required_flags else
526-
'Flags are accepted.')
527-
availability_lines.append(additional_flags + '\n')
528-
529-
if availability_lines:
530-
# Start the section with blank lines.
531-
availability_lines.insert(0, '\n')
532-
533-
if spec.varargs:
534-
items.append('[{varargs}]...'.format(varargs=spec.varargs.upper()))
535-
536-
args_and_flags = ' '.join(items)
537-
538-
hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else ''
539-
540-
return output_template.format(
541-
current_command=command,
542-
args_and_flags=args_and_flags,
543-
availability_lines=''.join(availability_lines),
544-
hyphen_hyphen=hyphen_hyphen)
545-
546-
547-
def UsageTextForObject(component, trace=None, verbose=False):
548-
"""Returns the usage text for the error screen for an object.
549-
550-
Constructs the usage text for the error screen to inform the user about how
551-
to use the current component.
552-
553-
Args:
554-
component: The component to determine the usage text for.
555-
trace: The Fire trace object containing all metadata of current execution.
556-
verbose: Whether to include private members in the usage text.
557-
Returns:
558-
String suitable for display in error screen.
559-
"""
560-
output_template = """Usage: {current_command}{possible_actions}
561-
{availability_lines}
562-
For detailed information on this command, run:
563-
{current_command} --help"""
564-
if trace:
565-
command = trace.GetCommand()
566-
else:
567-
command = None
568-
569-
if not command:
570-
command = ''
571-
572-
actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose)
573-
574-
possible_actions = []
575-
availability_lines = []
576-
for action_group in actions_grouped_by_kind:
577-
if action_group.members:
578-
possible_actions.append(action_group.name)
579-
availability_line = _CreateAvailabilityLine(
580-
header='available {plural}:'.format(plural=action_group.plural),
581-
items=action_group.names
582-
)
583-
availability_lines.append(availability_line)
584-
585-
if possible_actions:
586-
possible_actions_string = ' <{actions}>'.format(
587-
actions='|'.join(possible_actions))
588-
else:
589-
possible_actions_string = ''
590-
591-
availability_lines_string = ''.join(availability_lines)
592-
593-
return output_template.format(
594-
current_command=command,
595-
possible_actions=possible_actions_string,
596-
availability_lines=availability_lines_string)
581+
'flags are accepted')
582+
availability_lines.append(
583+
_CreateAvailabilityLine(header=additional_flags, items=[],
584+
header_indent=2))
585+
return availability_lines
597586

598587

599588
def _CreateAvailabilityLine(header, items,

0 commit comments

Comments
 (0)