Skip to content

Commit f55d7d5

Browse files
attribute lookup in simple_eval
1 parent 8be8f73 commit f55d7d5

File tree

4 files changed

+176
-6
lines changed

4 files changed

+176
-6
lines changed

bpython/simpleeval.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
from bpython import line as line_properties
3636
from bpython._py3compat import py3
37+
from bpython.inspection import is_new_style
3738

3839
_string_type_nodes = (ast.Str, ast.Bytes) if py3 else (ast.Str,)
3940
_numeric_types = (int, float, complex) + (() if py3 else (long,))
@@ -72,7 +73,9 @@ def safe_eval(expr, namespace):
7273
def simple_eval(node_or_string, namespace=None):
7374
"""
7475
Safely evaluate an expression node or a string containing a Python
75-
expression. The string or node provided may only consist of:
76+
expression without triggering any user code.
77+
78+
The string or node provided may only consist of:
7679
* the following Python literal structures: strings, numbers, tuples,
7780
lists, and dicts
7881
* variable names causing lookups in the passed in namespace or builtins
@@ -144,6 +147,12 @@ def _convert(node):
144147
index = _convert(node.slice.value)
145148
return safe_getitem(obj, index)
146149

150+
# this is a deviation from literal_eval: we allow attribute access
151+
if isinstance(node, ast.Attribute):
152+
obj = _convert(node.value)
153+
attr = node.attr
154+
return safe_get_attribute(obj, attr)
155+
147156
raise ValueError('malformed string')
148157
return _convert(node_or_string)
149158

@@ -225,3 +234,46 @@ def evaluate_current_attribute(cursor_offset, line, namespace=None):
225234
except AttributeError:
226235
raise EvaluationError(
227236
"can't lookup attribute %s on %r" % (attr.word, obj))
237+
238+
239+
def safe_get_attribute(obj, attr):
240+
"""Gets attributes without triggering descriptors on new-style clases"""
241+
if is_new_style(obj):
242+
result = safe_get_attribute_new_style(obj, attr)
243+
if isinstance(result, member_descriptor):
244+
# will either be the same slot descriptor or the value
245+
return getattr(obj, attr)
246+
return result
247+
return getattr(obj, attr)
248+
249+
250+
class _ClassWithSlots(object):
251+
__slots__ = ['a']
252+
member_descriptor = type(_ClassWithSlots.a)
253+
254+
255+
def safe_get_attribute_new_style(obj, attr):
256+
"""Returns approximately the attribute returned by getattr(obj, attr)
257+
258+
The object returned ought to be callable if getattr(obj, attr) was.
259+
Fake callable objects may be returned instead, in order to avoid executing
260+
arbitrary code in descriptors.
261+
262+
If the object is an instance of a class that uses __slots__, will return
263+
the member_descriptor object instead of the value.
264+
"""
265+
if not is_new_style(obj):
266+
raise ValueError("%r is not a new-style class or object" % obj)
267+
to_look_through = (obj.mro()
268+
if hasattr(obj, 'mro')
269+
else [obj] + type(obj).mro())
270+
271+
found_in_slots = hasattr(obj, '__slots__') and attr in obj.__slots__
272+
for cls in to_look_through:
273+
if hasattr(cls, '__dict__') and attr in cls.__dict__:
274+
return cls.__dict__[attr]
275+
276+
if found_in_slots:
277+
return AttributeIsEmptySlot
278+
279+
raise AttributeError()

bpython/test/test_autocomplete.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ def method(self, x):
235235
'In Python 3 there are no old style classes')
236236

237237

238+
class Properties(Foo):
239+
240+
@property
241+
def asserts_when_called(self):
242+
raise AssertionError("getter method called")
243+
244+
245+
class Slots(object):
246+
__slots__ = ['a', 'b']
247+
248+
238249
class TestAttrCompletion(unittest.TestCase):
239250
@classmethod
240251
def setUpClass(cls):
@@ -268,6 +279,17 @@ def __getattr__(self, attr):
268279
self.assertIn(u'a.__module__',
269280
self.com.matches(4, 'a.__', locals_=locals_))
270281

282+
def test_descriptor_attributes_not_run(self):
283+
com = autocomplete.AttrCompletion()
284+
self.assertSetEqual(com.matches(2, 'a.', locals_={'a': Properties()}),
285+
set(['a.b', 'a.a', 'a.method',
286+
'a.asserts_when_called']))
287+
288+
def test_slots_not_crash(self):
289+
com = autocomplete.AttrCompletion()
290+
self.assertSetEqual(com.matches(2, 'A.', locals_={'A': Slots}),
291+
set(['A.b', 'A.a', 'A.mro']))
292+
271293

272294
class TestExpressionAttributeCompletion(unittest.TestCase):
273295
@classmethod

bpython/test/test_line_properties.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def cursor(s):
1212
"""'ab|c' -> (2, 'abc')"""
1313
cursor_offset = s.index('|')
14-
line = s[:cursor_offset] + s[cursor_offset+1:]
14+
line = s[:cursor_offset] + s[cursor_offset + 1:]
1515
return cursor_offset, line
1616

1717

@@ -53,11 +53,12 @@ def encode(cursor_offset, line, result):
5353
if start < cursor_offset:
5454
encoded_line = encoded_line[:start] + '<' + encoded_line[start:]
5555
else:
56-
encoded_line = encoded_line[:start+1] + '<' + encoded_line[start+1:]
56+
encoded_line = (encoded_line[:start + 1] + '<' +
57+
encoded_line[start + 1:])
5758
if end < cursor_offset:
58-
encoded_line = encoded_line[:end+1] + '>' + encoded_line[end+1:]
59+
encoded_line = encoded_line[:end + 1] + '>' + encoded_line[end + 1:]
5960
else:
60-
encoded_line = encoded_line[:end+2] + '>' + encoded_line[end+2:]
61+
encoded_line = encoded_line[:end + 2] + '>' + encoded_line[end + 2:]
6162
return encoded_line
6263

6364

bpython/test/test_simpleeval.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55

66
from bpython.simpleeval import (simple_eval,
77
evaluate_current_expression,
8-
EvaluationError)
8+
EvaluationError,
9+
safe_get_attribute,
10+
safe_get_attribute_new_style)
911
from bpython.test import unittest
12+
from bpython._py3compat import py3
1013

1114

1215
class TestSimpleEval(unittest.TestCase):
@@ -86,6 +89,12 @@ def test_nonexistant_names_raise(self):
8689
with self.assertRaises(EvaluationError):
8790
simple_eval('a')
8891

92+
def test_attribute_access(self):
93+
class Foo(object):
94+
abc = 1
95+
96+
self.assertEqual(simple_eval('foo.abc', {'foo': Foo()}), 1)
97+
8998

9099
class TestEvaluateCurrentExpression(unittest.TestCase):
91100

@@ -123,5 +132,91 @@ def test_with_namespace(self):
123132
self.assertEvaled('a[1].a|bc', 'd', {'a': 'adsf'})
124133
self.assertCannotEval('a[1].a|bc', {})
125134

135+
136+
class A(object):
137+
a = 'a'
138+
139+
140+
class B(A):
141+
b = 'b'
142+
143+
144+
class Property(object):
145+
@property
146+
def prop(self):
147+
raise AssertionError('Property __get__ executed')
148+
149+
150+
class Slots(object):
151+
__slots__ = ['s1', 's2', 's3']
152+
153+
if not py3:
154+
@property
155+
def s3(self):
156+
raise AssertionError('Property __get__ executed')
157+
158+
159+
class SlotsSubclass(Slots):
160+
@property
161+
def s4(self):
162+
raise AssertionError('Property __get__ executed')
163+
164+
165+
member_descriptor = type(Slots.s1)
166+
167+
168+
class TestSafeGetAttribute(unittest.TestCase):
169+
170+
def test_lookup_on_object(self):
171+
a = A()
172+
a.x = 1
173+
self.assertEquals(safe_get_attribute_new_style(a, 'x'), 1)
174+
self.assertEquals(safe_get_attribute_new_style(a, 'a'), 'a')
175+
b = B()
176+
b.y = 2
177+
self.assertEquals(safe_get_attribute_new_style(b, 'y'), 2)
178+
self.assertEquals(safe_get_attribute_new_style(b, 'a'), 'a')
179+
self.assertEquals(safe_get_attribute_new_style(b, 'b'), 'b')
180+
181+
def test_avoid_running_properties(self):
182+
p = Property()
183+
self.assertEquals(safe_get_attribute_new_style(p, 'prop'),
184+
Property.prop)
185+
186+
@unittest.skipIf(py3, 'Old-style classes not in Python 3')
187+
def test_raises_on_old_style_class(self):
188+
class Old:
189+
pass
190+
with self.assertRaises(ValueError):
191+
safe_get_attribute_new_style(Old, 'asdf')
192+
193+
def test_lookup_with_slots(self):
194+
s = Slots()
195+
s.s1 = 's1'
196+
self.assertEquals(safe_get_attribute(s, 's1'), 's1')
197+
self.assertIsInstance(safe_get_attribute_new_style(s, 's1'),
198+
member_descriptor)
199+
with self.assertRaises(AttributeError):
200+
safe_get_attribute(s, 's2')
201+
self.assertIsInstance(safe_get_attribute_new_style(s, 's2'),
202+
member_descriptor)
203+
204+
def test_lookup_on_slots_classes(self):
205+
sga = safe_get_attribute
206+
s = SlotsSubclass()
207+
self.assertIsInstance(sga(Slots, 's1'), member_descriptor)
208+
self.assertIsInstance(sga(SlotsSubclass, 's1'), member_descriptor)
209+
self.assertIsInstance(sga(SlotsSubclass, 's4'), property)
210+
self.assertIsInstance(sga(s, 's4'), property)
211+
212+
@unittest.skipIf(py3, "Py 3 doesn't allow slots and prop in same class")
213+
def test_lookup_with_property_and_slots(self):
214+
sga = safe_get_attribute
215+
s = SlotsSubclass()
216+
self.assertIsInstance(sga(Slots, 's3'), property)
217+
self.assertEquals(safe_get_attribute(s, 's3'),
218+
Slots.__dict__['s3'])
219+
self.assertIsInstance(sga(SlotsSubclass, 's3'), property)
220+
126221
if __name__ == '__main__':
127222
unittest.main()

0 commit comments

Comments
 (0)