@@ -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