@@ -4262,6 +4262,184 @@ def __getattribute__(self, attr):
42624262 self .assertIn ("Did you mean" , actual )
42634263 self .assertIn ("bluch" , actual )
42644264
4265+ def test_getattr_nested_attribute_suggestions (self ):
4266+ # Test that nested attributes are suggested when no direct match
4267+ class Inner :
4268+ def __init__ (self ):
4269+ self .value = 42
4270+ self .data = "test"
4271+
4272+ class Outer :
4273+ def __init__ (self ):
4274+ self .inner = Inner ()
4275+
4276+ # Should suggest 'inner.value'
4277+ actual = self .get_suggestion (Outer (), 'value' )
4278+ self .assertIn ("Did you mean: 'inner.value'" , actual )
4279+
4280+ # Should suggest 'inner.data'
4281+ actual = self .get_suggestion (Outer (), 'data' )
4282+ self .assertIn ("Did you mean: 'inner.data'" , actual )
4283+
4284+ def test_getattr_nested_prioritizes_direct_matches (self ):
4285+ # Test that direct attribute matches are prioritized over nested ones
4286+ class Inner :
4287+ def __init__ (self ):
4288+ self .foo = 42
4289+
4290+ class Outer :
4291+ def __init__ (self ):
4292+ self .inner = Inner ()
4293+ self .fooo = 100 # Similar to 'foo'
4294+
4295+ # Should suggest 'fooo' (direct) not 'inner.foo' (nested)
4296+ actual = self .get_suggestion (Outer (), 'foo' )
4297+ self .assertIn ("Did you mean: 'fooo'" , actual )
4298+ self .assertNotIn ("inner.foo" , actual )
4299+
4300+ def test_getattr_nested_with_property (self ):
4301+ # Test that descriptors (including properties) are suggested in nested attributes
4302+ class Inner :
4303+ @property
4304+ def computed (self ):
4305+ return 42
4306+
4307+ class Outer :
4308+ def __init__ (self ):
4309+ self .inner = Inner ()
4310+
4311+ actual = self .get_suggestion (Outer (), 'computed' )
4312+ # Descriptors should not be suggested to avoid executing arbitrary code
4313+ self .assertIn ("inner.computed" , actual )
4314+
4315+ def test_getattr_nested_no_suggestion_for_deep_nesting (self ):
4316+ # Test that deeply nested attributes (2+ levels) are not suggested
4317+ class Deep :
4318+ def __init__ (self ):
4319+ self .value = 42
4320+
4321+ class Middle :
4322+ def __init__ (self ):
4323+ self .deep = Deep ()
4324+
4325+ class Outer :
4326+ def __init__ (self ):
4327+ self .middle = Middle ()
4328+
4329+ # Should not suggest 'middle.deep.value' (too deep)
4330+ actual = self .get_suggestion (Outer (), 'value' )
4331+ self .assertNotIn ("Did you mean" , actual )
4332+
4333+ def test_getattr_nested_ignores_private_attributes (self ):
4334+ # Test that nested suggestions ignore private attributes
4335+ class Inner :
4336+ def __init__ (self ):
4337+ self .public_value = 42
4338+
4339+ class Outer :
4340+ def __init__ (self ):
4341+ self ._private_inner = Inner ()
4342+
4343+ # Should not suggest '_private_inner.public_value'
4344+ actual = self .get_suggestion (Outer (), 'public_value' )
4345+ self .assertNotIn ("Did you mean" , actual )
4346+
4347+ def test_getattr_nested_limits_attribute_checks (self ):
4348+ # Test that nested suggestions are limited to checking first 20 non-private attributes
4349+ class Inner :
4350+ def __init__ (self ):
4351+ self .target_value = 42
4352+
4353+ class Outer :
4354+ def __init__ (self ):
4355+ # Add many attributes before 'inner'
4356+ for i in range (25 ):
4357+ setattr (self , f'attr_{ i :02d} ' , i )
4358+ # Add the inner object after 20+ attributes
4359+ self .inner = Inner ()
4360+
4361+ obj = Outer ()
4362+ # Verify that 'inner' is indeed present but after position 20
4363+ attrs = [x for x in sorted (dir (obj )) if not x .startswith ('_' )]
4364+ inner_position = attrs .index ('inner' )
4365+ self .assertGreater (inner_position , 19 , "inner should be after position 20 in sorted attributes" )
4366+
4367+ # Should not suggest 'inner.target_value' because inner is beyond the first 20 attributes checked
4368+ actual = self .get_suggestion (obj , 'target_value' )
4369+ self .assertNotIn ("inner.target_value" , actual )
4370+
4371+ def test_getattr_nested_returns_first_match_only (self ):
4372+ # Test that only the first nested match is returned (not multiple)
4373+ class Inner1 :
4374+ def __init__ (self ):
4375+ self .value = 1
4376+
4377+ class Inner2 :
4378+ def __init__ (self ):
4379+ self .value = 2
4380+
4381+ class Inner3 :
4382+ def __init__ (self ):
4383+ self .value = 3
4384+
4385+ class Outer :
4386+ def __init__ (self ):
4387+ # Multiple inner objects with same attribute
4388+ self .a_inner = Inner1 ()
4389+ self .b_inner = Inner2 ()
4390+ self .c_inner = Inner3 ()
4391+
4392+ # Should suggest only the first match (alphabetically)
4393+ actual = self .get_suggestion (Outer (), 'value' )
4394+ self .assertIn ("'a_inner.value'" , actual )
4395+ # Verify it's a single suggestion, not multiple
4396+ self .assertEqual (actual .count ("Did you mean" ), 1 )
4397+
4398+ def test_getattr_nested_handles_attribute_access_exceptions (self ):
4399+ # Test that exceptions raised when accessing attributes don't crash the suggestion system
4400+ class ExplodingProperty :
4401+ @property
4402+ def exploding_attr (self ):
4403+ raise RuntimeError ("BOOM! This property always explodes" )
4404+
4405+ def __repr__ (self ):
4406+ raise RuntimeError ("repr also explodes" )
4407+
4408+ class SafeInner :
4409+ def __init__ (self ):
4410+ self .target = 42
4411+
4412+ class Outer :
4413+ def __init__ (self ):
4414+ self .exploder = ExplodingProperty () # Accessing attributes will raise
4415+ self .safe_inner = SafeInner ()
4416+
4417+ # Should still suggest 'safe_inner.target' without crashing
4418+ # even though accessing exploder.target would raise an exception
4419+ actual = self .get_suggestion (Outer (), 'target' )
4420+ self .assertIn ("'safe_inner.target'" , actual )
4421+
4422+ def test_getattr_nested_handles_hasattr_exceptions (self ):
4423+ # Test that exceptions in hasattr don't crash the system
4424+ class WeirdObject :
4425+ def __getattr__ (self , name ):
4426+ if name == 'target' :
4427+ raise RuntimeError ("Can't check for target attribute" )
4428+ raise AttributeError (f"No attribute { name } " )
4429+
4430+ class NormalInner :
4431+ def __init__ (self ):
4432+ self .target = 100
4433+
4434+ class Outer :
4435+ def __init__ (self ):
4436+ self .weird = WeirdObject () # hasattr will raise for 'target'
4437+ self .normal = NormalInner ()
4438+
4439+ # Should still find 'normal.target' even though weird.target check fails
4440+ actual = self .get_suggestion (Outer (), 'target' )
4441+ self .assertIn ("'normal.target'" , actual )
4442+
42654443 def make_module (self , code ):
42664444 tmpdir = Path (tempfile .mkdtemp ())
42674445 self .addCleanup (shutil .rmtree , tmpdir )
0 commit comments