From 07fdb76e0e08fe7ea904bc1ba5ac196d6dadc72e Mon Sep 17 00:00:00 2001 From: Bhargav Kumar Nath Date: Wed, 10 Jun 2026 13:17:08 +0100 Subject: [PATCH] Handle exceptions from property getters during member enumeration inspect.getmembers in Python 3.13 only suppresses AttributeError when calling attribute getters. Any other exception (RuntimeError, ValueError, etc.) propagates uncaught, causing fire.Fire to crash on --help and bare invocation when a component has a property whose getter raises. Add GetSafeMembers to inspectutils, which wraps getattr in a broad exception handler and falls back to None for any attribute that raises. Replace the two unguarded inspect.getmembers call sites in core.py (_IsHelpShortcut) and completion.py (VisibleMembers) with GetSafeMembers. Fixes #672 --- fire/completion.py | 2 +- fire/core.py | 2 +- fire/inspectutils.py | 30 ++++++++++++++++++++++++++++++ fire/inspectutils_test.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/fire/completion.py b/fire/completion.py index 1597d464..79e6ceb3 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.GetSafeMembers(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..8097a8f9 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.GetSafeMembers(component)) show_help = target not in members if show_help: diff --git a/fire/inspectutils.py b/fire/inspectutils.py index 17508e30..6bddda6f 100644 --- a/fire/inspectutils.py +++ b/fire/inspectutils.py @@ -347,3 +347,33 @@ def IsCoroutineFunction(fn): return inspect.iscoroutinefunction(fn) except: # pylint: disable=bare-except return False + +def GetSafeMembers(component, predicate=None): + """Returns members of a component, skipping attributes that raise on access. + + Like inspect.getmembers, but catches all exceptions raised by property + getters or other dynamic attributes during member enumeration. Members + that raise are included with a value of None, preserving the member name + in the result so that callers can detect its presence without crashing. + + This behaviour differs from inspect.getmembers in Python 3.13+, which + only suppresses AttributeError and lets all other exceptions propagate. + + Args: + component: The object whose members to retrieve. + predicate: An optional predicate to filter members by value. + Returns: + A list of (name, value) pairs sorted by name. Members whose getters + raised are included as (name, None). + """ + results = [] + for key in dir(component): + try: + value = getattr(component, key) + except Exception: # pylint: disable=broad-except + value = None + if predicate and not predicate(value): + continue + results.append((key, value)) + results.sort(key=lambda pair: pair[0]) + return results diff --git a/fire/inspectutils_test.py b/fire/inspectutils_test.py index 47de7e72..0e250b90 100644 --- a/fire/inspectutils_test.py +++ b/fire/inspectutils_test.py @@ -125,6 +125,43 @@ def testInfoNoDocstring(self): info = inspectutils.Info(tc.NoDefaults) self.assertEqual(info['docstring'], None, 'Docstring should be None') + def testGetSafeMembersRaisingProperty(self): + class ComponentWithRaisingProperty: + @property + def status(self): + raise RuntimeError('backend unavailable') + + component = ComponentWithRaisingProperty() + members = dict(inspectutils.GetSafeMembers(component)) + self.assertIn('status', members) + self.assertIsNone(members['status']) + + def testGetSafeMembersWorkingProperty(self): + class ComponentWithWorkingProperty: + @property + def status(self): + return 'all good' + + component = ComponentWithWorkingProperty() + members = dict(inspectutils.GetSafeMembers(component)) + self.assertIn('status', members) + self.assertEqual(members['status'], 'all good') + + def testGetSafeMembersMixedProperties(self): + class ComponentWithMixedProperties: + @property + def good(self): + return 'ok' + @property + def bad(self): + raise ValueError('unavailable') + + component = ComponentWithMixedProperties() + members = dict(inspectutils.GetSafeMembers(component)) + self.assertIn('good', members) + self.assertEqual(members['good'], 'ok') + self.assertIn('bad', members) + self.assertIsNone(members['bad']) if __name__ == '__main__': testutils.main()