From 979c7887286f276078a5380b27f14fca0a1f4f21 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:39:12 -0700 Subject: [PATCH 001/205] Adds a subclass of dict as a test component. PiperOrigin-RevId: 259358342 Change-Id: I4218e6b702ada87eac1248072ccdaa18977226ba --- fire/test_components.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fire/test_components.py b/fire/test_components.py index 47d343cf..a36e393f 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -388,3 +388,12 @@ def simple_set(): def simple_frozenset(): return frozenset({1, 2, 'three'}) + + +class Subdict(dict): + + pass + + +subdict = Subdict({1: 2, + 'red': 'blue'}) From 149380b6b963fbad4de98b13d694de67867144af Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:41:09 -0700 Subject: [PATCH 002/205] Show help instead of value if any non-value non-collection (list, dict) objects are found PiperOrigin-RevId: 259358731 Change-Id: I12b48571ee4f642b12ec937b768e3877956f2355 --- fire/core.py | 2 +- fire/value_types.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/fire/core.py b/fire/core.py index db18ab65..5f253236 100644 --- a/fire/core.py +++ b/fire/core.py @@ -249,7 +249,7 @@ def _PrintResult(component_trace, verbose=False): print(_OneLineResult(i)) elif inspect.isgeneratorfunction(result): raise NotImplementedError - elif isinstance(result, dict): + elif isinstance(result, dict) and value_types.IsSimpleGroup(result): print(_DictAsString(result, verbose)) elif isinstance(result, tuple): print(_OneLineResult(result)) diff --git a/fire/value_types.py b/fire/value_types.py index 77d05dc7..9cb34f86 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -37,3 +37,22 @@ def IsCommand(component): def IsValue(component): return isinstance(component, VALUE_TYPES) + + +def IsSimpleGroup(component): + """If a group is simple enough, then we treat it as a value in PrintResult. + + Only if a group contains all value types do we consider it simple enough to + print as a value. + + Args: + component: The group to check for value-group status. + Returns: + A boolean indicating if the group should be treated as a value for printing + purposes. + """ + assert isinstance(component, dict) + for unused_key, value in component.items(): + if not IsValue(value) and not isinstance(value, (list, dict)): + return False + return True From 0820e6551a93218362d0914abc68090566265466 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:42:16 -0700 Subject: [PATCH 003/205] Treat Ellipsis, None, and NotImplemented as value types. PiperOrigin-RevId: 259358998 Change-Id: I1afa4a0d97c57ce3b4e89f2b6614663dc2660dfa --- fire/core.py | 5 +++-- fire/value_types.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fire/core.py b/fire/core.py index 5f253236..8e35b3cc 100644 --- a/fire/core.py +++ b/fire/core.py @@ -254,8 +254,9 @@ def _PrintResult(component_trace, verbose=False): elif isinstance(result, tuple): print(_OneLineResult(result)) elif isinstance(result, value_types.VALUE_TYPES): - print(result) - elif result is not None: + if result is not None: + print(result) + else: help_text = helptext.HelpText(result, component_trace, verbose) output = [help_text] Display(output, out=sys.stdout) diff --git a/fire/value_types.py b/fire/value_types.py index 9cb34f86..ad50cb23 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -23,7 +23,8 @@ import six -VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex) +VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex, + type(Ellipsis), type(None), type(NotImplemented)) def IsGroup(component): From ee210d16ef58b5bd9d80feef3754c0c56fdcbe72 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:43:21 -0700 Subject: [PATCH 004/205] Handle printing functions and modules a little more cleanly. PiperOrigin-RevId: 259359253 Change-Id: Ib61b3a2b9f0a4339cb396806e8c2aa77bee27223 --- fire/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fire/core.py b/fire/core.py index 8e35b3cc..99ae2d8c 100644 --- a/fire/core.py +++ b/fire/core.py @@ -334,6 +334,14 @@ def _OneLineResult(result): if isinstance(result, six.string_types): return str(result).replace('\n', ' ') + # TODO(dbieber): Show a small amount of usage information about the function + # or module if it fits cleanly on the line. + if inspect.isfunction(result): + return ''.format(name=result.__name__) + + if inspect.ismodule(result): + return ''.format(name=result.__name__) + try: # Don't force conversion to ascii. return json.dumps(result, ensure_ascii=False) From b9c32df6519a4cd49b9c68ab29f78f96ed9e092e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:44:36 -0700 Subject: [PATCH 005/205] Only sure Future Features in verbose mode, and only show six in verbose mode. Unifies verbosity logic. PiperOrigin-RevId: 259359499 Change-Id: I44cec5968b54faf88fda9ddbe36c499b64d1f4a1 --- fire/completion.py | 22 ++++++++++++++-------- fire/core.py | 12 ++---------- fire/helptext.py | 7 +++++-- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index cda7c936..705ef586 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -281,30 +281,36 @@ def _FishScript(name, commands, default_options=None): ) -def _IncludeMember(name, verbose): +def MemberVisible(name, member, verbose): """Returns whether a member should be included in auto-completion or help. Determines whether a member of an object with the specified name should be included in auto-completion or help text(both usage and detailed help). - If the member starts with '__', it will always be excluded. If the member + If the member name starts with '__', it will always be excluded. If it starts with only one '_', it will be included for all non-string types. If - verbose is True, the members, including the private members, are always - included. + verbose is True, the members, including the private members, are included. + + When not in verbose mode, some modules and functions are excluded as well. Args: name: The name of the member. + member: The member itself. verbose: Whether to include private members. Returns A boolean value indicating whether the member should be included. - """ - if isinstance(name, six.string_types) and name[:2] == '__': + if isinstance(name, six.string_types) and name.startswith('__'): return False if verbose: return True + if isinstance(member, type(absolute_import)): + return False + if inspect.ismodule(member) and member is six: + # TODO(dbieber): Determine more generally which modules to hide. + return False if isinstance(name, six.string_types): - return name and name[0] != '_' + return not name.startswith('_') return True # Default to including the member @@ -328,7 +334,7 @@ def _Members(component, verbose=False): return [ (member_name, member) for member_name, member in members - if _IncludeMember(member_name, verbose) + if MemberVisible(member_name, member, verbose) ] diff --git a/fire/core.py b/fire/core.py index 99ae2d8c..3ba081c2 100644 --- a/fire/core.py +++ b/fire/core.py @@ -303,7 +303,7 @@ def _DictAsString(result, verbose=False): # 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 completion.MemberVisible(key, value, verbose)} if not result_visible: return '{}' @@ -313,21 +313,13 @@ def _DictAsString(result, verbose=False): lines = [] for key, value in result.items(): - if _ComponentVisible(key, verbose): + if completion.MemberVisible(key, value, verbose): line = format_string.format(key=str(key) + ':', value=_OneLineResult(value)) lines.append(line) return '\n'.join(lines) -def _ComponentVisible(component, verbose=False): - """Returns whether a component should be visible in the output.""" - return ( - verbose - or not isinstance(component, six.string_types) - or not component.startswith('_')) - - def _OneLineResult(result): """Returns result serialized to a single line string.""" # TODO(dbieber): Ensure line is fewer than eg 120 characters. diff --git a/fire/helptext.py b/fire/helptext.py index af3372e2..23db4542 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -410,21 +410,24 @@ def _NewChoicesSection(name, choices): def UsageText(component, trace=None, verbose=False): if inspect.isroutine(component) or inspect.isclass(component): - return UsageTextForFunction(component, trace) + return UsageTextForFunction(component, trace, verbose) else: return UsageTextForObject(component, trace, verbose) -def UsageTextForFunction(component, trace=None): +def UsageTextForFunction(component, trace=None, verbose=False): """Returns usage text for function objects. Args: component: The component to determine the usage text for. trace: The Fire trace object containing all metadata of current execution. + verbose: Whether to display the usage text in verbose mode. Returns: String suitable for display in an error screen. """ + del verbose # Unused. + output_template = """Usage: {current_command} {args_and_flags} {availability_lines} For detailed information on this command, run: From 402ea3fc4cd6e9722e1e6137b170ed323b215603 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 10:46:34 -0700 Subject: [PATCH 006/205] Add indexes section to help and usage information for lists and tuples (including namedtuples). PiperOrigin-RevId: 259359939 Change-Id: I4bc6d8267cf2203bb1c252c3b245abe5d69d39c5 --- fire/helptext.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/fire/helptext.py b/fire/helptext.py index 23db4542..33642430 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -324,6 +324,16 @@ def HelpTextForObject(component, info, trace=None, verbose=False): usage_details_section = ValuesUsageDetailsSection(component, values) usage_details_sections.append(usage_details_section) + if isinstance(component, (list, tuple)) and component: + possible_actions.append('INDEX') + component_len = len(component) + if component_len < 10: + indexes_strings = [', '.join(str(x) for x in range(component_len))] + else: + indexes_strings = ['0..{max}'.format(max=component_len-1)] + usage_details_sections.append( + ('INDEXES', _NewChoicesSection('INDEX', indexes_strings))) + possible_actions_string = ' | '.join( formatting.Underline(action) for action in possible_actions) @@ -549,6 +559,18 @@ def UsageTextForObject(component, trace=None, verbose=False): items=values) availability_lines.append(values_text) + if isinstance(component, (list, tuple)) and component: + possible_actions.append('index') + component_len = len(component) + if component_len < 10: + indexes_strings = [str(x) for x in range(component_len)] + else: + indexes_strings = ['0..{max}'.format(max=component_len-1)] + indexes_text = _CreateAvailabilityLine( + header='available indexes:', + items=indexes_strings) + availability_lines.append(indexes_text) + if possible_actions: possible_actions_string = ' <{actions}>'.format( actions='|'.join(possible_actions)) From 34c82659550d964b711a6d4fff734f7be13a2836 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 11:22:06 -0700 Subject: [PATCH 007/205] Clean up subdict name for linter. PiperOrigin-RevId: 259368017 Change-Id: I0dc86fed9107660d439c0d2fe155b9f1348283d3 --- fire/test_components.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fire/test_components.py b/fire/test_components.py index a36e393f..f81c9a0f 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -391,9 +391,8 @@ def simple_frozenset(): class Subdict(dict): - - pass + """A subclass of dict, for testing purposes.""" -subdict = Subdict({1: 2, - 'red': 'blue'}) +# An example subdict. +SUBDICT = Subdict({1: 2, 'red': 'blue'}) From b80d596ae8b20519c542d8cc2cb03318f7098b2d Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 11:36:28 -0700 Subject: [PATCH 008/205] Unifies function and object help string building to make supporting callable functions more natural in a follow up CL. Refactor of helptext module to reduce redundant action collection logic and organize sections more consistently. PiperOrigin-RevId: 259371261 Change-Id: I5959988e9c6d656bb0e2ac46c786be5dac750151 --- fire/helptext.py | 490 +++++++++++++++++++++--------------------- fire/helptext_test.py | 10 +- 2 files changed, 253 insertions(+), 247 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 33642430..8f4fe97b 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -12,11 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""helptext is the new, work in progress, help text module for Fire. - -This is a fork of, and is intended to replace, helputils. - -Utility for producing help strings for use in Fire CLIs. +"""Utilities for producing help strings for use in Fire CLIs. Can produce help strings suitable for display in Fire CLIs for any type of Python object, module, class, or function. @@ -46,103 +42,201 @@ from fire import value_types -def GetArgsAngFlags(component): - """Returns all types of arguments and flags of a component.""" +def HelpText(component, trace=None, verbose=False): + """Gets the help string for the current component, suitalbe for a help screen. + + Args: + component: The component to construct the help string for. + trace: The Fire trace of the command so far. The command executed so far + can be extracted from this trace. + verbose: Whether to include private members in the help screen. + + Returns: + The full help screen as a string. + """ + # Preprocessing needed to create the sections: + info = inspectutils.Info(component) + actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose) spec = inspectutils.GetFullArgSpec(component) - args = spec.args - if spec.defaults is None: - num_defaults = 0 - else: - num_defaults = len(spec.defaults) - args_with_no_defaults = args[:len(args) - num_defaults] - args_with_defaults = args[len(args) - num_defaults:] - flags = args_with_defaults + spec.kwonlyargs - return args_with_no_defaults, args_with_defaults, flags + metadata = decorators.GetMetadata(component) + # Sections: + name_section = _NameSection(info, trace=trace, verbose=verbose) + synopsis_section = _SynopsisSection( + component, actions_grouped_by_kind, spec, metadata, trace=trace) + description_section = _DescriptionSection(info) + # TODO(dbieber): Add returns and raises sections for functions. -def GetSummaryAndDescription(docstring_info): - """Retrieves summary and description for help text generation.""" + if inspect.isroutine(component) or inspect.isclass(component): + # For functions (ARGUMENTS / POSITIONAL ARGUMENTS, FLAGS) + args_and_flags_sections, notes_sections = _ArgsAndFlagsSections( + info, spec, metadata) + usage_details_sections = [] + else: + # For objects (GROUPS, COMMANDS, VALUES, INDEXES) + # TODO(dbieber): Show callable function usage in help text. + args_and_flags_sections = [] + notes_sections = [] + usage_details_sections = _UsageDetailsSections(component, + actions_grouped_by_kind) + + sections = ( + [name_section, synopsis_section, description_section] + + args_and_flags_sections + + usage_details_sections + + notes_sections + ) + return '\n\n'.join( + _CreateOutputSection(*section) + for section in sections if section is not None + ) - # To handle both empty string and None - summary = docstring_info.summary if docstring_info.summary else None - description = ( - docstring_info.description if docstring_info.description else None) - return summary, description +def _NameSection(info, trace=None, verbose=False): + """The "Name" section of the help string.""" + # Only include separators in the name in verbose mode. + current_command = _GetCurrentCommand(trace, include_separators=verbose) + summary = _GetSummary(info) -def GetCurrentCommand(trace=None, include_separators=True): - """Returns current command for the purpose of generating help text.""" - if trace: - current_command = trace.GetCommand(include_separators=include_separators) + if summary: + text = current_command + ' - ' + summary else: - current_command = '' - return current_command + text = current_command + return ('NAME', text) -def HelpText(component, trace=None, verbose=False): - info = inspectutils.Info(component) +def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, + trace=None): + """The "Synopsis" section of the help string.""" + current_command = _GetCurrentCommand(trace=trace, include_separators=True) + + # TODO(dbieber): Support callable functions. if inspect.isroutine(component) or inspect.isclass(component): - return HelpTextForFunction(component, info, trace=trace, verbose=verbose) + # For function: + args_and_flags = _GetArgsAndFlagsString(spec, metadata) + synopsis_section_template = '{current_command} {args_and_flags}' + text = synopsis_section_template.format( + current_command=current_command, args_and_flags=args_and_flags) + else: - return HelpTextForObject(component, info, trace=trace, verbose=verbose) + # For object: + possible_actions_string = _GetPossibleActionsString(actions_grouped_by_kind) + synopsis_template = '{current_command} {possible_actions}' + text = synopsis_template.format( + current_command=current_command, + possible_actions=possible_actions_string) + + return ('SYNOPSIS', text) + + +def _DescriptionSection(info): + """The "Description" sections of the help string.""" + summary = _GetSummary(info) + description = _GetDescription(info) + # Returns the description if available. If not, returns the summary. + # If neither are available, returns None. + text = description or summary or None + if text: + return ('DESCRIPTION', text) + else: + return None -def GetDescriptionSectionText(summary, description): - """Returns description section text based on the input docstring info. +def _ArgsAndFlagsSections(info, spec, metadata): + """The "Args and Flags" sections of the help string.""" + args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] + args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] + flags = args_with_defaults + spec.kwonlyargs - Returns the string that should be used as description section based on the - input. The logic is the following: If there's description available, use it. - Otherwise, use summary if available. If neither description or summary is - available, returns None. + # Check if positional args are allowed. If not, require flag syntax for args. + accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) - Args: - summary: summary found in object summary - description: description found in object docstring + args_and_flags_sections = [] + notes_sections = [] - Returns: - String for the description section in help screen. - """ - if not (description or summary): - return None + docstring_info = info['docstring_info'] - if description: - return description - else: - return summary + arg_items = [ + _CreateArgItem(arg, docstring_info) + for arg in args_with_no_defaults + ] + if arg_items: + title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS' + arguments_section = (title, '\n'.join(arg_items).rstrip('\n')) + args_and_flags_sections.append(arguments_section) + if accepts_positional_args: + notes_sections.append( + ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') + ) + + flag_items = [ + _CreateFlagItem(flag, docstring_info) + for flag in flags + ] + if flag_items: + flags_section = ('FLAGS', '\n'.join(flag_items)) + args_and_flags_sections.append(flags_section) -def HelpTextForFunction(component, info, trace=None, verbose=False): - """Returns detail help text for a function component. + return args_and_flags_sections, notes_sections - Args: - component: Current component to generate help text for. - info: Info containing metadata of component. - trace: FireTrace object that leads to current component. - verbose: Whether to display help text in verbose mode. - Returns: - Formatted help text for display. - """ - # TODO(joejoevictor): Implement verbose related output - del verbose +def _UsageDetailsSections(component, actions_grouped_by_kind): + """The usage details sections of the help string.""" + groups, commands, values, indexes = actions_grouped_by_kind + + usage_details_sections = [] + + if groups: + usage_details_section = _GroupUsageDetailsSection(groups) + usage_details_sections.append(usage_details_section) + if commands: + usage_details_section = _CommandUsageDetailsSection(commands) + usage_details_sections.append(usage_details_section) + if values: + usage_details_section = _ValuesUsageDetailsSection(component, values) + usage_details_sections.append(usage_details_section) + if indexes: + usage_details_sections.append( + ('INDEXES', _NewChoicesSection('INDEX', [indexes]))) + + return usage_details_sections - current_command = GetCurrentCommand(trace) - current_command_without_separator = GetCurrentCommand( - trace, include_separators=False) - summary, description = GetSummaryAndDescription(info['docstring_info']) - args_with_no_defaults, args_with_defaults, flags = GetArgsAngFlags(component) - del args_with_defaults +def _GetSummary(info): + docstring_info = info['docstring_info'] + return docstring_info.summary if docstring_info.summary else None + - # Name section - name_section_template = '{current_command}{command_summary}' - command_summary_str = ' - ' + summary if summary else '' - name_section = name_section_template.format( - current_command=current_command_without_separator, - command_summary=command_summary_str) +def _GetDescription(info): + docstring_info = info['docstring_info'] + return docstring_info.description if docstring_info.description else None + + +def _GetArgsAndFlagsString(spec, metadata): + """The args and flags string for showing how to call a function. + + If positional arguments are accepted, the args will be shown as positional. + E.g. "ARG1 ARG2 [--flag=FLAG]" + + If positional arguments are disallowed, the args will be shown with flags + syntax. + E.g. "--arg1=ARG1 [--flag=FLAG]" + + Args: + spec: The full arg spec for the component to construct the args and flags + string for. + metadata: Metadata for the component, including whether it accepts + positional arguments. + + Returns: + The constructed args and flags string. + """ + args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] + args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] + flags = args_with_defaults + spec.kwonlyargs # Check if positional args are allowed. If not, require flag syntax for args. - metadata = decorators.GetMetadata(component) accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) arg_and_flag_strings = [] @@ -159,61 +253,68 @@ def HelpTextForFunction(component, info, trace=None, verbose=False): flag_string_template = '[--{flag_name}={flag_name_upper}]' if flags: - flag_strings = [ - flag_string_template.format( - flag_name=formatting.Underline(flag), flag_name_upper=flag.upper()) - for flag in flags - ] - arg_and_flag_strings.extend(flag_strings) - args_and_flags = ' '.join(arg_and_flag_strings) - - # Synopsis section - synopsis_section_template = '{current_command} {args_and_flags}' - synopsis_section = synopsis_section_template.format( - current_command=current_command, args_and_flags=args_and_flags) - - # Description section - command_description = GetDescriptionSectionText(summary, description) - description_sections = [] - if command_description: - description_sections.append(('DESCRIPTION', command_description)) - - # Positional arguments and flags section - docstring_info = info['docstring_info'] - args_and_flags_sections = [] - notes_sections = [] + for flag in flags: + flag_string = flag_string_template.format( + flag_name=formatting.Underline(flag), + flag_name_upper=flag.upper()) + arg_and_flag_strings.append(flag_string) + return ' '.join(arg_and_flag_strings) - arg_items = [ - _CreateArgItem(arg, docstring_info) - for arg in args_with_no_defaults - ] - if arg_items: - title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS' - arguments_section = (title, '\n'.join(arg_items).rstrip('\n')) - args_and_flags_sections.append(arguments_section) - if accepts_positional_args: - notes_sections.append( - ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') - ) - flag_items = [ - _CreateFlagItem(flag, docstring_info) - for flag in flags - ] +def _GetPossibleActionsString(actions_grouped_by_kind): + """A help screen string listing the possible action kinds available.""" + groups, commands, values, indexes = actions_grouped_by_kind - if flag_items: - flags_section = ('FLAGS', '\n'.join(flag_items)) - args_and_flags_sections.append(flags_section) + possible_actions = [] + if groups: + possible_actions.append('GROUP') + if commands: + possible_actions.append('COMMAND') + if values: + possible_actions.append('VALUE') + if indexes: + possible_actions.append('INDEX') - output_sections = [ - ('NAME', name_section), - ('SYNOPSIS', synopsis_section), - ] + description_sections + args_and_flags_sections + notes_sections + possible_actions_string = ' | '.join( + formatting.Underline(action) for action in possible_actions) + return possible_actions_string - return '\n\n'.join( - _CreateOutputSection(name, content) - for name, content in output_sections - ) + +def _GetActionsGroupedByKind(component, verbose=False): + """Gets lists of available actions, grouped by action kind.""" + groups = [] + commands = [] + values = [] + + members = completion._Members(component, verbose) # pylint: disable=protected-access + for member_name, member in members: + member_name = str(member_name) + if value_types.IsGroup(member): + groups.append((member_name, member)) + if value_types.IsCommand(member): + commands.append((member_name, member)) + if value_types.IsValue(member): + values.append((member_name, member)) + + indexes = None + if isinstance(component, (list, tuple)) and component: + component_len = len(component) + # WARNING: Note that indexes is a string, whereas the rest are lists. + if component_len < 10: + indexes = ', '.join(str(x) for x in range(component_len)) + else: + indexes = '0..{max}'.format(max=component_len-1) + + return groups, commands, values, indexes + + +def _GetCurrentCommand(trace=None, include_separators=True): + """Returns current command for the purpose of generating help text.""" + if trace: + current_command = trace.GetCommand(include_separators=include_separators) + else: + current_command = '' + return current_command def _CreateOutputSection(name, content): @@ -274,94 +375,7 @@ def _CreateItem(name, description, indent=2): description=formatting.Indent(description, indent)) -def HelpTextForObject(component, info, trace=None, verbose=False): - """Generates help text for python objects. - - Args: - component: Current component to generate help text for. - info: Info containing metadata of component. - trace: FireTrace object that leads to current component. - verbose: Whether to display help text in verbose mode. - - Returns: - Formatted help text for display. - """ - current_command = GetCurrentCommand(trace) - current_command_without_separator = GetCurrentCommand( - trace, include_separators=False) - docstring_info = info['docstring_info'] - command_summary = docstring_info.summary if docstring_info.summary else '' - command_description = GetDescriptionSectionText(docstring_info.summary, - docstring_info.description) - groups = [] - commands = [] - values = [] - members = completion._Members(component, verbose) # pylint: disable=protected-access - for member_name, member in members: - if value_types.IsGroup(member): - groups.append((member_name, member)) - if value_types.IsCommand(member): - commands.append((member_name, member)) - if value_types.IsValue(member): - values.append((member_name, member)) - - usage_details_sections = [] - possible_actions = [] - # TODO(joejoevictor): Add global flags to here. Also, if it's a callable, - # there will be additional flags. - possible_flags = '' - - if groups: - possible_actions.append('GROUP') - usage_details_section = GroupUsageDetailsSection(groups) - usage_details_sections.append(usage_details_section) - if commands: - possible_actions.append('COMMAND') - usage_details_section = CommandUsageDetailsSection(commands) - usage_details_sections.append(usage_details_section) - if values: - possible_actions.append('VALUE') - usage_details_section = ValuesUsageDetailsSection(component, values) - usage_details_sections.append(usage_details_section) - - if isinstance(component, (list, tuple)) and component: - possible_actions.append('INDEX') - component_len = len(component) - if component_len < 10: - indexes_strings = [', '.join(str(x) for x in range(component_len))] - else: - indexes_strings = ['0..{max}'.format(max=component_len-1)] - usage_details_sections.append( - ('INDEXES', _NewChoicesSection('INDEX', indexes_strings))) - - possible_actions_string = ' | '.join( - formatting.Underline(action) for action in possible_actions) - - synopsis_template = '{current_command} {possible_actions}{possible_flags}' - synopsis_string = synopsis_template.format( - current_command=current_command, - possible_actions=possible_actions_string, - possible_flags=possible_flags) - - description_sections = [] - if command_description: - description_sections.append(('DESCRIPTION', command_description)) - - name_line = '{current_command} - {command_summary}'.format( - current_command=current_command_without_separator, - command_summary=command_summary) - output_sections = [ - ('NAME', name_line), - ('SYNOPSIS', synopsis_string), - ] + description_sections + usage_details_sections - - return '\n\n'.join( - _CreateOutputSection(name, content) - for name, content in output_sections - ) - - -def GroupUsageDetailsSection(groups): +def _GroupUsageDetailsSection(groups): """Creates a section tuple for the groups section of the usage details.""" group_item_strings = [] for group_name, group in groups: @@ -376,7 +390,7 @@ def GroupUsageDetailsSection(groups): return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings)) -def CommandUsageDetailsSection(commands): +def _CommandUsageDetailsSection(commands): """Creates a section tuple for the commands section of the usage details.""" command_item_strings = [] for command_name, command in commands: @@ -391,7 +405,7 @@ def CommandUsageDetailsSection(commands): return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)) -def ValuesUsageDetailsSection(component, values): +def _ValuesUsageDetailsSection(component, values): """Creates a section tuple for the values section of the usage details.""" value_item_strings = [] for value_name, value in values: @@ -490,15 +504,6 @@ def UsageTextForFunction(component, trace=None, verbose=False): hyphen_hyphen=hyphen_hyphen) -def _CreateAvailabilityLine(header, items, - header_indent=2, items_indent=25, line_length=80): - items_width = line_length - items_indent - items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width)) - indented_items_text = formatting.Indent(items_text, spaces=items_indent) - indented_header = formatting.Indent(header, spaces=header_indent) - return indented_header + indented_items_text[len(indented_header):] + '\n' - - def UsageTextForObject(component, trace=None, verbose=False): """Returns the usage text for the error screen for an object. @@ -524,19 +529,8 @@ def UsageTextForObject(component, trace=None, verbose=False): if not command: command = '' - groups = [] - commands = [] - values = [] - - members = completion._Members(component, verbose) # pylint: disable=protected-access - for member_name, member in members: - member_name = str(member_name) - if value_types.IsGroup(member): - groups.append(member_name) - if value_types.IsCommand(member): - commands.append(member_name) - if value_types.IsValue(member): - values.append(member_name) + actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose) + groups, commands, values, indexes = actions_grouped_by_kind possible_actions = [] availability_lines = [] @@ -558,17 +552,11 @@ def UsageTextForObject(component, trace=None, verbose=False): header='available values:', items=values) availability_lines.append(values_text) - - if isinstance(component, (list, tuple)) and component: + if indexes: possible_actions.append('index') - component_len = len(component) - if component_len < 10: - indexes_strings = [str(x) for x in range(component_len)] - else: - indexes_strings = ['0..{max}'.format(max=component_len-1)] indexes_text = _CreateAvailabilityLine( header='available indexes:', - items=indexes_strings) + items=[(indexes, None)]) availability_lines.append(indexes_text) if possible_actions: @@ -583,3 +571,13 @@ def UsageTextForObject(component, trace=None, verbose=False): current_command=command, possible_actions=possible_actions_string, availability_lines=availability_lines_string) + + +def _CreateAvailabilityLine(header, items, + header_indent=2, items_indent=25, line_length=80): + items_width = line_length - items_indent + item_names = [item[0] for item in items] + items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width)) + indented_items_text = formatting.Indent(items_text, spaces=items_indent) + indented_header = formatting.Indent(header, spaces=header_indent) + return indented_header + indented_items_text[len(indented_header):] + '\n' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index dfd92455..b0b545dc 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -269,10 +269,18 @@ def testHelpTextNameSectionCommandWithSeparator(self): component = 9 t = trace.FireTrace(component, name='int', separator='-') t.AddSeparator() - help_screen = helptext.HelpText(component=component, trace=t, verbose=True) + help_screen = helptext.HelpText(component=component, trace=t, verbose=False) self.assertIn('int -', help_screen) self.assertNotIn('int - -', help_screen) + def testHelpTextNameSectionCommandWithSeparatorVerbobse(self): + component = 9 + t = trace.FireTrace(component, name='int', separator='-') + t.AddSeparator() + help_screen = helptext.HelpText(component=component, trace=t, verbose=True) + self.assertIn('int -', help_screen) + self.assertIn('int - -', help_screen) + class UsageTest(testutils.BaseTestCase): From f547c68c11960a602827e45bbc55e866c137d5a8 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 13:26:28 -0700 Subject: [PATCH 009/205] Fix broken b-key induced typo. PiperOrigin-RevId: 259393920 Change-Id: Ia44913e38741a42174497e686f50d6fe105778c8 --- fire/helptext_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index b0b545dc..21f88614 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -273,7 +273,7 @@ def testHelpTextNameSectionCommandWithSeparator(self): self.assertIn('int -', help_screen) self.assertNotIn('int - -', help_screen) - def testHelpTextNameSectionCommandWithSeparatorVerbobse(self): + def testHelpTextNameSectionCommandWithSeparatorVerbose(self): component = 9 t = trace.FireTrace(component, name='int', separator='-') t.AddSeparator() From 46d100e1fcb446a5481fbe148a4419c1e31777bc Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 22 Jul 2019 17:41:24 -0700 Subject: [PATCH 010/205] First step toward custom descriptions and summaries for primitive types with messy builtin docstrings. PiperOrigin-RevId: 259442516 Change-Id: I7e6013af7c9f893af1f80291fdcde5643f2d2830 --- fire/custom_descriptions.py | 71 +++++++++++++++++++++++++++++++++++++ fire/docstrings.py | 2 +- fire/helptext.py | 39 +++++++++++++++----- fire/helptext_test.py | 21 ++++++----- 4 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 fire/custom_descriptions.py diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py new file mode 100644 index 00000000..70bd666f --- /dev/null +++ b/fire/custom_descriptions.py @@ -0,0 +1,71 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Custom descriptions and summaries for the builtin types. + +The docstrings for objects of primitive types reflect the type of the object, +rather than the object itself. For example, the docstring for any dict is this: + +> print({'key': 'value'}.__doc__) +dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object's + (key, value) pairs +dict(iterable) -> new dictionary initialized as if via: + d = {} + for k, v in iterable: + d[k] = v +dict(**kwargs) -> new dictionary initialized with the name=value pairs + in the keyword argument list. For example: dict(one=1, two=2) + +As you can see, this docstring is more pertinant to the function `dict` and +would be suitable as the result of `dict.__doc__`, but is wholely unsuitable +as a description for the dict `{'key': 'value'}`. + +This modules aims to resolve that problem, providing custom summaries and +descriptions for primitive typed values. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import six + + +def NeedsCustomDescription(component): + """Whether the component should use a custom description and summary. + + Components of primitive type, such as ints, floats, dicts, lists, and others + have messy builtin docstrings. These are inappropriate for display as + descriptions and summaries in a CLI. This function determines whether the + provided component has one of these docstrings. + + Note that an object such as `int` has the same docstring as an int like `3`. + The docstring is OK for `int`, but is inappropriate as a docstring for `3`. + + Args: + component: The component of interest. + Returns: + Whether the component should use a custom description and summary. + """ + type_ = type(component) + if (type_ in six.string_types + or type_ in six.integer_types + or type_ is six.text_type + or type_ is six.binary_type + or type_ in (float, complex, bool) + or type_ in (dict, tuple, list, set) + ): + return True + return False diff --git a/fire/docstrings.py b/fire/docstrings.py index 4f0ffd93..6613d853 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -424,7 +424,7 @@ def _consume_line(line_info, state): elif state.section.format == Formats.NUMPY: line_stripped = line_info.remaining.strip() if _is_arg_name(line_stripped): - # Token on it's own line can either be the last word of the description + # Token on its own line can either be the last word of the description # of the previous arg, or a new arg. TODO: Whitespace can distinguish. arg = _get_or_create_arg_by_name(state, line_stripped) state.current_arg = arg diff --git a/fire/helptext.py b/fire/helptext.py index 8f4fe97b..257b9286 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -36,11 +36,14 @@ import inspect from fire import completion +from fire import custom_descriptions from fire import decorators from fire import formatting from fire import inspectutils from fire import value_types +LINE_LENGTH = 80 + def HelpText(component, trace=None, verbose=False): """Gets the help string for the current component, suitalbe for a help screen. @@ -61,10 +64,10 @@ def HelpText(component, trace=None, verbose=False): metadata = decorators.GetMetadata(component) # Sections: - name_section = _NameSection(info, trace=trace, verbose=verbose) + name_section = _NameSection(component, info, trace=trace, verbose=verbose) synopsis_section = _SynopsisSection( component, actions_grouped_by_kind, spec, metadata, trace=trace) - description_section = _DescriptionSection(info) + description_section = _DescriptionSection(component, info) # TODO(dbieber): Add returns and raises sections for functions. if inspect.isroutine(component) or inspect.isclass(component): @@ -92,12 +95,18 @@ def HelpText(component, trace=None, verbose=False): ) -def _NameSection(info, trace=None, verbose=False): +def _NameSection(component, info, trace=None, verbose=False): """The "Name" section of the help string.""" + # Only include separators in the name in verbose mode. current_command = _GetCurrentCommand(trace, include_separators=verbose) summary = _GetSummary(info) + # If the docstring is one of the messy builtin docstrings, don't show summary. + # TODO(dbieber): In follow up commits we can add in replacement summaries. + if custom_descriptions.NeedsCustomDescription(component): + summary = None + if summary: text = current_command + ' - ' + summary else: @@ -129,13 +138,26 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, return ('SYNOPSIS', text) -def _DescriptionSection(info): - """The "Description" sections of the help string.""" +def _DescriptionSection(component, info): + """The "Description" sections of the help string. + + Args: + component: The component to produce the description section for. + info: The info dict for the component of interest. + + Returns: + Returns the description if available. If not, returns the summary. + If neither are available, returns None. + """ + # If the docstring is one of the messy builtin docstrings, set it to None. + # TODO(dbieber): In follow up commits we can add in replacement docstrings. + if custom_descriptions.NeedsCustomDescription(component): + return None + summary = _GetSummary(info) description = _GetDescription(info) - # Returns the description if available. If not, returns the summary. - # If neither are available, returns None. text = description or summary or None + if text: return ('DESCRIPTION', text) else: @@ -574,7 +596,8 @@ def UsageTextForObject(component, trace=None, verbose=False): def _CreateAvailabilityLine(header, items, - header_indent=2, items_indent=25, line_length=80): + header_indent=2, items_indent=25, + line_length=LINE_LENGTH): items_width = line_length - items_indent item_names = [item[0] for item in items] items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width)) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 21f88614..9e93d98f 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -110,9 +110,8 @@ def testHelpTextEmptyList(self): trace=trace.FireTrace(component, 'list')) self.assertIn('NAME\n list', help_screen) self.assertIn('SYNOPSIS\n list COMMAND', help_screen) - # We don't check description content here since the content could be python - # version dependent. - self.assertIn('DESCRIPTION\n', help_screen) + # The list docstring is messy, so it is not shown. + self.assertNotIn('DESCRIPTION', help_screen) # We don't check the listed commands either since the list API could # potentially change between Python versions. self.assertIn('COMMANDS\n COMMAND is one of the following:\n', @@ -125,9 +124,8 @@ def testHelpTextShortList(self): trace=trace.FireTrace(component, 'list')) self.assertIn('NAME\n list', help_screen) self.assertIn('SYNOPSIS\n list COMMAND', help_screen) - # We don't check description content here since the content could be python - # version dependent. - self.assertIn('DESCRIPTION\n', help_screen) + # The list docstring is messy, so it is not shown. + self.assertNotIn('DESCRIPTION', help_screen) # We don't check the listed commands comprehensively since the list API # could potentially change between Python versions. Check a few @@ -142,7 +140,8 @@ def testHelpTextInt(self): component=component, trace=trace.FireTrace(component, '7')) self.assertIn('NAME\n 7', help_screen) self.assertIn('SYNOPSIS\n 7 COMMAND | VALUE', help_screen) - self.assertIn('DESCRIPTION\n', help_screen) + # The int docstring is messy, so it is not shown. + self.assertNotIn('DESCRIPTION', help_screen) self.assertIn('COMMANDS\n COMMAND is one of the following:\n', help_screen) self.assertIn('VALUES\n VALUE is one of the following:\n', help_screen) @@ -274,12 +273,12 @@ def testHelpTextNameSectionCommandWithSeparator(self): self.assertNotIn('int - -', help_screen) def testHelpTextNameSectionCommandWithSeparatorVerbose(self): - component = 9 - t = trace.FireTrace(component, name='int', separator='-') + component = tc.WithDefaults().double + t = trace.FireTrace(component, name='double', separator='-') t.AddSeparator() help_screen = helptext.HelpText(component=component, trace=t, verbose=True) - self.assertIn('int -', help_screen) - self.assertIn('int - -', help_screen) + self.assertIn('double -', help_screen) + self.assertIn('double - -', help_screen) class UsageTest(testutils.BaseTestCase): From a9b4819cb72cd272ea25d38f1e964d55af6c8fc6 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 23 Jul 2019 09:47:48 -0700 Subject: [PATCH 011/205] Adds the oft-neglected frozenset to the list of types needing custom descriptions. PiperOrigin-RevId: 259554805 Change-Id: I24e8586bf911f406468f858b6b9c4bdfa909ff16 --- fire/custom_descriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 70bd666f..695b01b3 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -65,7 +65,7 @@ def NeedsCustomDescription(component): or type_ is six.text_type or type_ is six.binary_type or type_ in (float, complex, bool) - or type_ in (dict, tuple, list, set) + or type_ in (dict, tuple, list, set, frozenset) ): return True return False From 337342e236cb32694f481d3ab84592a52c83029a Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 23 Jul 2019 15:38:05 -0700 Subject: [PATCH 012/205] Support for namedtuples in Python 2. PiperOrigin-RevId: 259626143 Change-Id: If87c68d56e5885aa43c9f7ce782e00cd77aa32ad --- fire/inspectutils.py | 21 +++++++++++++++++++-- fire/inspectutils_test.py | 20 ++++++++++++++++++++ fire/test_components.py | 7 +++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 645b7b18..5cd8d770 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -86,6 +86,7 @@ class with an __init__ method. def GetFullArgSpec(fn): """Returns a FullArgSpec describing the given callable.""" + original_fn = fn fn, skip_arg = _GetArgSpecInfo(fn) try: @@ -100,10 +101,26 @@ def GetFullArgSpec(fn): except TypeError: # If we can't get the argspec, how do we know if the fn should take args? # 1. If it's a builtin, it can take args. - # 2. If it's an implicit __init__ function (a 'slot wrapper'), take no args. - # Are there other cases? + # 2. If it's an implicit __init__ function (a 'slot wrapper'), that comes + # from a namedtuple, use _fields to determine the args. + # 3. If it's another slot wrapper (that comes from not subclassing object in + # Python 2), then there are no args. + # Are there other cases? We just don't know. + + # Case 1: Builtins accept args. if inspect.isbuiltin(fn): return FullArgSpec(varargs='vars', varkw='kwargs') + + # Case 2: namedtuples store their args in their _fields attribute. + # TODO(dbieber): Determine if there's a way to detect false positives. + # In Python 2, a class that does not subclass anything, does not define + # __init__, and has an attribute named _fields will cause Fire to think it + # expects args for its constructor when in fact it does not. + fields = getattr(original_fn, '_fields', None) + if fields: + return FullArgSpec(args=list(fields)) + + # Case 3: Other known slot wrappers do not accept args. return FullArgSpec() if skip_arg and args: diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index 0ebd4059..ea8eb0e2 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -70,6 +70,26 @@ def testGetFullArgSpecFromSlotWrapper(self): self.assertEqual(spec.kwonlydefaults, {}) self.assertEqual(spec.annotations, {}) + def testGetFullArgSpecFromNamedTuple(self): + spec = inspectutils.GetFullArgSpec(tc.NamedTuplePoint) + self.assertEqual(spec.args, ['x', 'y']) + self.assertEqual(spec.defaults, ()) + self.assertEqual(spec.varargs, None) + self.assertEqual(spec.varkw, None) + self.assertEqual(spec.kwonlyargs, []) + self.assertEqual(spec.kwonlydefaults, {}) + self.assertEqual(spec.annotations, {}) + + def testGetFullArgSpecFromNamedTupleSubclass(self): + spec = inspectutils.GetFullArgSpec(tc.SubPoint) + self.assertEqual(spec.args, ['x', 'y']) + self.assertEqual(spec.defaults, ()) + self.assertEqual(spec.varargs, None) + self.assertEqual(spec.varkw, None) + self.assertEqual(spec.kwonlyargs, []) + self.assertEqual(spec.kwonlydefaults, {}) + self.assertEqual(spec.annotations, {}) + def testGetFullArgSpecFromClassNoInit(self): spec = inspectutils.GetFullArgSpec(tc.OldStyleEmpty) self.assertEqual(spec.args, []) diff --git a/fire/test_components.py b/fire/test_components.py index f81c9a0f..fd868758 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -20,6 +20,7 @@ import collections +import enum import six if six.PY3: @@ -396,3 +397,9 @@ class Subdict(dict): # An example subdict. SUBDICT = Subdict({1: 2, 'red': 'blue'}) + + +class Color(enum.Enum): + RED = 1 + GREEN = 2 + BLUE = 3 From 73dde2ca4a01a70f14c4f89e28cccad1a81392cd Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 11:18:28 -0700 Subject: [PATCH 013/205] more explicit check for fields PiperOrigin-RevId: 259778984 Change-Id: I615eb97beccb1c5e20449ef97a7710c3dc0d5bb0 --- fire/inspectutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 5cd8d770..18a1f1ae 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -117,7 +117,7 @@ def GetFullArgSpec(fn): # __init__, and has an attribute named _fields will cause Fire to think it # expects args for its constructor when in fact it does not. fields = getattr(original_fn, '_fields', None) - if fields: + if fields is not None: return FullArgSpec(args=list(fields)) # Case 3: Other known slot wrappers do not accept args. From 01651a2841db5a3e45e1b3cf62b208730f80f3e4 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 11:20:22 -0700 Subject: [PATCH 014/205] Adds test for static and class methods. PiperOrigin-RevId: 259779362 Change-Id: I5b95f90793f99ef4d682d2b94a2e8d3a2cc725c8 --- fire/core_test.py | 14 ++++++++++++++ fire/test_components.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/fire/core_test.py b/fire/core_test.py index 171611a9..2f2df3fd 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -178,5 +178,19 @@ def testCallableWithPositionalArgs(self): # objects. core.Fire(tc.CallableWithPositionalArgs(), command=['3', '4']) + def testStaticMethod(self): + self.assertEqual( + core.Fire(tc.HasStaticAndClassMethods, + command=['static_fn', 'alpha']), + 'alpha', + ) + + def testClassMethod(self): + self.assertEqual( + core.Fire(tc.HasStaticAndClassMethods, + command=['class_fn', '6']), + 7, + ) + if __name__ == '__main__': testutils.main() diff --git a/fire/test_components.py b/fire/test_components.py index fd868758..1bf7ccce 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -403,3 +403,20 @@ class Color(enum.Enum): RED = 1 GREEN = 2 BLUE = 3 + + +class HasStaticAndClassMethods(object): + """A class with a static method and a class method.""" + + CLASS_STATE = 1 + + def __init__(self, instance_state): + self.instance_state = instance_state + + @staticmethod + def static_fn(args): + return args + + @classmethod + def class_fn(cls, args): + return args + cls.CLASS_STATE From 970d292f1d04fde282a7a3063d3c0f4b8b763348 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 11:28:11 -0700 Subject: [PATCH 015/205] Include varargs in help screens and usage screens. PiperOrigin-RevId: 259780867 Change-Id: Ibbfe7ca14ea136e25b29a75be12a4a4aa149c5b7 --- fire/helptext.py | 26 +++++++++++++++++++++----- fire/helptext_test.py | 2 +- fire/test_components.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 257b9286..f1866392 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -182,11 +182,17 @@ def _ArgsAndFlagsSections(info, spec, metadata): _CreateArgItem(arg, docstring_info) for arg in args_with_no_defaults ] + + if spec.varargs: + arg_items.append( + _CreateArgItem(spec.varargs, docstring_info) + ) + if arg_items: title = 'POSITIONAL ARGUMENTS' if accepts_positional_args else 'ARGUMENTS' arguments_section = (title, '\n'.join(arg_items).rstrip('\n')) args_and_flags_sections.append(arguments_section) - if accepts_positional_args: + if args_with_no_defaults and accepts_positional_args: notes_sections.append( ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') ) @@ -277,9 +283,15 @@ def _GetArgsAndFlagsString(spec, metadata): if flags: for flag in flags: flag_string = flag_string_template.format( - flag_name=formatting.Underline(flag), - flag_name_upper=flag.upper()) + flag_name=flag, + flag_name_upper=formatting.Underline(flag.upper())) arg_and_flag_strings.append(flag_string) + + if spec.varargs: + varargs_string = '[{varargs}]...'.format( + varargs=formatting.Underline(spec.varargs.upper())) + arg_and_flag_strings.append(varargs_string) + return ' '.join(arg_and_flag_strings) @@ -358,7 +370,7 @@ def _CreateArgItem(arg, docstring_info): description = None if docstring_info.args: for arg_in_docstring in docstring_info.args: - if arg_in_docstring.name == arg: + if arg_in_docstring.name in (arg, '*' + arg, '**' + arg): description = arg_in_docstring.description arg = arg.upper() @@ -387,7 +399,7 @@ def _CreateFlagItem(flag, docstring_info): flag = '--{flag}'.format(flag=formatting.Underline(flag)) if description: - return _CreateItem(flag, description, indent=2) + return _CreateItem(flag, description, indent=4) return flag @@ -515,6 +527,10 @@ def UsageTextForFunction(component, trace=None, verbose=False): + ' | '.join('--' + flag for flag in flags) + '\n') else: availability_lines = '' + + if spec.varargs: + items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) + args_and_flags = ' '.join(items) hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else '' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 9e93d98f..5e517860 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -221,7 +221,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): FLAGS --count - Input number that you want to double.""" + Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip()) diff --git a/fire/test_components.py b/fire/test_components.py index 1bf7ccce..04603b93 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -420,3 +420,22 @@ def static_fn(args): @classmethod def class_fn(cls, args): return args + cls.CLASS_STATE + + +def function_with_varargs(arg1, arg2, arg3=1, *varargs): # pylint: disable=keyword-arg-before-vararg + """Function with varargs. + + Args: + arg1: Position arg docstring. + arg2: Position arg docstring. + arg3: Flags docstring. + *varargs: Accepts unlimited positional args. + Returns: + The unlimited positional args. + """ + del arg1, arg2, arg3 # Unused. + return varargs + + +def function_with_keyword_arguments(**kwargs): + return kwargs From ce030f66b41279343b62e31a355f11c214c0cd99 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 11:48:24 -0700 Subject: [PATCH 016/205] call super in setUp to make internal linter happy PiperOrigin-RevId: 259785116 Change-Id: I1adab292e7fca7b27222d0f4f59278f38d9c37bb --- fire/helptext_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 5e517860..7fc6ce12 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -31,6 +31,7 @@ class HelpTest(testutils.BaseTestCase): def setUp(self): + super(HelpTest, self).setUp() os.environ['ANSI_COLORS_DISABLED'] = '1' def testHelpTextNoDefaults(self): From c1a4acf17433701b8048e6ff34beb3cd9c31121c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 13:42:02 -0700 Subject: [PATCH 017/205] Include kwargs in help screens and usage screens. PiperOrigin-RevId: 259807919 Change-Id: I3fb16fa529e1e4085f2fecaf7c1b0d95368d9c20 --- fire/helptext.py | 138 +++++++++++++++++++++++----------------- fire/helptext_test.py | 14 ++-- fire/test_components.py | 5 +- 3 files changed, 89 insertions(+), 68 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index f1866392..5fd7080a 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -168,7 +168,6 @@ def _ArgsAndFlagsSections(info, spec, metadata): """The "Args and Flags" sections of the help string.""" args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] - flags = args_with_defaults + spec.kwonlyargs # Check if positional args are allowed. If not, require flag syntax for args. accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) @@ -197,10 +196,23 @@ def _ArgsAndFlagsSections(info, spec, metadata): ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') ) - flag_items = [ - _CreateFlagItem(flag, docstring_info) - for flag in flags + optional_flag_items = [ + _CreateFlagItem(flag, docstring_info, required=False) + for flag in args_with_defaults ] + required_flag_items = [ + _CreateFlagItem(flag, docstring_info, required=True) + for flag in spec.kwonlyargs + ] + flag_items = optional_flag_items + required_flag_items + + if spec.varkw: + description = _GetArgDescription(spec.varkw, docstring_info) + message = ('Additional flags are accepted.' + if flag_items else + 'Flags are accepted.') + item = _CreateItem(message, description, indent=4) + flag_items.append(item) if flag_items: flags_section = ('FLAGS', '\n'.join(flag_items)) @@ -262,7 +274,6 @@ def _GetArgsAndFlagsString(spec, metadata): """ args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] - flags = args_with_defaults + spec.kwonlyargs # Check if positional args are allowed. If not, require flag syntax for args. accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) @@ -279,13 +290,9 @@ def _GetArgsAndFlagsString(spec, metadata): for arg in args_with_no_defaults] arg_and_flag_strings.extend(arg_strings) - flag_string_template = '[--{flag_name}={flag_name_upper}]' - if flags: - for flag in flags: - flag_string = flag_string_template.format( - flag_name=flag, - flag_name_upper=formatting.Underline(flag.upper())) - arg_and_flag_strings.append(flag_string) + # If there are any arguments that are treated as flags: + if args_with_defaults or spec.kwonlyargs or spec.varkw: + arg_and_flag_strings.append('') if spec.varargs: varargs_string = '[{varargs}]...'.format( @@ -367,48 +374,51 @@ def _CreateArgItem(arg, docstring_info): Returns: A string to be used in constructing the help screen for the function. """ - description = None - if docstring_info.args: - for arg_in_docstring in docstring_info.args: - if arg_in_docstring.name in (arg, '*' + arg, '**' + arg): - description = arg_in_docstring.description + description = _GetArgDescription(arg, docstring_info) arg = arg.upper() - if description: - return _CreateItem(formatting.BoldUnderline(arg), description, indent=4) - else: - return formatting.BoldUnderline(arg) + return _CreateItem(formatting.BoldUnderline(arg), description, indent=4) -def _CreateFlagItem(flag, docstring_info): +def _CreateFlagItem(flag, docstring_info, required=False): """Returns a string describing a flag using information from the docstring. Args: flag: The name of the flag. docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. + required: Whether the flag is required. Keyword-only arguments (only in + Python 3) become required flags, whereas normal keyword arguments become + optional flags. Returns: A string to be used in constructing the help screen for the function. """ - description = None - if docstring_info.args: - for arg_in_docstring in docstring_info.args: - if arg_in_docstring.name == flag: - description = arg_in_docstring.description - break + description = _GetArgDescription(flag, docstring_info) - flag = '--{flag}'.format(flag=formatting.Underline(flag)) - if description: - return _CreateItem(flag, description, indent=4) - return flag + flag_string_template = '--{flag_name}={flag_name_upper}' + flag = flag_string_template.format( + flag_name=flag, + flag_name_upper=formatting.Underline(flag.upper())) + if not required: + flag += ' (optional)' + return _CreateItem(flag, description, indent=4) def _CreateItem(name, description, indent=2): + if not description: + return name return """{name} {description}""".format(name=name, description=formatting.Indent(description, indent)) +def _GetArgDescription(name, docstring_info): + if docstring_info.args: + for arg_in_docstring in docstring_info.args: + if arg_in_docstring.name in (name, '*' + name, '**' + name): + return arg_in_docstring.description + + def _GroupUsageDetailsSection(groups): """Creates a section tuple for the groups section of the usage details.""" group_item_strings = [] @@ -417,9 +427,8 @@ def _GroupUsageDetailsSection(groups): group_item = group_name if 'docstring_info' in group_info: group_docstring_info = group_info['docstring_info'] - if group_docstring_info and group_docstring_info.summary: - group_item = _CreateItem(group_name, - group_docstring_info.summary) + if group_docstring_info: + group_item = _CreateItem(group_name, group_docstring_info.summary) group_item_strings.append(group_item) return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings)) @@ -432,9 +441,8 @@ def _CommandUsageDetailsSection(commands): command_item = command_name if 'docstring_info' in command_info: command_docstring_info = command_info['docstring_info'] - if command_docstring_info and command_docstring_info.summary: - command_item = _CreateItem(command_name, - command_docstring_info.summary) + if command_docstring_info: + command_item = _CreateItem(command_name, command_docstring_info.summary) command_item_strings.append(command_item) return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)) @@ -502,14 +510,8 @@ def UsageTextForFunction(component, trace=None, verbose=False): command = '' spec = inspectutils.GetFullArgSpec(component) - args = spec.args - if spec.defaults is None: - num_defaults = 0 - else: - num_defaults = len(spec.defaults) - args_with_no_defaults = args[:len(args) - num_defaults] - args_with_defaults = args[len(args) - num_defaults:] - flags = args_with_defaults + spec.kwonlyargs + args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] + args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] # Check if positional args are allowed. If not, show flag syntax for args. metadata = decorators.GetMetadata(component) @@ -520,13 +522,32 @@ def UsageTextForFunction(component, trace=None, verbose=False): else: items = [arg.upper() for arg in args_with_no_defaults] - if flags: + # If there are any arguments that are treated as flags: + if args_with_defaults or spec.kwonlyargs or spec.varkw: items.append('') - availability_lines = ( - '\nAvailable flags: ' - + ' | '.join('--' + flag for flag in flags) + '\n') - else: - availability_lines = '' + + optional_flags = [('--' + flag) for flag in args_with_defaults] + required_flags = [('--' + flag) for flag in spec.kwonlyargs] + + # Flags section: + availability_lines = [] + if optional_flags: + availability_lines.append( + _CreateAvailabilityLine(header='Optional flags:', items=optional_flags, + header_indent=0)) + if required_flags: + availability_lines.append( + _CreateAvailabilityLine(header='Required flags:', items=required_flags, + header_indent=0)) + if spec.varkw: + additional_flags = ('Additional flags are accepted.' + if optional_flags or required_flags else + 'Flags are accepted.') + availability_lines.append(additional_flags + '\n') + + if availability_lines: + # Start the section with blank lines. + availability_lines.insert(0, '\n') if spec.varargs: items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) @@ -538,7 +559,7 @@ def UsageTextForFunction(component, trace=None, verbose=False): return output_template.format( current_command=command, args_and_flags=args_and_flags, - availability_lines=availability_lines, + availability_lines=''.join(availability_lines), hyphen_hyphen=hyphen_hyphen) @@ -576,25 +597,25 @@ def UsageTextForObject(component, trace=None, verbose=False): possible_actions.append('group') groups_text = _CreateAvailabilityLine( header='available groups:', - items=groups) + items=[name for name, _ in groups]) availability_lines.append(groups_text) if commands: possible_actions.append('command') commands_text = _CreateAvailabilityLine( header='available commands:', - items=commands) + items=[name for name, _ in commands]) availability_lines.append(commands_text) if values: possible_actions.append('value') values_text = _CreateAvailabilityLine( header='available values:', - items=values) + items=[name for name, _ in values]) availability_lines.append(values_text) if indexes: possible_actions.append('index') indexes_text = _CreateAvailabilityLine( header='available indexes:', - items=[(indexes, None)]) + items=indexes) availability_lines.append(indexes_text) if possible_actions: @@ -615,8 +636,7 @@ def _CreateAvailabilityLine(header, items, header_indent=2, items_indent=25, line_length=LINE_LENGTH): items_width = line_length - items_indent - item_names = [item[0] for item in items] - items_text = '\n'.join(formatting.WrappedJoin(item_names, width=items_width)) + items_text = '\n'.join(formatting.WrappedJoin(items, width=items_width)) indented_items_text = formatting.Indent(items_text, spaces=items_indent) indented_header = formatting.Indent(header, spaces=header_indent) return indented_header + indented_items_text[len(indented_header):] + '\n' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 7fc6ce12..46f5b851 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -77,9 +77,9 @@ def testHelpTextFunctionWithDefaults(self): component=component, trace=trace.FireTrace(component, name='triple')) self.assertIn('NAME\n triple', help_screen) - self.assertIn('SYNOPSIS\n triple [--count=COUNT]', help_screen) + self.assertIn('SYNOPSIS\n triple ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - self.assertIn('FLAGS\n --count', help_screen) + self.assertIn('FLAGS\n --count=COUNT (optional)', help_screen) self.assertNotIn('NOTES', help_screen) def testHelpTextFunctionWithBuiltin(self): @@ -215,13 +215,13 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): double - Returns the input multiplied by 2. SYNOPSIS - double [--count=COUNT] + double DESCRIPTION Returns the input multiplied by 2. FLAGS - --count + --count=COUNT (optional) Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip()) @@ -232,7 +232,7 @@ def testHelpTextUnderlineFlag(self): help_screen = helptext.HelpText(component, t) self.assertIn(formatting.Bold('NAME') + '\n triple', help_screen) self.assertIn( - formatting.Bold('SYNOPSIS') + '\n triple [--count=COUNT]', + formatting.Bold('SYNOPSIS') + '\n triple ', help_screen) self.assertIn( formatting.Bold('FLAGS') + '\n --' + formatting.Underline('count'), @@ -334,7 +334,7 @@ def testUsageOutputFunctionWithHelp(self): expected_output = ''' Usage: function_with_help - Available flags: --help + Optional flags: --help For detailed information on this command, run: function_with_help -- --help''' @@ -349,7 +349,7 @@ def testUsageOutputFunctionWithDocstring(self): expected_output = ''' Usage: multiplier_with_docstring NUM - Available flags: --rate + Optional flags: --rate For detailed information on this command, run: multiplier_with_docstring --help''' diff --git a/fire/test_components.py b/fire/test_components.py index 04603b93..650d279a 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -437,5 +437,6 @@ def function_with_varargs(arg1, arg2, arg3=1, *varargs): # pylint: disable=keyw return varargs -def function_with_keyword_arguments(**kwargs): - return kwargs +def function_with_keyword_arguments(arg1, arg2=3, **kwargs): + del arg2 # Unused. + return arg1, kwargs From 97a401e435183420e83f85b4ae6e82ee4ed43b06 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 14:02:02 -0700 Subject: [PATCH 018/205] switch from marking optional flags to marking required flags. PiperOrigin-RevId: 259812051 Change-Id: I81809f372dd4f5afd46128bd70c69e6b572e7d0c --- fire/helptext.py | 4 ++-- fire/helptext_test.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 5fd7080a..ecfe10ed 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -399,8 +399,8 @@ def _CreateFlagItem(flag, docstring_info, required=False): flag = flag_string_template.format( flag_name=flag, flag_name_upper=formatting.Underline(flag.upper())) - if not required: - flag += ' (optional)' + if required: + flag += ' (required)' return _CreateItem(flag, description, indent=4) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 46f5b851..33676420 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -79,7 +79,7 @@ def testHelpTextFunctionWithDefaults(self): self.assertIn('NAME\n triple', help_screen) self.assertIn('SYNOPSIS\n triple ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - self.assertIn('FLAGS\n --count=COUNT (optional)', help_screen) + self.assertIn('FLAGS\n --count=COUNT', help_screen) self.assertNotIn('NOTES', help_screen) def testHelpTextFunctionWithBuiltin(self): @@ -221,7 +221,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): Returns the input multiplied by 2. FLAGS - --count=COUNT (optional) + --count=COUNT Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip()) From eea89511daeaded98edea91a808d348b89c16fe1 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 14:26:20 -0700 Subject: [PATCH 019/205] Adds hello world example to readme. Shows that --help works for help without isolated --. PiperOrigin-RevId: 259817005 Change-Id: I5c079d60dcac234fa1cb53d16f498fb06edc117b --- README.md | 38 +++++++++++++++++++++++++++++--------- docs/index.md | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9aea0877..862f5f23 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,26 @@ You can call `Fire` on any Python object:
functions, classes, modules, objects, dictionaries, lists, tuples, etc. They all work! +Here's an example of calling Fire on a function. + +```python +import fire + +def hello(name="World"): + return "Hello %s!" % name + +if __name__ == '__main__': + fire.Fire(hello) +``` + +Then, from the command line, you can run: + +```bash +python hello.py # Hello World! +python hello.py --name=David # Hello David! +python hello.py --help # Shows usage information. +``` + Here's an example of calling Fire on a class. ```python @@ -77,16 +97,16 @@ Please see [The Python Fire Guide](docs/guide.md). | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -| Using a CLI | Command | Notes -| :------------- | :------------------------- | :--------- -| [Help](docs/using-cli.md#help-flag) | `command -- --help` | -| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. -| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. -| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. -| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. -| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` | +Using a CLI | Command | Notes +:---------------------------------------------- | :-------------------------------------- | :---- +[Help](docs/using-cli.md#help-flag) | `command --help` or `command -- --help` | +[REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. +[Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. +[Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. +[Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. +[Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` | -_Note that flags are separated from the Fire command by an isolated `--` arg._ +_Note that these flags are separated from the Fire command by an isolated `--`._ ## License diff --git a/docs/index.md b/docs/index.md index f6503c0b..171ae26e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,6 +27,26 @@ You can call `Fire` on any Python object:
functions, classes, modules, objects, dictionaries, lists, tuples, etc. They all work! +Here's an example of calling Fire on a function. + +```python +import fire + +def hello(name="World"): + return "Hello %s!" % name + +if __name__ == '__main__': + fire.Fire(hello) +``` + +Then, from the command line, you can run: + +```bash +python hello.py # Hello World! +python hello.py --name=David # Hello David! +python hello.py --help # Shows usage information. +``` + Here's an example of calling Fire on a class. ```python @@ -77,16 +97,16 @@ Please see [The Python Fire Guide](guide.md). | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -| Using a CLI | Command | Notes -| :------------- | :------------------------- | :--------- -| [Help](using-cli.md#help-flag) | `command -- --help` | -| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. -| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. -| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. -| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. -| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | +Using a CLI | Command | Notes +:---------------------------------------------- | :-------------------------------------- | :---- +[Help](using-cli.md#help-flag) | `command --help` or `command -- --help` | +[REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. +[Separator](using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. +[Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. +[Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. +[Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | -_Note that flags are separated from the Fire command by an isolated `--` arg._ +_Note that these flags are separated from the Fire command by an isolated `--`._ ## License From ed3cd74b4d24b2f94c6fb620d15ab00c9c22c9ca Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 24 Jul 2019 14:50:01 -0700 Subject: [PATCH 020/205] resolve inconsistent-return-statements lint error. PiperOrigin-RevId: 259821764 Change-Id: I3983037f00f0d620b84363cd1bfc348c085ac8a2 --- fire/helptext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fire/helptext.py b/fire/helptext.py index ecfe10ed..56c1ff7b 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -417,6 +417,7 @@ def _GetArgDescription(name, docstring_info): for arg_in_docstring in docstring_info.args: if arg_in_docstring.name in (name, '*' + name, '**' + name): return arg_in_docstring.description + return None def _GroupUsageDetailsSection(groups): From dbc8022386a48462074c8a5ddd01055bfd690606 Mon Sep 17 00:00:00 2001 From: Evan Juarez Date: Thu, 25 Jul 2019 15:13:18 -0700 Subject: [PATCH 021/205] Don't strip whitespace when collecting descriptions in docstring PiperOrigin-RevId: 260030371 Change-Id: Id307fa5ca4c77e07290928daea6b3e4291e1c0e6 --- fire/docstrings.py | 38 ++++++++++++++++++++++++++++++++++++-- fire/docstrings_test.py | 14 ++++++++++---- fire/test_components.py | 17 +++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index 6613d853..b15cddce 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -56,6 +56,7 @@ import collections import re +import textwrap import enum @@ -179,7 +180,10 @@ def parse(docstring): _consume_line(line_info, state) summary = ' '.join(state.summary.lines) if state.summary.lines else None - description = _join_lines(state.description.lines) + state.description.lines = _strip_blank_lines(state.description.lines) + description = textwrap.dedent('\n'.join(state.description.lines)) + if not description: + description = None returns = _join_lines(state.returns.lines) yields = _join_lines(state.yields.lines) raises = _join_lines(state.raises.lines) @@ -203,6 +207,32 @@ def parse(docstring): ) +def _strip_blank_lines(lines): + """Removes lines containing only blank characters before and after the text. + + Args: + lines: A list of lines. + Returns: + A list of lines without trailing or leading blank lines. + """ + # Find the first non-blank line. + start = 0 + while lines and _is_blank(lines[start]): + start += 1 + + lines = lines[start:] + + # Remove trailing blank lines. + while lines and _is_blank(lines[-1]): + lines.pop() + + return lines + + +def _is_blank(line): + return not line or line.isspace() + + def _join_lines(lines): """Joins lines with the appropriate connective whitespace. @@ -391,7 +421,7 @@ def _consume_line(line_info, state): else: # We're past the end of the summary. # Additions now contribute to the description. - state.description.lines.append(line_info.remaining) + state.description.lines.append(line_info.remaining_raw) else: state.summary.permitted = False @@ -470,6 +500,7 @@ def _create_line_info(line, next_line): line_info = Namespace() # TODO(dbieber): Switch to an explicit class. line_info.line = line line_info.stripped = line.strip() + line_info.remaining_raw = line_info.line line_info.remaining = line_info.stripped line_info.indentation = len(line) - len(line.lstrip()) line_info.next.line = next_line @@ -497,6 +528,7 @@ def _update_section_state(line_info, state): state.section.format = Formats.GOOGLE state.section.title = google_section line_info.remaining = _get_after_google_header(line_info) + line_info.remaining_raw = line_info.remaining section_updated = True rst_section = _rst_section(line_info) @@ -504,6 +536,7 @@ def _update_section_state(line_info, state): state.section.format = Formats.RST state.section.title = rst_section line_info.remaining = _get_after_directive(line_info) + line_info.remaining_raw = line_info.remaining section_updated = True numpy_section = _numpy_section(line_info) @@ -511,6 +544,7 @@ def _update_section_state(line_info, state): state.section.format = Formats.NUMPY state.section.title = numpy_section line_info.remaining = '' + line_info.remaining_raw = line_info.remaining section_updated = True if section_updated: diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 96810c7e..5f7eeeb1 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -131,7 +131,7 @@ def test_google_format_typed_args_and_returns(self): expected_docstring_info = DocstringInfo( summary='Docstring summary.', description='This is a longer description of the docstring. It spans ' - 'multiple lines, as is allowed.', + 'multiple lines, as\nis allowed.', args=[ ArgInfo(name='param1', type='int', description='The first parameter.'), @@ -159,7 +159,7 @@ def test_rst_format_typed_args_and_returns(self): expected_docstring_info = DocstringInfo( summary='Docstring summary.', description='This is a longer description of the docstring. It spans ' - 'across multiple lines.', + 'across multiple\nlines.', args=[ ArgInfo(name='arg1', type='str', description='Description of arg1.'), @@ -193,7 +193,7 @@ def test_numpy_format_typed_args_and_returns(self): expected_docstring_info = DocstringInfo( summary='Docstring summary.', description='This is a longer description of the docstring. It spans ' - 'across multiple lines.', + 'across multiple\nlines.', args=[ ArgInfo(name='param1', type='int', description='The first parameter.'), @@ -217,7 +217,7 @@ def test_multisection_docstring(self): expected_docstring_info = DocstringInfo( summary='Docstring summary.', description='This is the first section of a docstring description.\n\n' - 'This is the second section of a docstring description. This docstring ' + 'This is the second section of a docstring description. This docstring\n' 'description has just two sections.', ) self.assertEqual(docstring_info, expected_docstring_info) @@ -232,6 +232,12 @@ def test_ill_formed_docstring(self): """ docstrings.parse(docstring) + def test_strip_blank_lines(self): + lines = [' ', ' foo ', ' '] + expected_output = [' foo '] + + self.assertEqual(docstrings._strip_blank_lines(lines), expected_output) + if __name__ == '__main__': testutils.main() diff --git a/fire/test_components.py b/fire/test_components.py index 650d279a..1ca39c14 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -440,3 +440,20 @@ def function_with_varargs(arg1, arg2, arg3=1, *varargs): # pylint: disable=keyw def function_with_keyword_arguments(arg1, arg2=3, **kwargs): del arg2 # Unused. return arg1, kwargs + + +def fn_with_code_in_docstring(): + """This has code in the docstring. + + + + Example: + x = fn_with_code_in_docstring() + indentation_matters = True + + + + Returns: + True. + """ + return True From 3d74ae8e486054d391c23650da201371b89de90c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 15:24:04 -0700 Subject: [PATCH 022/205] Show both call syntax and members for callable objects in help and usage screens. PiperOrigin-RevId: 260032359 Change-Id: I91963f1eb1fa570c15a1accd4588f53e1a77cabe --- fire/completion.py | 12 ++- fire/core.py | 4 +- fire/helptext.py | 203 ++++++++++++++++++---------------------- fire/inspectutils.py | 3 + fire/test_components.py | 5 + 5 files changed, 109 insertions(+), 118 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 705ef586..f1be1558 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -281,7 +281,7 @@ def _FishScript(name, commands, default_options=None): ) -def MemberVisible(name, member, verbose): +def MemberVisible(component, name, member, verbose): """Returns whether a member should be included in auto-completion or help. Determines whether a member of an object with the specified name should be @@ -294,6 +294,7 @@ def MemberVisible(name, member, verbose): When not in verbose mode, some modules and functions are excluded as well. Args: + component: The component containing the member. name: The name of the member. member: The member itself. verbose: Whether to include private members. @@ -309,6 +310,13 @@ def MemberVisible(name, member, verbose): if inspect.ismodule(member) and member is six: # TODO(dbieber): Determine more generally which modules to hide. return False + if (six.PY2 and inspect.isfunction(component) + and name in ('func_closure', 'func_code', 'func_defaults', + 'func_dict', 'func_doc', 'func_globals', 'func_name')): + return False + if (six.PY2 and inspect.ismethod(component) + and name in ('im_class', 'im_func', 'im_self')): + return False if isinstance(name, six.string_types): return not name.startswith('_') return True # Default to including the member @@ -334,7 +342,7 @@ def _Members(component, verbose=False): return [ (member_name, member) for member_name, member in members - if MemberVisible(member_name, member, verbose) + if MemberVisible(component, member_name, member, verbose) ] diff --git a/fire/core.py b/fire/core.py index 3ba081c2..d3d51d93 100644 --- a/fire/core.py +++ b/fire/core.py @@ -303,7 +303,7 @@ def _DictAsString(result, verbose=False): # 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 completion.MemberVisible(key, value, verbose)} + if completion.MemberVisible(result, key, value, verbose)} if not result_visible: return '{}' @@ -313,7 +313,7 @@ def _DictAsString(result, verbose=False): lines = [] for key, value in result.items(): - if completion.MemberVisible(key, value, verbose): + if completion.MemberVisible(result, key, value, verbose): line = format_string.format(key=str(key) + ':', value=_OneLineResult(value)) lines.append(line) diff --git a/fire/helptext.py b/fire/helptext.py index 56c1ff7b..7ae9bb0c 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -70,18 +70,14 @@ def HelpText(component, trace=None, verbose=False): description_section = _DescriptionSection(component, info) # TODO(dbieber): Add returns and raises sections for functions. - if inspect.isroutine(component) or inspect.isclass(component): - # For functions (ARGUMENTS / POSITIONAL ARGUMENTS, FLAGS) + if callable(component): args_and_flags_sections, notes_sections = _ArgsAndFlagsSections( info, spec, metadata) - usage_details_sections = [] else: - # For objects (GROUPS, COMMANDS, VALUES, INDEXES) - # TODO(dbieber): Show callable function usage in help text. args_and_flags_sections = [] notes_sections = [] - usage_details_sections = _UsageDetailsSections(component, - actions_grouped_by_kind) + usage_details_sections = _UsageDetailsSections(component, + actions_grouped_by_kind) sections = ( [name_section, synopsis_section, description_section] @@ -119,21 +115,19 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, """The "Synopsis" section of the help string.""" current_command = _GetCurrentCommand(trace=trace, include_separators=True) - # TODO(dbieber): Support callable functions. - if inspect.isroutine(component) or inspect.isclass(component): - # For function: - args_and_flags = _GetArgsAndFlagsString(spec, metadata) - synopsis_section_template = '{current_command} {args_and_flags}' - text = synopsis_section_template.format( - current_command=current_command, args_and_flags=args_and_flags) + possible_actions = _GetPossibleActions(actions_grouped_by_kind) - else: - # For object: - possible_actions_string = _GetPossibleActionsString(actions_grouped_by_kind) - synopsis_template = '{current_command} {possible_actions}' - text = synopsis_template.format( - current_command=current_command, - possible_actions=possible_actions_string) + continuations = [] + if possible_actions: + continuations.append(_GetPossibleActionsString(possible_actions)) + if callable(component): + continuations.append(_GetArgsAndFlagsString(spec, metadata)) + continuation = ' | '.join(continuations) + + synopsis_template = '{current_command} {continuation}' + text = synopsis_template.format( + current_command=current_command, + continuation=continuation) return ('SYNOPSIS', text) @@ -225,22 +219,17 @@ def _UsageDetailsSections(component, actions_grouped_by_kind): """The usage details sections of the help string.""" groups, commands, values, indexes = actions_grouped_by_kind - usage_details_sections = [] + sections = [] + if groups.members: + sections.append(_MakeUsageDetailsSection(groups)) + if commands.members: + sections.append(_MakeUsageDetailsSection(commands)) + if values.members: + sections.append(_ValuesUsageDetailsSection(component, values)) + if indexes.members: + sections.append(('INDEXES', _NewChoicesSection('INDEX', indexes.names))) - if groups: - usage_details_section = _GroupUsageDetailsSection(groups) - usage_details_sections.append(usage_details_section) - if commands: - usage_details_section = _CommandUsageDetailsSection(commands) - usage_details_sections.append(usage_details_section) - if values: - usage_details_section = _ValuesUsageDetailsSection(component, values) - usage_details_sections.append(usage_details_section) - if indexes: - usage_details_sections.append( - ('INDEXES', _NewChoicesSection('INDEX', [indexes]))) - - return usage_details_sections + return sections def _GetSummary(info): @@ -302,51 +291,46 @@ def _GetArgsAndFlagsString(spec, metadata): return ' '.join(arg_and_flag_strings) -def _GetPossibleActionsString(actions_grouped_by_kind): - """A help screen string listing the possible action kinds available.""" - groups, commands, values, indexes = actions_grouped_by_kind - +def _GetPossibleActions(actions_grouped_by_kind): + """The list of possible action kinds.""" possible_actions = [] - if groups: - possible_actions.append('GROUP') - if commands: - possible_actions.append('COMMAND') - if values: - possible_actions.append('VALUE') - if indexes: - possible_actions.append('INDEX') + for action_group in actions_grouped_by_kind: + if action_group.members: + possible_actions.append(action_group.name) + return possible_actions + - possible_actions_string = ' | '.join( - formatting.Underline(action) for action in possible_actions) - return possible_actions_string +def _GetPossibleActionsString(possible_actions): + """A help screen string listing the possible action kinds available.""" + return ' | '.join(formatting.Underline(action.upper()) + for action in possible_actions) def _GetActionsGroupedByKind(component, verbose=False): """Gets lists of available actions, grouped by action kind.""" - groups = [] - commands = [] - values = [] + groups = ActionGroup(name='group', plural='groups') + commands = ActionGroup(name='command', plural='commands') + values = ActionGroup(name='value', plural='values') + indexes = ActionGroup(name='index', plural='indexes') members = completion._Members(component, verbose) # pylint: disable=protected-access for member_name, member in members: member_name = str(member_name) if value_types.IsGroup(member): - groups.append((member_name, member)) + groups.Add(name=member_name, member=member) if value_types.IsCommand(member): - commands.append((member_name, member)) + commands.Add(name=member_name, member=member) if value_types.IsValue(member): - values.append((member_name, member)) + values.Add(name=member_name, member=member) - indexes = None if isinstance(component, (list, tuple)) and component: component_len = len(component) - # WARNING: Note that indexes is a string, whereas the rest are lists. if component_len < 10: - indexes = ', '.join(str(x) for x in range(component_len)) + indexes.Add(name=', '.join(str(x) for x in range(component_len))) else: - indexes = '0..{max}'.format(max=component_len-1) + indexes.Add(name='0..{max}'.format(max=component_len-1)) - return groups, commands, values, indexes + return [groups, commands, values, indexes] def _GetCurrentCommand(trace=None, include_separators=True): @@ -420,38 +404,28 @@ def _GetArgDescription(name, docstring_info): return None -def _GroupUsageDetailsSection(groups): - """Creates a section tuple for the groups section of the usage details.""" - group_item_strings = [] - for group_name, group in groups: - group_info = inspectutils.Info(group) - group_item = group_name - if 'docstring_info' in group_info: - group_docstring_info = group_info['docstring_info'] - if group_docstring_info: - group_item = _CreateItem(group_name, group_docstring_info.summary) - group_item_strings.append(group_item) - return ('GROUPS', _NewChoicesSection('GROUP', group_item_strings)) - - -def _CommandUsageDetailsSection(commands): - """Creates a section tuple for the commands section of the usage details.""" - command_item_strings = [] - for command_name, command in commands: - command_info = inspectutils.Info(command) - command_item = command_name - if 'docstring_info' in command_info: - command_docstring_info = command_info['docstring_info'] - if command_docstring_info: - command_item = _CreateItem(command_name, command_docstring_info.summary) - command_item_strings.append(command_item) - return ('COMMANDS', _NewChoicesSection('COMMAND', command_item_strings)) +def _MakeUsageDetailsSection(action_group): + """Creates a usage details section for the provided action group.""" + item_strings = [] + for name, member in action_group.GetItems(): + info = inspectutils.Info(member) + item = name + docstring_info = info.get('docstring_info') + if (docstring_info + and not custom_descriptions.NeedsCustomDescription(member)): + summary = docstring_info.summary + else: + summary = None + item = _CreateItem(name, summary) + item_strings.append(item) + return (action_group.plural.upper(), + _NewChoicesSection(action_group.name.upper(), item_strings)) def _ValuesUsageDetailsSection(component, values): """Creates a section tuple for the values section of the usage details.""" value_item_strings = [] - for value_name, value in values: + for value_name, value in values.GetItems(): del value init_info = inspectutils.Info(component.__class__.__init__) value_item = None @@ -590,34 +564,17 @@ def UsageTextForObject(component, trace=None, verbose=False): command = '' actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose) - groups, commands, values, indexes = actions_grouped_by_kind possible_actions = [] availability_lines = [] - if groups: - possible_actions.append('group') - groups_text = _CreateAvailabilityLine( - header='available groups:', - items=[name for name, _ in groups]) - availability_lines.append(groups_text) - if commands: - possible_actions.append('command') - commands_text = _CreateAvailabilityLine( - header='available commands:', - items=[name for name, _ in commands]) - availability_lines.append(commands_text) - if values: - possible_actions.append('value') - values_text = _CreateAvailabilityLine( - header='available values:', - items=[name for name, _ in values]) - availability_lines.append(values_text) - if indexes: - possible_actions.append('index') - indexes_text = _CreateAvailabilityLine( - header='available indexes:', - items=indexes) - availability_lines.append(indexes_text) + for action_group in actions_grouped_by_kind: + if action_group.members: + possible_actions.append(action_group.name) + availability_line = _CreateAvailabilityLine( + header='available {plural}:'.format(plural=action_group.plural), + items=action_group.names + ) + availability_lines.append(availability_line) if possible_actions: possible_actions_string = ' <{actions}>'.format( @@ -641,3 +598,21 @@ def _CreateAvailabilityLine(header, items, indented_items_text = formatting.Indent(items_text, spaces=items_indent) indented_header = formatting.Indent(header, spaces=header_indent) return indented_header + indented_items_text[len(indented_header):] + '\n' + + +class ActionGroup(object): + """A group of actions of the same kind.""" + + def __init__(self, name, plural): + self.name = name + self.plural = plural + self.names = [] + self.members = [] + + def Add(self, name, member=None): + self.names.append(name) + self.members.append(member) + + def GetItems(self): + return zip(self.names, self.members) + diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 18a1f1ae..66a106f5 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -80,6 +80,9 @@ class with an __init__ method. elif inspect.isbuiltin(fn): # If the function is a bound builtin, we skip the `self` argument. skip_arg = fn.__self__ is not None + elif not inspect.isfunction(fn): + # The purpose of this else clause is to set skip_arg for callable objects. + skip_arg = True return fn, skip_arg diff --git a/fire/test_components.py b/fire/test_components.py index 1ca39c14..a011919c 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -306,9 +306,14 @@ def matching_names(self): class CallableWithPositionalArgs(object): """Test class for supporting callable.""" + TEST = 1 + def __call__(self, x, y): return x + y + def foo(self, x): + return x + 1 + NamedTuplePoint = collections.namedtuple('NamedTuplePoint', ['x', 'y']) From 88999fd5664d24e52ab8477306b30aefa54369ae Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 15:25:27 -0700 Subject: [PATCH 023/205] Print "3+4j" instead of "(3+4j)". PiperOrigin-RevId: 260032586 Change-Id: I42c2a36bc65ea637435450b412ef3c634677ff85 --- fire/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fire/core.py b/fire/core.py index d3d51d93..7dbf6740 100644 --- a/fire/core.py +++ b/fire/core.py @@ -253,6 +253,9 @@ def _PrintResult(component_trace, verbose=False): print(_DictAsString(result, verbose)) elif isinstance(result, tuple): print(_OneLineResult(result)) + elif isinstance(result, complex): + # Print "3+4j" instead of "(3+4j)". + print(str(result).strip('()')) elif isinstance(result, value_types.VALUE_TYPES): if result is not None: print(result) From 8d47ddb0ff2eac9e5f61ab7f1063c5e7e476ae2f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 15:40:43 -0700 Subject: [PATCH 024/205] Show a separator as the continuation if no args are needed by a callable. PiperOrigin-RevId: 260035298 Change-Id: Ia5f54a1b78b45e11b00ecf2a2444724205811b96 --- fire/helptext.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fire/helptext.py b/fire/helptext.py index 7ae9bb0c..2b9d2de3 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -121,7 +121,13 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, if possible_actions: continuations.append(_GetPossibleActionsString(possible_actions)) if callable(component): - continuations.append(_GetArgsAndFlagsString(spec, metadata)) + callable_continuation = _GetArgsAndFlagsString(spec, metadata) + if callable_continuation: + continuations.append(callable_continuation) + elif trace: + # This continuation might be blank if no args are needed. + # In this case, show a separator. + continuations.append(trace.separator) continuation = ' | '.join(continuations) synopsis_template = '{current_command} {continuation}' From b05ae0d891657d924ab67ffa5da3fbc0a00f22d7 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 15:42:15 -0700 Subject: [PATCH 025/205] Use kwargs for calling HelpText and UsageText. PiperOrigin-RevId: 260035604 Change-Id: If680eea5e105b618ddc30068023c4b4ad72c9836 --- fire/core.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fire/core.py b/fire/core.py index 7dbf6740..f2ce54ad 100644 --- a/fire/core.py +++ b/fire/core.py @@ -144,7 +144,7 @@ def Fire(component=None, command=None, name=None): output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)] result = component_trace.GetResult() help_text = helptext.HelpText( - result, component_trace, component_trace.verbose) + result, trace=component_trace, verbose=component_trace.verbose) output.append(help_text) Display(output, out=sys.stderr) raise FireExit(0, component_trace) @@ -155,7 +155,7 @@ def Fire(component=None, command=None, name=None): if component_trace.show_help: result = component_trace.GetResult() help_text = helptext.HelpText( - result, component_trace, component_trace.verbose) + result, trace=component_trace, verbose=component_trace.verbose) output = [help_text] Display(output, out=sys.stderr) raise FireExit(0, component_trace) @@ -260,7 +260,8 @@ def _PrintResult(component_trace, verbose=False): if result is not None: print(result) else: - help_text = helptext.HelpText(result, component_trace, verbose) + help_text = helptext.HelpText( + result, trace=component_trace, verbose=verbose) output = [help_text] Display(output, out=sys.stdout) @@ -279,16 +280,16 @@ def _DisplayError(component_trace): 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) - help_text = helptext.HelpText(result, component_trace, - component_trace.verbose) + help_text = helptext.HelpText(result, trace=component_trace, + verbose=component_trace.verbose) output.append(help_text) Display(output, out=sys.stderr) else: print(formatting.Error('ERROR: ') + component_trace.elements[-1].ErrorAsStr(), file=sys.stderr) - error_text = helptext.UsageText(result, component_trace, - component_trace.verbose) + error_text = helptext.UsageText(result, trace=component_trace, + verbose=component_trace.verbose) print(error_text, file=sys.stderr) From c3d6b2d18fac35eef081b3e6065eb804000a1c6c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 15:54:50 -0700 Subject: [PATCH 026/205] fix line-too-long lint error PiperOrigin-RevId: 260037806 Change-Id: I7989608bdb1d4e75822630729e58d3ac6e03eced --- fire/docstrings_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 5f7eeeb1..5c805461 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -216,8 +216,10 @@ def test_multisection_docstring(self): docstring_info = docstrings.parse(docstring) expected_docstring_info = DocstringInfo( summary='Docstring summary.', - description='This is the first section of a docstring description.\n\n' - 'This is the second section of a docstring description. This docstring\n' + description='This is the first section of a docstring description.' + '\n\n' + 'This is the second section of a docstring description. This docstring' + '\n' 'description has just two sections.', ) self.assertEqual(docstring_info, expected_docstring_info) From 283e3a7c7589354576598af611a11e6c23018129 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:10:05 -0700 Subject: [PATCH 027/205] disable protected-access check for docstrings_test PiperOrigin-RevId: 260040819 Change-Id: If6b6eae54f59db76818fe662c24a857d77100b9a --- fire/docstrings_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 5c805461..23c46c5a 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -238,7 +238,7 @@ def test_strip_blank_lines(self): lines = [' ', ' foo ', ' '] expected_output = [' foo '] - self.assertEqual(docstrings._strip_blank_lines(lines), expected_output) + self.assertEqual(docstrings._strip_blank_lines(lines), expected_output) # pylint: disable=protected-access if __name__ == '__main__': From ba22c7841071c911cf691c7132d67409ef13a670 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:11:24 -0700 Subject: [PATCH 028/205] fix external lint errors. PiperOrigin-RevId: 260041059 Change-Id: Ic258e2fed908a36ccdbede96650c71625b16b61b --- fire/helptext.py | 1 - fire/test_components.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 2b9d2de3..1b04596e 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -621,4 +621,3 @@ def Add(self, name, member=None): def GetItems(self): return zip(self.names, self.members) - diff --git a/fire/test_components.py b/fire/test_components.py index a011919c..9c7ae0e5 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -311,7 +311,7 @@ class CallableWithPositionalArgs(object): def __call__(self, x, y): return x + y - def foo(self, x): + def fn(self, x): return x + 1 From 8de88360b6bf83aa2bb76cc64d117d0daa5e4dfc Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:12:50 -0700 Subject: [PATCH 029/205] Support for callable objects in usage test. Refactor of usage text. PiperOrigin-RevId: 260041306 Change-Id: Id14ddd3a935627a068ff4f4e3523e9cd9d90ce89 --- fire/completion.py | 47 ++++++++++-- fire/core.py | 11 ++- fire/helptext.py | 173 ++++++++++++++++++++---------------------- fire/helptext_test.py | 48 +++++++----- fire/inspectutils.py | 2 + 5 files changed, 160 insertions(+), 121 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index f1be1558..e18a27cc 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -281,7 +281,18 @@ def _FishScript(name, commands, default_options=None): ) -def MemberVisible(component, name, member, verbose): +def GetClassAttrsDict(component): + """Gets the attributes of the component class, as a dict with name keys.""" + if not inspect.isclass(component): + return None + class_attrs_list = inspect.classify_class_attrs(component) + return { + class_attr.name: class_attr + for class_attr in class_attrs_list + } + + +def MemberVisible(component, name, member, class_attrs=None, verbose=False): """Returns whether a member should be included in auto-completion or help. Determines whether a member of an object with the specified name should be @@ -297,6 +308,8 @@ def MemberVisible(component, name, member, verbose): component: The component containing the member. name: The name of the member. member: The member itself. + class_attrs: (optional) If component is a class, provide this as: + GetClassAttrsDict(component). If not provided, it will be computed. verbose: Whether to include private members. Returns A boolean value indicating whether the member should be included. @@ -310,6 +323,13 @@ def MemberVisible(component, name, member, verbose): if inspect.ismodule(member) and member is six: # TODO(dbieber): Determine more generally which modules to hide. return False + if inspect.isclass(component): + # If class_attrs has not been provided, compute it. + if class_attrs is None: + class_attrs = GetClassAttrsDict(class_attrs) + class_attr = class_attrs.get(name) + if class_attr and class_attr.kind == 'method': + return False if (six.PY2 and inspect.isfunction(component) and name in ('func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name')): @@ -322,7 +342,7 @@ def MemberVisible(component, name, member, verbose): return True # Default to including the member -def _Members(component, verbose=False): +def VisibleMembers(component, class_attrs=None, verbose=False): """Returns a list of the members of the given component. If verbose is True, then members starting with _ (normally ignored) are @@ -330,6 +350,12 @@ def _Members(component, verbose=False): Args: component: The component whose members to list. + class_attrs: (optional) If component is a class, you may provide this as: + GetClassAttrsDict(component). If not provided, it will be computed. + If provided, this determines how class members will be treated for + visibility. In particular, methods are generally hidden for + non-instantiated classes, but if you wish them to be shown (e.g. for + completion scripts) then pass in a different class_attr for them. verbose: Whether to include private members. Returns: A list of tuples (member_name, member) of all members of the component. @@ -339,10 +365,13 @@ def _Members(component, verbose=False): else: members = inspect.getmembers(component) + # If class_attrs has not been provided, compute it. + if class_attrs is None: + class_attrs = GetClassAttrsDict(component) return [ - (member_name, member) - for member_name, member in members - if MemberVisible(component, member_name, member, verbose) + (member_name, member) for member_name, member in members + if MemberVisible(component, member_name, member, class_attrs=class_attrs, + verbose=verbose) ] @@ -386,7 +415,7 @@ def Completions(component, verbose=False): return [ _FormatForCommand(member_name) - for member_name, unused_member in _Members(component, verbose) + for member_name, _ in VisibleMembers(component, verbose=verbose) ] @@ -427,7 +456,7 @@ def _Commands(component, depth=3): Only traverses the member DAG up to a depth of depth. """ if inspect.isroutine(component) or inspect.isclass(component): - for completion in Completions(component): + for completion in Completions(component, verbose=False): yield (completion,) if inspect.isroutine(component): return # Don't descend into routines. @@ -435,7 +464,9 @@ def _Commands(component, depth=3): if depth < 1: return - for member_name, member in _Members(component): + # By setting class_attrs={} we don't hide methods in completion. + for member_name, member in VisibleMembers(component, class_attrs={}, + verbose=False): # TODO(dbieber): Also skip components we've already seen. member_name = _FormatForCommand(member_name) diff --git a/fire/core.py b/fire/core.py index f2ce54ad..6903ae97 100644 --- a/fire/core.py +++ b/fire/core.py @@ -306,8 +306,12 @@ def _DictAsString(result, verbose=False): # 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 completion.MemberVisible(result, key, value, verbose)} + class_attrs = completion.GetClassAttrsDict(result) + result_visible = { + key: value for key, value in result.items() + if completion.MemberVisible(result, key, value, + class_attrs=class_attrs, verbose=verbose) + } if not result_visible: return '{}' @@ -317,7 +321,8 @@ def _DictAsString(result, verbose=False): lines = [] for key, value in result.items(): - if completion.MemberVisible(result, key, value, verbose): + if completion.MemberVisible(result, key, value, class_attrs=class_attrs, + verbose=verbose): line = format_string.format(key=str(key) + ':', value=_OneLineResult(value)) lines.append(line) diff --git a/fire/helptext.py b/fire/helptext.py index 1b04596e..68dbfe7a 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -33,8 +33,6 @@ from __future__ import division from __future__ import print_function -import inspect - from fire import completion from fire import custom_descriptions from fire import decorators @@ -319,7 +317,7 @@ def _GetActionsGroupedByKind(component, verbose=False): values = ActionGroup(name='value', plural='values') indexes = ActionGroup(name='index', plural='indexes') - members = completion._Members(component, verbose) # pylint: disable=protected-access + members = completion.VisibleMembers(component, verbose=verbose) for member_name, member in members: member_name = str(member_name) if value_types.IsGroup(member): @@ -456,14 +454,7 @@ def _NewChoicesSection(name, choices): def UsageText(component, trace=None, verbose=False): - if inspect.isroutine(component) or inspect.isclass(component): - return UsageTextForFunction(component, trace, verbose) - else: - return UsageTextForObject(component, trace, verbose) - - -def UsageTextForFunction(component, trace=None, verbose=False): - """Returns usage text for function objects. + """Returns usage text for the given component. Args: component: The component to determine the usage text for. @@ -473,13 +464,12 @@ def UsageTextForFunction(component, trace=None, verbose=False): Returns: String suitable for display in an error screen. """ - del verbose # Unused. - - output_template = """Usage: {current_command} {args_and_flags} + output_template = """Usage: {continued_command} {availability_lines} For detailed information on this command, run: - {current_command}{hyphen_hyphen} --help""" + {help_command}""" + # Get the command so far: if trace: command = trace.GetCommand() needs_separating_hyphen_hyphen = trace.NeedsSeparatingHyphenHyphen() @@ -490,13 +480,67 @@ def UsageTextForFunction(component, trace=None, verbose=False): if not command: command = '' + # Build the continuations for the command: + continued_command = command + spec = inspectutils.GetFullArgSpec(component) + metadata = decorators.GetMetadata(component) + + # Usage for objects. + actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose) + possible_actions = _GetPossibleActions(actions_grouped_by_kind) + + continuations = [] + if possible_actions: + continuations.append(_GetPossibleActionsUsageString(possible_actions)) + + availability_lines = _UsageAvailabilityLines(actions_grouped_by_kind) + + if callable(component): + callable_items = _GetCallableUsageItems(spec, metadata) + continuations.append(' '.join(callable_items)) + availability_lines.extend(_GetCallableAvailabilityLines(spec)) + + if continuations: + continued_command += ' ' + ' | '.join(continuations) + help_command = ( + command + + (' -- ' if needs_separating_hyphen_hyphen else ' ') + + '--help' + ) + + return output_template.format( + continued_command=continued_command, + availability_lines=''.join(availability_lines), + help_command=help_command) + + +def _GetPossibleActionsUsageString(possible_actions): + if possible_actions: + return '<{actions}>'.format(actions='|'.join(possible_actions)) + return None + + +def _UsageAvailabilityLines(actions_grouped_by_kind): + availability_lines = [] + for action_group in actions_grouped_by_kind: + if action_group.members: + availability_line = _CreateAvailabilityLine( + header='available {plural}:'.format(plural=action_group.plural), + items=action_group.names + ) + availability_lines.append(availability_line) + return availability_lines + + +def _GetCallableUsageItems(spec, metadata): + """A list of elements that comprise the usage summary for a callable.""" args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] # Check if positional args are allowed. If not, show flag syntax for args. - metadata = decorators.GetMetadata(component) accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) + if not accepts_positional_args: items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper()) for arg in args_with_no_defaults] @@ -507,6 +551,17 @@ def UsageTextForFunction(component, trace=None, verbose=False): if args_with_defaults or spec.kwonlyargs or spec.varkw: items.append('') + if spec.varargs: + items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) + + return items + + +def _GetCallableAvailabilityLines(spec): + """The list of availability lines for a callable for use in a usage string.""" + args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] + + # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args. optional_flags = [('--' + flag) for flag in args_with_defaults] required_flags = [('--' + flag) for flag in spec.kwonlyargs] @@ -514,86 +569,20 @@ def UsageTextForFunction(component, trace=None, verbose=False): availability_lines = [] if optional_flags: availability_lines.append( - _CreateAvailabilityLine(header='Optional flags:', items=optional_flags, - header_indent=0)) + _CreateAvailabilityLine(header='optional flags:', items=optional_flags, + header_indent=2)) if required_flags: availability_lines.append( - _CreateAvailabilityLine(header='Required flags:', items=required_flags, - header_indent=0)) + _CreateAvailabilityLine(header='required flags:', items=required_flags, + header_indent=2)) if spec.varkw: - additional_flags = ('Additional flags are accepted.' + additional_flags = ('additional flags are accepted' if optional_flags or required_flags else - 'Flags are accepted.') - availability_lines.append(additional_flags + '\n') - - if availability_lines: - # Start the section with blank lines. - availability_lines.insert(0, '\n') - - if spec.varargs: - items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) - - args_and_flags = ' '.join(items) - - hyphen_hyphen = ' --' if needs_separating_hyphen_hyphen else '' - - return output_template.format( - current_command=command, - args_and_flags=args_and_flags, - availability_lines=''.join(availability_lines), - hyphen_hyphen=hyphen_hyphen) - - -def UsageTextForObject(component, trace=None, verbose=False): - """Returns the usage text for the error screen for an object. - - Constructs the usage text for the error screen to inform the user about how - to use the current component. - - Args: - component: The component to determine the usage text for. - trace: The Fire trace object containing all metadata of current execution. - verbose: Whether to include private members in the usage text. - Returns: - String suitable for display in error screen. - """ - output_template = """Usage: {current_command}{possible_actions} -{availability_lines} -For detailed information on this command, run: - {current_command} --help""" - if trace: - command = trace.GetCommand() - else: - command = None - - if not command: - command = '' - - actions_grouped_by_kind = _GetActionsGroupedByKind(component, verbose=verbose) - - possible_actions = [] - availability_lines = [] - for action_group in actions_grouped_by_kind: - if action_group.members: - possible_actions.append(action_group.name) - availability_line = _CreateAvailabilityLine( - header='available {plural}:'.format(plural=action_group.plural), - items=action_group.names - ) - availability_lines.append(availability_line) - - if possible_actions: - possible_actions_string = ' <{actions}>'.format( - actions='|'.join(possible_actions)) - else: - possible_actions_string = '' - - availability_lines_string = ''.join(availability_lines) - - return output_template.format( - current_command=command, - possible_actions=possible_actions_string, - availability_lines=availability_lines_string) + 'flags are accepted') + availability_lines.append( + _CreateAvailabilityLine(header=additional_flags, items=[], + header_indent=2)) + return availability_lines def _CreateAvailabilityLine(header, items, diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 33676420..188bdb28 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -317,7 +317,7 @@ def testUsageOutputMethod(self): component = tc.NoDefaults().double t = trace.FireTrace(component, name='NoDefaults') t.AddAccessedProperty(component, 'double', ['double'], None, None) - usage_output = helptext.UsageText(component, trace=t, verbose=True) + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = ''' Usage: NoDefaults double COUNT @@ -330,11 +330,10 @@ def testUsageOutputMethod(self): def testUsageOutputFunctionWithHelp(self): component = tc.function_with_help t = trace.FireTrace(component, name='function_with_help') - usage_output = helptext.UsageText(component, trace=t, verbose=True) + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = ''' Usage: function_with_help - - Optional flags: --help + optional flags: --help For detailed information on this command, run: function_with_help -- --help''' @@ -345,17 +344,16 @@ def testUsageOutputFunctionWithHelp(self): def testUsageOutputFunctionWithDocstring(self): component = tc.multiplier_with_docstring t = trace.FireTrace(component, name='multiplier_with_docstring') - usage_output = helptext.UsageText(component, trace=t, verbose=True) + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = ''' Usage: multiplier_with_docstring NUM - - Optional flags: --rate + optional flags: --rate For detailed information on this command, run: multiplier_with_docstring --help''' self.assertEqual( - usage_output, - textwrap.dedent(expected_output).lstrip('\n')) + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) @testutils.skip('The functionality is not implemented yet') def testUsageOutputCallable(self): @@ -373,21 +371,35 @@ def testUsageOutputCallable(self): For detailed information on this command, run: CallableWithKeywordArgument -- --help''' self.assertEqual( - usage_output, - textwrap.dedent(expected_output).lstrip('\n')) + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) def testUsageOutputConstructorWithParameter(self): component = tc.InstanceVars t = trace.FireTrace(component, name='InstanceVars') - usage_output = helptext.UsageText(component, trace=t, verbose=True) + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = ''' Usage: InstanceVars --arg1=ARG1 --arg2=ARG2 For detailed information on this command, run: InstanceVars --help''' self.assertEqual( - usage_output, - textwrap.dedent(expected_output).lstrip('\n')) + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) + + def testUsageOutputConstructorWithParameterVerbose(self): + component = tc.InstanceVars + t = trace.FireTrace(component, name='InstanceVars') + usage_output = helptext.UsageText(component, trace=t, verbose=True) + expected_output = ''' + Usage: InstanceVars | --arg1=ARG1 --arg2=ARG2 + available commands: run + + For detailed information on this command, run: + InstanceVars --help''' + self.assertEqual( + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) def testUsageOutputEmptyDict(self): component = {} @@ -399,8 +411,8 @@ def testUsageOutputEmptyDict(self): For detailed information on this command, run: EmptyDict --help''' self.assertEqual( - usage_output, - textwrap.dedent(expected_output).lstrip('\n')) + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) def testUsageOutputNone(self): component = None @@ -412,8 +424,8 @@ def testUsageOutputNone(self): For detailed information on this command, run: None --help''' self.assertEqual( - usage_output, - textwrap.dedent(expected_output).lstrip('\n')) + textwrap.dedent(expected_output).lstrip('\n'), + usage_output) @testutils.skip('Only passes in Python 3 for now.') def testInitRequiresFlagSyntaxSubclassNamedTuple(self): diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 66a106f5..b612b859 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -112,6 +112,8 @@ def GetFullArgSpec(fn): # Case 1: Builtins accept args. if inspect.isbuiltin(fn): + # TODO(dbieber): Try parsing the docstring, if available. + # TODO(dbieber): Use known argspecs, like set.add and namedtuple.count. return FullArgSpec(varargs='vars', varkw='kwargs') # Case 2: namedtuples store their args in their _fields attribute. From 05080ec3afa6614703770bd06ade01d042a68e8c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:21:37 -0700 Subject: [PATCH 030/205] Don't show properties on uninstantiated classes outside verbose mode / completion scripts. PiperOrigin-RevId: 260042848 Change-Id: Ia773c9922a1138693b8fb17d7f5595289ea5d35b --- fire/completion.py | 4 +++- fire/helptext_test.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index e18a27cc..19d068fd 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -328,7 +328,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): if class_attrs is None: class_attrs = GetClassAttrsDict(class_attrs) class_attr = class_attrs.get(name) - if class_attr and class_attr.kind == 'method': + if class_attr and class_attr.kind in ('method', 'property'): + # methods and properties should be accessed on instantiated objects, + # not uninstantiated classes. return False if (six.PY2 and inspect.isfunction(component) and name in ('func_closure', 'func_code', 'func_defaults', diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 188bdb28..00bdcc2a 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -427,11 +427,10 @@ def testUsageOutputNone(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) - @testutils.skip('Only passes in Python 3 for now.') def testInitRequiresFlagSyntaxSubclassNamedTuple(self): component = tc.SubPoint t = trace.FireTrace(component, name='SubPoint') - usage_output = helptext.UsageText(component, trace=t, verbose=True) + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = 'Usage: SubPoint --x=X --y=Y' self.assertIn(expected_output, usage_output) From 88f392319afd22d95f342fc7e0b09a204873a0b0 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:38:17 -0700 Subject: [PATCH 031/205] enable test for callable object usage PiperOrigin-RevId: 260045817 Change-Id: I700cef5d0676fae7c79221beafcf07373f268a46 --- fire/helptext.py | 5 ++++- fire/helptext_test.py | 18 ++++++++---------- fire/inspectutils.py | 12 +++++++++++- fire/test_components.py | 3 +++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 68dbfe7a..2c360d2b 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -498,7 +498,10 @@ def UsageText(component, trace=None, verbose=False): if callable(component): callable_items = _GetCallableUsageItems(spec, metadata) - continuations.append(' '.join(callable_items)) + if callable_items: + continuations.append(' '.join(callable_items)) + elif trace: + continuations.append(trace.separator) availability_lines.extend(_GetCallableAvailabilityLines(spec)) if continuations: diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 00bdcc2a..d3850cc7 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -355,18 +355,16 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) - @testutils.skip('The functionality is not implemented yet') def testUsageOutputCallable(self): - # This is both a group and a command! - component = tc.CallableWithKeywordArgument - t = trace.FireTrace(component, name='CallableWithKeywordArgument') - usage_output = helptext.UsageText(component, trace=t, verbose=True) - # TODO(joejoevictor): We need to handle the case for keyword args as well - # i.e. __call__ method of CallableWithKeywordArgument + # This is both a group and a command. + component = tc.CallableWithKeywordArgument() + t = trace.FireTrace(component, name='CallableWithKeywordArgument', + separator='@') + usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = ''' - Usage: CallableWithKeywordArgument - - Available commands: print_msg + Usage: CallableWithKeywordArgument | + available commands: print_msg + flags are accepted For detailed information on this command, run: CallableWithKeywordArgument -- --help''' diff --git a/fire/inspectutils.py b/fire/inspectutils.py index b612b859..34c92317 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -86,6 +86,16 @@ class with an __init__ method. return fn, skip_arg +def Py2GetArgSpec(fn): + """A wrapper around getargspec that tries both fn and fn.__call__.""" + try: + return inspect.getargspec(fn) # pylint: disable=deprecated-method + except TypeError: + if hasattr(fn, '__call__'): + return inspect.getargspec(fn.__call__) # pylint: disable=deprecated-method + raise + + def GetFullArgSpec(fn): """Returns a FullArgSpec describing the given callable.""" @@ -94,7 +104,7 @@ def GetFullArgSpec(fn): try: if six.PY2: - args, varargs, varkw, defaults = inspect.getargspec(fn) # pylint: disable=deprecated-method + args, varargs, varkw, defaults = Py2GetArgSpec(fn) kwonlyargs = kwonlydefaults = None annotations = getattr(fn, '__annotations__', None) else: diff --git a/fire/test_components.py b/fire/test_components.py index 9c7ae0e5..6e3b7015 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -336,6 +336,9 @@ def print_msg(self, msg): print(msg) +callable_with_keyword_argument = CallableWithKeywordArgument() + + class ClassWithDocstring(object): """Test class for testing help text output. From 86e2c355517d51488488068cf833185442e91d3c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:39:33 -0700 Subject: [PATCH 032/205] Set version to 0.2.0. PiperOrigin-RevId: 260046043 Change-Id: I1f6f28c2b6984cb8645fa1b8f34931fcb0c30e9f --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index e15450db..1e92b531 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.1.4' +__version__ = '0.2.0' diff --git a/setup.py b/setup.py index 0e34a55f..68103649 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.1.4' +VERSION = '0.2.0' URL = 'https://github.com/google/python-fire' setup( From 962907495556305385ee73eaecf9ff6e83ebe6cd Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 16:56:53 -0700 Subject: [PATCH 033/205] Triple double-quotes, not triple single-quotes and linter error for callable_with_keyword_argument PiperOrigin-RevId: 260049158 Change-Id: I2b0cd024f2f700d73a60d972a0365c6674112c0f --- fire/helptext_test.py | 40 ++++++++++++++++++++-------------------- fire/test_components.py | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index d3850cc7..1371e2a1 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -288,12 +288,12 @@ def testUsageOutput(self): component = tc.NoDefaults() t = trace.FireTrace(component, name='NoDefaults') usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: NoDefaults available commands: double | triple For detailed information on this command, run: - NoDefaults --help''' + NoDefaults --help""" self.assertEqual( usage_output, @@ -303,12 +303,12 @@ def testUsageOutputVerbose(self): component = tc.NoDefaults() t = trace.FireTrace(component, name='NoDefaults') usage_output = helptext.UsageText(component, trace=t, verbose=True) - expected_output = ''' + expected_output = """ Usage: NoDefaults available commands: double | triple For detailed information on this command, run: - NoDefaults --help''' + NoDefaults --help""" self.assertEqual( usage_output, textwrap.dedent(expected_output).lstrip('\n')) @@ -318,11 +318,11 @@ def testUsageOutputMethod(self): t = trace.FireTrace(component, name='NoDefaults') t.AddAccessedProperty(component, 'double', ['double'], None, None) usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: NoDefaults double COUNT For detailed information on this command, run: - NoDefaults double --help''' + NoDefaults double --help""" self.assertEqual( usage_output, textwrap.dedent(expected_output).lstrip('\n')) @@ -331,12 +331,12 @@ def testUsageOutputFunctionWithHelp(self): component = tc.function_with_help t = trace.FireTrace(component, name='function_with_help') usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: function_with_help optional flags: --help For detailed information on this command, run: - function_with_help -- --help''' + function_with_help -- --help""" self.assertEqual( usage_output, textwrap.dedent(expected_output).lstrip('\n')) @@ -345,12 +345,12 @@ def testUsageOutputFunctionWithDocstring(self): component = tc.multiplier_with_docstring t = trace.FireTrace(component, name='multiplier_with_docstring') usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: multiplier_with_docstring NUM optional flags: --rate For detailed information on this command, run: - multiplier_with_docstring --help''' + multiplier_with_docstring --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) @@ -361,13 +361,13 @@ def testUsageOutputCallable(self): t = trace.FireTrace(component, name='CallableWithKeywordArgument', separator='@') usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: CallableWithKeywordArgument | available commands: print_msg flags are accepted For detailed information on this command, run: - CallableWithKeywordArgument -- --help''' + CallableWithKeywordArgument -- --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) @@ -376,11 +376,11 @@ def testUsageOutputConstructorWithParameter(self): component = tc.InstanceVars t = trace.FireTrace(component, name='InstanceVars') usage_output = helptext.UsageText(component, trace=t, verbose=False) - expected_output = ''' + expected_output = """ Usage: InstanceVars --arg1=ARG1 --arg2=ARG2 For detailed information on this command, run: - InstanceVars --help''' + InstanceVars --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) @@ -389,12 +389,12 @@ def testUsageOutputConstructorWithParameterVerbose(self): component = tc.InstanceVars t = trace.FireTrace(component, name='InstanceVars') usage_output = helptext.UsageText(component, trace=t, verbose=True) - expected_output = ''' + expected_output = """ Usage: InstanceVars | --arg1=ARG1 --arg2=ARG2 available commands: run For detailed information on this command, run: - InstanceVars --help''' + InstanceVars --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) @@ -403,11 +403,11 @@ def testUsageOutputEmptyDict(self): component = {} t = trace.FireTrace(component, name='EmptyDict') usage_output = helptext.UsageText(component, trace=t, verbose=True) - expected_output = ''' + expected_output = """ Usage: EmptyDict For detailed information on this command, run: - EmptyDict --help''' + EmptyDict --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) @@ -416,11 +416,11 @@ def testUsageOutputNone(self): component = None t = trace.FireTrace(component, name='None') usage_output = helptext.UsageText(component, trace=t, verbose=True) - expected_output = ''' + expected_output = """ Usage: None For detailed information on this command, run: - None --help''' + None --help""" self.assertEqual( textwrap.dedent(expected_output).lstrip('\n'), usage_output) diff --git a/fire/test_components.py b/fire/test_components.py index 6e3b7015..a3e049b7 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -336,7 +336,7 @@ def print_msg(self, msg): print(msg) -callable_with_keyword_argument = CallableWithKeywordArgument() +CALLABLE_WITH_KEYWORD_ARGUMENT = CallableWithKeywordArgument() class ClassWithDocstring(object): From 18ba635631b9aca7670f9b7a7979c0bf684ef1d9 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 25 Jul 2019 21:10:44 -0700 Subject: [PATCH 034/205] Allow custom types to define their own serialization with __str__. PiperOrigin-RevId: 260078541 Change-Id: I2999e0caaaf962708ab1de4272b1df58327b2ada --- docs/guide.md | 12 +++++++++++- docs/using-cli.md | 2 ++ fire/core.py | 12 +++++++++--- fire/inspectutils.py | 1 - 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 7f610699..2e59ce8a 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -413,7 +413,7 @@ if __name__ == '__main__': Now we can draw stuff :). ```bash -$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on __str__ +$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -428,6 +428,16 @@ $ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on It's supposed to be a smiley face. +### Custom Serialization + +You'll notice in the BinaryCanvas example, the canvas with the smiley face was +printed to the screen. You can determine how a component will be serialized by +defining its `__str__` method. + +If a custom `__str__` method is present on the final component, the object is +serialized and printed. If there's no custom `__str__` method, then the help +screen for the object is shown instead. + ### Can we make an even simpler example than Hello World? Yes, this program is even simpler than our original Hello World example. diff --git a/docs/using-cli.md b/docs/using-cli.md index 236a8228..4cd95e14 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -91,6 +91,8 @@ the arguments of the class's \_\_init\_\_ function. Arguments must be specified by name, using the flags syntax. See the section on [calling a function](#calling-a-function) for more details. +Similarly, when passing arguments to a callable object (an object with a custom +`__call__` function), those arguments must be passed using flags syntax. ## Using Flags with Fire CLIs diff --git a/fire/core.py b/fire/core.py index 6903ae97..dabfb865 100644 --- a/fire/core.py +++ b/fire/core.py @@ -244,6 +244,15 @@ def _PrintResult(component_trace, verbose=False): # and move serialization to its own module. result = component_trace.GetResult() + if hasattr(result, '__str__'): + # If the object has a custom __str__ method, rather than one inherited from + # object, then we use that to serialize the object. + class_attrs = completion.GetClassAttrsDict(type(result)) + str_attr = class_attrs.get('__str__') + if str_attr and str_attr.defining_class is not object: + print(str(result)) + return + if isinstance(result, (list, set, frozenset, types.GeneratorType)): for i in result: print(_OneLineResult(i)) @@ -253,9 +262,6 @@ def _PrintResult(component_trace, verbose=False): print(_DictAsString(result, verbose)) elif isinstance(result, tuple): print(_OneLineResult(result)) - elif isinstance(result, complex): - # Print "3+4j" instead of "(3+4j)". - print(str(result).strip('()')) elif isinstance(result, value_types.VALUE_TYPES): if result is not None: print(result) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 34c92317..aa130e05 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -98,7 +98,6 @@ def Py2GetArgSpec(fn): def GetFullArgSpec(fn): """Returns a FullArgSpec describing the given callable.""" - original_fn = fn fn, skip_arg = _GetArgSpecInfo(fn) From d77453938a4b7a5a2bb71d9cb40397ee8bbc2e0a Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 26 Jul 2019 09:10:44 -0700 Subject: [PATCH 035/205] make pytype happy w/ __str__ feature. PiperOrigin-RevId: 260155248 Change-Id: Iac4795a8177df8bf9108efe916035f94507e8902 --- fire/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/core.py b/fire/core.py index dabfb865..07993de9 100644 --- a/fire/core.py +++ b/fire/core.py @@ -247,7 +247,7 @@ def _PrintResult(component_trace, verbose=False): if hasattr(result, '__str__'): # If the object has a custom __str__ method, rather than one inherited from # object, then we use that to serialize the object. - class_attrs = completion.GetClassAttrsDict(type(result)) + class_attrs = completion.GetClassAttrsDict(type(result)) or {} str_attr = class_attrs.get('__str__') if str_attr and str_attr.defining_class is not object: print(str(result)) From a54ef58d16168d1e337a360a6f978072c8514117 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 26 Jul 2019 11:51:36 -0700 Subject: [PATCH 036/205] Adds BinaryCanvas as test component. PiperOrigin-RevId: 260185289 Change-Id: I8c8354359486b9f0c63a7c524eb9a904de6c3249 --- fire/test_components.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fire/test_components.py b/fire/test_components.py index a3e049b7..e35027c7 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -465,3 +465,36 @@ def fn_with_code_in_docstring(): True. """ return True + + +class BinaryCanvas(object): + """A canvas with which to make binary art, one bit at a time.""" + + def __init__(self, size=10): + self.pixels = [[0] * size for _ in range(size)] + self._size = size + self._row = 0 # The row of the cursor. + self._col = 0 # The column of the cursor. + + def __str__(self): + return '\n'.join( + ' '.join(str(pixel) for pixel in row) for row in self.pixels) + + def show(self): + print(self) + return self + + def move(self, row, col): + self._row = row % self._size + self._col = col % self._size + return self + + def on(self): + return self.set(1) + + def off(self): + return self.set(0) + + def set(self, value): + self.pixels[self._row][self._col] = value + return self From 0fa5c42ac8c1dddd15c82ca3607bc6923f971cc9 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 29 Jul 2019 15:32:39 -0700 Subject: [PATCH 037/205] robustify docstring parser to blank lines after section starts. Resolves https://github.com/google/python-fire/issues/183 PiperOrigin-RevId: 260593779 Change-Id: Iab9052b9d56b24d65df9beb3ed8add68e2288926 --- fire/docstrings.py | 17 +++++++---------- fire/docstrings_test.py | 37 +++++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index b15cddce..d14c4544 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -188,14 +188,9 @@ def parse(docstring): yields = _join_lines(state.yields.lines) raises = _join_lines(state.raises.lines) - args = [ - ArgInfo( - name=arg.name, - type=_cast_to_known_type(_join_lines(arg.type.lines)), - description=_join_lines(arg.description.lines), - ) - for arg in state.args - ] + args = [ArgInfo( + name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)), + description=_join_lines(arg.description.lines)) for arg in state.args] return DocstringInfo( summary=summary, @@ -503,10 +498,12 @@ def _create_line_info(line, next_line): line_info.remaining_raw = line_info.line line_info.remaining = line_info.stripped line_info.indentation = len(line) - len(line.lstrip()) + # TODO(dbieber): If next_line is blank, use the next non-blank line. line_info.next.line = next_line - line_info.next.stripped = next_line.strip() if next_line else None + next_line_exists = next_line is not None + line_info.next.stripped = next_line.strip() if next_line_exists else None line_info.next.indentation = ( - len(next_line) - len(next_line.lstrip()) if next_line else None) + len(next_line) - len(next_line.lstrip()) if next_line_exists else None) # Note: This counts all whitespace equally. return line_info diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 23c46c5a..adb89492 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -34,7 +34,7 @@ def test_one_line_simple(self): expected_docstring_info = DocstringInfo( summary='A simple one line docstring.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_one_line_simple_whitespace(self): docstring = """ @@ -44,7 +44,7 @@ def test_one_line_simple_whitespace(self): expected_docstring_info = DocstringInfo( summary='A simple one line docstring.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_one_line_too_long(self): # pylint: disable=line-too-long @@ -57,7 +57,7 @@ def test_one_line_too_long(self): 'a little too long so it keeps going well beyond a reasonable length ' 'for a one-liner.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_one_line_runs_over(self): # pylint: disable=line-too-long @@ -70,7 +70,7 @@ def test_one_line_runs_over(self): summary='A one line docstring thats both a little too verbose and ' 'a little too long so it runs onto a second line.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_one_line_runs_over_whitespace(self): docstring = """ @@ -82,7 +82,7 @@ def test_one_line_runs_over_whitespace(self): summary='A one line docstring thats both a little too verbose and ' 'a little too long so it runs onto a second line.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_google_format_args_only(self): docstring = """One line description. @@ -99,7 +99,7 @@ def test_google_format_args_only(self): ArgInfo(name='arg2', description='arg2_description'), ] ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_google_format_arg_named_args(self): docstring = """ @@ -112,7 +112,7 @@ def test_google_format_arg_named_args(self): ArgInfo(name='args', description='arg_description'), ] ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_google_format_typed_args_and_returns(self): docstring = """Docstring summary. @@ -140,7 +140,7 @@ def test_google_format_typed_args_and_returns(self): ], returns='bool: The return value. True for success, False otherwise.' ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_rst_format_typed_args_and_returns(self): docstring = """Docstring summary. @@ -169,7 +169,7 @@ def test_rst_format_typed_args_and_returns(self): returns='int -- description of the return value.', raises='AttributeError, KeyError', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_numpy_format_typed_args_and_returns(self): docstring = """Docstring summary. @@ -203,7 +203,7 @@ def test_numpy_format_typed_args_and_returns(self): # TODO(dbieber): Support return type. returns='bool True if successful, False otherwise.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) def test_multisection_docstring(self): docstring = """Docstring summary. @@ -222,7 +222,20 @@ def test_multisection_docstring(self): '\n' 'description has just two sections.', ) - self.assertEqual(docstring_info, expected_docstring_info) + self.assertEqual(expected_docstring_info, docstring_info) + + def test_google_section_with_blank_first_line(self): + docstring = """Inspired by requests HTTPAdapter docstring. + + :param x: Simple param. + + Usage: + + >>> import requests + """ + docstring_info = docstrings.parse(docstring) + self.assertEqual('Inspired by requests HTTPAdapter docstring.', + docstring_info.summary) def test_ill_formed_docstring(self): docstring = """Docstring summary. @@ -238,7 +251,7 @@ def test_strip_blank_lines(self): lines = [' ', ' foo ', ' '] expected_output = [' foo '] - self.assertEqual(docstrings._strip_blank_lines(lines), expected_output) # pylint: disable=protected-access + self.assertEqual(expected_output, docstrings._strip_blank_lines(lines)) # pylint: disable=protected-access if __name__ == '__main__': From c1a7e2f21fb95e37d80d3e32c7ecbefa12140edf Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 30 Jul 2019 11:28:50 -0700 Subject: [PATCH 038/205] Set version to 0.2.1. PiperOrigin-RevId: 260758966 Change-Id: I46fd2a5fbbbfa33c86782a25c9e92b387c6fb03d --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index 1e92b531..2d8f61b5 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.2.0' +__version__ = '0.2.1' diff --git a/setup.py b/setup.py index 68103649..44b7d7be 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.2.0' +VERSION = '0.2.1' URL = 'https://github.com/google/python-fire' setup( From f01aad347632791e3438c1a753e42a514520d690 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 5 Aug 2019 15:37:25 -0700 Subject: [PATCH 039/205] Prevent error when all lines are blank in _strip_blank_lines. PiperOrigin-RevId: 261784747 Change-Id: I40ac24b553b2b4fa081de3fe0b1250472e1011a7 --- fire/docstrings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index d14c4544..a58a42fe 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -212,7 +212,8 @@ def _strip_blank_lines(lines): """ # Find the first non-blank line. start = 0 - while lines and _is_blank(lines[start]): + num_lines = len(lines) + while lines and start < num_lines and _is_blank(lines[start]): start += 1 lines = lines[start:] From a7810582f81fef2e8fc99db4917f6eed8b74d886 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 8 Aug 2019 12:08:43 -0700 Subject: [PATCH 040/205] Set version number to 0.2.2 PiperOrigin-RevId: 262405343 Change-Id: Ia1e7b79b492618f7a965ef6ce8113fb5d4a437c9 --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index 2d8f61b5..9eff51c4 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.2.1' +__version__ = '0.2.2' diff --git a/setup.py b/setup.py index 44b7d7be..5ddbb483 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.2.1' +VERSION = '0.2.2' URL = 'https://github.com/google/python-fire' setup( From 720c0aa209c86bdb77b748a92b239a0edd9c2261 Mon Sep 17 00:00:00 2001 From: williamfzc <178894043@qq.com> Date: Fri, 6 Sep 2019 01:23:31 +0800 Subject: [PATCH 041/205] add pypi version badge in README (#192) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 862f5f23..8e4a3ec8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) +# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) [![PyPI version](https://badge.fury.io/py/fire.svg)](https://badge.fury.io/py/fire) _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ From 8d372e2a67d1ad8135eca074629b52ddbca96f25 Mon Sep 17 00:00:00 2001 From: Geoff Bacon Date: Sat, 7 Sep 2019 18:27:42 -0700 Subject: [PATCH 042/205] Exposes builtin functions from the standard library (#193) * fixes #187 --- fire/inspectutils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index aa130e05..d0e74e33 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -19,6 +19,7 @@ from __future__ import print_function import inspect +import types from fire import docstrings import six @@ -70,7 +71,7 @@ class with an __init__ method. """ skip_arg = False if inspect.isclass(fn): - # If the function is a class, we try to use it's init method. + # If the function is a class, we try to use its init method. skip_arg = True if six.PY2 and hasattr(fn, '__init__'): fn = fn.__init__ @@ -78,8 +79,11 @@ class with an __init__ method. # If the function is a bound method, we skip the `self` argument. skip_arg = fn.__self__ is not None elif inspect.isbuiltin(fn): - # If the function is a bound builtin, we skip the `self` argument. - skip_arg = fn.__self__ is not None + # If the function is a bound builtin, we skip the `self` argument, unless + # the function is from a standard library module in which case its __self__ + # attribute is that module. + if not isinstance(fn.__self__, types.ModuleType): + skip_arg = True elif not inspect.isfunction(fn): # The purpose of this else clause is to set skip_arg for callable objects. skip_arg = True From 4695f34429d22d78aab5d6bfc7b155b23a7165d5 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 5 Dec 2019 21:30:16 +0000 Subject: [PATCH 043/205] Add disable=import-outside-toplevel pylint lines. PiperOrigin-RevId: 282375002 Change-Id: I83c9354df82baa8d31fb068cf06a02df35341c50 --- fire/inspectutils.py | 2 +- fire/interact.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index d0e74e33..bf197f3f 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -195,7 +195,7 @@ def Info(component): A dict with information about the component. """ try: - from IPython.core import oinspect # pylint: disable=g-import-not-at-top + from IPython.core import oinspect # pylint: disable=import-outside-toplevel,g-import-not-at-top inspector = oinspect.Inspector() info = inspector.info(component) diff --git a/fire/interact.py b/fire/interact.py index 9f0a01e6..7df32841 100644 --- a/fire/interact.py +++ b/fire/interact.py @@ -89,11 +89,11 @@ def _EmbedIPython(variables, argv=None): Values are variable values. argv: The argv to use for starting ipython. Defaults to an empty list. """ - import IPython # pylint: disable=g-import-not-at-top + import IPython # pylint: disable=import-outside-toplevel,g-import-not-at-top argv = argv or [] IPython.start_ipython(argv=argv, user_ns=variables) def _EmbedCode(variables): - import code # pylint: disable=g-import-not-at-top + import code # pylint: disable=import-outside-toplevel,g-import-not-at-top code.InteractiveConsole(variables).interact() From 48038f55cb6ff2a302443bc0102ea3efcec95c53 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 3 Dec 2019 16:11:41 -0800 Subject: [PATCH 044/205] sync internal and external PiperOrigin-RevId: 283644109 Change-Id: I81f1648de1ff03a7f1a5e5c64aa1763dae875990 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e4a3ec8..862f5f23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) [![PyPI version](https://badge.fury.io/py/fire.svg)](https://badge.fury.io/py/fire) +# Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ From 15a6f6b59703ff292892321260da5625cbeb1160 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 13 Dec 2019 11:20:06 -0800 Subject: [PATCH 045/205] Install enum 34 if python version is lower than 3.4. PiperOrigin-RevId: 285436201 Change-Id: Idacf0059d86010c50bac61ee32449c575d5c53b0 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5ddbb483..2e7a679d 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ """The setup.py file for Python Fire.""" +import sys from setuptools import setup - LONG_DESCRIPTION = """ Python Fire is a library for automatically generating command line interfaces (CLIs) with a single line of code. @@ -32,7 +32,7 @@ DEPENDENCIES = [ 'six', 'termcolor', -] +] + (['enum34'] if sys.version < 3.4 else []) TEST_DEPENDENCIES = [ 'hypothesis', From cd95ae25814636111a3acb3b18d14db053643f97 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 13 Dec 2019 14:09:06 -0800 Subject: [PATCH 046/205] Fix the type in version comparison in setup script. PiperOrigin-RevId: 285468373 Change-Id: I9e7d2afe8fcf899aee975b20eebba873ab68bb62 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2e7a679d..c0898a1f 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ DEPENDENCIES = [ 'six', 'termcolor', -] + (['enum34'] if sys.version < 3.4 else []) +] + (['enum34'] if sys.version < '3.4' else []) TEST_DEPENDENCIES = [ 'hypothesis', From 074f4b25906b4c7bc309a64feeba8873ec5e849c Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 17 Jan 2020 10:01:31 -0800 Subject: [PATCH 047/205] Ignoring SIGINT in pager process and fire process while the pager process is alive. PiperOrigin-RevId: 290290435 Change-Id: Ia34edcce58619be8a7ff4296a388b5b6a46c8544 --- fire/console/console_io.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fire/console/console_io.py b/fire/console/console_io.py index 777f1f48..784369b7 100644 --- a/fire/console/console_io.py +++ b/fire/console/console_io.py @@ -20,6 +20,7 @@ from __future__ import print_function import os +import signal import subprocess import sys @@ -68,6 +69,10 @@ def IsInteractive(output=False, error=False, heuristic=False): return True +def PreexecFunc(): + signal.signal(signal.SIGINT, signal.SIG_IGN) + + def More(contents, out, prompt=None, check_pager=True): """Run a user specified pager or fall back to the internal pager. @@ -97,10 +102,19 @@ def More(contents, out, prompt=None, check_pager=True): less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None) less = '-R' + (less_orig or '') encoding.SetEncodedValue(os.environ, 'LESS', less) - p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True) + # Ignores SIGINT from this point on since the child process has started + # and we don't want to terminate either one when the child is still alive. + signal.signal(signal.SIGINT, signal.SIG_IGN) + # Runs PreexecFunc before starting the child so SIGINT is ignored for the + # child process as well. + p = subprocess.Popen( + pager, stdin=subprocess.PIPE, shell=True, preexec_fn=PreexecFunc) enc = console_attr.GetConsoleAttr().GetEncoding() p.communicate(input=contents.encode(enc)) p.wait() + # Starts using default disposition for SIGINT again after the child has + # exited. + signal.signal(signal.SIGINT, signal.SIG_DFL) if less_orig is None: encoding.SetEncodedValue(os.environ, 'LESS', None) return From 420ced0454f97b204f3ba178edf77344997df31e Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 24 Jan 2020 13:08:34 -0800 Subject: [PATCH 048/205] Print out predefined summary and descriptions for primitive types instead of the real docstring in the object since the docstring of the builtin objects are usually not very useful. PiperOrigin-RevId: 291428068 Change-Id: Ica21b25aff6a8f95fc1ec7bd2fae8672fcf13ebb --- fire/custom_descriptions.py | 80 ++++++++++++++++++++++++++++++++ fire/custom_descriptions_test.py | 73 +++++++++++++++++++++++++++++ fire/docstrings.py | 1 + fire/formatting.py | 28 +++++++++++ fire/formatting_test.py | 26 +++++++++++ fire/helptext.py | 34 +++++++++----- fire/helptext_test.py | 9 ++-- 7 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 fire/custom_descriptions_test.py diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 695b01b3..191e8b29 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -40,8 +40,12 @@ from __future__ import division from __future__ import print_function +from fire import formatting import six +TWO_DOUBLE_QUOTES = '""' +STRING_DESC_PREFIX = 'The string ' + def NeedsCustomDescription(component): """Whether the component should use a custom description and summary. @@ -69,3 +73,79 @@ def NeedsCustomDescription(component): ): return True return False + + +def GetStringTypeSummary(obj, available_space, line_length): + """Returns a custom summary for string type objects. + + This function constructs a summary for string type objects by double quoting + the string value. The double quoted string value will be potentially truncated + with ellipsis depending on whether it has enough space available to show the + full string value. + + Args: + obj: The object to generate summary for. + available_space: Number of character spaces available. + line_length: The full width of the terminal, default is 80. + + Returns: + A summary for the input object. + """ + if len(obj) + len(TWO_DOUBLE_QUOTES) <= available_space: + content = obj + else: + additional_len_needed = len(TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS) + if available_space < additional_len_needed: + available_space = line_length + content = formatting.EllipsisTruncate( + obj, available_space - len(TWO_DOUBLE_QUOTES), line_length) + return formatting.DoubleQuote(content) + + +def GetStringTypeDescription(obj, available_space, line_length): + """Returns the predefined description for string obj. + + This function constructs a description for string type objects in the format + of 'The string ""'. could be potentially + truncated depending on whether it has enough space available to show the full + string value. + + Args: + obj: The object to generate description for. + available_space: Number of character spaces available. + line_length: The full width of the terminal, default if 80. + + Returns: + A description for input object. + """ + additional_len_needed = len(STRING_DESC_PREFIX) + len( + TWO_DOUBLE_QUOTES) + len(formatting.ELLIPSIS) + if available_space < additional_len_needed: + available_space = line_length + + return STRING_DESC_PREFIX + formatting.DoubleQuote( + formatting.EllipsisTruncate( + obj, available_space - len(STRING_DESC_PREFIX) - + len(TWO_DOUBLE_QUOTES), line_length)) + + +CUSTOM_DESC_SUM_FN_DICT = { + 'str': (GetStringTypeSummary, GetStringTypeDescription), + 'unicode': (GetStringTypeSummary, GetStringTypeDescription), +} + + +def GetSummary(obj, available_space, line_length): + obj_type_name = type(obj).__name__ + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space, + line_length) + return None + + +def GetDescription(obj, available_space, line_length): + obj_type_name = type(obj).__name__ + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space, + line_length) + return None diff --git a/fire/custom_descriptions_test.py b/fire/custom_descriptions_test.py new file mode 100644 index 00000000..79d7c7a1 --- /dev/null +++ b/fire/custom_descriptions_test.py @@ -0,0 +1,73 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for custom description module.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from fire import custom_descriptions +from fire import testutils + +LINE_LENGTH = 80 + + +class CustomDescriptionTest(testutils.BaseTestCase): + + def test_string_type_summary_enough_space(self): + component = 'Test' + summary = custom_descriptions.GetSummary( + obj=component, available_space=80, line_length=LINE_LENGTH) + self.assertEqual(summary, '"Test"') + + def test_string_type_summary_not_enough_space_truncated(self): + component = 'Test' + summary = custom_descriptions.GetSummary( + obj=component, available_space=5, line_length=LINE_LENGTH) + self.assertEqual(summary, '"..."') + + def test_string_type_summary_not_enough_space_new_line(self): + component = 'Test' + summary = custom_descriptions.GetSummary( + obj=component, available_space=4, line_length=LINE_LENGTH) + self.assertEqual(summary, '"Test"') + + def test_string_type_summary_not_enough_space_long_truncated(self): + component = 'Lorem ipsum dolor sit amet' + summary = custom_descriptions.GetSummary( + obj=component, available_space=10, line_length=LINE_LENGTH) + self.assertEqual(summary, '"Lorem..."') + + def test_string_type_description_enough_space(self): + component = 'Test' + description = custom_descriptions.GetDescription( + obj=component, available_space=80, line_length=LINE_LENGTH) + self.assertEqual(description, 'The string "Test"') + + def test_string_type_description_not_enough_space_truncated(self): + component = 'Lorem ipsum dolor sit amet' + description = custom_descriptions.GetDescription( + obj=component, available_space=20, line_length=LINE_LENGTH) + self.assertEqual(description, 'The string "Lore..."') + + def test_string_type_description_not_enough_space_new_line(self): + component = 'Lorem ipsum dolor sit amet' + description = custom_descriptions.GetDescription( + obj=component, available_space=10, line_length=LINE_LENGTH) + self.assertEqual(description, 'The string "Lorem ipsum dolor sit amet"') + + +if __name__ == '__main__': + testutils.main() diff --git a/fire/docstrings.py b/fire/docstrings.py index a58a42fe..edacf53c 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -149,6 +149,7 @@ def parse(docstring): Args: docstring: The docstring to parse. + Returns: A DocstringInfo containing information about the docstring. """ diff --git a/fire/formatting.py b/fire/formatting.py index 880e2b18..2c7cb335 100644 --- a/fire/formatting.py +++ b/fire/formatting.py @@ -20,6 +20,8 @@ import termcolor +ELLIPSIS = '...' + def Indent(text, spaces=2): lines = text.split('\n') @@ -65,3 +67,29 @@ def WrappedJoin(items, separator=' | ', width=80): def Error(text): return termcolor.colored(text, color='red', attrs=['bold']) + + +def EllipsisTruncate(text, available_space, line_length): + """Truncate text from the end with ellipsis.""" + if available_space < len(ELLIPSIS): + available_space = line_length + # No need to truncate + if len(text) <= available_space: + return text + return text[:available_space - len(ELLIPSIS)] + ELLIPSIS + + +def EllipsisMiddleTruncate(text, available_space, line_length): + """Truncates text from the middle with ellipsis.""" + if available_space < len(ELLIPSIS): + available_space = line_length + if len(text) < available_space: + return text + available_string_len = available_space - len(ELLIPSIS) + first_half_len = int(available_string_len / 2) # start from middle + second_half_len = available_string_len - first_half_len + return text[:first_half_len] + ELLIPSIS + text[-second_half_len:] + + +def DoubleQuote(text): + return '"%s"' % text diff --git a/fire/formatting_test.py b/fire/formatting_test.py index c19db054..61cce0e8 100644 --- a/fire/formatting_test.py +++ b/fire/formatting_test.py @@ -21,6 +21,8 @@ from fire import formatting from fire import testutils +LINE_LENGTH = 80 + class FormattingTest(testutils.BaseTestCase): @@ -51,6 +53,30 @@ def test_wrap_multiple_items(self): 'chicken |', 'cheese'], lines) + def test_ellipsis_truncate(self): + text = 'This is a string' + truncated_text = formatting.EllipsisTruncate( + text=text, available_space=10, line_length=LINE_LENGTH) + self.assertEqual('This is...', truncated_text) + + def test_ellipsis_truncate_not_enough_space(self): + text = 'This is a string' + truncated_text = formatting.EllipsisTruncate( + text=text, available_space=2, line_length=LINE_LENGTH) + self.assertEqual('This is a string', truncated_text) + + def test_ellipsis_middle_truncate(self): + text = '1000000000L' + truncated_text = formatting.EllipsisMiddleTruncate( + text=text, available_space=7, line_length=LINE_LENGTH) + self.assertEqual('10...0L', truncated_text) + + def test_ellipsis_middle_truncate_not_enough_space(self): + text = '1000000000L' + truncated_text = formatting.EllipsisMiddleTruncate( + text=text, available_space=2, line_length=LINE_LENGTH) + self.assertEqual('1000000000L', truncated_text) + if __name__ == '__main__': testutils.main() diff --git a/fire/helptext.py b/fire/helptext.py index 2c360d2b..fd38cb48 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -41,6 +41,7 @@ from fire import value_types LINE_LENGTH = 80 +SECTION_INDENTATION = 4 def HelpText(component, trace=None, verbose=False): @@ -96,10 +97,12 @@ def _NameSection(component, info, trace=None, verbose=False): current_command = _GetCurrentCommand(trace, include_separators=verbose) summary = _GetSummary(info) - # If the docstring is one of the messy builtin docstrings, don't show summary. - # TODO(dbieber): In follow up commits we can add in replacement summaries. + # If the docstring is one of the messy builtin docstrings, show custom one. if custom_descriptions.NeedsCustomDescription(component): - summary = None + available_space = LINE_LENGTH - SECTION_INDENTATION - len(current_command + + ' - ') + summary = custom_descriptions.GetSummary(component, available_space, + LINE_LENGTH) if summary: text = current_command + ' - ' + summary @@ -147,15 +150,17 @@ def _DescriptionSection(component, info): Returns the description if available. If not, returns the summary. If neither are available, returns None. """ - # If the docstring is one of the messy builtin docstrings, set it to None. - # TODO(dbieber): In follow up commits we can add in replacement docstrings. if custom_descriptions.NeedsCustomDescription(component): - return None - - summary = _GetSummary(info) - description = _GetDescription(info) + available_space = LINE_LENGTH - SECTION_INDENTATION + description = custom_descriptions.GetDescription(component, available_space, + LINE_LENGTH) + summary = custom_descriptions.GetSummary(component, available_space, + LINE_LENGTH) + else: + description = _GetDescription(info) + summary = _GetSummary(info) + # Fall back to summary if description is not available. text = description or summary or None - if text: return ('DESCRIPTION', text) else: @@ -348,8 +353,9 @@ def _GetCurrentCommand(trace=None, include_separators=True): def _CreateOutputSection(name, content): return """{name} -{content}""".format(name=formatting.Bold(name), - content=formatting.Indent(content, 4)) +{content}""".format( + name=formatting.Bold(name), + content=formatting.Indent(content, SECTION_INDENTATION)) def _CreateArgItem(arg, docstring_info): @@ -359,6 +365,7 @@ def _CreateArgItem(arg, docstring_info): arg: The name of the positional argument. docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. + Returns: A string to be used in constructing the help screen for the function. """ @@ -418,6 +425,9 @@ def _MakeUsageDetailsSection(action_group): if (docstring_info and not custom_descriptions.NeedsCustomDescription(member)): summary = docstring_info.summary + elif custom_descriptions.NeedsCustomDescription(member): + summary = custom_descriptions.GetSummary( + member, LINE_LENGTH - SECTION_INDENTATION, LINE_LENGTH) else: summary = None item = _CreateItem(name, summary) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 1371e2a1..a866ee4d 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -111,7 +111,8 @@ def testHelpTextEmptyList(self): trace=trace.FireTrace(component, 'list')) self.assertIn('NAME\n list', help_screen) self.assertIn('SYNOPSIS\n list COMMAND', help_screen) - # The list docstring is messy, so it is not shown. + # TODO(zuhaochen): Change assertion after custom description is + # implemented for list type. self.assertNotIn('DESCRIPTION', help_screen) # We don't check the listed commands either since the list API could # potentially change between Python versions. @@ -125,7 +126,8 @@ def testHelpTextShortList(self): trace=trace.FireTrace(component, 'list')) self.assertIn('NAME\n list', help_screen) self.assertIn('SYNOPSIS\n list COMMAND', help_screen) - # The list docstring is messy, so it is not shown. + # TODO(zuhaochen): Change assertion after custom description is + # implemented for list type. self.assertNotIn('DESCRIPTION', help_screen) # We don't check the listed commands comprehensively since the list API @@ -141,7 +143,8 @@ def testHelpTextInt(self): component=component, trace=trace.FireTrace(component, '7')) self.assertIn('NAME\n 7', help_screen) self.assertIn('SYNOPSIS\n 7 COMMAND | VALUE', help_screen) - # The int docstring is messy, so it is not shown. + # TODO(zuhaochen): Change assertion after implementing custom + # description for int. self.assertNotIn('DESCRIPTION', help_screen) self.assertIn('COMMANDS\n COMMAND is one of the following:\n', help_screen) From 192c020d301fac2bf5fbd02b28c5b4ce36495cba Mon Sep 17 00:00:00 2001 From: David Caron Date: Tue, 28 Jan 2020 16:23:58 -0500 Subject: [PATCH 049/205] fix typo in guide (#218) * fix typo in guide - `{name: David}` -> `{name:David}` (no space) --- docs/guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide.md b/docs/guide.md index 2e59ce8a..b02f37b5 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -595,7 +595,7 @@ $ python example.py [1,2] list $ python example.py True bool -$ python example.py {name: David} +$ python example.py {name:David} dict ``` From 1ac5105a0437518785033f47dd78746678e5133a Mon Sep 17 00:00:00 2001 From: Mehmood Deshmukh Date: Sat, 22 Feb 2020 00:26:34 +0530 Subject: [PATCH 050/205] Use "dir()" to get available methods of object (#215) * Use dir() instead of inspect.getmembers() to get available methods of the object * Add test case for #149 Ref: #149 --- fire/core.py | 4 ++-- fire/fire_test.py | 5 +++++ fire/test_components.py | 10 ++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/fire/core.py b/fire/core.py index 07993de9..083163b9 100644 --- a/fire/core.py +++ b/fire/core.py @@ -632,7 +632,7 @@ def _GetMember(component, args): Raises: FireError: If we cannot consume an argument to get a member. """ - members = dict(inspect.getmembers(component)) + members = dir(component) arg = args[0] arg_names = [ arg, @@ -641,7 +641,7 @@ def _GetMember(component, args): for arg_name in arg_names: if arg_name in members: - return members[arg_name], [arg], args[1:] + return getattr(component, arg_name), [arg], args[1:] raise FireError('Could not consume arg:', arg) diff --git a/fire/fire_test.py b/fire/fire_test.py index 8cf121af..63302d26 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -703,6 +703,11 @@ def testTraceErrors(self): with self.assertRaisesFireExit(2): fire.Fire(tc.InstanceVars, command=['--arg1=a1', '--arg2=a2', '-', 'jog']) + def testClassWithDefaultMethod(self): + self.assertEqual( + fire.Fire(tc.DefaultMethod, command=['double', '10']), 20 + ) + if __name__ == '__main__': testutils.main() diff --git a/fire/test_components.py b/fire/test_components.py index e35027c7..e88ae907 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -498,3 +498,13 @@ def off(self): def set(self, value): self.pixels[self._row][self._col] = value return self + +class DefaultMethod(object): + + def double(self, number): + return 2 * number + + def __getattr__(self, name): + def _missing(): + return "Undefined Function" + return _missing From 34a10f9218e04b343ff1579b3505a3cbe8da4824 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 25 Feb 2020 13:16:32 -0800 Subject: [PATCH 051/205] Add InvalidProperty test and sync with GitHub PiperOrigin-RevId: 297186473 Change-Id: Id23c439ce057b9bc1c7e7c9fd02dd3fb3f3d6a49 --- docs/guide.md | 2 +- fire/test_components.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide.md b/docs/guide.md index b02f37b5..2e59ce8a 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -595,7 +595,7 @@ $ python example.py [1,2] list $ python example.py True bool -$ python example.py {name:David} +$ python example.py {name: David} dict ``` diff --git a/fire/test_components.py b/fire/test_components.py index e88ae907..d29dfa2d 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -499,6 +499,7 @@ def set(self, value): self.pixels[self._row][self._col] = value return self + class DefaultMethod(object): def double(self, number): @@ -508,3 +509,4 @@ def __getattr__(self, name): def _missing(): return "Undefined Function" return _missing + From 297a1adfae74cb7e688985d24a8683d808781826 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 25 Feb 2020 13:28:36 -0800 Subject: [PATCH 052/205] Quote lint formatting and fix accidentally reverted docs change. PiperOrigin-RevId: 297189113 Change-Id: I6db02e4be001d280b0d98afaeb9e201f837cbecb --- docs/guide.md | 2 +- fire/test_components.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 2e59ce8a..b02f37b5 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -595,7 +595,7 @@ $ python example.py [1,2] list $ python example.py True bool -$ python example.py {name: David} +$ python example.py {name:David} dict ``` diff --git a/fire/test_components.py b/fire/test_components.py index d29dfa2d..73217051 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -507,6 +507,5 @@ def double(self, number): def __getattr__(self, name): def _missing(): - return "Undefined Function" + return 'Undefined function' return _missing - From 04e2434001e809bdf05e6f47d71452d1e8614b78 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 25 Feb 2020 17:42:34 -0800 Subject: [PATCH 053/205] Add InvalidProperty test and fix the travis error resulting from pytype not being supported in python 3.4 PiperOrigin-RevId: 297245710 Change-Id: I0b0f6110f2aeb1951a585be3532e8d49c22e41c7 --- .travis.yml | 9 ++++++--- fire/fire_test.py | 4 ++++ fire/test_components.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 44edde6e..16246cb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,12 +23,15 @@ script: - pip install ipython - python -m pytest # Now run the tests with IPython. - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console - - pip install pytype + - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + pip install pytype; + fi # Run type-checking, excluding files that define or use py3 features in py2. - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then pytype -x fire/fire_test.py fire/inspectutils_test.py fire/test_components_py3.py; - else - pytype; fi + elif [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + pytype; + fi diff --git a/fire/fire_test.py b/fire/fire_test.py index 63302d26..e5c7fe3a 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -708,6 +708,10 @@ def testClassWithDefaultMethod(self): fire.Fire(tc.DefaultMethod, command=['double', '10']), 20 ) + def testClassWithInvalidProperty(self): + self.assertEqual( + fire.Fire(tc.InvalidProperty, command=['double', '10']), 20 + ) if __name__ == '__main__': testutils.main() diff --git a/fire/test_components.py b/fire/test_components.py index 73217051..ffd02b37 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -509,3 +509,13 @@ def __getattr__(self, name): def _missing(): return 'Undefined function' return _missing + + +class InvalidProperty(object): + + def double(self, number): + return 2 * number + + @property + def prop(self): + raise ValueError('test') From a5dba1301c64b179c39b3cfb214aefe954706cf0 Mon Sep 17 00:00:00 2001 From: Pranav Gupta Date: Thu, 27 Feb 2020 09:13:26 -0800 Subject: [PATCH 054/205] Copybara import of the project: -- 9d5f581e13deee9f654fbec0be02944b1d7e6a90 by Pranav Gupta : Update using-cli.md to reflect support for --help COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/194 from pranavgupta1234:patch-1 9d5f581e13deee9f654fbec0be02944b1d7e6a90 PiperOrigin-RevId: 297612634 Change-Id: I89eed909ad09226a9642d968497b413d12512dae --- docs/using-cli.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/using-cli.md b/docs/using-cli.md index 4cd95e14..0f369a9a 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -9,10 +9,13 @@ arguments. This command corresponds to the Python component you called the `Fire` function on. If you did not supply an object in the call to `Fire`, then the context in which `Fire` was called will be used as the Python component. -You can append `-- --help` to any command to see what Python component it +You can append `--help` or `-h` to a command to see what Python component it corresponds to, as well as the various ways in which you can extend the command. -Flags are always separated from the Fire command by an isolated `--` in order -to distinguish between flags and named arguments. + +Flags to Fire should be separated from the Fire command by an isolated `--` in +order to distinguish between flags and named arguments. So, for example, to +enter interactive mode append `-- -i` or `-- --interactive` to any command. To +use Fire in verbose mode, append `-- --verbose`. Given a Fire command that corresponds to a Python object, you can extend that command to access a member of that object, call it with arguments if it is a From 7d87eb260456bda43518dd1259f0677c7b2a6d39 Mon Sep 17 00:00:00 2001 From: Jared Trog Date: Fri, 28 Feb 2020 09:10:40 -0800 Subject: [PATCH 055/205] Copybara import of the project: -- 9061166c98474e4dd001844b2bb821b428c148a9 by Jared Trog : Support printing classes with overridden str method as SimpleGroups #197 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/224 from jaredtrog:fix-issue-197 9061166c98474e4dd001844b2bb821b428c148a9 PiperOrigin-RevId: 297857231 Change-Id: Ibf2c344eda89f5a042726bde003bb4653ae3835e --- fire/value_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fire/value_types.py b/fire/value_types.py index ad50cb23..39a0e9f3 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -37,7 +37,8 @@ def IsCommand(component): def IsValue(component): - return isinstance(component, VALUE_TYPES) + return isinstance(component, VALUE_TYPES) or ( + hasattr(component, '__str__') and inspect.ismethod(component.__str__)) def IsSimpleGroup(component): From 1c1afd16c6cf5943a0c62c0db654d5da570ae65f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 09:13:42 -0800 Subject: [PATCH 056/205] Treat objects with custom __str__ methods as Values / SimpleGroups. PiperOrigin-RevId: 297857807 Change-Id: I9ab3f50336cc30e6cdeccc2965a50821838507a3 --- fire/core.py | 9 +++------ fire/value_types.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/fire/core.py b/fire/core.py index 083163b9..be064c1c 100644 --- a/fire/core.py +++ b/fire/core.py @@ -244,14 +244,11 @@ def _PrintResult(component_trace, verbose=False): # and move serialization to its own module. result = component_trace.GetResult() - if hasattr(result, '__str__'): + if value_types.HasCustomStr(result): # If the object has a custom __str__ method, rather than one inherited from # object, then we use that to serialize the object. - class_attrs = completion.GetClassAttrsDict(type(result)) or {} - str_attr = class_attrs.get('__str__') - if str_attr and str_attr.defining_class is not object: - print(str(result)) - return + print(str(result)) + return if isinstance(result, (list, set, frozenset, types.GeneratorType)): for i in result: diff --git a/fire/value_types.py b/fire/value_types.py index 39a0e9f3..9ee63384 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -20,6 +20,7 @@ import inspect +from fire import completion import six @@ -37,8 +38,7 @@ def IsCommand(component): def IsValue(component): - return isinstance(component, VALUE_TYPES) or ( - hasattr(component, '__str__') and inspect.ismethod(component.__str__)) + return isinstance(component, VALUE_TYPES) or HasCustomStr(component) def IsSimpleGroup(component): @@ -58,3 +58,28 @@ def IsSimpleGroup(component): if not IsValue(value) and not isinstance(value, (list, dict)): return False return True + + +def HasCustomStr(component): + """Determines if a component has a custom __str__ method. + + Uses inspect.classify_class_attrs to determine the origin of the object's + __str__ method, if one is present. If it defined by `object` itself, then + it is not considered custom. Otherwise it is. This means that the __str__ + methods of primitives like ints and floats are considered custom. + + Objects with custom __str__ methods are treated as values and can be + serialized in places where more complex objects would have their help screen + shown instead. + + Args: + component: The object to check for a custom __str__ method. + Returns: + Whether `component` has a custom __str__ method. + """ + if hasattr(component, '__str__'): + class_attrs = completion.GetClassAttrsDict(type(component)) or {} + str_attr = class_attrs.get('__str__') + if str_attr and str_attr.defining_class is not object: + return True + return False From c8ea506b067ac13465f0331d54afce35e495da3f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 09:14:48 -0800 Subject: [PATCH 057/205] Hide future imports in MemberVisible The reason for this is that in PY34 and beyond, future imports have their own type that can be detected and those imports can be filtered out. However, in Python 2 the type of these imports is just "instance" which is too aggressive and filters out objects unnecessarily. PiperOrigin-RevId: 297858001 Change-Id: I86d10505455d708f4058ec2e05091a6405425f84 --- fire/completion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fire/completion.py b/fire/completion.py index 19d068fd..bf7678e5 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -318,7 +318,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): return False if verbose: return True - if isinstance(member, type(absolute_import)): + if member in (absolute_import, division, print_function): + return False + if isinstance(member, type(absolute_import)) and six.PY34: return False if inspect.ismodule(member) and member is six: # TODO(dbieber): Determine more generally which modules to hide. From c830177ce00b1c6f123316e3cae81cbaec8377ca Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 09:37:23 -0800 Subject: [PATCH 058/205] Move GetClassAttrsDict to inspectutils. PiperOrigin-RevId: 297862432 Change-Id: I8b37f6715f70085a9b967b183e27aaa031480ad1 --- fire/completion.py | 21 +++++---------------- fire/core.py | 2 +- fire/inspectutils.py | 11 +++++++++++ fire/value_types.py | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index bf7678e5..73360715 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -281,17 +281,6 @@ def _FishScript(name, commands, default_options=None): ) -def GetClassAttrsDict(component): - """Gets the attributes of the component class, as a dict with name keys.""" - if not inspect.isclass(component): - return None - class_attrs_list = inspect.classify_class_attrs(component) - return { - class_attr.name: class_attr - for class_attr in class_attrs_list - } - - def MemberVisible(component, name, member, class_attrs=None, verbose=False): """Returns whether a member should be included in auto-completion or help. @@ -328,7 +317,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): if inspect.isclass(component): # If class_attrs has not been provided, compute it. if class_attrs is None: - class_attrs = GetClassAttrsDict(class_attrs) + class_attrs = inspectutils.GetClassAttrsDict(class_attrs) class_attr = class_attrs.get(name) if class_attr and class_attr.kind in ('method', 'property'): # methods and properties should be accessed on instantiated objects, @@ -355,9 +344,9 @@ def VisibleMembers(component, class_attrs=None, verbose=False): Args: component: The component whose members to list. class_attrs: (optional) If component is a class, you may provide this as: - GetClassAttrsDict(component). If not provided, it will be computed. - If provided, this determines how class members will be treated for - visibility. In particular, methods are generally hidden for + inspectutils.GetClassAttrsDict(component). If not provided, it will be + computed. If provided, this determines how class members will be treated + for visibility. In particular, methods are generally hidden for non-instantiated classes, but if you wish them to be shown (e.g. for completion scripts) then pass in a different class_attr for them. verbose: Whether to include private members. @@ -371,7 +360,7 @@ def VisibleMembers(component, class_attrs=None, verbose=False): # If class_attrs has not been provided, compute it. if class_attrs is None: - class_attrs = GetClassAttrsDict(component) + class_attrs = inspectutils.GetClassAttrsDict(component) return [ (member_name, member) for member_name, member in members if MemberVisible(component, member_name, member, class_attrs=class_attrs, diff --git a/fire/core.py b/fire/core.py index be064c1c..763b3d13 100644 --- a/fire/core.py +++ b/fire/core.py @@ -309,7 +309,7 @@ def _DictAsString(result, verbose=False): # 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 - class_attrs = completion.GetClassAttrsDict(result) + class_attrs = inspectutils.GetClassAttrsDict(result) result_visible = { key: value for key, value in result.items() if completion.MemberVisible(result, key, value, diff --git a/fire/inspectutils.py b/fire/inspectutils.py index bf197f3f..f3e54a98 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -268,3 +268,14 @@ def IsNamedTuple(component): has_fields = bool(getattr(component, '_fields', None)) return has_fields + + +def GetClassAttrsDict(component): + """Gets the attributes of the component class, as a dict with name keys.""" + if not inspect.isclass(component): + return None + class_attrs_list = inspect.classify_class_attrs(component) + return { + class_attr.name: class_attr + for class_attr in class_attrs_list + } diff --git a/fire/value_types.py b/fire/value_types.py index 9ee63384..c0a137fd 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -20,7 +20,7 @@ import inspect -from fire import completion +from fire import inspectutils import six @@ -78,7 +78,7 @@ def HasCustomStr(component): Whether `component` has a custom __str__ method. """ if hasattr(component, '__str__'): - class_attrs = completion.GetClassAttrsDict(type(component)) or {} + class_attrs = inspectutils.GetClassAttrsDict(type(component)) or {} str_attr = class_attrs.get('__str__') if str_attr and str_attr.defining_class is not object: return True From 86ac2685a694b13da120ab52010ed22a1117c898 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 13:23:17 -0800 Subject: [PATCH 059/205] Satisfy pytype PiperOrigin-RevId: 297916197 Change-Id: I718ce9ffa5a4c137d2d111eaf3ca842287cef114 --- fire/completion.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 73360715..451e2021 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -298,7 +298,8 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): name: The name of the member. member: The member itself. class_attrs: (optional) If component is a class, provide this as: - GetClassAttrsDict(component). If not provided, it will be computed. + inspectutils.GetClassAttrsDict(component). If not provided, it will be + computed. verbose: Whether to include private members. Returns A boolean value indicating whether the member should be included. @@ -317,7 +318,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): if inspect.isclass(component): # If class_attrs has not been provided, compute it. if class_attrs is None: - class_attrs = inspectutils.GetClassAttrsDict(class_attrs) + class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {} class_attr = class_attrs.get(name) if class_attr and class_attr.kind in ('method', 'property'): # methods and properties should be accessed on instantiated objects, From deb33cc34f94cad86a875a9993864ac0acf93828 Mon Sep 17 00:00:00 2001 From: Vincent Barbaresi Date: Fri, 28 Feb 2020 14:24:21 -0800 Subject: [PATCH 060/205] use a custom getfullargspec that follows wrapped chains #159 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/203 from vbarbaresi:bugfix_159 27e024114a4cb663e7eb65623ba6443a7670f72e PiperOrigin-RevId: 297930634 Change-Id: Ieebd9769e44a7ca85688711da771f2fded2df720 --- fire/core_test.py | 13 ++++++ fire/fire_test.py | 12 +++++- fire/inspectutils.py | 85 ++++++++++++++++++++++++++++++++++--- fire/test_components.py | 13 ++++++ fire/test_components_py3.py | 16 +++++++ fire/testutils.py | 1 + 6 files changed, 132 insertions(+), 8 deletions(-) diff --git a/fire/core_test.py b/fire/core_test.py index 2f2df3fd..97389a2d 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -24,6 +24,8 @@ from fire import trace import mock +import six + class CoreTest(testutils.BaseTestCase): @@ -192,5 +194,16 @@ def testClassMethod(self): 7, ) + @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') + def testLruCacheDecoratorBoundArg(self): + self.assertEqual(core.Fire(tc.py3.LruCacheDecoratedMethod, + command=['lru_cache_in_class', 'foo']), 'foo') + + @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') + def testLruCacheDecorator(self): + self.assertEqual( + core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo') + + if __name__ == '__main__': testutils.main() diff --git a/fire/fire_test.py b/fire/fire_test.py index e5c7fe3a..2c7a580b 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -20,7 +20,6 @@ import os import sys -import unittest import fire from fire import test_components as tc @@ -185,7 +184,7 @@ def testFireAnnotatedArgs(self): self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10) self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15) - @unittest.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.') + @testutils.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.') def testFireKeywordOnlyArgs(self): with self.assertRaisesFireExit(2): # Keyword arguments must be passed with flag syntax. @@ -713,5 +712,14 @@ def testClassWithInvalidProperty(self): fire.Fire(tc.InvalidProperty, command=['double', '10']), 20 ) + @testutils.skipIf(six.PY2, 'Cannot inspect wrapped signatures in Python 2.') + def testHelpKwargsDecorator(self): + # Issue #190, follow the wrapped method instead of crashing. + with self.assertRaisesFireExit(0): + fire.Fire(tc.decorated_method, command=['-h']) + with self.assertRaisesFireExit(0): + fire.Fire(tc.decorated_method, command=['--help']) + + if __name__ == '__main__': testutils.main() diff --git a/fire/inspectutils.py b/fire/inspectutils.py index f3e54a98..a979b4ea 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -100,19 +100,92 @@ def Py2GetArgSpec(fn): raise +def Py3GetFullArgSpec(fn): + """A alternative to the builtin getfullargspec. + + The builtin inspect.getfullargspec uses: + `skip_bound_args=False, follow_wrapped_chains=False` + in order to be backwards compatible. + + This function instead skips bound args (self) and follows wrapped chains. + + Args: + fn: The function or class of interest. + Returns: + An inspect.FullArgSpec namedtuple with the full arg spec of the function. + """ + # pytype: disable=module-attr + try: + sig = inspect._signature_from_callable( # pylint: disable=protected-access + fn, + skip_bound_arg=True, + follow_wrapper_chains=True, + sigcls=inspect.Signature) + except Exception: + # 'signature' can raise ValueError (most common), AttributeError, and + # possibly others. We catch all exceptions here, and reraise a TypeError. + raise TypeError('Unsupported callable.') + + args = [] + varargs = None + varkw = None + kwonlyargs = [] + defaults = () + annotations = {} + defaults = () + kwdefaults = {} + + if sig.return_annotation is not sig.empty: + annotations['return'] = sig.return_annotation + + for param in sig.parameters.values(): + kind = param.kind + name = param.name + + # pylint: disable=protected-access + if kind is inspect._POSITIONAL_ONLY: + args.append(name) + elif kind is inspect._POSITIONAL_OR_KEYWORD: + args.append(name) + if param.default is not param.empty: + defaults += (param.default,) + elif kind is inspect._VAR_POSITIONAL: + varargs = name + elif kind is inspect._KEYWORD_ONLY: + kwonlyargs.append(name) + if param.default is not param.empty: + kwdefaults[name] = param.default + elif kind is inspect._VAR_KEYWORD: + varkw = name + if param.annotation is not param.empty: + annotations[name] = param.annotation + # pylint: enable=protected-access + + if not kwdefaults: + # compatibility with 'func.__kwdefaults__' + kwdefaults = None + + if not defaults: + # compatibility with 'func.__defaults__' + defaults = None + return inspect.FullArgSpec(args, varargs, varkw, defaults, + kwonlyargs, kwdefaults, annotations) + # pytype: enable=module-attr + + def GetFullArgSpec(fn): """Returns a FullArgSpec describing the given callable.""" original_fn = fn fn, skip_arg = _GetArgSpecInfo(fn) try: - if six.PY2: + if six.PY3: + (args, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn) + else: # six.PY2 args, varargs, varkw, defaults = Py2GetArgSpec(fn) kwonlyargs = kwonlydefaults = None annotations = getattr(fn, '__annotations__', None) - else: - (args, varargs, varkw, defaults, - kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member except TypeError: # If we can't get the argspec, how do we know if the fn should take args? @@ -141,9 +214,9 @@ def GetFullArgSpec(fn): # Case 3: Other known slot wrappers do not accept args. return FullArgSpec() - if skip_arg and args: + if six.PY2 and skip_arg and args: + # In Python 3, Py3GetFullArgSpec uses skip_bound_arg=True already. args.pop(0) # Remove 'self' or 'cls' from the list of arguments. - return FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) diff --git a/fire/test_components.py b/fire/test_components.py index ffd02b37..a25ba7af 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -19,6 +19,7 @@ from __future__ import print_function import collections +import functools import enum import six @@ -519,3 +520,15 @@ def double(self, number): @property def prop(self): raise ValueError('test') + + +def simple_decorator(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + return wrapper + + +@simple_decorator +def decorated_method(name='World'): + return 'Hello %s' % name diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index d705c43a..e714be30 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -12,9 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Lint as: python3 """This module has components that use Python 3 specific syntax.""" +import functools + +# pylint: disable=keyword-arg-before-vararg def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5, arg6, arg7: int, arg8=30, arg9: int = 40, **arg10): return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 @@ -27,3 +31,15 @@ def double(self, *, count): def triple(self, *, count): return count * 3 + + +class LruCacheDecoratedMethod(object): + + @functools.lru_cache() + def lru_cache_in_class(self, arg1): + return arg1 + + +@functools.lru_cache() +def lru_cache_decorated(arg1): + return arg1 diff --git a/fire/testutils.py b/fire/testutils.py index 0541a9a5..3463fd50 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -98,4 +98,5 @@ def assertRaisesFireExit(self, code, regexp='.*'): # pylint: disable=invalid-name main = unittest.main skip = unittest.skip +skipIf = unittest.skipIf # pylint: enable=invalid-name From b1f3f95d736e7e36126f2e6c457c52f995f587dc Mon Sep 17 00:00:00 2001 From: Vincent Barbaresi Date: Fri, 28 Feb 2020 14:31:51 -0800 Subject: [PATCH 061/205] handle NumPy style docstring params containing a colon #189 The problematic case was: name : str name, default: World The second line wasn't treated as a description if it contained a colon and ended up ignored We parse line by line, and used to keep track only of the next line. I added tracking of the previous line so that I can compare the indentations. That allows to distinguish descriptions and a parameter definitions. COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/201 from vbarbaresi:bugfix_189 8e6baa4f976d707ffdeeff951fdecd1eac7d09ca PiperOrigin-RevId: 297932310 Change-Id: Ia5ee774890a713c8ec1d5b3a895d2cb0163849c3 --- fire/docstrings.py | 40 ++++++++++++++++++++++++++++++++++++---- fire/docstrings_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index edacf53c..e173d192 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -176,8 +176,9 @@ def parse(docstring): for index, line in enumerate(lines): has_next = index + 1 < lines_len + previous_line = lines[index - 1] if index > 0 else None next_line = lines[index + 1] if has_next else None - line_info = _create_line_info(line, next_line) + line_info = _create_line_info(line, next_line, previous_line) _consume_line(line_info, state) summary = ' '.join(state.summary.lines) if state.summary.lines else None @@ -455,7 +456,7 @@ def _consume_line(line_info, state): # of the previous arg, or a new arg. TODO: Whitespace can distinguish. arg = _get_or_create_arg_by_name(state, line_stripped) state.current_arg = arg - elif ':' in line_stripped: + elif _line_is_numpy_parameter_type(line_info): possible_args, type_data = line_stripped.split(':', 1) arg_names = _as_arg_names(possible_args) # re.split(' |,', s) if arg_names: @@ -492,8 +493,8 @@ def _consume_line(line_info, state): pass -def _create_line_info(line, next_line): - """Returns information about the current and next line of the docstring.""" +def _create_line_info(line, next_line, previous_line): + """Returns information about the current line and surrounding lines.""" line_info = Namespace() # TODO(dbieber): Switch to an explicit class. line_info.line = line line_info.stripped = line.strip() @@ -506,6 +507,11 @@ def _create_line_info(line, next_line): line_info.next.stripped = next_line.strip() if next_line_exists else None line_info.next.indentation = ( len(next_line) - len(next_line.lstrip()) if next_line_exists else None) + line_info.previous.line = previous_line + previous_line_exists = previous_line is not None + line_info.previous.indentation = ( + len(previous_line) - + len(previous_line.lstrip()) if previous_line_exists else None) # Note: This counts all whitespace equally. return line_info @@ -726,3 +732,29 @@ def _numpy_section(line_info): return _section_from_possible_title(possible_title) else: return None + + +def _line_is_numpy_parameter_type(line_info): + """Returns whether the line contains a numpy style parameter type definition. + + We look for a line of the form: + x : type + + And we have to exclude false positives on argument descriptions containing a + colon by checking the indentation of the line above. + + Args: + line_info: Information about the current line. + Returns: + True if the line is a numpy parameter type definition, False otherwise. + """ + line_stripped = line_info.remaining.strip() + if ':' in line_stripped: + previous_indent = line_info.previous.indentation + current_indent = line_info.indentation + if ':' in line_info.previous.line and current_indent > previous_indent: + # The parameter type was the previous line; this is the description. + return False + else: + return True + return False diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index adb89492..8b1d7685 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -253,6 +253,32 @@ def test_strip_blank_lines(self): self.assertEqual(expected_output, docstrings._strip_blank_lines(lines)) # pylint: disable=protected-access + def test_numpy_colon_in_description(self): + docstring = """ + Greets name. + + Arguments + --------- + name : str + name, default : World + arg2 : int + arg2, default:None + arg3 : bool + """ + docstring_info = docstrings.parse(docstring) + expected_docstring_info = DocstringInfo( + summary='Greets name.', + description=None, + args=[ + ArgInfo(name='name', type='str', + description='name, default : World'), + ArgInfo(name='arg2', type='int', + description='arg2, default:None'), + ArgInfo(name='arg3', type='bool', description=None), + ] + ) + self.assertEqual(expected_docstring_info, docstring_info) + if __name__ == '__main__': testutils.main() From 4de6607fa0c9c077ff31962ae029f97689f946bf Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 14:40:08 -0800 Subject: [PATCH 062/205] Disable pytype in core_test PiperOrigin-RevId: 297936591 Change-Id: I1fe0379ce4c8a9e09f971a464b010862eada580c --- fire/core_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fire/core_test.py b/fire/core_test.py index 97389a2d..84a63864 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -196,13 +196,15 @@ def testClassMethod(self): @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): - self.assertEqual(core.Fire(tc.py3.LruCacheDecoratedMethod, - command=['lru_cache_in_class', 'foo']), 'foo') + self.assertEqual( + core.Fire(tc.py3.LruCacheDecoratedMethod, # pylint: disable=module-attr + command=['lru_cache_in_class', 'foo']), 'foo') @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecorator(self): self.assertEqual( - core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo') + core.Fire(tc.py3.lru_cache_decorated, # pylint: disable=module-attr + command=['foo']), 'foo') if __name__ == '__main__': From 780a360a6d4231b2fb171c3ae5da4e18bf3f7ccb Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 15:09:13 -0800 Subject: [PATCH 063/205] pytype: disable=module-attr PiperOrigin-RevId: 297944246 Change-Id: I23e8871ba0a968dbd20325883f2322114c5d8971 --- fire/core_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/core_test.py b/fire/core_test.py index 84a63864..27c9f418 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -197,13 +197,13 @@ def testClassMethod(self): @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): self.assertEqual( - core.Fire(tc.py3.LruCacheDecoratedMethod, # pylint: disable=module-attr + core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr command=['lru_cache_in_class', 'foo']), 'foo') @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecorator(self): self.assertEqual( - core.Fire(tc.py3.lru_cache_decorated, # pylint: disable=module-attr + core.Fire(tc.py3.lru_cache_decorated, # pytype: disable=module-attr command=['foo']), 'foo') From afc34abdfb3c1f6ce7de9b9b92e2a07e5a6b4c89 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 23:30:23 +0000 Subject: [PATCH 064/205] Restore support for Python 3.4 and lint for Python 2.7. PiperOrigin-RevId: 297948749 Change-Id: I5879e911b0e52404bc2ad7676340ce8d5e7aeae6 --- fire/inspectutils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index a979b4ea..cf9dae84 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -19,7 +19,9 @@ from __future__ import print_function import inspect +import sys import types + from fire import docstrings import six @@ -114,6 +116,7 @@ def Py3GetFullArgSpec(fn): Returns: An inspect.FullArgSpec namedtuple with the full arg spec of the function. """ + # pylint: disable=no-member # pytype: disable=module-attr try: sig = inspect._signature_from_callable( # pylint: disable=protected-access @@ -170,6 +173,7 @@ def Py3GetFullArgSpec(fn): defaults = None return inspect.FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwdefaults, annotations) + # pylint: enable=no-member # pytype: enable=module-attr @@ -179,9 +183,12 @@ def GetFullArgSpec(fn): fn, skip_arg = _GetArgSpecInfo(fn) try: - if six.PY3: + if sys.version_info[0:2] >= (3, 5): (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn) + elif six.PY3: # Specifically Python 3.4. + (args, varargs, varkw, defaults, + kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member else: # six.PY2 args, varargs, varkw, defaults = Py2GetArgSpec(fn) kwonlyargs = kwonlydefaults = None From bb90d16e1725fe9679988087f96306ed98b16e5f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 16:29:03 -0800 Subject: [PATCH 065/205] Use skip_arg for Python 3.4 again. PiperOrigin-RevId: 297961705 Change-Id: Ia5499dcdf01a55f476ca8fd59cf5ae5fa0f6b294 --- fire/inspectutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index cf9dae84..c7559ae6 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -221,8 +221,9 @@ def GetFullArgSpec(fn): # Case 3: Other known slot wrappers do not accept args. return FullArgSpec() - if six.PY2 and skip_arg and args: - # In Python 3, Py3GetFullArgSpec uses skip_bound_arg=True already. + # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already. + skip_arg_required = six.PY2 or sys.version_info[0:2] == (3, 4) + if skip_arg_required and skip_arg and args: args.pop(0) # Remove 'self' or 'cls' from the list of arguments. return FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) From b77f5aefbf5986391ab6cb9f51376c6d93d757cf Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 28 Feb 2020 20:04:23 -0800 Subject: [PATCH 066/205] Disable wrapped function test for Python 3.4 PiperOrigin-RevId: 297990663 Change-Id: I0c985fa611d04c764aed34ebfa9af05704d58332 --- fire/fire_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fire/fire_test.py b/fire/fire_test.py index 2c7a580b..4f2ffb9b 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -712,7 +712,8 @@ def testClassWithInvalidProperty(self): fire.Fire(tc.InvalidProperty, command=['double', '10']), 20 ) - @testutils.skipIf(six.PY2, 'Cannot inspect wrapped signatures in Python 2.') + @testutils.skipIf(sys.version_info[0:2] <= (3, 4), + 'Cannot inspect wrapped signatures in Python 2 or 3.4.') def testHelpKwargsDecorator(self): # Issue #190, follow the wrapped method instead of crashing. with self.assertRaisesFireExit(0): From 4a5b7c348cf3f5cff92093eab647a57d556651be Mon Sep 17 00:00:00 2001 From: Jared Trog Date: Fri, 6 Mar 2020 13:08:48 -0800 Subject: [PATCH 067/205] Update formatting to use colorama on windows if available. COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/232 from jaredtrog:issue-186 61f67405fd842a52a3ba6353789804713190cb43 PiperOrigin-RevId: 299419699 Change-Id: I72d9ac020e22b9b3c1eadfad466072ce6d2dca3f --- fire/formatting.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/fire/formatting.py b/fire/formatting.py index 2c7cb335..6c7c253a 100644 --- a/fire/formatting.py +++ b/fire/formatting.py @@ -18,10 +18,46 @@ from __future__ import division from __future__ import print_function +import os +import platform +import sys import termcolor ELLIPSIS = '...' +# Enable ANSI processing on Windows or disable entirely. +if sys.platform.startswith('win'): + try: + import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error + HAS_COLORAMA = True + except ImportError: + HAS_COLORAMA = False + + if HAS_COLORAMA: + SHOULD_WRAP = True + if sys.stdout.isatty() and platform.release() == '10': + # Enables native ANSI sequences in console. + # Windows 10, 2016, and 2019 only. + import ctypes # pylint: disable=g-import-not-at-top + import subprocess # pylint: disable=g-import-not-at-top + + SHOULD_WRAP = False + KERNEL32 = ctypes.windll.kernel32 # pytype: disable=module-attr + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 + OUT_HANDLE = KERNEL32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pytype: disable=module-attr + # GetConsoleMode fails if the terminal isn't native. + MODE = ctypes.wintypes.DWORD() + if KERNEL32.GetConsoleMode(OUT_HANDLE, ctypes.byref(MODE)) == 0: + SHOULD_WRAP = True + if not MODE.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING: + if KERNEL32.SetConsoleMode( + OUT_HANDLE, MODE.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: + # kernel32.SetConsoleMode to enable ANSI sequences failed + SHOULD_WRAP = True + colorama.init(wrap=SHOULD_WRAP) + else: + os.environ['ANSI_COLORS_DISABLED'] = '1' + def Indent(text, spaces=2): lines = text.split('\n') From d2aa542ca79f3c4f6a8e58b73770ca7ff858fc09 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 6 Mar 2020 22:17:08 +0000 Subject: [PATCH 068/205] Separate module for formatting_windows. PiperOrigin-RevId: 299434969 Change-Id: I071b2dd4e50e504bea3bf60b5599b74b7e230f1e --- fire/formatting.py | 38 ++---------------------- fire/formatting_windows.py | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 fire/formatting_windows.py diff --git a/fire/formatting.py b/fire/formatting.py index 6c7c253a..faef8047 100644 --- a/fire/formatting.py +++ b/fire/formatting.py @@ -18,45 +18,11 @@ from __future__ import division from __future__ import print_function -import os -import platform -import sys +from fire import formatting_windows # pylint: disable=unused-import import termcolor -ELLIPSIS = '...' -# Enable ANSI processing on Windows or disable entirely. -if sys.platform.startswith('win'): - try: - import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error - HAS_COLORAMA = True - except ImportError: - HAS_COLORAMA = False - - if HAS_COLORAMA: - SHOULD_WRAP = True - if sys.stdout.isatty() and platform.release() == '10': - # Enables native ANSI sequences in console. - # Windows 10, 2016, and 2019 only. - import ctypes # pylint: disable=g-import-not-at-top - import subprocess # pylint: disable=g-import-not-at-top - - SHOULD_WRAP = False - KERNEL32 = ctypes.windll.kernel32 # pytype: disable=module-attr - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 - OUT_HANDLE = KERNEL32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pytype: disable=module-attr - # GetConsoleMode fails if the terminal isn't native. - MODE = ctypes.wintypes.DWORD() - if KERNEL32.GetConsoleMode(OUT_HANDLE, ctypes.byref(MODE)) == 0: - SHOULD_WRAP = True - if not MODE.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING: - if KERNEL32.SetConsoleMode( - OUT_HANDLE, MODE.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0: - # kernel32.SetConsoleMode to enable ANSI sequences failed - SHOULD_WRAP = True - colorama.init(wrap=SHOULD_WRAP) - else: - os.environ['ANSI_COLORS_DISABLED'] = '1' +ELLIPSIS = '...' def Indent(text, spaces=2): diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py new file mode 100644 index 00000000..9d7204d7 --- /dev/null +++ b/fire/formatting_windows.py @@ -0,0 +1,60 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module is used for enabling formatting on Windows.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import ctypes +import os +import platform +import subprocess +import sys + + +def initialize_or_disable(): + """Enables ANSI processing on Windows or disables it as needed.""" + try: + import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error + has_colorama = True + except ImportError: + has_colorama = False + + if has_colorama: + wrap = True + if sys.stdout.isatty() and platform.release() == '10': + # Enables native ANSI sequences in console. + # Windows 10, 2016, and 2019 only. + + wrap = False + kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr + enable_virtual_terminal_processing = 0x04 + out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # pytype: disable=module-attr + # GetConsoleMode fails if the terminal isn't native. + mode = ctypes.wintypes.DWORD() + if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0: + wrap = True + if not mode.value & enable_virtual_terminal_processing: + if kernel32.SetConsoleMode( + out_handle, mode.value | enable_virtual_terminal_processing) == 0: + # kernel32.SetConsoleMode to enable ANSI sequences failed + wrap = True + colorama.init(wrap=wrap) + else: + os.environ['ANSI_COLORS_DISABLED'] = '1' + +if sys.platform.startswith('win'): + initialize_or_disable() From 41297c7b1d77474ddee579e5d4c7dcf38b80bf68 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 6 Mar 2020 22:37:53 +0000 Subject: [PATCH 069/205] Fix typo in HelpText docstring. PiperOrigin-RevId: 299439548 Change-Id: I7a35a5991da24402e56de17b19754618c9ef4395 --- fire/helptext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/helptext.py b/fire/helptext.py index fd38cb48..8a1d6881 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -45,7 +45,7 @@ def HelpText(component, trace=None, verbose=False): - """Gets the help string for the current component, suitalbe for a help screen. + """Gets the help string for the current component, suitable for a help screen. Args: component: The component to construct the help string for. From 0be2244c6a6a900d687d4d0bc2ddfd00acf7b84f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 6 Mar 2020 15:25:33 -0800 Subject: [PATCH 070/205] Move colorama import to top PiperOrigin-RevId: 299448843 Change-Id: I398dacae20798132646e5e9d30dc218ea4a24850 --- fire/formatting_windows.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index 9d7204d7..2b85820d 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -24,16 +24,16 @@ import subprocess import sys +try: + import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error + HAS_COLORAMA = True +except ImportError: + HAS_COLORAMA = False + def initialize_or_disable(): """Enables ANSI processing on Windows or disables it as needed.""" - try: - import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error - has_colorama = True - except ImportError: - has_colorama = False - - if has_colorama: + if HAS_COLORAMA: wrap = True if sys.stdout.isatty() and platform.release() == '10': # Enables native ANSI sequences in console. From 598937e0741e1e8e61f7afa75437a5ff860e7386 Mon Sep 17 00:00:00 2001 From: Jeff Tratner Date: Fri, 6 Mar 2020 15:35:13 -0800 Subject: [PATCH 071/205] Enable python -m fire to work Let users run fire as the main function to use fire to create CLIs around *any* python module. This allows you to wrap third party libraries with fire without writing any code! Exciting! :D E.g.: ``` $ python -m fire tempfile mkdtemp /var/folders/fm/sjgpzb856kld04jvq82bxrcw0000gp/T/tmpCbfg_1 ``` or ``` $ python -m fire urllib unquote genetics%3Dawesome%26editor%3Dcrispr genetics=awesome&editor=crispr ``` This follows normal python import semantics, so if you have a script file, it'll work if named just as a module: ``` # myscript.py def fn1(x, y): return x ``` COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/110 from jtratner:main-fire dbe70e23a3de6a85f23a509cfa32cca182ac0bb2 PiperOrigin-RevId: 299450705 Change-Id: I9ebead46b141657d826ca81a787e15d6f8d9c9b8 --- fire/__main__.py | 34 ++++++++++++++++++++++++++++++++++ fire/main_test.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 fire/__main__.py create mode 100644 fire/main_test.py diff --git a/fire/__main__.py b/fire/__main__.py new file mode 100644 index 00000000..949632ef --- /dev/null +++ b/fire/__main__.py @@ -0,0 +1,34 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=invalid-name +"""Enables use of Python Fire as a "main" function (i.e. `python -m fire`). + +This allows using Fire with third-party libraries without modifying their code. +""" + +import importlib +import sys + +import fire + + +def main(args): + module_name = args[1] + module = importlib.import_module(module_name) + fire.Fire(module, name=module_name, command=args[2:]) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/fire/main_test.py b/fire/main_test.py new file mode 100644 index 00000000..9da8f54f --- /dev/null +++ b/fire/main_test.py @@ -0,0 +1,42 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test using Fire via `python -m fire`.""" + +import os + +from fire import __main__ +from fire import testutils + + +class MainModuleTest(testutils.BaseTestCase): + """Tests to verify the behavior of __main__ (python -m fire).""" + + def testNameSetting(self): + # Confirm one of the usage lines has the gettempdir member. + with self.assertOutputMatches('gettempdir'): + __main__.main(['__main__.py', 'tempfile']) + + def testArgPassing(self): + expected = os.path.join('part1', 'part2', 'part3') + with self.assertOutputMatches('%s\n' % expected): + __main__.main(['__main__.py', 'os.path', 'join', 'part1', 'part2', + 'part3']) + with self.assertOutputMatches('%s\n' % expected): + __main__.main(['__main__.py', 'os', 'path', '-', 'join', 'part1', + 'part2', 'part3']) + + +if __name__ == '__main__': + testutils.main() From 335f6e9ab24b0b14200453d21f936e55c43cafa9 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 6 Mar 2020 15:52:05 -0800 Subject: [PATCH 072/205] Set version number in preparation for release. PiperOrigin-RevId: 299454075 Change-Id: Id29897414a82d3be7e4990e6b99094a439702eca --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index 9eff51c4..b97eeb82 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.2.2' +__version__ = '0.3.0' diff --git a/setup.py b/setup.py index c0898a1f..8887b7d7 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.2.2' +VERSION = '0.3.0' URL = 'https://github.com/google/python-fire' setup( From cd008fb42bcfec5daf4215105a92e95a40dde12b Mon Sep 17 00:00:00 2001 From: Daiki Katsuragawa <50144563+daikikatsuragawa@users.noreply.github.com> Date: Mon, 9 Mar 2020 11:16:29 -0700 Subject: [PATCH 073/205] Update README.md formatting COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/233 from daikikatsuragawa:master 5150b412a88af0d75b56188ffd30d4efa30817ef PiperOrigin-RevId: 299886851 Change-Id: Idf29b844e05b3caaeb07b1e205a93eb9b199d326 --- README.md | 22 +++++++++------------- docs/index.md | 22 +++++++++------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 862f5f23..eeb7dc37 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) + _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ @@ -10,7 +11,6 @@ into a CLI. [[3]](docs/benefits.md#exploring) - Python Fire makes using a Python REPL easier by setting up the REPL with the modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl) - ## Installation To install Python Fire with pip, run: `pip install fire` @@ -20,7 +20,6 @@ To install Python Fire with conda, run: `conda install fire -c conda-forge` To install Python Fire from source, first clone the repository and then run: `python setup.py install` - ## Basic Usage You can call `Fire` on any Python object:
@@ -74,17 +73,14 @@ about Fire's other features, see the [Using a Fire CLI page](docs/using-cli.md). For additional examples, see [The Python Fire Guide](docs/guide.md). - ## Why is it called Fire? When you call `Fire`, it fires off (executes) your command. - ## Where can I learn more? Please see [The Python Fire Guide](docs/guide.md). - ## Reference | Setup | Command | Notes @@ -97,14 +93,14 @@ Please see [The Python Fire Guide](docs/guide.md). | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -Using a CLI | Command | Notes -:---------------------------------------------- | :-------------------------------------- | :---- -[Help](docs/using-cli.md#help-flag) | `command --help` or `command -- --help` | -[REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. -[Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. -[Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. -[Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. -[Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` | +| Using a CLI | Command | Notes +| :---------------------------------------------- | :-------------------------------------- | :---- +| [Help](docs/using-cli.md#help-flag) | `command --help` or `command -- --help` | +| [REPL](docs/using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. +| [Separator](docs/using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. +| [Completion](docs/using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. +| [Trace](docs/using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. +| [Verbose](docs/using-cli.md#verbose-flag) | `command -- --verbose` | _Note that these flags are separated from the Fire command by an isolated `--`._ diff --git a/docs/index.md b/docs/index.md index 171ae26e..7dcbb0f1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ # Python Fire [![PyPI](https://img.shields.io/pypi/pyversions/fire.svg?style=plastic)](https://github.com/google/python-fire) + _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ @@ -10,7 +11,6 @@ into a CLI. [[3]](benefits.md#exploring) - Python Fire makes using a Python REPL easier by setting up the REPL with the modules and variables you'll need already imported and created. [[5]](benefits.md#repl) - ## Installation To install Python Fire with pip, run: `pip install fire` @@ -20,7 +20,6 @@ To install Python Fire with conda, run: `conda install fire -c conda-forge` To install Python Fire from source, first clone the repository and then run: `python setup.py install` - ## Basic Usage You can call `Fire` on any Python object:
@@ -74,17 +73,14 @@ about Fire's other features, see the [Using a Fire CLI page](using-cli.md). For additional examples, see [The Python Fire Guide](guide.md). - ## Why is it called Fire? When you call `Fire`, it fires off (executes) your command. - ## Where can I learn more? Please see [The Python Fire Guide](guide.md). - ## Reference | Setup | Command | Notes @@ -97,14 +93,14 @@ Please see [The Python Fire Guide](guide.md). | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -Using a CLI | Command | Notes -:---------------------------------------------- | :-------------------------------------- | :---- -[Help](using-cli.md#help-flag) | `command --help` or `command -- --help` | -[REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. -[Separator](using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. -[Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. -[Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. -[Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | +| Using a CLI | Command | Notes +| :---------------------------------------------- | :-------------------------------------- | :---- +| [Help](using-cli.md#help-flag) | `command --help` or `command -- --help` | +| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. +| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | Sets the separator to `X`. The default separator is `-`. +| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generates a completion script for the CLI. +| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. +| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | _Note that these flags are separated from the Fire command by an isolated `--`._ From aa72776fdeff7ea0dbfe67606bcf89545ef81027 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 13 Mar 2020 11:49:37 -0700 Subject: [PATCH 074/205] Documentation updates for '--help' and 'python -m fire' PiperOrigin-RevId: 300798279 Change-Id: I21bfe6fe5596546b092c2968c9234febf357d4ba --- README.md | 18 +++++++++------ docs/api.md | 63 +++++++++++++++++++++++++++++++++++++++++++-------- docs/guide.md | 21 +++++++++++++++-- docs/index.md | 18 +++++++++------ 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index eeb7dc37..1482d56d 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,17 @@ _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ -- Python Fire is a simple way to create a CLI in Python. [[1]](docs/benefits.md#simple-cli) -- Python Fire is a helpful tool for developing and debugging Python code. [[2]](docs/benefits.md#debugging) -- Python Fire helps with exploring existing code or turning other people's code -into a CLI. [[3]](docs/benefits.md#exploring) -- Python Fire makes transitioning between Bash and Python easier. [[4]](docs/benefits.md#bash) -- Python Fire makes using a Python REPL easier by setting up the REPL with the -modules and variables you'll need already imported and created. [[5]](docs/benefits.md#repl) +- Python Fire is a simple way to create a CLI in Python. + [[1]](docs/benefits.md#simple-cli) +- Python Fire is a helpful tool for developing and debugging Python code. + [[2]](docs/benefits.md#debugging) +- Python Fire helps with exploring existing code or turning other people's + code into a CLI. [[3]](docs/benefits.md#exploring) +- Python Fire makes transitioning between Bash and Python easier. + [[4]](docs/benefits.md#bash) +- Python Fire makes using a Python REPL easier by setting up the REPL with the + modules and variables you'll need already imported and created. + [[5]](docs/benefits.md#repl) ## Installation diff --git a/docs/api.md b/docs/api.md index c3bb2ef6..aa918160 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,3 +1,5 @@ +## Python Fire Quick Reference + | Setup | Command | Notes | :------ | :------------------ | :--------- | install | `pip install fire` | @@ -8,13 +10,56 @@ | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -| Using a CLI | Command | Notes -| :------------- | :------------------------- | :--------- -| [Help](using-cli.md#help-flag) | `command -- --help` | -| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. -| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. -| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. -| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. -| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | +| Using a CLI | Command | Notes | +| :----------------------------------------- | :------------- | :------------- | +| [Help](using-cli.md#help-flag) | `command | Show the help | +: : --help` : screen. : +| [REPL](using-cli.md#interactive-flag) | `command -- | Enters | +: : --interactive` : interactive : +: : : mode. : +| [Separator](using-cli.md#separator-flag) | `command -- | This sets the | +: : --separator=X` : separator to : +: : : `X`. The : +: : : default : +: : : separator is : +: : : `-`. : +| [Completion](using-cli.md#completion-flag) | `command -- | Generate a | +: : --completion : completion : +: : [shell]` : script for the : +: : : CLI. : +| [Trace](using-cli.md#trace-flag) | `command -- | Gets a Fire | +: : --trace` : trace for the : +: : : command. : +| [Verbose](using-cli.md#verbose-flag) | `command -- | | +: : --verbose` : : + +_Note that flags are separated from the Fire command by an isolated `--` arg. +Help is an exception; the isolated `--` is optional for getting help._ + +## Arguments for Calling fire.Fire() + +| Argument | Usage | Notes | +| :-------- | :------------------------ | :----------------------------------- | +| component | `fire.Fire(component)` | If omitted, defaults to a dict of | +: : : all locals and globals. : +| command | `fire.Fire(command='hello | Either a string or a list of | +: : --name=5')` : arguments. If a string is provided, : +: : : it is split to determine the : +: : : arguments. If a list or tuple is : +: : : provided, they are the arguments. If : +: : : `command` is omitted, then : +: : : `sys.argv[1\:]` (the arguments from : +: : : the command line) are used by : +: : : default. : +| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the | +: : : name users will enter to run the : +: : : CLI. This name will be used in the : +: : : CLI's help screens. If the argument : +: : : is omitted, it will be inferred : +: : : automatically. : + +## Using a Fire CLI without modifying any code + +`python -m fire ` -_Note that flags are separated from the Fire command by an isolated `--` arg._ +For example, `python -m fire calendar -h`. diff --git a/docs/guide.md b/docs/guide.md index b02f37b5..5f693a40 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -99,6 +99,22 @@ def main(): fire.Fire(hello) ``` +##### Version 4: Fire Without Code Changes + +If you have a file `example.py` that doesn't even import fire: + +```python +def hello(name): + return 'Hello {name}!'.format(name=name) +``` + +Then you can use it with Fire like this: + +```bash +$ python -m fire example hello --name=World +Hello World! +``` + ### Exposing Multiple Commands In the previous example, we exposed a single function to the command line. Now @@ -685,8 +701,9 @@ You can add the help flag to any command to see help and usage information. Fire incorporates your docstrings into the help and usage information that it generates. Fire will try to provide help even if you omit the isolated `--` separating the flags from the Fire command, but may not always be able to, since -`help` is a valid argument name. Use this feature like this: -`python example.py -- --help`. +`help` is a valid argument name. Use this feature like this: `python +example.py -- --help` or `python example.py --help` (or even `python example.py +-h`). The complete set of flags available is shown below, in the reference section. diff --git a/docs/index.md b/docs/index.md index 7dcbb0f1..4eb114b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,13 +3,17 @@ _Python Fire is a library for automatically generating command line interfaces (CLIs) from absolutely any Python object._ -- Python Fire is a simple way to create a CLI in Python. [[1]](benefits.md#simple-cli) -- Python Fire is a helpful tool for developing and debugging Python code. [[2]](benefits.md#debugging) -- Python Fire helps with exploring existing code or turning other people's code -into a CLI. [[3]](benefits.md#exploring) -- Python Fire makes transitioning between Bash and Python easier. [[4]](benefits.md#bash) -- Python Fire makes using a Python REPL easier by setting up the REPL with the -modules and variables you'll need already imported and created. [[5]](benefits.md#repl) +- Python Fire is a simple way to create a CLI in Python. + [[1]](benefits.md#simple-cli) +- Python Fire is a helpful tool for developing and debugging Python code. + [[2]](benefits.md#debugging) +- Python Fire helps with exploring existing code or turning other people's + code into a CLI. [[3]](benefits.md#exploring) +- Python Fire makes transitioning between Bash and Python easier. + [[4]](benefits.md#bash) +- Python Fire makes using a Python REPL easier by setting up the REPL with the + modules and variables you'll need already imported and created. + [[5]](benefits.md#repl) ## Installation From b97816196071901b714eb70258a6818a59782379 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Mon, 16 Mar 2020 17:21:33 +0000 Subject: [PATCH 075/205] Fix the issue where keyword only arguments with default value is incorrectly marked as required. PiperOrigin-RevId: 301185283 Change-Id: I0d7e2df1d5411769db6250f819f776c955c788f9 --- fire/helptext.py | 31 ++++++++++++++++++++++--------- fire/helptext_test.py | 22 ++++++++++++++++++++++ fire/test_components_py3.py | 3 +++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 8a1d6881..1d165b37 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -33,6 +33,8 @@ from __future__ import division from __future__ import print_function +import itertools + from fire import completion from fire import custom_descriptions from fire import decorators @@ -167,6 +169,11 @@ def _DescriptionSection(component, info): return None +def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec): + return _CreateFlagItem( + flag, docstring_info, required=flag not in spec.kwonlydefaults) + + def _ArgsAndFlagsSections(info, spec, metadata): """The "Args and Flags" sections of the help string.""" args_with_no_defaults = spec.args[:len(spec.args) - len(spec.defaults)] @@ -199,15 +206,15 @@ def _ArgsAndFlagsSections(info, spec, metadata): ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') ) - optional_flag_items = [ + positional_flag_items = [ _CreateFlagItem(flag, docstring_info, required=False) for flag in args_with_defaults ] - required_flag_items = [ - _CreateFlagItem(flag, docstring_info, required=True) + kwonly_flag_items = [ + _CreateKeywordOnlyFlagItem(flag, docstring_info, spec) for flag in spec.kwonlyargs ] - flag_items = optional_flag_items + required_flag_items + flag_items = positional_flag_items + kwonly_flag_items if spec.varkw: description = _GetArgDescription(spec.varkw, docstring_info) @@ -382,9 +389,7 @@ def _CreateFlagItem(flag, docstring_info, required=False): flag: The name of the flag. docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. - required: Whether the flag is required. Keyword-only arguments (only in - Python 3) become required flags, whereas normal keyword arguments become - optional flags. + required: Whether the flag is required. Returns: A string to be used in constructing the help screen for the function. """ @@ -570,13 +575,21 @@ def _GetCallableUsageItems(spec, metadata): return items +def _KeywordOnlyArguments(spec, required=True): + return (flag for flag in spec.kwonlyargs + if required == (flag in spec.kwonlydefaults)) + + def _GetCallableAvailabilityLines(spec): """The list of availability lines for a callable for use in a usage string.""" args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args. - optional_flags = [('--' + flag) for flag in args_with_defaults] - required_flags = [('--' + flag) for flag in spec.kwonlyargs] + optional_flags = [('--' + flag) for flag in itertools.chain( + args_with_defaults, _KeywordOnlyArguments(spec, required=False))] + required_flags = [ + ('--' + flag) for flag in _KeywordOnlyArguments(spec, required=True) + ] # Flags section: availability_lines = [] diff --git a/fire/helptext_test.py b/fire/helptext_test.py index a866ee4d..00e88c2d 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -20,12 +20,14 @@ import os import textwrap +import unittest from fire import formatting from fire import helptext from fire import test_components as tc from fire import testutils from fire import trace +import six class HelpTest(testutils.BaseTestCase): @@ -158,6 +160,26 @@ def testHelpTextNoInit(self): self.assertIn('NAME\n OldStyleEmpty', help_screen) self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) + @unittest.skipIf( + six.PY2, + 'Python 2 does not support single asterisk in function definition') + def testHelpTextKeywordOnlyArgumentsWithDefault(self): + component = tc.py3.KeywordOnly.with_default + output = helptext.HelpText( + component=component, trace=trace.FireTrace(component, 'with_default')) + self.assertIn('NAME\n with_default', output) + self.assertIn('FLAGS\n --x=X', output) + + @unittest.skipIf( + six.PY2, + 'Python 2 does not support single asterisk in function definition') + def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): + component = tc.py3.KeywordOnly.double + output = helptext.HelpText( + component=component, trace=trace.FireTrace(component, 'double')) + self.assertIn('NAME\n double', output) + self.assertIn('FLAGS\n --count=COUNT (required)', output) + def testHelpScreen(self): component = tc.ClassWithDocstring() t = trace.FireTrace(component, name='ClassWithDocstring') diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index e714be30..dc37c3b1 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -32,6 +32,9 @@ def double(self, *, count): def triple(self, *, count): return count * 3 + def with_default(self, *, x="x"): + print("x: " + x) + class LruCacheDecoratedMethod(object): From 61785e614c9ee1f0f16f8d8f1fd18b92a1981b4a Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Mar 2020 12:47:16 -0700 Subject: [PATCH 076/205] Switch from unittest to testutils. PiperOrigin-RevId: 301875411 Change-Id: Ie5df8560ce1a17f3cbf30b8087fb807398eb5ee0 --- fire/helptext_test.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 00e88c2d..3619fb06 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -20,7 +20,6 @@ import os import textwrap -import unittest from fire import formatting from fire import helptext @@ -160,21 +159,19 @@ def testHelpTextNoInit(self): self.assertIn('NAME\n OldStyleEmpty', help_screen) self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) - @unittest.skipIf( - six.PY2, - 'Python 2 does not support single asterisk in function definition') + @testutils.skipIf( + six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithDefault(self): - component = tc.py3.KeywordOnly.with_default + component = tc.py3.KeywordOnly.with_default # pytype: disable=module-attr output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'with_default')) self.assertIn('NAME\n with_default', output) self.assertIn('FLAGS\n --x=X', output) - @unittest.skipIf( - six.PY2, - 'Python 2 does not support single asterisk in function definition') + @testutils.skipIf( + six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): - component = tc.py3.KeywordOnly.double + component = tc.py3.KeywordOnly.double # pytype: disable=module-attr output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'double')) self.assertIn('NAME\n double', output) From af9d8488d595e71a73b95373e5a23cdf1ba54f2e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 3 Apr 2020 17:41:42 +0000 Subject: [PATCH 077/205] Remove preexec_fn from subprocess call in console_io.py PiperOrigin-RevId: 304646913 Change-Id: I16b76e90490984fd9f751823bf1304d75eb087a0 --- fire/console/console_io.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/fire/console/console_io.py b/fire/console/console_io.py index 784369b7..3d3b9f81 100644 --- a/fire/console/console_io.py +++ b/fire/console/console_io.py @@ -69,10 +69,6 @@ def IsInteractive(output=False, error=False, heuristic=False): return True -def PreexecFunc(): - signal.signal(signal.SIGINT, signal.SIG_IGN) - - def More(contents, out, prompt=None, check_pager=True): """Run a user specified pager or fall back to the internal pager. @@ -102,18 +98,14 @@ def More(contents, out, prompt=None, check_pager=True): less_orig = encoding.GetEncodedValue(os.environ, 'LESS', None) less = '-R' + (less_orig or '') encoding.SetEncodedValue(os.environ, 'LESS', less) - # Ignores SIGINT from this point on since the child process has started - # and we don't want to terminate either one when the child is still alive. + # Ignore SIGINT while the pager is running. + # We don't want to terminate the parent while the child is still alive. signal.signal(signal.SIGINT, signal.SIG_IGN) - # Runs PreexecFunc before starting the child so SIGINT is ignored for the - # child process as well. - p = subprocess.Popen( - pager, stdin=subprocess.PIPE, shell=True, preexec_fn=PreexecFunc) + p = subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True) enc = console_attr.GetConsoleAttr().GetEncoding() p.communicate(input=contents.encode(enc)) p.wait() - # Starts using default disposition for SIGINT again after the child has - # exited. + # Start using default signal handling for SIGINT again. signal.signal(signal.SIGINT, signal.SIG_DFL) if less_orig is None: encoding.SetEncodedValue(os.environ, 'LESS', None) From fb7ee3a716020f6a04c0e55967612f9322d16893 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 3 Apr 2020 11:04:51 -0700 Subject: [PATCH 078/205] Bump version number to 0.3.1. PiperOrigin-RevId: 304652311 Change-Id: I7e7e7c1d8cce1eb2c9dd59130b97ccc873b537fb --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index b97eeb82..2bacc8fe 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.3.0' +__version__ = '0.3.1' diff --git a/setup.py b/setup.py index 8887b7d7..0047fa95 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.3.0' +VERSION = '0.3.1' URL = 'https://github.com/google/python-fire' setup( From 457b1562b88ae8f018c7b2bed46028035a75cccf Mon Sep 17 00:00:00 2001 From: uburuntu Date: Fri, 12 Jun 2020 10:51:40 -0700 Subject: [PATCH 079/205] Support for Python 3.8 - Enables Travis for 3.8 - Fixes namedtuple test for 3.8 COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/263 from uburuntu:master efaf65fe5b0986121f0e1b76a949a07169dd354f PiperOrigin-RevId: 316134191 Change-Id: I5e3ef20b488fa917b81525d5936a8e147b0458e1 --- .travis.yml | 11 +++-------- fire/completion.py | 14 ++++++++++---- setup.py | 1 + 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 16246cb8..30f0cad6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,14 +3,9 @@ python: - "2.7" - "3.4" - "3.5" - - "3.6" -# Workaround for testing Python 3.7: -# https://github.com/travis-ci/travis-ci/issues/9815 -matrix: - include: - - python: 3.7 - dist: xenial - sudo: yes + - "3.7" + - "3.8" + before_install: - pip install --upgrade setuptools pip - pip install --upgrade pylint pytest pytest-pylint pytest-runner diff --git a/fire/completion.py b/fire/completion.py index 451e2021..2c9f15c0 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -320,10 +320,16 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): if class_attrs is None: class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {} class_attr = class_attrs.get(name) - if class_attr and class_attr.kind in ('method', 'property'): - # methods and properties should be accessed on instantiated objects, - # not uninstantiated classes. - return False + if class_attr: + # Methods and properties should only be accessible on instantiated + # objects, not on uninstantiated classes. + if class_attr.kind in ('method', 'property'): + return False + # Backward compatibility notes: Before Python 3.8, namedtuple attributes + # were properties. In Python 3.8, they have type tuplegetter. + tuplegetter = getattr(collections, '_tuplegetter', type(None)) + if isinstance(class_attr.object, tuplegetter): + return False if (six.PY2 and inspect.isfunction(component) and name in ('func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name')): diff --git a/setup.py b/setup.py index 0047fa95..efd3f766 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: OS Independent', 'Operating System :: POSIX', From 79d85df2706b2dab5dd5075f2a76953743ee9bf3 Mon Sep 17 00:00:00 2001 From: Michael Garbutt Date: Tue, 14 Jul 2020 09:00:56 -0700 Subject: [PATCH 080/205] Added types and defaults to function help text. PiperOrigin-RevId: 321168198 Change-Id: I228b706f9db615ac10ad0865b9c5cc82107f3fb7 --- fire/helptext.py | 122 ++++++++++++++++++++++++++++++++---- fire/helptext_test.py | 92 ++++++++++++++++++++++++++- fire/test_components.py | 9 ++- fire/test_components_py3.py | 41 ++++++++++++ setup.cfg | 3 +- 5 files changed, 252 insertions(+), 15 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 1d165b37..ecb6bbb4 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -34,6 +34,7 @@ from __future__ import print_function import itertools +import sys from fire import completion from fire import custom_descriptions @@ -44,6 +45,7 @@ LINE_LENGTH = 80 SECTION_INDENTATION = 4 +SUBSECTION_INDENTATION = 4 def HelpText(component, trace=None, verbose=False): @@ -171,7 +173,7 @@ def _DescriptionSection(component, info): def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec): return _CreateFlagItem( - flag, docstring_info, required=flag not in spec.kwonlydefaults) + flag, docstring_info, spec, required=flag not in spec.kwonlydefaults) def _ArgsAndFlagsSections(info, spec, metadata): @@ -188,13 +190,13 @@ def _ArgsAndFlagsSections(info, spec, metadata): docstring_info = info['docstring_info'] arg_items = [ - _CreateArgItem(arg, docstring_info) + _CreateArgItem(arg, docstring_info, spec) for arg in args_with_no_defaults ] if spec.varargs: arg_items.append( - _CreateArgItem(spec.varargs, docstring_info) + _CreateArgItem(spec.varargs, docstring_info, spec) ) if arg_items: @@ -207,7 +209,7 @@ def _ArgsAndFlagsSections(info, spec, metadata): ) positional_flag_items = [ - _CreateFlagItem(flag, docstring_info, required=False) + _CreateFlagItem(flag, docstring_info, spec, required=False) for flag in args_with_defaults ] kwonly_flag_items = [ @@ -365,43 +367,139 @@ def _CreateOutputSection(name, content): content=formatting.Indent(content, SECTION_INDENTATION)) -def _CreateArgItem(arg, docstring_info): +def _CreateArgItem(arg, docstring_info, spec): """Returns a string describing a positional argument. Args: arg: The name of the positional argument. docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. + spec: An instance of fire.inspectutils.FullArgSpec, containing type and + default information about the arguments to a callable. Returns: A string to be used in constructing the help screen for the function. """ + + # The help string is indented, so calculate the maximum permitted length + # before indentation to avoid exceeding the maximum line length. + max_str_length = LINE_LENGTH - SECTION_INDENTATION - SUBSECTION_INDENTATION + description = _GetArgDescription(arg, docstring_info) - arg = arg.upper() - return _CreateItem(formatting.BoldUnderline(arg), description, indent=4) + arg_string = formatting.BoldUnderline(arg.upper()) + + arg_type = _GetArgType(arg, spec) + arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + available_space = max_str_length - len(arg_type) + arg_type = ( + formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) + + description = '\n'.join(part for part in (arg_type, description) if part) + return _CreateItem(arg_string, description, indent=SUBSECTION_INDENTATION) -def _CreateFlagItem(flag, docstring_info, required=False): - """Returns a string describing a flag using information from the docstring. + +def _CreateFlagItem(flag, docstring_info, spec, required=False): + """Returns a string describing a flag using docstring and FullArgSpec info. Args: flag: The name of the flag. docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. + spec: An instance of fire.inspectutils.FullArgSpec, containing type and + default information about the arguments to a callable. required: Whether the flag is required. Returns: A string to be used in constructing the help screen for the function. """ + # pylint: disable=g-bad-todo + # TODO(MichaelCG8): Get type and default information from docstrings if it is + # not available in FullArgSpec. This will require updating + # fire.docstrings.parser(). + + # The help string is indented, so calculate the maximum permitted length + # before indentation to avoid exceeding the maximum line length. + max_str_length = LINE_LENGTH - SECTION_INDENTATION - SUBSECTION_INDENTATION + description = _GetArgDescription(flag, docstring_info) flag_string_template = '--{flag_name}={flag_name_upper}' - flag = flag_string_template.format( + flag_string = flag_string_template.format( flag_name=flag, flag_name_upper=formatting.Underline(flag.upper())) if required: - flag += ' (required)' - return _CreateItem(flag, description, indent=4) + flag_string += ' (required)' + + arg_type = _GetArgType(flag, spec) + arg_default = _GetArgDefault(flag, spec) + + # We need to handle the case where there is a default of None, but otherwise + # the argument has another type. + if arg_default == 'None': + arg_type = 'Optional[{}]'.format(arg_type) + + arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + available_space = max_str_length - len(arg_type) + arg_type = ( + formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) + + arg_default = 'Default: {}'.format(arg_default) if arg_default else '' + available_space = max_str_length - len(arg_default) + arg_default = ( + formatting.EllipsisTruncate(arg_default, available_space, max_str_length)) + + description = '\n'.join( + part for part in (arg_type, arg_default, description) if part + ) + + return _CreateItem(flag_string, description, indent=SUBSECTION_INDENTATION) + + +def _GetArgType(arg, spec): + """Returns a string describing the type of an argument. + + Args: + arg: The name of the argument. + spec: An instance of fire.inspectutils.FullArgSpec, containing type and + default information about the arguments to a callable. + Returns: + A string to be used in constructing the help screen for the function, the + empty string if the argument type is not available. + """ + if arg in spec.annotations: + arg_type = spec.annotations[arg] + try: + if sys.version_info[0:2] >= (3, 3): + return arg_type.__qualname__ + return arg_type.__name__ + except AttributeError: + # Some typing objects, such as typing.Union do not have either a __name__ + # or __qualname__ attribute. + # repr(typing.Union[int, str]) will return ': typing.Union[int, str]' + return repr(arg_type) + return '' + + +def _GetArgDefault(flag, spec): + """Returns a string describing a flag's default value. + + Args: + flag: The name of the flag. + spec: An instance of fire.inspectutils.FullArgSpec, containing type and + default information about the arguments to a callable. + Returns: + A string to be used in constructing the help screen for the function, the + empty string if the flag does not have a default or the default is not + available. + """ + num_defaults = len(spec.defaults) + args_with_defaults = spec.args[-num_defaults:] + + for arg, default in zip(args_with_defaults, spec.defaults): + if arg == flag: + return repr(default) + return '' def _CreateItem(name, description, indent=2): diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 3619fb06..d9a4dab8 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -19,6 +19,7 @@ from __future__ import print_function import os +import sys import textwrap from fire import formatting @@ -80,9 +81,97 @@ def testHelpTextFunctionWithDefaults(self): self.assertIn('NAME\n triple', help_screen) self.assertIn('SYNOPSIS\n triple ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - self.assertIn('FLAGS\n --count=COUNT', help_screen) + self.assertIn('FLAGS\n --count=COUNT\n Default: 0', help_screen) self.assertNotIn('NOTES', help_screen) + def testHelpTextFunctionWithLongDefaults(self): + component = tc.WithDefaults().text + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='text')) + self.assertIn('NAME\n text', help_screen) + self.assertIn('SYNOPSIS\n text ', help_screen) + self.assertNotIn('DESCRIPTION', help_screen) + self.assertIn( + 'FLAGS\n --string=STRING\n' + ' Default: \'0001020304050607080910' + '1112131415161718192021222324252627282...', + help_screen) + self.assertNotIn('NOTES', help_screen) + + @testutils.skipIf( + sys.version_info[0:2] < (3, 5), + 'Python < 3.5 does not support type hints.') + def testHelpTextFunctionWithDefaultsAndTypes(self): + component = tc.py3.WithDefaultsAndTypes().double + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='double')) + self.assertIn('NAME\n double', help_screen) + self.assertIn('SYNOPSIS\n double ', help_screen) + self.assertIn('DESCRIPTION', help_screen) + self.assertIn( + 'FLAGS\n --count=COUNT\n Type: float\n Default: 0', + help_screen) + self.assertNotIn('NOTES', help_screen) + + @testutils.skipIf( + sys.version_info[0:2] < (3, 5), + 'Python < 3.5 does not support type hints.') + def testHelpTextFunctionWithTypesAndDefaultNone(self): + component = tc.py3.WithDefaultsAndTypes().get_int + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='get_int')) + self.assertIn('NAME\n get_int', help_screen) + self.assertIn('SYNOPSIS\n get_int ', help_screen) + self.assertNotIn('DESCRIPTION', help_screen) + self.assertIn( + 'FLAGS\n --value=VALUE\n' + ' Type: Optional[int]\n Default: None', + help_screen) + self.assertNotIn('NOTES', help_screen) + + @testutils.skipIf( + sys.version_info[0:2] < (3, 5), + 'Python < 3.5 does not support type hints.') + def testHelpTextFunctionWithTypes(self): + component = tc.py3.WithTypes().double + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='double')) + self.assertIn('NAME\n double', help_screen) + self.assertIn('SYNOPSIS\n double COUNT', help_screen) + self.assertIn('DESCRIPTION', help_screen) + self.assertIn( + 'POSITIONAL ARGUMENTS\n COUNT\n Type: float', + help_screen) + self.assertIn( + 'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS', + help_screen) + + @testutils.skipIf( + sys.version_info[0:2] < (3, 5), + 'Python < 3.5 does not support type hints.') + def testHelpTextFunctionWithLongTypes(self): + component = tc.py3.WithTypes().long_type + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='long_type')) + self.assertIn('NAME\n long_type', help_screen) + self.assertIn('SYNOPSIS\n long_type LONG_OBJ', help_screen) + self.assertNotIn('DESCRIPTION', help_screen) + # TODO(dbieber): Assert type is displayed correctly. Type displays + # differently in Travis vs in Google. + # self.assertIn( + # 'POSITIONAL ARGUMENTS\n LONG_OBJ\n' + # ' Type: typing.Tuple[typing.Tuple[' + # 'typing.Tuple[typing.Tuple[typing.Tupl...', + # help_screen) + self.assertIn( + 'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS', + help_screen) + def testHelpTextFunctionWithBuiltin(self): component = 'test'.upper help_screen = helptext.HelpText( @@ -244,6 +333,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): FLAGS --count=COUNT + Default: 0 Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), help_output.strip()) diff --git a/fire/test_components.py b/fire/test_components.py index a25ba7af..824e73fc 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -19,9 +19,9 @@ from __future__ import print_function import collections +import enum import functools -import enum import six if six.PY3: @@ -105,6 +105,13 @@ def double(self, count=0): def triple(self, count=0): return 3 * count + def text( + self, + string=('0001020304050607080910111213141516171819' + '2021222324252627282930313233343536373839') + ): + return string + class OldStyleWithDefaults: # pylint: disable=old-style-class,no-init diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index dc37c3b1..56f64356 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -16,6 +16,7 @@ """This module has components that use Python 3 specific syntax.""" import functools +from typing import Tuple # pylint: disable=keyword-arg-before-vararg @@ -46,3 +47,43 @@ def lru_cache_in_class(self, arg1): @functools.lru_cache() def lru_cache_decorated(arg1): return arg1 + + +class WithTypes(object): + """Class with functions that have default arguments and types.""" + + def double(self, count: float) -> float: + """Returns the input multiplied by 2. + + Args: + count: Input number that you want to double. + + Returns: + A number that is the double of count.s + """ + return 2 * count + + def long_type( + self, + long_obj: (Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[Tuple[ + Tuple[Tuple[Tuple[Tuple[Tuple[int]]]]]]]]]]]]) + ): + return long_obj + + +class WithDefaultsAndTypes(object): + """Class with functions that have default arguments and types.""" + + def double(self, count: float = 0) -> float: + """Returns the input multiplied by 2. + + Args: + count: Input number that you want to double. + + Returns: + A number that is the double of count.s + """ + return 2 * count + + def get_int(self, value: int = None): + return 0 if value is None else value diff --git a/setup.cfg b/setup.cfg index 058f329c..7a980136 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,8 @@ universal = 1 test = pytest [tool:pytest] -addopts = --ignore=fire/test_components_py3.py --ignore=fire/parser_fuzz_test.py +addopts = --ignore=fire/test_components_py3.py + --ignore=fire/parser_fuzz_test.py [pytype] inputs = . From b5d6341d3034efc594421783d45620551c286a37 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 14 Jul 2020 11:01:11 -0700 Subject: [PATCH 081/205] Add module-attr disable hints. PiperOrigin-RevId: 321192380 Change-Id: Ifead01df416bb2fbdd6cc220efd948f237c2120b --- fire/helptext_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index d9a4dab8..1ec16295 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -103,7 +103,8 @@ def testHelpTextFunctionWithLongDefaults(self): sys.version_info[0:2] < (3, 5), 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithDefaultsAndTypes(self): - component = tc.py3.WithDefaultsAndTypes().double + component = ( + tc.py3.WithDefaultsAndTypes().double) # pytype: disable=module-attr help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -119,7 +120,8 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): sys.version_info[0:2] < (3, 5), 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypesAndDefaultNone(self): - component = tc.py3.WithDefaultsAndTypes().get_int + component = ( + tc.py3.WithDefaultsAndTypes().get_int) # pytype: disable=module-attr help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='get_int')) @@ -136,7 +138,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): sys.version_info[0:2] < (3, 5), 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypes(self): - component = tc.py3.WithTypes().double + component = tc.py3.WithTypes().double # pytype: disable=module-attr help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -154,7 +156,7 @@ def testHelpTextFunctionWithTypes(self): sys.version_info[0:2] < (3, 5), 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithLongTypes(self): - component = tc.py3.WithTypes().long_type + component = tc.py3.WithTypes().long_type # pytype: disable=module-attr help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='long_type')) From c3789064e2e161992e3c5a23eb0c47b66072707f Mon Sep 17 00:00:00 2001 From: Bradley D'Amato <53579156+bradleydamato@users.noreply.github.com> Date: Fri, 2 Oct 2020 11:15:43 -0700 Subject: [PATCH 082/205] Change member future check to fix issue 272 bb58f2611469851d842f95a65da1bf926c0c85c3 by bradleydamato ebc446fa79702ce532fb2e0355b69335e6ef027a by bradleydamato 98b131f15d2f9540628942f975699a00a89d0c87 by bradleydamato COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/290 from bradleydamato:Issue272Fix 98b131f15d2f9540628942f975699a00a89d0c87 PiperOrigin-RevId: 335063289 Change-Id: I1715092509aad11b0bbb681b76a1db7ab48e9a78 --- fire/completion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fire/completion.py b/fire/completion.py index 2c9f15c0..ed7a1b61 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -308,7 +308,9 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): return False if verbose: return True - if member in (absolute_import, division, print_function): + if (member is absolute_import + or member is division + or member is print_function): return False if isinstance(member, type(absolute_import)) and six.PY34: return False From 7d5011ad0562966ee470a44cf0abcfee05825295 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 2 Oct 2020 13:40:56 -0700 Subject: [PATCH 083/205] disable lint checks super-with-arguments and raise-missing-from PiperOrigin-RevId: 335092048 Change-Id: I3d6328a9f4b4704e4c665d657b032cada0463594 --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 37bfa447..1b1c5cc2 100644 --- a/pylintrc +++ b/pylintrc @@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from [REPORTS] From 878b8d86f488ef2606cffdf58297dd2781708316 Mon Sep 17 00:00:00 2001 From: Daniel Zheng Date: Mon, 5 Oct 2020 11:20:49 -0700 Subject: [PATCH 084/205] Fix inclusion of enum34 if setup.py. 7d100c145a5a0afc44394e36c4c5c02afba288dc by Daniel Zheng COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/289 from dzheng256:patch-1 7d100c145a5a0afc44394e36c4c5c02afba288dc PiperOrigin-RevId: 335459087 Change-Id: Ifafd1ed3793604ed78b487189a0be908d699bd51 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index efd3f766..572fb3c6 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ """The setup.py file for Python Fire.""" -import sys from setuptools import setup LONG_DESCRIPTION = """ @@ -32,7 +31,8 @@ DEPENDENCIES = [ 'six', 'termcolor', -] + (['enum34'] if sys.version < '3.4' else []) + 'enum34; python_version < "3.4"' +] TEST_DEPENDENCIES = [ 'hypothesis', From 5311d21b1b4ddbc6330396ce2d9fd1eb9642ff97 Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Thu, 29 Oct 2020 10:24:31 -0700 Subject: [PATCH 085/205] Added better error handling for module imports and PiperOrigin-RevId: 339695282 Change-Id: I28e98d51a9d1ee20aeb69d1cb667980e3c7607cf --- fire/__main__.py | 69 +++++++++++++++++++++++++++++++++++++++++++++-- fire/main_test.py | 58 ++++++++++++++++++++++++++++++++++++--- fire/testutils.py | 18 +++++++++++-- 3 files changed, 137 insertions(+), 8 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 949632ef..f23c7d9a 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -19,14 +19,79 @@ """ import importlib +import os import sys import fire +cli_string = """usage: python -m fire [module] [arg] ..." -def main(args): - module_name = args[1] +Python Fire is a library for creating CLIs from absolutely any Python +object or program. To run Python Fire from the command line on an +existing Python file, it can be invoked with "python -m fire [module]" +and passed a Python module using module notation: + +"python -m fire packageA.packageB.module" + +or with a file path: + +"python -m fire packageA/packageB/module.py" """ + + +def import_from_file_path(path): + """Performs a module import given the filename.""" + module_name = os.path.basename(path) + + if sys.version_info.major == 3: + from importlib import util # pylint: disable=g-import-not-at-top + spec = util.spec_from_file_location(module_name, path) + + if spec is None: + raise IOError('Unable to load module from specified path.') + + module = util.module_from_spec(spec) + spec.loader.exec_module(module) # pytype: disable=attribute-error + else: + import imp # pylint: disable=g-import-not-at-top + module = imp.load_source(module_name, path) + + return module, module_name + + +def import_from_module_name(module_name): module = importlib.import_module(module_name) + return module, module_name + + +def import_module(module_or_filename): + """Imports a given module or filename.""" + + if os.path.exists(module_or_filename): + # importlib.util.spec_from_file_location requires .py + if not module_or_filename.endswith('.py'): + try: # try as module instead + return import_from_module_name(module_or_filename) + except ImportError: + raise ValueError('Fire can only be called on .py files.') + + return import_from_file_path(module_or_filename) + + if os.path.sep in module_or_filename: # Use / to detect if it was a filename. + raise IOError('Fire was passed a filename which could not be found.') + + return import_from_module_name(module_or_filename) # Assume it's a module. + + +def main(args): + """Entrypoint for fire when invoked as a module with python -m fire.""" + + if len(args) < 2: + print(cli_string) + exit(1) + + module_or_filename = args[1] + module, module_name = import_module(module_or_filename) + fire.Fire(module, name=module_name, command=args[2:]) diff --git a/fire/main_test.py b/fire/main_test.py index 9da8f54f..478e370a 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -15,6 +15,7 @@ """Test using Fire via `python -m fire`.""" import os +import tempfile from fire import __main__ from fire import testutils @@ -31,11 +32,60 @@ def testNameSetting(self): def testArgPassing(self): expected = os.path.join('part1', 'part2', 'part3') with self.assertOutputMatches('%s\n' % expected): - __main__.main(['__main__.py', 'os.path', 'join', 'part1', 'part2', - 'part3']) + __main__.main( + ['__main__.py', 'os.path', 'join', 'part1', 'part2', 'part3']) with self.assertOutputMatches('%s\n' % expected): - __main__.main(['__main__.py', 'os', 'path', '-', 'join', 'part1', - 'part2', 'part3']) + __main__.main( + ['__main__.py', 'os', 'path', '-', 'join', 'part1', 'part2', 'part3']) + + +class MainModuleFileTest(testutils.BaseTestCase): + """Tests to verify correct import behavior for file executables.""" + + def setUp(self): + super(MainModuleFileTest, self).setUp() + self.file = tempfile.NamedTemporaryFile(suffix='.py') + self.file.write(b'class Foo:\n def double(self, n):\n return 2 * n\n') + self.file.flush() + + self.file2 = tempfile.NamedTemporaryFile() + + def testFileNameFire(self): + # Confirm that the file is correctly imported and doubles the number. + with self.assertOutputMatches('4'): + __main__.main( + ['__main__.py', self.file.name, 'Foo', 'double', '--n', '2']) + + def testFileNameFailure(self): + # Confirm that an existing file without a .py suffix raises a ValueError. + with self.assertRaises(ValueError): + __main__.main( + ['__main__.py', self.file2.name, 'Foo', 'double', '--n', '2']) + + def testFileNameModuleDuplication(self): + # Confirm that a file that masks a module still loads the module. + with self.assertOutputMatches('gettempdir'): + file = self.create_tempfile('tempfile') + + with testutils.ChangeDirectory(os.path.dirname(file.full_path)): + __main__.main([ + '__main__.py', + 'tempfile', + ]) + + def testFileNameModuleFileFailure(self): + # Confirm that an invalid file that masks a non-existent module fails. + with self.assertRaisesWithLiteralMatch( + ValueError, 'Fire can only be called on .py files.'): + file = self.create_tempfile('foobar') + + with testutils.ChangeDirectory(os.path.dirname(file.full_path)): + assert os.path.exists('foobar') + + __main__.main([ + '__main__.py', + 'foobar', + ]) if __name__ == '__main__': diff --git a/fire/testutils.py b/fire/testutils.py index 3463fd50..831ce78a 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -19,6 +19,7 @@ from __future__ import print_function import contextlib +import os import re import sys import unittest @@ -44,6 +45,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): stdout: (str) regexp to match against stdout (None will check no stdout) stderr: (str) regexp to match against stderr (None will check no stderr) capture: (bool, default True) do not bubble up stdout or stderr + Yields: Yields to the wrapped context. """ @@ -80,6 +82,7 @@ def assertRaisesFireExit(self, code, regexp='.*'): Args: code: The status code that the FireExit should contain. regexp: stdout must match this regex. + Yields: Yields to the wrapped context. """ @@ -89,12 +92,23 @@ def assertRaisesFireExit(self, code, regexp='.*'): yield except core.FireExit as exc: if exc.code != code: - raise AssertionError('Incorrect exit code: %r != %r' % (exc.code, - code)) + raise AssertionError('Incorrect exit code: %r != %r' % + (exc.code, code)) self.assertIsInstance(exc.trace, trace.FireTrace) raise +@contextlib.contextmanager +def ChangeDirectory(directory): + cwdir = os.getcwd() + os.chdir(directory) + + try: + yield directory + finally: + os.chdir(cwdir) + + # pylint: disable=invalid-name main = unittest.main skip = unittest.skip From c1a0450c66e0c3d9b177d5618d9864f04c565091 Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Thu, 29 Oct 2020 11:44:38 -0700 Subject: [PATCH 086/205] Fixed compatibility issues with unittest module and Python versions below 3.5. PiperOrigin-RevId: 339713431 Change-Id: I80cb92f74a3958cde56ece04e393d1f2c079d86e --- fire/__main__.py | 13 ++++++++++++- fire/main_test.py | 36 +++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index f23c7d9a..2ea8ae10 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -42,7 +42,17 @@ def import_from_file_path(path): """Performs a module import given the filename.""" module_name = os.path.basename(path) - if sys.version_info.major == 3: + if sys.version_info.major == 3 and sys.version_info.minor < 5: + loader = importlib.machinery.SourceFileLoader( + fullname=module_name, + path=path, + ) + spec = importlib.util.spec_from_loader(loader.name, loader, origin=path) + module = importlib.util.module_from_spec(spec) + sys.modules[loader.name] = module + loader.exec_module(module) + + elif sys.version_info.major == 3: from importlib import util # pylint: disable=g-import-not-at-top spec = util.spec_from_file_location(module_name, path) @@ -51,6 +61,7 @@ def import_from_file_path(path): module = util.module_from_spec(spec) spec.loader.exec_module(module) # pytype: disable=attribute-error + else: import imp # pylint: disable=g-import-not-at-top module = imp.load_source(module_name, path) diff --git a/fire/main_test.py b/fire/main_test.py index 478e370a..75707dc4 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -65,27 +65,29 @@ def testFileNameFailure(self): def testFileNameModuleDuplication(self): # Confirm that a file that masks a module still loads the module. with self.assertOutputMatches('gettempdir'): - file = self.create_tempfile('tempfile') + dirname = os.path.dirname(self.file.name) + with testutils.ChangeDirectory(dirname): + with open('tempfile', 'w'): + __main__.main([ + '__main__.py', + 'tempfile', + ]) - with testutils.ChangeDirectory(os.path.dirname(file.full_path)): - __main__.main([ - '__main__.py', - 'tempfile', - ]) + os.remove('tempfile') def testFileNameModuleFileFailure(self): # Confirm that an invalid file that masks a non-existent module fails. - with self.assertRaisesWithLiteralMatch( - ValueError, 'Fire can only be called on .py files.'): - file = self.create_tempfile('foobar') - - with testutils.ChangeDirectory(os.path.dirname(file.full_path)): - assert os.path.exists('foobar') - - __main__.main([ - '__main__.py', - 'foobar', - ]) + with self.assertRaisesRegex(ValueError, + r'Fire can only be called on \.py files\.'): + dirname = os.path.dirname(self.file.name) + with testutils.ChangeDirectory(dirname): + with open('foobar', 'w'): + __main__.main([ + '__main__.py', + 'foobar', + ]) + + os.remove('foobar') if __name__ == '__main__': From 6ef90c0155c7e092163cac9413ea22dd2a31bfeb Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Thu, 29 Oct 2020 20:13:20 -0700 Subject: [PATCH 087/205] Updated to fix unittest compatibility and Python 3.4 backwards compatibility. PiperOrigin-RevId: 339798577 Change-Id: I94655e65e19dc56d6ee608e84ec02056558154d3 --- fire/__main__.py | 50 +++++++++++++++++++++++++++++++++++++---------- fire/testutils.py | 8 ++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 2ea8ae10..3a26ade1 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Lint as: python2, python3 # pylint: disable=invalid-name -"""Enables use of Python Fire as a "main" function (i.e. `python -m fire`). +"""Enables use of Python Fire as a "main" function (i.e. "python -m fire"). This allows using Fire with third-party libraries without modifying their code. """ @@ -39,7 +40,22 @@ def import_from_file_path(path): - """Performs a module import given the filename.""" + """Performs a module import given the filename. + + Args: + path (str): the path to the file to be imported. + + Raises: + IOError: if the given file does not exist or importlib fails to load it. + + Returns: + Tuple[ModuleType, str]: returns the imported module and the module name, + usually extracted from the path itself. + """ + + if not os.path.exists(path): + raise IOError('Given file path does not exist.') + module_name = os.path.basename(path) if sys.version_info.major == 3 and sys.version_info.minor < 5: @@ -47,13 +63,11 @@ def import_from_file_path(path): fullname=module_name, path=path, ) - spec = importlib.util.spec_from_loader(loader.name, loader, origin=path) - module = importlib.util.module_from_spec(spec) - sys.modules[loader.name] = module - loader.exec_module(module) + + module = loader.load_module(module_name) # pylint: disable=deprecated-method elif sys.version_info.major == 3: - from importlib import util # pylint: disable=g-import-not-at-top + from importlib import util # pylint: disable=g-import-not-at-top,import-outside-toplevel spec = util.spec_from_file_location(module_name, path) if spec is None: @@ -63,19 +77,35 @@ def import_from_file_path(path): spec.loader.exec_module(module) # pytype: disable=attribute-error else: - import imp # pylint: disable=g-import-not-at-top + import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel module = imp.load_source(module_name, path) return module, module_name def import_from_module_name(module_name): + """Imports a module and returns it and its name.""" module = importlib.import_module(module_name) return module, module_name def import_module(module_or_filename): - """Imports a given module or filename.""" + """Imports a given module or filename. + + If the module_or_filename exists in the file system and ends with .py, we + attempt to import it. If that import fails, try to import it as a module. + + Args: + module_or_filename (str): string name of path or module. + + Raises: + ValueError: if the given file is invalid. + IOError: if the file or module can not be found or imported. + + Returns: + Tuple[ModuleType, str]: returns the imported module and the module name, + usually extracted from the path itself. + """ if os.path.exists(module_or_filename): # importlib.util.spec_from_file_location requires .py @@ -98,7 +128,7 @@ def main(args): if len(args) < 2: print(cli_string) - exit(1) + sys.exit(1) module_or_filename = args[1] module, module_name = import_module(module_or_filename) diff --git a/fire/testutils.py b/fire/testutils.py index 831ce78a..b66b51e4 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Lint as: python2, python3 """Utilities for Python Fire's tests.""" from __future__ import absolute_import @@ -72,6 +73,12 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): raise AssertionError('%s: Expected %r to match %r' % (name, value, regexp)) + def assertRaisesRegex(self, *args, **kwargs): + if sys.version_info.major == 2: + return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method + else: + return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) + @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): """Asserts that a FireExit error is raised in the context. @@ -100,6 +107,7 @@ def assertRaisesFireExit(self, code, regexp='.*'): @contextlib.contextmanager def ChangeDirectory(directory): + """Context manager to mock a directory change and revert on exit.""" cwdir = os.getcwd() os.chdir(directory) From 5aa754a1e15f095294d2aad17ff78a1cad7bfa8b Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Fri, 30 Oct 2020 07:12:06 -0700 Subject: [PATCH 088/205] Fixed linting issues with import compatibility across multiple Python versions. PiperOrigin-RevId: 339865504 Change-Id: I9ee3cf4e6324c67c34665b20116109e17acf9e93 --- fire/__main__.py | 10 +++++++--- fire/testutils.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 3a26ade1..30d556e4 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -19,6 +19,10 @@ This allows using Fire with third-party libraries without modifying their code. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + import importlib import os import sys @@ -59,7 +63,7 @@ def import_from_file_path(path): module_name = os.path.basename(path) if sys.version_info.major == 3 and sys.version_info.minor < 5: - loader = importlib.machinery.SourceFileLoader( + loader = importlib.machinery.SourceFileLoader( # pylint: disable=no-member fullname=module_name, path=path, ) @@ -67,13 +71,13 @@ def import_from_file_path(path): module = loader.load_module(module_name) # pylint: disable=deprecated-method elif sys.version_info.major == 3: - from importlib import util # pylint: disable=g-import-not-at-top,import-outside-toplevel + from importlib import util # pylint: disable=g-import-not-at-top,import-outside-toplevel,no-name-in-module spec = util.spec_from_file_location(module_name, path) if spec is None: raise IOError('Unable to load module from specified path.') - module = util.module_from_spec(spec) + module = util.module_from_spec(spec) # pylint: disable=no-member spec.loader.exec_module(module) # pytype: disable=attribute-error else: diff --git a/fire/testutils.py b/fire/testutils.py index b66b51e4..dfcb2d74 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -77,7 +77,7 @@ def assertRaisesRegex(self, *args, **kwargs): if sys.version_info.major == 2: return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method else: - return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) + return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member,arguments-differ @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): From fea0be79ccfa130853aca0dd2b298ae817289e09 Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Fri, 30 Oct 2020 10:14:16 -0700 Subject: [PATCH 089/205] CL fixes for pytype and pylint. PiperOrigin-RevId: 339893046 Change-Id: I9df74fefee03eb801262bccdf1f92999453bfb85 --- fire/main_test.py | 2 +- fire/testutils.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fire/main_test.py b/fire/main_test.py index 75707dc4..20eaa26f 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self): def testFileNameModuleFileFailure(self): # Confirm that an invalid file that masks a non-existent module fails. with self.assertRaisesRegex(ValueError, - r'Fire can only be called on \.py files\.'): + r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long # pytype: disable=attribute-error dirname = os.path.dirname(self.file.name) with testutils.ChangeDirectory(dirname): with open('foobar', 'w'): diff --git a/fire/testutils.py b/fire/testutils.py index dfcb2d74..31a2badb 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -73,11 +73,11 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): raise AssertionError('%s: Expected %r to match %r' % (name, value, regexp)) - def assertRaisesRegex(self, *args, **kwargs): + def assertRaisesRegex(self, *args, **kwargs): # pylint: disable=arguments-differ if sys.version_info.major == 2: return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method else: - return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member,arguments-differ + return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): From 3be260e65a0c25d1dbbe1b15eeb0bf13ac7ec38f Mon Sep 17 00:00:00 2001 From: Jacob Austin Date: Fri, 30 Oct 2020 10:52:24 -0700 Subject: [PATCH 090/205] Fixed line-too-long linting issue in Python 2.7. PiperOrigin-RevId: 339900581 Change-Id: Ibedaed45ffca68f8b12344f8905b4ff379c6e589 --- fire/main_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/main_test.py b/fire/main_test.py index 20eaa26f..41699ac6 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self): def testFileNameModuleFileFailure(self): # Confirm that an invalid file that masks a non-existent module fails. with self.assertRaisesRegex(ValueError, - r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long # pytype: disable=attribute-error + r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, # pytype: disable=attribute-error dirname = os.path.dirname(self.file.name) with testutils.ChangeDirectory(dirname): with open('foobar', 'w'): From e4cb72a1350012cf2651f4a233ae2d0ceacb7265 Mon Sep 17 00:00:00 2001 From: Robert Weiss Date: Wed, 16 Dec 2020 12:19:42 -0800 Subject: [PATCH 091/205] Support asyncio coroutines COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/127 from robweiss:run_coroutines 56bea93ea0340bea962a111bbd20122c1ad4beff PiperOrigin-RevId: 347872801 Change-Id: I17407ac8c3cb7b29026bd7d78729747a3a9875cf --- fire/core.py | 11 ++++++++++- fire/fire_test.py | 5 +++++ fire/inspectutils.py | 12 +++++++++++- fire/test_components_py3.py | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/fire/core.py b/fire/core.py index 763b3d13..43f13e64 100644 --- a/fire/core.py +++ b/fire/core.py @@ -74,6 +74,9 @@ def main(argv): from fire.console import console_io import six +if six.PY34: + import asyncio # pylint: disable=g-import-not-at-top,import-error # pytype: disable=import-error + def Fire(component=None, command=None, name=None): """This function, Fire, is the main entrypoint for Python Fire. @@ -669,7 +672,13 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class', fn = component.__call__ if treatment == 'callable' else component parse = _MakeParseFn(fn, metadata) (varargs, kwargs), consumed_args, remaining_args, capacity = parse(args) - component = fn(*varargs, **kwargs) + + # Call the function. + if inspectutils.IsCoroutineFunction(fn): + loop = asyncio.get_event_loop() + component = loop.run_until_complete(fn(*varargs, **kwargs)) + else: + component = fn(*varargs, **kwargs) if treatment == 'class': action = trace.INSTANTIATED_CLASS diff --git a/fire/fire_test.py b/fire/fire_test.py index 4f2ffb9b..8b904c29 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -721,6 +721,11 @@ def testHelpKwargsDecorator(self): with self.assertRaisesFireExit(0): fire.Fire(tc.decorated_method, command=['--help']) + @testutils.skipIf(six.PY2, 'Asyncio not available in Python 2.') + def testFireAsyncio(self): + self.assertEqual(fire.Fire(tc.py3.WithAsyncio, + command=['double', '--count', '10']), 20) + if __name__ == '__main__': testutils.main() diff --git a/fire/inspectutils.py b/fire/inspectutils.py index c7559ae6..80cc43f2 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -26,6 +26,9 @@ import six +if six.PY34: + import asyncio # pylint: disable=g-import-not-at-top,import-error # pytype: disable=import-error + class FullArgSpec(object): """The arguments of a function, as in Python 3's inspect.FullArgSpec.""" @@ -250,7 +253,7 @@ def GetFileAndLine(component): try: unused_code, lineindex = inspect.findsource(component) lineno = lineindex + 1 - except IOError: + except (IOError, IndexError): lineno = None return filename, lineno @@ -360,3 +363,10 @@ def GetClassAttrsDict(component): class_attr.name: class_attr for class_attr in class_attrs_list } + + +def IsCoroutineFunction(fn): + try: + return six.PY34 and asyncio.iscoroutinefunction(fn) + except: # pylint: disable=bare-except + return False diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 56f64356..3c21f4ba 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -15,6 +15,7 @@ # Lint as: python3 """This module has components that use Python 3 specific syntax.""" +import asyncio import functools from typing import Tuple @@ -49,6 +50,13 @@ def lru_cache_decorated(arg1): return arg1 +class WithAsyncio(object): + + @asyncio.coroutine + def double(self, count=0): + return 2 * count + + class WithTypes(object): """Class with functions that have default arguments and types.""" From aa18534de3e1f44ed3dd525162568605a13e839a Mon Sep 17 00:00:00 2001 From: Amy Lei <42757189+amy-lei@users.noreply.github.com> Date: Wed, 16 Dec 2020 12:26:35 -0800 Subject: [PATCH 092/205] Add :key type name: parameters to help text a22cb7dd08c49357cdc5bf62d59d6c650fb9548b by Amy 26aba559cef2ad07ea0516e92db029ee04956a16 by Amy 9bcccf992b15d1eebb05a3e2267bc6228269dd90 by Amy COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/275 from amy-lei:key-param 9bcccf992b15d1eebb05a3e2267bc6228269dd90 PiperOrigin-RevId: 347874060 Change-Id: Id7d59472a73bf1fa33d37334e7e72702a07c72f6 --- fire/docstrings.py | 32 ++++++++++++++++++++++++-------- fire/docstrings_test.py | 32 +++++++++++++++++++++++++++++--- fire/helptext.py | 41 +++++++++++++++++++++++++++++++++-------- fire/helptext_test.py | 28 ++++++++++++++++++++++++++++ fire/test_components.py | 24 ++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 19 deletions(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index e173d192..de18ed9b 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -53,13 +53,11 @@ from __future__ import division from __future__ import print_function - import collections +import enum import re import textwrap -import enum - class DocstringInfo( collections.namedtuple( @@ -77,6 +75,11 @@ class ArgInfo( ArgInfo.__new__.__defaults__ = (None,) * len(ArgInfo._fields) +class KwargInfo(ArgInfo): + pass +KwargInfo.__new__.__defaults__ = (None,) * len(KwargInfo._fields) + + class Namespace(dict): """A dict with attribute (dot-notation) access enabled.""" @@ -108,7 +111,7 @@ class Formats(enum.Enum): SECTION_TITLES = { - Sections.ARGS: ('argument', 'arg', 'parameter', 'param'), + Sections.ARGS: ('argument', 'arg', 'parameter', 'param', 'key'), Sections.RETURNS: ('return',), Sections.YIELDS: ('yield',), Sections.RAISES: ('raise', 'except', 'exception', 'throw', 'error', 'warn'), @@ -169,6 +172,7 @@ def parse(docstring): state.summary.lines = [] state.description.lines = [] state.args = [] + state.kwargs = [] state.current_arg = None state.returns.lines = [] state.yields.lines = [] @@ -194,6 +198,10 @@ def parse(docstring): name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)), description=_join_lines(arg.description.lines)) for arg in state.args] + args.extend([KwargInfo( + name=arg.name, type=_cast_to_known_type(_join_lines(arg.type.lines)), + description=_join_lines(arg.description.lines)) for arg in state.kwargs]) + return DocstringInfo( summary=summary, description=description, @@ -267,7 +275,7 @@ def _join_lines(lines): return '\n\n'.join(group_texts) -def _get_or_create_arg_by_name(state, name): +def _get_or_create_arg_by_name(state, name, is_kwarg=False): """Gets or creates a new Arg. These Arg objects (Namespaces) are turned into the ArgInfo namedtuples @@ -277,17 +285,21 @@ def _get_or_create_arg_by_name(state, name): Args: state: The state of the parser. name: The name of the arg to create. + is_kwarg: A boolean representing whether the argument is a keyword arg. Returns: The new Arg. """ - for arg in state.args: + for arg in state.args + state.kwargs: if arg.name == name: return arg arg = Namespace() # TODO(dbieber): Switch to an explicit class. arg.name = name arg.type.lines = [] arg.description.lines = [] - state.args.append(arg) + if is_kwarg: + state.kwargs.append(arg) + else: + state.args.append(arg) return arg @@ -429,7 +441,11 @@ def _consume_line(line_info, state): directive_tokens = directive.split() # pytype: disable=attribute-error if state.section.title == Sections.ARGS: name = directive_tokens[-1] - arg = _get_or_create_arg_by_name(state, name) + arg = _get_or_create_arg_by_name( + state, + name, + is_kwarg=directive_tokens[0] == 'key' + ) if len(directive_tokens) == 3: # A param directive of the form ":param type arg:". arg.type.lines.append(directive_tokens[1]) diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 8b1d7685..63aa5e78 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -21,9 +21,11 @@ from fire import docstrings from fire import testutils - -DocstringInfo = docstrings.DocstringInfo # pylint: disable=invalid-name -ArgInfo = docstrings.ArgInfo # pylint: disable=invalid-name +# pylint: disable=invalid-name +DocstringInfo = docstrings.DocstringInfo +ArgInfo = docstrings.ArgInfo +KwargInfo = docstrings.KwargInfo +# pylint: enable=invalid-name class DocstringsTest(testutils.BaseTestCase): @@ -279,6 +281,30 @@ def test_numpy_colon_in_description(self): ) self.assertEqual(expected_docstring_info, docstring_info) + def test_rst_format_typed_args_and_kwargs(self): + docstring = """Docstring summary. + + :param arg1: Description of arg1. + :type arg1: str. + :key arg2: Description of arg2. + :type arg2: bool. + :key arg3: Description of arg3. + :type arg3: str. + """ + docstring_info = docstrings.parse(docstring) + expected_docstring_info = DocstringInfo( + summary='Docstring summary.', + args=[ + ArgInfo(name='arg1', type='str', + description='Description of arg1.'), + KwargInfo(name='arg2', type='bool', + description='Description of arg2.'), + KwargInfo(name='arg3', type='str', + description='Description of arg3.'), + ], + ) + self.assertEqual(expected_docstring_info, docstring_info) + if __name__ == '__main__': testutils.main() diff --git a/fire/helptext.py b/fire/helptext.py index ecb6bbb4..b1d10b44 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -39,6 +39,7 @@ from fire import completion from fire import custom_descriptions from fire import decorators +from fire import docstrings from fire import formatting from fire import inspectutils from fire import value_types @@ -219,10 +220,30 @@ def _ArgsAndFlagsSections(info, spec, metadata): flag_items = positional_flag_items + kwonly_flag_items if spec.varkw: + # Include kwargs documented via :key param: + flag_string = '--{name}' + documented_kwargs = [] + for flag in docstring_info.args or []: + if isinstance(flag, docstrings.KwargInfo): + flag_item = _CreateFlagItem( + flag.name, docstring_info, spec, + flag_string=flag_string.format(name=flag.name)) + documented_kwargs.append(flag_item) + if documented_kwargs: + # Separate documented kwargs from other flags using a message + if flag_items: + message = 'The following flags are also accepted.' + item = _CreateItem(message, None, indent=4) + flag_items.append(item) + flag_items.extend(documented_kwargs) + description = _GetArgDescription(spec.varkw, docstring_info) - message = ('Additional flags are accepted.' - if flag_items else - 'Flags are accepted.') + if documented_kwargs: + message = 'Additional undocumented flags may also be accepted.' + elif flag_items: + message = 'Additional flags are accepted.' + else: + message = 'Flags are accepted.' item = _CreateItem(message, description, indent=4) flag_items.append(item) @@ -400,7 +421,8 @@ def _CreateArgItem(arg, docstring_info, spec): return _CreateItem(arg_string, description, indent=SUBSECTION_INDENTATION) -def _CreateFlagItem(flag, docstring_info, spec, required=False): +def _CreateFlagItem(flag, docstring_info, spec, required=False, + flag_string=None): """Returns a string describing a flag using docstring and FullArgSpec info. Args: @@ -410,6 +432,8 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False): spec: An instance of fire.inspectutils.FullArgSpec, containing type and default information about the arguments to a callable. required: Whether the flag is required. + flag_string: If provided, use this string for the flag, rather than + constructing one from the flag name. Returns: A string to be used in constructing the help screen for the function. """ @@ -424,10 +448,11 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False): description = _GetArgDescription(flag, docstring_info) - flag_string_template = '--{flag_name}={flag_name_upper}' - flag_string = flag_string_template.format( - flag_name=flag, - flag_name_upper=formatting.Underline(flag.upper())) + if not flag_string: + flag_string_template = '--{flag_name}={flag_name_upper}' + flag_string = flag_string_template.format( + flag_name=flag, + flag_name_upper=formatting.Underline(flag.upper())) if required: flag_string += ' (required)' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 1ec16295..3cb40fbe 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -99,6 +99,34 @@ def testHelpTextFunctionWithLongDefaults(self): help_screen) self.assertNotIn('NOTES', help_screen) + def testHelpTextFunctionWithKwargs(self): + component = tc.fn_with_kwarg + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='text')) + self.assertIn('NAME\n text', help_screen) + self.assertIn('SYNOPSIS\n text ARG1 ARG2 ', help_screen) + self.assertIn('DESCRIPTION\n Function with kwarg', help_screen) + self.assertIn( + 'FLAGS\n --arg3\n Description of arg3.\n ' + 'Additional undocumented flags may also be accepted.', + help_screen) + + def testHelpTextFunctionWithKwargsAndDefaults(self): + component = tc.fn_with_kwarg_and_defaults + help_screen = helptext.HelpText( + component=component, + trace=trace.FireTrace(component, name='text')) + self.assertIn('NAME\n text', help_screen) + self.assertIn('SYNOPSIS\n text ARG1 ARG2 ', help_screen) + self.assertIn('DESCRIPTION\n Function with kwarg', help_screen) + self.assertIn( + 'FLAGS\n --opt=OPT\n Default: True\n' + ' The following flags are also accepted.' + '\n --arg3\n Description of arg3.\n ' + 'Additional undocumented flags may also be accepted.', + help_screen) + @testutils.skipIf( sys.version_info[0:2] < (3, 5), 'Python < 3.5 does not support type hints.') diff --git a/fire/test_components.py b/fire/test_components.py index 824e73fc..eee9a07c 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -539,3 +539,27 @@ def wrapper(*args, **kwargs): @simple_decorator def decorated_method(name='World'): return 'Hello %s' % name + + +# pylint: disable=g-doc-args,g-doc-return-or-yield +def fn_with_kwarg(arg1, arg2, **kwargs): + """Function with kwarg. + + :param arg1: Description of arg1. + :param arg2: Description of arg2. + :key arg3: Description of arg3. + """ + del arg1, arg2 + return kwargs.get('arg3') + + +def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs): + """Function with kwarg and defaults. + + :param arg1: Description of arg1. + :param arg2: Description of arg2. + :key arg3: Description of arg3. + """ + del arg1, arg2, opt + return kwargs.get('arg3') +# pylint: enable=g-doc-args,g-doc-return-or-yield From a938ef905674bb8e6c6eeb62f61425e0765cd70f Mon Sep 17 00:00:00 2001 From: Rebecca Chen Date: Wed, 16 Dec 2020 12:42:03 -0800 Subject: [PATCH 093/205] Don't run pytype in Python 3.5. Copybara import of the project: -- eeef066e30e7fbdee0420c5624d8fd4c3d59d5ea by Rebecca Chen : Don't run pytype in Python 3.5. The next release of pytype drops support for running under Python 3.5: https://github.com/google/pytype/issues/677. COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/296 from rchen152:drop35 eeef066e30e7fbdee0420c5624d8fd4c3d59d5ea PiperOrigin-RevId: 347877059 Change-Id: I8b2e8acba3d3ce3ebc47612d7f136d7a643650d3 --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30f0cad6..c6529bce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,8 @@ script: - pip install ipython - python -m pytest # Now run the tests with IPython. - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + - if [[ $TRAVIS_PYTHON_VERSION != 3.4 && \ + $TRAVIS_PYTHON_VERSION != 3.5 ]]; then pip install pytype; fi # Run type-checking, excluding files that define or use py3 features in py2. @@ -27,6 +28,7 @@ script: fire/fire_test.py fire/inspectutils_test.py fire/test_components_py3.py; - elif [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then + elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \ + $TRAVIS_PYTHON_VERSION != 3.5 ]]; then pytype; fi From bc43dca6a52ef6914f0e5d3ec76b316d8fdd648b Mon Sep 17 00:00:00 2001 From: Roopesh V S Date: Wed, 16 Dec 2020 13:58:14 -0800 Subject: [PATCH 094/205] Support functions even when they override getattr in non-standard ways. This resolves an issue with Beautiful Soup. Copybara import of the project: -- f4cd8e946a9b2a874deb1024c6b2e62358f6e537 by Roopesh V S : COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/281 from roopeshvs:master f4cd8e946a9b2a874deb1024c6b2e62358f6e537 PiperOrigin-RevId: 347892296 Change-Id: Ifbbfc9397ce9623cd1a354599252826ac7ddcc92 --- fire/core.py | 2 +- fire/decorators.py | 20 ++++++++++++++++++-- fire/inspectutils.py | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/fire/core.py b/fire/core.py index 43f13e64..8ca142c7 100644 --- a/fire/core.py +++ b/fire/core.py @@ -75,7 +75,7 @@ def main(argv): import six if six.PY34: - import asyncio # pylint: disable=g-import-not-at-top,import-error # pytype: disable=import-error + import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error def Fire(component=None, command=None, name=None): diff --git a/fire/decorators.py b/fire/decorators.py index b7b3c660..9e56d6df 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -71,7 +71,7 @@ def SetParseFns(*positional, **named): def _Decorator(fn): parse_fns = GetParseFns(fn) parse_fns['positional'] = positional - parse_fns['named'].update(named) + parse_fns['named'].update(named) # pytype: disable=attribute-error _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns) return fn @@ -85,15 +85,31 @@ def _SetMetadata(fn, attribute, value): def GetMetadata(fn): + # type: (...) -> dict + """Gets metadata attached to the function `fn` as an attribute. + + Args: + fn: The function from which to retrieve the function metadata. + Returns: + A dictionary mapping property strings to their value. + """ # Class __init__ functions and object __call__ functions require flag style # arguments. Other methods and functions may accept positional args. default = { ACCEPTS_POSITIONAL_ARGS: inspect.isroutine(fn), } - return getattr(fn, FIRE_METADATA, default) + try: + metadata = getattr(fn, FIRE_METADATA, default) + if ACCEPTS_POSITIONAL_ARGS in metadata: + return metadata + else: + return default + except: # pylint: disable=bare-except + return default def GetParseFns(fn): + # type: (...) -> dict metadata = GetMetadata(fn) default = dict(default=None, positional=[], named={}) return metadata.get(FIRE_PARSE_FNS, default) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 80cc43f2..0fa8e7d3 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -27,7 +27,7 @@ import six if six.PY34: - import asyncio # pylint: disable=g-import-not-at-top,import-error # pytype: disable=import-error + import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error class FullArgSpec(object): From 629d91c3f3656049f61b44b734044e51a7895619 Mon Sep 17 00:00:00 2001 From: MichaelCG8 <42502192+MichaelCG8@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:54:46 -0800 Subject: [PATCH 095/205] Added detail to CONTRIBUTING.md Copybara import of the project: -- 7a2eeb93bda700749a0edf45cffdf87b22b88e64 by Michael Garbutt : COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/253 from MichaelCG8:issue-252-adding-detail-to-CONTRIBUTING.md 7a2eeb93bda700749a0edf45cffdf87b22b88e64 PiperOrigin-RevId: 348052636 Change-Id: I75d88223a0fbb7aada074f97b4491272ee2f295e --- CONTRIBUTING.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0786fdf4..08c2aef1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,14 @@ We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. +First, read these guidelines. +Before you begin making changes, state your intent to do so in an Issue. +Then, fork the project. Make changes in your copy of the repository. +Then open a pull request once your changes are ready. +If this is your first contribution, sign the Contributor License Agreement. +A discussion about your change will follow, and if accepted your contribution +will be incorporated into the Python Fire codebase. + ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License @@ -17,8 +25,38 @@ again. ## Code reviews -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult [GitHub Help] for more -information on using pull requests. +All submissions, including submissions by project members, require review. +For changes introduced by non-Googlers, we use GitHub pull requests for this +purpose. Consult [GitHub Help] for more information on using pull requests. [GitHub Help]: https://help.github.com/articles/about-pull-requests/ + +## Code style + +In general, Python Fire follows the guidelines in the +[Google Python Style Guide]. + +In addition, the project follows a convention of: +- Maximum line length: 80 characters +- Indentation: 2 spaces (4 for line continuation) +- PascalCase for function and method names. +- No type hints, as described in [PEP 484], to maintain compatibility with +Python versions < 3.5. +- Single quotes around strings, three double quotes around docstrings. + +[Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html +[PEP 484]: https://www.python.org/dev/peps/pep-0484 + +## Testing + +Python Fire uses Travis CI to run tests on each pull request. You can run +these tests yourself as well. To do this, first install the test dependencies +listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis). +Then run the tests by running `pytest` in the root directory of the repository. + +## Linting + +Please run lint on your pull requests to make accepting the requests easier. +To do this, run `pylint fire` in the root directory of the repository. +Note that even if lint is passing, additional style changes to your submission +may be made during merging. From c39de6ae333fdaf7d3371ab7cb213cb28703120d Mon Sep 17 00:00:00 2001 From: MichaelCG8 <42502192+MichaelCG8@users.noreply.github.com> Date: Fri, 22 Jan 2021 08:40:05 -0800 Subject: [PATCH 096/205] issue-261 Fixed missing parts of argument descriptions for Google and Numpy style docstrings. 482bb9da4bc09522984d982716a79caeafabf3e2 by Michael Garbutt : COPYBARA_INTEGRATE_REVIEW=https://github.com/google/python-fire/pull/262 from MichaelCG8:issue-261-bugfix-multi-line-docstring-parameter-descriptions 482bb9da4bc09522984d982716a79caeafabf3e2 PiperOrigin-RevId: 353248737 Change-Id: I3033200c69c5aba0ce9bb819574f37248e32c1cb --- fire/docstrings.py | 14 ++++++----- fire/docstrings_test.py | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/fire/docstrings.py b/fire/docstrings.py index de18ed9b..1cfadea9 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -307,9 +307,8 @@ def _is_arg_name(name): """Returns whether name is a valid arg name. This is used to prevent multiple words (plaintext) from being misinterpreted - as an argument name. So if ":" appears in the middle of a line in a docstring, - we don't accidentally interpret the first half of that line as a single arg - name. + as an argument name. Any line that doesn't match the pattern for a valid + argument is treated as not being an argument. Args: name: The name of the potential arg. @@ -317,9 +316,11 @@ def _is_arg_name(name): True if name looks like an arg name, False otherwise. """ name = name.strip() - return (name - and ' ' not in name - and ':' not in name) + # arg_pattern is a letter or underscore followed by + # zero or more letters, numbers, or underscores. + arg_pattern = r'^[a-zA-Z_]\w*$' + re.match(arg_pattern, name) + return re.match(arg_pattern, name) is not None def _as_arg_name_and_type(text): @@ -402,6 +403,7 @@ def _consume_google_args_line(line_info, state): arg = _get_or_create_arg_by_name(state, arg_name) arg.type.lines.append(type_str) arg.description.lines.append(second.strip()) + state.current_arg = arg else: if state.current_arg: state.current_arg.description.lines.append(split_line[0]) diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 63aa5e78..2328ef16 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -144,6 +144,32 @@ def test_google_format_typed_args_and_returns(self): ) self.assertEqual(expected_docstring_info, docstring_info) + def test_google_format_multiline_arg_description(self): + docstring = """Docstring summary. + + This is a longer description of the docstring. It spans multiple lines, as + is allowed. + + Args: + param1 (int): The first parameter. + param2 (str): The second parameter. This has a lot of text, enough to + cover two lines. + """ + docstring_info = docstrings.parse(docstring) + expected_docstring_info = DocstringInfo( + summary='Docstring summary.', + description='This is a longer description of the docstring. It spans ' + 'multiple lines, as\nis allowed.', + args=[ + ArgInfo(name='param1', type='int', + description='The first parameter.'), + ArgInfo(name='param2', type='str', + description='The second parameter. This has a lot of text, ' + 'enough to cover two lines.'), + ], + ) + self.assertEqual(expected_docstring_info, docstring_info) + def test_rst_format_typed_args_and_returns(self): docstring = """Docstring summary. @@ -207,6 +233,35 @@ def test_numpy_format_typed_args_and_returns(self): ) self.assertEqual(expected_docstring_info, docstring_info) + def test_numpy_format_multiline_arg_description(self): + docstring = """Docstring summary. + + This is a longer description of the docstring. It spans across multiple + lines. + + Parameters + ---------- + param1 : int + The first parameter. + param2 : str + The second parameter. This has a lot of text, enough to cover two + lines. + """ + docstring_info = docstrings.parse(docstring) + expected_docstring_info = DocstringInfo( + summary='Docstring summary.', + description='This is a longer description of the docstring. It spans ' + 'across multiple\nlines.', + args=[ + ArgInfo(name='param1', type='int', + description='The first parameter.'), + ArgInfo(name='param2', type='str', + description='The second parameter. This has a lot of text, ' + 'enough to cover two lines.'), + ], + ) + self.assertEqual(expected_docstring_info, docstring_info) + def test_multisection_docstring(self): docstring = """Docstring summary. From 56d28ebbb41e0feb50510aa96c88a78e5b6c9e4e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 08:53:33 -0800 Subject: [PATCH 097/205] Bump Python Fire version number from 0.3.1 to 0.4.0 PiperOrigin-RevId: 353250692 Change-Id: I85231dd561a90f9f7224683b82b7dda0a3632347 --- fire/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fire/__init__.py b/fire/__init__.py index 2bacc8fe..9c34a5af 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.3.1' +__version__ = '0.4.0' diff --git a/setup.py b/setup.py index 572fb3c6..dd1d7e1c 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.3.1' +VERSION = '0.4.0' URL = 'https://github.com/google/python-fire' setup( From f2ab298e436f12d6a3fe36744b7eab6afcd04d80 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 08:57:47 -0800 Subject: [PATCH 098/205] Test Python Fire on Python 3.9 PiperOrigin-RevId: 353251383 Change-Id: Iaa74341995640e59619f922997bbc451003a0f1c --- .travis.yml | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c6529bce..2999ad29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.5" - "3.7" - "3.8" + - "3.9" before_install: - pip install --upgrade setuptools pip diff --git a/setup.py b/setup.py index dd1d7e1c..1cc64a07 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Operating System :: OS Independent', 'Operating System :: POSIX', From b61b68c7086e3af9fbccbdcf7244928b4258221f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 09:16:24 -0800 Subject: [PATCH 099/205] Don't run pytype on Python 3.9, not supported yet. PiperOrigin-RevId: 353254829 Change-Id: I4709add1f381cd3a83f83d713834782c2f4dd3a8 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2999ad29..559eb749 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ script: fire/inspectutils_test.py fire/test_components_py3.py; elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \ - $TRAVIS_PYTHON_VERSION != 3.5 ]]; then + $TRAVIS_PYTHON_VERSION != 3.5 && \ + $TRAVIS_PYTHON_VERSION != 3.9 ]]; then pytype; fi From fac1ea00430757fd3aa3d8e2b5f8226d25afe9b0 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 09:18:27 -0800 Subject: [PATCH 100/205] Documentation updates for 0.4.0. (python -m fire path/to/file.py) PiperOrigin-RevId: 353255172 Change-Id: I3c88d5e308d236e7a14d0a3639fd8752a60e6cd1 --- docs/api.md | 10 +++++++++- docs/guide.md | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index aa918160..66451071 100644 --- a/docs/api.md +++ b/docs/api.md @@ -60,6 +60,14 @@ Help is an exception; the isolated `--` is optional for getting help._ ## Using a Fire CLI without modifying any code +You can use Python Fire on a module without modifying the code of the module. +The syntax for this is: + `python -m fire ` -For example, `python -m fire calendar -h`. +or + +`python -m fire ` + +For example, `python -m fire calendar -h` will treat the built in `calendar` +module as a CLI and provide its help. diff --git a/docs/guide.md b/docs/guide.md index 5f693a40..909ec439 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -115,6 +115,14 @@ $ python -m fire example hello --name=World Hello World! ``` +You can also specify the filepath of example.py rather than its module path, +like so: + +```bash +$ python -m fire example.py hello --name=World +Hello World! +``` + ### Exposing Multiple Commands In the previous example, we exposed a single function to the command line. Now From 4c51b892e26d0b558bb9d67f42f60754b8cb10b9 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 09:39:51 -0800 Subject: [PATCH 101/205] Only run pytype on 2.7 and 3.7 PiperOrigin-RevId: 353259081 Change-Id: I410e5af3f280db6cb2780e2837977c725b21bb0f --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 559eb749..de9d1c59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,8 @@ script: - pip install ipython - python -m pytest # Now run the tests with IPython. - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 && \ - $TRAVIS_PYTHON_VERSION != 3.5 ]]; then + - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || \ + $TRAVIS_PYTHON_VERSION == 3.7 ]]; then pip install pytype; fi # Run type-checking, excluding files that define or use py3 features in py2. @@ -29,8 +29,6 @@ script: fire/fire_test.py fire/inspectutils_test.py fire/test_components_py3.py; - elif [[ $TRAVIS_PYTHON_VERSION != 3.4 && \ - $TRAVIS_PYTHON_VERSION != 3.5 && \ - $TRAVIS_PYTHON_VERSION != 3.9 ]]; then + elif [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then pytype; fi From 2b5902a15857287066108fdd204a790b3cca8887 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 10:01:09 -0800 Subject: [PATCH 102/205] Travis pytype bug fix PiperOrigin-RevId: 353263304 Change-Id: Ic4d40ba1aa60eb1c4433cc36e0fdb22af561efdf --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index de9d1c59..a7193ca1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,7 @@ script: - pip install ipython - python -m pytest # Now run the tests with IPython. - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || \ - $TRAVIS_PYTHON_VERSION == 3.7 ]]; then + - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || $TRAVIS_PYTHON_VERSION == 3.7 ]]; then pip install pytype; fi # Run type-checking, excluding files that define or use py3 features in py2. From 59eb4daeeef9e655610d786302f3d69080bbb15c Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 22 Jan 2021 11:43:57 -0800 Subject: [PATCH 103/205] Migrate Python Fire CI from Travis to Github Actions. PiperOrigin-RevId: 353286136 Change-Id: I030d43b4c911137c2859b1d5dcd91e5831dc6b2d --- .github/scripts/build.sh | 40 ++++++++++++++++++++++++++++++++++++ .github/workflows/build.yaml | 28 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/scripts/build.sh create mode 100644 .github/workflows/build.yaml diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh new file mode 100644 index 00000000..ee768eb0 --- /dev/null +++ b/.github/scripts/build.sh @@ -0,0 +1,40 @@ +# Copyright (C) 2018 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env bash + +PYTHON_VERSION=${PYTHON_VERSION:-2.7} + +pip install --upgrade setuptools pip +pip install --upgrade pylint pytest pytest-pylint pytest-runner +pip install termcolor +pip install hypothesis python-Levenshtein +python setup.py develop +python -m pytest # Run the tests without IPython. +pip install ipython +python -m pytest # Now run the tests with IPython. +pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console +if [[ ${PYTHON_VERSION} == 2.7 || ${PYTHON_VERSION} == 3.7 ]]; then + pip install pytype; +fi +# Run type-checking, excluding files that define or use py3 features in py2. +if [[ ${PYTHON_VERSION} == 2.7 ]]; then + pytype -x \ + fire/fire_test.py \ + fire/inspectutils_test.py \ + fire/test_components_py3.py; +elif [[ ${PYTHON_VERSION} == 3.7 ]]; then + pytype; +fi + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..b87d1f8a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,28 @@ +name: Python Fire + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.4, 3.5, 3.7, 3.8, 3.9] + + steps: + # Checkout the repo. + - name: Checkout Python Fire repository + uses: actions/checkout@v2 + + # Set up Python environment. + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + # Build Python Fire using the build.sh script. + - name: Run build script + shell: bash + run: ./.github/scripts/build.sh + env: + PYTHON_VERSION: ${{ matrix.python-version }} From fbe1bf858f71cd3c38c730e16f865ac55d5b719e Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 22 Jan 2021 12:02:50 -0800 Subject: [PATCH 104/205] chmod +x .github/scripts/build.sh PiperOrigin-RevId: 353290438 Change-Id: Ia5cf4c00333f8e7439edebce3fb2e366eef88462 --- .github/scripts/build.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/build.sh diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh old mode 100644 new mode 100755 From c97ac311eeb478d5e0d723985f73f133891e7f00 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 22 Jan 2021 12:25:46 -0800 Subject: [PATCH 105/205] Trigger copybara Renames yaml to yml. PiperOrigin-RevId: 353295857 Change-Id: Ifdd0b0dca8819257ae51bd1dc62690a05ae7fdcc --- .github/workflows/{build.yaml => build.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{build.yaml => build.yml} (100%) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yml similarity index 100% rename from .github/workflows/build.yaml rename to .github/workflows/build.yml From 58fd850862a10808460b654fb8b57074a324dde6 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 22 Jan 2021 13:43:12 -0800 Subject: [PATCH 106/205] - Installed mock, required by the tests. - Exits the build shell script if any of the commands fails. PiperOrigin-RevId: 353310695 Change-Id: I7e4df5de32ec4b3c8ca03ab6f5cf7a03f8f61856 --- .github/scripts/build.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index ee768eb0..2d5fcd10 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -14,12 +14,16 @@ #!/usr/bin/env bash +# Exit when any command fails. +set -e + PYTHON_VERSION=${PYTHON_VERSION:-2.7} pip install --upgrade setuptools pip pip install --upgrade pylint pytest pytest-pylint pytest-runner pip install termcolor pip install hypothesis python-Levenshtein +pip install mock python setup.py develop python -m pytest # Run the tests without IPython. pip install ipython From 8e9b1d52132cfab5f789435a9d88bdf881bbbe22 Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Mon, 25 Jan 2021 10:21:24 -0800 Subject: [PATCH 107/205] - Removed travis config file. - Updated reference of travis to Github Actions. PiperOrigin-RevId: 353675111 Change-Id: Ib1d357252e47acfa6cf03e82109fca6d6bf407da --- .travis.yml | 33 --------------------------------- CONTRIBUTING.md | 2 +- fire/helptext_test.py | 2 +- 3 files changed, 2 insertions(+), 35 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a7193ca1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: python -python: - - "2.7" - - "3.4" - - "3.5" - - "3.7" - - "3.8" - - "3.9" - -before_install: - - pip install --upgrade setuptools pip - - pip install --upgrade pylint pytest pytest-pylint pytest-runner -install: - - pip install termcolor - - pip install hypothesis python-Levenshtein - - python setup.py develop -script: - - python -m pytest # Run the tests without IPython. - - pip install ipython - - python -m pytest # Now run the tests with IPython. - - pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 || $TRAVIS_PYTHON_VERSION == 3.7 ]]; then - pip install pytype; - fi - # Run type-checking, excluding files that define or use py3 features in py2. - - if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then - pytype -x - fire/fire_test.py - fire/inspectutils_test.py - fire/test_components_py3.py; - elif [[ $TRAVIS_PYTHON_VERSION == 3.7 ]]; then - pytype; - fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08c2aef1..b3c758e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ Python versions < 3.5. ## Testing -Python Fire uses Travis CI to run tests on each pull request. You can run +Python Fire uses [Github Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run these tests yourself as well. To do this, first install the test dependencies listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis). Then run the tests by running `pytest` in the root directory of the repository. diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 3cb40fbe..29250cec 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -191,7 +191,7 @@ def testHelpTextFunctionWithLongTypes(self): self.assertIn('NAME\n long_type', help_screen) self.assertIn('SYNOPSIS\n long_type LONG_OBJ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - # TODO(dbieber): Assert type is displayed correctly. Type displays + # TODO(dbieber): Assert type is displayed correctly. Type displayed # differently in Travis vs in Google. # self.assertIn( # 'POSITIONAL ARGUMENTS\n LONG_OBJ\n' From c1266d0dbb2114514fcf8be62044344b5a51c733 Mon Sep 17 00:00:00 2001 From: Pratyush Raj Date: Mon, 12 Apr 2021 14:50:21 +0000 Subject: [PATCH 108/205] Clarify docs by giving the grouping example some output. PiperOrigin-RevId: 367994276 --- docs/guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide.md b/docs/guide.md index 909ec439..d5da3212 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -320,6 +320,7 @@ class Pipeline(object): def run(self): self.ingestion.run() self.digestion.run() + return 'Pipeline complete' if __name__ == '__main__': fire.Fire(Pipeline) From ed44d8b801fc24e40729abef11b2dcbf6588d361 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 17 Jun 2021 11:02:53 -0700 Subject: [PATCH 109/205] Fix typos in test component docstrings. PiperOrigin-RevId: 379999792 Change-Id: I2594db4540cfe20820951bae479d16c167ba391a --- fire/test_components.py | 2 +- fire/test_components_py3.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fire/test_components.py b/fire/test_components.py index eee9a07c..027a6b19 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -98,7 +98,7 @@ def double(self, count=0): count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 3c21f4ba..b6c78c84 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -67,7 +67,7 @@ def double(self, count: float) -> float: count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count @@ -89,7 +89,7 @@ def double(self, count: float = 0) -> float: count: Input number that you want to double. Returns: - A number that is the double of count.s + A number that is the double of count. """ return 2 * count From 703f8a2d59cfea11c5f816b785ae7b2ced31876c Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Thu, 14 Apr 2022 07:52:52 -0700 Subject: [PATCH 110/205] Remove unused comments related to Python 2 compatibility. PiperOrigin-RevId: 441760209 Change-Id: I32e87cdbd00d99ba2eb015c5709c2acceb38e53d --- fire/__main__.py | 1 - fire/test_components_py3.py | 1 - fire/testutils.py | 1 - 3 files changed, 3 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 30d556e4..c7248e29 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python2, python3 # pylint: disable=invalid-name """Enables use of Python Fire as a "main" function (i.e. "python -m fire"). diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index b6c78c84..9f7590a0 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python3 """This module has components that use Python 3 specific syntax.""" import asyncio diff --git a/fire/testutils.py b/fire/testutils.py index 31a2badb..ea410e82 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Lint as: python2, python3 """Utilities for Python Fire's tests.""" from __future__ import absolute_import From 21ae57c38d00d8ba3594b94944bcd955977939df Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Apr 2022 15:58:07 -0400 Subject: [PATCH 111/205] Remove testing for Python 3.4 (#388) * Removes testing from CI for Python 3.4 * Adds lint disabling for new linter checks that have been added. --- .github/scripts/build.sh | 2 +- .github/workflows/build.yml | 2 +- fire/__main__.py | 2 +- fire/custom_descriptions.py | 4 ++-- fire/main_test.py | 4 ++-- pylintrc | 2 +- setup.py | 1 - 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 2d5fcd10..ed75b37b 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -39,6 +39,6 @@ if [[ ${PYTHON_VERSION} == 2.7 ]]; then fire/inspectutils_test.py \ fire/test_components_py3.py; elif [[ ${PYTHON_VERSION} == 3.7 ]]; then - pytype; + pytype -x fire/test_components_py3.py; fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b87d1f8a..b934b759 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.4, 3.5, 3.7, 3.8, 3.9] + python-version: [2.7, 3.5, 3.7, 3.8, 3.9] steps: # Checkout the repo. diff --git a/fire/__main__.py b/fire/__main__.py index c7248e29..2ad217d6 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -80,7 +80,7 @@ def import_from_file_path(path): spec.loader.exec_module(module) # pytype: disable=attribute-error else: - import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel + import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module module = imp.load_source(module_name, path) return module, module_name diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 191e8b29..865a528e 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -137,7 +137,7 @@ def GetStringTypeDescription(obj, available_space, line_length): def GetSummary(obj, available_space, line_length): obj_type_name = type(obj).__name__ - if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space, line_length) return None @@ -145,7 +145,7 @@ def GetSummary(obj, available_space, line_length): def GetDescription(obj, available_space, line_length): obj_type_name = type(obj).__name__ - if obj_type_name in CUSTOM_DESC_SUM_FN_DICT.keys(): + if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space, line_length) return None diff --git a/fire/main_test.py b/fire/main_test.py index 41699ac6..a0184620 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -44,11 +44,11 @@ class MainModuleFileTest(testutils.BaseTestCase): def setUp(self): super(MainModuleFileTest, self).setUp() - self.file = tempfile.NamedTemporaryFile(suffix='.py') + self.file = tempfile.NamedTemporaryFile(suffix='.py') # pylint: disable=consider-using-with self.file.write(b'class Foo:\n def double(self, n):\n return 2 * n\n') self.file.flush() - self.file2 = tempfile.NamedTemporaryFile() + self.file2 = tempfile.NamedTemporaryFile() # pylint: disable=consider-using-with def testFileNameFire(self): # Confirm that the file is correctly imported and doubles the number. diff --git a/pylintrc b/pylintrc index 1b1c5cc2..fa054fb5 100644 --- a/pylintrc +++ b/pylintrc @@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding [REPORTS] diff --git a/setup.py b/setup.py index 1cc64a07..e1efe1ab 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', From 09f836e8db7147472192b6c5468f0b7bc997cfad Mon Sep 17 00:00:00 2001 From: Xavier Figueroa Date: Sat, 16 Apr 2022 15:20:32 -0500 Subject: [PATCH 112/205] Fix #315 Change the _KeywordOnlyArguments filtering condition (#316) * Change the _KeywordOnlyArguments filtering condition * Add test case for usage output of function with mixed defaults Signed-off-by: Xavier Figueroa --- fire/helptext.py | 2 +- fire/helptext_test.py | 14 ++++++++++++++ fire/test_components.py | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/fire/helptext.py b/fire/helptext.py index b1d10b44..5098b26b 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -700,7 +700,7 @@ def _GetCallableUsageItems(spec, metadata): def _KeywordOnlyArguments(spec, required=True): return (flag for flag in spec.kwonlyargs - if required == (flag in spec.kwonlydefaults)) + if required != (flag in spec.kwonlydefaults)) def _GetCallableAvailabilityLines(spec): diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 29250cec..81600965 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -497,6 +497,20 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) + def testUsageOutputFunctionMixedDefaults(self): + component = tc.MixedDefaults().identity2 + t = trace.FireTrace(component, name='FunctionMixedDefaults') + usage_output = helptext.UsageText(component, trace=t, verbose=False) + expected_output = """ + Usage: FunctionMixedDefaults + optional flags: --beta + required flags: --alpha + + For detailed information on this command, run: + FunctionMixedDefaults --help""" + expected_output = textwrap.dedent(expected_output).lstrip('\n') + self.assertEqual(expected_output, usage_output) + def testUsageOutputCallable(self): # This is both a group and a command. component = tc.CallableWithKeywordArgument() diff --git a/fire/test_components.py b/fire/test_components.py index 027a6b19..f29021a9 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -133,6 +133,9 @@ def sum(self, alpha=0, beta=0): def identity(self, alpha, beta='0'): return alpha, beta + def identity2(self, *, alpha, beta='0'): + return alpha, beta + class SimilarArgNames(object): From 8469e487dcb9392856a513f6f861eaf86da80595 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Apr 2022 16:42:02 -0400 Subject: [PATCH 113/205] Move python 3 only component to test_components_py3.py (#389) * #316 adds a test using Python 3 only features; this CL makes that test only run for Python 3 versions. --- .github/scripts/build.sh | 12 +++--------- fire/helptext_test.py | 5 ++++- fire/test_components.py | 3 --- fire/test_components_py3.py | 6 ++++++ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index ed75b37b..c05ea1c3 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -29,16 +29,10 @@ python -m pytest # Run the tests without IPython. pip install ipython python -m pytest # Now run the tests with IPython. pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console -if [[ ${PYTHON_VERSION} == 2.7 || ${PYTHON_VERSION} == 3.7 ]]; then +if [[ ${PYTHON_VERSION} == 3.7 ]]; then pip install pytype; fi -# Run type-checking, excluding files that define or use py3 features in py2. -if [[ ${PYTHON_VERSION} == 2.7 ]]; then - pytype -x \ - fire/fire_test.py \ - fire/inspectutils_test.py \ - fire/test_components_py3.py; -elif [[ ${PYTHON_VERSION} == 3.7 ]]; then +# Run type-checking. +if [[ ${PYTHON_VERSION} == 3.7 ]]; then pytype -x fire/test_components_py3.py; fi - diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 81600965..03ade4a5 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -497,8 +497,11 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) + @testutils.skipIf( + six.PY2, + 'Python 2 does not support required name-only arguments.') def testUsageOutputFunctionMixedDefaults(self): - component = tc.MixedDefaults().identity2 + component = tc.py3.HelpTextComponent().identity t = trace.FireTrace(component, name='FunctionMixedDefaults') usage_output = helptext.UsageText(component, trace=t, verbose=False) expected_output = """ diff --git a/fire/test_components.py b/fire/test_components.py index f29021a9..027a6b19 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -133,9 +133,6 @@ def sum(self, alpha=0, beta=0): def identity(self, alpha, beta='0'): return alpha, beta - def identity2(self, *, alpha, beta='0'): - return alpha, beta - class SimilarArgNames(object): diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 9f7590a0..5140921d 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -25,6 +25,12 @@ def identity(arg1, arg2: int, arg3=10, arg4: int = 20, *arg5, return arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 +class HelpTextComponent: + + def identity(self, *, alpha, beta='0'): + return alpha, beta + + class KeywordOnly(object): def double(self, *, count): From 8bddeec6bd9c78b9b5ad20e9a58625cbd3096fe9 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 16 Apr 2022 17:21:05 -0400 Subject: [PATCH 114/205] Add custom formatter for Fire result (#345) Fixes #344 (see issue for more details) This lets you define a function that will take the result from the Fire component and allows the user to alter it before fire looks at it to render it. --- fire/core.py | 13 ++++++++++--- fire/core_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/fire/core.py b/fire/core.py index 8ca142c7..6fd1bf7a 100644 --- a/fire/core.py +++ b/fire/core.py @@ -78,7 +78,7 @@ def main(argv): import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error -def Fire(component=None, command=None, name=None): +def Fire(component=None, command=None, name=None, serialize=None): """This function, Fire, is the main entrypoint for Python Fire. Executes a command either from the `command` argument or from sys.argv by @@ -164,7 +164,7 @@ def Fire(component=None, command=None, name=None): raise FireExit(0, component_trace) # The command succeeded normally; print the result. - _PrintResult(component_trace, verbose=component_trace.verbose) + _PrintResult(component_trace, verbose=component_trace.verbose, serialize=serialize) result = component_trace.GetResult() return result @@ -241,12 +241,19 @@ def _IsHelpShortcut(component_trace, remaining_args): return show_help -def _PrintResult(component_trace, verbose=False): +def _PrintResult(component_trace, verbose=False, serialize=None): """Prints the result of the Fire call to stdout in a human readable way.""" # TODO(dbieber): Design human readable deserializable serialization method # and move serialization to its own module. result = component_trace.GetResult() + # Allow users to modify the return value of the component and provide + # custom formatting. + if serialize: + if not callable(serialize): + raise FireError("serialize argument {} must be empty or callable.".format(serialize)) + result = serialize(result) + if value_types.HasCustomStr(result): # If the object has a custom __str__ method, rather than one inherited from # object, then we use that to serialize the object. diff --git a/fire/core_test.py b/fire/core_test.py index 27c9f418..a0576ee9 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -194,6 +194,30 @@ def testClassMethod(self): 7, ) + def testCustomSerialize(self): + def serialize(x): + if isinstance(x, list): + return ', '.join(str(xi) for xi in x) + if isinstance(x, dict): + return ', '.join('{}={!r}'.format(k, v) for k, v in x.items()) + if x == 'special': + return ['SURPRISE!!', "I'm a list!"] + return x + + ident = lambda x: x + + with self.assertOutputMatches(stdout='a, b', stderr=None): + result = core.Fire(ident, command=['[a,b]'], serialize=serialize) + with self.assertOutputMatches(stdout='a=5, b=6', stderr=None): + result = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize) + with self.assertOutputMatches(stdout='asdf', stderr=None): + result = core.Fire(ident, command=['asdf'], serialize=serialize) + with self.assertOutputMatches(stdout="SURPRISE!!\nI'm a list!\n", stderr=None): + result = core.Fire(ident, command=['special'], serialize=serialize) + with self.assertRaises(core.FireError): + core.Fire(ident, command=['asdf'], serialize=55) + + @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): self.assertEqual( From 37c4305194ff3d0a63f435f08a40e14b1978bd4e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Apr 2022 18:20:08 -0400 Subject: [PATCH 115/205] Lint error cleanup following #345 (#390) * Lint error cleanup following #345 * Makes new serialize= test deterministic --- fire/core.py | 8 +++++--- fire/core_test.py | 15 ++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/fire/core.py b/fire/core.py index 6fd1bf7a..4546b273 100644 --- a/fire/core.py +++ b/fire/core.py @@ -164,7 +164,8 @@ def Fire(component=None, command=None, name=None, serialize=None): raise FireExit(0, component_trace) # The command succeeded normally; print the result. - _PrintResult(component_trace, verbose=component_trace.verbose, serialize=serialize) + _PrintResult( + component_trace, verbose=component_trace.verbose, serialize=serialize) result = component_trace.GetResult() return result @@ -247,11 +248,12 @@ def _PrintResult(component_trace, verbose=False, serialize=None): # and move serialization to its own module. result = component_trace.GetResult() - # Allow users to modify the return value of the component and provide + # Allow users to modify the return value of the component and provide # custom formatting. if serialize: if not callable(serialize): - raise FireError("serialize argument {} must be empty or callable.".format(serialize)) + raise FireError( + 'The argument `serialize` must be empty or callable:', serialize) result = serialize(result) if value_types.HasCustomStr(result): diff --git a/fire/core_test.py b/fire/core_test.py index a0576ee9..0d11467e 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -199,21 +199,22 @@ def serialize(x): if isinstance(x, list): return ', '.join(str(xi) for xi in x) if isinstance(x, dict): - return ', '.join('{}={!r}'.format(k, v) for k, v in x.items()) + return ', '.join('{}={!r}'.format(k, v) for k, v in sorted(x.items())) if x == 'special': return ['SURPRISE!!', "I'm a list!"] return x ident = lambda x: x - + with self.assertOutputMatches(stdout='a, b', stderr=None): - result = core.Fire(ident, command=['[a,b]'], serialize=serialize) + _ = core.Fire(ident, command=['[a,b]'], serialize=serialize) with self.assertOutputMatches(stdout='a=5, b=6', stderr=None): - result = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize) + _ = core.Fire(ident, command=['{a:5,b:6}'], serialize=serialize) with self.assertOutputMatches(stdout='asdf', stderr=None): - result = core.Fire(ident, command=['asdf'], serialize=serialize) - with self.assertOutputMatches(stdout="SURPRISE!!\nI'm a list!\n", stderr=None): - result = core.Fire(ident, command=['special'], serialize=serialize) + _ = core.Fire(ident, command=['asdf'], serialize=serialize) + with self.assertOutputMatches( + stdout="SURPRISE!!\nI'm a list!\n", stderr=None): + _ = core.Fire(ident, command=['special'], serialize=serialize) with self.assertRaises(core.FireError): core.Fire(ident, command=['asdf'], serialize=55) From c367ce99aa9b8e69abb48d3296cac45b33638bb8 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 28 Nov 2022 16:09:56 -0500 Subject: [PATCH 116/205] Bring continuous integration to working state (#415) Contained in this PR: * Use latest versions of checkout and python setup GitHub actions * Fix Ubuntu version since not all Python versions are available on ubuntu-latest * Lint fixes: use setattr directly instead of via dunder-method, and ignore unnecessary lambda lint warnings. * Loosen formatting tests --- .github/workflows/build.yml | 6 +++--- fire/core.py | 3 ++- fire/formatting_test.py | 4 ++-- fire/parser.py | 2 +- pylintrc | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b934b759..bc5e0405 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: [2.7, 3.5, 3.7, 3.8, 3.9] @@ -12,11 +12,11 @@ jobs: steps: # Checkout the repo. - name: Checkout Python Fire repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Set up Python environment. - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/fire/core.py b/fire/core.py index 4546b273..c1e97367 100644 --- a/fire/core.py +++ b/fire/core.py @@ -525,7 +525,8 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # The target isn't present in the dict as a string key, but maybe it is # a key as another type. # TODO(dbieber): Consider alternatives for accessing non-string keys. - for key, value in component_dict.items(): + for key, value in ( + component_dict.items()): # pytype: disable=attribute-error if target == str(key): component = value handled = True diff --git a/fire/formatting_test.py b/fire/formatting_test.py index 61cce0e8..05a88c49 100644 --- a/fire/formatting_test.py +++ b/fire/formatting_test.py @@ -28,11 +28,11 @@ class FormattingTest(testutils.BaseTestCase): def test_bold(self): text = formatting.Bold('hello') - self.assertEqual('\x1b[1mhello\x1b[0m', text) + self.assertIn(text, ['hello', '\x1b[1mhello\x1b[0m']) def test_underline(self): text = formatting.Underline('hello') - self.assertEqual('\x1b[4mhello\x1b[0m', text) + self.assertIn(text, ['hello', '\x1b[4mhello\x1b[0m']) def test_indent(self): text = formatting.Indent('hello', spaces=2) diff --git a/fire/parser.py b/fire/parser.py index 404e18e7..2aff8bd7 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -106,7 +106,7 @@ def _LiteralEval(value): elif isinstance(child, ast.Name): replacement = _Replacement(child) - node.__setattr__(field, replacement) + setattr(node, field, replacement) # ast.literal_eval supports the following types: # strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None diff --git a/pylintrc b/pylintrc index fa054fb5..b89b16d1 100644 --- a/pylintrc +++ b/pylintrc @@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment [REPORTS] From c4bd14b45a3a68574cd30ce6699312ebf66ae0b3 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 28 Nov 2022 16:13:34 -0500 Subject: [PATCH 117/205] Python Fire Version Bump (#416) Bumps version to 0.5.0 in preparation for next release --- .github/scripts/build.sh | 4 +--- fire/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index c05ea1c3..6fd8f73b 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -30,9 +30,7 @@ pip install ipython python -m pytest # Now run the tests with IPython. pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console if [[ ${PYTHON_VERSION} == 3.7 ]]; then + # Run type-checking. pip install pytype; -fi -# Run type-checking. -if [[ ${PYTHON_VERSION} == 3.7 ]]; then pytype -x fire/test_components_py3.py; fi diff --git a/fire/__init__.py b/fire/__init__.py index 9c34a5af..4cc76210 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.4.0' +__version__ = '0.5.0' diff --git a/setup.py b/setup.py index e1efe1ab..f1f91103 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.4.0' +VERSION = '0.5.0' URL = 'https://github.com/google/python-fire' setup( From a49184782c37746278e42e8a175dfab4fe89db8a Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 28 Nov 2022 16:46:26 -0500 Subject: [PATCH 118/205] Show default values for kwonly arguments in help text (#414) Fixes #410 * Shows default values for kwonly arguments in help text * Adds test verifying that default values are shown for kwonly args --- fire/helptext.py | 2 ++ fire/helptext_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/fire/helptext.py b/fire/helptext.py index 5098b26b..331b6649 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -524,6 +524,8 @@ def _GetArgDefault(flag, spec): for arg, default in zip(args_with_defaults, spec.defaults): if arg == flag: return repr(default) + if flag in spec.kwonlydefaults: + return repr(spec.kwonlydefaults[flag]) return '' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 03ade4a5..14e0874a 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -296,6 +296,18 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): self.assertIn('NAME\n double', output) self.assertIn('FLAGS\n --count=COUNT (required)', output) + @testutils.skipIf( + six.PY2, + 'Python 2 does not support required name-only arguments.') + def testHelpTextFunctionMixedDefaults(self): + component = tc.py3.HelpTextComponent().identity + t = trace.FireTrace(component, name='FunctionMixedDefaults') + output = helptext.HelpText(component, trace=t) + self.assertIn('NAME\n FunctionMixedDefaults', output) + self.assertIn('FunctionMixedDefaults ', output) + self.assertIn('--alpha=ALPHA (required)', output) + self.assertIn('--beta=BETA\n Default: \'0\'', output) + def testHelpScreen(self): component = tc.ClassWithDocstring() t = trace.FireTrace(component, name='ClassWithDocstring') From 82e2d87f0e2a01e9c01e958dd2d2ec510a77fc6d Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 28 Nov 2022 16:49:17 -0500 Subject: [PATCH 119/205] Remove extra newline (#417) Remove extra newline in core_test.py #417 --- fire/core_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fire/core_test.py b/fire/core_test.py index 0d11467e..75b76998 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -218,7 +218,6 @@ def serialize(x): with self.assertRaises(core.FireError): core.Fire(ident, command=['asdf'], serialize=55) - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): self.assertEqual( From e5b33f6a15b3a2ded83f924079b9c2645983bd05 Mon Sep 17 00:00:00 2001 From: kpakda <5531544+khodadadp@users.noreply.github.com> Date: Fri, 9 Dec 2022 15:16:49 -0500 Subject: [PATCH 120/205] Update guide.md (#387) Should either print the returned value or join and return. Otherwise, the output in the example won't get generated. --- docs/guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index d5da3212..8e3f03c9 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -318,8 +318,8 @@ class Pipeline(object): self.digestion = DigestionStage() def run(self): - self.ingestion.run() - self.digestion.run() + print(self.ingestion.run()) + print(self.digestion.run()) return 'Pipeline complete' if __name__ == '__main__': From 1eec95480568e4f67a0ce287b10c977d269a5c49 Mon Sep 17 00:00:00 2001 From: Martin Gruber Date: Fri, 9 Dec 2022 21:18:54 +0100 Subject: [PATCH 121/205] Interactive mode: Mention IPython REPL requirement (#383) Mention that the ipython package is an optional requirement for using the IPython REPL, as described in the [source file](https://github.com/google/python-fire/blob/master/fire/interact.py#L17). --- docs/using-cli.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/using-cli.md b/docs/using-cli.md index 0f369a9a..ba1c35dc 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -137,6 +137,9 @@ will put you in an IPython REPL, with the variable `widget` already defined. You can then explore the Python object that `widget` corresponds to interactively using Python. +Note: if you want fire to start the IPython REPL instead of the regular Python one, +the `ipython` package needs to be installed in your environment. + ### `--completion`: Generating a completion script From c3703f443c7ae5603538eb4fa4354787b9fb7c88 Mon Sep 17 00:00:00 2001 From: Martin Gruber Date: Fri, 9 Dec 2022 21:19:49 +0100 Subject: [PATCH 122/205] Interactive mode: Mention IPython REPL requirement (#383) Mention that the ipython package is an optional requirement for using the IPython REPL, as described in the [source file](https://github.com/google/python-fire/blob/master/fire/interact.py#L17). From 0ad057143b326f78398fbc9a3840d9d6b378e0be Mon Sep 17 00:00:00 2001 From: Conor Sheehan Date: Fri, 9 Dec 2022 20:31:12 +0000 Subject: [PATCH 123/205] Fix #317 Help text short args (#318) * Add short args to help text for all types of kwargs. * Updates tests to reflect new help text. --- fire/helptext.py | 58 +++++++++++++++++++++++++++++++++++------ fire/helptext_test.py | 36 ++++++++++++++++++------- fire/test_components.py | 10 +++++++ 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/fire/helptext.py b/fire/helptext.py index 331b6649..6e7fbb07 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -33,6 +33,7 @@ from __future__ import division from __future__ import print_function +import collections import itertools import sys @@ -172,9 +173,25 @@ def _DescriptionSection(component, info): return None -def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec): +def _CreateKeywordOnlyFlagItem(flag, docstring_info, spec, short_arg): return _CreateFlagItem( - flag, docstring_info, spec, required=flag not in spec.kwonlydefaults) + flag, docstring_info, spec, required=flag not in spec.kwonlydefaults, + short_arg=short_arg) + + +def _GetShortFlags(flags): + """Gets a list of single-character flags that uniquely identify a flag. + + Args: + flags: list of strings representing flags + + Returns: + List of single character short flags, + where the character occurred at the start of a flag once. + """ + short_flags = [f[0] for f in flags] + short_flag_counts = collections.Counter(short_flags) + return [v for v in short_flags if short_flag_counts[v] == 1] def _ArgsAndFlagsSections(info, spec, metadata): @@ -209,25 +226,47 @@ def _ArgsAndFlagsSections(info, spec, metadata): ('NOTES', 'You can also use flags syntax for POSITIONAL ARGUMENTS') ) + unique_short_args = _GetShortFlags(args_with_defaults) positional_flag_items = [ - _CreateFlagItem(flag, docstring_info, spec, required=False) + _CreateFlagItem( + flag, docstring_info, spec, required=False, + short_arg=flag[0] in unique_short_args + ) for flag in args_with_defaults ] + + unique_short_kwonly_flags = _GetShortFlags(spec.kwonlyargs) kwonly_flag_items = [ - _CreateKeywordOnlyFlagItem(flag, docstring_info, spec) + _CreateKeywordOnlyFlagItem( + flag, docstring_info, spec, + short_arg=flag[0] in unique_short_kwonly_flags + ) for flag in spec.kwonlyargs ] flag_items = positional_flag_items + kwonly_flag_items if spec.varkw: # Include kwargs documented via :key param: - flag_string = '--{name}' documented_kwargs = [] - for flag in docstring_info.args or []: + flag_string = '--{name}' + short_flag_string = '-{short_name}, --{name}' + + # add short flags if possible + flags = docstring_info.args or [] + flag_names = [f.name for f in flags] + unique_short_flags = _GetShortFlags(flag_names) + for flag in flags: if isinstance(flag, docstrings.KwargInfo): + if flag.name[0] in unique_short_flags: + flag_string = short_flag_string.format( + name=flag.name, short_name=flag.name[0] + ) + else: + flag_string = flag_string.format(name=flag.name) + flag_item = _CreateFlagItem( flag.name, docstring_info, spec, - flag_string=flag_string.format(name=flag.name)) + flag_string=flag_string) documented_kwargs.append(flag_item) if documented_kwargs: # Separate documented kwargs from other flags using a message @@ -422,7 +461,7 @@ def _CreateArgItem(arg, docstring_info, spec): def _CreateFlagItem(flag, docstring_info, spec, required=False, - flag_string=None): + flag_string=None, short_arg=False): """Returns a string describing a flag using docstring and FullArgSpec info. Args: @@ -434,6 +473,7 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, required: Whether the flag is required. flag_string: If provided, use this string for the flag, rather than constructing one from the flag name. + short_arg: Whether the flag has a short variation or not. Returns: A string to be used in constructing the help screen for the function. """ @@ -455,6 +495,8 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, flag_name_upper=formatting.Underline(flag.upper())) if required: flag_string += ' (required)' + if short_arg: + flag_string = '-{short_flag}, '.format(short_flag=flag[0]) + flag_string arg_type = _GetArgType(flag, spec) arg_default = _GetArgDefault(flag, spec) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 14e0874a..404d9812 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -81,7 +81,9 @@ def testHelpTextFunctionWithDefaults(self): self.assertIn('NAME\n triple', help_screen) self.assertIn('SYNOPSIS\n triple ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) - self.assertIn('FLAGS\n --count=COUNT\n Default: 0', help_screen) + self.assertIn( + 'FLAGS\n -c, --count=COUNT\n Default: 0', + help_screen) self.assertNotIn('NOTES', help_screen) def testHelpTextFunctionWithLongDefaults(self): @@ -93,7 +95,7 @@ def testHelpTextFunctionWithLongDefaults(self): self.assertIn('SYNOPSIS\n text ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --string=STRING\n' + 'FLAGS\n -s, --string=STRING\n' ' Default: \'0001020304050607080910' '1112131415161718192021222324252627282...', help_screen) @@ -121,7 +123,7 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): self.assertIn('SYNOPSIS\n text ARG1 ARG2 ', help_screen) self.assertIn('DESCRIPTION\n Function with kwarg', help_screen) self.assertIn( - 'FLAGS\n --opt=OPT\n Default: True\n' + 'FLAGS\n -o, --opt=OPT\n Default: True\n' ' The following flags are also accepted.' '\n --arg3\n Description of arg3.\n ' 'Additional undocumented flags may also be accepted.', @@ -140,7 +142,7 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): self.assertIn('SYNOPSIS\n double ', help_screen) self.assertIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --count=COUNT\n Type: float\n Default: 0', + 'FLAGS\n -c, --count=COUNT\n Type: float\n Default: 0', help_screen) self.assertNotIn('NOTES', help_screen) @@ -157,7 +159,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): self.assertIn('SYNOPSIS\n get_int ', help_screen) self.assertNotIn('DESCRIPTION', help_screen) self.assertIn( - 'FLAGS\n --value=VALUE\n' + 'FLAGS\n -v, --value=VALUE\n' ' Type: Optional[int]\n Default: None', help_screen) self.assertNotIn('NOTES', help_screen) @@ -285,7 +287,7 @@ def testHelpTextKeywordOnlyArgumentsWithDefault(self): output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'with_default')) self.assertIn('NAME\n with_default', output) - self.assertIn('FLAGS\n --x=X', output) + self.assertIn('FLAGS\n -x, --x=X', output) @testutils.skipIf( six.PY2, 'Python 2 does not support keyword-only arguments.') @@ -294,7 +296,7 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'double')) self.assertIn('NAME\n double', output) - self.assertIn('FLAGS\n --count=COUNT (required)', output) + self.assertIn('FLAGS\n -c, --count=COUNT (required)', output) @testutils.skipIf( six.PY2, @@ -374,7 +376,7 @@ def testHelpScreenForFunctionFunctionWithDefaultArgs(self): Returns the input multiplied by 2. FLAGS - --count=COUNT + -c, --count=COUNT Default: 0 Input number that you want to double.""" self.assertEqual(textwrap.dedent(expected_output).strip(), @@ -389,7 +391,8 @@ def testHelpTextUnderlineFlag(self): formatting.Bold('SYNOPSIS') + '\n triple ', help_screen) self.assertIn( - formatting.Bold('FLAGS') + '\n --' + formatting.Underline('count'), + formatting.Bold('FLAGS') + '\n -c, --' + + formatting.Underline('count'), help_screen) def testHelpTextBoldCommandName(self): @@ -435,6 +438,21 @@ def testHelpTextNameSectionCommandWithSeparatorVerbose(self): self.assertIn('double -', help_screen) self.assertIn('double - -', help_screen) + def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self): + component = tc.fn_with_multiple_defaults + t = trace.FireTrace(component, name='shortargs') + help_screen = helptext.HelpText(component, t) + self.assertIn(formatting.Bold('NAME') + '\n shortargs', help_screen) + self.assertIn( + formatting.Bold('SYNOPSIS') + '\n shortargs ', + help_screen) + self.assertIn( + formatting.Bold('FLAGS') + '\n -f, --first', + help_screen) + self.assertIn('\n --last', help_screen) + self.assertIn('\n --late', help_screen) + + class UsageTest(testutils.BaseTestCase): diff --git a/fire/test_components.py b/fire/test_components.py index 027a6b19..5fcb056e 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -563,3 +563,13 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs): del arg1, arg2, opt return kwargs.get('arg3') # pylint: enable=g-doc-args,g-doc-return-or-yield + +def fn_with_multiple_defaults(first='first', last='last', late='late'): + """Function with kwarg and defaults. + + :key first: Description of first. + :key last: Description of last. + :key late: Description of late. + """ + del last, late + return first From eda25f878900146d929c32d9ac2a87656c6e974a Mon Sep 17 00:00:00 2001 From: Martial Himanshu <66832784+mhimanshu0101@users.noreply.github.com> Date: Sat, 10 Dec 2022 02:15:14 +0530 Subject: [PATCH 124/205] Small docs improvement for using CLI (#372) * Updated the formatting of the text to improve the documentation. --- docs/using-cli.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/using-cli.md b/docs/using-cli.md index ba1c35dc..bdfcb7db 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -57,7 +57,7 @@ If your command corresponds to a list or tuple, you can extend your command by adding the index of an element of the component to your command as an argument. For example, `widget function-that-returns-list 2` will correspond to item 2 of -the result of function_that_returns_list. +the result of `function_that_returns_list`. ### Calling a function @@ -90,7 +90,7 @@ See also the section on [Changing the Separator](#separator-flag). ### Instantiating a class If your command corresponds to a class, you can extend your command by adding -the arguments of the class's \_\_init\_\_ function. Arguments must be specified +the arguments of the class's `__init__` function. Arguments must be specified by name, using the flags syntax. See the section on [calling a function](#calling-a-function) for more details. @@ -105,8 +105,8 @@ after the final standalone `--` argument. (If there is no `--` argument, then no arguments are used for flags.) For example, to set the alsologtostderr flag, you could run the command: -`widget bang --noise=boom -- --alsologtostderr`. The --noise argument is -consumed by Fire, but the --alsologtostderr argument is treated as a normal +`widget bang --noise=boom -- --alsologtostderr`. The `--noise` argument is +consumed by Fire, but the `--alsologtostderr` argument is treated as a normal Flag. All CLIs built with Python Fire share some flags, as described in the next @@ -146,7 +146,7 @@ the `ipython` package needs to be installed in your environment. Call `widget -- --completion` to generate a completion script for the Fire CLI `widget`. To save the completion script to your home directory, you could e.g. run `widget -- --completion > ~/.widget-completion`. You should then source this -file; to get permanent completion, source this file from your .bashrc file. +file; to get permanent completion, source this file from your `.bashrc` file. Call `widget -- --completion fish` to generate a completion script for the Fish shell. Source this file from your fish.config. @@ -177,7 +177,7 @@ corresponds to, as well as usage information for how to extend that command. ### `--trace`: Getting a Fire trace In order to understand what is happening when you call Python Fire, it can be -useful to request a trace. This is done via the --trace flag, e.g. +useful to request a trace. This is done via the `--trace` flag, e.g. `widget whack 5 -- --trace`. A trace provides step by step information about how the Fire command was From 7b5d4f713ba4c252cc34f523811a49a6bb15f681 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 10 Dec 2022 07:53:52 +1100 Subject: [PATCH 125/205] docs: Fix a few typos (#399) There are small typos in: - fire/console/console_attr.py - fire/custom_descriptions.py Fixes: - Should read `support` rather than `suport`. - Should read `pertinent` rather than `pertinant`. - Should read `environment` rather than `envrionment`. --- fire/console/console_attr.py | 4 ++-- fire/custom_descriptions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py index 35c10fba..f88d5788 100644 --- a/fire/console/console_attr.py +++ b/fire/console/console_attr.py @@ -288,7 +288,7 @@ def __init__(self, encoding=None, suppress_output=False): elif self._encoding == 'cp437' and not is_screen_reader: self._box_line_characters = BoxLineCharactersUnicode() self._bullets = self._BULLETS_WINDOWS - # Windows does not suport the unicode characters used for the spinner. + # Windows does not support the unicode characters used for the spinner. self._progress_tracker_symbols = ProgressTrackerSymbolsAscii() else: self._box_line_characters = BoxLineCharactersAscii() @@ -456,7 +456,7 @@ def GetRawKey(self): return self._get_raw_key[0]() def GetTermIdentifier(self): - """Returns the TERM envrionment variable for the console. + """Returns the TERM environment variable for the console. Returns: str: A str that describes the console's text capabilities diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 865a528e..266671f1 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -28,7 +28,7 @@ dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2) -As you can see, this docstring is more pertinant to the function `dict` and +As you can see, this docstring is more pertinent to the function `dict` and would be suitable as the result of `dict.__doc__`, but is wholely unsuitable as a description for the dict `{'key': 'value'}`. From b2415b9331268050fbfe4da7ea377333aa4cf9cd Mon Sep 17 00:00:00 2001 From: Wei-Chung Liao Date: Fri, 9 Dec 2022 16:16:15 -0500 Subject: [PATCH 126/205] Fix completion for Fire(fn) (#336) --- fire/completion.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fire/completion.py b/fire/completion.py index ed7a1b61..9659ec6a 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -156,7 +156,11 @@ def _GetOptsAssignmentTemplate(command): return opts_assignment_subcommand_template lines = [] - for command in set(subcommands_map.keys()).union(set(options_map.keys())): + commands_set = set() + commands_set.add(name) + commands_set = commands_set.union(set(subcommands_map.keys())) + commands_set = commands_set.union(set(options_map.keys())) + for command in commands_set: opts_assignment = _GetOptsAssignmentTemplate(command).format( options=' '.join( sorted(options_map[command].union(subcommands_map[command])) From 2cb16f66c52562dd8f4ad7ae8513d78ae6c0a659 Mon Sep 17 00:00:00 2001 From: Wei-Chung Liao Date: Fri, 9 Dec 2022 16:20:13 -0500 Subject: [PATCH 127/205] Modify the grouping example to fit the output (#334) Co-authored-by: David Bieber --- docs/guide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 8e3f03c9..cb2c07db 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -318,9 +318,9 @@ class Pipeline(object): self.digestion = DigestionStage() def run(self): - print(self.ingestion.run()) - print(self.digestion.run()) - return 'Pipeline complete' + ingestion_output = self.ingestion.run() + digestion_output = self.digestion.run() + return [ingestion_output, digestion_output] if __name__ == '__main__': fire.Fire(Pipeline) From 51afa538b9dc3df50cb15970b19aece54480f851 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 9 Dec 2022 17:11:58 -0500 Subject: [PATCH 128/205] Update documentation to show new serialize kwarg and to clean up misformatted tables (#419) * Shows new serialize kwarg * Cleans up misformatted tables --- docs/api.md | 59 ++++++++++++-------------------------------- docs/guide.md | 13 +++++++++- docs/index.md | 3 ++- docs/installation.md | 4 +-- 4 files changed, 32 insertions(+), 47 deletions(-) diff --git a/docs/api.md b/docs/api.md index 66451071..aae92cd6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,37 +1,23 @@ ## Python Fire Quick Reference | Setup | Command | Notes -| :------ | :------------------ | :--------- -| install | `pip install fire` | +| ------- | ------------------- | ---------- +| install | `pip install fire` | Installs fire from pypi | Creating a CLI | Command | Notes -| :--------------| :--------------------- | :--------- +| ---------------| ---------------------- | ---------- | import | `import fire` | | Call | `fire.Fire()` | Turns the current module into a Fire CLI. | Call | `fire.Fire(component)` | Turns `component` into a Fire CLI. -| Using a CLI | Command | Notes | -| :----------------------------------------- | :------------- | :------------- | -| [Help](using-cli.md#help-flag) | `command | Show the help | -: : --help` : screen. : -| [REPL](using-cli.md#interactive-flag) | `command -- | Enters | -: : --interactive` : interactive : -: : : mode. : -| [Separator](using-cli.md#separator-flag) | `command -- | This sets the | -: : --separator=X` : separator to : -: : : `X`. The : -: : : default : -: : : separator is : -: : : `-`. : -| [Completion](using-cli.md#completion-flag) | `command -- | Generate a | -: : --completion : completion : -: : [shell]` : script for the : -: : : CLI. : -| [Trace](using-cli.md#trace-flag) | `command -- | Gets a Fire | -: : --trace` : trace for the : -: : : command. : -| [Verbose](using-cli.md#verbose-flag) | `command -- | | -: : --verbose` : : +| Using a CLI | Command | Notes | +| ------------------------------------------ | ----------------- | -------------- | +| [Help](using-cli.md#help-flag) | `command --help` | Show the help screen. | +| [REPL](using-cli.md#interactive-flag) | `command -- --interactive` | Enters interactive mode. | +| [Separator](using-cli.md#separator-flag) | `command -- --separator=X` | This sets the separator to `X`. The default separator is `-`. | +| [Completion](using-cli.md#completion-flag) | `command -- --completion [shell]` | Generate a completion script for the CLI. | +| [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. | +| [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | | _Note that flags are separated from the Fire command by an isolated `--` arg. Help is an exception; the isolated `--` is optional for getting help._ @@ -39,24 +25,11 @@ Help is an exception; the isolated `--` is optional for getting help._ ## Arguments for Calling fire.Fire() | Argument | Usage | Notes | -| :-------- | :------------------------ | :----------------------------------- | -| component | `fire.Fire(component)` | If omitted, defaults to a dict of | -: : : all locals and globals. : -| command | `fire.Fire(command='hello | Either a string or a list of | -: : --name=5')` : arguments. If a string is provided, : -: : : it is split to determine the : -: : : arguments. If a list or tuple is : -: : : provided, they are the arguments. If : -: : : `command` is omitted, then : -: : : `sys.argv[1\:]` (the arguments from : -: : : the command line) are used by : -: : : default. : -| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the | -: : : name users will enter to run the : -: : : CLI. This name will be used in the : -: : : CLI's help screens. If the argument : -: : : is omitted, it will be inferred : -: : : automatically. : +| --------- | ------------------------- | ------------------------------------ | +| component | `fire.Fire(component)` | If omitted, defaults to a dict of all locals and globals. | +| command | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. | +| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.| +| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. | ## Using a Fire CLI without modifying any code diff --git a/docs/guide.md b/docs/guide.md index cb2c07db..44d8a46d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -742,7 +742,18 @@ The complete set of flags available is shown below, in the reference section. | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | Include private members in the output. -_Note that flags are separated from the Fire command by an isolated `--` arg._ +_Note that flags are separated from the Fire command by an isolated `--` arg. +Help is an exception; the isolated `--` is optional for getting help._ + + +##### Arguments for Calling fire.Fire() + +| Argument | Usage | Notes | +| --------- | ------------------------- | ------------------------------------ | +| component | `fire.Fire(component)` | If omitted, defaults to a dict of all locals and globals. | +| command | `fire.Fire(command='hello --name=5')` | Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If `command` is omitted, then `sys.argv[1:]` (the arguments from the command line) are used by default. | +| name | `fire.Fire(name='tool')` | The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI's help screens. If the argument is omitted, it will be inferred automatically.| +| serialize | `fire.Fire(serialize=custom_serializer)` | If omitted, simple types are serialized via their builtin str method, and any objects that define a custom `__str__` method are serialized with that. If specified, all objects are serialized to text via the provided method. | ### Disclaimer diff --git a/docs/index.md b/docs/index.md index 4eb114b1..8dcc5db6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -106,7 +106,8 @@ Please see [The Python Fire Guide](guide.md). | [Trace](using-cli.md#trace-flag) | `command -- --trace` | Gets a Fire trace for the command. | [Verbose](using-cli.md#verbose-flag) | `command -- --verbose` | -_Note that these flags are separated from the Fire command by an isolated `--`._ +_Note that flags are separated from the Fire command by an isolated `--` arg. +Help is an exception; the isolated `--` is optional for getting help._ ## License diff --git a/docs/installation.md b/docs/installation.md index 614243af..7e4cccb8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,5 +4,5 @@ To install Python Fire with pip, run: `pip install fire` To install Python Fire with conda, run: `conda install fire -c conda-forge` -To install Python Fire from source, first clone the repository and then run: -`python setup.py install` +To install Python Fire from source, first clone the repository and then run +`python setup.py install`. To install from source for development, instead run `python setup.py develop`. From e1e95ffe2454f7e9887640883e35c01543f54139 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 12 Dec 2022 15:43:49 -0500 Subject: [PATCH 129/205] Change mkdocs from pages to nav (#421) "pages" has been deprecated in favor of "nav" --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index bb815e37..bbe1e848 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Python Fire theme: readthedocs markdown_extensions: [fenced_code] -pages: +nav: - Overview: index.md - Installation: installation.md - Benefits: benefits.md From 2d2fa6196e5a8ce4ddcbabc565bacad163323b0f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 1 Feb 2023 10:53:09 -0800 Subject: [PATCH 130/205] Use literal dict (#430) Use literal dict to satisfy linter --- fire/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/decorators.py b/fire/decorators.py index 9e56d6df..b2e9b322 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -111,5 +111,5 @@ def GetMetadata(fn): def GetParseFns(fn): # type: (...) -> dict metadata = GetMetadata(fn) - default = dict(default=None, positional=[], named={}) + default = {"default": None, "positional": [], "named": {}} return metadata.get(FIRE_PARSE_FNS, default) From 396ef1c4efa1d977217e52267855b8acdc8d5a62 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Thu, 2 Feb 2023 04:47:53 +0900 Subject: [PATCH 131/205] freeze CI requirements (#431) * freeze CI requirements, including pylint --- .github/scripts/build.sh | 6 +----- .github/scripts/requirements.txt | 10 ++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/requirements.txt diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 6fd8f73b..958bc677 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -19,11 +19,7 @@ set -e PYTHON_VERSION=${PYTHON_VERSION:-2.7} -pip install --upgrade setuptools pip -pip install --upgrade pylint pytest pytest-pylint pytest-runner -pip install termcolor -pip install hypothesis python-Levenshtein -pip install mock +pip install -U -r requirements.txt python setup.py develop python -m pytest # Run the tests without IPython. pip install ipython diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 00000000..9e48e20d --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1,10 @@ +setuptools <65.7.0 +pip <23.0 +pylint <2.15.10 +pytest <=7.2.1 +pytest-pylint <=1.1.2 +pytest-runner <6.0.0 +termcolor <2.2.0 +hypothesis <6.62.0 +python-Levenshtein <0.20.9 +mock <5.0.0 \ No newline at end of file From 910b1f8a9448d305f3a4107a294c1963de18bddf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 1 Feb 2023 22:34:49 +0200 Subject: [PATCH 132/205] Fix path to requirements.txt (#433) --- .github/scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 958bc677..1f9ed766 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -19,7 +19,7 @@ set -e PYTHON_VERSION=${PYTHON_VERSION:-2.7} -pip install -U -r requirements.txt +pip install -U -r .github/scripts/requirements.txt python setup.py develop python -m pytest # Run the tests without IPython. pip install ipython From 0438367c157c3ff9b46c68fad0b7d66903e7b3a1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 1 Feb 2023 22:36:20 +0200 Subject: [PATCH 133/205] Fix deprecation warning: LICENSE is autodetected (#434) --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7a980136..977056b0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[metadata] -license-file = LICENSE - [wheel] universal = 1 From c29b923133668a86b802d7451e83aa92ed648de1 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Tue, 7 Feb 2023 00:41:38 +0900 Subject: [PATCH 134/205] adding python 3.10 [tag & CI] (#428) * adding python 3.10 --- .github/workflows/build.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc5e0405..9bf78e8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [2.7, 3.5, 3.7, 3.8, 3.9] + python-version: ["2.7", "3.5", "3.7", "3.8", "3.9", "3.10"] steps: # Checkout the repo. diff --git a/setup.py b/setup.py index f1f91103..8e95f414 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Operating System :: OS Independent', 'Operating System :: POSIX', From 71743b6fbaa2c1ca35a229ad3cc3e4638606bb35 Mon Sep 17 00:00:00 2001 From: John Bampton Date: Tue, 7 Feb 2023 01:43:43 +1000 Subject: [PATCH 135/205] docs: fix brand name `Github` -> `GitHub` (#425) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3c758e1..baae1a6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ Python versions < 3.5. ## Testing -Python Fire uses [Github Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run +Python Fire uses [GitHub Actions](https://github.com/google/python-fire/actions) to run tests on each pull request. You can run these tests yourself as well. To do this, first install the test dependencies listed in setup.py (e.g. pytest, mock, termcolor, and hypothesis). Then run the tests by running `pytest` in the root directory of the repository. From 527e3d056ecc134531f1ef3ed45d46581b5844b9 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 13 Feb 2023 14:20:46 -0500 Subject: [PATCH 136/205] Fix typos in console and tests (#436) --- fire/console/console_attr.py | 6 +++--- fire/console/console_attr_os.py | 2 +- fire/console/console_pager.py | 2 +- fire/docstrings_test.py | 12 ++++++------ fire/parser_test.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py index f88d5788..815e16b8 100644 --- a/fire/console/console_attr.py +++ b/fire/console/console_attr.py @@ -268,7 +268,7 @@ def __init__(self, encoding=None, suppress_output=False): # ANSI "standard" attributes. if self.SupportsAnsi(): - # Select Graphic Rendition paramaters from + # Select Graphic Rendition parameters from # http://en.wikipedia.org/wiki/ANSI_escape_code#graphics # Italic '3' would be nice here but its not widely supported. self._csi = '\x1b[' @@ -394,7 +394,7 @@ def GetControlSequenceIndicator(self): """Returns the control sequence indicator string. Returns: - The conrol sequence indicator string or None if control sequences are not + The control sequence indicator string or None if control sequences are not supported. """ return self._csi @@ -408,7 +408,7 @@ def GetControlSequenceLen(self, buf): buf: The string to check for a control sequence. Returns: - The conrol sequence length at the beginning of buf or 0 if buf does not + The control sequence length at the beginning of buf or 0 if buf does not start with a control sequence. """ if not self._csi or not buf.startswith(self._csi): diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py index 8482c7bc..869c5949 100644 --- a/fire/console/console_attr_os.py +++ b/fire/console/console_attr_os.py @@ -123,7 +123,7 @@ def _GetTermSizeEnvironment(): def _GetTermSizeTput(): - """Returns the terminal x and y dimemsions from tput(1).""" + """Returns the terminal x and y dimensions from tput(1).""" import subprocess # pylint: disable=g-import-not-at-top output = encoding.Decode(subprocess.check_output(['tput', 'cols'], stderr=subprocess.STDOUT)) diff --git a/fire/console/console_pager.py b/fire/console/console_pager.py index 044fcb37..565c7e1e 100644 --- a/fire/console/console_pager.py +++ b/fire/console/console_pager.py @@ -94,7 +94,7 @@ def __init__(self, contents, out=None, prompt=None): Args: contents: The entire contents of the text lines to page. out: The output stream, log.out (effectively) if None. - prompt: The page break prompt, a defalt prompt is used if None.. + prompt: The page break prompt, a default prompt is used if None.. """ self._contents = contents self._out = out or sys.stdout diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 2328ef16..0d6e5d18 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -50,12 +50,12 @@ def test_one_line_simple_whitespace(self): def test_one_line_too_long(self): # pylint: disable=line-too-long - docstring = """A one line docstring thats both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner. + docstring = """A one line docstring that is both a little too verbose and a little too long so it keeps going well beyond a reasonable length for a one-liner. """ # pylint: enable=line-too-long docstring_info = docstrings.parse(docstring) expected_docstring_info = DocstringInfo( - summary='A one line docstring thats both a little too verbose and ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it keeps going well beyond a reasonable length ' 'for a one-liner.', ) @@ -63,25 +63,25 @@ def test_one_line_too_long(self): def test_one_line_runs_over(self): # pylint: disable=line-too-long - docstring = """A one line docstring thats both a little too verbose and a little too long + docstring = """A one line docstring that is both a little too verbose and a little too long so it runs onto a second line. """ # pylint: enable=line-too-long docstring_info = docstrings.parse(docstring) expected_docstring_info = DocstringInfo( - summary='A one line docstring thats both a little too verbose and ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it runs onto a second line.', ) self.assertEqual(expected_docstring_info, docstring_info) def test_one_line_runs_over_whitespace(self): docstring = """ - A one line docstring thats both a little too verbose and a little too long + A one line docstring that is both a little too verbose and a little too long so it runs onto a second line. """ docstring_info = docstrings.parse(docstring) expected_docstring_info = DocstringInfo( - summary='A one line docstring thats both a little too verbose and ' + summary='A one line docstring that is both a little too verbose and ' 'a little too long so it runs onto a second line.', ) self.assertEqual(expected_docstring_info, docstring_info) diff --git a/fire/parser_test.py b/fire/parser_test.py index 0257be28..6b6b79b1 100644 --- a/fire/parser_test.py +++ b/fire/parser_test.py @@ -117,8 +117,8 @@ def testDefaultParseValueBareWordsTuple(self): def testDefaultParseValueNestedContainers(self): self.assertEqual( - parser.DefaultParseValue('[(A, 2, "3"), 5, {alph: 10.2, beta: "cat"}]'), - [('A', 2, '3'), 5, {'alph': 10.2, 'beta': 'cat'}]) + parser.DefaultParseValue('[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'), + [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}]) def testDefaultParseValueComments(self): self.assertEqual(parser.DefaultParseValue('"0#comments"'), '0#comments') From 6e485c971f4998175b841c775edcd980f2fb55ea Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 13 Feb 2023 15:24:46 -0500 Subject: [PATCH 137/205] Split too long line, fixing lint #437 --- fire/parser_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fire/parser_test.py b/fire/parser_test.py index 6b6b79b1..8aeabc61 100644 --- a/fire/parser_test.py +++ b/fire/parser_test.py @@ -117,7 +117,8 @@ def testDefaultParseValueBareWordsTuple(self): def testDefaultParseValueNestedContainers(self): self.assertEqual( - parser.DefaultParseValue('[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'), + parser.DefaultParseValue( + '[(A, 2, "3"), 5, {alpha: 10.2, beta: "cat"}]'), [('A', 2, '3'), 5, {'alpha': 10.2, 'beta': 'cat'}]) def testDefaultParseValueComments(self): From 1bcb5d1a265ed78e02b57174171aae9d813dbcf5 Mon Sep 17 00:00:00 2001 From: Panagiotis Simakis Date: Thu, 5 Oct 2023 18:39:58 +0300 Subject: [PATCH 138/205] Add missing parameter description (#462) --- fire/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fire/core.py b/fire/core.py index c1e97367..6367262d 100644 --- a/fire/core.py +++ b/fire/core.py @@ -94,6 +94,8 @@ def Fire(component=None, command=None, name=None, serialize=None): a string or a list of strings; a list of strings is preferred. name: Optional. The name of the command as entered at the command line. Used in interactive mode and for generating the completion script. + serialize: Optional. If supplied, all objects are serialized to text via + the provided callable. Returns: The result of executing the Fire command. Execution begins with the initial target component. The component is updated by using the command arguments From d44d33d4ac9389854b046ca0270c112693b309e6 Mon Sep 17 00:00:00 2001 From: Max <4649120+maximehk@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:49:58 +0100 Subject: [PATCH 139/205] Fix missing `$` sign in bash completion (#472) Related issue https://github.com/google/python-fire/issues/64 Co-authored-by: Max Hacker --- fire/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/completion.py b/fire/completion.py index 9659ec6a..4393880d 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -104,7 +104,7 @@ def _BashScript(name, commands, default_options=None): option_already_entered() {{ local opt - for opt in ${{COMP_WORDS[@]:0:COMP_CWORD}} + for opt in ${{COMP_WORDS[@]:0:$COMP_CWORD}} do if [ $1 == $opt ]; then return 0 From 1d8a137893222b977fc23764ad6e56293cf83a32 Mon Sep 17 00:00:00 2001 From: Hai Zhu <35182391+cocolato@users.noreply.github.com> Date: Wed, 3 Jan 2024 04:25:30 +0800 Subject: [PATCH 140/205] remove asyncio.coroutine (#440) --- fire/test_components_py3.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 5140921d..17fb932c 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -57,8 +57,7 @@ def lru_cache_decorated(arg1): class WithAsyncio(object): - @asyncio.coroutine - def double(self, count=0): + async def double(self, count=0): return 2 * count From ffb8121ab8d342bad2e613df41fe406b2ed5e133 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Tue, 2 Jan 2024 15:45:49 -0500 Subject: [PATCH 141/205] Update build.yml dropping Python 2.7 (#479) We'll want to reenable later. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bf78e8b..9864ae98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["2.7", "3.5", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.5", "3.7", "3.8", "3.9", "3.10"] steps: # Checkout the repo. From 8a41c91a474209b38d58839437f5711190fd3cee Mon Sep 17 00:00:00 2001 From: eXcript Date: Tue, 9 Jan 2024 15:22:03 -0300 Subject: [PATCH 142/205] Update formatting_windows.py (#477) Add a check if 'isatty' member exists in the object, required for packaging with PyInstaller. --- fire/formatting_windows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index 2b85820d..4bcf82e0 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -35,7 +35,7 @@ def initialize_or_disable(): """Enables ANSI processing on Windows or disables it as needed.""" if HAS_COLORAMA: wrap = True - if sys.stdout.isatty() and platform.release() == '10': + if hasattr(sys.stdout, "isatty") and sys.stdout.isatty() and platform.release() == '10': # Enables native ANSI sequences in console. # Windows 10, 2016, and 2019 only. From 343e6b6cec2d174d511e99dec7e5a24849121c2e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 24 Feb 2024 13:07:41 -0500 Subject: [PATCH 143/205] Add Python 3.11 and Python 3.12 to build workflow (#485) This change brings github actions back into the green. --- .github/scripts/requirements.txt | 11 +++++++---- .github/workflows/build.yml | 2 +- fire/__init__.py | 2 +- fire/__main__.py | 2 +- fire/console/encoding.py | 12 ++++++------ fire/formatting_windows.py | 4 +++- fire/inspectutils.py | 4 ++-- fire/testutils.py | 2 +- pylintrc | 2 +- setup.py | 4 +++- 10 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 9e48e20d..13880c9c 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,10 +1,13 @@ -setuptools <65.7.0 -pip <23.0 +setuptools <65.7.0 ; python_version == '2.7' +setuptools <=69.1.1 ; python_version >= '3.8' +pip <23.0 ; python_version == '2.7' +pip ; python_version >= '3.5' pylint <2.15.10 pytest <=7.2.1 pytest-pylint <=1.1.2 pytest-runner <6.0.0 termcolor <2.2.0 hypothesis <6.62.0 -python-Levenshtein <0.20.9 -mock <5.0.0 \ No newline at end of file +python-Levenshtein <0.20.9 ; python_version == '2.7' +levenshtein <=0.25.0 ; python_version >= '3.5' +mock <5.0.0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9864ae98..eb510f03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.5", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.5", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: # Checkout the repo. diff --git a/fire/__init__.py b/fire/__init__.py index 4cc76210..fae18489 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -21,4 +21,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.5.0' +__version__ = '0.6.0' diff --git a/fire/__main__.py b/fire/__main__.py index 2ad217d6..9d8227ad 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -80,7 +80,7 @@ def import_from_file_path(path): spec.loader.exec_module(module) # pytype: disable=attribute-error else: - import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module + import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module,import-error module = imp.load_source(module_name, path) return module, module_name diff --git a/fire/console/encoding.py b/fire/console/encoding.py index 780e5a28..41bda634 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -86,7 +86,7 @@ def Decode(data, encoding=None): try: # Just return the string if its pure ASCII. - return string.decode('ascii') + return string.decode('ascii') # pytype: disable=attribute-error except UnicodeError: # The string is not ASCII encoded. pass @@ -94,7 +94,7 @@ def Decode(data, encoding=None): # Try the suggested encoding if specified. if encoding: try: - return string.decode(encoding) + return string.decode(encoding) # pytype: disable=attribute-error except UnicodeError: # Bad suggestion. pass @@ -103,21 +103,21 @@ def Decode(data, encoding=None): # be exceptional if a valid extended ascii encoding with extended chars # were also a valid UITF-8 encoding. try: - return string.decode('utf8') + return string.decode('utf8') # pytype: disable=attribute-error except UnicodeError: # Not a UTF-8 encoding. pass # Try the filesystem encoding. try: - return string.decode(sys.getfilesystemencoding()) + return string.decode(sys.getfilesystemencoding()) # pytype: disable=attribute-error except UnicodeError: # string is not encoded for filesystem paths. pass # Try the system default encoding. try: - return string.decode(sys.getdefaultencoding()) + return string.decode(sys.getdefaultencoding()) # pytype: disable=attribute-error except UnicodeError: # string is not encoded using the default encoding. pass @@ -137,7 +137,7 @@ def Decode(data, encoding=None): # string = '\xdc' # string = string.decode('iso-8859-1') # string = string.encode('ascii', 'backslashreplace') - return string.decode('iso-8859-1') + return string.decode('iso-8859-1') # pytype: disable=attribute-error def GetEncodedValue(env, name, default=None): diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index 4bcf82e0..ce0f677d 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -35,7 +35,9 @@ def initialize_or_disable(): """Enables ANSI processing on Windows or disables it as needed.""" if HAS_COLORAMA: wrap = True - if hasattr(sys.stdout, "isatty") and sys.stdout.isatty() and platform.release() == '10': + if (hasattr(sys.stdout, "isatty") + and sys.stdout.isatty() + and platform.release() == '10'): # Enables native ANSI sequences in console. # Windows 10, 2016, and 2019 only. diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 0fa8e7d3..15f32f91 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -98,10 +98,10 @@ class with an __init__ method. def Py2GetArgSpec(fn): """A wrapper around getargspec that tries both fn and fn.__call__.""" try: - return inspect.getargspec(fn) # pylint: disable=deprecated-method + return inspect.getargspec(fn) # pylint: disable=deprecated-method,no-member except TypeError: if hasattr(fn, '__call__'): - return inspect.getargspec(fn.__call__) # pylint: disable=deprecated-method + return inspect.getargspec(fn.__call__) # pylint: disable=deprecated-method,no-member raise diff --git a/fire/testutils.py b/fire/testutils.py index ea410e82..5f875147 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -74,7 +74,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): def assertRaisesRegex(self, *args, **kwargs): # pylint: disable=arguments-differ if sys.version_info.major == 2: - return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method + return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method,no-member else: return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member diff --git a/pylintrc b/pylintrc index b89b16d1..558d3ba2 100644 --- a/pylintrc +++ b/pylintrc @@ -32,7 +32,7 @@ enable=indexing-exception,old-raise-syntax # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifier separated by comma (,) or put this option # multiple time. -disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment +disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-disabled,star-args,pointless-except,bad-option-value,global-statement,fixme,suppressed-message,useless-suppression,locally-enabled,file-ignored,wrong-import-order,useless-object-inheritance,no-else-return,super-with-arguments,raise-missing-from,consider-using-f-string,unspecified-encoding,unnecessary-lambda-assignment,wrong-import-position,ungrouped-imports,deprecated-module [REPORTS] diff --git a/setup.py b/setup.py index 8e95f414..24e0e325 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.5.0' +VERSION = '0.6.0' URL = 'https://github.com/google/python-fire' setup( @@ -72,6 +72,8 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Operating System :: OS Independent', 'Operating System :: POSIX', From 3c230d8c347225cb4430a823464c5336a01b36a6 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:34:50 +0100 Subject: [PATCH 144/205] Adding GitHub dependabot (#432) Co-authored-by: Hugo van Kemenade --- .github/dependabot.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..17c20d04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +# Basic dependabot.yml file with minimum configuration for two package managers + +version: 2 +updates: + # Enable version updates for python + - package-ecosystem: "pip" + directory: ".github/scripts/" + schedule: + interval: "monthly" + labels: ["ci"] + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 5 + reviewers: + - "dbieber" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + pip: + patterns: + - "*" # Check all dependencies + labels: ["ci"] + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 5 + reviewers: + - "dbieber" From db3fcaf737f917d61015f3b408a6fac0898b2030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:43:24 -0400 Subject: [PATCH 145/205] 2 dependabot updates (#491) 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: pip - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb510f03..f5562820 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,11 +12,11 @@ jobs: steps: # Checkout the repo. - name: Checkout Python Fire repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Set up Python environment. - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From c417aec195cee982d24f85223e7831f71fa5adc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:44:16 -0400 Subject: [PATCH 146/205] Update hypothesis requirement in /.github/scripts (#492) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.99.4) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 13880c9c..493ad0f0 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -7,7 +7,7 @@ pytest <=7.2.1 pytest-pylint <=1.1.2 pytest-runner <6.0.0 termcolor <2.2.0 -hypothesis <6.62.0 +hypothesis <6.100.0 python-Levenshtein <0.20.9 ; python_version == '2.7' levenshtein <=0.25.0 ; python_version >= '3.5' mock <5.0.0 From 027c50272911e92f02be7ae93c60b4559c98a1a1 Mon Sep 17 00:00:00 2001 From: Vladimir Pestov <92364726+BasedDepartment1@users.noreply.github.com> Date: Tue, 12 Mar 2024 01:47:19 +0500 Subject: [PATCH 147/205] #444: Removed pipes dependency (#447) Co-authored-by: Svayp11 --- fire/core.py | 5 ++--- fire/trace.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/fire/core.py b/fire/core.py index 6367262d..fada01b1 100644 --- a/fire/core.py +++ b/fire/core.py @@ -56,7 +56,6 @@ def main(argv): import inspect import json import os -import pipes import re import shlex import sys @@ -240,7 +239,7 @@ def _IsHelpShortcut(component_trace, remaining_args): 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) + cmd=shlex.quote(command)), file=sys.stderr) return show_help @@ -296,7 +295,7 @@ def _DisplayError(component_trace): if show_help: 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) + cmd=shlex.quote(command)), file=sys.stderr) help_text = helptext.HelpText(result, trace=component_trace, verbose=component_trace.verbose) output.append(help_text) diff --git a/fire/trace.py b/fire/trace.py index 7174f994..4c9674e3 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -29,7 +29,7 @@ from __future__ import division from __future__ import print_function -import pipes +import shlex from fire import inspectutils @@ -166,8 +166,8 @@ def display(arg1, arg2='!'): def _Quote(self, arg): if arg.startswith('--') and '=' in arg: prefix, value = arg.split('=', 1) - return pipes.quote(prefix) + '=' + pipes.quote(value) - return pipes.quote(arg) + return shlex.quote(prefix) + '=' + shlex.quote(value) + return shlex.quote(arg) def GetCommand(self, include_separators=True): """Returns the command representing the trace up to this point. From 8beb85e4e3192d1ec0c5614ce2f8a10450670e82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:51:50 -0400 Subject: [PATCH 148/205] Update termcolor requirement from <2.2.0 to <2.5.0 in /.github/scripts (#493) Updates the requirements on [termcolor](https://github.com/termcolor/termcolor) to permit the latest version. - [Release notes](https://github.com/termcolor/termcolor/releases) - [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md) - [Commits](https://github.com/termcolor/termcolor/compare/0.1...2.4.0) --- updated-dependencies: - dependency-name: termcolor dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 493ad0f0..31238e4d 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -6,7 +6,7 @@ pylint <2.15.10 pytest <=7.2.1 pytest-pylint <=1.1.2 pytest-runner <6.0.0 -termcolor <2.2.0 +termcolor <2.5.0 hypothesis <6.100.0 python-Levenshtein <0.20.9 ; python_version == '2.7' levenshtein <=0.25.0 ; python_version >= '3.5' From 595239ec7c096d8d95822153ee61190e8985f7bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:54:31 -0400 Subject: [PATCH 149/205] Update mock requirement from <5.0.0 to <6.0.0 in /.github/scripts (#495) Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version. - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/testing-cabal/mock/compare/release-0.5.0...5.1.0) --- updated-dependencies: - dependency-name: mock dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 31238e4d..b4efe7a9 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -10,4 +10,4 @@ termcolor <2.5.0 hypothesis <6.100.0 python-Levenshtein <0.20.9 ; python_version == '2.7' levenshtein <=0.25.0 ; python_version >= '3.5' -mock <5.0.0 +mock <6.0.0 From ab310cf8847b0c47216b37525cd54a6d358f9fc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:02:02 -0400 Subject: [PATCH 150/205] Update pytest requirement from <=7.2.1 to <=8.1.1 in /.github/scripts (#494) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index b4efe7a9..8cb6ad15 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -3,7 +3,7 @@ setuptools <=69.1.1 ; python_version >= '3.8' pip <23.0 ; python_version == '2.7' pip ; python_version >= '3.5' pylint <2.15.10 -pytest <=7.2.1 +pytest <=8.1.1 pytest-pylint <=1.1.2 pytest-runner <6.0.0 termcolor <2.5.0 From 014a637f668db5fcfb81c1d426995b08e2b2ade7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:02:46 -0400 Subject: [PATCH 151/205] Update pytest-runner requirement in /.github/scripts (#496) Updates the requirements on [pytest-runner](https://github.com/pytest-dev/pytest-runner) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest-runner/releases) - [Changelog](https://github.com/pytest-dev/pytest-runner/blob/main/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-runner/compare/1.0a1...v6.0.1) --- updated-dependencies: - dependency-name: pytest-runner dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 8cb6ad15..654f6079 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,7 +5,7 @@ pip ; python_version >= '3.5' pylint <2.15.10 pytest <=8.1.1 pytest-pylint <=1.1.2 -pytest-runner <6.0.0 +pytest-runner <7.0.0 termcolor <2.5.0 hypothesis <6.100.0 python-Levenshtein <0.20.9 ; python_version == '2.7' From f332cb1fe60c3a381d1ef5dc23c6e6d2142117df Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Thu, 14 Mar 2024 18:02:23 +0100 Subject: [PATCH 152/205] Fix typo in dependabot github actions group (#497) --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 17c20d04..d31b409b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,7 +20,7 @@ updates: schedule: interval: "monthly" groups: - pip: + gh-actions: patterns: - "*" # Check all dependencies labels: ["ci"] From de2852a41746538e9077d8ab2586875cffd3dc57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:11:36 -0400 Subject: [PATCH 153/205] Update setuptools requirement in /.github/scripts (#500) Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/0.6...v69.2.0) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 654f6079..98111196 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,5 +1,5 @@ setuptools <65.7.0 ; python_version == '2.7' -setuptools <=69.1.1 ; python_version >= '3.8' +setuptools <=69.2.0 ; python_version >= '3.8' pip <23.0 ; python_version == '2.7' pip ; python_version >= '3.5' pylint <2.15.10 From 6902939a317dca9d41446168d8e88f108e0c0f11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:21:26 -0400 Subject: [PATCH 154/205] Update hypothesis requirement in /.github/scripts (#506) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.100.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 98111196..15d2b017 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -7,7 +7,7 @@ pytest <=8.1.1 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 -hypothesis <6.100.0 +hypothesis <6.101.0 python-Levenshtein <0.20.9 ; python_version == '2.7' levenshtein <=0.25.0 ; python_version >= '3.5' mock <6.0.0 From e9f49b0fa9d5ee627e80a15e74624fcd41a34add Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:15:45 -0400 Subject: [PATCH 155/205] Update levenshtein requirement in /.github/scripts (#510) Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version. - [Release notes](https://github.com/rapidfuzz/Levenshtein/releases) - [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md) - [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.25.1) --- updated-dependencies: - dependency-name: levenshtein dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 15d2b017..f7d3cacd 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -9,5 +9,5 @@ pytest-runner <7.0.0 termcolor <2.5.0 hypothesis <6.101.0 python-Levenshtein <0.20.9 ; python_version == '2.7' -levenshtein <=0.25.0 ; python_version >= '3.5' +levenshtein <=0.25.1 ; python_version >= '3.5' mock <6.0.0 From a59f6bad3f72ae6035b076504744d3e9f619afea Mon Sep 17 00:00:00 2001 From: Weida Hong Date: Sat, 10 Aug 2024 04:16:48 +0800 Subject: [PATCH 156/205] Use ast.Constant for recent Python versions (#526) ast.Str is planned to removed in Python 3.14, use ast.Constant instead whenever the later is available. --- fire/parser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fire/parser.py b/fire/parser.py index 2aff8bd7..bdf3cdbf 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -20,7 +20,12 @@ import argparse import ast +import sys +if sys.version_info[0:2] < (3, 8): + _StrNode = ast.Str +else: + _StrNode = ast.Constant def CreateParser(): parser = argparse.ArgumentParser(add_help=False) @@ -127,4 +132,4 @@ def _Replacement(node): # These are the only builtin constants supported by literal_eval. if value in ('True', 'False', 'None'): return node - return ast.Str(value) + return _StrNode(value) From 8b063b952fba6dec79dfbc8688a9edf047de3b6c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 20:20:44 -0400 Subject: [PATCH 157/205] Remove future imports now that we've dropped support for Python 2 (#539) * Remove future imports now that we've dropped support for Python 2 * Keep future imports for use in MemberVisible * Drop support for Python 3.5 * Remove indications of support for Python <3.7 and bump version number. --- .github/scripts/build.sh | 2 +- .github/workflows/build.yml | 2 +- fire/__init__.py | 4 ---- fire/__main__.py | 4 ---- fire/completion_test.py | 4 ---- fire/console/console_io.py | 4 ---- fire/core.py | 4 ---- fire/core_test.py | 4 ---- fire/custom_descriptions.py | 4 ---- fire/custom_descriptions_test.py | 4 ---- fire/decorators.py | 4 ---- fire/decorators_test.py | 4 ---- fire/docstrings.py | 4 ---- fire/docstrings_fuzz_test.py | 4 ---- fire/docstrings_test.py | 4 ---- fire/fire_test.py | 4 ---- fire/formatting.py | 4 ---- fire/formatting_test.py | 4 ---- fire/formatting_windows.py | 4 ---- fire/helptext.py | 4 ---- fire/helptext_test.py | 4 ---- fire/inspectutils.py | 4 ---- fire/inspectutils_test.py | 4 ---- fire/interact.py | 4 ---- fire/interact_test.py | 4 ---- fire/parser.py | 4 ---- fire/parser_fuzz_test.py | 4 ---- fire/parser_test.py | 4 ---- fire/test_components.py | 4 ---- fire/test_components_bin.py | 4 ---- fire/test_components_test.py | 4 ---- fire/testutils.py | 4 ---- fire/testutils_test.py | 4 ---- fire/trace.py | 4 ---- fire/trace_test.py | 4 ---- fire/value_types.py | 4 ---- setup.py | 6 +----- 37 files changed, 3 insertions(+), 143 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 1f9ed766..111257ae 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -17,7 +17,7 @@ # Exit when any command fails. set -e -PYTHON_VERSION=${PYTHON_VERSION:-2.7} +PYTHON_VERSION=${PYTHON_VERSION:-3.7} pip install -U -r .github/scripts/requirements.txt python setup.py develop diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5562820..7f5225c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.5", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: # Checkout the repo. diff --git a/fire/__init__.py b/fire/__init__.py index fae18489..742b03ac 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -14,10 +14,6 @@ """The Python Fire module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire.core import Fire __all__ = ['Fire'] diff --git a/fire/__main__.py b/fire/__main__.py index 9d8227ad..15a9d6c8 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -18,10 +18,6 @@ This allows using Fire with third-party libraries without modifying their code. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import importlib import os import sys diff --git a/fire/completion_test.py b/fire/completion_test.py index 582e5bbc..5bafc279 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -14,10 +14,6 @@ """Tests for the completion module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import completion from fire import test_components as tc from fire import testutils diff --git a/fire/console/console_io.py b/fire/console/console_io.py index 3d3b9f81..ec0858d9 100644 --- a/fire/console/console_io.py +++ b/fire/console/console_io.py @@ -15,10 +15,6 @@ """General console printing utilities used by the Cloud SDK.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import signal import subprocess diff --git a/fire/core.py b/fire/core.py index fada01b1..0a6dae7d 100644 --- a/fire/core.py +++ b/fire/core.py @@ -49,10 +49,6 @@ def main(argv): --trace: Get the Fire Trace for the command. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect import json import os diff --git a/fire/core_test.py b/fire/core_test.py index 75b76998..b9033c22 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -14,10 +14,6 @@ """Tests for the core module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import core from fire import test_components as tc from fire import testutils diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 266671f1..f7df90b0 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -36,10 +36,6 @@ descriptions for primitive typed values. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting import six diff --git a/fire/custom_descriptions_test.py b/fire/custom_descriptions_test.py index 79d7c7a1..6cff2d5d 100644 --- a/fire/custom_descriptions_test.py +++ b/fire/custom_descriptions_test.py @@ -14,10 +14,6 @@ """Tests for custom description module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import custom_descriptions from fire import testutils diff --git a/fire/decorators.py b/fire/decorators.py index b2e9b322..eb5b0d20 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -18,10 +18,6 @@ command line arguments to client code. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect FIRE_METADATA = 'FIRE_METADATA' diff --git a/fire/decorators_test.py b/fire/decorators_test.py index cc7d6203..a316b79f 100644 --- a/fire/decorators_test.py +++ b/fire/decorators_test.py @@ -14,10 +14,6 @@ """Tests for the decorators module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import core from fire import decorators from fire import testutils diff --git a/fire/docstrings.py b/fire/docstrings.py index 1cfadea9..2d7c7e63 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -49,10 +49,6 @@ - "True | False" indicates bool type. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import collections import enum import re diff --git a/fire/docstrings_fuzz_test.py b/fire/docstrings_fuzz_test.py index 7609f4f8..66be8006 100644 --- a/fire/docstrings_fuzz_test.py +++ b/fire/docstrings_fuzz_test.py @@ -14,10 +14,6 @@ """Fuzz tests for the docstring parser module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import docstrings from fire import testutils diff --git a/fire/docstrings_test.py b/fire/docstrings_test.py index 0d6e5d18..ce516944 100644 --- a/fire/docstrings_test.py +++ b/fire/docstrings_test.py @@ -14,10 +14,6 @@ """Tests for fire docstrings module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import docstrings from fire import testutils diff --git a/fire/fire_test.py b/fire/fire_test.py index 8b904c29..6b9a2fa2 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -14,10 +14,6 @@ """Tests for the fire module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import sys diff --git a/fire/formatting.py b/fire/formatting.py index faef8047..68484c27 100644 --- a/fire/formatting.py +++ b/fire/formatting.py @@ -14,10 +14,6 @@ """Formatting utilities for use in creating help text.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting_windows # pylint: disable=unused-import import termcolor diff --git a/fire/formatting_test.py b/fire/formatting_test.py index 05a88c49..e0f6699d 100644 --- a/fire/formatting_test.py +++ b/fire/formatting_test.py @@ -14,10 +14,6 @@ """Tests for formatting.py.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import formatting from fire import testutils diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index ce0f677d..f8241eaa 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -14,10 +14,6 @@ """This module is used for enabling formatting on Windows.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import ctypes import os import platform diff --git a/fire/helptext.py b/fire/helptext.py index 6e7fbb07..93072897 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -29,10 +29,6 @@ information. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import collections import itertools import sys diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 404d9812..9a0f4f6d 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -14,10 +14,6 @@ """Tests for the helptext module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import sys import textwrap diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 15f32f91..ca51a9a5 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -14,10 +14,6 @@ """Inspection utility functions for Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect import sys import types diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index ea8eb0e2..bb62f402 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -14,10 +14,6 @@ """Tests for the inspectutils module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import unittest diff --git a/fire/interact.py b/fire/interact.py index 7df32841..7bdeb9a7 100644 --- a/fire/interact.py +++ b/fire/interact.py @@ -20,10 +20,6 @@ InteractiveConsole class. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect diff --git a/fire/interact_test.py b/fire/interact_test.py index 29fa7597..99cde285 100644 --- a/fire/interact_test.py +++ b/fire/interact_test.py @@ -14,10 +14,6 @@ """Tests for the interact module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import interact from fire import testutils diff --git a/fire/parser.py b/fire/parser.py index bdf3cdbf..c4708455 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -14,10 +14,6 @@ """Provides parsing functionality used by Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import argparse import ast import sys diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py index af0be038..38e17725 100644 --- a/fire/parser_fuzz_test.py +++ b/fire/parser_fuzz_test.py @@ -14,10 +14,6 @@ """Fuzz tests for the parser module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import parser from fire import testutils from hypothesis import example diff --git a/fire/parser_test.py b/fire/parser_test.py index 8aeabc61..a404eea2 100644 --- a/fire/parser_test.py +++ b/fire/parser_test.py @@ -14,10 +14,6 @@ """Tests for the parser module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import parser from fire import testutils diff --git a/fire/test_components.py b/fire/test_components.py index 5fcb056e..e50f647c 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -14,10 +14,6 @@ """This module has components that are used for testing Python Fire.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import collections import enum import functools diff --git a/fire/test_components_bin.py b/fire/test_components_bin.py index fbb41952..62afdf11 100644 --- a/fire/test_components_bin.py +++ b/fire/test_components_bin.py @@ -17,10 +17,6 @@ This file is useful for replicating test results manually. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import fire from fire import test_components diff --git a/fire/test_components_test.py b/fire/test_components_test.py index f35d7ab5..531f882c 100644 --- a/fire/test_components_test.py +++ b/fire/test_components_test.py @@ -14,10 +14,6 @@ """Tests for the test_components module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import test_components as tc from fire import testutils diff --git a/fire/testutils.py b/fire/testutils.py index 5f875147..76faa3f4 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -14,10 +14,6 @@ """Utilities for Python Fire's tests.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import contextlib import os import re diff --git a/fire/testutils_test.py b/fire/testutils_test.py index ad604193..0999a4c8 100644 --- a/fire/testutils_test.py +++ b/fire/testutils_test.py @@ -14,10 +14,6 @@ """Test the test utilities for Fire's tests.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import sys from fire import testutils diff --git a/fire/trace.py b/fire/trace.py index 4c9674e3..2145186e 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -25,10 +25,6 @@ component will be None. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import shlex from fire import inspectutils diff --git a/fire/trace_test.py b/fire/trace_test.py index 1621a593..1f858f5e 100644 --- a/fire/trace_test.py +++ b/fire/trace_test.py @@ -14,10 +14,6 @@ """Tests for the trace module.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - from fire import testutils from fire import trace diff --git a/fire/value_types.py b/fire/value_types.py index c0a137fd..b2d0a0b3 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -14,10 +14,6 @@ """Types of values.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import inspect from fire import inspectutils diff --git a/setup.py b/setup.py index 24e0e325..f861f9a5 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'python-Levenshtein', ] -VERSION = '0.6.0' +VERSION = '0.7.0' URL = 'https://github.com/google/python-fire' setup( @@ -63,11 +63,7 @@ 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', From 5d0706d814e8c9297f078fabc0a1638c34c1ef30 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 20:57:15 -0400 Subject: [PATCH 158/205] remove six: Replace six.string_types and six.integer_types, etc. (#541) This commit includes: * assuming Python 3 for test skipping * assuming Python 3 for fire's various type checks * assuming Python 3 for imports (like asyncio) * assuming Python 3 for getting function signatures * six is no longer considered a hidden module (and so if a user of fire has six in their globals when they call Fire(), six will now show up where it used to be hidden) This commit does not remove six from console/ code. --- fire/completion.py | 21 +++++++-------------- fire/core.py | 8 +++----- fire/core_test.py | 4 ---- fire/custom_descriptions.py | 9 +++------ fire/fire_test.py | 3 --- fire/helptext_test.py | 11 ----------- fire/inspectutils.py | 27 ++++----------------------- fire/inspectutils_test.py | 9 +-------- fire/parser_fuzz_test.py | 7 +++---- fire/test_components.py | 5 +---- fire/testutils.py | 11 ++++------- fire/testutils_test.py | 8 +++----- fire/value_types.py | 3 +-- 13 files changed, 30 insertions(+), 96 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 4393880d..3aa8ab11 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -23,7 +23,6 @@ import inspect from fire import inspectutils -import six def Script(name, component, default_options=None, shell='bash'): @@ -308,7 +307,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): Returns A boolean value indicating whether the member should be included. """ - if isinstance(name, six.string_types) and name.startswith('__'): + if isinstance(name, str) and name.startswith('__'): return False if verbose: return True @@ -316,10 +315,11 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): or member is division or member is print_function): return False - if isinstance(member, type(absolute_import)) and six.PY34: + if isinstance(member, type(absolute_import)): return False - if inspect.ismodule(member) and member is six: - # TODO(dbieber): Determine more generally which modules to hide. + # TODO(dbieber): Determine more generally which modules to hide. + modules_to_hide = [] + if inspect.ismodule(member) and member in modules_to_hide: return False if inspect.isclass(component): # If class_attrs has not been provided, compute it. @@ -336,14 +336,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): tuplegetter = getattr(collections, '_tuplegetter', type(None)) if isinstance(class_attr.object, tuplegetter): return False - if (six.PY2 and inspect.isfunction(component) - and name in ('func_closure', 'func_code', 'func_defaults', - 'func_dict', 'func_doc', 'func_globals', 'func_name')): - return False - if (six.PY2 and inspect.ismethod(component) - and name in ('im_class', 'im_func', 'im_self')): - return False - if isinstance(name, six.string_types): + if isinstance(name, str): return not name.startswith('_') return True # Default to including the member @@ -438,7 +431,7 @@ def _FormatForCommand(token): Returns: The transformed token. """ - if not isinstance(token, six.string_types): + if not isinstance(token, str): token = str(token) if token.startswith('_'): diff --git a/fire/core.py b/fire/core.py index 0a6dae7d..c61a8b57 100644 --- a/fire/core.py +++ b/fire/core.py @@ -67,10 +67,8 @@ def main(argv): from fire import trace from fire import value_types from fire.console import console_io -import six -if six.PY34: - import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error +import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error def Fire(component=None, command=None, name=None, serialize=None): @@ -109,7 +107,7 @@ def Fire(component=None, command=None, name=None, serialize=None): name = name or os.path.basename(sys.argv[0]) # Get args as a list. - if isinstance(command, six.string_types): + if isinstance(command, str): args = shlex.split(command) elif isinstance(command, (list, tuple)): args = command @@ -344,7 +342,7 @@ def _DictAsString(result, verbose=False): def _OneLineResult(result): """Returns result serialized to a single line string.""" # TODO(dbieber): Ensure line is fewer than eg 120 characters. - if isinstance(result, six.string_types): + if isinstance(result, str): return str(result).replace('\n', ' ') # TODO(dbieber): Show a small amount of usage information about the function diff --git a/fire/core_test.py b/fire/core_test.py index b9033c22..9e1f7dba 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -20,8 +20,6 @@ from fire import trace import mock -import six - class CoreTest(testutils.BaseTestCase): @@ -214,13 +212,11 @@ def serialize(x): with self.assertRaises(core.FireError): core.Fire(ident, command=['asdf'], serialize=55) - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecoratorBoundArg(self): self.assertEqual( core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr command=['lru_cache_in_class', 'foo']), 'foo') - @testutils.skipIf(six.PY2, 'lru_cache is Python 3 only.') def testLruCacheDecorator(self): self.assertEqual( core.Fire(tc.py3.lru_cache_decorated, # pytype: disable=module-attr diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index f7df90b0..768f0e23 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -37,7 +37,6 @@ """ from fire import formatting -import six TWO_DOUBLE_QUOTES = '""' STRING_DESC_PREFIX = 'The string ' @@ -60,13 +59,11 @@ def NeedsCustomDescription(component): Whether the component should use a custom description and summary. """ type_ = type(component) - if (type_ in six.string_types - or type_ in six.integer_types - or type_ is six.text_type - or type_ is six.binary_type + if ( + type_ in (str, int, bytes) or type_ in (float, complex, bool) or type_ in (dict, tuple, list, set, frozenset) - ): + ): return True return False diff --git a/fire/fire_test.py b/fire/fire_test.py index 6b9a2fa2..74b3bb25 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -22,7 +22,6 @@ from fire import testutils import mock -import six class FireTest(testutils.BaseTestCase): @@ -180,7 +179,6 @@ def testFireAnnotatedArgs(self): self.assertEqual(fire.Fire(tc.Annotations, command=['double', '5']), 10) self.assertEqual(fire.Fire(tc.Annotations, command=['triple', '5']), 15) - @testutils.skipIf(six.PY2, 'Keyword-only arguments not in Python 2.') def testFireKeywordOnlyArgs(self): with self.assertRaisesFireExit(2): # Keyword arguments must be passed with flag syntax. @@ -717,7 +715,6 @@ def testHelpKwargsDecorator(self): with self.assertRaisesFireExit(0): fire.Fire(tc.decorated_method, command=['--help']) - @testutils.skipIf(six.PY2, 'Asyncio not available in Python 2.') def testFireAsyncio(self): self.assertEqual(fire.Fire(tc.py3.WithAsyncio, command=['double', '--count', '10']), 20) diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 9a0f4f6d..2250f199 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -23,7 +23,6 @@ from fire import test_components as tc from fire import testutils from fire import trace -import six class HelpTest(testutils.BaseTestCase): @@ -276,8 +275,6 @@ def testHelpTextNoInit(self): self.assertIn('NAME\n OldStyleEmpty', help_screen) self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) - @testutils.skipIf( - six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithDefault(self): component = tc.py3.KeywordOnly.with_default # pytype: disable=module-attr output = helptext.HelpText( @@ -285,8 +282,6 @@ def testHelpTextKeywordOnlyArgumentsWithDefault(self): self.assertIn('NAME\n with_default', output) self.assertIn('FLAGS\n -x, --x=X', output) - @testutils.skipIf( - six.PY2, 'Python 2 does not support keyword-only arguments.') def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): component = tc.py3.KeywordOnly.double # pytype: disable=module-attr output = helptext.HelpText( @@ -294,9 +289,6 @@ def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): self.assertIn('NAME\n double', output) self.assertIn('FLAGS\n -c, --count=COUNT (required)', output) - @testutils.skipIf( - six.PY2, - 'Python 2 does not support required name-only arguments.') def testHelpTextFunctionMixedDefaults(self): component = tc.py3.HelpTextComponent().identity t = trace.FireTrace(component, name='FunctionMixedDefaults') @@ -523,9 +515,6 @@ def testUsageOutputFunctionWithDocstring(self): textwrap.dedent(expected_output).lstrip('\n'), usage_output) - @testutils.skipIf( - six.PY2, - 'Python 2 does not support required name-only arguments.') def testUsageOutputFunctionMixedDefaults(self): component = tc.py3.HelpTextComponent().identity t = trace.FireTrace(component, name='FunctionMixedDefaults') diff --git a/fire/inspectutils.py b/fire/inspectutils.py index ca51a9a5..0d0b048d 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -20,10 +20,7 @@ from fire import docstrings -import six - -if six.PY34: - import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error +import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error class FullArgSpec(object): @@ -74,8 +71,6 @@ class with an __init__ method. if inspect.isclass(fn): # If the function is a class, we try to use its init method. skip_arg = True - if six.PY2 and hasattr(fn, '__init__'): - fn = fn.__init__ elif inspect.ismethod(fn): # If the function is a bound method, we skip the `self` argument. skip_arg = fn.__self__ is not None @@ -91,16 +86,6 @@ class with an __init__ method. return fn, skip_arg -def Py2GetArgSpec(fn): - """A wrapper around getargspec that tries both fn and fn.__call__.""" - try: - return inspect.getargspec(fn) # pylint: disable=deprecated-method,no-member - except TypeError: - if hasattr(fn, '__call__'): - return inspect.getargspec(fn.__call__) # pylint: disable=deprecated-method,no-member - raise - - def Py3GetFullArgSpec(fn): """A alternative to the builtin getfullargspec. @@ -185,13 +170,9 @@ def GetFullArgSpec(fn): if sys.version_info[0:2] >= (3, 5): (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = Py3GetFullArgSpec(fn) - elif six.PY3: # Specifically Python 3.4. + else: # Specifically Python 3.4. (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(fn) # pylint: disable=deprecated-method,no-member - else: # six.PY2 - args, varargs, varkw, defaults = Py2GetArgSpec(fn) - kwonlyargs = kwonlydefaults = None - annotations = getattr(fn, '__annotations__', None) except TypeError: # If we can't get the argspec, how do we know if the fn should take args? @@ -221,7 +202,7 @@ def GetFullArgSpec(fn): return FullArgSpec() # In Python 3.5+ Py3GetFullArgSpec uses skip_bound_arg=True already. - skip_arg_required = six.PY2 or sys.version_info[0:2] == (3, 4) + skip_arg_required = sys.version_info[0:2] == (3, 4) if skip_arg_required and skip_arg and args: args.pop(0) # Remove 'self' or 'cls' from the list of arguments. return FullArgSpec(args, varargs, varkw, defaults, @@ -363,6 +344,6 @@ def GetClassAttrsDict(component): def IsCoroutineFunction(fn): try: - return six.PY34 and asyncio.iscoroutinefunction(fn) + return asyncio.iscoroutinefunction(fn) except: # pylint: disable=bare-except return False diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index bb62f402..47de7e72 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -15,14 +15,11 @@ """Tests for the inspectutils module.""" import os -import unittest from fire import inspectutils from fire import test_components as tc from fire import testutils -import six - class InspectUtilsTest(testutils.BaseTestCase): @@ -36,7 +33,6 @@ def testGetFullArgSpec(self): self.assertEqual(spec.kwonlydefaults, {}) self.assertEqual(spec.annotations, {'arg2': int, 'arg4': int}) - @unittest.skipIf(six.PY2, 'No keyword arguments in python 2') def testGetFullArgSpecPy3(self): spec = inspectutils.GetFullArgSpec(tc.py3.identity) self.assertEqual(spec.args, ['arg1', 'arg2', 'arg3', 'arg4']) @@ -121,10 +117,7 @@ def testInfoClass(self): def testInfoClassNoInit(self): info = inspectutils.Info(tc.OldStyleEmpty) - if six.PY2: - self.assertEqual(info.get('type_name'), 'classobj') - else: - self.assertEqual(info.get('type_name'), 'type') + self.assertEqual(info.get('type_name'), 'type') self.assertIn(os.path.join('fire', 'test_components.py'), info.get('file')) self.assertGreater(info.get('line'), 0) diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py index 38e17725..9739ec4e 100644 --- a/fire/parser_fuzz_test.py +++ b/fire/parser_fuzz_test.py @@ -21,7 +21,6 @@ from hypothesis import settings from hypothesis import strategies as st import Levenshtein -import six class ParserFuzzTest(testutils.BaseTestCase): @@ -64,8 +63,8 @@ def testDefaultParseValueFuzz(self, value): raise try: - uvalue = six.text_type(value) - uresult = six.text_type(result) + uvalue = str(value) + uresult = str(result) except UnicodeDecodeError: # This is not what we're testing. return @@ -82,7 +81,7 @@ def testDefaultParseValueFuzz(self, value): if '#' in value: max_distance += len(value) - value.index('#') - if not isinstance(result, six.string_types): + if not isinstance(result, str): max_distance += value.count('0') # Leading 0s are stripped. # Note: We don't check distance for dicts since item order can be changed. diff --git a/fire/test_components.py b/fire/test_components.py index e50f647c..540a9e16 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -18,10 +18,7 @@ import enum import functools -import six - -if six.PY3: - from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top +from fire import test_components_py3 as py3 # pylint: disable=unused-import,no-name-in-module,g-import-not-at-top def identity(arg1, arg2, arg3=10, arg4=20, *arg5, **arg6): # pylint: disable=keyword-arg-before-vararg diff --git a/fire/testutils.py b/fire/testutils.py index 76faa3f4..fa1ca86d 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -15,6 +15,7 @@ """Utilities for Python Fire's tests.""" import contextlib +import io import os import re import sys @@ -24,7 +25,6 @@ from fire import trace import mock -import six class BaseTestCase(unittest.TestCase): @@ -45,8 +45,8 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): Yields: Yields to the wrapped context. """ - stdout_fp = six.StringIO() - stderr_fp = six.StringIO() + stdout_fp = io.StringIO() + stderr_fp = io.StringIO() try: with mock.patch.object(sys, 'stdout', stdout_fp): with mock.patch.object(sys, 'stderr', stderr_fp): @@ -69,10 +69,7 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): (name, value, regexp)) def assertRaisesRegex(self, *args, **kwargs): # pylint: disable=arguments-differ - if sys.version_info.major == 2: - return super(BaseTestCase, self).assertRaisesRegexp(*args, **kwargs) # pylint: disable=deprecated-method,no-member - else: - return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member + return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): diff --git a/fire/testutils_test.py b/fire/testutils_test.py index 0999a4c8..4cfc0937 100644 --- a/fire/testutils_test.py +++ b/fire/testutils_test.py @@ -18,8 +18,6 @@ from fire import testutils -import six - class TestTestUtils(testutils.BaseTestCase): """Let's get meta.""" @@ -30,15 +28,15 @@ def testNoCheckOnException(self): raise ValueError() def testCheckStdoutOrStderrNone(self): - with six.assertRaisesRegex(self, AssertionError, 'stdout:'): + with self.assertRaisesRegex(AssertionError, 'stdout:'): with self.assertOutputMatches(stdout=None): print('blah') - with six.assertRaisesRegex(self, AssertionError, 'stderr:'): + with self.assertRaisesRegex(AssertionError, 'stderr:'): with self.assertOutputMatches(stderr=None): print('blah', file=sys.stderr) - with six.assertRaisesRegex(self, AssertionError, 'stderr:'): + with self.assertRaisesRegex(AssertionError, 'stderr:'): with self.assertOutputMatches(stdout='apple', stderr=None): print('apple') print('blah', file=sys.stderr) diff --git a/fire/value_types.py b/fire/value_types.py index b2d0a0b3..81308973 100644 --- a/fire/value_types.py +++ b/fire/value_types.py @@ -17,10 +17,9 @@ import inspect from fire import inspectutils -import six -VALUE_TYPES = (bool, six.string_types, six.integer_types, float, complex, +VALUE_TYPES = (bool, str, bytes, int, float, complex, type(Ellipsis), type(None), type(NotImplemented)) From b13c13bf1767caf80bf38349dd5fbf0dd08e18b8 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 21:08:42 -0400 Subject: [PATCH 159/205] Remove sys.version_info checks (#542) * Remove sys.version_info checks where no longer needed --- fire/__main__.py | 25 ++++++------------------- fire/fire_test.py | 2 -- fire/helptext.py | 5 +---- fire/helptext_test.py | 13 ------------- fire/parser.py | 1 + 5 files changed, 8 insertions(+), 38 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 15a9d6c8..11fb1b42 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -19,6 +19,7 @@ """ import importlib +from importlib import util import os import sys @@ -57,27 +58,13 @@ def import_from_file_path(path): module_name = os.path.basename(path) - if sys.version_info.major == 3 and sys.version_info.minor < 5: - loader = importlib.machinery.SourceFileLoader( # pylint: disable=no-member - fullname=module_name, - path=path, - ) + spec = util.spec_from_file_location(module_name, path) - module = loader.load_module(module_name) # pylint: disable=deprecated-method + if spec is None: + raise IOError('Unable to load module from specified path.') - elif sys.version_info.major == 3: - from importlib import util # pylint: disable=g-import-not-at-top,import-outside-toplevel,no-name-in-module - spec = util.spec_from_file_location(module_name, path) - - if spec is None: - raise IOError('Unable to load module from specified path.') - - module = util.module_from_spec(spec) # pylint: disable=no-member - spec.loader.exec_module(module) # pytype: disable=attribute-error - - else: - import imp # pylint: disable=g-import-not-at-top,import-outside-toplevel,deprecated-module,import-error - module = imp.load_source(module_name, path) + module = util.module_from_spec(spec) # pylint: disable=no-member + spec.loader.exec_module(module) # pytype: disable=attribute-error return module, module_name diff --git a/fire/fire_test.py b/fire/fire_test.py index 74b3bb25..74f1f6e6 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -706,8 +706,6 @@ def testClassWithInvalidProperty(self): fire.Fire(tc.InvalidProperty, command=['double', '10']), 20 ) - @testutils.skipIf(sys.version_info[0:2] <= (3, 4), - 'Cannot inspect wrapped signatures in Python 2 or 3.4.') def testHelpKwargsDecorator(self): # Issue #190, follow the wrapped method instead of crashing. with self.assertRaisesFireExit(0): diff --git a/fire/helptext.py b/fire/helptext.py index 93072897..1c0cb626 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -31,7 +31,6 @@ import collections import itertools -import sys from fire import completion from fire import custom_descriptions @@ -533,9 +532,7 @@ def _GetArgType(arg, spec): if arg in spec.annotations: arg_type = spec.annotations[arg] try: - if sys.version_info[0:2] >= (3, 3): - return arg_type.__qualname__ - return arg_type.__name__ + return arg_type.__qualname__ except AttributeError: # Some typing objects, such as typing.Union do not have either a __name__ # or __qualname__ attribute. diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 2250f199..4d35dc0a 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -15,7 +15,6 @@ """Tests for the helptext module.""" import os -import sys import textwrap from fire import formatting @@ -124,9 +123,6 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): 'Additional undocumented flags may also be accepted.', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithDefaultsAndTypes(self): component = ( tc.py3.WithDefaultsAndTypes().double) # pytype: disable=module-attr @@ -141,9 +137,6 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): help_screen) self.assertNotIn('NOTES', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypesAndDefaultNone(self): component = ( tc.py3.WithDefaultsAndTypes().get_int) # pytype: disable=module-attr @@ -159,9 +152,6 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): help_screen) self.assertNotIn('NOTES', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithTypes(self): component = tc.py3.WithTypes().double # pytype: disable=module-attr help_screen = helptext.HelpText( @@ -177,9 +167,6 @@ def testHelpTextFunctionWithTypes(self): 'NOTES\n You can also use flags syntax for POSITIONAL ARGUMENTS', help_screen) - @testutils.skipIf( - sys.version_info[0:2] < (3, 5), - 'Python < 3.5 does not support type hints.') def testHelpTextFunctionWithLongTypes(self): component = tc.py3.WithTypes().long_type # pytype: disable=module-attr help_screen = helptext.HelpText( diff --git a/fire/parser.py b/fire/parser.py index c4708455..d945b8ce 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -23,6 +23,7 @@ else: _StrNode = ast.Constant + def CreateParser(): parser = argparse.ArgumentParser(add_help=False) parser.add_argument('--verbose', '-v', action='store_true') From 2d950337499156ccb9a17dbf5f389d0c7d10ec24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:09:02 -0400 Subject: [PATCH 160/205] Update setuptools requirement in /.github/scripts (#540) Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.1.0) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index f7d3cacd..1932f53d 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,5 +1,5 @@ setuptools <65.7.0 ; python_version == '2.7' -setuptools <=69.2.0 ; python_version >= '3.8' +setuptools <=75.1.0 ; python_version >= '3.8' pip <23.0 ; python_version == '2.7' pip ; python_version >= '3.5' pylint <2.15.10 From f012df240f16bd7e1c3eb90e96472ef5051ac5e2 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 21:20:37 -0400 Subject: [PATCH 161/205] Simplify requirements.txt by assuming Python 3 (#543) * Remove universal=1 line * Update setup.py * Update requirements.txt --- .github/scripts/requirements.txt | 9 +++------ setup.cfg | 3 --- setup.py | 3 +-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 1932f53d..d0344221 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,13 +1,10 @@ -setuptools <65.7.0 ; python_version == '2.7' -setuptools <=75.1.0 ; python_version >= '3.8' -pip <23.0 ; python_version == '2.7' -pip ; python_version >= '3.5' +setuptools <=75.1.0 +pip pylint <2.15.10 pytest <=8.1.1 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 hypothesis <6.101.0 -python-Levenshtein <0.20.9 ; python_version == '2.7' -levenshtein <=0.25.1 ; python_version >= '3.5' +levenshtein <=0.25.1 mock <6.0.0 diff --git a/setup.cfg b/setup.cfg index 977056b0..ed53d83b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [aliases] test = pytest diff --git a/setup.py b/setup.py index f861f9a5..6adbef46 100644 --- a/setup.py +++ b/setup.py @@ -31,13 +31,12 @@ DEPENDENCIES = [ 'six', 'termcolor', - 'enum34; python_version < "3.4"' ] TEST_DEPENDENCIES = [ 'hypothesis', 'mock', - 'python-Levenshtein', + 'levenshtein', ] VERSION = '0.7.0' From 9825623bd5c66692be16126087d3492eed9a0161 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:26:51 -0400 Subject: [PATCH 162/205] Update hypothesis requirement in /.github/scripts (#544) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.112.1) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index d0344221..669f09e1 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,6 +5,6 @@ pytest <=8.1.1 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 -hypothesis <6.101.0 +hypothesis <6.113.0 levenshtein <=0.25.1 mock <6.0.0 From b83fa05b72d8b225182043740e35cd0c28a29293 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 21:30:54 -0400 Subject: [PATCH 163/205] Remove mock in favor of unittest.mock (#545) * Remove mock in favor of unittest.mock --- .github/scripts/requirements.txt | 1 - fire/core_test.py | 3 ++- fire/fire_import_test.py | 2 +- fire/fire_test.py | 3 +-- fire/interact_test.py | 4 ++-- fire/testutils.py | 3 +-- setup.py | 1 - 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 669f09e1..8c0a5dcc 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -7,4 +7,3 @@ pytest-runner <7.0.0 termcolor <2.5.0 hypothesis <6.113.0 levenshtein <=0.25.1 -mock <6.0.0 diff --git a/fire/core_test.py b/fire/core_test.py index 9e1f7dba..90b7f466 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -14,11 +14,12 @@ """Tests for the core module.""" +from unittest import mock + from fire import core from fire import test_components as tc from fire import testutils from fire import trace -import mock class CoreTest(testutils.BaseTestCase): diff --git a/fire/fire_import_test.py b/fire/fire_import_test.py index c5975681..a6b4acc3 100644 --- a/fire/fire_import_test.py +++ b/fire/fire_import_test.py @@ -15,10 +15,10 @@ """Tests importing the fire module.""" import sys +from unittest import mock import fire from fire import testutils -import mock class FireImportTest(testutils.BaseTestCase): diff --git a/fire/fire_test.py b/fire/fire_test.py index 74f1f6e6..99b4a7c6 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -16,13 +16,12 @@ import os import sys +from unittest import mock import fire from fire import test_components as tc from fire import testutils -import mock - class FireTest(testutils.BaseTestCase): diff --git a/fire/interact_test.py b/fire/interact_test.py index 99cde285..2f286824 100644 --- a/fire/interact_test.py +++ b/fire/interact_test.py @@ -14,11 +14,11 @@ """Tests for the interact module.""" +from unittest import mock + from fire import interact from fire import testutils -import mock - try: import IPython # pylint: disable=unused-import, g-import-not-at-top diff --git a/fire/testutils.py b/fire/testutils.py index fa1ca86d..816551b5 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -20,12 +20,11 @@ import re import sys import unittest +from unittest import mock from fire import core from fire import trace -import mock - class BaseTestCase(unittest.TestCase): """Shared test case for Python Fire tests.""" diff --git a/setup.py b/setup.py index 6adbef46..53f3381a 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,6 @@ TEST_DEPENDENCIES = [ 'hypothesis', - 'mock', 'levenshtein', ] From 374d8c60787f66b803aab7590fc7ab5e4307d84a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:36:29 -0400 Subject: [PATCH 164/205] Update pytest requirement from <=8.1.1 to <=8.3.3 in /.github/scripts (#546) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.3.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 8c0a5dcc..b9cd377e 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,7 +1,7 @@ setuptools <=75.1.0 pip pylint <2.15.10 -pytest <=8.1.1 +pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 From 32b5142151789d5c64618db1ea5b0a47eb27a6c0 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 21:41:22 -0400 Subject: [PATCH 165/205] Update label used by dependabot (#547) --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d31b409b..ba1b7f19 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,7 @@ updates: directory: ".github/scripts/" schedule: interval: "monthly" - labels: ["ci"] + labels: ["dependabot"] pull-request-branch-name: separator: "-" open-pull-requests-limit: 5 @@ -23,7 +23,7 @@ updates: gh-actions: patterns: - "*" # Check all dependencies - labels: ["ci"] + labels: ["dependabot"] pull-request-branch-name: separator: "-" open-pull-requests-limit: 5 From 9c9e8c63f745da0aeb6332a1f027b05972f579d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:59:27 -0700 Subject: [PATCH 166/205] Update levenshtein requirement in /.github/scripts (#548) Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version. - [Release notes](https://github.com/rapidfuzz/Levenshtein/releases) - [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md) - [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.26.0) --- updated-dependencies: - dependency-name: levenshtein dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index b9cd377e..157c77b2 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -6,4 +6,4 @@ pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 hypothesis <6.113.0 -levenshtein <=0.25.1 +levenshtein <=0.26.0 From f9293c9da7fe6645a25448537d40b563d970b2f4 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 22:06:38 -0400 Subject: [PATCH 167/205] Upgrade pylint version (#549) * Upgrade pylint * Fixing lint errors like removing a useless super call * Merging bad-names lists and other modernization in pylintrc --- .github/scripts/requirements.txt | 2 +- fire/core.py | 1 + fire/test_components.py | 3 +-- fire/testutils.py | 3 --- pylintrc | 22 ++-------------------- 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 157c77b2..a5648989 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,6 +1,6 @@ setuptools <=75.1.0 pip -pylint <2.15.10 +pylint <3.2.8 pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 diff --git a/fire/core.py b/fire/core.py index c61a8b57..bce9b641 100644 --- a/fire/core.py +++ b/fire/core.py @@ -872,6 +872,7 @@ def _ParseKeywordArgs(args, fn_spec): key, value = stripped_argument.split('=', 1) else: key = stripped_argument + value = None # value will be set later on. key = key.replace('-', '_') is_bool_syntax = (not contains_equals and diff --git a/fire/test_components.py b/fire/test_components.py index 540a9e16..eb3a9e24 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -388,8 +388,7 @@ def example_generator(n): [0, 1, 2, 3] """ - for i in range(n): - yield i + yield from range(n) def simple_set(): diff --git a/fire/testutils.py b/fire/testutils.py index 816551b5..eca37f43 100644 --- a/fire/testutils.py +++ b/fire/testutils.py @@ -67,9 +67,6 @@ def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True): raise AssertionError('%s: Expected %r to match %r' % (name, value, regexp)) - def assertRaisesRegex(self, *args, **kwargs): # pylint: disable=arguments-differ - return super(BaseTestCase, self).assertRaisesRegex(*args, **kwargs) # pylint: disable=no-member - @contextlib.contextmanager def assertRaisesFireExit(self, code, regexp='.*'): """Asserts that a FireExit error is raised in the context. diff --git a/pylintrc b/pylintrc index 558d3ba2..8896bb5b 100644 --- a/pylintrc +++ b/pylintrc @@ -7,9 +7,6 @@ # pygtk.require(). #init-hook= -# Profiled execution. -profile=no - # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore= @@ -41,14 +38,6 @@ disable=design,similarities,no-self-use,attribute-defined-outside-init,locally-d # (visual studio) and html output-format=text -# Include message's id in output -include-ids=no - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - # Tells whether to display a full report or only the messages reports=yes @@ -59,10 +48,6 @@ reports=yes # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - [VARIABLES] @@ -79,9 +64,6 @@ additional-builtins= [BASIC] -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input,reduce - # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ @@ -114,7 +96,7 @@ inlinevar-rgx=^[a-z][a-z0-9_]*$ good-names=i,j,k,ex,main,Run,_ # Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata +bad-names=map,filter,apply,input,reduce,foo,bar,baz,toto,tutu,tata # Regular expression which should only match functions or classes name which do # not require a docstring @@ -186,7 +168,7 @@ max-locals=15 max-returns=6 # Maximum number of branch for function / method body -max-branchs=12 +max-branches=12 # Maximum number of statements in function / method body max-statements=50 From c5f5f9008303a661558339ea3c298d247248fdbe Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 22:18:32 -0400 Subject: [PATCH 168/205] Run github action on pull_request (#550) * Run github action on pull_request --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f5225c5..a6649201 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,12 @@ name: Python Fire -on: [push] +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: build: From ca4e80b9072397ec1c03bdf83a0bd62524a601aa Mon Sep 17 00:00:00 2001 From: Kai Chen Date: Fri, 20 Sep 2024 10:22:06 +0800 Subject: [PATCH 169/205] Add current system MSYS check (#278) --- fire/console/platforms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fire/console/platforms.py b/fire/console/platforms.py index 018eb89e..13fd8204 100644 --- a/fire/console/platforms.py +++ b/fire/console/platforms.py @@ -153,6 +153,8 @@ def Current(): return OperatingSystem.MACOSX elif 'cygwin' in sys.platform: return OperatingSystem.CYGWIN + elif 'msys' in sys.platform: + return OperatingSystem.MSYS return None @staticmethod From 4efd44dbb14ba2bf044f2fae701f787da0bfbe1e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 23:06:02 -0400 Subject: [PATCH 170/205] Remove .format in favor of f-strings (#551) * Remove .format in favor of f-strings --- docs/guide.md | 10 ++--- examples/widget/widget.py | 2 +- fire/completion.py | 7 +-- fire/completion_test.py | 3 +- fire/core.py | 32 +++++++------- fire/helptext.py | 92 +++++++++++++++++---------------------- fire/interact.py | 11 ++--- fire/trace.py | 13 +++--- 8 files changed, 75 insertions(+), 95 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 44d8a46d..cdc3b2d0 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -30,7 +30,7 @@ the program to the command line. import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' if __name__ == '__main__': fire.Fire() @@ -52,7 +52,7 @@ command line. import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' if __name__ == '__main__': fire.Fire(hello) @@ -76,7 +76,7 @@ We can alternatively write this program like this: import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def main(): fire.Fire(hello) @@ -93,7 +93,7 @@ then simply this: import fire def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def main(): fire.Fire(hello) @@ -105,7 +105,7 @@ If you have a file `example.py` that doesn't even import fire: ```python def hello(name): - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' ``` Then you can use it with Fire like this: diff --git a/examples/widget/widget.py b/examples/widget/widget.py index bf1cbeb2..9092ad75 100644 --- a/examples/widget/widget.py +++ b/examples/widget/widget.py @@ -25,7 +25,7 @@ def whack(self, n=1): def bang(self, noise='bang'): """Makes a loud noise.""" - return '{noise} bang!'.format(noise=noise) + return f'{noise} bang!' def main(): diff --git a/fire/completion.py b/fire/completion.py index 3aa8ab11..625e9d86 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -277,10 +277,7 @@ def _FishScript(name, commands, default_options=None): ) return fish_source.format( - global_options=' '.join( - '"{option}"'.format(option=option) - for option in global_options - ) + global_options=' '.join(f'"{option}"' for option in global_options) ) @@ -385,7 +382,7 @@ def _CompletionsFromArgs(fn_args): completions = [] for arg in fn_args: arg = arg.replace('_', '-') - completions.append('--{arg}'.format(arg=arg)) + completions.append(f'--{arg}') return completions diff --git a/fire/completion_test.py b/fire/completion_test.py index 5bafc279..c0d5d24f 100644 --- a/fire/completion_test.py +++ b/fire/completion_test.py @@ -33,9 +33,8 @@ def testCompletionBashScript(self): self.assertIn('command', script) self.assertIn('halt', script) - assert_template = '{command})' for last_command in ['command', 'halt']: - self.assertIn(assert_template.format(command=last_command), script) + self.assertIn(f'{last_command})', script) def testCompletionFishScript(self): # A sanity check test to make sure the fish completion script satisfies diff --git a/fire/core.py b/fire/core.py index bce9b641..e4156760 100644 --- a/fire/core.py +++ b/fire/core.py @@ -139,7 +139,7 @@ def Fire(component=None, command=None, name=None, serialize=None): _DisplayError(component_trace) raise FireExit(2, component_trace) if component_trace.show_trace and component_trace.show_help: - output = ['Fire trace:\n{trace}\n'.format(trace=component_trace)] + output = [f'Fire trace:\n{component_trace}\n'] result = component_trace.GetResult() help_text = helptext.HelpText( result, trace=component_trace, verbose=component_trace.verbose) @@ -147,7 +147,7 @@ def Fire(component=None, command=None, name=None, serialize=None): Display(output, out=sys.stderr) raise FireExit(0, component_trace) if component_trace.show_trace: - output = ['Fire trace:\n{trace}'.format(trace=component_trace)] + output = [f'Fire trace:\n{component_trace}'] Display(output, out=sys.stderr) raise FireExit(0, component_trace) if component_trace.show_help: @@ -231,9 +231,9 @@ def _IsHelpShortcut(component_trace, remaining_args): 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=shlex.quote(command)), file=sys.stderr) + command = f'{component_trace.GetCommand()} -- --help' + print(f'INFO: Showing help with the command {shlex.quote(command)}.\n', + file=sys.stderr) return show_help @@ -287,9 +287,9 @@ def _DisplayError(component_trace): show_help = True if show_help: - command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand()) - print('INFO: Showing help with the command {cmd}.\n'.format( - cmd=shlex.quote(command)), file=sys.stderr) + command = f'{component_trace.GetCommand()} -- --help' + print(f'INFO: Showing help with the command {shlex.quote(command)}.\n', + file=sys.stderr) help_text = helptext.HelpText(result, trace=component_trace, verbose=component_trace.verbose) output.append(help_text) @@ -327,14 +327,13 @@ def _DictAsString(result, verbose=False): return '{}' longest_key = max(len(str(key)) for key in result_visible.keys()) - format_string = '{{key:{padding}s}} {{value}}'.format(padding=longest_key + 1) + format_string = f'{{key:{longest_key + 1}s}} {{value}}' lines = [] for key, value in result.items(): if completion.MemberVisible(result, key, value, class_attrs=class_attrs, verbose=verbose): - line = format_string.format(key=str(key) + ':', - value=_OneLineResult(value)) + line = format_string.format(key=f'{key}:', value=_OneLineResult(value)) lines.append(line) return '\n'.join(lines) @@ -348,10 +347,10 @@ def _OneLineResult(result): # TODO(dbieber): Show a small amount of usage information about the function # or module if it fits cleanly on the line. if inspect.isfunction(result): - return ''.format(name=result.__name__) + return f'' if inspect.ismodule(result): - return ''.format(name=result.__name__) + return f'' try: # Don't force conversion to ascii. @@ -890,9 +889,10 @@ def _ParseKeywordArgs(args, fn_spec): if len(matching_fn_args) == 1: keyword = matching_fn_args[0] elif len(matching_fn_args) > 1: - raise FireError("The argument '{}' is ambiguous as it could " - "refer to any of the following arguments: {}".format( - argument, matching_fn_args)) + raise FireError( + f"The argument '{argument}' is ambiguous as it could " + f"refer to any of the following arguments: {matching_fn_args}" + ) # Determine the value. if not keyword: diff --git a/fire/helptext.py b/fire/helptext.py index 1c0cb626..e57eb7d8 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -106,7 +106,7 @@ def _NameSection(component, info, trace=None, verbose=False): LINE_LENGTH) if summary: - text = current_command + ' - ' + summary + text = f'{current_command} - {summary}' else: text = current_command return ('NAME', text) @@ -132,11 +132,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, continuations.append(trace.separator) continuation = ' | '.join(continuations) - synopsis_template = '{current_command} {continuation}' - text = synopsis_template.format( - current_command=current_command, - continuation=continuation) - + text = f'{current_command} {continuation}' return ('SYNOPSIS', text) @@ -243,8 +239,6 @@ def _ArgsAndFlagsSections(info, spec, metadata): if spec.varkw: # Include kwargs documented via :key param: documented_kwargs = [] - flag_string = '--{name}' - short_flag_string = '-{short_name}, --{name}' # add short flags if possible flags = docstring_info.args or [] @@ -253,11 +247,10 @@ def _ArgsAndFlagsSections(info, spec, metadata): for flag in flags: if isinstance(flag, docstrings.KwargInfo): if flag.name[0] in unique_short_flags: - flag_string = short_flag_string.format( - name=flag.name, short_name=flag.name[0] - ) + short_name = flag.name[0] + flag_string = f'-{short_name}, --{flag.name}' else: - flag_string = flag_string.format(name=flag.name) + flag_string = f'--{flag.name}' flag_item = _CreateFlagItem( flag.name, docstring_info, spec, @@ -347,9 +340,9 @@ def _GetArgsAndFlagsString(spec, metadata): for arg in args_with_no_defaults] else: arg_strings = [ - '--{arg}={arg_upper}'.format( - arg=arg, arg_upper=formatting.Underline(arg.upper())) - for arg in args_with_no_defaults] + f'--{arg}={formatting.Underline(arg.upper())}' + for arg in args_with_no_defaults + ] arg_and_flag_strings.extend(arg_strings) # If there are any arguments that are treated as flags: @@ -357,8 +350,8 @@ def _GetArgsAndFlagsString(spec, metadata): arg_and_flag_strings.append('') if spec.varargs: - varargs_string = '[{varargs}]...'.format( - varargs=formatting.Underline(spec.varargs.upper())) + varargs_underlined = formatting.Underline(spec.varargs.upper()) + varargs_string = f'[{varargs_underlined}]...' arg_and_flag_strings.append(varargs_string) return ' '.join(arg_and_flag_strings) @@ -401,7 +394,7 @@ def _GetActionsGroupedByKind(component, verbose=False): if component_len < 10: indexes.Add(name=', '.join(str(x) for x in range(component_len))) else: - indexes.Add(name='0..{max}'.format(max=component_len-1)) + indexes.Add(name=f'0..{component_len-1}') return [groups, commands, values, indexes] @@ -416,10 +409,8 @@ def _GetCurrentCommand(trace=None, include_separators=True): def _CreateOutputSection(name, content): - return """{name} -{content}""".format( - name=formatting.Bold(name), - content=formatting.Indent(content, SECTION_INDENTATION)) + return f"""{formatting.Bold(name)} +{formatting.Indent(content, SECTION_INDENTATION)}""" def _CreateArgItem(arg, docstring_info, spec): @@ -430,7 +421,7 @@ def _CreateArgItem(arg, docstring_info, spec): docstring_info: A docstrings.DocstringInfo namedtuple with information about the containing function's docstring. spec: An instance of fire.inspectutils.FullArgSpec, containing type and - default information about the arguments to a callable. + default information about the arguments to a callable. Returns: A string to be used in constructing the help screen for the function. @@ -445,7 +436,7 @@ def _CreateArgItem(arg, docstring_info, spec): arg_string = formatting.BoldUnderline(arg.upper()) arg_type = _GetArgType(arg, spec) - arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + arg_type = f'Type: {arg_type}' if arg_type else '' available_space = max_str_length - len(arg_type) arg_type = ( formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) @@ -484,14 +475,13 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, description = _GetArgDescription(flag, docstring_info) if not flag_string: - flag_string_template = '--{flag_name}={flag_name_upper}' - flag_string = flag_string_template.format( - flag_name=flag, - flag_name_upper=formatting.Underline(flag.upper())) + flag_name_upper=formatting.Underline(flag.upper()) + flag_string = f'--{flag}={flag_name_upper}' if required: flag_string += ' (required)' if short_arg: - flag_string = '-{short_flag}, '.format(short_flag=flag[0]) + flag_string + short_flag = flag[0] + flag_string = f'-{short_flag}, {flag_string}' arg_type = _GetArgType(flag, spec) arg_default = _GetArgDefault(flag, spec) @@ -499,14 +489,14 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, # We need to handle the case where there is a default of None, but otherwise # the argument has another type. if arg_default == 'None': - arg_type = 'Optional[{}]'.format(arg_type) + arg_type = f'Optional[{arg_type}]' - arg_type = 'Type: {}'.format(arg_type) if arg_type else '' + arg_type = f'Type: {arg_type}' if arg_type else '' available_space = max_str_length - len(arg_type) arg_type = ( formatting.EllipsisTruncate(arg_type, available_space, max_str_length)) - arg_default = 'Default: {}'.format(arg_default) if arg_default else '' + arg_default = f'Default: {arg_default}' if arg_default else '' available_space = max_str_length - len(arg_default) arg_default = ( formatting.EllipsisTruncate(arg_default, available_space, max_str_length)) @@ -567,15 +557,15 @@ def _GetArgDefault(flag, spec): def _CreateItem(name, description, indent=2): if not description: return name - return """{name} -{description}""".format(name=name, - description=formatting.Indent(description, indent)) + description = formatting.Indent(description, indent) + return f"""{name} +{description}""" def _GetArgDescription(name, docstring_info): if docstring_info.args: for arg_in_docstring in docstring_info.args: - if arg_in_docstring.name in (name, '*' + name, '**' + name): + if arg_in_docstring.name in (name, f'*{name}', f'**{name}'): return arg_in_docstring.description return None @@ -621,9 +611,9 @@ def _ValuesUsageDetailsSection(component, values): def _NewChoicesSection(name, choices): + name_formatted = formatting.Bold(formatting.Underline(name)) return _CreateItem( - '{name} is one of the following:'.format( - name=formatting.Bold(formatting.Underline(name))), + f'{name_formatted} is one of the following:', '\n' + '\n\n'.join(choices), indent=1) @@ -639,11 +629,6 @@ def UsageText(component, trace=None, verbose=False): Returns: String suitable for display in an error screen. """ - output_template = """Usage: {continued_command} -{availability_lines} -For detailed information on this command, run: - {help_command}""" - # Get the command so far: if trace: command = trace.GetCommand() @@ -687,15 +672,16 @@ def UsageText(component, trace=None, verbose=False): + '--help' ) - return output_template.format( - continued_command=continued_command, - availability_lines=''.join(availability_lines), - help_command=help_command) + return f"""Usage: {continued_command} +{''.join(availability_lines)} +For detailed information on this command, run: + {help_command}""" def _GetPossibleActionsUsageString(possible_actions): if possible_actions: - return '<{actions}>'.format(actions='|'.join(possible_actions)) + actions_str = '|'.join(possible_actions) + return f'<{actions_str}>' return None @@ -704,7 +690,7 @@ def _UsageAvailabilityLines(actions_grouped_by_kind): for action_group in actions_grouped_by_kind: if action_group.members: availability_line = _CreateAvailabilityLine( - header='available {plural}:'.format(plural=action_group.plural), + header=f'available {action_group.plural}:', items=action_group.names ) availability_lines.append(availability_line) @@ -720,7 +706,7 @@ def _GetCallableUsageItems(spec, metadata): accepts_positional_args = metadata.get(decorators.ACCEPTS_POSITIONAL_ARGS) if not accepts_positional_args: - items = ['--{arg}={upper}'.format(arg=arg, upper=arg.upper()) + items = [f'--{arg}={arg.upper()}' for arg in args_with_no_defaults] else: items = [arg.upper() for arg in args_with_no_defaults] @@ -730,7 +716,7 @@ def _GetCallableUsageItems(spec, metadata): items.append('') if spec.varargs: - items.append('[{varargs}]...'.format(varargs=spec.varargs.upper())) + items.append(f'[{spec.varargs.upper()}]...') return items @@ -745,10 +731,10 @@ def _GetCallableAvailabilityLines(spec): args_with_defaults = spec.args[len(spec.args) - len(spec.defaults):] # TODO(dbieber): Handle args_with_no_defaults if not accepts_positional_args. - optional_flags = [('--' + flag) for flag in itertools.chain( + optional_flags = [f'--{flag}' for flag in itertools.chain( args_with_defaults, _KeywordOnlyArguments(spec, required=False))] required_flags = [ - ('--' + flag) for flag in _KeywordOnlyArguments(spec, required=True) + f'--{flag}' for flag in _KeywordOnlyArguments(spec, required=True) ] # Flags section: diff --git a/fire/interact.py b/fire/interact.py index 7bdeb9a7..eccd3990 100644 --- a/fire/interact.py +++ b/fire/interact.py @@ -65,16 +65,17 @@ def _AvailableString(variables, verbose=False): lists = [ ('Modules', modules), ('Objects', other)] - liststrs = [] + list_strs = [] for name, varlist in lists: if varlist: - liststrs.append( - '{name}: {items}'.format(name=name, items=', '.join(sorted(varlist)))) + items_str = ', '.join(sorted(varlist)) + list_strs.append(f'{name}: {items_str}') + lists_str = '\n'.join(list_strs) return ( 'Fire is starting a Python REPL with the following objects:\n' - '{liststrs}\n' - ).format(liststrs='\n'.join(liststrs)) + f'{lists_str}\n' + ) def _EmbedIPython(variables, argv=None): diff --git a/fire/trace.py b/fire/trace.py index 2145186e..68b48ce5 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -212,10 +212,7 @@ def NeedsSeparator(self): def __str__(self): lines = [] for index, element in enumerate(self.elements): - line = '{index}. {trace_string}'.format( - index=index + 1, - trace_string=element, - ) + line = f'{index + 1}. {element}' lines.append(line) return '\n'.join(lines) @@ -261,7 +258,7 @@ def __init__(self, Args: component: The result of this element of the trace. - action: The type of action (eg instantiating a class) taking place. + action: The type of action (e.g. instantiating a class) taking place. target: (string) The name of the component being acted upon. args: The args consumed by the represented action. filename: The file in which the action is defined, or None if N/A. @@ -301,11 +298,11 @@ def __str__(self): # Format is: {action} "{target}" ({filename}:{lineno}) string = self._action if self._target is not None: - string += ' "{target}"'.format(target=self._target) + string += f' "{self._target}"' if self._filename is not None: path = self._filename if self._lineno is not None: - path += ':{lineno}'.format(lineno=self._lineno) + path += f':{self._lineno}' - string += ' ({path})'.format(path=path) + string += f' ({path})' return string From 93b0e3243b522ae9ab6ebbdc6dcf9a5bb68cc30d Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 23:24:46 -0400 Subject: [PATCH 171/205] Remove six from console, eliminates six entirely (#552) --- fire/console/console_attr.py | 18 ++++++++---------- fire/console/encoding.py | 31 ++++++------------------------- fire/console/files.py | 4 +--- setup.py | 1 - 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/fire/console/console_attr.py b/fire/console/console_attr.py index 815e16b8..c0a3d784 100644 --- a/fire/console/console_attr.py +++ b/fire/console/console_attr.py @@ -100,8 +100,6 @@ from fire.console import encoding as encoding_util from fire.console import text -import six - # TODO: Unify this logic with console.style.mappings class BoxLineCharacters(object): @@ -355,9 +353,9 @@ def ConvertOutputToUnicode(self, buf): Returns: The console output string buf converted to unicode. """ - if isinstance(buf, six.text_type): + if isinstance(buf, str): buf = buf.encode(self._encoding) - return six.text_type(buf, self._encoding, 'replace') + return str(buf, self._encoding, 'replace') def GetBoxLineCharacters(self): """Returns the box/line drawing characters object. @@ -480,7 +478,7 @@ def DisplayWidth(self, buf): Returns: The display width of buf, handling unicode and ANSI controls. """ - if not isinstance(buf, six.string_types): + if not isinstance(buf, str): # Handle non-string objects like Colorizer(). return len(buf) @@ -595,16 +593,16 @@ def __init__(self, string, color, justify=None): self._justify = justify def __eq__(self, other): - return self._string == six.text_type(other) + return self._string == str(other) def __ne__(self, other): return not self == other def __gt__(self, other): - return self._string > six.text_type(other) + return self._string > str(other) def __lt__(self, other): - return self._string < six.text_type(other) + return self._string < str(other) def __ge__(self, other): return not self < other @@ -692,7 +690,7 @@ def GetCharacterDisplayWidth(char): Returns: The monospaced terminal display width of char: either 0, 1, or 2. """ - if not isinstance(char, six.text_type): + if not isinstance(char, str): # Non-unicode chars have width 1. Don't use this function on control chars. return 1 @@ -779,7 +777,7 @@ def EncodeToBytes(data): return data # Coerce to text that will be converted to bytes. - s = six.text_type(data) + s = str(data) try: # Assume the text can be directly converted to bytes (8-bit ascii). diff --git a/fire/console/encoding.py b/fire/console/encoding.py index 41bda634..0a7fedfc 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -22,8 +22,6 @@ import sys -import six - def Encode(string, encoding=None): """Encode the text string to a byte string. @@ -35,18 +33,7 @@ def Encode(string, encoding=None): Returns: str, The binary string. """ - if string is None: - return None - if not six.PY2: - # In Python 3, the environment sets and gets accept and return text strings - # only, and it handles the encoding itself so this is not necessary. - return string - if isinstance(string, six.binary_type): - # Already an encoded byte string, we are done - return string - - encoding = encoding or _GetEncoding() - return string.encode(encoding) + return string def Decode(data, encoding=None): @@ -67,20 +54,13 @@ def Decode(data, encoding=None): return None # First we are going to get the data object to be a text string. - # Don't use six.string_types here because on Python 3 bytes is not considered - # a string type and we want to include that. - if isinstance(data, six.text_type) or isinstance(data, six.binary_type): + if isinstance(data, str) or isinstance(data, bytes): string = data else: # Some non-string type of object. - try: - string = six.text_type(data) - except (TypeError, UnicodeError): - # The string cannot be converted to unicode -- default to str() which will - # catch objects with special __str__ methods. - string = str(data) + string = str(data) - if isinstance(string, six.text_type): + if isinstance(string, str): # Our work is done here. return string @@ -199,7 +179,8 @@ def EncodeEnv(env, encoding=None): encoding = encoding or _GetEncoding() return { Encode(k, encoding=encoding): Encode(v, encoding=encoding) - for k, v in six.iteritems(env)} + for k, v in env.items() + } def _GetEncoding(): diff --git a/fire/console/files.py b/fire/console/files.py index 69970f43..97222c3d 100644 --- a/fire/console/files.py +++ b/fire/console/files.py @@ -24,8 +24,6 @@ from fire.console import encoding as encoding_util from fire.console import platforms -import six - def _GetSystemPath(): """Returns properly encoded system PATH variable string.""" @@ -48,7 +46,7 @@ def _FindExecutableOnPath(executable, path, pathext): ValueError: invalid input. """ - if isinstance(pathext, six.string_types): + if isinstance(pathext, str): raise ValueError('_FindExecutableOnPath(..., pathext=\'{0}\') failed ' 'because pathext must be an iterable of strings, but got ' 'a string.'.format(pathext)) diff --git a/setup.py b/setup.py index 53f3381a..82073be4 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ A library for automatically generating command line interfaces.""".strip() DEPENDENCIES = [ - 'six', 'termcolor', ] From 5b2dadd7f3912bf8f05e7f1c381631ef5c14cada Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 19 Sep 2024 23:47:19 -0400 Subject: [PATCH 172/205] Move asyncio imports and update docs (#553) --- docs/guide.md | 19 +++++++++++++++++++ fire/core.py | 3 +-- fire/inspectutils.py | 3 +-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index cdc3b2d0..444a76ff 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -589,6 +589,25 @@ default values that you don't want to specify. It is also important to remember to change the separator if you want to pass `-` as an argument. +##### Async Functions + +Fire supports calling async functions too. Here's a simple example. + +```python +import asyncio + +async def count_to_ten(): + for i in range(1, 11): + await asyncio.sleep(1) + print(i) + +if __name__ == '__main__': + fire.Fire(count_to_ten) +``` + +Whenever fire encounters a coroutine function, it runs it, blocking until it completes. + + ### Argument Parsing The types of the arguments are determined by their values, rather than by the diff --git a/fire/core.py b/fire/core.py index e4156760..6cd1907e 100644 --- a/fire/core.py +++ b/fire/core.py @@ -49,6 +49,7 @@ def main(argv): --trace: Get the Fire Trace for the command. """ +import asyncio import inspect import json import os @@ -68,8 +69,6 @@ def main(argv): from fire import value_types from fire.console import console_io -import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error - def Fire(component=None, command=None, name=None, serialize=None): """This function, Fire, is the main entrypoint for Python Fire. diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 0d0b048d..a3ae7c27 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -14,14 +14,13 @@ """Inspection utility functions for Python Fire.""" +import asyncio import inspect import sys import types from fire import docstrings -import asyncio # pylint: disable=import-error,g-import-not-at-top # pytype: disable=import-error - class FullArgSpec(object): """The arguments of a function, as in Python 3's inspect.FullArgSpec.""" From d3204373c4bba38a09db92f910d048222b8d6f0f Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 20 Sep 2024 07:20:04 -0700 Subject: [PATCH 173/205] Include Python 3.13 in github actions (#554) * Include Python 3.13 in github actions list * Include version in supported versions list --- .github/workflows/build.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6649201..63c87edf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] steps: # Checkout the repo. diff --git a/setup.py b/setup.py index 82073be4..beb367cf 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Operating System :: OS Independent', 'Operating System :: POSIX', From 36a56c0a777d874f30e39412b2877ab171118d54 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 21 Sep 2024 08:04:39 -0700 Subject: [PATCH 174/205] Continue upgrade of codebase to Python 3 (#556) Obtained with pyupgrade, reverting any changes that remove a module we still depend on. `uv run pyupgrade **/**.py --py3-plus` --- fire/__main__.py | 6 ++-- fire/core.py | 2 +- fire/decorators_test.py | 12 +++---- fire/helptext.py | 2 +- fire/helptext_test.py | 2 +- fire/inspectutils.py | 6 ++-- fire/main_test.py | 2 +- fire/parser_fuzz_test.py | 2 +- fire/test_components.py | 66 ++++++++++++++++++------------------- fire/test_components_py3.py | 10 +++--- fire/trace.py | 4 +-- 11 files changed, 57 insertions(+), 57 deletions(-) diff --git a/fire/__main__.py b/fire/__main__.py index 11fb1b42..140b4a76 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -54,14 +54,14 @@ def import_from_file_path(path): """ if not os.path.exists(path): - raise IOError('Given file path does not exist.') + raise OSError('Given file path does not exist.') module_name = os.path.basename(path) spec = util.spec_from_file_location(module_name, path) if spec is None: - raise IOError('Unable to load module from specified path.') + raise OSError('Unable to load module from specified path.') module = util.module_from_spec(spec) # pylint: disable=no-member spec.loader.exec_module(module) # pytype: disable=attribute-error @@ -104,7 +104,7 @@ def import_module(module_or_filename): return import_from_file_path(module_or_filename) if os.path.sep in module_or_filename: # Use / to detect if it was a filename. - raise IOError('Fire was passed a filename which could not be found.') + raise OSError('Fire was passed a filename which could not be found.') return import_from_module_name(module_or_filename) # Assume it's a module. diff --git a/fire/core.py b/fire/core.py index 6cd1907e..26a25753 100644 --- a/fire/core.py +++ b/fire/core.py @@ -199,7 +199,7 @@ def __init__(self, code, component_trace): code: (int) Exit code for the Fire CLI. component_trace: (FireTrace) The trace for the Fire command. """ - super(FireExit, self).__init__(code) + super().__init__(code) self.trace = component_trace diff --git a/fire/decorators_test.py b/fire/decorators_test.py index a316b79f..9988743c 100644 --- a/fire/decorators_test.py +++ b/fire/decorators_test.py @@ -19,7 +19,7 @@ from fire import testutils -class NoDefaults(object): +class NoDefaults: """A class for testing decorated functions without default values.""" @decorators.SetParseFns(count=int) @@ -40,7 +40,7 @@ def double(count): return 2 * count -class WithDefaults(object): +class WithDefaults: @decorators.SetParseFns(float) def example1(self, arg1=10): @@ -51,14 +51,14 @@ def example2(self, arg1=10): return arg1, type(arg1) -class MixedArguments(object): +class MixedArguments: @decorators.SetParseFns(float, arg2=str) def example3(self, arg1, arg2): return arg1, arg2 -class PartialParseFn(object): +class PartialParseFn: @decorators.SetParseFns(arg1=str) def example4(self, arg1, arg2): @@ -69,7 +69,7 @@ def example5(self, arg1, arg2): return arg1, arg2 -class WithKwargs(object): +class WithKwargs: @decorators.SetParseFns(mode=str, count=int) def example6(self, **kwargs): @@ -79,7 +79,7 @@ def example6(self, **kwargs): ) -class WithVarArgs(object): +class WithVarArgs: @decorators.SetParseFn(str) def example7(self, arg1, arg2=None, *varargs, **kwargs): # pylint: disable=keyword-arg-before-vararg diff --git a/fire/helptext.py b/fire/helptext.py index e57eb7d8..9b578fac 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -767,7 +767,7 @@ def _CreateAvailabilityLine(header, items, return indented_header + indented_items_text[len(indented_header):] + '\n' -class ActionGroup(object): +class ActionGroup: """A group of actions of the same kind.""" def __init__(self, name, plural): diff --git a/fire/helptext_test.py b/fire/helptext_test.py index 4d35dc0a..d1a3f368 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -27,7 +27,7 @@ class HelpTest(testutils.BaseTestCase): def setUp(self): - super(HelpTest, self).setUp() + super().setUp() os.environ['ANSI_COLORS_DISABLED'] = '1' def testHelpTextNoDefaults(self): diff --git a/fire/inspectutils.py b/fire/inspectutils.py index a3ae7c27..d1438972 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -22,7 +22,7 @@ from fire import docstrings -class FullArgSpec(object): +class FullArgSpec: """The arguments of a function, as in Python 3's inspect.FullArgSpec.""" def __init__(self, args=None, varargs=None, varkw=None, defaults=None, @@ -229,7 +229,7 @@ def GetFileAndLine(component): try: unused_code, lineindex = inspect.findsource(component) lineno = lineindex + 1 - except (IOError, IndexError): + except (OSError, IndexError): lineno = None return filename, lineno @@ -268,7 +268,7 @@ def Info(component): try: unused_code, lineindex = inspect.findsource(component) info['line'] = lineindex + 1 - except (TypeError, IOError): + except (TypeError, OSError): info['line'] = None if 'docstring' in info: diff --git a/fire/main_test.py b/fire/main_test.py index a0184620..a2723347 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -43,7 +43,7 @@ class MainModuleFileTest(testutils.BaseTestCase): """Tests to verify correct import behavior for file executables.""" def setUp(self): - super(MainModuleFileTest, self).setUp() + super().setUp() self.file = tempfile.NamedTemporaryFile(suffix='.py') # pylint: disable=consider-using-with self.file.write(b'class Foo:\n def double(self, n):\n return 2 * n\n') self.file.flush() diff --git a/fire/parser_fuzz_test.py b/fire/parser_fuzz_test.py index 9739ec4e..10f497cf 100644 --- a/fire/parser_fuzz_test.py +++ b/fire/parser_fuzz_test.py @@ -53,7 +53,7 @@ def testDefaultParseValueFuzz(self, value): result = parser.DefaultParseValue(value) except TypeError: # It's OK to get a TypeError if the string has the null character. - if u'\x00' in value: + if '\x00' in value: return raise except MemoryError: diff --git a/fire/test_components.py b/fire/test_components.py index eb3a9e24..2dc4e0cc 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -43,7 +43,7 @@ def function_with_help(help=True): # pylint: disable=redefined-builtin return help -class Empty(object): +class Empty: pass @@ -51,20 +51,20 @@ class OldStyleEmpty: # pylint: disable=old-style-class,no-init pass -class WithInit(object): +class WithInit: def __init__(self): pass -class ErrorInConstructor(object): +class ErrorInConstructor: def __init__(self, value='value'): self.value = value raise ValueError('Error in constructor') -class WithHelpArg(object): +class WithHelpArg: """Test class for testing when class has a help= arg.""" def __init__(self, help=True): # pylint: disable=redefined-builtin @@ -72,7 +72,7 @@ def __init__(self, help=True): # pylint: disable=redefined-builtin self.dictionary = {'__help': 'help in a dict'} -class NoDefaults(object): +class NoDefaults: def double(self, count): return 2 * count @@ -81,7 +81,7 @@ def triple(self, count): return 3 * count -class WithDefaults(object): +class WithDefaults: """Class with functions that have default arguments.""" def double(self, count=0): @@ -115,7 +115,7 @@ def triple(self, count=0): return 3 * count -class MixedDefaults(object): +class MixedDefaults: def ten(self): return 10 @@ -127,7 +127,7 @@ def identity(self, alpha, beta='0'): return alpha, beta -class SimilarArgNames(object): +class SimilarArgNames: def identity(self, bool_one=False, bool_two=False): return bool_one, bool_two @@ -136,13 +136,13 @@ def identity2(self, a=None, alpha=None): return a, alpha -class CapitalizedArgNames(object): +class CapitalizedArgNames: def sum(self, Delta=1.0, Gamma=2.0): # pylint: disable=invalid-name return Delta + Gamma -class Annotations(object): +class Annotations: def double(self, count=0): return 2 * count @@ -154,7 +154,7 @@ def triple(self, count=0): triple.__annotations__ = {'count': float} -class TypedProperties(object): +class TypedProperties: """Test class for testing Python Fire with properties of various types.""" def __init__(self): @@ -173,7 +173,7 @@ def __init__(self): self.gamma = 'myexcitingstring' -class VarArgs(object): +class VarArgs: """Test class for testing Python Fire with a property with varargs.""" def cumsums(self, *items): @@ -191,7 +191,7 @@ def varchars(self, alpha=0, beta=0, *chars): # pylint: disable=keyword-arg-befo return alpha, beta, ''.join(chars) -class Underscores(object): +class Underscores: def __init__(self): self.underscore_example = 'fish fingers' @@ -200,20 +200,20 @@ def underscore_function(self, underscore_arg): return underscore_arg -class BoolConverter(object): +class BoolConverter: def as_bool(self, arg=False): return bool(arg) -class ReturnsObj(object): +class ReturnsObj: def get_obj(self, *items): del items # Unused return BoolConverter() -class NumberDefaults(object): +class NumberDefaults: def reciprocal(self, divisor=10.0): return 1.0 / divisor @@ -222,7 +222,7 @@ def integer_reciprocal(self, divisor=10): return 1.0 / divisor -class InstanceVars(object): +class InstanceVars: def __init__(self, arg1, arg2): self.arg1 = arg1 @@ -232,7 +232,7 @@ def run(self, arg1, arg2): return (self.arg1, self.arg2, arg1, arg2) -class Kwargs(object): +class Kwargs: def props(self, **kwargs): return kwargs @@ -244,13 +244,13 @@ def run(self, positional, named=None, **kwargs): return (positional, named, kwargs) -class ErrorRaiser(object): +class ErrorRaiser: def fail(self): raise ValueError('This error is part of a test.') -class NonComparable(object): +class NonComparable: def __eq__(self, other): raise ValueError('Instances of this class cannot be compared.') @@ -259,7 +259,7 @@ def __ne__(self, other): raise ValueError('Instances of this class cannot be compared.') -class EmptyDictOutput(object): +class EmptyDictOutput: def totally_empty(self): return {} @@ -268,7 +268,7 @@ def nothing_printable(self): return {'__do_not_print_me': 1} -class CircularReference(object): +class CircularReference: def create(self): x = {} @@ -276,7 +276,7 @@ def create(self): return x -class OrderedDictionary(object): +class OrderedDictionary: def empty(self): return collections.OrderedDict() @@ -288,7 +288,7 @@ def non_empty(self): return ordered_dict -class NamedTuple(object): +class NamedTuple: """Functions returning named tuples used for testing.""" def point(self): @@ -304,7 +304,7 @@ def matching_names(self): return Point(x='x', y='y') -class CallableWithPositionalArgs(object): +class CallableWithPositionalArgs: """Test class for supporting callable.""" TEST = 1 @@ -326,12 +326,12 @@ def coordinate_sum(self): return self.x + self.y -class CallableWithKeywordArgument(object): +class CallableWithKeywordArgument: """Test class for supporting callable.""" def __call__(self, **kwargs): for key, value in kwargs.items(): - print('%s: %s' % (key, value)) + print('{}: {}'.format(key, value)) def print_msg(self, msg): print(msg) @@ -340,7 +340,7 @@ def print_msg(self, msg): CALLABLE_WITH_KEYWORD_ARGUMENT = CallableWithKeywordArgument() -class ClassWithDocstring(object): +class ClassWithDocstring: """Test class for testing help text output. This is some detail description of this test class. @@ -363,7 +363,7 @@ def print_msg(self, msg=None): print(msg) -class ClassWithMultilineDocstring(object): +class ClassWithMultilineDocstring: """Test class for testing help text output with multiline docstring. This is a test class that has a long docstring description that spans across @@ -413,7 +413,7 @@ class Color(enum.Enum): BLUE = 3 -class HasStaticAndClassMethods(object): +class HasStaticAndClassMethods: """A class with a static method and a class method.""" CLASS_STATE = 1 @@ -467,7 +467,7 @@ def fn_with_code_in_docstring(): return True -class BinaryCanvas(object): +class BinaryCanvas: """A canvas with which to make binary art, one bit at a time.""" def __init__(self, size=10): @@ -500,7 +500,7 @@ def set(self, value): return self -class DefaultMethod(object): +class DefaultMethod: def double(self, number): return 2 * number @@ -511,7 +511,7 @@ def _missing(): return _missing -class InvalidProperty(object): +class InvalidProperty: def double(self, number): return 2 * number diff --git a/fire/test_components_py3.py b/fire/test_components_py3.py index 17fb932c..192302d3 100644 --- a/fire/test_components_py3.py +++ b/fire/test_components_py3.py @@ -31,7 +31,7 @@ def identity(self, *, alpha, beta='0'): return alpha, beta -class KeywordOnly(object): +class KeywordOnly: def double(self, *, count): return count * 2 @@ -43,7 +43,7 @@ def with_default(self, *, x="x"): print("x: " + x) -class LruCacheDecoratedMethod(object): +class LruCacheDecoratedMethod: @functools.lru_cache() def lru_cache_in_class(self, arg1): @@ -55,13 +55,13 @@ def lru_cache_decorated(arg1): return arg1 -class WithAsyncio(object): +class WithAsyncio: async def double(self, count=0): return 2 * count -class WithTypes(object): +class WithTypes: """Class with functions that have default arguments and types.""" def double(self, count: float) -> float: @@ -83,7 +83,7 @@ def long_type( return long_obj -class WithDefaultsAndTypes(object): +class WithDefaultsAndTypes: """Class with functions that have default arguments and types.""" def double(self, count: float = 0) -> float: diff --git a/fire/trace.py b/fire/trace.py index 68b48ce5..3a75cc9c 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -38,7 +38,7 @@ INTERACTIVE_MODE = 'Entered interactive mode' -class FireTrace(object): +class FireTrace: """A FireTrace represents the steps taken during a single Fire execution. A FireTrace consists of a sequence of FireTraceElement objects. Each element @@ -238,7 +238,7 @@ def NeedsSeparatingHyphenHyphen(self, flag='help'): or flag in spec.kwonlyargs) -class FireTraceElement(object): +class FireTraceElement: """A FireTraceElement represents a single step taken by a Fire execution. Examples of a FireTraceElement are the instantiation of a class or the From 8227364f113fcaf8661290fa3bb0c79741ff3be7 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Sat, 21 Sep 2024 17:09:24 +0200 Subject: [PATCH 175/205] Update required Python 3.7 in `setup.py` (#555) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index beb367cf..8d4a381b 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ keywords='command line interface cli python fire interactive bash tool', + requires_python='>=3.7', packages=['fire', 'fire.console'], install_requires=DEPENDENCIES, From 1c43c36174feb7020e7c5a5c53f3a47bc8e368c8 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 21 Sep 2024 08:12:09 -0700 Subject: [PATCH 176/205] Bump version number in __init__ to setup.py --- fire/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/__init__.py b/fire/__init__.py index 742b03ac..9ff696d3 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -17,4 +17,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.6.0' +__version__ = '0.7.0' From efcf60f7f8202d9887b9da16e4ff81a554b9d023 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 21 Sep 2024 08:45:04 -0700 Subject: [PATCH 177/205] Lint improvements and type safety (#558) * Clean up of lint errors * Always return an element when requested --- fire/console/encoding.py | 1 + fire/decorators.py | 2 +- fire/formatting_windows.py | 2 +- fire/helptext.py | 2 +- fire/helptext_test.py | 1 - fire/test_components.py | 3 ++- fire/trace.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fire/console/encoding.py b/fire/console/encoding.py index 0a7fedfc..3ce30cb5 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -33,6 +33,7 @@ def Encode(string, encoding=None): Returns: str, The binary string. """ + del encoding # Unused. return string diff --git a/fire/decorators.py b/fire/decorators.py index eb5b0d20..2758b0aa 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -107,5 +107,5 @@ def GetMetadata(fn): def GetParseFns(fn): # type: (...) -> dict metadata = GetMetadata(fn) - default = {"default": None, "positional": [], "named": {}} + default = {'default': None, 'positional': [], 'named': {}} return metadata.get(FIRE_PARSE_FNS, default) diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index f8241eaa..cee6f393 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -31,7 +31,7 @@ def initialize_or_disable(): """Enables ANSI processing on Windows or disables it as needed.""" if HAS_COLORAMA: wrap = True - if (hasattr(sys.stdout, "isatty") + if (hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() and platform.release() == '10'): # Enables native ANSI sequences in console. diff --git a/fire/helptext.py b/fire/helptext.py index 9b578fac..318d6276 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -475,7 +475,7 @@ def _CreateFlagItem(flag, docstring_info, spec, required=False, description = _GetArgDescription(flag, docstring_info) if not flag_string: - flag_name_upper=formatting.Underline(flag.upper()) + flag_name_upper = formatting.Underline(flag.upper()) flag_string = f'--{flag}={flag_name_upper}' if required: flag_string += ' (required)' diff --git a/fire/helptext_test.py b/fire/helptext_test.py index d1a3f368..aeff5240 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -428,7 +428,6 @@ def testHelpTextMultipleKeywoardArgumentsWithShortArgs(self): self.assertIn('\n --late', help_screen) - class UsageTest(testutils.BaseTestCase): def testUsageOutput(self): diff --git a/fire/test_components.py b/fire/test_components.py index 2dc4e0cc..887a0dc6 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -554,7 +554,7 @@ def fn_with_kwarg_and_defaults(arg1, arg2, opt=True, **kwargs): """ del arg1, arg2, opt return kwargs.get('arg3') -# pylint: enable=g-doc-args,g-doc-return-or-yield + def fn_with_multiple_defaults(first='first', last='last', late='late'): """Function with kwarg and defaults. @@ -565,3 +565,4 @@ def fn_with_multiple_defaults(first='first', last='last', late='late'): """ del last, late return first +# pylint: enable=g-doc-args,g-doc-return-or-yield diff --git a/fire/trace.py b/fire/trace.py index 3a75cc9c..4a6d4776 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -77,7 +77,7 @@ def GetLastHealthyElement(self): for element in reversed(self.elements): if not element.HasError(): return element - return None + return self.elements[0] # The initial element is always healthy. def HasError(self): """Returns whether the Fire execution encountered a Fire usage error.""" From a0cb1ca3c2697ea7f5f1f7314353f1c7e920088f Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:00:15 +0200 Subject: [PATCH 178/205] Expand build matrix to include mac (#490) * Expand build matrix to include mac (#490) --------- Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Co-authored-by: David Bieber --- .github/workflows/build.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63c87edf..59b0a4ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,18 +2,23 @@ name: Python Fire on: push: - branches: - - master + branches: ["master"] pull_request: - branches: - - master + branches: ["master"] + +defaults: + run: + shell: bash jobs: build: - runs-on: ubuntu-20.04 + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] + os: ["macos-latest", "ubuntu-latest"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] + include: + - {os: "ubuntu-20.04", python-version: "3.7"} steps: # Checkout the repo. @@ -28,7 +33,6 @@ jobs: # Build Python Fire using the build.sh script. - name: Run build script - shell: bash run: ./.github/scripts/build.sh env: PYTHON_VERSION: ${{ matrix.python-version }} From 90b7f824f2e760e6363b0d10c52b1940346a0fa6 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sun, 22 Sep 2024 09:14:50 -0700 Subject: [PATCH 179/205] Replace Python 2 type hints with real type annotations (#559) * Replace Python 2 type hints with real type annotations --- CONTRIBUTING.md | 3 --- fire/decorators.py | 7 +++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index baae1a6e..b5d67c96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,9 @@ In addition, the project follows a convention of: - Maximum line length: 80 characters - Indentation: 2 spaces (4 for line continuation) - PascalCase for function and method names. -- No type hints, as described in [PEP 484], to maintain compatibility with -Python versions < 3.5. - Single quotes around strings, three double quotes around docstrings. [Google Python Style Guide]: http://google.github.io/styleguide/pyguide.html -[PEP 484]: https://www.python.org/dev/peps/pep-0484 ## Testing diff --git a/fire/decorators.py b/fire/decorators.py index 2758b0aa..914b1de6 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -18,6 +18,7 @@ command line arguments to client code. """ +from typing import Any, Dict import inspect FIRE_METADATA = 'FIRE_METADATA' @@ -80,8 +81,7 @@ def _SetMetadata(fn, attribute, value): setattr(fn, FIRE_METADATA, metadata) -def GetMetadata(fn): - # type: (...) -> dict +def GetMetadata(fn) -> Dict[str, Any]: """Gets metadata attached to the function `fn` as an attribute. Args: @@ -104,8 +104,7 @@ def GetMetadata(fn): return default -def GetParseFns(fn): - # type: (...) -> dict +def GetParseFns(fn) -> Dict[str, Any]: metadata = GetMetadata(fn) default = {'default': None, 'positional': [], 'named': {}} return metadata.get(FIRE_PARSE_FNS, default) From 8feb04a5936ba7b94e96430cc04f6f4b9ba8170c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:34:19 -0400 Subject: [PATCH 180/205] Update pylint requirement from <3.2.8 to <3.3.2 in /.github/scripts (#562) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.1) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index a5648989..0b32ac05 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,6 +1,6 @@ setuptools <=75.1.0 pip -pylint <3.2.8 +pylint <3.3.2 pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 From 0aff6b9452080a1fe53bf8537b5e71bd04986c24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 13:56:00 -0400 Subject: [PATCH 181/205] Update hypothesis requirement in /.github/scripts (#566) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.116.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 0b32ac05..5c947b3e 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,5 +5,5 @@ pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.5.0 -hypothesis <6.113.0 +hypothesis <6.117.0 levenshtein <=0.26.0 From dfa1071b7a9aee0ca1538dc4e08236c0c5dedda4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:09:07 -0400 Subject: [PATCH 182/205] Update termcolor requirement from <2.5.0 to <2.6.0 in /.github/scripts (#569) Updates the requirements on [termcolor](https://github.com/termcolor/termcolor) to permit the latest version. - [Release notes](https://github.com/termcolor/termcolor/releases) - [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md) - [Commits](https://github.com/termcolor/termcolor/compare/0.1...2.5.0) --- updated-dependencies: - dependency-name: termcolor dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 5c947b3e..6147fcbb 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -4,6 +4,6 @@ pylint <3.3.2 pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 -termcolor <2.5.0 +termcolor <2.6.0 hypothesis <6.117.0 levenshtein <=0.26.0 From 98d7fbce9c93a6b0c3b209a8ed1144cd233decc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:14:29 -0400 Subject: [PATCH 183/205] Update setuptools requirement in /.github/scripts (#567) Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.3.0) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 6147fcbb..6df54e28 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,4 +1,4 @@ -setuptools <=75.1.0 +setuptools <=75.3.0 pip pylint <3.3.2 pytest <=8.3.3 From c3b4474ea42c2f5330ee28fea7a9fc31e4c59451 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 18:49:46 -0400 Subject: [PATCH 184/205] Update levenshtein requirement in /.github/scripts (#568) Updates the requirements on [levenshtein](https://github.com/rapidfuzz/Levenshtein) to permit the latest version. - [Release notes](https://github.com/rapidfuzz/Levenshtein/releases) - [Changelog](https://github.com/rapidfuzz/Levenshtein/blob/main/HISTORY.md) - [Commits](https://github.com/rapidfuzz/Levenshtein/compare/v0.13.0...v0.26.1) --- updated-dependencies: - dependency-name: levenshtein dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 6df54e28..cf8a3420 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -6,4 +6,4 @@ pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.6.0 hypothesis <6.117.0 -levenshtein <=0.26.0 +levenshtein <=0.26.1 From deb25efee883191f9dac1cec579d3f96f1e32226 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:52:02 -0500 Subject: [PATCH 185/205] Update setuptools requirement in /.github/scripts (#573) Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/0.6...v75.6.0) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index cf8a3420..0480a001 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,4 +1,4 @@ -setuptools <=75.3.0 +setuptools <=75.6.0 pip pylint <3.3.2 pytest <=8.3.3 From 6cf45c663075c96b20dd0dfa733c2374545a4ad6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:53:58 -0500 Subject: [PATCH 186/205] Update hypothesis requirement in /.github/scripts (#574) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.122.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 0480a001..958c6248 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,5 +5,5 @@ pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.6.0 -hypothesis <6.117.0 +hypothesis <6.123.0 levenshtein <=0.26.1 From 2e0867d3371db9db6e95fad7f82d58ccb894d94c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sun, 23 Mar 2025 14:43:44 -0400 Subject: [PATCH 187/205] Use Neutral theme for Inspector (#588) * Use Neutral theme for Inspector * Catch when theme_name not available --- fire/inspectutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index d1438972..06c30ef1 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -256,7 +256,10 @@ def Info(component): """ try: from IPython.core import oinspect # pylint: disable=import-outside-toplevel,g-import-not-at-top - inspector = oinspect.Inspector() + try: + inspector = oinspect.Inspector(theme_name="Neutral") + except TypeError: # Only recent versions of IPython support theme_name. + inspector = oinspect.Inspector() info = inspector.info(component) # IPython's oinspect.Inspector.info may return '' From 45152e18255e5c5803f3805604eb738c50befeff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:34:56 -0400 Subject: [PATCH 188/205] Update pylint requirement from <3.3.2 to <3.3.5 in /.github/scripts (#581) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 958c6248..5810abf5 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,6 +1,6 @@ setuptools <=75.6.0 pip -pylint <3.3.2 +pylint <3.3.5 pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 From 525708c3d7bcfc36a71e23694f09d0b587a7bf72 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Mon, 14 Apr 2025 14:40:30 -0400 Subject: [PATCH 189/205] Use lowercase neutral instead of upper (#596) --- fire/inspectutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 06c30ef1..d9c62ca7 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -257,7 +257,7 @@ def Info(component): try: from IPython.core import oinspect # pylint: disable=import-outside-toplevel,g-import-not-at-top try: - inspector = oinspect.Inspector(theme_name="Neutral") + inspector = oinspect.Inspector(theme_name="neutral") except TypeError: # Only recent versions of IPython support theme_name. inspector = oinspect.Inspector() info = inspector.info(component) From c5ab602240a160902986e48db8980d59338be944 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:40:45 -0400 Subject: [PATCH 190/205] Update hypothesis requirement in /.github/scripts (#594) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.130.6) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.130.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 5810abf5..b922f04a 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,5 +5,5 @@ pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.6.0 -hypothesis <6.123.0 +hypothesis <6.131.0 levenshtein <=0.26.1 From 8527235d18835223dad5055e29d50664ab5bfb2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:41:02 -0400 Subject: [PATCH 191/205] Update setuptools requirement in /.github/scripts (#593) Updates the requirements on [setuptools](https://github.com/pypa/setuptools) to permit the latest version. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/0.6...v78.1.0) --- updated-dependencies: - dependency-name: setuptools dependency-version: 78.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index b922f04a..8db34c71 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,4 +1,4 @@ -setuptools <=75.6.0 +setuptools <=78.1.0 pip pylint <3.3.5 pytest <=8.3.3 From fb01c7c619eda3107c7e32c42370573f6f63f33c Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 21 May 2025 12:23:08 -0700 Subject: [PATCH 192/205] Call inspectutils.GetClassAttrsDict on component, not None (#606) * inspectutils.GetClassAttrsDict on component, not None * Remove ubuntu-20.04 in favor of ubuntu-22.04 for Python 3.7 --- .github/workflows/build.yml | 2 +- fire/completion.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59b0a4ba..75a687f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: os: ["macos-latest", "ubuntu-latest"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] include: - - {os: "ubuntu-20.04", python-version: "3.7"} + - {os: "ubuntu-22.04", python-version: "3.7"} steps: # Checkout the repo. diff --git a/fire/completion.py b/fire/completion.py index 625e9d86..1597d464 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -321,7 +321,7 @@ def MemberVisible(component, name, member, class_attrs=None, verbose=False): if inspect.isclass(component): # If class_attrs has not been provided, compute it. if class_attrs is None: - class_attrs = inspectutils.GetClassAttrsDict(class_attrs) or {} + class_attrs = inspectutils.GetClassAttrsDict(component) or {} class_attr = class_attrs.get(name) if class_attr: # Methods and properties should only be accessible on instantiated From 51974c67bf72ac649ed28015d960884712bcbc0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 12:23:37 -0700 Subject: [PATCH 193/205] Update pylint requirement from <3.3.5 to <3.3.7 in /.github/scripts (#591) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.6) --- updated-dependencies: - dependency-name: pylint dependency-version: 3.3.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 8db34c71..82b1be4a 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,6 +1,6 @@ setuptools <=78.1.0 pip -pylint <3.3.5 +pylint <3.3.7 pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 From dba7e1d0da014e555d174225fdf5ab4c4574b18b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 10:55:56 -0400 Subject: [PATCH 194/205] Update hypothesis requirement in /.github/scripts (#608) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.132.0) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.132.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 82b1be4a..613c4da0 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -5,5 +5,5 @@ pytest <=8.3.3 pytest-pylint <=1.1.2 pytest-runner <7.0.0 termcolor <2.6.0 -hypothesis <6.131.0 +hypothesis <6.133.0 levenshtein <=0.26.1 From 2e6f8d2b248411fb4bbfb7fbf3701ee96c0e9a61 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 18 Jul 2025 21:23:40 -0400 Subject: [PATCH 195/205] Bump version to 0.7.1 (#609) * Bump version to 0.7.1 * Bump dependency versions --- .github/scripts/requirements.txt | 8 ++++---- fire/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt index 613c4da0..882dd440 100644 --- a/.github/scripts/requirements.txt +++ b/.github/scripts/requirements.txt @@ -1,9 +1,9 @@ -setuptools <=78.1.0 +setuptools <=80.9.0 pip pylint <3.3.7 -pytest <=8.3.3 +pytest <=8.3.5 pytest-pylint <=1.1.2 pytest-runner <7.0.0 -termcolor <2.6.0 +termcolor <3.2.0 hypothesis <6.133.0 -levenshtein <=0.26.1 +levenshtein <=0.27.1 diff --git a/fire/__init__.py b/fire/__init__.py index 9ff696d3..b1470692 100644 --- a/fire/__init__.py +++ b/fire/__init__.py @@ -17,4 +17,4 @@ from fire.core import Fire __all__ = ['Fire'] -__version__ = '0.7.0' +__version__ = '0.7.1' diff --git a/setup.py b/setup.py index 8d4a381b..23b7b472 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ 'levenshtein', ] -VERSION = '0.7.0' +VERSION = '0.7.1' URL = 'https://github.com/google/python-fire' setup( From d33056cb32f217c57b432040484901f34b9f5411 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 18 Jul 2025 22:27:20 -0400 Subject: [PATCH 196/205] Move to pyproject.toml (#613) * Migrate from setup.py and setup.cfg to pyproject.toml * Point dependabot at the pyproject file --- .github/dependabot.yml | 2 +- .github/scripts/build.sh | 3 +- .github/scripts/requirements.txt | 9 ---- pyproject.toml | 69 ++++++++++++++++++++++++++ requirements.txt | 1 - setup.cfg | 10 ---- setup.py | 85 -------------------------------- 7 files changed, 71 insertions(+), 108 deletions(-) delete mode 100644 .github/scripts/requirements.txt create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ba1b7f19..8be46672 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ version: 2 updates: # Enable version updates for python - package-ecosystem: "pip" - directory: ".github/scripts/" + directory: "/" schedule: interval: "monthly" labels: ["dependabot"] diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 111257ae..7f5cf491 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -19,8 +19,7 @@ set -e PYTHON_VERSION=${PYTHON_VERSION:-3.7} -pip install -U -r .github/scripts/requirements.txt -python setup.py develop +pip install -e .[test] python -m pytest # Run the tests without IPython. pip install ipython python -m pytest # Now run the tests with IPython. diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt deleted file mode 100644 index 882dd440..00000000 --- a/.github/scripts/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -setuptools <=80.9.0 -pip -pylint <3.3.7 -pytest <=8.3.5 -pytest-pylint <=1.1.2 -pytest-runner <7.0.0 -termcolor <3.2.0 -hypothesis <6.133.0 -levenshtein <=0.27.1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6a6ba63e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "fire" +version = "0.7.1" +description = "A library for automatically generating command line interfaces." +readme = "README.md" +license = {text = "Apache-2.0"} +authors = [ + {name = "David Bieber", email = "dbieber@google.com"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", +] +keywords = ["command", "line", "interface", "cli", "python", "fire", "interactive", "bash", "tool"] +requires-python = ">=3.7" +dependencies = [ + "termcolor", +] + +[project.urls] +Homepage = "https://github.com/google/python-fire" +Repository = "https://github.com/google/python-fire" + +[project.optional-dependencies] +test = [ + "setuptools<=80.9.0", + "pip", + "pylint<3.3.7", + "pytest<=8.3.5", + "pytest-pylint<=1.1.2", + "pytest-runner<7.0.0", + "termcolor<3.2.0", + "hypothesis<6.133.0", + "levenshtein<=0.27.1", +] + +[tool.setuptools.packages.find] +include = ["fire*"] + +[tool.setuptools.package-data] +fire = ["console/*"] + +[tool.pytest.ini_options] +addopts = [ + "--ignore=fire/test_components_py3.py", + "--ignore=fire/parser_fuzz_test.py" +] + +[tool.pytype] +inputs = "." +output = ".pytype" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9c558e35..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -. diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ed53d83b..00000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[aliases] -test = pytest - -[tool:pytest] -addopts = --ignore=fire/test_components_py3.py - --ignore=fire/parser_fuzz_test.py - -[pytype] -inputs = . -output = .pytype diff --git a/setup.py b/setup.py deleted file mode 100644 index 23b7b472..00000000 --- a/setup.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (C) 2018 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""The setup.py file for Python Fire.""" - -from setuptools import setup - -LONG_DESCRIPTION = """ -Python Fire is a library for automatically generating command line interfaces -(CLIs) with a single line of code. - -It will turn any Python module, class, object, function, etc. (any Python -component will work!) into a CLI. It's called Fire because when you call Fire(), -it fires off your command. -""".strip() - -SHORT_DESCRIPTION = """ -A library for automatically generating command line interfaces.""".strip() - -DEPENDENCIES = [ - 'termcolor', -] - -TEST_DEPENDENCIES = [ - 'hypothesis', - 'levenshtein', -] - -VERSION = '0.7.1' -URL = 'https://github.com/google/python-fire' - -setup( - name='fire', - version=VERSION, - description=SHORT_DESCRIPTION, - long_description=LONG_DESCRIPTION, - url=URL, - - author='David Bieber', - author_email='dbieber@google.com', - license='Apache Software License', - - classifiers=[ - 'Development Status :: 4 - Beta', - - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - - 'License :: OSI Approved :: Apache Software License', - - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - - 'Operating System :: OS Independent', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', - ], - - keywords='command line interface cli python fire interactive bash tool', - - requires_python='>=3.7', - packages=['fire', 'fire.console'], - - install_requires=DEPENDENCIES, - tests_require=TEST_DEPENDENCIES, -) From 84496196045c96ade6ef7c42ebd374f9b6bddee0 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 18 Jul 2025 23:21:02 -0400 Subject: [PATCH 197/205] Use ty in place of pytype (#617) * Use ty in place of pytype --- .github/scripts/build.sh | 8 ++++---- fire/__main__.py | 4 ++-- fire/console/console_attr_os.py | 5 +---- fire/console/encoding.py | 12 ++++++------ fire/core.py | 4 ++-- fire/core_test.py | 4 ++-- fire/custom_descriptions.py | 8 ++++---- fire/decorators.py | 2 +- fire/docstrings.py | 2 +- fire/formatting_windows.py | 6 +++--- fire/helptext.py | 15 +++++++++------ fire/helptext_test.py | 12 ++++++------ fire/inspectutils.py | 17 ++++++++--------- fire/main_test.py | 2 +- fire/parser.py | 2 +- fire/trace.py | 2 -- pyproject.toml | 4 ---- 17 files changed, 51 insertions(+), 58 deletions(-) diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh index 7f5cf491..d9207dfe 100755 --- a/.github/scripts/build.sh +++ b/.github/scripts/build.sh @@ -24,8 +24,8 @@ python -m pytest # Run the tests without IPython. pip install ipython python -m pytest # Now run the tests with IPython. pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py,console -if [[ ${PYTHON_VERSION} == 3.7 ]]; then - # Run type-checking. - pip install pytype; - pytype -x fire/test_components_py3.py; +if [[ ${PYTHON_VERSION} == 3.12 ]]; then + # Run type-checking + pip install ty + python -m ty check --python $(which python) --exclude fire/test_components_py3.py --exclude fire/console/ --exclude fire/formatting_windows.py fi diff --git a/fire/__main__.py b/fire/__main__.py index 140b4a76..eb98b1a4 100644 --- a/fire/__main__.py +++ b/fire/__main__.py @@ -60,11 +60,11 @@ def import_from_file_path(path): spec = util.spec_from_file_location(module_name, path) - if spec is None: + if spec is None or spec.loader is None: raise OSError('Unable to load module from specified path.') module = util.module_from_spec(spec) # pylint: disable=no-member - spec.loader.exec_module(module) # pytype: disable=attribute-error + spec.loader.exec_module(module) return module, module_name diff --git a/fire/console/console_attr_os.py b/fire/console/console_attr_os.py index 869c5949..a7f38d4f 100644 --- a/fire/console/console_attr_os.py +++ b/fire/console/console_attr_os.py @@ -14,9 +14,6 @@ # limitations under the License. """OS specific console_attr helper functions.""" -# This file contains platform specific code which is not currently handled -# by pytype. -# pytype: skip-file from __future__ import absolute_import from __future__ import division @@ -73,7 +70,7 @@ def _GetXY(fd): try: # This magic incantation converts a struct from ioctl(2) containing two # binary shorts to a (rows, columns) int tuple. - rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'junk')) + rc = struct.unpack(b'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, b'junk')) return (rc[1], rc[0]) if rc else None except: # pylint: disable=bare-except return None diff --git a/fire/console/encoding.py b/fire/console/encoding.py index 3ce30cb5..662342c6 100644 --- a/fire/console/encoding.py +++ b/fire/console/encoding.py @@ -67,7 +67,7 @@ def Decode(data, encoding=None): try: # Just return the string if its pure ASCII. - return string.decode('ascii') # pytype: disable=attribute-error + return string.decode('ascii') except UnicodeError: # The string is not ASCII encoded. pass @@ -75,7 +75,7 @@ def Decode(data, encoding=None): # Try the suggested encoding if specified. if encoding: try: - return string.decode(encoding) # pytype: disable=attribute-error + return string.decode(encoding) except UnicodeError: # Bad suggestion. pass @@ -84,21 +84,21 @@ def Decode(data, encoding=None): # be exceptional if a valid extended ascii encoding with extended chars # were also a valid UITF-8 encoding. try: - return string.decode('utf8') # pytype: disable=attribute-error + return string.decode('utf8') except UnicodeError: # Not a UTF-8 encoding. pass # Try the filesystem encoding. try: - return string.decode(sys.getfilesystemencoding()) # pytype: disable=attribute-error + return string.decode(sys.getfilesystemencoding()) except UnicodeError: # string is not encoded for filesystem paths. pass # Try the system default encoding. try: - return string.decode(sys.getdefaultencoding()) # pytype: disable=attribute-error + return string.decode(sys.getdefaultencoding()) except UnicodeError: # string is not encoded using the default encoding. pass @@ -118,7 +118,7 @@ def Decode(data, encoding=None): # string = '\xdc' # string = string.decode('iso-8859-1') # string = string.encode('ascii', 'backslashreplace') - return string.decode('iso-8859-1') # pytype: disable=attribute-error + return string.decode('iso-8859-1') def GetEncodedValue(env, name, default=None): diff --git a/fire/core.py b/fire/core.py index 26a25753..32e0e9cc 100644 --- a/fire/core.py +++ b/fire/core.py @@ -504,7 +504,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # Treat namedtuples as dicts when handling them as a map. if inspectutils.IsNamedTuple(component): - component_dict = component._asdict() # pytype: disable=attribute-error + component_dict = component._asdict() else: component_dict = component @@ -519,7 +519,7 @@ def _Fire(component, args, parsed_flag_args, context, name=None): # a key as another type. # TODO(dbieber): Consider alternatives for accessing non-string keys. for key, value in ( - component_dict.items()): # pytype: disable=attribute-error + component_dict.items()): if target == str(key): component = value handled = True diff --git a/fire/core_test.py b/fire/core_test.py index 90b7f466..f48d6e2d 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -215,12 +215,12 @@ def serialize(x): def testLruCacheDecoratorBoundArg(self): self.assertEqual( - core.Fire(tc.py3.LruCacheDecoratedMethod, # pytype: disable=module-attr + core.Fire(tc.py3.LruCacheDecoratedMethod, command=['lru_cache_in_class', 'foo']), 'foo') def testLruCacheDecorator(self): self.assertEqual( - core.Fire(tc.py3.lru_cache_decorated, # pytype: disable=module-attr + core.Fire(tc.py3.lru_cache_decorated, command=['foo']), 'foo') diff --git a/fire/custom_descriptions.py b/fire/custom_descriptions.py index 768f0e23..ef1130a3 100644 --- a/fire/custom_descriptions.py +++ b/fire/custom_descriptions.py @@ -131,14 +131,14 @@ def GetStringTypeDescription(obj, available_space, line_length): def GetSummary(obj, available_space, line_length): obj_type_name = type(obj).__name__ if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: - return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[0](obj, available_space, - line_length) + return CUSTOM_DESC_SUM_FN_DICT[obj_type_name][0](obj, available_space, + line_length) return None def GetDescription(obj, available_space, line_length): obj_type_name = type(obj).__name__ if obj_type_name in CUSTOM_DESC_SUM_FN_DICT: - return CUSTOM_DESC_SUM_FN_DICT.get(obj_type_name)[1](obj, available_space, - line_length) + return CUSTOM_DESC_SUM_FN_DICT[obj_type_name][1](obj, available_space, + line_length) return None diff --git a/fire/decorators.py b/fire/decorators.py index 914b1de6..547153c6 100644 --- a/fire/decorators.py +++ b/fire/decorators.py @@ -68,7 +68,7 @@ def SetParseFns(*positional, **named): def _Decorator(fn): parse_fns = GetParseFns(fn) parse_fns['positional'] = positional - parse_fns['named'].update(named) # pytype: disable=attribute-error + parse_fns['named'].update(named) _SetMetadata(fn, FIRE_PARSE_FNS, parse_fns) return fn diff --git a/fire/docstrings.py b/fire/docstrings.py index 2d7c7e63..2adfe5ec 100644 --- a/fire/docstrings.py +++ b/fire/docstrings.py @@ -436,7 +436,7 @@ def _consume_line(line_info, state): if state.section.new and state.section.format == Formats.RST: # The current line starts with an RST directive, e.g. ":param arg:". directive = _get_directive(line_info) - directive_tokens = directive.split() # pytype: disable=attribute-error + directive_tokens = directive.split() if state.section.title == Sections.ARGS: name = directive_tokens[-1] arg = _get_or_create_arg_by_name( diff --git a/fire/formatting_windows.py b/fire/formatting_windows.py index cee6f393..749ab6d0 100644 --- a/fire/formatting_windows.py +++ b/fire/formatting_windows.py @@ -21,7 +21,7 @@ import sys try: - import colorama # pylint: disable=g-import-not-at-top, # pytype: disable=import-error + import colorama # pylint: disable=g-import-not-at-top HAS_COLORAMA = True except ImportError: HAS_COLORAMA = False @@ -38,9 +38,9 @@ def initialize_or_disable(): # Windows 10, 2016, and 2019 only. wrap = False - kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr + kernel32 = ctypes.windll.kernel32 enable_virtual_terminal_processing = 0x04 - out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # pytype: disable=module-attr + out_handle = kernel32.GetStdHandle(subprocess.STD_OUTPUT_HANDLE) # pylint: disable=line-too-long, # GetConsoleMode fails if the terminal isn't native. mode = ctypes.wintypes.DWORD() if kernel32.GetConsoleMode(out_handle, ctypes.byref(mode)) == 0: diff --git a/fire/helptext.py b/fire/helptext.py index 318d6276..347278da 100644 --- a/fire/helptext.py +++ b/fire/helptext.py @@ -29,6 +29,8 @@ information. """ +from __future__ import annotations + import collections import itertools @@ -85,13 +87,14 @@ def HelpText(component, trace=None, verbose=False): + usage_details_sections + notes_sections ) + valid_sections = [section for section in sections if section is not None] return '\n\n'.join( - _CreateOutputSection(*section) - for section in sections if section is not None + _CreateOutputSection(name, content) + for name, content in valid_sections ) -def _NameSection(component, info, trace=None, verbose=False): +def _NameSection(component, info, trace=None, verbose=False) -> tuple[str, str]: """The "Name" section of the help string.""" # Only include separators in the name in verbose mode. @@ -113,7 +116,7 @@ def _NameSection(component, info, trace=None, verbose=False): def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, - trace=None): + trace=None) -> tuple[str, str]: """The "Synopsis" section of the help string.""" current_command = _GetCurrentCommand(trace=trace, include_separators=True) @@ -136,7 +139,7 @@ def _SynopsisSection(component, actions_grouped_by_kind, spec, metadata, return ('SYNOPSIS', text) -def _DescriptionSection(component, info): +def _DescriptionSection(component, info) -> tuple[str, str] | None: """The "Description" sections of the help string. Args: @@ -408,7 +411,7 @@ def _GetCurrentCommand(trace=None, include_separators=True): return current_command -def _CreateOutputSection(name, content): +def _CreateOutputSection(name: str, content: str) -> str: return f"""{formatting.Bold(name)} {formatting.Indent(content, SECTION_INDENTATION)}""" diff --git a/fire/helptext_test.py b/fire/helptext_test.py index aeff5240..c7098fc4 100644 --- a/fire/helptext_test.py +++ b/fire/helptext_test.py @@ -125,7 +125,7 @@ def testHelpTextFunctionWithKwargsAndDefaults(self): def testHelpTextFunctionWithDefaultsAndTypes(self): component = ( - tc.py3.WithDefaultsAndTypes().double) # pytype: disable=module-attr + tc.py3.WithDefaultsAndTypes().double) help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -139,7 +139,7 @@ def testHelpTextFunctionWithDefaultsAndTypes(self): def testHelpTextFunctionWithTypesAndDefaultNone(self): component = ( - tc.py3.WithDefaultsAndTypes().get_int) # pytype: disable=module-attr + tc.py3.WithDefaultsAndTypes().get_int) help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='get_int')) @@ -153,7 +153,7 @@ def testHelpTextFunctionWithTypesAndDefaultNone(self): self.assertNotIn('NOTES', help_screen) def testHelpTextFunctionWithTypes(self): - component = tc.py3.WithTypes().double # pytype: disable=module-attr + component = tc.py3.WithTypes().double help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='double')) @@ -168,7 +168,7 @@ def testHelpTextFunctionWithTypes(self): help_screen) def testHelpTextFunctionWithLongTypes(self): - component = tc.py3.WithTypes().long_type # pytype: disable=module-attr + component = tc.py3.WithTypes().long_type help_screen = helptext.HelpText( component=component, trace=trace.FireTrace(component, name='long_type')) @@ -263,14 +263,14 @@ def testHelpTextNoInit(self): self.assertIn('SYNOPSIS\n OldStyleEmpty', help_screen) def testHelpTextKeywordOnlyArgumentsWithDefault(self): - component = tc.py3.KeywordOnly.with_default # pytype: disable=module-attr + component = tc.py3.KeywordOnly.with_default output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'with_default')) self.assertIn('NAME\n with_default', output) self.assertIn('FLAGS\n -x, --x=X', output) def testHelpTextKeywordOnlyArgumentsWithoutDefault(self): - component = tc.py3.KeywordOnly.double # pytype: disable=module-attr + component = tc.py3.KeywordOnly.double output = helptext.HelpText( component=component, trace=trace.FireTrace(component, 'double')) self.assertIn('NAME\n double', output) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index d9c62ca7..6dd8fd67 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -100,9 +100,9 @@ def Py3GetFullArgSpec(fn): An inspect.FullArgSpec namedtuple with the full arg spec of the function. """ # pylint: disable=no-member - # pytype: disable=module-attr + try: - sig = inspect._signature_from_callable( # pylint: disable=protected-access + sig = inspect._signature_from_callable( # pylint: disable=protected-access # type: ignore fn, skip_bound_arg=True, follow_wrapper_chains=True, @@ -129,19 +129,19 @@ def Py3GetFullArgSpec(fn): name = param.name # pylint: disable=protected-access - if kind is inspect._POSITIONAL_ONLY: + if kind is inspect._POSITIONAL_ONLY: # type: ignore args.append(name) - elif kind is inspect._POSITIONAL_OR_KEYWORD: + elif kind is inspect._POSITIONAL_OR_KEYWORD: # type: ignore args.append(name) if param.default is not param.empty: defaults += (param.default,) - elif kind is inspect._VAR_POSITIONAL: + elif kind is inspect._VAR_POSITIONAL: # type: ignore varargs = name - elif kind is inspect._KEYWORD_ONLY: + elif kind is inspect._KEYWORD_ONLY: # type: ignore kwonlyargs.append(name) if param.default is not param.empty: kwdefaults[name] = param.default - elif kind is inspect._VAR_KEYWORD: + elif kind is inspect._VAR_KEYWORD: # type: ignore varkw = name if param.annotation is not param.empty: annotations[name] = param.annotation @@ -157,7 +157,6 @@ def Py3GetFullArgSpec(fn): return inspect.FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwdefaults, annotations) # pylint: enable=no-member - # pytype: enable=module-attr def GetFullArgSpec(fn): @@ -259,7 +258,7 @@ def Info(component): try: inspector = oinspect.Inspector(theme_name="neutral") except TypeError: # Only recent versions of IPython support theme_name. - inspector = oinspect.Inspector() + inspector = oinspect.Inspector() # type: ignore info = inspector.info(component) # IPython's oinspect.Inspector.info may return '' diff --git a/fire/main_test.py b/fire/main_test.py index a2723347..9e1c382b 100644 --- a/fire/main_test.py +++ b/fire/main_test.py @@ -78,7 +78,7 @@ def testFileNameModuleDuplication(self): def testFileNameModuleFileFailure(self): # Confirm that an invalid file that masks a non-existent module fails. with self.assertRaisesRegex(ValueError, - r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, # pytype: disable=attribute-error + r'Fire can only be called on \.py files\.'): # pylint: disable=line-too-long, dirname = os.path.dirname(self.file.name) with testutils.ChangeDirectory(dirname): with open('foobar', 'w'): diff --git a/fire/parser.py b/fire/parser.py index d945b8ce..b8e7f19c 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -96,7 +96,7 @@ def _LiteralEval(value): SyntaxError: If the value string has a syntax error. """ root = ast.parse(value, mode='eval') - if isinstance(root.body, ast.BinOp): # pytype: disable=attribute-error + if isinstance(root.body, ast.BinOp): raise ValueError(value) for node in ast.walk(root): diff --git a/fire/trace.py b/fire/trace.py index 4a6d4776..601026fd 100644 --- a/fire/trace.py +++ b/fire/trace.py @@ -62,9 +62,7 @@ def __init__(self, initial_component, name=None, separator='-', verbose=False, def GetResult(self): """Returns the component from the last element of the trace.""" - # pytype: disable=attribute-error return self.GetLastHealthyElement().component - # pytype: enable=attribute-error def GetLastHealthyElement(self): """Returns the last element of the trace that is not an error. diff --git a/pyproject.toml b/pyproject.toml index 6a6ba63e..dfb1eeba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,3 @@ addopts = [ "--ignore=fire/test_components_py3.py", "--ignore=fire/parser_fuzz_test.py" ] - -[tool.pytype] -inputs = "." -output = ".pytype" From cec0119b10d2007e9de7c58ea4d7eac22682dc04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:22:05 -0400 Subject: [PATCH 198/205] Update hypothesis requirement from <6.133.0 to <6.136.0 (#616) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.135.33) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.135.33 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfb1eeba..b42c0aec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ test = [ "pytest-pylint<=1.1.2", "pytest-runner<7.0.0", "termcolor<3.2.0", - "hypothesis<6.133.0", + "hypothesis<6.136.0", "levenshtein<=0.27.1", ] From 8c62e05569cd7111731e2f704cbba5e3e4157b01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:25:48 -0400 Subject: [PATCH 199/205] Update pytest requirement from <=8.3.5 to <=8.4.1 (#615) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/1.0.0b3...8.4.1) --- updated-dependencies: - dependency-name: pytest dependency-version: 8.4.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b42c0aec..34ad8426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ test = [ "setuptools<=80.9.0", "pip", "pylint<3.3.7", - "pytest<=8.3.5", + "pytest<=8.4.1", "pytest-pylint<=1.1.2", "pytest-runner<7.0.0", "termcolor<3.2.0", From 86bf4ca693106a85827d9419ae36ff2c7ac29a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:30:59 -0400 Subject: [PATCH 200/205] Update pylint requirement from <3.3.7 to <3.3.8 (#614) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/pylint-version-0.18.1...v3.3.7) --- updated-dependencies: - dependency-name: pylint dependency-version: 3.3.7 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 34ad8426..b9a7217b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ Repository = "https://github.com/google/python-fire" test = [ "setuptools<=80.9.0", "pip", - "pylint<3.3.7", + "pylint<3.3.8", "pytest<=8.4.1", "pytest-pylint<=1.1.2", "pytest-runner<7.0.0", From ea8c7f5e74157c9f6bf2e251fce8ddcac81ef3d5 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 19 Jul 2025 08:20:55 -0400 Subject: [PATCH 201/205] Remove unused MANIFEST --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 1aba38f6..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include LICENSE From 8ea2f631e6dc904f69ec59f645fa81eb0a3c2b8e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Aug 2025 16:15:49 -0400 Subject: [PATCH 202/205] Update email address --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9a7217b..eccee91b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "A library for automatically generating command line interfaces." readme = "README.md" license = {text = "Apache-2.0"} authors = [ - {name = "David Bieber", email = "dbieber@google.com"} + {name = "David Bieber", email = "david810+fire@gmail.com"} ] classifiers = [ "Development Status :: 4 - Beta", From 0b2837778776bc7be024093f7342d746826a1c38 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Aug 2025 16:39:17 -0400 Subject: [PATCH 203/205] Add type hints to satisfy ty (#622) --- fire/inspectutils.py | 6 +++--- fire/parser.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 6dd8fd67..0a79d24b 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -269,12 +269,12 @@ def Info(component): try: unused_code, lineindex = inspect.findsource(component) - info['line'] = lineindex + 1 + info['line'] = lineindex + 1 # type: ignore except (TypeError, OSError): - info['line'] = None + info['line'] = None # type: ignore if 'docstring' in info: - info['docstring_info'] = docstrings.parse(info['docstring']) + info['docstring_info'] = docstrings.parse(info['docstring']) # type: ignore return info diff --git a/fire/parser.py b/fire/parser.py index b8e7f19c..1a4f7d9b 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -19,7 +19,7 @@ import sys if sys.version_info[0:2] < (3, 8): - _StrNode = ast.Str + _StrNode = ast.Str # type: ignore # deprecated but needed for Python < 3.8 else: _StrNode = ast.Constant From 27f41ac38f7c77a32d2020c9c13bbf6154065166 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:55:48 -0400 Subject: [PATCH 204/205] Update hypothesis requirement from <6.136.0 to <6.137.0 (#621) Updates the requirements on [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-ruby-0.0.1...hypothesis-python-6.136.6) --- updated-dependencies: - dependency-name: hypothesis dependency-version: 6.136.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index eccee91b..d0bf34c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ test = [ "pytest-pylint<=1.1.2", "pytest-runner<7.0.0", "termcolor<3.2.0", - "hypothesis<6.136.0", + "hypothesis<6.137.0", "levenshtein<=0.27.1", ] From 716bbc23d7eca949fdb682172283c8d18f742cb6 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Sat, 16 Aug 2025 17:26:04 -0400 Subject: [PATCH 205/205] Add Python 3.13 and 3.14 checking in build workflow (#623) * Add Python 3.13 and 3.14 checking in build workflow --- .github/workflows/build.yml | 2 +- fire/core.py | 10 ++++++++-- fire/inspectutils.py | 3 +-- fire/parser.py | 2 +- pyproject.toml | 1 + 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75a687f3..6b9d1eae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: ["macos-latest", "ubuntu-latest"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14.0-rc.2"] include: - {os: "ubuntu-22.04", python-version: "3.7"} diff --git a/fire/core.py b/fire/core.py index 32e0e9cc..8e23e76b 100644 --- a/fire/core.py +++ b/fire/core.py @@ -678,8 +678,14 @@ def _CallAndUpdateTrace(component, args, component_trace, treatment='class', # Call the function. if inspectutils.IsCoroutineFunction(fn): - loop = asyncio.get_event_loop() - component = loop.run_until_complete(fn(*varargs, **kwargs)) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No event loop running, create a new one + component = asyncio.run(fn(*varargs, **kwargs)) + else: + # Event loop is already running + component = loop.run_until_complete(fn(*varargs, **kwargs)) else: component = fn(*varargs, **kwargs) diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 0a79d24b..17508e30 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -14,7 +14,6 @@ """Inspection utility functions for Python Fire.""" -import asyncio import inspect import sys import types @@ -345,6 +344,6 @@ def GetClassAttrsDict(component): def IsCoroutineFunction(fn): try: - return asyncio.iscoroutinefunction(fn) + return inspect.iscoroutinefunction(fn) except: # pylint: disable=bare-except return False diff --git a/fire/parser.py b/fire/parser.py index 1a4f7d9b..a335cc2c 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -19,7 +19,7 @@ import sys if sys.version_info[0:2] < (3, 8): - _StrNode = ast.Str # type: ignore # deprecated but needed for Python < 3.8 + _StrNode = ast.Str # type: ignore # pylint: disable=no-member # deprecated but needed for Python < 3.8 else: _StrNode = ast.Constant diff --git a/pyproject.toml b/pyproject.toml index d0bf34c8..912c08aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS",