Skip to content

Commit 07fdb76

Browse files
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
1 parent 716bbc2 commit 07fdb76

4 files changed

Lines changed: 69 additions & 2 deletions

File tree

fire/completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def VisibleMembers(component, class_attrs=None, verbose=False):
359359
if isinstance(component, dict):
360360
members = component.items()
361361
else:
362-
members = inspect.getmembers(component)
362+
members = inspectutils.GetSafeMembers(component)
363363

364364
# If class_attrs has not been provided, compute it.
365365
if class_attrs is None:

fire/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def _IsHelpShortcut(component_trace, remaining_args):
225225
_, remaining_kwargs, _ = _ParseKeywordArgs(remaining_args, fn_spec)
226226
show_help = target in remaining_kwargs
227227
else:
228-
members = dict(inspect.getmembers(component))
228+
members = dict(inspectutils.GetSafeMembers(component))
229229
show_help = target not in members
230230

231231
if show_help:

fire/inspectutils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,33 @@ def IsCoroutineFunction(fn):
347347
return inspect.iscoroutinefunction(fn)
348348
except: # pylint: disable=bare-except
349349
return False
350+
351+
def GetSafeMembers(component, predicate=None):
352+
"""Returns members of a component, skipping attributes that raise on access.
353+
354+
Like inspect.getmembers, but catches all exceptions raised by property
355+
getters or other dynamic attributes during member enumeration. Members
356+
that raise are included with a value of None, preserving the member name
357+
in the result so that callers can detect its presence without crashing.
358+
359+
This behaviour differs from inspect.getmembers in Python 3.13+, which
360+
only suppresses AttributeError and lets all other exceptions propagate.
361+
362+
Args:
363+
component: The object whose members to retrieve.
364+
predicate: An optional predicate to filter members by value.
365+
Returns:
366+
A list of (name, value) pairs sorted by name. Members whose getters
367+
raised are included as (name, None).
368+
"""
369+
results = []
370+
for key in dir(component):
371+
try:
372+
value = getattr(component, key)
373+
except Exception: # pylint: disable=broad-except
374+
value = None
375+
if predicate and not predicate(value):
376+
continue
377+
results.append((key, value))
378+
results.sort(key=lambda pair: pair[0])
379+
return results

fire/inspectutils_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,43 @@ def testInfoNoDocstring(self):
125125
info = inspectutils.Info(tc.NoDefaults)
126126
self.assertEqual(info['docstring'], None, 'Docstring should be None')
127127

128+
def testGetSafeMembersRaisingProperty(self):
129+
class ComponentWithRaisingProperty:
130+
@property
131+
def status(self):
132+
raise RuntimeError('backend unavailable')
133+
134+
component = ComponentWithRaisingProperty()
135+
members = dict(inspectutils.GetSafeMembers(component))
136+
self.assertIn('status', members)
137+
self.assertIsNone(members['status'])
138+
139+
def testGetSafeMembersWorkingProperty(self):
140+
class ComponentWithWorkingProperty:
141+
@property
142+
def status(self):
143+
return 'all good'
144+
145+
component = ComponentWithWorkingProperty()
146+
members = dict(inspectutils.GetSafeMembers(component))
147+
self.assertIn('status', members)
148+
self.assertEqual(members['status'], 'all good')
149+
150+
def testGetSafeMembersMixedProperties(self):
151+
class ComponentWithMixedProperties:
152+
@property
153+
def good(self):
154+
return 'ok'
155+
@property
156+
def bad(self):
157+
raise ValueError('unavailable')
158+
159+
component = ComponentWithMixedProperties()
160+
members = dict(inspectutils.GetSafeMembers(component))
161+
self.assertIn('good', members)
162+
self.assertEqual(members['good'], 'ok')
163+
self.assertIn('bad', members)
164+
self.assertIsNone(members['bad'])
128165

129166
if __name__ == '__main__':
130167
testutils.main()

0 commit comments

Comments
 (0)