Skip to content

Commit abc261a

Browse files
authored
Refactoring to start using getattr_static (#832)
Avoids having to use AttrCleaner in many contexts. Also adds some testing.
1 parent c39a9ab commit abc261a

File tree

6 files changed

+202
-194
lines changed

6 files changed

+202
-194
lines changed

bpython/autocomplete.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,6 @@ def attr_matches(self, text, namespace):
340340
"""Taken from rlcompleter.py and bent to my will.
341341
"""
342342

343-
# Gna, Py 2.6's rlcompleter searches for __call__ inside the
344-
# instance instead of the type, so we monkeypatch to prevent
345-
# side-effects (__getattr__/__getattribute__)
346343
m = self.attr_matches_re.match(text)
347344
if not m:
348345
return []
@@ -356,19 +353,17 @@ def attr_matches(self, text, namespace):
356353
obj = safe_eval(expr, namespace)
357354
except EvaluationError:
358355
return []
359-
with inspection.AttrCleaner(obj):
360-
matches = self.attr_lookup(obj, expr, attr)
356+
matches = self.attr_lookup(obj, expr, attr)
361357
return matches
362358

363359
def attr_lookup(self, obj, expr, attr):
364-
"""Second half of original attr_matches method factored out so it can
365-
be wrapped in a safe try/finally block in case anything bad happens to
366-
restore the original __getattribute__ method."""
360+
"""Second half of attr_matches."""
367361
words = self.list_attributes(obj)
368-
if hasattr(obj, "__class__"):
362+
if inspection.hasattr_safe(obj, "__class__"):
369363
words.append("__class__")
370-
words = words + rlcompleter.get_class_members(obj.__class__)
371-
if not isinstance(obj.__class__, abc.ABCMeta):
364+
klass = inspection.getattr_safe(obj, "__class__")
365+
words = words + rlcompleter.get_class_members(klass)
366+
if not isinstance(klass, abc.ABCMeta):
372367
try:
373368
words.remove("__abstractmethods__")
374369
except ValueError:
@@ -388,21 +383,25 @@ def attr_lookup(self, obj, expr, attr):
388383
if py3:
389384

390385
def list_attributes(self, obj):
391-
return dir(obj)
386+
# TODO: re-implement dir using getattr_static to avoid using
387+
# AttrCleaner here?
388+
with inspection.AttrCleaner(obj):
389+
return dir(obj)
392390

393391
else:
394392

395393
def list_attributes(self, obj):
396-
if isinstance(obj, InstanceType):
397-
try:
394+
with inspection.AttrCleaner(obj):
395+
if isinstance(obj, InstanceType):
396+
try:
397+
return dir(obj)
398+
except Exception:
399+
# This is a case where we can not prevent user code from
400+
# running. We return a default list attributes on error
401+
# instead. (#536)
402+
return ["__doc__", "__module__"]
403+
else:
398404
return dir(obj)
399-
except Exception:
400-
# This is a case where we can not prevent user code from
401-
# running. We return a default list attributes on error
402-
# instead. (#536)
403-
return ["__doc__", "__module__"]
404-
else:
405-
return dir(obj)
406405

407406

408407
class DictKeyCompletion(BaseCompletionType):
@@ -537,10 +536,9 @@ def matches(self, cursor_offset, line, **kwargs):
537536
obj = evaluate_current_expression(cursor_offset, line, locals_)
538537
except EvaluationError:
539538
return set()
540-
with inspection.AttrCleaner(obj):
541-
# strips leading dot
542-
matches = [m[1:] for m in self.attr_lookup(obj, "", attr.word)]
543539

540+
# strips leading dot
541+
matches = [m[1:] for m in self.attr_lookup(obj, "", attr.word)]
544542
return set(m for m in matches if few_enough_underscores(attr.word, m))
545543

546544

@@ -679,7 +677,6 @@ def get_completer_bpython(cursor_offset, line, **kwargs):
679677

680678
def _callable_postfix(value, word):
681679
"""rlcompleter's _callable_postfix done right."""
682-
with inspection.AttrCleaner(value):
683-
if inspection.is_callable(value):
684-
word += "("
680+
if inspection.is_callable(value):
681+
word += "("
685682
return word

bpython/inspection.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from six.moves import range
3434

3535
from pygments.token import Token
36+
from types import MemberDescriptorType
3637

3738
from ._py3compat import PythonLexer, py3
3839
from .lazyre import LazyReCompile
@@ -214,7 +215,7 @@ def getpydocspec(f, func):
214215
if s is None:
215216
return None
216217

217-
if not hasattr(f, "__name__") or s.groups()[0] != f.__name__:
218+
if not hasattr_safe(f, "__name__") or s.groups()[0] != f.__name__:
218219
return None
219220

220221
args = list()
@@ -276,8 +277,7 @@ def getfuncprops(func, f):
276277
argspec = ArgSpec(*argspec)
277278
fprops = FuncProps(func, argspec, is_bound_method)
278279
except (TypeError, KeyError, ValueError):
279-
with AttrCleaner(f):
280-
argspec = getpydocspec(f, func)
280+
argspec = getpydocspec(f, func)
281281
if argspec is None:
282282
return None
283283
if inspect.ismethoddescriptor(f):
@@ -392,6 +392,47 @@ def get_encoding_file(fname):
392392
return "ascii"
393393

394394

395+
if not py3:
396+
397+
def getattr_safe(obj, name):
398+
"""side effect free getattr"""
399+
if not is_new_style(obj):
400+
return getattr(obj, name)
401+
402+
with AttrCleaner(obj):
403+
to_look_through = (
404+
obj.__mro__
405+
if inspect.isclass(obj)
406+
else (obj,) + type(obj).__mro__
407+
)
408+
for cls in to_look_through:
409+
if hasattr(cls, "__dict__") and name in cls.__dict__:
410+
result = cls.__dict__[name]
411+
if isinstance(result, MemberDescriptorType):
412+
result = getattr(obj, name)
413+
return result
414+
raise AttributeError(name)
415+
416+
417+
else:
418+
419+
def getattr_safe(obj, name):
420+
"""side effect free getattr (calls getattr_static)."""
421+
result = inspect.getattr_static(obj, name)
422+
# Slots are a MemberDescriptorType
423+
if isinstance(result, MemberDescriptorType):
424+
result = getattr(obj, name)
425+
return result
426+
427+
428+
def hasattr_safe(obj, name):
429+
try:
430+
getattr_safe(obj, name)
431+
return True
432+
except AttributeError:
433+
return False
434+
435+
395436
if py3:
396437

397438
def get_source_unicode(obj):

bpython/simpleeval.py

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
from . import line as line_properties
4040
from ._py3compat import py3
41-
from .inspection import is_new_style, AttrCleaner
41+
from .inspection import getattr_safe
4242

4343
_string_type_nodes = (ast.Str, ast.Bytes) if py3 else (ast.Str,)
4444
_numeric_types = (int, float, complex) + (() if py3 else (long,))
@@ -163,14 +163,15 @@ def _convert(node):
163163
if isinstance(node, ast.Attribute):
164164
obj = _convert(node.value)
165165
attr = node.attr
166-
return safe_get_attribute(obj, attr)
166+
return getattr_safe(obj, attr)
167167

168168
raise ValueError("malformed string")
169169

170170
return _convert(node_or_string)
171171

172172

173173
def safe_getitem(obj, index):
174+
""" Safely tries to access obj[index] """
174175
if type(obj) in (list, tuple, dict, bytes) + string_types:
175176
try:
176177
return obj[index]
@@ -251,44 +252,3 @@ def evaluate_current_attribute(cursor_offset, line, namespace=None):
251252
"can't lookup attribute %s on %r" % (attr.word, obj)
252253
)
253254

