| """ |
| Find intermediate evalutation results in assert statements through builtin AST. |
| """ |
| import ast |
| import sys |
| |
| import _pytest._code |
| import py |
| from _pytest.assertion import util |
| u = py.builtin._totext |
| |
| |
| class AssertionError(util.BuiltinAssertionError): |
| def __init__(self, *args): |
| util.BuiltinAssertionError.__init__(self, *args) |
| if args: |
| # on Python2.6 we get len(args)==2 for: assert 0, (x,y) |
| # on Python2.7 and above we always get len(args) == 1 |
| # with args[0] being the (x,y) tuple. |
| if len(args) > 1: |
| toprint = args |
| else: |
| toprint = args[0] |
| try: |
| self.msg = u(toprint) |
| except Exception: |
| self.msg = u( |
| "<[broken __repr__] %s at %0xd>" |
| % (toprint.__class__, id(toprint))) |
| else: |
| f = _pytest._code.Frame(sys._getframe(1)) |
| try: |
| source = f.code.fullsource |
| if source is not None: |
| try: |
| source = source.getstatement(f.lineno, assertion=True) |
| except IndexError: |
| source = None |
| else: |
| source = str(source.deindent()).strip() |
| except py.error.ENOENT: |
| source = None |
| # this can also occur during reinterpretation, when the |
| # co_filename is set to "<run>". |
| if source: |
| self.msg = reinterpret(source, f, should_fail=True) |
| else: |
| self.msg = "<could not determine information>" |
| if not self.args: |
| self.args = (self.msg,) |
| |
| if sys.version_info > (3, 0): |
| AssertionError.__module__ = "builtins" |
| |
| if sys.platform.startswith("java"): |
| # See http://bugs.jython.org/issue1497 |
| _exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", |
| "ListComp", "GeneratorExp", "Yield", "Compare", "Call", |
| "Repr", "Num", "Str", "Attribute", "Subscript", "Name", |
| "List", "Tuple") |
| _stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign", |
| "AugAssign", "Print", "For", "While", "If", "With", "Raise", |
| "TryExcept", "TryFinally", "Assert", "Import", "ImportFrom", |
| "Exec", "Global", "Expr", "Pass", "Break", "Continue") |
| _expr_nodes = set(getattr(ast, name) for name in _exprs) |
| _stmt_nodes = set(getattr(ast, name) for name in _stmts) |
| def _is_ast_expr(node): |
| return node.__class__ in _expr_nodes |
| def _is_ast_stmt(node): |
| return node.__class__ in _stmt_nodes |
| else: |
| def _is_ast_expr(node): |
| return isinstance(node, ast.expr) |
| def _is_ast_stmt(node): |
| return isinstance(node, ast.stmt) |
| |
| try: |
| _Starred = ast.Starred |
| except AttributeError: |
| # Python 2. Define a dummy class so isinstance() will always be False. |
| class _Starred(object): pass |
| |
| |
| class Failure(Exception): |
| """Error found while interpreting AST.""" |
| |
| def __init__(self, explanation=""): |
| self.cause = sys.exc_info() |
| self.explanation = explanation |
| |
| |
| def reinterpret(source, frame, should_fail=False): |
| mod = ast.parse(source) |
| visitor = DebugInterpreter(frame) |
| try: |
| visitor.visit(mod) |
| except Failure: |
| failure = sys.exc_info()[1] |
| return getfailure(failure) |
| if should_fail: |
| return ("(assertion failed, but when it was re-run for " |
| "printing intermediate values, it did not fail. Suggestions: " |
| "compute assert expression before the assert or use --assert=plain)") |
| |
| def run(offending_line, frame=None): |
| if frame is None: |
| frame = _pytest._code.Frame(sys._getframe(1)) |
| return reinterpret(offending_line, frame) |
| |
| def getfailure(e): |
| explanation = util.format_explanation(e.explanation) |
| value = e.cause[1] |
| if str(value): |
| lines = explanation.split('\n') |
| lines[0] += " << %s" % (value,) |
| explanation = '\n'.join(lines) |
| text = "%s: %s" % (e.cause[0].__name__, explanation) |
| if text.startswith('AssertionError: assert '): |
| text = text[16:] |
| return text |
| |
| operator_map = { |
| ast.BitOr : "|", |
| ast.BitXor : "^", |
| ast.BitAnd : "&", |
| ast.LShift : "<<", |
| ast.RShift : ">>", |
| ast.Add : "+", |
| ast.Sub : "-", |
| ast.Mult : "*", |
| ast.Div : "/", |
| ast.FloorDiv : "//", |
| ast.Mod : "%", |
| ast.Eq : "==", |
| ast.NotEq : "!=", |
| ast.Lt : "<", |
| ast.LtE : "<=", |
| ast.Gt : ">", |
| ast.GtE : ">=", |
| ast.Pow : "**", |
| ast.Is : "is", |
| ast.IsNot : "is not", |
| ast.In : "in", |
| ast.NotIn : "not in" |
| } |
| |
| unary_map = { |
| ast.Not : "not %s", |
| ast.Invert : "~%s", |
| ast.USub : "-%s", |
| ast.UAdd : "+%s" |
| } |
| |
| |
| class DebugInterpreter(ast.NodeVisitor): |
| """Interpret AST nodes to gleam useful debugging information. """ |
| |
| def __init__(self, frame): |
| self.frame = frame |
| |
| def generic_visit(self, node): |
| # Fallback when we don't have a special implementation. |
| if _is_ast_expr(node): |
| mod = ast.Expression(node) |
| co = self._compile(mod) |
| try: |
| result = self.frame.eval(co) |
| except Exception: |
| raise Failure() |
| explanation = self.frame.repr(result) |
| return explanation, result |
| elif _is_ast_stmt(node): |
| mod = ast.Module([node]) |
| co = self._compile(mod, "exec") |
| try: |
| self.frame.exec_(co) |
| except Exception: |
| raise Failure() |
| return None, None |
| else: |
| raise AssertionError("can't handle %s" %(node,)) |
| |
| def _compile(self, source, mode="eval"): |
| return compile(source, "<assertion interpretation>", mode) |
| |
| def visit_Expr(self, expr): |
| return self.visit(expr.value) |
| |
| def visit_Module(self, mod): |
| for stmt in mod.body: |
| self.visit(stmt) |
| |
| def visit_Name(self, name): |
| explanation, result = self.generic_visit(name) |
| # See if the name is local. |
| source = "%r in locals() is not globals()" % (name.id,) |
| co = self._compile(source) |
| try: |
| local = self.frame.eval(co) |
| except Exception: |
| # have to assume it isn't |
| local = None |
| if local is None or not self.frame.is_true(local): |
| return name.id, result |
| return explanation, result |
| |
| def visit_Compare(self, comp): |
| left = comp.left |
| left_explanation, left_result = self.visit(left) |
| for op, next_op in zip(comp.ops, comp.comparators): |
| next_explanation, next_result = self.visit(next_op) |
| op_symbol = operator_map[op.__class__] |
| explanation = "%s %s %s" % (left_explanation, op_symbol, |
| next_explanation) |
| source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,) |
| co = self._compile(source) |
| try: |
| result = self.frame.eval(co, __exprinfo_left=left_result, |
| __exprinfo_right=next_result) |
| except Exception: |
| raise Failure(explanation) |
| try: |
| if not self.frame.is_true(result): |
| break |
| except KeyboardInterrupt: |
| raise |
| except: |
| break |
| left_explanation, left_result = next_explanation, next_result |
| |
| if util._reprcompare is not None: |
| res = util._reprcompare(op_symbol, left_result, next_result) |
| if res: |
| explanation = res |
| return explanation, result |
| |
| def visit_BoolOp(self, boolop): |
| is_or = isinstance(boolop.op, ast.Or) |
| explanations = [] |
| for operand in boolop.values: |
| explanation, result = self.visit(operand) |
| explanations.append(explanation) |
| if result == is_or: |
| break |
| name = is_or and " or " or " and " |
| explanation = "(" + name.join(explanations) + ")" |
| return explanation, result |
| |
| def visit_UnaryOp(self, unary): |
| pattern = unary_map[unary.op.__class__] |
| operand_explanation, operand_result = self.visit(unary.operand) |
| explanation = pattern % (operand_explanation,) |
| co = self._compile(pattern % ("__exprinfo_expr",)) |
| try: |
| result = self.frame.eval(co, __exprinfo_expr=operand_result) |
| except Exception: |
| raise Failure(explanation) |
| return explanation, result |
| |
| def visit_BinOp(self, binop): |
| left_explanation, left_result = self.visit(binop.left) |
| right_explanation, right_result = self.visit(binop.right) |
| symbol = operator_map[binop.op.__class__] |
| explanation = "(%s %s %s)" % (left_explanation, symbol, |
| right_explanation) |
| source = "__exprinfo_left %s __exprinfo_right" % (symbol,) |
| co = self._compile(source) |
| try: |
| result = self.frame.eval(co, __exprinfo_left=left_result, |
| __exprinfo_right=right_result) |
| except Exception: |
| raise Failure(explanation) |
| return explanation, result |
| |
| def visit_Call(self, call): |
| func_explanation, func = self.visit(call.func) |
| arg_explanations = [] |
| ns = {"__exprinfo_func" : func} |
| arguments = [] |
| for arg in call.args: |
| arg_explanation, arg_result = self.visit(arg) |
| if isinstance(arg, _Starred): |
| arg_name = "__exprinfo_star" |
| ns[arg_name] = arg_result |
| arguments.append("*%s" % (arg_name,)) |
| arg_explanations.append("*%s" % (arg_explanation,)) |
| else: |
| arg_name = "__exprinfo_%s" % (len(ns),) |
| ns[arg_name] = arg_result |
| arguments.append(arg_name) |
| arg_explanations.append(arg_explanation) |
| for keyword in call.keywords: |
| arg_explanation, arg_result = self.visit(keyword.value) |
| if keyword.arg: |
| arg_name = "__exprinfo_%s" % (len(ns),) |
| keyword_source = "%s=%%s" % (keyword.arg) |
| arguments.append(keyword_source % (arg_name,)) |
| arg_explanations.append(keyword_source % (arg_explanation,)) |
| else: |
| arg_name = "__exprinfo_kwds" |
| arguments.append("**%s" % (arg_name,)) |
| arg_explanations.append("**%s" % (arg_explanation,)) |
| |
| ns[arg_name] = arg_result |
| |
| if getattr(call, 'starargs', None): |
| arg_explanation, arg_result = self.visit(call.starargs) |
| arg_name = "__exprinfo_star" |
| ns[arg_name] = arg_result |
| arguments.append("*%s" % (arg_name,)) |
| arg_explanations.append("*%s" % (arg_explanation,)) |
| |
| if getattr(call, 'kwargs', None): |
| arg_explanation, arg_result = self.visit(call.kwargs) |
| arg_name = "__exprinfo_kwds" |
| ns[arg_name] = arg_result |
| arguments.append("**%s" % (arg_name,)) |
| arg_explanations.append("**%s" % (arg_explanation,)) |
| args_explained = ", ".join(arg_explanations) |
| explanation = "%s(%s)" % (func_explanation, args_explained) |
| args = ", ".join(arguments) |
| source = "__exprinfo_func(%s)" % (args,) |
| co = self._compile(source) |
| try: |
| result = self.frame.eval(co, **ns) |
| except Exception: |
| raise Failure(explanation) |
| pattern = "%s\n{%s = %s\n}" |
| rep = self.frame.repr(result) |
| explanation = pattern % (rep, rep, explanation) |
| return explanation, result |
| |
| def _is_builtin_name(self, name): |
| pattern = "%r not in globals() and %r not in locals()" |
| source = pattern % (name.id, name.id) |
| co = self._compile(source) |
| try: |
| return self.frame.eval(co) |
| except Exception: |
| return False |
| |
| def visit_Attribute(self, attr): |
| if not isinstance(attr.ctx, ast.Load): |
| return self.generic_visit(attr) |
| source_explanation, source_result = self.visit(attr.value) |
| explanation = "%s.%s" % (source_explanation, attr.attr) |
| source = "__exprinfo_expr.%s" % (attr.attr,) |
| co = self._compile(source) |
| try: |
| try: |
| result = self.frame.eval(co, __exprinfo_expr=source_result) |
| except AttributeError: |
| # Maybe the attribute name needs to be mangled? |
| if not attr.attr.startswith("__") or attr.attr.endswith("__"): |
| raise |
| source = "getattr(__exprinfo_expr.__class__, '__name__', '')" |
| co = self._compile(source) |
| class_name = self.frame.eval(co, __exprinfo_expr=source_result) |
| mangled_attr = "_" + class_name + attr.attr |
| source = "__exprinfo_expr.%s" % (mangled_attr,) |
| co = self._compile(source) |
| result = self.frame.eval(co, __exprinfo_expr=source_result) |
| except Exception: |
| raise Failure(explanation) |
| explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result), |
| self.frame.repr(result), |
| source_explanation, attr.attr) |
| # Check if the attr is from an instance. |
| source = "%r in getattr(__exprinfo_expr, '__dict__', {})" |
| source = source % (attr.attr,) |
| co = self._compile(source) |
| try: |
| from_instance = self.frame.eval(co, __exprinfo_expr=source_result) |
| except Exception: |
| from_instance = None |
| if from_instance is None or self.frame.is_true(from_instance): |
| rep = self.frame.repr(result) |
| pattern = "%s\n{%s = %s\n}" |
| explanation = pattern % (rep, rep, explanation) |
| return explanation, result |
| |
| def visit_Assert(self, assrt): |
| test_explanation, test_result = self.visit(assrt.test) |
| explanation = "assert %s" % (test_explanation,) |
| if not self.frame.is_true(test_result): |
| try: |
| raise util.BuiltinAssertionError |
| except Exception: |
| raise Failure(explanation) |
| return explanation, test_result |
| |
| def visit_Assign(self, assign): |
| value_explanation, value_result = self.visit(assign.value) |
| explanation = "... = %s" % (value_explanation,) |
| name = ast.Name("__exprinfo_expr", ast.Load(), |
| lineno=assign.value.lineno, |
| col_offset=assign.value.col_offset) |
| new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno, |
| col_offset=assign.col_offset) |
| mod = ast.Module([new_assign]) |
| co = self._compile(mod, "exec") |
| try: |
| self.frame.exec_(co, __exprinfo_expr=value_result) |
| except Exception: |
| raise Failure(explanation) |
| return explanation, value_result |
| |