diff --git a/Lib/rlcompleter.py b/Lib/rlcompleter.py index 6c6d9bb6b34244..271b77a322fdd2 100644 --- a/Lib/rlcompleter.py +++ b/Lib/rlcompleter.py @@ -40,6 +40,9 @@ __all__ = ["Completer"] +# Sentinel object to distinguish "missing" from "present but None" +_SENTINEL = object() + class Completer: def __init__(self, namespace = None): """Create a new completer for the command line. @@ -194,14 +197,14 @@ def attr_matches(self, text): and isinstance(thisobject.__dict__.get(word), types.LazyImportType) - ): + ): value = thisobject.__dict__.get(word) else: - value = getattr(thisobject, word, None) + value = getattr(thisobject, word, _SENTINEL) - if value is not None: + if value is not _SENTINEL: matches.append(self._callable_postfix(value, match)) - else: + elif word in getattr(type(thisobject), '__slots__', ()): matches.append(match) if matches or not noprefix: break diff --git a/Lib/test/test_rlcompleter.py b/Lib/test/test_rlcompleter.py index e6d727d417b298..c0b5a4da8cb256 100644 --- a/Lib/test/test_rlcompleter.py +++ b/Lib/test/test_rlcompleter.py @@ -107,19 +107,35 @@ def test_excessive_getattr(self): # we use __dir__ and __getattr__ in class Foo to create a "magic" # class attribute 'bar'. This forces `getattr` to call __getattr__ # (which is doesn't necessarily do). - class Foo: + # Test 1: Attribute returns None + class FooReturnsNone: calls = 0 - bar = '' + bar = None def __getattribute__(self, name): if name == 'bar': self.calls += 1 return None return super().__getattribute__(name) - f = Foo() - completer = rlcompleter.Completer(dict(f=f)) - self.assertEqual(completer.complete('f.b', 0), 'f.bar') - self.assertEqual(f.calls, 1) + f1 = FooReturnsNone() + completer1 = rlcompleter.Completer(dict(f=f1)) + self.assertEqual(completer1.complete('f.b', 0), 'f.bar') + self.assertEqual(f1.calls, 1) + + # Test 2: Attribute returns non-None value + class FooReturnsValue: + calls = 0 + bar = '' + def __getattribute__(self, name): + if name == 'bar': + self.calls += 1 + return '' + return super().__getattribute__(name) + + f2 = FooReturnsValue() + completer2 = rlcompleter.Completer(dict(f=f2)) + self.assertEqual(completer2.complete('f.b', 0), 'f.bar') + self.assertEqual(f2.calls, 1) def test_property_method_not_called(self): class Foo: @@ -196,6 +212,7 @@ class Foo: completer = rlcompleter.Completer(dict(f=Foo())) self.assertEqual(completer.complete('f.', 0), 'f.bar') + @unittest.mock.patch('rlcompleter._readline_available', False) def test_complete(self): completer = rlcompleter.Completer() diff --git a/Misc/NEWS.d/next/Library/2025-10-09-12-13-29.gh-issue-139819.YxUDyH.rst b/Misc/NEWS.d/next/Library/2025-10-09-12-13-29.gh-issue-139819.YxUDyH.rst new file mode 100644 index 00000000000000..ab3a8c6ed16ce4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-09-12-13-29.gh-issue-139819.YxUDyH.rst @@ -0,0 +1,3 @@ +:mod:`rlcompleter`: Avoid suggesting attributes that are not accessible on +instances (e.g., Enum members showing ``__name__``). Patch by Peter +(ttw225).