Skip to content

Commit 0aeb040

Browse files
[3.14] gh-112821: Fix rlcompleter failures on objects with descriptors (GH-149577) (#149657)
* gh-112821: Fix rlcompleter failures on objects with descriptors (GH-149577) * gh-112821: Fix rlcompleter failures on objects with descriptors * Confirm no accesses (cherry picked from commit f23a183) Co-authored-by: Michael Droettboom <mdboom@gmail.com> * Add missing import --------- Co-authored-by: Michael Droettboom <mdboom@gmail.com> Co-authored-by: Michael Droettboom <mdroettboom@nvidia.com>
1 parent 745d609 commit 0aeb040

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

Lib/rlcompleter.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import inspect
3535
import keyword
3636
import re
37+
import types
3738
import __main__
3839
import warnings
3940

@@ -178,14 +179,14 @@ def attr_matches(self, text):
178179
if (word[:n] == attr and
179180
not (noprefix and word[:n+1] == noprefix)):
180181
match = "%s.%s" % (expr, word)
181-
if isinstance(getattr(type(thisobject), word, None),
182-
property):
183-
# bpo-44752: thisobject.word is a method decorated by
184-
# `@property`. What follows applies a postfix if
185-
# thisobject.word is callable, but know we know that
186-
# this is not callable (because it is a property).
187-
# Also, getattr(thisobject, word) will evaluate the
188-
# property method, which is not desirable.
182+
183+
class_attr = getattr(type(thisobject), word, None)
184+
if isinstance(
185+
class_attr,
186+
(property, types.GetSetDescriptorType, types.MemberDescriptorType)
187+
) or (hasattr(class_attr, '__get__') and not callable(class_attr)):
188+
# Avoid evaluating descriptors, which could run
189+
# arbitrary code or raise exceptions.
189190
matches.append(match)
190191
continue
191192
if (value := getattr(thisobject, word, None)) is not None:

Lib/test/test_rlcompleter.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22
from unittest.mock import patch
33
import builtins
4+
import types
45
import rlcompleter
56
from test.support import MISSING_C_DOCSTRINGS
67

@@ -135,6 +136,57 @@ def bar(self):
135136
self.assertEqual(completer.complete('f.b', 0), 'f.bar')
136137
self.assertFalse(f.property_called)
137138

139+
def test_released_memoryview_completion_works(self):
140+
mv = memoryview(b"abc")
141+
mv.release()
142+
143+
self.assertIsInstance(type(mv).shape, types.GetSetDescriptorType)
144+
self.assertIsInstance(type(mv).strides, types.GetSetDescriptorType)
145+
146+
completer = rlcompleter.Completer(dict(mv=mv))
147+
matches = completer.attr_matches('mv.')
148+
149+
# These are getset descriptors on memoryview and should be completed
150+
# without evaluating the released-memoryview getters.
151+
self.assertIn('mv.shape', matches)
152+
self.assertIn('mv.strides', matches)
153+
154+
def test_member_descriptor_not_evaluated(self):
155+
class Foo:
156+
__slots__ = ("boom",)
157+
boom_accesses = 0
158+
159+
def __getattribute__(self, name):
160+
if name == "boom":
161+
type(self).boom_accesses += 1
162+
raise RuntimeError("boom access should be skipped")
163+
return super().__getattribute__(name)
164+
165+
self.assertIsInstance(Foo.boom, types.MemberDescriptorType)
166+
167+
completer = rlcompleter.Completer(dict(f=Foo()))
168+
matches = completer.attr_matches('f.')
169+
self.assertIn('f.boom', matches)
170+
self.assertEqual(Foo.boom_accesses, 0)
171+
172+
def test_raising_descriptor_completion_works(self):
173+
class ExplodingDescriptor:
174+
def __init__(self):
175+
self.instance_get_calls = 0
176+
177+
def __get__(self, obj, owner):
178+
if obj is None:
179+
return self
180+
self.instance_get_calls += 1
181+
raise RuntimeError("descriptor getter exploded")
182+
183+
class Foo:
184+
boom = ExplodingDescriptor()
185+
186+
completer = rlcompleter.Completer(dict(f=Foo()))
187+
matches = completer.attr_matches('f.')
188+
self.assertIn('f.boom', matches)
189+
self.assertEqual(Foo.boom.instance_get_calls, 0)
138190

139191
def test_uncreated_attr(self):
140192
# Attributes like properties and slots should be completed even when
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
In the REPL, autocompletion might run arbitrary code in the getter of a
2+
descriptor. If that getter raised an exception, autocompletion would fail to
3+
present any options for the entire object. Autocompletion now works as
4+
expected for these objects.

0 commit comments

Comments
 (0)