Skip to content

Commit 2ca6b7c

Browse files
committed
Refactor to start using getattr_static for python3
Avoids having to use AttrCleaner in many contexts
1 parent ac93c74 commit 2ca6b7c

File tree

5 files changed

+106
-123
lines changed

5 files changed

+106
-123
lines changed

bpython/autocomplete.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,16 +356,15 @@ def attr_matches(self, text, namespace):
356356
obj = safe_eval(expr, namespace)
357357
except EvaluationError:
358358
return []
359-
with inspection.AttrCleaner(obj):
360-
matches = self.attr_lookup(obj, expr, attr)
359+
matches = self.attr_lookup(obj, expr, attr)
361360
return matches
362361

363362
def attr_lookup(self, obj, expr, attr):
364363
"""Second half of original attr_matches method factored out so it can
365364
be wrapped in a safe try/finally block in case anything bad happens to
366365
restore the original __getattribute__ method."""
367366
words = self.list_attributes(obj)
368-
if hasattr(obj, "__class__"):
367+
if inspection.has_attr_safe(obj, "__class__"):
369368
words.append("__class__")
370369
words = words + rlcompleter.get_class_members(obj.__class__)
371370
if not isinstance(obj.__class__, abc.ABCMeta):
@@ -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,7 @@ 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+
# TODO: do we need this done within an AttrCleaner?
681+
if inspection.is_callable(value):
682+
word += "("
685683
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 has_attr_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 get_attr_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 get_attr_safe(obj, name):
420+
"""side effect and AttrCleaner 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 has_attr_safe(obj, name):
429+
try:
430+
get_attr_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 get_attr_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 get_attr_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_inspection.py

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

157157

158+
class TestSafeGetAttribute(unittest.TestCase):
159+
def test_lookup_on_object(self):
160+
a = A()
161+
a.x = 1
162+
self.assertEqual(get_attr_safe(a, "x"), 1)
163+
self.assertEqual(get_attr_safe(a, "a"), "a")
164+
b = B()
165+
b.y = 2
166+
self.assertEqual(get_attr_safe(b, "y"), 2)
167+
self.assertEqual(get_attr_safe(b, "a"), "a")
168+
self.assertEqual(get_attr_safe(b, "b"), "b")
169+
170+
def test_avoid_running_properties(self):
171+
p = Property()
172+
self.assertEqual(get_attr_safe(p, "prop"), Property.prop)
173+
174+
def test_lookup_with_slots(self):
175+
s = Slots()
176+
s.s1 = "s1"
177+
self.assertEqual(get_attr_safe(s, "s1"), "s1")
178+
with self.assertRaises(AttributeError):
179+
get_attr_safe(s, "s2")
180+
181+
def test_lookup_on_slots_classes(self):
182+
sga = get_attr_safe
183+
s = SlotsSubclass()
184+
self.assertIsInstance(sga(Slots, "s1"), member_descriptor)
185+
self.assertIsInstance(sga(SlotsSubclass, "s1"), member_descriptor)
186+
self.assertIsInstance(sga(SlotsSubclass, "s4"), property)
187+
self.assertIsInstance(sga(s, "s4"), property)
188+
189+
@unittest.skipIf(py3, "Py 3 doesn't allow slots and prop in same class")
190+
def test_lookup_with_property_and_slots(self):
191+
sga = get_attr_safe
192+
s = SlotsSubclass()
193+
self.assertIsInstance(sga(Slots, "s3"), property)
194+
self.assertEqual(get_attr_safe(s, "s3"), Slots.__dict__["s3"])
195+
self.assertIsInstance(sga(SlotsSubclass, "s3"), property)
196+
197+
def test_lookup_on_overridden_methods(self):
198+
sga = get_attr_safe
199+
self.assertEqual(sga(OverriddenGetattr(), "a"), 1)
200+
self.assertEqual(sga(OverriddenGetattribute(), "a"), 1)
201+
self.assertEqual(sga(OverriddenMRO(), "a"), 1)
202+
with self.assertRaises(AttributeError):
203+
sga(OverriddenGetattr(), "b")
204+
with self.assertRaises(AttributeError):
205+
sga(OverriddenGetattribute(), "b")
206+
with self.assertRaises(AttributeError):
207+
sga(OverriddenMRO(), "b")
208+
209+
158210
if __name__ == "__main__":
159211
unittest.main()

bpython/test/test_simpleeval.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
simple_eval,
88
evaluate_current_expression,
99
EvaluationError,
10-
safe_get_attribute,
11-
safe_get_attribute_new_style,
1210
)
1311
from bpython.test import unittest
1412
from bpython._py3compat import py3
@@ -189,71 +187,5 @@ def __mro__(self):
189187
member_descriptor = type(Slots.s1)
190188

191189

192-
class TestSafeGetAttribute(unittest.TestCase):
193-
def test_lookup_on_object(self):
194-
a = A()
195-
a.x = 1
196-
self.assertEqual(safe_get_attribute_new_style(a, "x"), 1)
197-
self.assertEqual(safe_get_attribute_new_style(a, "a"), "a")
198-
b = B()
199-
b.y = 2
200-
self.assertEqual(safe_get_attribute_new_style(b, "y"), 2)
201-
self.assertEqual(safe_get_attribute_new_style(b, "a"), "a")
202-
self.assertEqual(safe_get_attribute_new_style(b, "b"), "b")
203-
204-
def test_avoid_running_properties(self):
205-
p = Property()
206-
self.assertEqual(safe_get_attribute_new_style(p, "prop"), Property.prop)
207-
208-
@unittest.skipIf(py3, "Old-style classes not in Python 3")
209-
def test_raises_on_old_style_class(self):
210-
class Old:
211-
pass
212-
213-
with self.assertRaises(ValueError):
214-
safe_get_attribute_new_style(Old, "asdf")
215-
216-
def test_lookup_with_slots(self):
217-
s = Slots()
218-
s.s1 = "s1"
219-
self.assertEqual(safe_get_attribute(s, "s1"), "s1")
220-
self.assertIsInstance(
221-
safe_get_attribute_new_style(s, "s1"), member_descriptor
222-
)
223-
with self.assertRaises(AttributeError):
224-
safe_get_attribute(s, "s2")
225-
self.assertIsInstance(
226-
safe_get_attribute_new_style(s, "s2"), member_descriptor
227-
)
228-
229-
def test_lookup_on_slots_classes(self):
230-
sga = safe_get_attribute
231-
s = SlotsSubclass()
232-
self.assertIsInstance(sga(Slots, "s1"), member_descriptor)
233-
self.assertIsInstance(sga(SlotsSubclass, "s1"), member_descriptor)
234-
self.assertIsInstance(sga(SlotsSubclass, "s4"), property)
235-
self.assertIsInstance(sga(s, "s4"), property)
236-
237-
@unittest.skipIf(py3, "Py 3 doesn't allow slots and prop in same class")
238-
def test_lookup_with_property_and_slots(self):
239-
sga = safe_get_attribute
240-
s = SlotsSubclass()
241-
self.assertIsInstance(sga(Slots, "s3"), property)
242-
self.assertEqual(safe_get_attribute(s, "s3"), Slots.__dict__["s3"])
243-
self.assertIsInstance(sga(SlotsSubclass, "s3"), property)
244-
245-
def test_lookup_on_overridden_methods(self):
246-
sga = safe_get_attribute
247-
self.assertEqual(sga(OverriddenGetattr(), "a"), 1)
248-
self.assertEqual(sga(OverriddenGetattribute(), "a"), 1)
249-
self.assertEqual(sga(OverriddenMRO(), "a"), 1)
250-
with self.assertRaises(AttributeError):
251-
sga(OverriddenGetattr(), "b")
252-
with self.assertRaises(AttributeError):
253-
sga(OverriddenGetattribute(), "b")
254-
with self.assertRaises(AttributeError):
255-
sga(OverriddenMRO(), "b")
256-
257-
258190
if __name__ == "__main__":
259191
unittest.main()

0 commit comments

Comments
 (0)