Skip to content

Commit 39b22ca

Browse files
Safely get attributes in attr completion
1 parent 0809e12 commit 39b22ca

File tree

4 files changed

+85
-2
lines changed

4 files changed

+85
-2
lines changed

bpython/autocomplete.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ def attr_lookup(obj, expr, attr):
707707
n = len(attr)
708708
for word in words:
709709
if method_match(word, n, attr) and word != "__builtins__":
710-
attr_obj = getattr(obj, word) # scary!
710+
attr_obj = inspection.safe_get_attribute(obj, word)
711711
matches.append(_callable_postfix(attr_obj, "%s.%s" % (expr, word)))
712712
return matches
713713

bpython/inspection.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,43 @@ def is_callable(obj):
282282
return callable(obj)
283283

284284

285+
class AttributeIsEmptySlot(object):
286+
pass
287+
288+
289+
def safe_get_attribute(obj, attr):
290+
"""Gets attributes without triggering descriptors on new-style clases
291+
292+
Returns AttributeIsEmptySlot if requested attribute does not have a value,
293+
but is a slot entry.
294+
"""
295+
if not is_new_style(obj):
296+
raise ValueError("%r is not a new-style class or object" % obj)
297+
to_look_through = (obj.mro()
298+
if hasattr(obj, 'mro')
299+
else [obj] + type(obj).mro())
300+
301+
for cls in to_look_through:
302+
if hasattr(cls, '__dict__') and attr in cls.__dict__:
303+
return cls.__dict__[attr]
304+
elif hasattr(cls, '__slots__') and attr in cls.__slots__:
305+
try:
306+
return getattr(cls, attr)
307+
except AttributeError:
308+
return AttributeIsEmptySlot
309+
310+
raise AttributeError()
311+
312+
def get_attribute(obj, attr):
313+
cls = type(obj)
314+
for cls in [obj] + cls.mro():
315+
if attr in cls.__dict__:
316+
return cls.__dict__[attr]
317+
break
318+
raise AttributeError
319+
320+
321+
285322
get_encoding_re = LazyReCompile(r'coding[:=]\s*([-\w.]+)')
286323

287324

bpython/test/test_autocomplete.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ def __getattr__(self, attr):
277277
def test_descriptor_attributes_not_run(self):
278278
com = autocomplete.AttrCompletion()
279279
self.assertSetEqual(com.matches(2, 'a.', locals_={'a': Properties()}),
280-
set(['a.asserts_when_called']))
280+
set(['a.b', 'a.a', 'a.method(',
281+
'a.asserts_when_called']))
281282

282283

283284
class TestArrayItemCompletion(unittest.TestCase):

bpython/test/test_inspection.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,50 @@ def test_get_source_file(self):
135135
self.assertEqual(encoding, 'utf-8')
136136

137137

138+
class A(object):
139+
a = 'a'
140+
141+
class B(A):
142+
b = 'b'
143+
144+
class Property(object):
145+
@property
146+
def prop(self):
147+
raise AssertionError('Property __get__ executed')
148+
149+
class Slots(object):
150+
__slots__ = ['s1', 's2']
151+
152+
class TestSafeGetAttribute(unittest.TestCase):
153+
154+
def test_lookup_on_object(self):
155+
a = A()
156+
a.x = 1
157+
self.assertEquals(inspection.safe_get_attribute(a, 'x'), 1)
158+
self.assertEquals(inspection.safe_get_attribute(a, 'a'), 'a')
159+
b = B()
160+
b.y = 2
161+
self.assertEquals(inspection.safe_get_attribute(b, 'y'), 2)
162+
self.assertEquals(inspection.safe_get_attribute(b, 'a'), 'a')
163+
self.assertEquals(inspection.safe_get_attribute(b, 'b'), 'b')
164+
165+
def test_avoid_running_properties(self):
166+
p = Property()
167+
self.assertEquals(inspection.safe_get_attribute(p, 'prop'),
168+
Property.prop)
169+
170+
def test_raises_on_old_style_class(self):
171+
class Old: pass
172+
with self.assertRaises(ValueError):
173+
inspection.safe_get_attribute(Old, 'asdf')
174+
175+
def test_lookup_with_slots(self):
176+
s = Slots()
177+
s.s1 = 's1'
178+
self.assertEquals(inspection.safe_get_attribute(s, 's1'), 's1')
179+
self.assertEquals(inspection.safe_get_attribute(s, 's2'),
180+
inspection.AttributeIsEmptySlot)
181+
182+
138183
if __name__ == '__main__':
139184
unittest.main()

0 commit comments

Comments
 (0)