Skip to content
Merged
11 changes: 7 additions & 4 deletions Lib/rlcompleter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@

__all__ = ["Completer"]

# Sentinel object to distinguish "missing" from "present but None"
_SENTINEL = object()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use the new sentinel builtin?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #151222.


class Completer:
def __init__(self, namespace = None):
"""Create a new completer for the command line.
Expand Down Expand Up @@ -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
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 @@ -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:
Expand Down Expand Up @@ -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()
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