Skip to content

Commit 81c1cdc

Browse files
ttw225ethanfurman
andauthored
gh-139819: rlcompleter – avoid suggesting attributes not accessible on instances (GH-139820)
Co-authored-by: Ethan Furman <ethan@stoneleaf.us>
1 parent 7053bbd commit 81c1cdc

3 files changed

Lines changed: 33 additions & 10 deletions

File tree

Lib/rlcompleter.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040

4141
__all__ = ["Completer"]
4242

43+
# Sentinel object to distinguish "missing" from "present but None"
44+
_SENTINEL = object()
45+
4346
class Completer:
4447
def __init__(self, namespace = None):
4548
"""Create a new completer for the command line.
@@ -194,14 +197,14 @@ def attr_matches(self, text):
194197
and
195198
isinstance(thisobject.__dict__.get(word),
196199
types.LazyImportType)
197-
):
200+
):
198201
value = thisobject.__dict__.get(word)
199202
else:
200-
value = getattr(thisobject, word, None)
203+
value = getattr(thisobject, word, _SENTINEL)
201204

202-
if value is not None:
205+
if value is not _SENTINEL:
203206
matches.append(self._callable_postfix(value, match))
204-
else:
207+
elif word in getattr(type(thisobject), '__slots__', ()):
205208
matches.append(match)
206209
if matches or not noprefix:
207210
break

Lib/test/test_rlcompleter.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,35 @@ def test_excessive_getattr(self):
107107
# we use __dir__ and __getattr__ in class Foo to create a "magic"
108108
# class attribute 'bar'. This forces `getattr` to call __getattr__
109109
# (which is doesn't necessarily do).
110-
class Foo:
110+
# Test 1: Attribute returns None
111+
class FooReturnsNone:
111112
calls = 0
112-
bar = ''
113+
bar = None
113114
def __getattribute__(self, name):
114115
if name == 'bar':
115116
self.calls += 1
116117
return None
117118
return super().__getattribute__(name)
118119

119-
f = Foo()
120-
completer = rlcompleter.Completer(dict(f=f))
121-
self.assertEqual(completer.complete('f.b', 0), 'f.bar')
122-
self.assertEqual(f.calls, 1)
120+
f1 = FooReturnsNone()
121+
completer1 = rlcompleter.Completer(dict(f=f1))
122+
self.assertEqual(completer1.complete('f.b', 0), 'f.bar')
123+
self.assertEqual(f1.calls, 1)
124+
125+
# Test 2: Attribute returns non-None value
126+
class FooReturnsValue:
127+
calls = 0
128+
bar = ''
129+
def __getattribute__(self, name):
130+
if name == 'bar':
131+
self.calls += 1
132+
return ''
133+
return super().__getattribute__(name)
134+
135+
f2 = FooReturnsValue()
136+
completer2 = rlcompleter.Completer(dict(f=f2))
137+
self.assertEqual(completer2.complete('f.b', 0), 'f.bar')
138+
self.assertEqual(f2.calls, 1)
123139

124140
def test_property_method_not_called(self):
125141
class Foo:
@@ -196,6 +212,7 @@ class Foo:
196212
completer = rlcompleter.Completer(dict(f=Foo()))
197213
self.assertEqual(completer.complete('f.', 0), 'f.bar')
198214

215+
199216
@unittest.mock.patch('rlcompleter._readline_available', False)
200217
def test_complete(self):
201218
completer = rlcompleter.Completer()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`rlcompleter`: Avoid suggesting attributes that are not accessible on
2+
instances (e.g., Enum members showing ``__name__``). Patch by Peter
3+
(ttw225).

0 commit comments

Comments
 (0)