Skip to content
7 changes: 5 additions & 2 deletions Lib/rlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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.
Expand Down Expand Up @@ -188,9 +191,9 @@ def attr_matches(self, text):
# property method, which is not desirable.
matches.append(match)
continue
if (value := getattr(thisobject, word, None)) is not None:
if (value := getattr(thisobject, word, _SENTINEL)) 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
Expand Down
29 changes: 23 additions & 6 deletions Lib/test/test_rlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,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:
Expand All @@ -144,6 +160,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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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).
Loading