254-
255-
def safe_get_attribute(obj, attr):
256-
"""Gets attributes without triggering descriptors on new-style classes"""
257-
if is_new_style(obj):
258-
with AttrCleaner(obj):
259-
result = safe_get_attribute_new_style(obj, attr)
260-
if isinstance(result, member_descriptor):
261-
# will either be the same slot descriptor or the value
262-
return getattr(obj, attr)
263-
return result
264-
return getattr(obj, attr)
265-
266-
267-
class _ClassWithSlots(object):
268-
__slots__ = ["a"]
269-
270-
271-
member_descriptor = type(_ClassWithSlots.a)
272-
273-
274-
def safe_get_attribute_new_style(obj, attr):
275-
"""Returns approximately the attribute returned by getattr(obj, attr)
276-
277-
The object returned ought to be callable if getattr(obj, attr) was.
278-
Fake callable objects may be returned instead, in order to avoid executing
279-
arbitrary code in descriptors.
280-
281-
If the object is an instance of a class that uses __slots__, will return
282-
the member_descriptor object instead of the value.
283-
"""
284-
if not is_new_style(obj):
285-
raise ValueError("%r is not a new-style class or object" % obj)
286-
to_look_through = (
287-
obj.__mro__ if inspect.isclass(obj) else (obj,) + type(obj).__mro__
288-
)
289-
290-
for cls in to_look_through:
291-
if hasattr(cls, "__dict__") and attr in cls.__dict__:
292-
return cls.__dict__[attr]
293-
294-
raise AttributeError()

