Skip to content

Commit d76cc10

Browse files
property deal with slots in safe attr lookup
1 parent a4b174c commit d76cc10

File tree

4 files changed

+71
-22
lines changed

4 files changed

+71
-22
lines changed

bpython/autocomplete.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,12 @@ 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 = inspection.safe_get_attribute(obj, word)
711-
matches.append(_callable_postfix(attr_obj, "%s.%s" % (expr, word)))
710+
try:
711+
attr_obj = inspection.safe_get_attribute(obj, word)
712+
except AttributeError:
713+
matches.append("%s.%s" % (expr, word))
714+
else:
715+
matches.append(_callable_postfix(attr_obj, "%s.%s" % (expr, word)))
712716
return matches
713717

714718

bpython/inspection.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,39 +282,49 @@ def is_callable(obj):
282282
return callable(obj)
283283

284284

285-
class AttributeIsEmptySlot(object):
286-
pass
287-
288-
289285
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-
"""
286+
"""Gets attributes without triggering descriptors on new-style clases"""
295287
if is_new_style(obj):
296-
return safe_get_attribute_new_style(obj, attr)
288+
result = safe_get_attribute_new_style(obj, attr)
289+
if isinstance(result, member_descriptor):
290+
# will either be the same slot descriptor or the value
291+
return getattr(obj, attr)
292+
return result
297293
return getattr(obj, attr)
298294

299295

296+
class _ClassWithSlots(object):
297+
__slots__ = ['a']
298+
member_descriptor = type(_ClassWithSlots.a)
299+
300+
300301
def safe_get_attribute_new_style(obj, attr):
302+
"""Returns approximately the attribute returned by getattr(obj, attr)
303+
304+
The object returned ought to be callable if getattr(obj, attr) was.
305+
Fake callable objects may be returned instead, in order to avoid executing
306+
arbitrary code in descriptors.
307+
308+
If the object is an instance of a class that uses __slots__, will return
309+
the member_descriptor object instead of the value.
310+
"""
301311
if not is_new_style(obj):
302312
raise ValueError("%r is not a new-style class or object" % obj)
303313
to_look_through = (obj.mro()
304314
if hasattr(obj, 'mro')
305315
else [obj] + type(obj).mro())
306316

317+
found_in_slots = hasattr(obj, '__slots__') and attr in obj.__slots__
307318
for cls in to_look_through:
308319
if hasattr(cls, '__dict__') and attr in cls.__dict__:
309320
return cls.__dict__[attr]
310-
elif hasattr(cls, '__slots__') and attr in cls.__slots__:
311-
try:
312-
return getattr(cls, attr)
313-
except AttributeError:
314-
return AttributeIsEmptySlot
321+
322+
if found_in_slots:
323+
return AttributeIsEmptySlot
315324

316325
raise AttributeError()
317326

327+
318328
def get_attribute(obj, attr):
319329
cls = type(obj)
320330
for cls in [obj] + cls.mro():

bpython/test/test_autocomplete.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ def asserts_when_called(self):
242242
raise AssertionError("getter method called")
243243

244244

245+
class Slots(object):
246+
__slots__ = ['a', 'b']
247+
248+
245249
class TestAttrCompletion(unittest.TestCase):
246250
@classmethod
247251
def setUpClass(cls):
@@ -280,6 +284,11 @@ def test_descriptor_attributes_not_run(self):
280284
set(['a.b', 'a.a', 'a.method(',
281285
'a.asserts_when_called']))
282286

287+
def test_slots_not_crash(self):
288+
com = autocomplete.AttrCompletion()
289+
self.assertSetEqual(com.matches(2, 'A.', locals_={'A': Slots}),
290+
set(['A.b', 'A.a', 'A.mro']))
291+
283292

284293
class TestArrayItemCompletion(unittest.TestCase):
285294
@classmethod

bpython/test/test_inspection.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,19 @@ def prop(self):
161161

162162

163163
class Slots(object):
164-
__slots__ = ['s1', 's2']
164+
__slots__ = ['s1', 's2', 's3']
165+
166+
@property
167+
def s3(self):
168+
raise AssertionError('Property __get__ executed')
169+
170+
171+
class SlotsSubclass(Slots):
172+
pass
165173

166174

175+
member_descriptor = type(Slots.s1)
176+
167177
class TestSafeGetAttribute(unittest.TestCase):
168178

169179
def test_lookup_on_object(self):
@@ -191,10 +201,26 @@ class Old: pass
191201
def test_lookup_with_slots(self):
192202
s = Slots()
193203
s.s1 = 's1'
194-
self.assertEquals(inspection.safe_get_attribute_new_style(s, 's1'), 's1')
195-
self.assertEquals(inspection.safe_get_attribute_new_style(s, 's2'),
196-
inspection.AttributeIsEmptySlot)
197-
204+
self.assertEquals(inspection.safe_get_attribute(s, 's1'), 's1')
205+
self.assertIsInstance(inspection.safe_get_attribute_new_style(s, 's1'),
206+
member_descriptor)
207+
with self.assertRaises(AttributeError):
208+
inspection.safe_get_attribute(s, 's2')
209+
self.assertIsInstance(inspection.safe_get_attribute_new_style(s, 's2'),
210+
member_descriptor)
211+
self.assertEquals(inspection.safe_get_attribute(s, 's3'),
212+
Slots.__dict__['s3'])
213+
214+
def test_lookup_on_slots_classes(self):
215+
sga = inspection.safe_get_attribute
216+
self.assertIsInstance(inspection.safe_get_attribute(Slots, 's1'),
217+
member_descriptor)
218+
self.assertIsInstance(inspection.safe_get_attribute(Slots, 's3'),
219+
property)
220+
self.assertIsInstance(inspection.safe_get_attribute(SlotsSubclass, 's1'),
221+
member_descriptor)
222+
self.assertIsInstance(inspection.safe_get_attribute(SlotsSubclass, 's3'),
223+
property)
198224

199225
if __name__ == '__main__':
200226
unittest.main()

0 commit comments

Comments
 (0)