From ea233b87353262c3639436b7a9c9442091206628 Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Thu, 3 Sep 2026 22:01:32 +0800 Subject: [PATCH 1/2] Skip members that raise when listing them for help Enumerating a component's members for help or completion runs each member's getter, including property getters. A getter that raises anything other than AttributeError currently makes inspect.getmembers abort, so running a CLI with no args or with --help crashes with the property's raw traceback instead of showing usage. Add inspectutils.GetMembers, which keeps the normal getmembers path but falls back to reading members one at a time and skipping the ones that raise, and use it where members are enumerated for help. --- fire/completion.py | 2 +- fire/core.py | 2 +- fire/inspectutils.py | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 1597d464..b5460b34 100644 --- a/fire/completion.py +++ b/fire/completion.py @@ -359,7 +359,7 @@ def VisibleMembers(component, class_attrs=None, verbose=False): if isinstance(component, dict): members = component.items() else: - members = inspect.getmembers(component) + members = inspectutils.GetMembers(component) # If class_attrs has not been provided, compute it. if class_attrs is None: diff --git a/fire/core.py b/fire/core.py index 8e23e76b..0bde49d1 100644 --- a/fire/core.py +++ b/fire/core.py @@ -225,7 +225,7 @@ def _IsHelpShortcut(component_trace, remaining_args): _, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec) show_help = target in remaining_kwargs else: - members = dict(inspect.getmembers(component)) + members = dict(inspectutils.GetMembers(component)) show_help = target not in members if show_help: diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 17508e30..4e5b654e 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -342,6 +342,33 @@ def GetClassAttrsDict(component): } +def GetMembers(component): + """Returns a list of (name, member) pairs for the members of component. + + Reading a member can run arbitrary code, such as a property getter, which may + raise an exception. inspect.getmembers aborts on any exception that is not an + AttributeError, which would crash Fire while it only intends to enumerate the + members of the component, for example to build help text. This falls back to + reading the members one at a time and skipping any that cannot be read, so a + single broken member does not prevent the rest from being listed. + + Args: + component: The object whose members to list. + Returns: + A list of (name, value) pairs for the readable members of component. + """ + try: + return inspect.getmembers(component) + except Exception: # pylint: disable=broad-except + members = [] + for name in dir(component): + try: + members.append((name, getattr(component, name))) + except Exception: # pylint: disable=broad-except + continue + return members + + def IsCoroutineFunction(fn): try: return inspect.iscoroutinefunction(fn) From a16f8c041ffb3fe4cada899b46b904afc0a7666f Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Thu, 3 Sep 2026 22:01:42 +0800 Subject: [PATCH 2/2] Add a regression test for help with raising properties A component with a property whose getter raises used to crash Fire when it tried to show help or list the component. The test covers bare invocation, the --help flag, and the --help shortcut. --- fire/fire_test.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/fire/fire_test.py b/fire/fire_test.py index 99b4a7c6..ae8bc90d 100644 --- a/fire/fire_test.py +++ b/fire/fire_test.py @@ -556,6 +556,35 @@ def testHelpFlagAndTraceFlag(self): with self.assertRaisesFireExit(0, 'Fire trace:\n.*SYNOPSIS'): fire.Fire(tc.BoolConverter, command=['--', '-h', '--trace']) + def testHelpWithRaisingProperty(self): + # A property whose getter raises should not prevent help from being shown. + # See https://github.com/google/python-fire/issues/672. + class Component(object): + + @property + def broken(self): + raise RuntimeError('backend unavailable') + + def works(self, value): + return value + + component = Component() + + # Bare invocation prints the component's help without crashing. + with self.assertOutputMatches(stdout='.*works.*'): + result = fire.Fire(component, command=[]) + self.assertEqual(result, component) + + # Both the --help flag and the --help shortcut list the working command. + with self.assertRaisesFireExit(0, 'works'): + fire.Fire(component, command=['--', '--help']) + with self.assertRaisesFireExit(0, 'works'): + fire.Fire(component, command=['--help']) + + # The broken property is still usable directly, but raises on access. + with self.assertRaises(RuntimeError): + component.broken # pylint: disable=pointless-statement + def testTabCompletionNoName(self): completion_script = fire.Fire(tc.NoDefaults, command=['--', '--completion']) self.assertIn('double', completion_script)