bpython/test/test_autocomplete.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ def asserts_when_called(self):
251251
class Slots(object):
252252
__slots__ = ["a", "b"]
253253

254+
class OverriddenGetattribute(Foo):
255+
def __getattribute__(self,name):
256+
raise AssertionError("custom get attribute invoked")
254257

255258
class TestAttrCompletion(unittest.TestCase):
256259
@classmethod
@@ -298,12 +301,20 @@ def test_descriptor_attributes_not_run(self):
298301
com.matches(2, "a.", locals_={"a": Properties()}),
299302
set(["a.b", "a.a", "a.method", "a.asserts_when_called"]),
300303
)
304+
305+
def test_custom_get_attribute_not_invoked(self):
306+
com = autocomplete.AttrCompletion()
307+
self.assertSetEqual(
308+
com.matches(2, "a.", locals_={"a": OverriddenGetattribute()}),
309+
set(["a.b", "a.a", "a.method"]),
310+
)
311+
301312

302313
def test_slots_not_crash(self):
303314
com = autocomplete.AttrCompletion()
304315
self.assertSetEqual(
305316
com.matches(2, "A.", locals_={"A": Slots}),
306-
set(["A.b", "A.a", "A.mro"]),
317+
set(["A.b", "A.a"]),
307318
)
308319

309320

bpython/test/test_inspection.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,124 @@ def test_get_source_file(self):
155155
self.assertEqual(encoding, "utf-8")
156156

157157

