Skip to content

Commit 488d5e6

Browse files
Evaluation of expression before dot
1 parent 8254e87 commit 488d5e6

File tree

4 files changed

+129
-65
lines changed

4 files changed

+129
-65
lines changed

bpython/line.py

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@
55
word."""
66
from __future__ import unicode_literals
77

8-
import re
9-
108
from itertools import chain
119
from collections import namedtuple
12-
from pygments.token import Token
1310

1411
from bpython.lazyre import LazyReCompile
15-
from bpython._py3compat import PythonLexer, py3
1612

1713
current_word_re = LazyReCompile(r'[\w_][\w0-9._]*[(]?')
1814
LinePart = namedtuple('LinePart', ['start', 'stop', 'word'])
@@ -99,11 +95,12 @@ def current_object(cursor_offset, line):
9995
return LinePart(start, start+len(s), s)
10096

10197

102-
current_object_attribute_re = LazyReCompile(r'([\w_][\w0-9_]*)')
98+
current_object_attribute_re = LazyReCompile(r'([\w_][\w0-9_]*)[.]?')
10399

104100

105101
def current_object_attribute(cursor_offset, line):
106102
"""If in attribute completion, the attribute being completed"""
103+
#TODO replace with more general current_expression_attribute
107104
match = current_word(cursor_offset, line)
108105
if match is None:
109106
return None
@@ -270,40 +267,15 @@ def current_indexed_member_access_member(cursor_offset, line):
270267
if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset:
271268
return LinePart(m.start(3), m.end(3), m.group(3))
272269

273-
current_simple_expression_re = LazyReCompile(
274-
r'''([a-zA-Z_][\w.]*)\[([a-zA-Z0-9_"']+)\]\.([\w.]*)''')
275-
276-
def _current_simple_expression(cursor_offset, line):
277-
"""
278-
Returns the current "simple expression" being attribute accessed
279-
280-
build asts from with increasing numbers of characters.
281-
Find the biggest valid ast.
282-
Once our attribute access is a subtree, stop
283-
284-
285-
"""
286-
for i in range(cursor):
287-
pass
288-
289-
290-
def current_simple_expression(cursor_offset, line):
291-
"""The expression attribute lookup being performed on
292-
293-
e.g. <foo[0][1].bar>.ba|z
294-
A "simple expression" contains only . lookup and [] indexing."""
295-
296-
297-
def current_simple_expression_attribute(cursor_offset, line):
298-
"""The attribute being looked up on a simple expression
299-
300-
e.g. foo[0][1].bar.<ba|z>
301-
A "simple expression" contains only . lookup and [] indexing."""
302-
303-
304-
305-
306-
307270

271+
current_expression_attribute_re = LazyReCompile(r'[.]\s*((?:[\w_][\w0-9_]*)|(?:))')
308272

309273

274+
def current_expression_attribute(cursor_offset, line):
275+
"""If after a dot, the attribute being completed"""
276+
#TODO replace with more general current_expression_attribute
277+
matches = current_expression_attribute_re.finditer(line)
278+
for m in matches:
279+
if (m.start(1) <= cursor_offset and m.end(1) >= cursor_offset):
280+
return LinePart(m.start(1), m.end(1), m.group(1))
281+
return None

bpython/simpleeval.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from ast import *
99
from six import string_types
1010

11+
from bpython import line as line_properties
12+
1113
class EvaluationError(Exception):
1214
"""Raised if an exception occurred in safe_eval."""
1315

@@ -21,7 +23,7 @@ def safe_eval(expr, namespace):
2123
# raise
2224
raise EvaluationError
2325

24-
def simple_eval(node_or_string, namespace={}):
26+
def simple_eval(node_or_string, namespace=None):
2527
"""
2628
Safely evaluate an expression node or a string containing a Python
2729
expression. The string or node provided may only consist of:
@@ -35,6 +37,8 @@ def simple_eval(node_or_string, namespace={}):
3537
3638
The optional namespace dict-like ought not to cause side effects on lookup
3739
"""
40+
if namespace is None:
41+
namespace = {}
3842
if isinstance(node_or_string, string_types):
3943
node_or_string = parse(node_or_string, mode='eval')
4044
if isinstance(node_or_string, Expression):
@@ -77,37 +81,65 @@ def _convert(node):
7781
raise ValueError('malformed string')
7882
return _convert(node_or_string)
7983

84+
8085
def safe_getitem(obj, index):
8186
if type(obj) in (list, tuple, dict, bytes) + string_types:
8287
return obj[index]
8388
raise ValueError('unsafe to lookup on object of type %s' % (type(obj), ))
8489

8590

86-
class AttributeSearcher(NodeVisitor):
87-
"""Search for a Load of an Attribute at col_offset"""
88-
def visit_attribute(self, node):
89-
print node.attribute
90-
91-
def _current_simple_expression(cursor_offset, line):
91+
def find_attribute_with_name(node, name):
92+
"""Based on ast.NodeVisitor"""
93+
if isinstance(node, Attribute) and node.attr == name:
94+
return node
95+
for field, value in iter_fields(node):
96+
if isinstance(value, list):
97+
for item in value:
98+
if isinstance(item, AST):
99+
r = find_attribute_with_name(item, name)
100+
if r:
101+
return r
102+
elif isinstance(value, AST):
103+
r = find_attribute_with_name(value, name)
104+
if r:
105+
return r
106+
107+
108+
def evaluate_current_expression(cursor_offset, line, namespace={}):
92109
"""
93-
Returns the current "simple expression" being attribute accessed
110+
Return evaluted expression to the right of the dot of current attribute.
94111
95112
build asts from with increasing numbers of characters.
96113
Find the biggest valid ast.
97114
Once our attribute access is a subtree, stop
98-
99-
100115
"""
101116

102117
# in case attribute is blank, e.g. foo.| -> foo.xxx|
103118
temp_line = line[:cursor_offset] + 'xxx' + line[cursor_offset:]
104119
temp_cursor = cursor_offset + 3
105-
106-
for i in range(temp_cursor-1, -1, -1):
107-
try:
108-
tree = parse(temp_line[i:temp_cursor])
109-
except SyntaxError:
110-
return None
111-
112-
113-
120+
temp_attribute = line_properties.current_expression_attribute(
121+
temp_cursor, temp_line)
122+
if temp_attribute is None:
123+
raise EvaluationError("No current attribute")
124+
attr_before_cursor = temp_line[temp_attribute.start:temp_cursor]
125+
126+
def parse_trees(cursor_offset, line):
127+
for i in range(cursor_offset-1, -1, -1):
128+
try:
129+
ast = parse(line[i:cursor_offset])
130+
yield ast
131+
except SyntaxError:
132+
continue
133+
134+
largest_ast = None
135+
for tree in parse_trees(temp_cursor, temp_line):
136+
attribute_access = find_attribute_with_name(tree, attr_before_cursor)
137+
if attribute_access:
138+
largest_ast = attribute_access.value
139+
140+
if largest_ast is None:
141+
raise EvaluationError("Corresponding ASTs to right of cursor are invalid")
142+
try:
143+
return simple_eval(largest_ast, namespace)
144+
except (ValueError, KeyError, IndexError):
145+
raise EvaluationError("Could not safely evaluate")

bpython/test/test_line_properties.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
current_string_literal_attr, current_indexed_member_access_identifier, \
99
current_indexed_member_access_identifier_with_index, \
1010
current_indexed_member_access_member, \
11-
current_simple_expression, current_simple_expression_attribute
11+
current_expression_attribute
1212

1313

1414
def cursor(s):
@@ -227,10 +227,6 @@ def test_simple(self):
227227
self.assertAccess('Object.<attr1|>.attr2')
228228
self.assertAccess('Object.<attr1|>.attr2')
229229

230-
def test_after_dot(self):
231-
self.assertAccess('Object.<attr1|>.')
232-
self.assertAccess('Object.attr1.|')
233-
234230

235231
class TestCurrentFromImportFrom(LineTestCase):
236232
def setUp(self):
@@ -348,6 +344,33 @@ def test_simple(self):
348344
self.assertAccess('abc[def].gh |i')
349345
self.assertAccess('abc[def]|')
350346

347+
class TestCurrentExpressionAttribute(LineTestCase):
348+
def setUp(self):
349+
self.func = current_expression_attribute
350+
351+
def test_simple(self):
352+
self.assertAccess('Object.<attr1|>.')
353+
self.assertAccess('Object.<|attr1>.')
354+
self.assertAccess('Object.(|)')
355+
self.assertAccess('Object.another.(|)')
356+
self.assertAccess('asdf asdf asdf.(abc|)')
357+
358+
def test_without_dot(self):
359+
self.assertAccess('Object|')
360+
self.assertAccess('Object|.')
361+
self.assertAccess('|Object.')
362+
363+
def test_with_whitespace(self):
364+
self.assertAccess('Object. <attr|>')
365+
self.assertAccess('Object .<attr|>')
366+
self.assertAccess('Object . <attr|>')
367+
self.assertAccess('Object .asdf attr|')
368+
self.assertAccess('Object .<asdf|> attr')
369+
self.assertAccess('Object. asdf attr|')
370+
self.assertAccess('Object. <asdf|> attr')
371+
self.assertAccess('Object . asdf attr|')
372+
self.assertAccess('Object . <asdf|> attr')
373+
351374

352375
if __name__ == '__main__':
353376
unittest.main()

bpython/test/test_simpleeval.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import ast
44

5-
from bpython.simpleeval import simple_eval
5+
from bpython.simpleeval import (simple_eval,
6+
evaluate_current_expression,
7+
EvaluationError)
68
from bpython.test import unittest
79

810

9-
class TestInspection(unittest.TestCase):
11+
class TestSimpleEval(unittest.TestCase):
1012
def assertMatchesStdlib(self, expr):
1113
self.assertEqual(ast.literal_eval(expr), simple_eval(expr))
1214

@@ -66,6 +68,41 @@ def test_nonexistant_names_raise(self):
6668
with self.assertRaises(KeyError):
6769
simple_eval('a')
6870

71+
class TestEvaluateCurrentExpression(unittest.TestCase):
72+
73+
def assertEvaled(self, line, value, ns=None):
74+
assert line.count('|') == 1
75+
cursor_offset = line.find('|')
76+
line = line.replace('|', '')
77+
self.assertEqual(evaluate_current_expression(cursor_offset, line, ns),
78+
value)
79+
80+
def assertCannotEval(self, line, ns=None):
81+
assert line.count('|') == 1
82+
cursor_offset = line.find('|')
83+
line = line.replace('|', '')
84+
with self.assertRaises(EvaluationError):
85+
evaluate_current_expression(cursor_offset, line, ns)
86+
87+
def test_simple(self):
88+
self.assertEvaled('[1].a|bc', [1])
89+
self.assertEvaled('[1].abc|', [1])
90+
self.assertEvaled('[1].|abc', [1])
91+
self.assertEvaled('[1]. |abc', [1])
92+
self.assertEvaled('[1] .|abc', [1])
93+
self.assertCannotEval('[1].abc |', [1])
94+
self.assertCannotEval('[1]. abc |', [1])
95+
self.assertCannotEval('[2][1].a|bc', [1])
96+
97+
def test_nonsense(self):
98+
self.assertEvaled('!@#$ [1].a|bc', [1])
99+
self.assertEvaled('--- [2][0].a|bc', 2)
100+
self.assertCannotEval('"asdf".centered()[1].a|bc')
101+
self.assertEvaled('"asdf"[1].a|bc', 's')
102+
103+
def test_with_namespace(self):
104+
self.assertEvaled('a[1].a|bc', 'd', {'a':'adsf'})
105+
self.assertCannotEval('a[1].a|bc', {})
69106

70107
if __name__ == '__main__':
71108
unittest.main()

0 commit comments

Comments
 (0)