499 lines
15 KiB
Python
499 lines
15 KiB
Python
from compiler import parse, ast, pycodegen
|
|
import py
|
|
import __builtin__, sys
|
|
|
|
passthroughex = (KeyboardInterrupt, SystemExit, MemoryError)
|
|
|
|
class Failure:
|
|
def __init__(self, node):
|
|
self.exc, self.value, self.tb = sys.exc_info()
|
|
self.node = node
|
|
#import traceback
|
|
#traceback.print_exc()
|
|
|
|
from py.__.magic.viewtype import View
|
|
|
|
class Interpretable(View):
|
|
"""A parse tree node with a few extra methods."""
|
|
explanation = None
|
|
|
|
def is_builtin(self, frame):
|
|
return False
|
|
|
|
def eval(self, frame):
|
|
# fall-back for unknown expression nodes
|
|
try:
|
|
expr = ast.Expression(self.__obj__)
|
|
expr.filename = '<eval>'
|
|
self.__obj__.filename = '<eval>'
|
|
co = pycodegen.ExpressionCodeGenerator(expr).getCode()
|
|
result = frame.eval(co)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
self.result = result
|
|
self.explanation = self.explanation or frame.repr(self.result)
|
|
|
|
def run(self, frame):
|
|
# fall-back for unknown statement nodes
|
|
try:
|
|
expr = ast.Module(None, ast.Stmt([self.__obj__]))
|
|
expr.filename = '<run>'
|
|
co = pycodegen.ModuleCodeGenerator(expr).getCode()
|
|
frame.exec_(co)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
|
|
def nice_explanation(self):
|
|
# uck! See CallFunc for where \n{ and \n} escape sequences are used
|
|
raw_lines = (self.explanation or '').split('\n')
|
|
# escape newlines not followed by { and }
|
|
lines = [raw_lines[0]]
|
|
for l in raw_lines[1:]:
|
|
if l.startswith('{') or l.startswith('}'):
|
|
lines.append(l)
|
|
else:
|
|
lines[-1] += '\\n' + l
|
|
|
|
result = lines[:1]
|
|
stack = [0]
|
|
stackcnt = [0]
|
|
for line in lines[1:]:
|
|
if line.startswith('{'):
|
|
if stackcnt[-1]:
|
|
s = 'and '
|
|
else:
|
|
s = 'where '
|
|
stack.append(len(result))
|
|
stackcnt[-1] += 1
|
|
stackcnt.append(0)
|
|
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
|
else:
|
|
assert line.startswith('}')
|
|
stack.pop()
|
|
stackcnt.pop()
|
|
result[stack[-1]] += line[1:]
|
|
assert len(stack) == 1
|
|
return '\n'.join(result)
|
|
|
|
class Name(Interpretable):
|
|
__view__ = ast.Name
|
|
|
|
def is_local(self, frame):
|
|
co = compile('%r in locals() is not globals()' % self.name, '?', 'eval')
|
|
try:
|
|
return frame.is_true(frame.eval(co))
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
return False
|
|
|
|
def is_global(self, frame):
|
|
co = compile('%r in globals()' % self.name, '?', 'eval')
|
|
try:
|
|
return frame.is_true(frame.eval(co))
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
return False
|
|
|
|
def is_builtin(self, frame):
|
|
co = compile('%r not in locals() and %r not in globals()' % (
|
|
self.name, self.name), '?', 'eval')
|
|
try:
|
|
return frame.is_true(frame.eval(co))
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
return False
|
|
|
|
def eval(self, frame):
|
|
super(Name, self).eval(frame)
|
|
if not self.is_local(frame):
|
|
self.explanation = self.name
|
|
|
|
class Compare(Interpretable):
|
|
__view__ = ast.Compare
|
|
|
|
def eval(self, frame):
|
|
expr = Interpretable(self.expr)
|
|
expr.eval(frame)
|
|
for operation, expr2 in self.ops:
|
|
expr2 = Interpretable(expr2)
|
|
expr2.eval(frame)
|
|
self.explanation = "%s %s %s" % (
|
|
expr.explanation, operation, expr2.explanation)
|
|
co = compile("__exprinfo_left %s __exprinfo_right" % operation,
|
|
'?', 'eval')
|
|
try:
|
|
self.result = frame.eval(co, __exprinfo_left=expr.result,
|
|
__exprinfo_right=expr2.result)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
if not frame.is_true(self.result):
|
|
break
|
|
expr = expr2
|
|
|
|
class And(Interpretable):
|
|
__view__ = ast.And
|
|
|
|
def eval(self, frame):
|
|
explanations = []
|
|
for expr in self.nodes:
|
|
expr = Interpretable(expr)
|
|
expr.eval(frame)
|
|
explanations.append(expr.explanation)
|
|
self.result = expr.result
|
|
if not frame.is_true(expr.result):
|
|
break
|
|
self.explanation = '(' + ' and '.join(explanations) + ')'
|
|
|
|
class Or(Interpretable):
|
|
__view__ = ast.Or
|
|
|
|
def eval(self, frame):
|
|
explanations = []
|
|
for expr in self.nodes:
|
|
expr = Interpretable(expr)
|
|
expr.eval(frame)
|
|
explanations.append(expr.explanation)
|
|
self.result = expr.result
|
|
if frame.is_true(expr.result):
|
|
break
|
|
self.explanation = '(' + ' or '.join(explanations) + ')'
|
|
|
|
|
|
# == Unary operations ==
|
|
keepalive = []
|
|
for astclass, astpattern in {
|
|
ast.Not : 'not __exprinfo_expr',
|
|
ast.Invert : '(~__exprinfo_expr)',
|
|
}.items():
|
|
|
|
class UnaryArith(Interpretable):
|
|
__view__ = astclass
|
|
|
|
def eval(self, frame, astpattern=astpattern,
|
|
co=compile(astpattern, '?', 'eval')):
|
|
expr = Interpretable(self.expr)
|
|
expr.eval(frame)
|
|
self.explanation = astpattern.replace('__exprinfo_expr',
|
|
expr.explanation)
|
|
try:
|
|
self.result = frame.eval(co, __exprinfo_expr=expr.result)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
|
|
keepalive.append(UnaryArith)
|
|
|
|
# == Binary operations ==
|
|
for astclass, astpattern in {
|
|
ast.Add : '(__exprinfo_left + __exprinfo_right)',
|
|
ast.Sub : '(__exprinfo_left - __exprinfo_right)',
|
|
ast.Mul : '(__exprinfo_left * __exprinfo_right)',
|
|
ast.Div : '(__exprinfo_left / __exprinfo_right)',
|
|
ast.Mod : '(__exprinfo_left % __exprinfo_right)',
|
|
ast.Power : '(__exprinfo_left ** __exprinfo_right)',
|
|
}.items():
|
|
|
|
class BinaryArith(Interpretable):
|
|
__view__ = astclass
|
|
|
|
def eval(self, frame, astpattern=astpattern,
|
|
co=compile(astpattern, '?', 'eval')):
|
|
left = Interpretable(self.left)
|
|
left.eval(frame)
|
|
right = Interpretable(self.right)
|
|
right.eval(frame)
|
|
self.explanation = (astpattern
|
|
.replace('__exprinfo_left', left .explanation)
|
|
.replace('__exprinfo_right', right.explanation))
|
|
try:
|
|
self.result = frame.eval(co, __exprinfo_left=left.result,
|
|
__exprinfo_right=right.result)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
|
|
keepalive.append(BinaryArith)
|
|
|
|
|
|
class CallFunc(Interpretable):
|
|
__view__ = ast.CallFunc
|
|
|
|
def is_bool(self, frame):
|
|
co = compile('isinstance(__exprinfo_value, bool)', '?', 'eval')
|
|
try:
|
|
return frame.is_true(frame.eval(co, __exprinfo_value=self.result))
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
return False
|
|
|
|
def eval(self, frame):
|
|
node = Interpretable(self.node)
|
|
node.eval(frame)
|
|
explanations = []
|
|
vars = {'__exprinfo_fn': node.result}
|
|
source = '__exprinfo_fn('
|
|
for a in self.args:
|
|
if isinstance(a, ast.Keyword):
|
|
keyword = a.name
|
|
a = a.expr
|
|
else:
|
|
keyword = None
|
|
a = Interpretable(a)
|
|
a.eval(frame)
|
|
argname = '__exprinfo_%d' % len(vars)
|
|
vars[argname] = a.result
|
|
if keyword is None:
|
|
source += argname + ','
|
|
explanations.append(a.explanation)
|
|
else:
|
|
source += '%s=%s,' % (keyword, argname)
|
|
explanations.append('%s=%s' % (keyword, a.explanation))
|
|
if self.star_args:
|
|
star_args = Interpretable(self.star_args)
|
|
star_args.eval(frame)
|
|
argname = '__exprinfo_star'
|
|
vars[argname] = star_args.result
|
|
source += '*' + argname + ','
|
|
explanations.append('*' + star_args.explanation)
|
|
if self.dstar_args:
|
|
dstar_args = Interpretable(self.dstar_args)
|
|
dstar_args.eval(frame)
|
|
argname = '__exprinfo_kwds'
|
|
vars[argname] = dstar_args.result
|
|
source += '**' + argname + ','
|
|
explanations.append('**' + dstar_args.explanation)
|
|
self.explanation = "%s(%s)" % (
|
|
node.explanation, ', '.join(explanations))
|
|
if source.endswith(','):
|
|
source = source[:-1]
|
|
source += ')'
|
|
co = compile(source, '?', 'eval')
|
|
try:
|
|
self.result = frame.eval(co, **vars)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
if not node.is_builtin(frame) or not self.is_bool(frame):
|
|
r = frame.repr(self.result)
|
|
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
|
|
|
class Getattr(Interpretable):
|
|
__view__ = ast.Getattr
|
|
|
|
def eval(self, frame):
|
|
expr = Interpretable(self.expr)
|
|
expr.eval(frame)
|
|
co = compile('__exprinfo_expr.%s' % self.attrname, '?', 'eval')
|
|
try:
|
|
self.result = frame.eval(co, __exprinfo_expr=expr.result)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
self.explanation = '%s.%s' % (expr.explanation, self.attrname)
|
|
# if the attribute comes from the instance, its value is interesting
|
|
co = compile('hasattr(__exprinfo_expr, "__dict__") and '
|
|
'%r in __exprinfo_expr.__dict__' % self.attrname,
|
|
'?', 'eval')
|
|
try:
|
|
from_instance = frame.is_true(
|
|
frame.eval(co, __exprinfo_expr=expr.result))
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
from_instance = True
|
|
if from_instance:
|
|
r = frame.repr(self.result)
|
|
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
|
|
|
# == Re-interpretation of full statements ==
|
|
import __builtin__
|
|
BuiltinAssertionError = __builtin__.AssertionError
|
|
|
|
class Assert(Interpretable):
|
|
__view__ = ast.Assert
|
|
|
|
def run(self, frame):
|
|
test = Interpretable(self.test)
|
|
test.eval(frame)
|
|
# simplify 'assert False where False = ...'
|
|
if (test.explanation.startswith('False\n{False = ') and
|
|
test.explanation.endswith('\n}')):
|
|
test.explanation = test.explanation[15:-2]
|
|
# print the result as 'assert <explanation>'
|
|
self.result = test.result
|
|
self.explanation = 'assert ' + test.explanation
|
|
if not frame.is_true(test.result):
|
|
try:
|
|
raise BuiltinAssertionError
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
|
|
class Assign(Interpretable):
|
|
__view__ = ast.Assign
|
|
|
|
def run(self, frame):
|
|
expr = Interpretable(self.expr)
|
|
expr.eval(frame)
|
|
self.result = expr.result
|
|
self.explanation = '... = ' + expr.explanation
|
|
# fall-back-run the rest of the assignment
|
|
ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr'))
|
|
mod = ast.Module(None, ast.Stmt([ass]))
|
|
mod.filename = '<run>'
|
|
co = pycodegen.ModuleCodeGenerator(mod).getCode()
|
|
try:
|
|
frame.exec_(co, __exprinfo_expr=expr.result)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
raise Failure(self)
|
|
|
|
class Discard(Interpretable):
|
|
__view__ = ast.Discard
|
|
|
|
def run(self, frame):
|
|
expr = Interpretable(self.expr)
|
|
expr.eval(frame)
|
|
self.result = expr.result
|
|
self.explanation = expr.explanation
|
|
|
|
class Stmt(Interpretable):
|
|
__view__ = ast.Stmt
|
|
|
|
def run(self, frame):
|
|
for stmt in self.nodes:
|
|
stmt = Interpretable(stmt)
|
|
stmt.run(frame)
|
|
|
|
|
|
def report_failure(e):
|
|
explanation = e.node.nice_explanation()
|
|
if explanation:
|
|
explanation = ", in: " + explanation
|
|
else:
|
|
explanation = ""
|
|
print "%s: %s%s" % (e.exc.__name__, e.value, explanation)
|
|
|
|
def check(s, frame=None):
|
|
if frame is None:
|
|
import sys
|
|
frame = sys._getframe(1)
|
|
frame = py.code.Frame(frame)
|
|
expr = parse(s, 'eval')
|
|
assert isinstance(expr, ast.Expression)
|
|
node = Interpretable(expr.node)
|
|
try:
|
|
node.eval(frame)
|
|
except passthroughex:
|
|
raise
|
|
except Failure, e:
|
|
report_failure(e)
|
|
else:
|
|
if not frame.is_true(node.result):
|
|
print "assertion failed:", node.nice_explanation()
|
|
|
|
|
|
###########################################################
|
|
# API / Entry points
|
|
# #########################################################
|
|
|
|
def interpret(source, frame, should_fail=False):
|
|
module = Interpretable(parse(source, 'exec').node)
|
|
#print "got module", module
|
|
if isinstance(frame, py.std.types.FrameType):
|
|
frame = py.code.Frame(frame)
|
|
try:
|
|
module.run(frame)
|
|
except Failure, e:
|
|
return getfailure(e)
|
|
except passthroughex:
|
|
raise
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
if should_fail:
|
|
return "(inconsistently failed then succeeded)"
|
|
else:
|
|
return None
|
|
|
|
def getmsg(excinfo):
|
|
if isinstance(excinfo, tuple):
|
|
excinfo = py.code.ExceptionInfo(excinfo)
|
|
#frame, line = gettbline(tb)
|
|
#frame = py.code.Frame(frame)
|
|
#return interpret(line, frame)
|
|
|
|
tb = excinfo.traceback[-1]
|
|
source = str(tb.statement).strip()
|
|
x = interpret(source, tb.frame, should_fail=True)
|
|
if not isinstance(x, str):
|
|
raise TypeError, "interpret returned non-string %r" % (x,)
|
|
return x
|
|
|
|
def getfailure(e):
|
|
explanation = e.node.nice_explanation()
|
|
if str(e.value):
|
|
lines = explanation.split('\n')
|
|
lines[0] += " << %s" % (e.value,)
|
|
explanation = '\n'.join(lines)
|
|
text = "%s: %s" % (e.exc.__name__, explanation)
|
|
if text.startswith('AssertionError: assert '):
|
|
text = text[16:]
|
|
return text
|
|
|
|
def run(s, frame=None):
|
|
if frame is None:
|
|
import sys
|
|
frame = sys._getframe(1)
|
|
frame = py.code.Frame(frame)
|
|
module = Interpretable(parse(s, 'exec').node)
|
|
try:
|
|
module.run(frame)
|
|
except Failure, e:
|
|
report_failure(e)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# example:
|
|
def f():
|
|
return 5
|
|
def g():
|
|
return 3
|
|
def h(x):
|
|
return 'never'
|
|
check("f() * g() == 5")
|
|
check("not f()")
|
|
check("not (f() and g() or 0)")
|
|
check("f() == g()")
|
|
i = 4
|
|
check("i == f()")
|
|
check("len(f()) == 0")
|
|
check("isinstance(2+3+4, float)")
|
|
|
|
run("x = i")
|
|
check("x == 5")
|
|
|
|
run("assert not f(), 'oops'")
|
|
run("a, b, c = 1, 2")
|
|
run("a, b, c = f()")
|
|
|
|
check("max([f(),g()]) == 4")
|
|
check("'hello'[g()] == 'h'")
|
|
run("'guk%d' % h(f())")
|