158+
class A(object):
159+
a = "a"
160+
161+
162+
class B(A):
163+
b = "b"
164+
165+
166+
class Property(object):
167+
@property
168+
def prop(self):
169+
raise AssertionError("Property __get__ executed")
170+
171+
172+
class Slots(object):
173+
__slots__ = ["s1", "s2", "s3"]
174+
175+
if not py3:
176+
177+
@property
178+
def s3(self):
179+
raise AssertionError("Property __get__ executed")
180+
181+
182+
class SlotsSubclass(Slots):
183+
@property
184+
def s4(self):
185+
raise AssertionError("Property __get__ executed")
186+
187+
188+
class OverriddenGetattr(object):
189+
def __getattr__(self, attr):
190+
raise AssertionError("custom __getattr__ executed")
191+
192+
a = 1
193+
194+
195+
class OverriddenGetattribute(object):
196+
def __getattribute__(self, attr):
197+
raise AssertionError("custom __getattribute__ executed")
198+
199+
a = 1
200+
201+
202+
class OverriddenMRO(object):
203+
def __mro__(self):
204+
raise AssertionError("custom mro executed")
205+
206+
a = 1
207+
208+
member_descriptor = type(Slots.s1)
209+
210+
class TestSafeGetAttribute(unittest.TestCase):
211+
def test_lookup_on_object(self):
212+
a = A()
213+
a.x = 1
214+
self.assertEqual(inspection.getattr_safe(a, "x"), 1)
215+
self.assertEqual(inspection.getattr_safe(a, "a"), "a")
216+
b = B()
217+
b.y = 2
218+
self.assertEqual(inspection.getattr_safe(b, "y"), 2)
219+
self.assertEqual(inspection.getattr_safe(b, "a"), "a")
220+
self.assertEqual(inspection.getattr_safe(b, "b"), "b")
221+
222+
self.assertEqual(inspection.hasattr_safe(b, "y"), True)
223+
self.assertEqual(inspection.hasattr_safe(b, "b"), True)
224+
225+
226+
def test_avoid_running_properties(self):
227+
p = Property()
228+
self.assertEqual(inspection.getattr_safe(p, "prop"), Property.prop)
229+
self.assertEqual(inspection.hasattr_safe(p, "prop"), True)
230+
231+
def test_lookup_with_slots(self):
232+
s = Slots()
233+
s.s1 = "s1"
234+
self.assertEqual(inspection.getattr_safe(s, "s1"), "s1")
235+
with self.assertRaises(AttributeError):
236+
inspection.getattr_safe(s, "s2")
237+
238+
self.assertEqual(inspection.hasattr_safe(s, "s1"), True)
239+
self.assertEqual(inspection.hasattr_safe(s, "s2"), False)
240+
241+
def test_lookup_on_slots_classes(self):
242+
sga = inspection.getattr_safe
243+
s = SlotsSubclass()
244+
self.assertIsInstance(sga(Slots, "s1"), member_descriptor)
245+
self.assertIsInstance(sga(SlotsSubclass, "s1"), member_descriptor)
246+
self.assertIsInstance(sga(SlotsSubclass, "s4"), property)
247+
self.assertIsInstance(sga(s, "s4"), property)
248+
249+
self.assertEqual(inspection.hasattr_safe(s, "s1"), False)
250+
self.assertEqual(inspection.hasattr_safe(s, "s4"), True)
251+
252+
@unittest.skipIf(py3, "Py 3 doesn't allow slots and prop in same class")
253+
def test_lookup_with_property_and_slots(self):
254+
sga = inspection.getattr_safe
255+
s = SlotsSubclass()
256+
self.assertIsInstance(sga(Slots, "s3"), property)
257+
self.assertEqual(inspection.getattr_safe(s, "s3"), Slots.__dict__["s3"])
258+
self.assertIsInstance(sga(SlotsSubclass, "s3"), property)
259+
260+
def test_lookup_on_overridden_methods(self):
261+
sga = inspection.getattr_safe
262+
self.assertEqual(sga(OverriddenGetattr(), "a"), 1)
263+
self.assertEqual(sga(OverriddenGetattribute(), "a"), 1)
264+
self.assertEqual(sga(OverriddenMRO(), "a"), 1)
265+
with self.assertRaises(AttributeError):
266+
sga(OverriddenGetattr(), "b")
267+
with self.assertRaises(AttributeError):
268+
sga(OverriddenGetattribute(), "b")
269+
with self.assertRaises(AttributeError):
270+
sga(OverriddenMRO(), "b")
271+
272+
self.assertEqual(inspection.hasattr_safe(OverriddenGetattr(), "b"), False)
273+
self.assertEqual(inspection.hasattr_safe(OverriddenGetattribute(), "b"), False)
274+
self.assertEqual(inspection.hasattr_safe(OverriddenMRO(), "b"), False)
275+
276+
158277
if __name__ == "__main__":
159278
unittest.main()

0 commit comments

Comments
 (0)