Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fire/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions fire/inspectutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions fire/inspectutils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()