Skip to content

Commit ae6a7db

Browse files
committed
add wrapper around interpreter for handling ast parsing
1 parent da9914a commit ae6a7db

File tree

4 files changed

+145
-26
lines changed

4 files changed

+145
-26
lines changed

after/syntax/python.vim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
syntax match evalMarker /#?\s*$/ conceal cchar=#
1+
"syntax match evalMarker /#?\s*$/ conceal cchar=#

rplugin/python3/mokka/__init__.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22

33
import neovim
4-
from .interpreter import Interpreter
4+
from .wrapper import Wrapper
55

66

77
@neovim.plugin
@@ -26,23 +26,15 @@ def eval_until_current(self, args):
2626
lineno, _ = self.nvim.current.window.cursor
2727
buf = self.nvim.current.buffer
2828

29-
# match current indention level and
30-
# append spaces if last line end with ':'
31-
spaces = re.match(r'^(\s*)', buf[lineno-2]).group(1)
32-
spaces += ' ' if buf[lineno-2].strip().endswith(':') else ''
29+
wrapper = Wrapper(buf)
3330

34-
interpreter = Interpreter(filename=buf.name)
31+
try:
32+
rep = wrapper.eval_line(lineno)
33+
except ValueError:
34+
return
35+
36+
maxlen = 64
37+
short = rep[:maxlen-3] + '...' if len(rep) > maxlen else rep
38+
# TODO: get window width for dynamic lenght
3539

36-
# run with `pass` appended using precalulated spaces
37-
source = '\n'.join(buf[0:lineno-1]+[spaces+'pass'])
38-
39-
error_status = interpreter.exec(source)
40-
if error_status:
41-
buf[lineno-1] = buf[lineno-1] + ' # ' + str(error_status)
42-
# TODO: reformat error message (no filename if current file
43-
else:
44-
value = interpreter.eval(buf[lineno-1])
45-
rep = str(value).replace('\n', '')
46-
if len(rep) > 32:
47-
rep = rep[:29]+'...'
48-
buf[lineno-1] = buf[lineno-1] + ' # ' + rep
40+
buf[lineno-1] = buf[lineno-1] + ' # ' + short

rplugin/python3/mokka/interpreter.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
# pylint: disable=eval-used,exec-used,broad-except
12
import types
2-
from typing import Optional
3+
import ast
4+
from typing import Optional, Union
35

46

57
class Interpreter(object):
@@ -12,7 +14,7 @@ def __init__(self, filename: str) -> None:
1214
self.locals = {'__name__': '__console__', '__doc__': None}
1315
self.filename = filename
1416

15-
def compile(self, source: str, mode: str) -> types.CodeType:
17+
def compile(self, source: Union[ast.AST, str], mode: str) -> types.CodeType:
1618
"""
1719
Wrapper around builtin compile function.
1820
@@ -40,8 +42,22 @@ def eval(self, source: str) -> object:
4042
code = self.compile(source, 'eval')
4143
except (OverflowError, SyntaxError, ValueError) as exc:
4244
return exc
45+
# TODO: remove line number and filename from error, it's confusing
4346

44-
return eval(code, self.locals)
47+
return self.eval_code(code)
48+
49+
def eval_code(self, code: types.CodeType) -> object:
50+
"""
51+
Evaluate a compiled expression in the context of the virtual
52+
interpreter.
53+
54+
Returns:
55+
- the object resulting from evaluation if the expression is valid
56+
"""
57+
try:
58+
return eval(code, self.locals)
59+
except Exception as exc:
60+
return exc
4561

4662
def exec(self, source: str) -> Optional[Exception]:
4763
"""
@@ -57,6 +73,19 @@ def exec(self, source: str) -> Optional[Exception]:
5773
except (OverflowError, SyntaxError, ValueError) as exc:
5874
return exc
5975

60-
# TODO: handle other exceptions?
61-
exec(code, self.locals)
62-
return None
76+
return self.exec_code(code)
77+
78+
def exec_code(self, code: types.CodeType) -> Optional[Exception]:
79+
"""
80+
Execute the source inside the virtual interpreter.
81+
82+
Returns:
83+
- None if the execution was sucessfull
84+
- an exception if there is a SyntaxError, ValueError or OverflowError
85+
"""
86+
try:
87+
exec(code, self.locals)
88+
except Exception as exc:
89+
return exc
90+
else:
91+
return None

rplugin/python3/mokka/wrapper.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import ast
2+
from .interpreter import Interpreter
3+
4+
5+
class Wrapper(object):
6+
"""
7+
wrapper around Interpreter that parses the source and aranges for special
8+
syntax elements like assignments, for loops and with statements to be
9+
evaluated in a specific sence
10+
"""
11+
ignored_tokens = ('import', 'def', 'for', 'with')
12+
13+
def __init__(self, buf) -> None:
14+
self.filename = buf.name
15+
self.buf = buf # TODO: is this necessary?
16+
17+
try:
18+
self.ast = ast.parse('\n'.join(buf))
19+
self.msg = None
20+
except SyntaxError as exc:
21+
self.ast = None
22+
self.msg = self._format_exc(exc)
23+
24+
def _format_exc(self, exc: Exception) -> str:
25+
if isinstance(exc, SyntaxError):
26+
# text = exc.text.replace("\n", "")
27+
# return ('{0.__class__.__name__}: {1}, line {0.lineno}'
28+
# .format(exc, text))
29+
return '{0.__class__.__name__}, line {0.lineno}'.format(exc)
30+
31+
return '{0.__class__.__name__}: {0}'.format(exc)
32+
33+
def _format_value(self, value: object) -> str:
34+
return repr(value).replace('\n', '')
35+
36+
def eval_line(self, number: int) -> object:
37+
"""
38+
Evaluate a line by number
39+
40+
Arguments:
41+
- number: int
42+
number of line to evaluate
43+
44+
Returns:
45+
- result of evaluating the line
46+
"""
47+
48+
if self.ast:
49+
interpreter = Interpreter(self.filename)
50+
51+
cur_line = self.buf[number-1].strip()
52+
if (not cur_line) or any(cur_line.startswith(token)
53+
for token in self.ignored_tokens):
54+
raise ValueError()
55+
56+
# constuction area:
57+
# =================
58+
pre_nodes = []
59+
60+
def find_cur_node(node):
61+
if hasattr(node, 'lineno') and node.lineno == number:
62+
return node
63+
64+
if hasattr(node, 'body'):
65+
for subn in node.body:
66+
if subn.lineno > number:
67+
break
68+
pre_nodes.append(subn)
69+
nextn = subn
70+
else:
71+
return None
72+
73+
pre_nodes.pop()
74+
return find_cur_node(nextn)
75+
76+
node = find_cur_node(self.ast)
77+
78+
compiled = interpreter.compile(ast.Module(pre_nodes), 'exec')
79+
interpreter.exec_code(compiled)
80+
81+
if isinstance(node, ast.If):
82+
source = node.test
83+
else:
84+
source = node.value
85+
86+
compiled = interpreter.compile(ast.Expression(source), 'eval')
87+
value = interpreter.eval_code(compiled)
88+
# =================
89+
90+
return self._format_value(value)
91+
else:
92+
return self.msg
93+
94+
def eval_range(self, start: int, end: int) -> object:
95+
"""
96+
experimental
97+
"""
98+
pass

0 commit comments

Comments
 (0)