@@ -4262,6 +4262,184 @@ def __getattribute__(self, attr):
4262
4262
self .assertIn ("Did you mean" , actual )
4263
4263
self .assertIn ("bluch" , actual )
4264
4264
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
+
4265
4443
def make_module (self , code ):
4266
4444
tmpdir = Path (tempfile .mkdtemp ())
4267
4445
self .addCleanup (shutil .rmtree , tmpdir )
0 commit comments