self.assertNotIn("inner.foo", actual)
def test_getattr_nested_with_property(self):
- # Test that descriptors (including properties) are suggested in nested attributes
+ # Property suggestions should not execute the property getter.
class Inner:
@property
def computed(self):
- return 42
+ return missing_name
class Outer:
def __init__(self):
self.inner = Inner()
actual = self.get_suggestion(Outer(), 'computed')
- # Descriptors should not be suggested to avoid executing arbitrary code
- self.assertIn("inner.computed", actual)
+ self.assertIn(
+ "Did you mean '.inner.computed' instead of '.computed'",
+ actual,
+ )
def test_getattr_nested_no_suggestion_for_deep_nesting(self):
# Test that deeply nested attributes (2+ levels) are not suggested
Returns the first nested attribute suggestion found, or None.
Limited to checking 20 attributes.
- Only considers non-descriptor attributes to avoid executing arbitrary code.
+ Only considers non-descriptor outer attributes to avoid executing
+ arbitrary code. Checks nested attributes statically so descriptors such
+ as properties can still be suggested without invoking them.
Skips lazy imports to avoid triggering module loading.
"""
+ from inspect import getattr_static
+
# Check for nested attributes (only one level deep)
attrs_to_check = [x for x in attrs if not x.startswith('_')][:20] # Limit number of attributes to check
for attr_name in attrs_to_check:
with suppress(Exception):
# Check if attr_name is a descriptor - if so, skip it
- attr_from_class = getattr(type(obj), attr_name, None)
- if attr_from_class is not None and hasattr(attr_from_class, '__get__'):
+ attr_from_class = getattr_static(type(obj), attr_name, _sentinel)
+ if attr_from_class is not _sentinel and hasattr(attr_from_class, '__get__'):
continue # Skip descriptors to avoid executing arbitrary code
# Skip lazy imports to avoid triggering module loading
# Safe to get the attribute since it's not a descriptor
attr_obj = getattr(obj, attr_name)
- # Check if the nested attribute exists and is not a descriptor
- nested_attr_from_class = getattr(type(attr_obj), wrong_name, None)
+ if _is_lazy_import(attr_obj, wrong_name):
+ continue
- if hasattr(attr_obj, wrong_name):
+ if getattr_static(attr_obj, wrong_name, _sentinel) is not _sentinel:
return f"{attr_name}.{wrong_name}"
return None