aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRonan Lamy <ronan.lamy@gmail.com>2016-11-15 01:46:49 +0000
committerRonan Lamy <ronan.lamy@gmail.com>2016-11-15 01:46:49 +0000
commitc4d582fc64bcec47f47825bafc85abb9ed175c42 (patch)
tree68b430e6f82cfbcae037ed63d2ad1d5d2c9ac506 /_pytest
parentreinstate pytest_cov.py (diff)
downloadpypy-c4d582fc64bcec47f47825bafc85abb9ed175c42.tar.gz
pypy-c4d582fc64bcec47f47825bafc85abb9ed175c42.tar.bz2
pypy-c4d582fc64bcec47f47825bafc85abb9ed175c42.zip
copy upstream pytest-2.9.2 and py-1.4.29
Diffstat (limited to '_pytest')
-rw-r--r--_pytest/__init__.py2
-rw-r--r--_pytest/_argcomplete.py3
-rw-r--r--_pytest/assertion/__init__.py108
-rw-r--r--_pytest/assertion/newinterpret.py333
-rw-r--r--_pytest/assertion/oldinterpret.py554
-rw-r--r--_pytest/assertion/reinterpret.py373
-rw-r--r--_pytest/assertion/rewrite.py297
-rw-r--r--_pytest/assertion/util.py98
-rw-r--r--_pytest/capture.py764
-rw-r--r--_pytest/config.py818
-rw-r--r--_pytest/core.py394
-rw-r--r--_pytest/doctest.py261
-rwxr-xr-x_pytest/genscript.py74
-rw-r--r--_pytest/helpconfig.py146
-rw-r--r--_pytest/hookspec.py144
-rw-r--r--_pytest/junitxml.py410
-rw-r--r--_pytest/main.py171
-rw-r--r--_pytest/mark.py44
-rw-r--r--_pytest/monkeypatch.py168
-rw-r--r--_pytest/nose.py37
-rw-r--r--_pytest/pastebin.py79
-rw-r--r--_pytest/pdb.py62
-rw-r--r--_pytest/pytester.py943
-rw-r--r--_pytest/python.py848
-rw-r--r--_pytest/recwarn.py244
-rw-r--r--_pytest/resultlog.py4
-rw-r--r--_pytest/runner.py90
-rw-r--r--_pytest/skipping.py229
-rwxr-xr-x_pytest/standalonetemplate.py25
-rw-r--r--_pytest/terminal.py249
-rw-r--r--_pytest/tmpdir.py66
-rw-r--r--_pytest/unittest.py117
32 files changed, 4694 insertions, 3461 deletions
diff --git a/_pytest/__init__.py b/_pytest/__init__.py
index af129122fa..23dac6d055 100644
--- a/_pytest/__init__.py
+++ b/_pytest/__init__.py
@@ -1,2 +1,2 @@
#
-__version__ = '2.5.2'
+__version__ = '2.9.2'
diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py
index 4f4eaf925f..955855a964 100644
--- a/_pytest/_argcomplete.py
+++ b/_pytest/_argcomplete.py
@@ -88,9 +88,6 @@ class FastFilesCompleter:
return completion
if os.environ.get('_ARGCOMPLETE'):
- # argcomplete 0.5.6 is not compatible with python 2.5.6: print/with/format
- if sys.version_info[:2] < (2, 6):
- sys.exit(1)
try:
import argcomplete.completers
except ImportError:
diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py
index fdc279387e..6921deb2a6 100644
--- a/_pytest/assertion/__init__.py
+++ b/_pytest/assertion/__init__.py
@@ -2,24 +2,37 @@
support for presenting detailed information in failing assertions.
"""
import py
+import os
import sys
from _pytest.monkeypatch import monkeypatch
from _pytest.assertion import util
+
def pytest_addoption(parser):
group = parser.getgroup("debugconfig")
- group.addoption('--assert', action="store", dest="assertmode",
+ group.addoption('--assert',
+ action="store",
+ dest="assertmode",
choices=("rewrite", "reinterp", "plain",),
- default="rewrite", metavar="MODE",
- help="""control assertion debugging tools.
-'plain' performs no assertion debugging.
-'reinterp' reinterprets assert statements after they failed to provide assertion expression information.
-'rewrite' (the default) rewrites assert statements in test modules on import
-to provide assert expression information. """)
- group.addoption('--no-assert', action="store_true", default=False,
- dest="noassert", help="DEPRECATED equivalent to --assert=plain")
- group.addoption('--nomagic', '--no-magic', action="store_true",
- default=False, help="DEPRECATED equivalent to --assert=plain")
+ default="rewrite",
+ metavar="MODE",
+ help="""control assertion debugging tools. 'plain'
+ performs no assertion debugging. 'reinterp'
+ reinterprets assert statements after they failed
+ to provide assertion expression information.
+ 'rewrite' (the default) rewrites assert
+ statements in test modules on import to
+ provide assert expression information. """)
+ group.addoption('--no-assert',
+ action="store_true",
+ default=False,
+ dest="noassert",
+ help="DEPRECATED equivalent to --assert=plain")
+ group.addoption('--nomagic', '--no-magic',
+ action="store_true",
+ default=False,
+ help="DEPRECATED equivalent to --assert=plain")
+
class AssertionState:
"""State for the assertion plugin."""
@@ -28,6 +41,7 @@ class AssertionState:
self.mode = mode
self.trace = config.trace.root.get("assertion")
+
def pytest_configure(config):
mode = config.getvalue("assertmode")
if config.getvalue("noassert") or config.getvalue("nomagic"):
@@ -41,7 +55,7 @@ def pytest_configure(config):
# Both Jython and CPython 2.6.0 have AST bugs that make the
# assertion rewriting hook malfunction.
if (sys.platform.startswith('java') or
- sys.version_info[:3] == (2, 6, 0)):
+ sys.version_info[:3] == (2, 6, 0)):
mode = "reinterp"
if mode != "plain":
_load_modules(mode)
@@ -57,11 +71,12 @@ def pytest_configure(config):
config._assertstate = AssertionState(config, mode)
config._assertstate.hook = hook
config._assertstate.trace("configured with mode set to %r" % (mode,))
+ def undo():
+ hook = config._assertstate.hook
+ if hook is not None and hook in sys.meta_path:
+ sys.meta_path.remove(hook)
+ config.add_cleanup(undo)
-def pytest_unconfigure(config):
- hook = config._assertstate.hook
- if hook is not None:
- sys.meta_path.remove(hook)
def pytest_collection(session):
# this hook is only called when test modules are collected
@@ -71,36 +86,66 @@ def pytest_collection(session):
if hook is not None:
hook.set_session(session)
+
+def _running_on_ci():
+ """Check if we're currently running on a CI system."""
+ env_vars = ['CI', 'BUILD_NUMBER']
+ return any(var in os.environ for var in env_vars)
+
+
def pytest_runtest_setup(item):
+ """Setup the pytest_assertrepr_compare hook
+
+ The newinterpret and rewrite modules will use util._reprcompare if
+ it exists to use custom reporting via the
+ pytest_assertrepr_compare hook. This sets up this custom
+ comparison for the test.
+ """
def callbinrepr(op, left, right):
+ """Call the pytest_assertrepr_compare hook and prepare the result
+
+ This uses the first result from the hook and then ensures the
+ following:
+ * Overly verbose explanations are dropped unless -vv was used or
+ running on a CI.
+ * Embedded newlines are escaped to help util.format_explanation()
+ later.
+ * If the rewrite mode is used embedded %-characters are replaced
+ to protect later % formatting.
+
+ The result can be formatted by util.format_explanation() for
+ pretty printing.
+ """
hook_result = item.ihook.pytest_assertrepr_compare(
config=item.config, op=op, left=left, right=right)
-
for new_expl in hook_result:
if new_expl:
- # Don't include pageloads of data unless we are very
- # verbose (-vv)
- if (sum(len(p) for p in new_expl[1:]) > 80*8
- and item.config.option.verbose < 2):
- new_expl[1:] = [py.builtin._totext(
- 'Detailed information truncated, use "-vv" to show')]
- res = py.builtin._totext('\n~').join(new_expl)
+ if (sum(len(p) for p in new_expl[1:]) > 80*8 and
+ item.config.option.verbose < 2 and
+ not _running_on_ci()):
+ show_max = 10
+ truncated_lines = len(new_expl) - show_max
+ new_expl[show_max:] = [py.builtin._totext(
+ 'Detailed information truncated (%d more lines)'
+ ', use "-vv" to show' % truncated_lines)]
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
+ res = py.builtin._totext("\n~").join(new_expl)
if item.config.getvalue("assertmode") == "rewrite":
- # The result will be fed back a python % formatting
- # operation, which will fail if there are extraneous
- # '%'s in the string. Escape them here.
res = res.replace("%", "%%")
return res
util._reprcompare = callbinrepr
+
def pytest_runtest_teardown(item):
util._reprcompare = None
+
def pytest_sessionfinish(session):
hook = session.config._assertstate.hook
if hook is not None:
hook.session = None
+
def _load_modules(mode):
"""Lazily import assertion related code."""
global rewrite, reinterpret
@@ -108,6 +153,7 @@ def _load_modules(mode):
if mode == "rewrite":
from _pytest.assertion import rewrite # noqa
+
def warn_about_missing_assertion(mode):
try:
assert False
@@ -121,8 +167,10 @@ def warn_about_missing_assertion(mode):
specifically = "failing tests may report as passing"
sys.stderr.write("WARNING: " + specifically +
- " because assert statements are not executed "
- "by the underlying Python interpreter "
- "(are you using python -O?)\n")
+ " because assert statements are not executed "
+ "by the underlying Python interpreter "
+ "(are you using python -O?)\n")
+
+# Expose this plugin's implementation for the pytest_assertrepr_compare hook
pytest_assertrepr_compare = util.assertrepr_compare
diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py
deleted file mode 100644
index e7e9658d7d..0000000000
--- a/_pytest/assertion/newinterpret.py
+++ /dev/null
@@ -1,333 +0,0 @@
-"""
-Find intermediate evalutation results in assert statements through builtin AST.
-This should replace oldinterpret.py eventually.
-"""
-
-import sys
-import ast
-
-import py
-from _pytest.assertion import util
-from _pytest.assertion.reinterpret import BuiltinAssertionError
-
-
-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)
-
-
-class Failure(Exception):
- """Error found while interpreting AST."""
-
- def __init__(self, explanation=""):
- self.cause = sys.exc_info()
- self.explanation = explanation
-
-
-def interpret(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 = py.code.Frame(sys._getframe(1))
- return interpret(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)
- 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)
- arg_name = "__exprinfo_%s" % (len(ns),)
- ns[arg_name] = arg_result
- keyword_source = "%s=%%s" % (keyword.arg)
- arguments.append(keyword_source % (arg_name,))
- arg_explanations.append(keyword_source % (arg_explanation,))
- if call.starargs:
- 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 call.kwargs:
- 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:
- 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 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
diff --git a/_pytest/assertion/oldinterpret.py b/_pytest/assertion/oldinterpret.py
deleted file mode 100644
index 27cfff03d8..0000000000
--- a/_pytest/assertion/oldinterpret.py
+++ /dev/null
@@ -1,554 +0,0 @@
-import py
-import sys, inspect
-from compiler import parse, ast, pycodegen
-from _pytest.assertion.util import format_explanation, BuiltinAssertionError
-
-passthroughex = py.builtin._sysex
-
-class Failure:
- def __init__(self, node):
- self.exc, self.value, self.tb = sys.exc_info()
- self.node = node
-
-class View(object):
- """View base class.
-
- If C is a subclass of View, then C(x) creates a proxy object around
- the object x. The actual class of the proxy is not C in general,
- but a *subclass* of C determined by the rules below. To avoid confusion
- we call view class the class of the proxy (a subclass of C, so of View)
- and object class the class of x.
-
- Attributes and methods not found in the proxy are automatically read on x.
- Other operations like setting attributes are performed on the proxy, as
- determined by its view class. The object x is available from the proxy
- as its __obj__ attribute.
-
- The view class selection is determined by the __view__ tuples and the
- optional __viewkey__ method. By default, the selected view class is the
- most specific subclass of C whose __view__ mentions the class of x.
- If no such subclass is found, the search proceeds with the parent
- object classes. For example, C(True) will first look for a subclass
- of C with __view__ = (..., bool, ...) and only if it doesn't find any
- look for one with __view__ = (..., int, ...), and then ..., object,...
- If everything fails the class C itself is considered to be the default.
-
- Alternatively, the view class selection can be driven by another aspect
- of the object x, instead of the class of x, by overriding __viewkey__.
- See last example at the end of this module.
- """
-
- _viewcache = {}
- __view__ = ()
-
- def __new__(rootclass, obj, *args, **kwds):
- self = object.__new__(rootclass)
- self.__obj__ = obj
- self.__rootclass__ = rootclass
- key = self.__viewkey__()
- try:
- self.__class__ = self._viewcache[key]
- except KeyError:
- self.__class__ = self._selectsubclass(key)
- return self
-
- def __getattr__(self, attr):
- # attributes not found in the normal hierarchy rooted on View
- # are looked up in the object's real class
- return getattr(self.__obj__, attr)
-
- def __viewkey__(self):
- return self.__obj__.__class__
-
- def __matchkey__(self, key, subclasses):
- if inspect.isclass(key):
- keys = inspect.getmro(key)
- else:
- keys = [key]
- for key in keys:
- result = [C for C in subclasses if key in C.__view__]
- if result:
- return result
- return []
-
- def _selectsubclass(self, key):
- subclasses = list(enumsubclasses(self.__rootclass__))
- for C in subclasses:
- if not isinstance(C.__view__, tuple):
- C.__view__ = (C.__view__,)
- choices = self.__matchkey__(key, subclasses)
- if not choices:
- return self.__rootclass__
- elif len(choices) == 1:
- return choices[0]
- else:
- # combine the multiple choices
- return type('?', tuple(choices), {})
-
- def __repr__(self):
- return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__)
-
-
-def enumsubclasses(cls):
- for subcls in cls.__subclasses__():
- for subsubclass in enumsubclasses(subcls):
- yield subsubclass
- yield cls
-
-
-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):
- return format_explanation(self.explanation)
-
-
-class Name(Interpretable):
- __view__ = ast.Name
-
- def is_local(self, frame):
- source = '%r in locals() is not globals()' % self.name
- try:
- return frame.is_true(frame.eval(source))
- except passthroughex:
- raise
- except:
- return False
-
- def is_global(self, frame):
- source = '%r in globals()' % self.name
- try:
- return frame.is_true(frame.eval(source))
- except passthroughex:
- raise
- except:
- return False
-
- def is_builtin(self, frame):
- source = '%r not in locals() and %r not in globals()' % (
- self.name, self.name)
- try:
- return frame.is_true(frame.eval(source))
- 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:
- if hasattr(self, 'result'):
- # shortcutting in chained expressions
- if not frame.is_true(self.result):
- break
- expr2 = Interpretable(expr2)
- expr2.eval(frame)
- self.explanation = "%s %s %s" % (
- expr.explanation, operation, expr2.explanation)
- source = "__exprinfo_left %s __exprinfo_right" % operation
- try:
- self.result = frame.eval(source,
- __exprinfo_left=expr.result,
- __exprinfo_right=expr2.result)
- except passthroughex:
- raise
- except:
- raise Failure(self)
- 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):
- expr = Interpretable(self.expr)
- expr.eval(frame)
- self.explanation = astpattern.replace('__exprinfo_expr',
- expr.explanation)
- try:
- self.result = frame.eval(astpattern,
- __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):
- 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(astpattern,
- __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):
- source = 'isinstance(__exprinfo_value, bool)'
- try:
- return frame.is_true(frame.eval(source,
- __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 += ')'
- try:
- self.result = frame.eval(source, **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)
- source = '__exprinfo_expr.%s' % self.attrname
- try:
- self.result = frame.eval(source, __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
- source = ('hasattr(__exprinfo_expr, "__dict__") and '
- '%r in __exprinfo_expr.__dict__' % self.attrname)
- try:
- from_instance = frame.is_true(
- frame.eval(source, __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 ==
-
-class Assert(Interpretable):
- __view__ = ast.Assert
-
- def run(self, frame):
- test = Interpretable(self.test)
- test.eval(frame)
- # 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 = ""
- sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation))
-
-def check(s, frame=None):
- if frame is None:
- 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 = sys.exc_info()[1]
- report_failure(e)
- else:
- if not frame.is_true(node.result):
- sys.stderr.write("assertion failed: %s\n" % 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 = sys.exc_info()[1]
- return getfailure(e)
- except passthroughex:
- raise
- except:
- import traceback
- traceback.print_exc()
- 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)")
- 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:
- frame = sys._getframe(1)
- frame = py.code.Frame(frame)
- module = Interpretable(parse(s, 'exec').node)
- try:
- module.run(frame)
- except Failure:
- e = sys.exc_info()[1]
- 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())")
diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py
index fe1f87d2da..f4262c3ace 100644
--- a/_pytest/assertion/reinterpret.py
+++ b/_pytest/assertion/reinterpret.py
@@ -1,12 +1,18 @@
+"""
+Find intermediate evalutation results in assert statements through builtin AST.
+"""
+import ast
import sys
+
+import _pytest._code
import py
-from _pytest.assertion.util import BuiltinAssertionError
+from _pytest.assertion import util
u = py.builtin._totext
-class AssertionError(BuiltinAssertionError):
+class AssertionError(util.BuiltinAssertionError):
def __init__(self, *args):
- BuiltinAssertionError.__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
@@ -22,7 +28,7 @@ class AssertionError(BuiltinAssertionError):
"<[broken __repr__] %s at %0xd>"
% (toprint.__class__, id(toprint)))
else:
- f = py.code.Frame(sys._getframe(1))
+ f = _pytest._code.Frame(sys._getframe(1))
try:
source = f.code.fullsource
if source is not None:
@@ -45,10 +51,357 @@ class AssertionError(BuiltinAssertionError):
if sys.version_info > (3, 0):
AssertionError.__module__ = "builtins"
- reinterpret_old = "old reinterpretation not available for py3"
-else:
- from _pytest.assertion.oldinterpret import interpret as reinterpret_old
-if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
- from _pytest.assertion.newinterpret import interpret as reinterpret
+
+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:
- reinterpret = reinterpret_old
+ 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
+
diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py
index 95bf4117df..14b8e49db2 100644
--- a/_pytest/assertion/rewrite.py
+++ b/_pytest/assertion/rewrite.py
@@ -35,6 +35,12 @@ PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
+if sys.version_info >= (3,5):
+ ast_Call = ast.Call
+else:
+ ast_Call = lambda a,b,c: ast.Call(a, b, c, None, None)
+
+
class AssertionRewritingHook(object):
"""PEP302 Import hook which rewrites asserts."""
@@ -122,7 +128,7 @@ class AssertionRewritingHook(object):
# One of the path components was not a directory, likely
# because we're in a zip file.
write = False
- elif e == errno.EACCES:
+ elif e in [errno.EACCES, errno.EROFS, errno.EPERM]:
state.trace("read only directory: %r" % fn_pypath.dirname)
write = False
else:
@@ -131,21 +137,27 @@ class AssertionRewritingHook(object):
pyc = os.path.join(cache_dir, cache_name)
# Notice that even if we're in a read-only directory, I'm going
# to check for a cached pyc. This may not be optimal...
- co = _read_pyc(fn_pypath, pyc)
+ co = _read_pyc(fn_pypath, pyc, state.trace)
if co is None:
state.trace("rewriting %r" % (fn,))
- co = _rewrite_test(state, fn_pypath)
+ source_stat, co = _rewrite_test(state, fn_pypath)
if co is None:
# Probably a SyntaxError in the test.
return None
if write:
- _make_rewritten_pyc(state, fn_pypath, pyc, co)
+ _make_rewritten_pyc(state, source_stat, pyc, co)
else:
state.trace("found cached rewritten pyc for %r" % (fn,))
self.modules[name] = co, pyc
return self
def load_module(self, name):
+ # If there is an existing module object named 'fullname' in
+ # sys.modules, the loader must use that existing module. (Otherwise,
+ # the reload() builtin will not work correctly.)
+ if name in sys.modules:
+ return sys.modules[name]
+
co, pyc = self.modules.pop(name)
# I wish I could just call imp.load_compiled here, but __file__ has to
# be set properly. In Python 3.2+, this all would be handled correctly
@@ -191,14 +203,19 @@ class AssertionRewritingHook(object):
# DefaultProvider is appropriate.
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
+ def get_data(self, pathname):
+ """Optional PEP302 get_data API.
+ """
+ with open(pathname, 'rb') as f:
+ return f.read()
-def _write_pyc(state, co, source_path, pyc):
+
+def _write_pyc(state, co, source_stat, pyc):
# Technically, we don't have to have the same pyc format as
# (C)Python, since these "pycs" should never be seen by builtin
# import. However, there's little reason deviate, and I hope
# sometime to be able to use imp.load_compiled to load them. (See
# the comment in load_module above.)
- mtime = int(source_path.mtime())
try:
fp = open(pyc, "wb")
except IOError:
@@ -210,7 +227,9 @@ def _write_pyc(state, co, source_path, pyc):
return False
try:
fp.write(imp.get_magic())
- fp.write(struct.pack("<l", mtime))
+ mtime = int(source_stat.mtime)
+ size = source_stat.size & 0xFFFFFFFF
+ fp.write(struct.pack("<ll", mtime, size))
marshal.dump(co, fp)
finally:
fp.close()
@@ -225,9 +244,10 @@ BOM_UTF8 = '\xef\xbb\xbf'
def _rewrite_test(state, fn):
"""Try to read and rewrite *fn* and return the code object."""
try:
+ stat = fn.stat()
source = fn.read("rb")
except EnvironmentError:
- return None
+ return None, None
if ASCII_IS_DEFAULT_ENCODING:
# ASCII is the default encoding in Python 2. Without a coding
# declaration, Python 2 will complain about any bytes in the file
@@ -246,14 +266,15 @@ def _rewrite_test(state, fn):
cookie_re.match(source[0:end1]) is None and
cookie_re.match(source[end1 + 1:end2]) is None):
if hasattr(state, "_indecode"):
- return None # encodings imported us again, we don't rewrite
+ # encodings imported us again, so don't rewrite.
+ return None, None
state._indecode = True
try:
try:
source.decode("ascii")
except UnicodeDecodeError:
# Let it fail in real import.
- return None
+ return None, None
finally:
del state._indecode
# On Python versions which are not 2.7 and less than or equal to 3.1, the
@@ -265,7 +286,7 @@ def _rewrite_test(state, fn):
except SyntaxError:
# Let this pop up again in the real import.
state.trace("failed to parse: %r" % (fn,))
- return None
+ return None, None
rewrite_asserts(tree)
try:
co = compile(tree, fn.strpath, "exec")
@@ -273,23 +294,23 @@ def _rewrite_test(state, fn):
# It's possible that this error is from some bug in the
# assertion rewriting, but I don't know of a fast way to tell.
state.trace("failed to compile: %r" % (fn,))
- return None
- return co
+ return None, None
+ return stat, co
-def _make_rewritten_pyc(state, fn, pyc, co):
+def _make_rewritten_pyc(state, source_stat, pyc, co):
"""Try to dump rewritten code to *pyc*."""
if sys.platform.startswith("win"):
# Windows grants exclusive access to open files and doesn't have atomic
# rename, so just write into the final file.
- _write_pyc(state, co, fn, pyc)
+ _write_pyc(state, co, source_stat, pyc)
else:
# When not on windows, assume rename is atomic. Dump the code object
# into a file specific to this process and atomically replace it.
proc_pyc = pyc + "." + str(os.getpid())
- if _write_pyc(state, co, fn, proc_pyc):
+ if _write_pyc(state, co, source_stat, proc_pyc):
os.rename(proc_pyc, pyc)
-def _read_pyc(source, pyc):
+def _read_pyc(source, pyc, trace=lambda x: None):
"""Possibly read a pytest pyc containing rewritten code.
Return rewritten code if successful or None if not.
@@ -298,23 +319,28 @@ def _read_pyc(source, pyc):
fp = open(pyc, "rb")
except IOError:
return None
- try:
+ with fp:
try:
mtime = int(source.mtime())
- data = fp.read(8)
- except EnvironmentError:
+ size = source.size()
+ data = fp.read(12)
+ except EnvironmentError as e:
+ trace('_read_pyc(%s): EnvironmentError %s' % (source, e))
return None
# Check for invalid or out of date pyc file.
- if (len(data) != 8 or data[:4] != imp.get_magic() or
- struct.unpack("<l", data[4:])[0] != mtime):
+ if (len(data) != 12 or data[:4] != imp.get_magic() or
+ struct.unpack("<ll", data[4:]) != (mtime, size)):
+ trace('_read_pyc(%s): invalid or out of date pyc' % source)
+ return None
+ try:
+ co = marshal.load(fp)
+ except Exception as e:
+ trace('_read_pyc(%s): marshal.load error %s' % (source, e))
return None
- co = marshal.load(fp)
if not isinstance(co, types.CodeType):
- # That's interesting....
+ trace('_read_pyc(%s): not a code object' % source)
return None
return co
- finally:
- fp.close()
def rewrite_asserts(mod):
@@ -322,14 +348,64 @@ def rewrite_asserts(mod):
AssertionRewriter().run(mod)
-_saferepr = py.io.saferepr
+def _saferepr(obj):
+ """Get a safe repr of an object for assertion error messages.
+
+ The assertion formatting (util.format_explanation()) requires
+ newlines to be escaped since they are a special character for it.
+ Normally assertion.util.format_explanation() does this but for a
+ custom repr it is possible to contain one of the special escape
+ sequences, especially '\n{' and '\n}' are likely to be present in
+ JSON reprs.
+
+ """
+ repr = py.io.saferepr(obj)
+ if py.builtin._istext(repr):
+ t = py.builtin.text
+ else:
+ t = py.builtin.bytes
+ return repr.replace(t("\n"), t("\\n"))
+
+
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
+def _format_assertmsg(obj):
+ """Format the custom assertion message given.
+
+ For strings this simply replaces newlines with '\n~' so that
+ util.format_explanation() will preserve them instead of escaping
+ newlines. For other objects py.io.saferepr() is used first.
+
+ """
+ # reprlib appears to have a bug which means that if a string
+ # contains a newline it gets escaped, however if an object has a
+ # .__repr__() which contains newlines it does not get escaped.
+ # However in either case we want to preserve the newline.
+ if py.builtin._istext(obj) or py.builtin._isbytes(obj):
+ s = obj
+ is_repr = False
+ else:
+ s = py.io.saferepr(obj)
+ is_repr = True
+ if py.builtin._istext(s):
+ t = py.builtin.text
+ else:
+ t = py.builtin.bytes
+ s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
+ if is_repr:
+ s = s.replace(t("\\n"), t("\n~"))
+ return s
+
def _should_repr_global_name(obj):
return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
def _format_boolop(explanations, is_or):
- return "(" + (is_or and " or " or " and ").join(explanations) + ")"
+ explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
+ if py.builtin._istext(explanation):
+ t = py.builtin.text
+ else:
+ t = py.builtin.bytes
+ return explanation.replace(t('%'), t('%%'))
def _call_reprcompare(ops, results, expls, each_obj):
for i, res, expl in zip(range(len(ops)), results, expls):
@@ -377,6 +453,18 @@ binop_map = {
ast.In: "in",
ast.NotIn: "not in"
}
+# Python 3.5+ compatibility
+try:
+ binop_map[ast.MatMult] = "@"
+except AttributeError:
+ pass
+
+# Python 3.4+ compatibility
+if hasattr(ast, "NameConstant"):
+ _NameConstant = ast.NameConstant
+else:
+ def _NameConstant(c):
+ return ast.Name(str(c), ast.Load())
def set_location(node, lineno, col_offset):
@@ -393,6 +481,56 @@ def set_location(node, lineno, col_offset):
class AssertionRewriter(ast.NodeVisitor):
+ """Assertion rewriting implementation.
+
+ The main entrypoint is to call .run() with an ast.Module instance,
+ this will then find all the assert statements and re-write them to
+ provide intermediate values and a detailed assertion error. See
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
+ for an overview of how this works.
+
+ The entry point here is .run() which will iterate over all the
+ statements in an ast.Module and for each ast.Assert statement it
+ finds call .visit() with it. Then .visit_Assert() takes over and
+ is responsible for creating new ast statements to replace the
+ original assert statement: it re-writes the test of an assertion
+ to provide intermediate values and replace it with an if statement
+ which raises an assertion error with a detailed explanation in
+ case the expression is false.
+
+ For this .visit_Assert() uses the visitor pattern to visit all the
+ AST nodes of the ast.Assert.test field, each visit call returning
+ an AST node and the corresponding explanation string. During this
+ state is kept in several instance attributes:
+
+ :statements: All the AST statements which will replace the assert
+ statement.
+
+ :variables: This is populated by .variable() with each variable
+ used by the statements so that they can all be set to None at
+ the end of the statements.
+
+ :variable_counter: Counter to create new unique variables needed
+ by statements. Variables are created using .variable() and
+ have the form of "@py_assert0".
+
+ :on_failure: The AST statements which will be executed if the
+ assertion test fails. This is the code which will construct
+ the failure message and raises the AssertionError.
+
+ :explanation_specifiers: A dict filled by .explanation_param()
+ with %-formatting placeholders and their corresponding
+ expressions to use in the building of an assertion message.
+ This is used by .pop_format_context() to build a message.
+
+ :stack: A stack of the explanation_specifiers dicts maintained by
+ .push_format_context() and .pop_format_context() which allows
+ to build another %-formatted string while already building one.
+
+ This state is reset on every new assert statement visited and used
+ by the other visitors.
+
+ """
def run(self, mod):
"""Find all assert statements in *mod* and rewrite them."""
@@ -466,7 +604,7 @@ class AssertionRewriter(ast.NodeVisitor):
"""Call a helper in this module."""
py_name = ast.Name("@pytest_ar", ast.Load())
attr = ast.Attribute(py_name, "_" + name, ast.Load())
- return ast.Call(attr, list(args), [], None, None)
+ return ast_Call(attr, list(args), [])
def builtin(self, name):
"""Return the builtin called *name*."""
@@ -474,15 +612,41 @@ class AssertionRewriter(ast.NodeVisitor):
return ast.Attribute(builtin_name, name, ast.Load())
def explanation_param(self, expr):
+ """Return a new named %-formatting placeholder for expr.
+
+ This creates a %-formatting placeholder for expr in the
+ current formatting context, e.g. ``%(py0)s``. The placeholder
+ and expr are placed in the current format context so that it
+ can be used on the next call to .pop_format_context().
+
+ """
specifier = "py" + str(next(self.variable_counter))
self.explanation_specifiers[specifier] = expr
return "%(" + specifier + ")s"
def push_format_context(self):
+ """Create a new formatting context.
+
+ The format context is used for when an explanation wants to
+ have a variable value formatted in the assertion message. In
+ this case the value required can be added using
+ .explanation_param(). Finally .pop_format_context() is used
+ to format a string of %-formatted values as added by
+ .explanation_param().
+
+ """
self.explanation_specifiers = {}
self.stack.append(self.explanation_specifiers)
def pop_format_context(self, expl_expr):
+ """Format the %-formatted string with current format context.
+
+ The expl_expr should be an ast.Str instance constructed from
+ the %-placeholders created by .explanation_param(). This will
+ add the required code to format said string to .on_failure and
+ return the ast.Name instance of the formatted string.
+
+ """
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
@@ -500,11 +664,15 @@ class AssertionRewriter(ast.NodeVisitor):
return res, self.explanation_param(self.display(res))
def visit_Assert(self, assert_):
- if assert_.msg:
- # There's already a message. Don't mess with it.
- return [assert_]
+ """Return the AST statements to replace the ast.Assert instance.
+
+ This re-writes the test of an assertion to provide
+ intermediate values and replace it with an if statement which
+ raises an assertion error with a detailed explanation in case
+ the expression is false.
+
+ """
self.statements = []
- self.cond_chain = ()
self.variables = []
self.variable_counter = itertools.count()
self.stack = []
@@ -516,12 +684,17 @@ class AssertionRewriter(ast.NodeVisitor):
body = self.on_failure
negation = ast.UnaryOp(ast.Not(), top_condition)
self.statements.append(ast.If(negation, body, []))
- explanation = "assert " + explanation
- template = ast.Str(explanation)
+ if assert_.msg:
+ assertmsg = self.helper('format_assertmsg', assert_.msg)
+ explanation = "\n>assert " + explanation
+ else:
+ assertmsg = ast.Str("")
+ explanation = "assert " + explanation
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
- exc = ast.Call(err_name, [fmt], [], None, None)
+ exc = ast_Call(err_name, [fmt], [])
if sys.version_info[0] >= 3:
raise_ = ast.Raise(exc, None)
else:
@@ -531,7 +704,7 @@ class AssertionRewriter(ast.NodeVisitor):
if self.variables:
variables = [ast.Name(name, ast.Store())
for name in self.variables]
- clear = ast.Assign(variables, ast.Name("None", ast.Load()))
+ clear = ast.Assign(variables, _NameConstant(None))
self.statements.append(clear)
# Fix line numbers.
for stmt in self.statements:
@@ -541,7 +714,7 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Name(self, name):
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
- locs = ast.Call(self.builtin("locals"), [], [], None, None)
+ locs = ast_Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
dorepr = self.helper("should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
@@ -568,7 +741,7 @@ class AssertionRewriter(ast.NodeVisitor):
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl))
- call = ast.Call(app, [expl_format], [], None, None)
+ call = ast_Call(app, [expl_format], [])
self.on_failure.append(ast.Expr(call))
if i < levels:
cond = res
@@ -597,7 +770,42 @@ class AssertionRewriter(ast.NodeVisitor):
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
return res, explanation
- def visit_Call(self, call):
+ def visit_Call_35(self, call):
+ """
+ visit `ast.Call` nodes on Python3.5 and after
+ """
+ new_func, func_expl = self.visit(call.func)
+ arg_expls = []
+ new_args = []
+ new_kwargs = []
+ for arg in call.args:
+ res, expl = self.visit(arg)
+ arg_expls.append(expl)
+ new_args.append(res)
+ for keyword in call.keywords:
+ res, expl = self.visit(keyword.value)
+ new_kwargs.append(ast.keyword(keyword.arg, res))
+ if keyword.arg:
+ arg_expls.append(keyword.arg + "=" + expl)
+ else: ## **args have `arg` keywords with an .arg of None
+ arg_expls.append("**" + expl)
+
+ expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
+ new_call = ast.Call(new_func, new_args, new_kwargs)
+ res = self.assign(new_call)
+ res_expl = self.explanation_param(self.display(res))
+ outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
+ return res, outer_expl
+
+ def visit_Starred(self, starred):
+ # From Python 3.5, a Starred node can appear in a function call
+ res, expl = self.visit(starred.value)
+ return starred, '*' + expl
+
+ def visit_Call_legacy(self, call):
+ """
+ visit `ast.Call nodes on 3.4 and below`
+ """
new_func, func_expl = self.visit(call.func)
arg_expls = []
new_args = []
@@ -625,6 +833,15 @@ class AssertionRewriter(ast.NodeVisitor):
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
return res, outer_expl
+ # ast.Call signature changed on 3.5,
+ # conditionally change which methods is named
+ # visit_Call depending on Python version
+ if sys.version_info >= (3, 5):
+ visit_Call = visit_Call_35
+ else:
+ visit_Call = visit_Call_legacy
+
+
def visit_Attribute(self, attr):
if not isinstance(attr.ctx, ast.Load):
return self.generic_visit(attr)
diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py
index 13a31a4a96..f2f23efea2 100644
--- a/_pytest/assertion/util.py
+++ b/_pytest/assertion/util.py
@@ -1,5 +1,7 @@
"""Utilities for assertion debugging"""
+import pprint
+import _pytest._code
import py
try:
from collections import Sequence
@@ -16,6 +18,15 @@ u = py.builtin._totext
_reprcompare = None
+# the re-encoding is needed for python2 repr
+# with non-ascii characters (see issue 877 and 1379)
+def ecu(s):
+ try:
+ return u(s, 'utf-8', 'replace')
+ except TypeError:
+ return s
+
+
def format_explanation(explanation):
"""This formats an explanation
@@ -26,6 +37,7 @@ def format_explanation(explanation):
for when one explanation needs to span multiple lines, e.g. when
displaying diffs.
"""
+ explanation = ecu(explanation)
explanation = _collapse_false(explanation)
lines = _split_explanation(explanation)
result = _format_lines(lines)
@@ -44,13 +56,15 @@ def _collapse_false(explanation):
if where == -1:
break
level = 0
+ prev_c = explanation[start]
for i, c in enumerate(explanation[start:]):
- if c == "{":
+ if prev_c + c == "\n{":
level += 1
- elif c == "}":
+ elif prev_c + c == "\n}":
level -= 1
if not level:
break
+ prev_c = c
else:
raise AssertionError("unbalanced braces: %r" % (explanation,))
end = start + i
@@ -72,7 +86,7 @@ def _split_explanation(explanation):
raw_lines = (explanation or u('')).split('\n')
lines = [raw_lines[0]]
for l in raw_lines[1:]:
- if l.startswith('{') or l.startswith('}') or l.startswith('~'):
+ if l and l[0] in ['{', '}', '~', '>']:
lines.append(l)
else:
lines[-1] += '\\n' + l
@@ -102,13 +116,14 @@ def _format_lines(lines):
stackcnt.append(0)
result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:])
elif line.startswith('}'):
- assert line.startswith('}')
stack.pop()
stackcnt.pop()
result[stack[-1]] += line[1:]
else:
- assert line.startswith('~')
- result.append(u(' ')*len(stack) + line[1:])
+ assert line[0] in ['~', '>']
+ stack[-1] += 1
+ indent = len(stack) if line.startswith('~') else len(stack) - 1
+ result.append(u(' ')*indent + line[1:])
assert len(stack) == 1
return result
@@ -125,35 +140,49 @@ def assertrepr_compare(config, op, left, right):
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
left_repr = py.io.saferepr(left, maxsize=int(width/2))
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
- summary = u('%s %s %s') % (left_repr, op, right_repr)
- issequence = lambda x: (isinstance(x, (list, tuple, Sequence))
- and not isinstance(x, basestring))
+ summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
+
+ issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and
+ not isinstance(x, basestring))
istext = lambda x: isinstance(x, basestring)
isdict = lambda x: isinstance(x, dict)
isset = lambda x: isinstance(x, (set, frozenset))
+ def isiterable(obj):
+ try:
+ iter(obj)
+ return not istext(obj)
+ except TypeError:
+ return False
+
verbose = config.getoption('verbose')
explanation = None
try:
if op == '==':
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
- elif issequence(left) and issequence(right):
- explanation = _compare_eq_sequence(left, right, verbose)
- elif isset(left) and isset(right):
- explanation = _compare_eq_set(left, right, verbose)
- elif isdict(left) and isdict(right):
- explanation = _compare_eq_dict(left, right, verbose)
+ else:
+ if issequence(left) and issequence(right):
+ explanation = _compare_eq_sequence(left, right, verbose)
+ elif isset(left) and isset(right):
+ explanation = _compare_eq_set(left, right, verbose)
+ elif isdict(left) and isdict(right):
+ explanation = _compare_eq_dict(left, right, verbose)
+ if isiterable(left) and isiterable(right):
+ expl = _compare_eq_iterable(left, right, verbose)
+ if explanation is not None:
+ explanation.extend(expl)
+ else:
+ explanation = expl
elif op == 'not in':
if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose)
except Exception:
- excinfo = py.code.ExceptionInfo()
explanation = [
u('(pytest_assertion plugin: representation of details failed. '
'Probably an object has a faulty __repr__.)'),
- u(excinfo)]
+ u(_pytest._code.ExceptionInfo())]
if not explanation:
return None
@@ -169,6 +198,7 @@ def _diff_text(left, right, verbose=False):
If the input are bytes they will be safely converted to text.
"""
+ from difflib import ndiff
explanation = []
if isinstance(left, py.builtin.bytes):
left = u(repr(left)[1:-1]).replace(r'\n', '\n')
@@ -196,8 +226,29 @@ def _diff_text(left, right, verbose=False):
left = left[:-i]
right = right[:-i]
explanation += [line.strip('\n')
- for line in py.std.difflib.ndiff(left.splitlines(),
- right.splitlines())]
+ for line in ndiff(left.splitlines(),
+ right.splitlines())]
+ return explanation
+
+
+def _compare_eq_iterable(left, right, verbose=False):
+ if not verbose:
+ return [u('Use -v to get the full diff')]
+ # dynamic import to speedup pytest
+ import difflib
+
+ try:
+ left_formatting = pprint.pformat(left).splitlines()
+ right_formatting = pprint.pformat(right).splitlines()
+ explanation = [u('Full diff:')]
+ except Exception:
+ # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling
+ # sorted() on a list would raise. See issue #718.
+ # As a workaround, the full diff is generated by using the repr() string of each item of each container.
+ left_formatting = sorted(repr(x) for x in left)
+ right_formatting = sorted(repr(x) for x in right)
+ explanation = [u('Full diff (fallback to calling repr on each item):')]
+ explanation.extend(line.strip() for line in difflib.ndiff(left_formatting, right_formatting))
return explanation
@@ -215,8 +266,7 @@ def _compare_eq_sequence(left, right, verbose=False):
explanation += [
u('Right contains more items, first extra item: %s') %
py.io.saferepr(right[len(left)],)]
- return explanation # + _diff_text(py.std.pprint.pformat(left),
- # py.std.pprint.pformat(right))
+ return explanation
def _compare_eq_set(left, right, verbose=False):
@@ -243,7 +293,7 @@ def _compare_eq_dict(left, right, verbose=False):
len(same)]
elif same:
explanation += [u('Common items:')]
- explanation += py.std.pprint.pformat(same).splitlines()
+ explanation += pprint.pformat(same).splitlines()
diff = set(k for k in common if left[k] != right[k])
if diff:
explanation += [u('Differing items:')]
@@ -253,12 +303,12 @@ def _compare_eq_dict(left, right, verbose=False):
extra_left = set(left) - set(right)
if extra_left:
explanation.append(u('Left contains more items:'))
- explanation.extend(py.std.pprint.pformat(
+ explanation.extend(pprint.pformat(
dict((k, left[k]) for k in extra_left)).splitlines())
extra_right = set(right) - set(left)
if extra_right:
explanation.append(u('Right contains more items:'))
- explanation.extend(py.std.pprint.pformat(
+ explanation.extend(pprint.pformat(
dict((k, right[k]) for k in extra_right)).splitlines())
return explanation
diff --git a/_pytest/capture.py b/_pytest/capture.py
index 04fcbbd0d5..3895a714aa 100644
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -1,40 +1,18 @@
"""
- per-test stdout/stderr capturing mechanisms,
- ``capsys`` and ``capfd`` function arguments.
+per-test stdout/stderr capturing mechanism.
+
"""
-# note: py.io capture was where copied from
-# pylib 1.4.20.dev2 (rev 13d9af95547e)
+from __future__ import with_statement
+
import sys
import os
-import tempfile
+from tempfile import TemporaryFile
import py
import pytest
-try:
- from io import StringIO
-except ImportError:
- from StringIO import StringIO
-
-try:
- from io import BytesIO
-except ImportError:
- class BytesIO(StringIO):
- def write(self, data):
- if isinstance(data, unicode):
- raise TypeError("not a byte value: %r" % (data,))
- StringIO.write(self, data)
-
-if sys.version_info < (3, 0):
- class TextIO(StringIO):
- def write(self, data):
- if not isinstance(data, unicode):
- enc = getattr(self, '_encoding', 'UTF-8')
- data = unicode(data, enc, 'replace')
- StringIO.write(self, data)
-else:
- TextIO = StringIO
-
+from py.io import TextIO
+unicode = py.builtin.text
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
@@ -42,255 +20,179 @@ patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
def pytest_addoption(parser):
group = parser.getgroup("general")
group._addoption(
- '--capture', action="store", default=None,
+ '--capture', action="store",
+ default="fd" if hasattr(os, "dup") else "sys",
metavar="method", choices=['fd', 'sys', 'no'],
- help="per-test capturing method: one of fd (default)|sys|no.")
+ help="per-test capturing method: one of fd|sys|no.")
group._addoption(
'-s', action="store_const", const="no", dest="capture",
help="shortcut for --capture=no.")
-@pytest.mark.tryfirst
-def pytest_load_initial_conftests(early_config, parser, args, __multicall__):
- ns = parser.parse_known_args(args)
- method = ns.capture
- if not method:
- method = "fd"
- if method == "fd" and not hasattr(os, "dup"):
- method = "sys"
- capman = CaptureManager(method)
- early_config.pluginmanager.register(capman, "capturemanager")
+@pytest.hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(early_config, parser, args):
+ _readline_workaround()
+ ns = early_config.known_args_namespace
+ pluginmanager = early_config.pluginmanager
+ capman = CaptureManager(ns.capture)
+ pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown
- def teardown():
- try:
- capman.reset_capturings()
- except ValueError:
- pass
-
- early_config.pluginmanager.add_shutdown(teardown)
+ early_config.add_cleanup(capman.reset_capturings)
# make sure logging does not raise exceptions at the end
def silence_logging_at_shutdown():
if "logging" in sys.modules:
sys.modules["logging"].raiseExceptions = False
- early_config.pluginmanager.add_shutdown(silence_logging_at_shutdown)
+ early_config.add_cleanup(silence_logging_at_shutdown)
# finally trigger conftest loading but while capturing (issue93)
- capman.resumecapture()
- try:
- try:
- return __multicall__.execute()
- finally:
- out, err = capman.suspendcapture()
- except:
+ capman.init_capturings()
+ outcome = yield
+ out, err = capman.suspendcapture()
+ if outcome.excinfo is not None:
sys.stdout.write(out)
sys.stderr.write(err)
- raise
-
-
-def addouterr(rep, outerr):
- for secname, content in zip(["out", "err"], outerr):
- if content:
- rep.sections.append(("Captured std%s" % secname, content))
-
-
-class NoCapture:
- def startall(self):
- pass
-
- def resume(self):
- pass
-
- def reset(self):
- pass
-
- def suspend(self):
- return "", ""
class CaptureManager:
- def __init__(self, defaultmethod=None):
- self._method2capture = {}
- self._defaultmethod = defaultmethod
-
- def _maketempfile(self):
- f = py.std.tempfile.TemporaryFile()
- newf = dupfile(f, encoding="UTF-8")
- f.close()
- return newf
-
- def _makestringio(self):
- return TextIO()
+ def __init__(self, method):
+ self._method = method
def _getcapture(self, method):
if method == "fd":
- return StdCaptureFD(
- out=self._maketempfile(),
- err=self._maketempfile(),
- )
+ return MultiCapture(out=True, err=True, Capture=FDCapture)
elif method == "sys":
- return StdCapture(
- out=self._makestringio(),
- err=self._makestringio(),
- )
+ return MultiCapture(out=True, err=True, Capture=SysCapture)
elif method == "no":
- return NoCapture()
+ return MultiCapture(out=False, err=False, in_=False)
else:
raise ValueError("unknown capturing method: %r" % method)
- def _getmethod(self, config, fspath):
- if config.option.capture:
- method = config.option.capture
- else:
- try:
- method = config._conftest.rget("option_capture", path=fspath)
- except KeyError:
- method = "fd"
- if method == "fd" and not hasattr(os, 'dup'): # e.g. jython
- method = "sys"
- return method
+ def init_capturings(self):
+ assert not hasattr(self, "_capturing")
+ self._capturing = self._getcapture(self._method)
+ self._capturing.start_capturing()
def reset_capturings(self):
- for cap in self._method2capture.values():
- cap.reset()
-
- def resumecapture_item(self, item):
- method = self._getmethod(item.config, item.fspath)
- if not hasattr(item, 'outerr'):
- item.outerr = ('', '') # we accumulate outerr on the item
- return self.resumecapture(method)
-
- def resumecapture(self, method=None):
- if hasattr(self, '_capturing'):
- raise ValueError(
- "cannot resume, already capturing with %r" %
- (self._capturing,))
- if method is None:
- method = self._defaultmethod
- cap = self._method2capture.get(method)
- self._capturing = method
- if cap is None:
- self._method2capture[method] = cap = self._getcapture(method)
- cap.startall()
- else:
- cap.resume()
+ cap = self.__dict__.pop("_capturing", None)
+ if cap is not None:
+ cap.pop_outerr_to_orig()
+ cap.stop_capturing()
- def suspendcapture(self, item=None):
+ def resumecapture(self):
+ self._capturing.resume_capturing()
+
+ def suspendcapture(self, in_=False):
self.deactivate_funcargs()
- if hasattr(self, '_capturing'):
- method = self._capturing
- cap = self._method2capture.get(method)
- if cap is not None:
- outerr = cap.suspend()
- del self._capturing
- if item:
- outerr = (item.outerr[0] + outerr[0],
- item.outerr[1] + outerr[1])
+ cap = getattr(self, "_capturing", None)
+ if cap is not None:
+ try:
+ outerr = cap.readouterr()
+ finally:
+ cap.suspend_capturing(in_=in_)
return outerr
- if hasattr(item, 'outerr'):
- return item.outerr
- return "", ""
def activate_funcargs(self, pyfuncitem):
- funcargs = getattr(pyfuncitem, "funcargs", None)
- if funcargs is not None:
- for name, capfuncarg in funcargs.items():
- if name in ('capsys', 'capfd'):
- assert not hasattr(self, '_capturing_funcarg')
- self._capturing_funcarg = capfuncarg
- capfuncarg._start()
+ capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
+ if capfuncarg is not None:
+ capfuncarg._start()
+ self._capfuncarg = capfuncarg
def deactivate_funcargs(self):
- capturing_funcarg = getattr(self, '_capturing_funcarg', None)
- if capturing_funcarg:
- outerr = capturing_funcarg._finalize()
- del self._capturing_funcarg
- return outerr
-
- def pytest_make_collect_report(self, __multicall__, collector):
- method = self._getmethod(collector.config, collector.fspath)
- try:
- self.resumecapture(method)
- except ValueError:
- # recursive collect, XXX refactor capturing
- # to allow for more lightweight recursive capturing
- return
- try:
- rep = __multicall__.execute()
- finally:
- outerr = self.suspendcapture()
- addouterr(rep, outerr)
- return rep
+ capfuncarg = self.__dict__.pop("_capfuncarg", None)
+ if capfuncarg is not None:
+ capfuncarg.close()
+
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_make_collect_report(self, collector):
+ if isinstance(collector, pytest.File):
+ self.resumecapture()
+ outcome = yield
+ out, err = self.suspendcapture()
+ rep = outcome.get_result()
+ if out:
+ rep.sections.append(("Captured stdout", out))
+ if err:
+ rep.sections.append(("Captured stderr", err))
+ else:
+ yield
- @pytest.mark.tryfirst
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
- self.resumecapture_item(item)
+ self.resumecapture()
+ yield
+ self.suspendcapture_item(item, "setup")
- @pytest.mark.tryfirst
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item):
- self.resumecapture_item(item)
+ self.resumecapture()
self.activate_funcargs(item)
+ yield
+ #self.deactivate_funcargs() called from suspendcapture()
+ self.suspendcapture_item(item, "call")
- @pytest.mark.tryfirst
+ @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
- self.resumecapture_item(item)
+ self.resumecapture()
+ yield
+ self.suspendcapture_item(item, "teardown")
+ @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo):
- if hasattr(self, '_capturing'):
- self.suspendcapture()
-
- @pytest.mark.tryfirst
- def pytest_runtest_makereport(self, __multicall__, item, call):
- funcarg_outerr = self.deactivate_funcargs()
- rep = __multicall__.execute()
- outerr = self.suspendcapture(item)
- if funcarg_outerr is not None:
- outerr = (outerr[0] + funcarg_outerr[0],
- outerr[1] + funcarg_outerr[1])
- addouterr(rep, outerr)
- if not rep.passed or rep.when == "teardown":
- outerr = ('', '')
- item.outerr = outerr
- return rep
+ self.reset_capturings()
+
+ @pytest.hookimpl(tryfirst=True)
+ def pytest_internalerror(self, excinfo):
+ self.reset_capturings()
+
+ def suspendcapture_item(self, item, when):
+ out, err = self.suspendcapture()
+ item.add_report_section(when, "stdout", out)
+ item.add_report_section(when, "stderr", err)
error_capsysfderror = "cannot use capsys and capfd at the same time"
-def pytest_funcarg__capsys(request):
+@pytest.fixture
+def capsys(request):
"""enables capturing of writes to sys.stdout/sys.stderr and makes
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
if "capfd" in request._funcargs:
raise request.raiseerror(error_capsysfderror)
- return CaptureFixture(StdCapture)
-
+ request.node._capfuncarg = c = CaptureFixture(SysCapture)
+ return c
-def pytest_funcarg__capfd(request):
+@pytest.fixture
+def capfd(request):
"""enables capturing of writes to file descriptors 1 and 2 and makes
- captured output available via ``capsys.readouterr()`` method calls
+ captured output available via ``capfd.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
if "capsys" in request._funcargs:
request.raiseerror(error_capsysfderror)
if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup")
- return CaptureFixture(StdCaptureFD)
+ request.node._capfuncarg = c = CaptureFixture(FDCapture)
+ return c
class CaptureFixture:
def __init__(self, captureclass):
- self._capture = captureclass()
+ self.captureclass = captureclass
def _start(self):
- self._capture.startall()
+ self._capture = MultiCapture(out=True, err=True, in_=False,
+ Capture=self.captureclass)
+ self._capture.start_capturing()
- def _finalize(self):
- if hasattr(self, '_capture'):
- outerr = self._outerr = self._capture.reset()
- del self._capture
- return outerr
+ def close(self):
+ cap = self.__dict__.pop("_capture", None)
+ if cap is not None:
+ self._outerr = cap.pop_outerr_to_orig()
+ cap.stop_capturing()
def readouterr(self):
try:
@@ -298,295 +200,223 @@ class CaptureFixture:
except AttributeError:
return self._outerr
- def close(self):
- self._finalize()
-
-
-class FDCapture:
- """ Capture IO to/from a given os-level filedescriptor. """
-
- def __init__(self, targetfd, tmpfile=None, patchsys=False):
- """ save targetfd descriptor, and open a new
- temporary file there. If no tmpfile is
- specified a tempfile.Tempfile() will be opened
- in text mode.
- """
- self.targetfd = targetfd
- if tmpfile is None and targetfd != 0:
- f = tempfile.TemporaryFile('wb+')
- tmpfile = dupfile(f, encoding="UTF-8")
- f.close()
- self.tmpfile = tmpfile
- self._savefd = os.dup(self.targetfd)
- if patchsys:
- self._oldsys = getattr(sys, patchsysdict[targetfd])
-
- def start(self):
- try:
- os.fstat(self._savefd)
- except OSError:
- raise ValueError(
- "saved filedescriptor not valid, "
- "did you call start() twice?")
- if self.targetfd == 0 and not self.tmpfile:
- fd = os.open(os.devnull, os.O_RDONLY)
- os.dup2(fd, 0)
- os.close(fd)
- if hasattr(self, '_oldsys'):
- setattr(sys, patchsysdict[self.targetfd], DontReadFromInput())
- else:
- os.dup2(self.tmpfile.fileno(), self.targetfd)
- if hasattr(self, '_oldsys'):
- setattr(sys, patchsysdict[self.targetfd], self.tmpfile)
-
- def done(self):
- """ unpatch and clean up, returns the self.tmpfile (file object)
- """
- os.dup2(self._savefd, self.targetfd)
- os.close(self._savefd)
- if self.targetfd != 0:
- self.tmpfile.seek(0)
- if hasattr(self, '_oldsys'):
- setattr(sys, patchsysdict[self.targetfd], self._oldsys)
- return self.tmpfile
- def writeorg(self, data):
- """ write a string to the original file descriptor
- """
- tempfp = tempfile.TemporaryFile()
- try:
- os.dup2(self._savefd, tempfp.fileno())
- tempfp.write(data)
- finally:
- tempfp.close()
-
-
-def dupfile(f, mode=None, buffering=0, raising=False, encoding=None):
- """ return a new open file object that's a duplicate of f
-
- mode is duplicated if not given, 'buffering' controls
- buffer size (defaulting to no buffering) and 'raising'
- defines whether an exception is raised when an incompatible
- file object is passed in (if raising is False, the file
- object itself will be returned)
+def safe_text_dupfile(f, mode, default_encoding="UTF8"):
+ """ return a open text file object that's a duplicate of f on the
+ FD-level if possible.
"""
+ encoding = getattr(f, "encoding", None)
try:
fd = f.fileno()
- mode = mode or f.mode
- except AttributeError:
- if raising:
- raise
- return f
- newfd = os.dup(fd)
- if sys.version_info >= (3, 0):
- if encoding is not None:
- mode = mode.replace("b", "")
- buffering = True
- return os.fdopen(newfd, mode, buffering, encoding, closefd=True)
+ except Exception:
+ if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"):
+ # we seem to have a text stream, let's just use it
+ return f
else:
- f = os.fdopen(newfd, mode, buffering)
- if encoding is not None:
- return EncodedFile(f, encoding)
- return f
+ newfd = os.dup(fd)
+ if "b" not in mode:
+ mode += "b"
+ f = os.fdopen(newfd, mode, 0) # no buffering
+ return EncodedFile(f, encoding or default_encoding)
class EncodedFile(object):
- def __init__(self, _stream, encoding):
- self._stream = _stream
+ errors = "strict" # possibly needed by py3 code (issue555)
+ def __init__(self, buffer, encoding):
+ self.buffer = buffer
self.encoding = encoding
def write(self, obj):
if isinstance(obj, unicode):
- obj = obj.encode(self.encoding)
- self._stream.write(obj)
+ obj = obj.encode(self.encoding, "replace")
+ self.buffer.write(obj)
def writelines(self, linelist):
data = ''.join(linelist)
self.write(data)
def __getattr__(self, name):
- return getattr(self._stream, name)
+ return getattr(object.__getattribute__(self, "buffer"), name)
-class Capture(object):
- def reset(self):
- """ reset sys.stdout/stderr and return captured output as strings. """
- if hasattr(self, '_reset'):
- raise ValueError("was already reset")
- self._reset = True
- outfile, errfile = self.done(save=False)
- out, err = "", ""
- if outfile and not outfile.closed:
- out = outfile.read()
- outfile.close()
- if errfile and errfile != outfile and not errfile.closed:
- err = errfile.read()
- errfile.close()
- return out, err
+class MultiCapture(object):
+ out = err = in_ = None
- def suspend(self):
- """ return current snapshot captures, memorize tempfiles. """
- outerr = self.readouterr()
- outfile, errfile = self.done()
- return outerr
-
-
-class StdCaptureFD(Capture):
- """ This class allows to capture writes to FD1 and FD2
- and may connect a NULL file to FD0 (and prevent
- reads from sys.stdin). If any of the 0,1,2 file descriptors
- is invalid it will not be captured.
- """
- def __init__(self, out=True, err=True, in_=True, patchsys=True):
- self._options = {
- "out": out,
- "err": err,
- "in_": in_,
- "patchsys": patchsys,
- }
- self._save()
-
- def _save(self):
- in_ = self._options['in_']
- out = self._options['out']
- err = self._options['err']
- patchsys = self._options['patchsys']
+ def __init__(self, out=True, err=True, in_=True, Capture=None):
if in_:
- try:
- self.in_ = FDCapture(
- 0, tmpfile=None,
- patchsys=patchsys)
- except OSError:
- pass
+ self.in_ = Capture(0)
if out:
- tmpfile = None
- if hasattr(out, 'write'):
- tmpfile = out
- try:
- self.out = FDCapture(
- 1, tmpfile=tmpfile,
- patchsys=patchsys)
- self._options['out'] = self.out.tmpfile
- except OSError:
- pass
+ self.out = Capture(1)
if err:
- if hasattr(err, 'write'):
- tmpfile = err
- else:
- tmpfile = None
- try:
- self.err = FDCapture(
- 2, tmpfile=tmpfile,
- patchsys=patchsys)
- self._options['err'] = self.err.tmpfile
- except OSError:
- pass
-
- def startall(self):
- if hasattr(self, 'in_'):
+ self.err = Capture(2)
+
+ def start_capturing(self):
+ if self.in_:
self.in_.start()
- if hasattr(self, 'out'):
+ if self.out:
self.out.start()
- if hasattr(self, 'err'):
+ if self.err:
self.err.start()
- def resume(self):
- """ resume capturing with original temp files. """
- self.startall()
-
- def done(self, save=True):
- """ return (outfile, errfile) and stop capturing. """
- outfile = errfile = None
- if hasattr(self, 'out') and not self.out.tmpfile.closed:
- outfile = self.out.done()
- if hasattr(self, 'err') and not self.err.tmpfile.closed:
- errfile = self.err.done()
- if hasattr(self, 'in_'):
+ def pop_outerr_to_orig(self):
+ """ pop current snapshot out/err capture and flush to orig streams. """
+ out, err = self.readouterr()
+ if out:
+ self.out.writeorg(out)
+ if err:
+ self.err.writeorg(err)
+ return out, err
+
+ def suspend_capturing(self, in_=False):
+ if self.out:
+ self.out.suspend()
+ if self.err:
+ self.err.suspend()
+ if in_ and self.in_:
+ self.in_.suspend()
+ self._in_suspended = True
+
+ def resume_capturing(self):
+ if self.out:
+ self.out.resume()
+ if self.err:
+ self.err.resume()
+ if hasattr(self, "_in_suspended"):
+ self.in_.resume()
+ del self._in_suspended
+
+ def stop_capturing(self):
+ """ stop capturing and reset capturing streams """
+ if hasattr(self, '_reset'):
+ raise ValueError("was already stopped")
+ self._reset = True
+ if self.out:
+ self.out.done()
+ if self.err:
+ self.err.done()
+ if self.in_:
self.in_.done()
- if save:
- self._save()
- return outfile, errfile
def readouterr(self):
- """ return snapshot value of stdout/stderr capturings. """
- out = self._readsnapshot('out')
- err = self._readsnapshot('err')
- return out, err
+ """ return snapshot unicode value of stdout/stderr capturings. """
+ return (self.out.snap() if self.out is not None else "",
+ self.err.snap() if self.err is not None else "")
- def _readsnapshot(self, name):
- if hasattr(self, name):
- f = getattr(self, name).tmpfile
+class NoCapture:
+ __init__ = start = done = suspend = resume = lambda *args: None
+
+class FDCapture:
+ """ Capture IO to/from a given os-level filedescriptor. """
+
+ def __init__(self, targetfd, tmpfile=None):
+ self.targetfd = targetfd
+ try:
+ self.targetfd_save = os.dup(self.targetfd)
+ except OSError:
+ self.start = lambda: None
+ self.done = lambda: None
else:
- return ''
+ if targetfd == 0:
+ assert not tmpfile, "cannot set tmpfile with stdin"
+ tmpfile = open(os.devnull, "r")
+ self.syscapture = SysCapture(targetfd)
+ else:
+ if tmpfile is None:
+ f = TemporaryFile()
+ with f:
+ tmpfile = safe_text_dupfile(f, mode="wb+")
+ if targetfd in patchsysdict:
+ self.syscapture = SysCapture(targetfd, tmpfile)
+ else:
+ self.syscapture = NoCapture()
+ self.tmpfile = tmpfile
+ self.tmpfile_fd = tmpfile.fileno()
+
+ def __repr__(self):
+ return "<FDCapture %s oldfd=%s>" % (self.targetfd, self.targetfd_save)
+ def start(self):
+ """ Start capturing on targetfd using memorized tmpfile. """
+ try:
+ os.fstat(self.targetfd_save)
+ except (AttributeError, OSError):
+ raise ValueError("saved filedescriptor not valid anymore")
+ os.dup2(self.tmpfile_fd, self.targetfd)
+ self.syscapture.start()
+
+ def snap(self):
+ f = self.tmpfile
f.seek(0)
res = f.read()
- enc = getattr(f, "encoding", None)
- if enc:
- res = py.builtin._totext(res, enc, "replace")
+ if res:
+ enc = getattr(f, "encoding", None)
+ if enc and isinstance(res, bytes):
+ res = py.builtin._totext(res, enc, "replace")
+ f.truncate(0)
+ f.seek(0)
+ return res
+ return ''
+
+ def done(self):
+ """ stop capturing, restore streams, return original capture file,
+ seeked to position zero. """
+ targetfd_save = self.__dict__.pop("targetfd_save")
+ os.dup2(targetfd_save, self.targetfd)
+ os.close(targetfd_save)
+ self.syscapture.done()
+ self.tmpfile.close()
+
+ def suspend(self):
+ self.syscapture.suspend()
+ os.dup2(self.targetfd_save, self.targetfd)
+
+ def resume(self):
+ self.syscapture.resume()
+ os.dup2(self.tmpfile_fd, self.targetfd)
+
+ def writeorg(self, data):
+ """ write to original file descriptor. """
+ if py.builtin._istext(data):
+ data = data.encode("utf8") # XXX use encoding of original stream
+ os.write(self.targetfd_save, data)
+
+
+class SysCapture:
+ def __init__(self, fd, tmpfile=None):
+ name = patchsysdict[fd]
+ self._old = getattr(sys, name)
+ self.name = name
+ if tmpfile is None:
+ if name == "stdin":
+ tmpfile = DontReadFromInput()
+ else:
+ tmpfile = TextIO()
+ self.tmpfile = tmpfile
+
+ def start(self):
+ setattr(sys, self.name, self.tmpfile)
+
+ def snap(self):
+ f = self.tmpfile
+ res = f.getvalue()
f.truncate(0)
f.seek(0)
return res
+ def done(self):
+ setattr(sys, self.name, self._old)
+ del self._old
+ self.tmpfile.close()
-class StdCapture(Capture):
- """ This class allows to capture writes to sys.stdout|stderr "in-memory"
- and will raise errors on tries to read from sys.stdin. It only
- modifies sys.stdout|stderr|stdin attributes and does not
- touch underlying File Descriptors (use StdCaptureFD for that).
- """
- def __init__(self, out=True, err=True, in_=True):
- self._oldout = sys.stdout
- self._olderr = sys.stderr
- self._oldin = sys.stdin
- if out and not hasattr(out, 'file'):
- out = TextIO()
- self.out = out
- if err:
- if not hasattr(err, 'write'):
- err = TextIO()
- self.err = err
- self.in_ = in_
-
- def startall(self):
- if self.out:
- sys.stdout = self.out
- if self.err:
- sys.stderr = self.err
- if self.in_:
- sys.stdin = self.in_ = DontReadFromInput()
-
- def done(self, save=True):
- """ return (outfile, errfile) and stop capturing. """
- outfile = errfile = None
- if self.out and not self.out.closed:
- sys.stdout = self._oldout
- outfile = self.out
- outfile.seek(0)
- if self.err and not self.err.closed:
- sys.stderr = self._olderr
- errfile = self.err
- errfile.seek(0)
- if self.in_:
- sys.stdin = self._oldin
- return outfile, errfile
+ def suspend(self):
+ setattr(sys, self.name, self._old)
def resume(self):
- """ resume capturing with original temp files. """
- self.startall()
+ setattr(sys, self.name, self.tmpfile)
- def readouterr(self):
- """ return snapshot value of stdout/stderr capturings. """
- out = err = ""
- if self.out:
- out = self.out.getvalue()
- self.out.truncate(0)
- self.out.seek(0)
- if self.err:
- err = self.err.getvalue()
- self.err.truncate(0)
- self.err.seek(0)
- return out, err
+ def writeorg(self, data):
+ self._old.write(data)
+ self._old.flush()
class DontReadFromInput:
@@ -596,6 +426,9 @@ class DontReadFromInput:
because in automated test runs it is better to crash than
hang indefinitely.
"""
+
+ encoding = None
+
def read(self, *args):
raise IOError("reading from stdin while output is captured")
readline = read
@@ -610,3 +443,30 @@ class DontReadFromInput:
def close(self):
pass
+
+
+def _readline_workaround():
+ """
+ Ensure readline is imported so that it attaches to the correct stdio
+ handles on Windows.
+
+ Pdb uses readline support where available--when not running from the Python
+ prompt, the readline module is not imported until running the pdb REPL. If
+ running py.test with the --pdb option this means the readline module is not
+ imported until after I/O capture has been started.
+
+ This is a problem for pyreadline, which is often used to implement readline
+ support on Windows, as it does not attach to the correct handles for stdout
+ and/or stdin if they have been redirected by the FDCapture mechanism. This
+ workaround ensures that readline is imported before I/O capture is setup so
+ that it can attach to the actual stdin/out for the console.
+
+ See https://github.com/pytest-dev/pytest/pull/1281
+ """
+
+ if not sys.platform.startswith('win32'):
+ return
+ try:
+ import readline # noqa
+ except ImportError:
+ pass
diff --git a/_pytest/config.py b/_pytest/config.py
index 338b403020..9a308df2bb 100644
--- a/_pytest/config.py
+++ b/_pytest/config.py
@@ -1,12 +1,30 @@
""" command line options, ini-file and conftest.py processing. """
+import argparse
+import shlex
+import traceback
+import types
+import warnings
import py
# DON't import pytest here because it causes import cycle troubles
import sys, os
-from _pytest import hookspec # the extension point definitions
-from _pytest.core import PluginManager
+import _pytest._code
+import _pytest.hookspec # the extension point definitions
+from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker
+
+hookimpl = HookimplMarker("pytest")
+hookspec = HookspecMarker("pytest")
# pytest startup
+#
+
+
+class ConftestImportFailure(Exception):
+ def __init__(self, path, excinfo):
+ Exception.__init__(self, path, excinfo)
+ self.path = path
+ self.excinfo = excinfo
+
def main(args=None, plugins=None):
""" return exit code, after performing an in-process test run.
@@ -16,8 +34,25 @@ def main(args=None, plugins=None):
:arg plugins: list of plugin objects to be auto-registered during
initialization.
"""
- config = _prepareconfig(args, plugins)
- return config.hook.pytest_cmdline_main(config=config)
+ try:
+ try:
+ config = _prepareconfig(args, plugins)
+ except ConftestImportFailure as e:
+ tw = py.io.TerminalWriter(sys.stderr)
+ for line in traceback.format_exception(*e.excinfo):
+ tw.line(line.rstrip(), red=True)
+ tw.line("ERROR: could not load %s\n" % (e.path), red=True)
+ return 4
+ else:
+ try:
+ config.pluginmanager.check_pending()
+ return config.hook.pytest_cmdline_main(config=config)
+ finally:
+ config._ensure_unconfigure()
+ except UsageError as e:
+ for msg in e.args:
+ sys.stderr.write("ERROR: %s\n" %(msg,))
+ return 4
class cmdline: # compatibility namespace
main = staticmethod(main)
@@ -30,21 +65,36 @@ _preinit = []
default_plugins = (
"mark main terminal runner python pdb unittest capture skipping "
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
- "junitxml resultlog doctest").split()
+ "junitxml resultlog doctest cacheprovider").split()
+
+builtin_plugins = set(default_plugins)
+builtin_plugins.add("pytester")
+
def _preloadplugins():
assert not _preinit
- _preinit.append(get_plugin_manager())
+ _preinit.append(get_config())
-def get_plugin_manager():
+def get_config():
if _preinit:
return _preinit.pop(0)
# subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager()
- pluginmanager.config = Config(pluginmanager) # XXX attr needed?
+ config = Config(pluginmanager)
for spec in default_plugins:
pluginmanager.import_plugin(spec)
- return pluginmanager
+ return config
+
+def get_plugin_manager():
+ """
+ Obtain a new instance of the
+ :py:class:`_pytest.config.PytestPluginManager`, with default plugins
+ already loaded.
+
+ This function can be used by integration with other tools, like hooking
+ into pytest to run tests into an IDE.
+ """
+ return get_config().pluginmanager
def _prepareconfig(args=None, plugins=None):
if args is None:
@@ -54,17 +104,43 @@ def _prepareconfig(args=None, plugins=None):
elif not isinstance(args, (tuple, list)):
if not isinstance(args, str):
raise ValueError("not a string or argument list: %r" % (args,))
- args = py.std.shlex.split(args)
- pluginmanager = get_plugin_manager()
- if plugins:
- for plugin in plugins:
- pluginmanager.register(plugin)
- return pluginmanager.hook.pytest_cmdline_parse(
- pluginmanager=pluginmanager, args=args)
+ args = shlex.split(args, posix=sys.platform != "win32")
+ config = get_config()
+ pluginmanager = config.pluginmanager
+ try:
+ if plugins:
+ for plugin in plugins:
+ if isinstance(plugin, py.builtin._basestring):
+ pluginmanager.consider_pluginarg(plugin)
+ else:
+ pluginmanager.register(plugin)
+ return pluginmanager.hook.pytest_cmdline_parse(
+ pluginmanager=pluginmanager, args=args)
+ except BaseException:
+ config._ensure_unconfigure()
+ raise
+
class PytestPluginManager(PluginManager):
- def __init__(self, hookspecs=[hookspec]):
- super(PytestPluginManager, self).__init__(hookspecs=hookspecs)
+ """
+ Overwrites :py:class:`pluggy.PluginManager` to add pytest-specific
+ functionality:
+
+ * loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and
+ ``pytest_plugins`` global variables found in plugins being loaded;
+ * ``conftest.py`` loading during start-up;
+ """
+ def __init__(self):
+ super(PytestPluginManager, self).__init__("pytest", implprefix="pytest_")
+ self._conftest_plugins = set()
+
+ # state related to local conftest plugins
+ self._path2confmods = {}
+ self._conftestpath2mod = {}
+ self._confcutdir = None
+ self._noconftest = False
+
+ self.add_hookspecs(_pytest.hookspec)
self.register(self)
if os.environ.get('PYTEST_DEBUG'):
err = sys.stderr
@@ -74,8 +150,78 @@ class PytestPluginManager(PluginManager):
except Exception:
pass
self.trace.root.setwriter(err.write)
+ self.enable_tracing()
+
+ def addhooks(self, module_or_class):
+ """
+ .. deprecated:: 2.8
+
+ Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead.
+ """
+ warning = dict(code="I2",
+ fslocation=_pytest._code.getfslineno(sys._getframe(1)),
+ nodeid=None,
+ message="use pluginmanager.add_hookspecs instead of "
+ "deprecated addhooks() method.")
+ self._warn(warning)
+ return self.add_hookspecs(module_or_class)
+
+ def parse_hookimpl_opts(self, plugin, name):
+ # pytest hooks are always prefixed with pytest_
+ # so we avoid accessing possibly non-readable attributes
+ # (see issue #1073)
+ if not name.startswith("pytest_"):
+ return
+ # ignore some historic special names which can not be hooks anyway
+ if name == "pytest_plugins" or name.startswith("pytest_funcarg__"):
+ return
+
+ method = getattr(plugin, name)
+ opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name)
+ if opts is not None:
+ for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
+ opts.setdefault(name, hasattr(method, name))
+ return opts
+
+ def parse_hookspec_opts(self, module_or_class, name):
+ opts = super(PytestPluginManager, self).parse_hookspec_opts(
+ module_or_class, name)
+ if opts is None:
+ method = getattr(module_or_class, name)
+ if name.startswith("pytest_"):
+ opts = {"firstresult": hasattr(method, "firstresult"),
+ "historic": hasattr(method, "historic")}
+ return opts
+
+ def _verify_hook(self, hook, hookmethod):
+ super(PytestPluginManager, self)._verify_hook(hook, hookmethod)
+ if "__multicall__" in hookmethod.argnames:
+ fslineno = _pytest._code.getfslineno(hookmethod.function)
+ warning = dict(code="I1",
+ fslocation=fslineno,
+ nodeid=None,
+ message="%r hook uses deprecated __multicall__ "
+ "argument" % (hook.name))
+ self._warn(warning)
+
+ def register(self, plugin, name=None):
+ ret = super(PytestPluginManager, self).register(plugin, name)
+ if ret:
+ self.hook.pytest_plugin_registered.call_historic(
+ kwargs=dict(plugin=plugin, manager=self))
+ return ret
+
+ def getplugin(self, name):
+ # support deprecated naming because plugins (xdist e.g.) use it
+ return self.get_plugin(name)
+
+ def hasplugin(self, name):
+ """Return True if the plugin with the given name is registered."""
+ return bool(self.get_plugin(name))
def pytest_configure(self, config):
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
+ # we should remove tryfirst/trylast as markers
config.addinivalue_line("markers",
"tryfirst: mark a hook implementation function such that the "
"plugin machinery will try to call it first/as early as possible.")
@@ -83,9 +229,184 @@ class PytestPluginManager(PluginManager):
"trylast: mark a hook implementation function such that the "
"plugin machinery will try to call it last/as late as possible.")
+ def _warn(self, message):
+ kwargs = message if isinstance(message, dict) else {
+ 'code': 'I1',
+ 'message': message,
+ 'fslocation': None,
+ 'nodeid': None,
+ }
+ self.hook.pytest_logwarning.call_historic(kwargs=kwargs)
+
+ #
+ # internal API for local conftest plugin handling
+ #
+ def _set_initial_conftests(self, namespace):
+ """ load initial conftest files given a preparsed "namespace".
+ As conftest files may add their own command line options
+ which have arguments ('--my-opt somepath') we might get some
+ false positives. All builtin and 3rd party plugins will have
+ been loaded, however, so common options will not confuse our logic
+ here.
+ """
+ current = py.path.local()
+ self._confcutdir = current.join(namespace.confcutdir, abs=True) \
+ if namespace.confcutdir else None
+ self._noconftest = namespace.noconftest
+ testpaths = namespace.file_or_dir
+ foundanchor = False
+ for path in testpaths:
+ path = str(path)
+ # remove node-id syntax
+ i = path.find("::")
+ if i != -1:
+ path = path[:i]
+ anchor = current.join(path, abs=1)
+ if exists(anchor): # we found some file object
+ self._try_load_conftest(anchor)
+ foundanchor = True
+ if not foundanchor:
+ self._try_load_conftest(current)
+
+ def _try_load_conftest(self, anchor):
+ self._getconftestmodules(anchor)
+ # let's also consider test* subdirs
+ if anchor.check(dir=1):
+ for x in anchor.listdir("test*"):
+ if x.check(dir=1):
+ self._getconftestmodules(x)
+
+ def _getconftestmodules(self, path):
+ if self._noconftest:
+ return []
+ try:
+ return self._path2confmods[path]
+ except KeyError:
+ if path.isfile():
+ clist = self._getconftestmodules(path.dirpath())
+ else:
+ # XXX these days we may rather want to use config.rootdir
+ # and allow users to opt into looking into the rootdir parent
+ # directories instead of requiring to specify confcutdir
+ clist = []
+ for parent in path.parts():
+ if self._confcutdir and self._confcutdir.relto(parent):
+ continue
+ conftestpath = parent.join("conftest.py")
+ if conftestpath.isfile():
+ mod = self._importconftest(conftestpath)
+ clist.append(mod)
+
+ self._path2confmods[path] = clist
+ return clist
+
+ def _rget_with_confmod(self, name, path):
+ modules = self._getconftestmodules(path)
+ for mod in reversed(modules):
+ try:
+ return mod, getattr(mod, name)
+ except AttributeError:
+ continue
+ raise KeyError(name)
+
+ def _importconftest(self, conftestpath):
+ try:
+ return self._conftestpath2mod[conftestpath]
+ except KeyError:
+ pkgpath = conftestpath.pypkgpath()
+ if pkgpath is None:
+ _ensure_removed_sysmodule(conftestpath.purebasename)
+ try:
+ mod = conftestpath.pyimport()
+ except Exception:
+ raise ConftestImportFailure(conftestpath, sys.exc_info())
+
+ self._conftest_plugins.add(mod)
+ self._conftestpath2mod[conftestpath] = mod
+ dirpath = conftestpath.dirpath()
+ if dirpath in self._path2confmods:
+ for path, mods in self._path2confmods.items():
+ if path and path.relto(dirpath) or path == dirpath:
+ assert mod not in mods
+ mods.append(mod)
+ self.trace("loaded conftestmodule %r" %(mod))
+ self.consider_conftest(mod)
+ return mod
+
+ #
+ # API for bootstrapping plugin loading
+ #
+ #
+
+ def consider_preparse(self, args):
+ for opt1,opt2 in zip(args, args[1:]):
+ if opt1 == "-p":
+ self.consider_pluginarg(opt2)
+
+ def consider_pluginarg(self, arg):
+ if arg.startswith("no:"):
+ name = arg[3:]
+ self.set_blocked(name)
+ if not name.startswith("pytest_"):
+ self.set_blocked("pytest_" + name)
+ else:
+ self.import_plugin(arg)
+
+ def consider_conftest(self, conftestmodule):
+ if self.register(conftestmodule, name=conftestmodule.__file__):
+ self.consider_module(conftestmodule)
+
+ def consider_env(self):
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
+
+ def consider_module(self, mod):
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", None))
+
+ def _import_plugin_specs(self, spec):
+ if spec:
+ if isinstance(spec, str):
+ spec = spec.split(",")
+ for import_spec in spec:
+ self.import_plugin(import_spec)
+
+ def import_plugin(self, modname):
+ # most often modname refers to builtin modules, e.g. "pytester",
+ # "terminal" or "capture". Those plugins are registered under their
+ # basename for historic purposes but must be imported with the
+ # _pytest prefix.
+ assert isinstance(modname, str)
+ if self.get_plugin(modname) is not None:
+ return
+ if modname in builtin_plugins:
+ importspec = "_pytest." + modname
+ else:
+ importspec = modname
+ try:
+ __import__(importspec)
+ except ImportError as e:
+ new_exc = ImportError('Error importing plugin "%s": %s' % (modname, e))
+ # copy over name and path attributes
+ for attr in ('name', 'path'):
+ if hasattr(e, attr):
+ setattr(new_exc, attr, getattr(e, attr))
+ raise new_exc
+ except Exception as e:
+ import pytest
+ if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
+ raise
+ self._warn("skipped plugin %r: %s" %((modname, e.msg)))
+ else:
+ mod = sys.modules[importspec]
+ self.register(mod, modname)
+ self.consider_module(mod)
+
class Parser:
- """ Parser for command line arguments and ini-file values. """
+ """ Parser for command line arguments and ini-file values.
+
+ :ivar extra_info: dict of generic param -> value to display in case
+ there's an error processing the command line arguments.
+ """
def __init__(self, usage=None, processopt=None):
self._anonymous = OptionGroup("custom options", parser=self)
@@ -94,7 +415,7 @@ class Parser:
self._usage = usage
self._inidict = {}
self._ininames = []
- self.hints = []
+ self.extra_info = {}
def processoption(self, option):
if self._processopt:
@@ -140,15 +461,15 @@ class Parser:
"""
self._anonymous.addoption(*opts, **attrs)
- def parse(self, args):
+ def parse(self, args, namespace=None):
from _pytest._argcomplete import try_argcomplete
self.optparser = self._getparser()
try_argcomplete(self.optparser)
- return self.optparser.parse_args([str(x) for x in args])
+ return self.optparser.parse_args([str(x) for x in args], namespace=namespace)
def _getparser(self):
from _pytest._argcomplete import filescompleter
- optparser = MyOptionParser(self)
+ optparser = MyOptionParser(self, self.extra_info)
groups = self._groups + [self._anonymous]
for group in groups:
if group.options:
@@ -159,32 +480,41 @@ class Parser:
a = option.attrs()
arggroup.add_argument(*n, **a)
# bash like autocompletion for dirs (appending '/')
- optparser.add_argument(FILE_OR_DIR, nargs='*'
- ).completer=filescompleter
+ optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter
return optparser
- def parse_setoption(self, args, option):
- parsedoption = self.parse(args)
+ def parse_setoption(self, args, option, namespace=None):
+ parsedoption = self.parse(args, namespace=namespace)
for name, value in parsedoption.__dict__.items():
setattr(option, name, value)
return getattr(parsedoption, FILE_OR_DIR)
- def parse_known_args(self, args):
+ def parse_known_args(self, args, namespace=None):
+ """parses and returns a namespace object with known arguments at this
+ point.
+ """
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
+
+ def parse_known_and_unknown_args(self, args, namespace=None):
+ """parses and returns a namespace object with known arguments, and
+ the remaining arguments unknown at this point.
+ """
optparser = self._getparser()
args = [str(x) for x in args]
- return optparser.parse_known_args(args)[0]
+ return optparser.parse_known_args(args, namespace=namespace)
def addini(self, name, help, type=None, default=None):
""" register an ini-file option.
:name: name of the ini-variable
- :type: type of the variable, can be ``pathlist``, ``args`` or ``linelist``.
+ :type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
+ or ``bool``.
:default: default value if no ini-file option exists but is queried.
The value of ini-variables can be retrieved via a call to
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
"""
- assert type in (None, "pathlist", "args", "linelist")
+ assert type in (None, "pathlist", "args", "linelist", "bool")
self._inidict[name] = (help, type, default)
self._ininames.append(name)
@@ -207,7 +537,7 @@ class ArgumentError(Exception):
class Argument:
- """class that mimics the necessary behaviour of py.std.optparse.Option """
+ """class that mimics the necessary behaviour of optparse.Option """
_typ_map = {
'int': int,
'string': str,
@@ -225,7 +555,7 @@ class Argument:
try:
help = attrs['help']
if '%default' in help:
- py.std.warnings.warn(
+ warnings.warn(
'pytest now uses argparse. "%default" should be'
' changed to "%(default)s" ',
FutureWarning,
@@ -241,7 +571,7 @@ class Argument:
if isinstance(typ, py.builtin._basestring):
if typ == 'choice':
if self.TYPE_WARN:
- py.std.warnings.warn(
+ warnings.warn(
'type argument to addoption() is a string %r.'
' For parsearg this is optional and when supplied '
' should be a type.'
@@ -253,7 +583,7 @@ class Argument:
attrs['type'] = type(attrs['choices'][0])
else:
if self.TYPE_WARN:
- py.std.warnings.warn(
+ warnings.warn(
'type argument to addoption() is a string %r.'
' For parsearg this should be a type.'
' (options: %s)' % (typ, names),
@@ -373,19 +703,16 @@ class OptionGroup:
self.options.append(option)
-class MyOptionParser(py.std.argparse.ArgumentParser):
- def __init__(self, parser):
+class MyOptionParser(argparse.ArgumentParser):
+ def __init__(self, parser, extra_info=None):
+ if not extra_info:
+ extra_info = {}
self._parser = parser
- py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
+ argparse.ArgumentParser.__init__(self, usage=parser._usage,
add_help=False, formatter_class=DropShorterLongHelpFormatter)
-
- def format_epilog(self, formatter):
- hints = self._parser.hints
- if hints:
- s = "\n".join(["hint: " + x for x in hints]) + "\n"
- s = "\n" + s + "\n"
- return s
- return ""
+ # extra_info is a dict of (param -> value) to display if there's
+ # an usage error to provide more contextual information to the user
+ self.extra_info = extra_info
def parse_args(self, args=None, namespace=None):
"""allow splitting of positional arguments"""
@@ -393,12 +720,15 @@ class MyOptionParser(py.std.argparse.ArgumentParser):
if argv:
for arg in argv:
if arg and arg[0] == '-':
- msg = py.std.argparse._('unrecognized arguments: %s')
- self.error(msg % ' '.join(argv))
+ lines = ['unrecognized arguments: %s' % (' '.join(argv))]
+ for k, v in sorted(self.extra_info.items()):
+ lines.append(' %s: %s' % (k, v))
+ self.error('\n'.join(lines))
getattr(args, FILE_OR_DIR).extend(argv)
return args
-class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
+
+class DropShorterLongHelpFormatter(argparse.HelpFormatter):
"""shorten help for long options that differ only in extra hyphens
- collapse **long** options that are the same except for extra hyphens
@@ -408,7 +738,7 @@ class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
- cache result on action object as this is called at least 2 times
"""
def _format_action_invocation(self, action):
- orgstr = py.std.argparse.HelpFormatter._format_action_invocation(self, action)
+ orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
if orgstr and orgstr[0] != '-': # only optional arguments
return orgstr
res = getattr(action, '_formatted_action_invocation', None)
@@ -442,113 +772,11 @@ class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
if len(option) == 2 or option[2] == ' ':
return_list.append(option)
if option[2:] == short_long.get(option.replace('-', '')):
- return_list.append(option)
+ return_list.append(option.replace(' ', '='))
action._formatted_action_invocation = ', '.join(return_list)
return action._formatted_action_invocation
-class Conftest(object):
- """ the single place for accessing values and interacting
- towards conftest modules from pytest objects.
- """
- def __init__(self, onimport=None, confcutdir=None):
- self._path2confmods = {}
- self._onimport = onimport
- self._conftestpath2mod = {}
- self._confcutdir = confcutdir
-
- def setinitial(self, args):
- """ try to find a first anchor path for looking up global values
- from conftests. This function is usually called _before_
- argument parsing. conftest files may add command line options
- and we thus have no completely safe way of determining
- which parts of the arguments are actually related to options
- and which are file system paths. We just try here to get
- bootstrapped ...
- """
- current = py.path.local()
- opt = '--confcutdir'
- for i in range(len(args)):
- opt1 = str(args[i])
- if opt1.startswith(opt):
- if opt1 == opt:
- if len(args) > i:
- p = current.join(args[i+1], abs=True)
- elif opt1.startswith(opt + "="):
- p = current.join(opt1[len(opt)+1:], abs=1)
- self._confcutdir = p
- break
- foundanchor = False
- for arg in args:
- if hasattr(arg, 'startswith') and arg.startswith("--"):
- continue
- anchor = current.join(arg, abs=1)
- if exists(anchor): # we found some file object
- self._try_load_conftest(anchor)
- foundanchor = True
- if not foundanchor:
- self._try_load_conftest(current)
-
- def _try_load_conftest(self, anchor):
- self._path2confmods[None] = self.getconftestmodules(anchor)
- # let's also consider test* subdirs
- if anchor.check(dir=1):
- for x in anchor.listdir("test*"):
- if x.check(dir=1):
- self.getconftestmodules(x)
-
- def getconftestmodules(self, path):
- try:
- clist = self._path2confmods[path]
- except KeyError:
- if path is None:
- raise ValueError("missing default conftest.")
- clist = []
- for parent in path.parts():
- if self._confcutdir and self._confcutdir.relto(parent):
- continue
- conftestpath = parent.join("conftest.py")
- if conftestpath.check(file=1):
- clist.append(self.importconftest(conftestpath))
- self._path2confmods[path] = clist
- return clist
-
- def rget(self, name, path=None):
- mod, value = self.rget_with_confmod(name, path)
- return value
-
- def rget_with_confmod(self, name, path=None):
- modules = self.getconftestmodules(path)
- modules.reverse()
- for mod in modules:
- try:
- return mod, getattr(mod, name)
- except AttributeError:
- continue
- raise KeyError(name)
-
- def importconftest(self, conftestpath):
- assert conftestpath.check(), conftestpath
- try:
- return self._conftestpath2mod[conftestpath]
- except KeyError:
- pkgpath = conftestpath.pypkgpath()
- if pkgpath is None:
- _ensure_removed_sysmodule(conftestpath.purebasename)
- self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport()
- dirpath = conftestpath.dirpath()
- if dirpath in self._path2confmods:
- for path, mods in self._path2confmods.items():
- if path and path.relto(dirpath) or path == dirpath:
- assert mod not in mods
- mods.append(mod)
- self._postimport(mod)
- return mod
-
- def _postimport(self, mod):
- if self._onimport:
- self._onimport(mod)
- return mod
def _ensure_removed_sysmodule(modname):
try:
@@ -558,12 +786,20 @@ def _ensure_removed_sysmodule(modname):
class CmdOptions(object):
""" holds cmdline options as attributes."""
- def __init__(self, **kwargs):
- self.__dict__.update(kwargs)
+ def __init__(self, values=()):
+ self.__dict__.update(values)
def __repr__(self):
return "<CmdOptions %r>" %(self.__dict__,)
+ def copy(self):
+ return CmdOptions(self.__dict__)
+
+class Notset:
+ def __repr__(self):
+ return "<NOTSET>"
+notset = Notset()
FILE_OR_DIR = 'file_or_dir'
+
class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """
@@ -579,50 +815,52 @@ class Config(object):
#: a pluginmanager instance
self.pluginmanager = pluginmanager
self.trace = self.pluginmanager.trace.root.get("config")
- self._conftest = Conftest(onimport=self._onimportconftest)
self.hook = self.pluginmanager.hook
self._inicache = {}
self._opt2dest = {}
self._cleanup = []
+ self._warn = self.pluginmanager._warn
self.pluginmanager.register(self, "pytestconfig")
- self.pluginmanager.set_register_callback(self._register_plugin)
self._configured = False
-
- def _register_plugin(self, plugin, name):
- call_plugin = self.pluginmanager.call_plugin
- call_plugin(plugin, "pytest_addhooks",
- {'pluginmanager': self.pluginmanager})
- self.hook.pytest_plugin_registered(plugin=plugin,
- manager=self.pluginmanager)
- dic = call_plugin(plugin, "pytest_namespace", {}) or {}
- if dic:
+ def do_setns(dic):
import pytest
setns(pytest, dic)
- call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
- if self._configured:
- call_plugin(plugin, "pytest_configure", {'config': self})
+ self.hook.pytest_namespace.call_historic(do_setns, {})
+ self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
- def do_configure(self):
+ def add_cleanup(self, func):
+ """ Add a function to be called when the config object gets out of
+ use (usually coninciding with pytest_unconfigure)."""
+ self._cleanup.append(func)
+
+ def _do_configure(self):
assert not self._configured
self._configured = True
- self.hook.pytest_configure(config=self)
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
- def do_unconfigure(self):
- assert self._configured
- self._configured = False
- self.hook.pytest_unconfigure(config=self)
- self.pluginmanager.ensure_shutdown()
+ def _ensure_unconfigure(self):
+ if self._configured:
+ self._configured = False
+ self.hook.pytest_unconfigure(config=self)
+ self.hook.pytest_configure._call_history = []
+ while self._cleanup:
+ fin = self._cleanup.pop()
+ fin()
+
+ def warn(self, code, message, fslocation=None):
+ """ generate a warning for this test session. """
+ self.hook.pytest_logwarning.call_historic(kwargs=dict(
+ code=code, message=message,
+ fslocation=fslocation, nodeid=None))
+
+ def get_terminal_writer(self):
+ return self.pluginmanager.get_plugin("terminalreporter")._tw
def pytest_cmdline_parse(self, pluginmanager, args):
- assert self == pluginmanager.config, (self, pluginmanager.config)
+ # REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
self.parse(args)
return self
- def pytest_unconfigure(config):
- while config._cleanup:
- fin = config._cleanup.pop()
- fin()
-
def notify_exception(self, excinfo, option=None):
if option and option.fulltrace:
style = "long"
@@ -639,22 +877,23 @@ class Config(object):
sys.stderr.write("INTERNALERROR> %s\n" %line)
sys.stderr.flush()
+ def cwd_relative_nodeid(self, nodeid):
+ # nodeid's are relative to the rootpath, compute relative to cwd
+ if self.invocation_dir != self.rootdir:
+ fullpath = self.rootdir.join(nodeid)
+ nodeid = self.invocation_dir.bestrelpath(fullpath)
+ return nodeid
@classmethod
def fromdictargs(cls, option_dict, args):
""" constructor useable for subprocesses. """
- pluginmanager = get_plugin_manager()
- config = pluginmanager.config
- config._preparse(args, addopts=False)
+ config = get_config()
config.option.__dict__.update(option_dict)
+ config.parse(args, addopts=False)
for x in config.option.plugins:
config.pluginmanager.consider_pluginarg(x)
return config
- def _onimportconftest(self, conftestmodule):
- self.trace("loaded conftestmodule %r" %(conftestmodule,))
- self.pluginmanager.consider_conftest(conftestmodule)
-
def _processopt(self, opt):
for name in opt._short_opts + opt._long_opts:
self._opt2dest[name] = opt.dest
@@ -663,32 +902,47 @@ class Config(object):
if not hasattr(self.option, opt.dest):
setattr(self.option, opt.dest, opt.default)
- def _getmatchingplugins(self, fspath):
- allconftests = self._conftest._conftestpath2mod.values()
- plugins = [x for x in self.pluginmanager.getplugins()
- if x not in allconftests]
- plugins += self._conftest.getconftestmodules(fspath)
- return plugins
-
- def pytest_load_initial_conftests(self, parser, args):
- self._conftest.setinitial(args)
- pytest_load_initial_conftests.trylast = True
+ @hookimpl(trylast=True)
+ def pytest_load_initial_conftests(self, early_config):
+ self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
def _initini(self, args):
- self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"])
+ ns, unknown_args = self._parser.parse_known_and_unknown_args(args, namespace=self.option.copy())
+ r = determine_setup(ns.inifilename, ns.file_or_dir + unknown_args)
+ self.rootdir, self.inifile, self.inicfg = r
+ self._parser.extra_info['rootdir'] = self.rootdir
+ self._parser.extra_info['inifile'] = self.inifile
+ self.invocation_dir = py.path.local()
self._parser.addini('addopts', 'extra command line options', 'args')
self._parser.addini('minversion', 'minimally required pytest version')
def _preparse(self, args, addopts=True):
self._initini(args)
if addopts:
+ args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
args[:] = self.getini("addopts") + args
self._checkversion()
self.pluginmanager.consider_preparse(args)
- self.pluginmanager.consider_setuptools_entrypoints()
+ try:
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
+ except ImportError as e:
+ self.warn("I2", "could not load setuptools entry import: %s" % (e,))
self.pluginmanager.consider_env()
- self.hook.pytest_load_initial_conftests(early_config=self,
- args=args, parser=self._parser)
+ self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy())
+ if self.known_args_namespace.confcutdir is None and self.inifile:
+ confcutdir = py.path.local(self.inifile).dirname
+ self.known_args_namespace.confcutdir = confcutdir
+ try:
+ self.hook.pytest_load_initial_conftests(early_config=self,
+ args=args, parser=self._parser)
+ except ConftestImportFailure:
+ e = sys.exc_info()[1]
+ if ns.help or ns.version:
+ # we don't want to prevent --help/--version to work
+ # so just let is pass and print a warning at the end
+ self._warn("could not load initial conftests (%s)\n" % e.path)
+ else:
+ raise
def _checkversion(self):
import pytest
@@ -702,19 +956,23 @@ class Config(object):
self.inicfg.config.path, self.inicfg.lineof('minversion'),
minver, pytest.__version__))
- def parse(self, args):
+ def parse(self, args, addopts=True):
# parse given cmdline arguments into this config object.
- # Note that this can only be called once per testing process.
assert not hasattr(self, 'args'), (
"can only parse cmdline args at most once per Config object")
self._origargs = args
- self._preparse(args)
+ self.hook.pytest_addhooks.call_historic(
+ kwargs=dict(pluginmanager=self.pluginmanager))
+ self._preparse(args, addopts=addopts)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)
- self._parser.hints.extend(self.pluginmanager._hints)
- args = self._parser.parse_setoption(args, self.option)
+ args = self._parser.parse_setoption(args, self.option, namespace=self.option)
if not args:
- args.append(py.std.os.getcwd())
+ cwd = os.getcwd()
+ if cwd == self.rootdir:
+ args = self.getini('testpaths')
+ if not args:
+ args = [cwd]
self.args = args
def addinivalue_line(self, name, line):
@@ -752,20 +1010,22 @@ class Config(object):
if type == "pathlist":
dp = py.path.local(self.inicfg.config.path).dirpath()
l = []
- for relpath in py.std.shlex.split(value):
+ for relpath in shlex.split(value):
l.append(dp.join(relpath, abs=True))
return l
elif type == "args":
- return py.std.shlex.split(value)
+ return shlex.split(value)
elif type == "linelist":
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
+ elif type == "bool":
+ return bool(_strtobool(value.strip()))
else:
assert type is None
return value
- def _getconftest_pathlist(self, name, path=None):
+ def _getconftest_pathlist(self, name, path):
try:
- mod, relroots = self._conftest.rget_with_confmod(name, path)
+ mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
except KeyError:
return None
modpath = py.path.local(mod.__file__).dirpath()
@@ -777,47 +1037,36 @@ class Config(object):
l.append(relroot)
return l
- def _getconftest(self, name, path=None, check=False):
- if check:
- self._checkconftest(name)
- return self._conftest.rget(name, path)
-
- def getoption(self, name):
+ def getoption(self, name, default=notset, skip=False):
""" return command line option value.
:arg name: name of the option. You may also specify
the literal ``--OPT`` option instead of the "dest" option name.
+ :arg default: default value if no option of that name exists.
+ :arg skip: if True raise pytest.skip if option does not exists
+ or has a None value.
"""
name = self._opt2dest.get(name, name)
try:
- return getattr(self.option, name)
+ val = getattr(self.option, name)
+ if val is None and skip:
+ raise AttributeError(name)
+ return val
except AttributeError:
+ if default is not notset:
+ return default
+ if skip:
+ import pytest
+ pytest.skip("no %r option found" %(name,))
raise ValueError("no option named %r" % (name,))
def getvalue(self, name, path=None):
- """ return command line option value.
-
- :arg name: name of the command line option
-
- (deprecated) if we can't find the option also lookup
- the name in a matching conftest file.
- """
- try:
- return getattr(self.option, name)
- except AttributeError:
- return self._getconftest(name, path, check=False)
+ """ (deprecated, use getoption()) """
+ return self.getoption(name)
def getvalueorskip(self, name, path=None):
- """ (deprecated) return getvalue(name) or call
- pytest.skip if no value exists. """
- __tracebackhide__ = True
- try:
- val = self.getvalue(name, path)
- if val is None:
- raise KeyError(name)
- return val
- except KeyError:
- py.test.skip("no %r value found" %(name,))
+ """ (deprecated, use getoption(skip=True)) """
+ return self.getoption(name, skip=True)
def exists(path, ignore=EnvironmentError):
try:
@@ -837,8 +1086,58 @@ def getcfg(args, inibasenames):
if exists(p):
iniconfig = py.iniconfig.IniConfig(p)
if 'pytest' in iniconfig.sections:
- return iniconfig['pytest']
- return {}
+ return base, p, iniconfig['pytest']
+ elif inibasename == "pytest.ini":
+ # allowed to be empty
+ return base, p, {}
+ return None, None, None
+
+
+def get_common_ancestor(args):
+ # args are what we get after early command line parsing (usually
+ # strings, but can be py.path.local objects as well)
+ common_ancestor = None
+ for arg in args:
+ if str(arg)[0] == "-":
+ continue
+ p = py.path.local(arg)
+ if common_ancestor is None:
+ common_ancestor = p
+ else:
+ if p.relto(common_ancestor) or p == common_ancestor:
+ continue
+ elif common_ancestor.relto(p):
+ common_ancestor = p
+ else:
+ shared = p.common(common_ancestor)
+ if shared is not None:
+ common_ancestor = shared
+ if common_ancestor is None:
+ common_ancestor = py.path.local()
+ elif not common_ancestor.isdir():
+ common_ancestor = common_ancestor.dirpath()
+ return common_ancestor
+
+
+def determine_setup(inifile, args):
+ if inifile:
+ iniconfig = py.iniconfig.IniConfig(inifile)
+ try:
+ inicfg = iniconfig["pytest"]
+ except KeyError:
+ inicfg = None
+ rootdir = get_common_ancestor(args)
+ else:
+ ancestor = get_common_ancestor(args)
+ rootdir, inifile, inicfg = getcfg(
+ [ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
+ if rootdir is None:
+ for rootdir in ancestor.parts(reverse=True):
+ if rootdir.join("setup.py").exists():
+ break
+ else:
+ rootdir = ancestor
+ return rootdir, inifile, inicfg or {}
def setns(obj, dic):
@@ -848,7 +1147,7 @@ def setns(obj, dic):
mod = getattr(obj, name, None)
if mod is None:
modname = "pytest.%s" % name
- mod = py.std.types.ModuleType(modname)
+ mod = types.ModuleType(modname)
sys.modules[modname] = mod
mod.__all__ = []
setattr(obj, name, mod)
@@ -860,3 +1159,34 @@ def setns(obj, dic):
#if obj != pytest:
# pytest.__all__.append(name)
setattr(pytest, name, value)
+
+
+def create_terminal_writer(config, *args, **kwargs):
+ """Create a TerminalWriter instance configured according to the options
+ in the config object. Every code which requires a TerminalWriter object
+ and has access to a config object should use this function.
+ """
+ tw = py.io.TerminalWriter(*args, **kwargs)
+ if config.option.color == 'yes':
+ tw.hasmarkup = True
+ if config.option.color == 'no':
+ tw.hasmarkup = False
+ return tw
+
+
+def _strtobool(val):
+ """Convert a string representation of truth to true (1) or false (0).
+
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
+ 'val' is anything else.
+
+ .. note:: copied from distutils.util
+ """
+ val = val.lower()
+ if val in ('y', 'yes', 't', 'true', 'on', '1'):
+ return 1
+ elif val in ('n', 'no', 'f', 'false', 'off', '0'):
+ return 0
+ else:
+ raise ValueError("invalid truth value %r" % (val,))
diff --git a/_pytest/core.py b/_pytest/core.py
deleted file mode 100644
index d029e7223e..0000000000
--- a/_pytest/core.py
+++ /dev/null
@@ -1,394 +0,0 @@
-"""
-pytest PluginManager, basic initialization and tracing.
-"""
-import sys
-import inspect
-import py
-# don't import pytest to avoid circular imports
-
-assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: "
- "%s is too old, remove or upgrade 'py'" % (py.__version__))
-
-class TagTracer:
- def __init__(self):
- self._tag2proc = {}
- self.writer = None
- self.indent = 0
-
- def get(self, name):
- return TagTracerSub(self, (name,))
-
- def format_message(self, tags, args):
- if isinstance(args[-1], dict):
- extra = args[-1]
- args = args[:-1]
- else:
- extra = {}
-
- content = " ".join(map(str, args))
- indent = " " * self.indent
-
- lines = [
- "%s%s [%s]\n" %(indent, content, ":".join(tags))
- ]
-
- for name, value in extra.items():
- lines.append("%s %s: %s\n" % (indent, name, value))
- return lines
-
- def processmessage(self, tags, args):
- if self.writer is not None and args:
- lines = self.format_message(tags, args)
- self.writer(''.join(lines))
- try:
- self._tag2proc[tags](tags, args)
- except KeyError:
- pass
-
- def setwriter(self, writer):
- self.writer = writer
-
- def setprocessor(self, tags, processor):
- if isinstance(tags, str):
- tags = tuple(tags.split(":"))
- else:
- assert isinstance(tags, tuple)
- self._tag2proc[tags] = processor
-
-class TagTracerSub:
- def __init__(self, root, tags):
- self.root = root
- self.tags = tags
- def __call__(self, *args):
- self.root.processmessage(self.tags, args)
- def setmyprocessor(self, processor):
- self.root.setprocessor(self.tags, processor)
- def get(self, name):
- return self.__class__(self.root, self.tags + (name,))
-
-class PluginManager(object):
- def __init__(self, hookspecs=None):
- self._name2plugin = {}
- self._listattrcache = {}
- self._plugins = []
- self._hints = []
- self.trace = TagTracer().get("pluginmanage")
- self._plugin_distinfo = []
- self._shutdown = []
- self.hook = HookRelay(hookspecs or [], pm=self)
-
- def do_configure(self, config):
- # backward compatibility
- config.do_configure()
-
- def set_register_callback(self, callback):
- assert not hasattr(self, "_registercallback")
- self._registercallback = callback
-
- def register(self, plugin, name=None, prepend=False):
- if self._name2plugin.get(name, None) == -1:
- return
- name = name or getattr(plugin, '__name__', str(id(plugin)))
- if self.isregistered(plugin, name):
- raise ValueError("Plugin already registered: %s=%s\n%s" %(
- name, plugin, self._name2plugin))
- #self.trace("registering", name, plugin)
- self._name2plugin[name] = plugin
- reg = getattr(self, "_registercallback", None)
- if reg is not None:
- reg(plugin, name)
- if not prepend:
- self._plugins.append(plugin)
- else:
- self._plugins.insert(0, plugin)
- return True
-
- def unregister(self, plugin=None, name=None):
- if plugin is None:
- plugin = self.getplugin(name=name)
- self._plugins.remove(plugin)
- for name, value in list(self._name2plugin.items()):
- if value == plugin:
- del self._name2plugin[name]
-
- def add_shutdown(self, func):
- self._shutdown.append(func)
-
- def ensure_shutdown(self):
- while self._shutdown:
- func = self._shutdown.pop()
- func()
- self._plugins = []
- self._name2plugin.clear()
- self._listattrcache.clear()
-
- def isregistered(self, plugin, name=None):
- if self.getplugin(name) is not None:
- return True
- for val in self._name2plugin.values():
- if plugin == val:
- return True
-
- def addhooks(self, spec, prefix="pytest_"):
- self.hook._addhooks(spec, prefix=prefix)
-
- def getplugins(self):
- return list(self._plugins)
-
- def skipifmissing(self, name):
- if not self.hasplugin(name):
- py.test.skip("plugin %r is missing" % name)
-
- def hasplugin(self, name):
- return bool(self.getplugin(name))
-
- def getplugin(self, name):
- if name is None:
- return None
- try:
- return self._name2plugin[name]
- except KeyError:
- return self._name2plugin.get("_pytest." + name, None)
-
- # API for bootstrapping
- #
- def _envlist(self, varname):
- val = py.std.os.environ.get(varname, None)
- if val is not None:
- return val.split(',')
- return ()
-
- def consider_env(self):
- for spec in self._envlist("PYTEST_PLUGINS"):
- self.import_plugin(spec)
-
- def consider_setuptools_entrypoints(self):
- try:
- from pkg_resources import iter_entry_points, DistributionNotFound
- except ImportError:
- return # XXX issue a warning
- for ep in iter_entry_points('pytest11'):
- name = ep.name
- if name.startswith("pytest_"):
- name = name[7:]
- if ep.name in self._name2plugin or name in self._name2plugin:
- continue
- try:
- plugin = ep.load()
- except DistributionNotFound:
- continue
- self._plugin_distinfo.append((ep.dist, plugin))
- self.register(plugin, name=name)
-
- def consider_preparse(self, args):
- for opt1,opt2 in zip(args, args[1:]):
- if opt1 == "-p":
- self.consider_pluginarg(opt2)
-
- def consider_pluginarg(self, arg):
- if arg.startswith("no:"):
- name = arg[3:]
- if self.getplugin(name) is not None:
- self.unregister(None, name=name)
- self._name2plugin[name] = -1
- else:
- if self.getplugin(arg) is None:
- self.import_plugin(arg)
-
- def consider_conftest(self, conftestmodule):
- if self.register(conftestmodule, name=conftestmodule.__file__):
- self.consider_module(conftestmodule)
-
- def consider_module(self, mod):
- attr = getattr(mod, "pytest_plugins", ())
- if attr:
- if not isinstance(attr, (list, tuple)):
- attr = (attr,)
- for spec in attr:
- self.import_plugin(spec)
-
- def import_plugin(self, modname):
- assert isinstance(modname, str)
- if self.getplugin(modname) is not None:
- return
- try:
- mod = importplugin(modname)
- except KeyboardInterrupt:
- raise
- except ImportError:
- if modname.startswith("pytest_"):
- return self.import_plugin(modname[7:])
- raise
- except:
- e = py.std.sys.exc_info()[1]
- if not hasattr(py.test, 'skip'):
- raise
- elif not isinstance(e, py.test.skip.Exception):
- raise
- self._hints.append("skipped plugin %r: %s" %((modname, e.msg)))
- else:
- self.register(mod, modname)
- self.consider_module(mod)
-
- def listattr(self, attrname, plugins=None):
- if plugins is None:
- plugins = self._plugins
- key = (attrname,) + tuple(plugins)
- try:
- return list(self._listattrcache[key])
- except KeyError:
- pass
- l = []
- last = []
- for plugin in plugins:
- try:
- meth = getattr(plugin, attrname)
- if hasattr(meth, 'tryfirst'):
- last.append(meth)
- elif hasattr(meth, 'trylast'):
- l.insert(0, meth)
- else:
- l.append(meth)
- except AttributeError:
- continue
- l.extend(last)
- self._listattrcache[key] = list(l)
- return l
-
- def call_plugin(self, plugin, methname, kwargs):
- return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
- kwargs=kwargs, firstresult=True).execute()
-
-
-def importplugin(importspec):
- name = importspec
- try:
- mod = "_pytest." + name
- __import__(mod)
- return sys.modules[mod]
- except ImportError:
- __import__(importspec)
- return sys.modules[importspec]
-
-class MultiCall:
- """ execute a call into multiple python functions/methods. """
- def __init__(self, methods, kwargs, firstresult=False):
- self.methods = list(methods)
- self.kwargs = kwargs
- self.results = []
- self.firstresult = firstresult
-
- def __repr__(self):
- status = "%d results, %d meths" % (len(self.results), len(self.methods))
- return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
-
- def execute(self):
- while self.methods:
- method = self.methods.pop()
- kwargs = self.getkwargs(method)
- res = method(**kwargs)
- if res is not None:
- self.results.append(res)
- if self.firstresult:
- return res
- if not self.firstresult:
- return self.results
-
- def getkwargs(self, method):
- kwargs = {}
- for argname in varnames(method):
- try:
- kwargs[argname] = self.kwargs[argname]
- except KeyError:
- if argname == "__multicall__":
- kwargs[argname] = self
- return kwargs
-
-def varnames(func):
- """ return argument name tuple for a function, method, class or callable.
-
- In case of a class, its "__init__" method is considered.
- For methods the "self" parameter is not included unless you are passing
- an unbound method with Python3 (which has no supports for unbound methods)
- """
- cache = getattr(func, "__dict__", {})
- try:
- return cache["_varnames"]
- except KeyError:
- pass
- if inspect.isclass(func):
- try:
- func = func.__init__
- except AttributeError:
- return ()
- ismethod = True
- else:
- if not inspect.isfunction(func) and not inspect.ismethod(func):
- func = getattr(func, '__call__', func)
- ismethod = inspect.ismethod(func)
- rawcode = py.code.getrawcode(func)
- try:
- x = rawcode.co_varnames[ismethod:rawcode.co_argcount]
- except AttributeError:
- x = ()
- try:
- cache["_varnames"] = x
- except TypeError:
- pass
- return x
-
-class HookRelay:
- def __init__(self, hookspecs, pm, prefix="pytest_"):
- if not isinstance(hookspecs, list):
- hookspecs = [hookspecs]
- self._hookspecs = []
- self._pm = pm
- self.trace = pm.trace.root.get("hook")
- for hookspec in hookspecs:
- self._addhooks(hookspec, prefix)
-
- def _addhooks(self, hookspecs, prefix):
- self._hookspecs.append(hookspecs)
- added = False
- for name, method in vars(hookspecs).items():
- if name.startswith(prefix):
- firstresult = getattr(method, 'firstresult', False)
- hc = HookCaller(self, name, firstresult=firstresult)
- setattr(self, name, hc)
- added = True
- #print ("setting new hook", name)
- if not added:
- raise ValueError("did not find new %r hooks in %r" %(
- prefix, hookspecs,))
-
-
-class HookCaller:
- def __init__(self, hookrelay, name, firstresult):
- self.hookrelay = hookrelay
- self.name = name
- self.firstresult = firstresult
- self.trace = self.hookrelay.trace
-
- def __repr__(self):
- return "<HookCaller %r>" %(self.name,)
-
- def __call__(self, **kwargs):
- methods = self.hookrelay._pm.listattr(self.name)
- return self._docall(methods, kwargs)
-
- def pcall(self, plugins, **kwargs):
- methods = self.hookrelay._pm.listattr(self.name, plugins=plugins)
- return self._docall(methods, kwargs)
-
- def _docall(self, methods, kwargs):
- self.trace(self.name, kwargs)
- self.trace.root.indent += 1
- mc = MultiCall(methods, kwargs, firstresult=self.firstresult)
- try:
- res = mc.execute()
- if res:
- self.trace("finish", self.name, "-->", res)
- finally:
- self.trace.root.indent -= 1
- return res
-
diff --git a/_pytest/doctest.py b/_pytest/doctest.py
index 82bbc4b491..a57f7a4949 100644
--- a/_pytest/doctest.py
+++ b/_pytest/doctest.py
@@ -1,49 +1,84 @@
""" discover and run doctests in modules and test files."""
+from __future__ import absolute_import
+
+import traceback
+
+import pytest
+from _pytest._code.code import TerminalRepr, ReprFileLocation, ExceptionInfo
+from _pytest.python import FixtureRequest
+
-import pytest, py
-from _pytest.python import FixtureRequest, FuncFixtureInfo
-from py._code.code import TerminalRepr, ReprFileLocation
def pytest_addoption(parser):
+ parser.addini('doctest_optionflags', 'option flags for doctests',
+ type="args", default=["ELLIPSIS"])
group = parser.getgroup("collect")
group.addoption("--doctest-modules",
action="store_true", default=False,
help="run doctests in all .py modules",
dest="doctestmodules")
group.addoption("--doctest-glob",
- action="store", default="test*.txt", metavar="pat",
+ action="append", default=[], metavar="pat",
help="doctests file matching pattern, default: test*.txt",
dest="doctestglob")
+ group.addoption("--doctest-ignore-import-errors",
+ action="store_true", default=False,
+ help="ignore doctest ImportErrors",
+ dest="doctest_ignore_import_errors")
+
def pytest_collect_file(path, parent):
config = parent.config
if path.ext == ".py":
if config.option.doctestmodules:
return DoctestModule(path, parent)
- elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \
- path.check(fnmatch=config.getvalue("doctestglob")):
+ elif _is_doctest(config, path, parent):
return DoctestTextfile(path, parent)
+
+def _is_doctest(config, path, parent):
+ if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path):
+ return True
+ globs = config.getoption("doctestglob") or ['test*.txt']
+ for glob in globs:
+ if path.check(fnmatch=glob):
+ return True
+ return False
+
+
class ReprFailDoctest(TerminalRepr):
+
def __init__(self, reprlocation, lines):
self.reprlocation = reprlocation
self.lines = lines
+
def toterminal(self, tw):
for line in self.lines:
tw.line(line)
self.reprlocation.toterminal(tw)
+
class DoctestItem(pytest.Item):
+
def __init__(self, name, parent, runner=None, dtest=None):
super(DoctestItem, self).__init__(name, parent)
self.runner = runner
self.dtest = dtest
+ self.obj = None
+ self.fixture_request = None
+
+ def setup(self):
+ if self.dtest is not None:
+ self.fixture_request = _setup_fixtures(self)
+ globs = dict(getfixture=self.fixture_request.getfuncargvalue)
+ self.dtest.globs.update(globs)
def runtest(self):
+ _check_all_skipped(self.dtest)
self.runner.run(self.dtest)
def repr_failure(self, excinfo):
- doctest = py.std.doctest
+ import doctest
if excinfo.errisinstance((doctest.DocTestFailure,
doctest.UnexpectedException)):
doctestfailure = excinfo.value
@@ -56,17 +91,17 @@ class DoctestItem(pytest.Item):
lineno = test.lineno + example.lineno + 1
message = excinfo.type.__name__
reprlocation = ReprFileLocation(filename, lineno, message)
- checker = py.std.doctest.OutputChecker()
- REPORT_UDIFF = py.std.doctest.REPORT_UDIFF
- filelines = py.path.local(filename).readlines(cr=0)
- lines = []
+ checker = _get_checker()
+ REPORT_UDIFF = doctest.REPORT_UDIFF
if lineno is not None:
- i = max(test.lineno, max(0, lineno - 10)) # XXX?
- for line in filelines[i:lineno]:
- lines.append("%03d %s" % (i+1, line))
- i += 1
+ lines = doctestfailure.test.docstring.splitlines(False)
+ # add line numbers to the left of the error message
+ lines = ["%03d %s" % (i + test.lineno + 1, x)
+ for (i, x) in enumerate(lines)]
+ # trim docstring error lines to 10
+ lines = lines[example.lineno - 9:example.lineno + 1]
else:
- lines.append('EXAMPLE LOCATION UNKNOWN, not showing all tests of that example')
+ lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
indent = '>>>'
for line in example.source.splitlines():
lines.append('??? %s %s' % (indent, line))
@@ -75,10 +110,10 @@ class DoctestItem(pytest.Item):
lines += checker.output_difference(example,
doctestfailure.got, REPORT_UDIFF).split("\n")
else:
- inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
+ inner_excinfo = ExceptionInfo(excinfo.value.exc_info)
lines += ["UNEXPECTED EXCEPTION: %s" %
repr(inner_excinfo.value)]
- lines += py.std.traceback.format_exception(*excinfo.value.exc_info)
+ lines += traceback.format_exception(*excinfo.value.exc_info)
return ReprFailDoctest(reprlocation, lines)
else:
return super(DoctestItem, self).repr_failure(excinfo)
@@ -86,40 +121,170 @@ class DoctestItem(pytest.Item):
def reportinfo(self):
return self.fspath, None, "[doctest] %s" % self.name
-class DoctestTextfile(DoctestItem, pytest.File):
+
+def _get_flag_lookup():
+ import doctest
+ return dict(DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
+ DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
+ NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
+ ELLIPSIS=doctest.ELLIPSIS,
+ IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
+ COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
+ ALLOW_UNICODE=_get_allow_unicode_flag(),
+ ALLOW_BYTES=_get_allow_bytes_flag(),
+ )
+
+
+def get_optionflags(parent):
+ optionflags_str = parent.config.getini("doctest_optionflags")
+ flag_lookup_table = _get_flag_lookup()
+ flag_acc = 0
+ for flag in optionflags_str:
+ flag_acc |= flag_lookup_table[flag]
+ return flag_acc
+
+
+class DoctestTextfile(DoctestItem, pytest.Module):
+
def runtest(self):
- doctest = py.std.doctest
- # satisfy `FixtureRequest` constructor...
- self.funcargs = {}
- fm = self.session._fixturemanager
- def func():
- pass
- self._fixtureinfo = fm.getfixtureinfo(node=self, func=func,
- cls=None, funcargs=False)
- fixture_request = FixtureRequest(self)
- fixture_request._fillfixtures()
- failed, tot = doctest.testfile(
- str(self.fspath), module_relative=False,
- optionflags=doctest.ELLIPSIS,
- extraglobs=dict(getfixture=fixture_request.getfuncargvalue),
- raise_on_error=True, verbose=0)
-
-class DoctestModule(pytest.File):
+ import doctest
+ fixture_request = _setup_fixtures(self)
+
+ # inspired by doctest.testfile; ideally we would use it directly,
+ # but it doesn't support passing a custom checker
+ text = self.fspath.read()
+ filename = str(self.fspath)
+ name = self.fspath.basename
+ globs = dict(getfixture=fixture_request.getfuncargvalue)
+ if '__name__' not in globs:
+ globs['__name__'] = '__main__'
+
+ optionflags = get_optionflags(self)
+ runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
+ checker=_get_checker())
+
+ parser = doctest.DocTestParser()
+ test = parser.get_doctest(text, globs, name, filename, 0)
+ _check_all_skipped(test)
+ runner.run(test)
+
+
+def _check_all_skipped(test):
+ """raises pytest.skip() if all examples in the given DocTest have the SKIP
+ option set.
+ """
+ import doctest
+ all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
+ if all_skipped:
+ pytest.skip('all tests skipped by +SKIP option')
+
+
+class DoctestModule(pytest.Module):
def collect(self):
- doctest = py.std.doctest
+ import doctest
if self.fspath.basename == "conftest.py":
- module = self.config._conftest.importconftest(self.fspath)
+ module = self.config.pluginmanager._importconftest(self.fspath)
else:
- module = self.fspath.pyimport()
- # satisfy `FixtureRequest` constructor...
- self.funcargs = {}
- self._fixtureinfo = FuncFixtureInfo((), [], {})
- fixture_request = FixtureRequest(self)
- doctest_globals = dict(getfixture=fixture_request.getfuncargvalue)
+ try:
+ module = self.fspath.pyimport()
+ except ImportError:
+ if self.config.getvalue('doctest_ignore_import_errors'):
+ pytest.skip('unable to import module %r' % self.fspath)
+ else:
+ raise
# uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder()
- runner = doctest.DebugRunner(verbose=0, optionflags=doctest.ELLIPSIS)
- for test in finder.find(module, module.__name__,
- extraglobs=doctest_globals):
- if test.examples: # skip empty doctests
+ optionflags = get_optionflags(self)
+ runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
+ checker=_get_checker())
+ for test in finder.find(module, module.__name__):
+ if test.examples: # skip empty doctests
yield DoctestItem(test.name, self, runner, test)
+
+
+def _setup_fixtures(doctest_item):
+ """
+ Used by DoctestTextfile and DoctestItem to setup fixture information.
+ """
+ def func():
+ pass
+
+ doctest_item.funcargs = {}
+ fm = doctest_item.session._fixturemanager
+ doctest_item._fixtureinfo = fm.getfixtureinfo(node=doctest_item, func=func,
+ cls=None, funcargs=False)
+ fixture_request = FixtureRequest(doctest_item)
+ fixture_request._fillfixtures()
+ return fixture_request
+
+
+def _get_checker():
+ """
+ Returns a doctest.OutputChecker subclass that takes in account the
+ ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES
+ to strip b'' prefixes.
+ Useful when the same doctest should run in Python 2 and Python 3.
+
+ An inner class is used to avoid importing "doctest" at the module
+ level.
+ """
+ if hasattr(_get_checker, 'LiteralsOutputChecker'):
+ return _get_checker.LiteralsOutputChecker()
+
+ import doctest
+ import re
+
+ class LiteralsOutputChecker(doctest.OutputChecker):
+ """
+ Copied from doctest_nose_plugin.py from the nltk project:
+ https://github.com/nltk/nltk
+
+ Further extended to also support byte literals.
+ """
+
+ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
+ _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
+
+ def check_output(self, want, got, optionflags):
+ res = doctest.OutputChecker.check_output(self, want, got,
+ optionflags)
+ if res:
+ return True
+
+ allow_unicode = optionflags & _get_allow_unicode_flag()
+ allow_bytes = optionflags & _get_allow_bytes_flag()
+ if not allow_unicode and not allow_bytes:
+ return False
+
+ else: # pragma: no cover
+ def remove_prefixes(regex, txt):
+ return re.sub(regex, r'\1\2', txt)
+
+ if allow_unicode:
+ want = remove_prefixes(self._unicode_literal_re, want)
+ got = remove_prefixes(self._unicode_literal_re, got)
+ if allow_bytes:
+ want = remove_prefixes(self._bytes_literal_re, want)
+ got = remove_prefixes(self._bytes_literal_re, got)
+ res = doctest.OutputChecker.check_output(self, want, got,
+ optionflags)
+ return res
+
+ _get_checker.LiteralsOutputChecker = LiteralsOutputChecker
+ return _get_checker.LiteralsOutputChecker()
+
+
+def _get_allow_unicode_flag():
+ """
+ Registers and returns the ALLOW_UNICODE flag.
+ """
+ import doctest
+ return doctest.register_optionflag('ALLOW_UNICODE')
+
+
+def _get_allow_bytes_flag():
+ """
+ Registers and returns the ALLOW_BYTES flag.
+ """
+ import doctest
+ return doctest.register_optionflag('ALLOW_BYTES')
diff --git a/_pytest/genscript.py b/_pytest/genscript.py
index 25e8e6a0f9..d2962d8fc8 100755
--- a/_pytest/genscript.py
+++ b/_pytest/genscript.py
@@ -1,9 +1,15 @@
-""" generate a single-file self-contained version of pytest """
-import py
+""" (deprecated) generate a single-file self-contained version of pytest """
+import os
import sys
+import pkgutil
+
+import py
+import _pytest
+
+
def find_toplevel(name):
- for syspath in py.std.sys.path:
+ for syspath in sys.path:
base = py.path.local(syspath)
lib = base/name
if lib.check(dir=1):
@@ -26,12 +32,16 @@ def pkg_to_mapping(name):
for pyfile in toplevel.visit('*.py'):
pkg = pkgname(name, toplevel, pyfile)
name2src[pkg] = pyfile.read()
+ # with wheels py source code might be not be installed
+ # and the resulting genscript is useless, just bail out.
+ assert name2src, "no source code found for %r at %r" %(name, toplevel)
return name2src
def compress_mapping(mapping):
- data = py.std.pickle.dumps(mapping, 2)
- data = py.std.zlib.compress(data, 9)
- data = py.std.base64.encodestring(data)
+ import base64, pickle, zlib
+ data = pickle.dumps(mapping, 2)
+ data = zlib.compress(data, 9)
+ data = base64.encodestring(data)
data = data.decode('ascii')
return data
@@ -58,17 +68,20 @@ def pytest_addoption(parser):
help="create standalone pytest script at given target path.")
def pytest_cmdline_main(config):
+ import _pytest.config
genscript = config.getvalue("genscript")
if genscript:
- tw = py.io.TerminalWriter()
- deps = ['py', '_pytest', 'pytest']
+ tw = _pytest.config.create_terminal_writer(config)
+ tw.line("WARNING: usage of genscript is deprecated.",
+ red=True)
+ deps = ['py', '_pytest', 'pytest'] # pluggy is vendored
if sys.version_info < (2,7):
deps.append("argparse")
- tw.line("generated script will run on python2.5-python3.3++")
+ tw.line("generated script will run on python2.6-python3.3++")
else:
tw.line("WARNING: generated script will not run on python2.6 "
- "or below due to 'argparse' dependency. Use python2.6 "
- "to generate a python2.5/6 compatible script", red=True)
+ "due to 'argparse' dependency. Use python2.6 "
+ "to generate a python2.6 compatible script", red=True)
script = generate_script(
'import pytest; raise SystemExit(pytest.cmdline.main())',
deps,
@@ -78,3 +91,42 @@ def pytest_cmdline_main(config):
tw.line("generated pytest standalone script: %s" % genscript,
bold=True)
return 0
+
+
+def pytest_namespace():
+ return {'freeze_includes': freeze_includes}
+
+
+def freeze_includes():
+ """
+ Returns a list of module names used by py.test that should be
+ included by cx_freeze.
+ """
+ result = list(_iter_all_modules(py))
+ result += list(_iter_all_modules(_pytest))
+ return result
+
+
+def _iter_all_modules(package, prefix=''):
+ """
+ Iterates over the names of all modules that can be found in the given
+ package, recursively.
+
+ Example:
+ _iter_all_modules(_pytest) ->
+ ['_pytest.assertion.newinterpret',
+ '_pytest.capture',
+ '_pytest.core',
+ ...
+ ]
+ """
+ if type(package) is not str:
+ path, prefix = package.__path__[0], package.__name__ + '.'
+ else:
+ path = package
+ for _, name, is_package in pkgutil.iter_modules([path]):
+ if is_package:
+ for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
+ yield prefix + m
+ else:
+ yield prefix + name
diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py
index 1fef7eb3f8..1df0c56ac7 100644
--- a/_pytest/helpconfig.py
+++ b/_pytest/helpconfig.py
@@ -1,8 +1,7 @@
""" version info, help messages, tracing configuration. """
import py
import pytest
-import os, inspect, sys
-from _pytest.core import varnames
+import os, sys
def pytest_addoption(parser):
group = parser.getgroup('debugconfig')
@@ -23,27 +22,28 @@ def pytest_addoption(parser):
help="store internal tracing debug information in 'pytestdebug.log'.")
-def pytest_cmdline_parse(__multicall__):
- config = __multicall__.execute()
+@pytest.hookimpl(hookwrapper=True)
+def pytest_cmdline_parse():
+ outcome = yield
+ config = outcome.get_result()
if config.option.debug:
path = os.path.abspath("pytestdebug.log")
- f = open(path, 'w')
- config._debugfile = f
- f.write("versions pytest-%s, py-%s, python-%s\ncwd=%s\nargs=%s\n\n" %(
- pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)),
+ debugfile = open(path, 'w')
+ debugfile.write("versions pytest-%s, py-%s, "
+ "python-%s\ncwd=%s\nargs=%s\n\n" %(
+ pytest.__version__, py.__version__,
+ ".".join(map(str, sys.version_info)),
os.getcwd(), config._origargs))
- config.trace.root.setwriter(f.write)
+ config.trace.root.setwriter(debugfile.write)
+ undo_tracing = config.pluginmanager.enable_tracing()
sys.stderr.write("writing pytestdebug information to %s\n" % path)
- return config
-
-@pytest.mark.trylast
-def pytest_unconfigure(config):
- if hasattr(config, '_debugfile'):
- config._debugfile.close()
- sys.stderr.write("wrote pytestdebug information to %s\n" %
- config._debugfile.name)
- config.trace.root.setwriter(None)
-
+ def unset_tracing():
+ debugfile.close()
+ sys.stderr.write("wrote pytestdebug information to %s\n" %
+ debugfile.name)
+ config.trace.root.setwriter(None)
+ undo_tracing()
+ config.add_cleanup(unset_tracing)
def pytest_cmdline_main(config):
if config.option.version:
@@ -56,15 +56,15 @@ def pytest_cmdline_main(config):
sys.stderr.write(line + "\n")
return 0
elif config.option.help:
- config.do_configure()
+ config._do_configure()
showhelp(config)
- config.do_unconfigure()
+ config._ensure_unconfigure()
return 0
def showhelp(config):
- tw = py.io.TerminalWriter()
+ reporter = config.pluginmanager.get_plugin('terminalreporter')
+ tw = reporter._tw
tw.write(config._parser.optparser.format_help())
- tw.write(config._parser.optparser.format_epilog(None))
tw.line()
tw.line()
#tw.sep( "=", "config file settings")
@@ -80,22 +80,27 @@ def showhelp(config):
line = " %-24s %s" %(spec, help)
tw.line(line[:tw.fullwidth])
- tw.line() ; tw.line()
- #tw.sep("=")
+ tw.line()
+ tw.line("environment variables:")
+ vars = [
+ ("PYTEST_ADDOPTS", "extra command line options"),
+ ("PYTEST_PLUGINS", "comma-separated plugins to load during startup"),
+ ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals")
+ ]
+ for name, help in vars:
+ tw.line(" %-24s %s" % (name, help))
+ tw.line()
+ tw.line()
+
tw.line("to see available markers type: py.test --markers")
tw.line("to see available fixtures type: py.test --fixtures")
tw.line("(shown according to specified file_or_dir or current dir "
"if not specified)")
+
+ for warningreport in reporter.stats.get('warnings', []):
+ tw.line("warning : " + warningreport.message, red=True)
return
- tw.line("conftest.py options:")
- tw.line()
- conftestitems = sorted(config._parser._conftestdict.items())
- for name, help in conftest_options + conftestitems:
- line = " %-15s %s" %(name, help)
- tw.line(line[:tw.fullwidth])
- tw.line()
- #tw.sep( "=")
conftest_options = [
('pytest_plugins', 'list of plugin names to load'),
@@ -103,10 +108,10 @@ conftest_options = [
def getpluginversioninfo(config):
lines = []
- plugininfo = config.pluginmanager._plugin_distinfo
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
lines.append("setuptools registered plugins:")
- for dist, plugin in plugininfo:
+ for plugin, dist in plugininfo:
loc = getattr(plugin, '__file__', repr(plugin))
content = "%s-%s at %s" % (dist.project_name, dist.version, loc)
lines.append(" " + content)
@@ -124,7 +129,7 @@ def pytest_report_header(config):
if config.option.traceconfig:
lines.append("active plugins:")
- items = config.pluginmanager._name2plugin.items()
+ items = config.pluginmanager.list_name_plugin()
for name, plugin in items:
if hasattr(plugin, '__file__'):
r = plugin.__file__
@@ -132,72 +137,3 @@ def pytest_report_header(config):
r = repr(plugin)
lines.append(" %-20s: %s" %(name, r))
return lines
-
-
-# =====================================================
-# validate plugin syntax and hooks
-# =====================================================
-
-def pytest_plugin_registered(manager, plugin):
- methods = collectattr(plugin)
- hooks = {}
- for hookspec in manager.hook._hookspecs:
- hooks.update(collectattr(hookspec))
-
- stringio = py.io.TextIO()
- def Print(*args):
- if args:
- stringio.write(" ".join(map(str, args)))
- stringio.write("\n")
-
- fail = False
- while methods:
- name, method = methods.popitem()
- #print "checking", name
- if isgenerichook(name):
- continue
- if name not in hooks:
- if not getattr(method, 'optionalhook', False):
- Print("found unknown hook:", name)
- fail = True
- else:
- #print "checking", method
- method_args = list(varnames(method))
- if '__multicall__' in method_args:
- method_args.remove('__multicall__')
- hook = hooks[name]
- hookargs = varnames(hook)
- for arg in method_args:
- if arg not in hookargs:
- Print("argument %r not available" %(arg, ))
- Print("actual definition: %s" %(formatdef(method)))
- Print("available hook arguments: %s" %
- ", ".join(hookargs))
- fail = True
- break
- #if not fail:
- # print "matching hook:", formatdef(method)
- if fail:
- name = getattr(plugin, '__name__', plugin)
- raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue()))
-
-class PluginValidationError(Exception):
- """ plugin failed validation. """
-
-def isgenerichook(name):
- return name == "pytest_plugins" or \
- name.startswith("pytest_funcarg__")
-
-def collectattr(obj):
- methods = {}
- for apiname in dir(obj):
- if apiname.startswith("pytest_"):
- methods[apiname] = getattr(obj, apiname)
- return methods
-
-def formatdef(func):
- return "%s%s" % (
- func.__name__,
- inspect.formatargspec(*inspect.getargspec(func))
- )
-
diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py
index 244100f5a9..60e9b47d26 100644
--- a/_pytest/hookspec.py
+++ b/_pytest/hookspec.py
@@ -1,33 +1,42 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
+from _pytest._pluggy import HookspecMarker
+
+hookspec = HookspecMarker("pytest")
+
# -------------------------------------------------------------------------
-# Initialization
+# Initialization hooks called for every plugin
# -------------------------------------------------------------------------
+@hookspec(historic=True)
def pytest_addhooks(pluginmanager):
- """called at plugin load time to allow adding new hooks via a call to
- pluginmanager.registerhooks(module)."""
+ """called at plugin registration time to allow adding new hooks via a call to
+ pluginmanager.add_hookspecs(module_or_class, prefix)."""
+@hookspec(historic=True)
def pytest_namespace():
"""return dict of name->object to be made globally available in
- the pytest namespace. This hook is called before command line options
- are parsed.
+ the pytest namespace. This hook is called at plugin registration
+ time.
"""
-def pytest_cmdline_parse(pluginmanager, args):
- """return initialized config object, parsing the specified args. """
-pytest_cmdline_parse.firstresult = True
+@hookspec(historic=True)
+def pytest_plugin_registered(plugin, manager):
+ """ a new pytest plugin got registered. """
-def pytest_cmdline_preparse(config, args):
- """(deprecated) modify command line arguments before option parsing. """
+@hookspec(historic=True)
def pytest_addoption(parser):
- """register argparse-style options and ini-style config values.
-
- This function must be implemented in a :ref:`plugin <pluginorder>` and is
+ """register argparse-style options and ini-style config values,
called once at the beginning of a test run.
+ .. note::
+
+ This function should be implemented only in plugins or ``conftest.py``
+ files situated at the tests root directory due to how py.test
+ :ref:`discovers plugins during startup <pluginorder>`.
+
:arg parser: To add command line options, call
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...)
@@ -47,35 +56,43 @@ def pytest_addoption(parser):
via (deprecated) ``pytest.config``.
"""
-def pytest_cmdline_main(config):
- """ called for performing the main command line action. The default
- implementation will invoke the configure hooks and runtest_mainloop. """
-pytest_cmdline_main.firstresult = True
-
-def pytest_load_initial_conftests(args, early_config, parser):
- """ implements loading initial conftests.
- """
-
+@hookspec(historic=True)
def pytest_configure(config):
""" called after command line options have been parsed
- and all plugins and initial conftest files been loaded.
+ and all plugins and initial conftest files been loaded.
+ This hook is called for every plugin.
"""
-def pytest_unconfigure(config):
- """ called before test process is exited. """
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins as well as directly
+# discoverable conftest.py local plugins.
+# -------------------------------------------------------------------------
+
+@hookspec(firstresult=True)
+def pytest_cmdline_parse(pluginmanager, args):
+ """return initialized config object, parsing the specified args. """
+
+def pytest_cmdline_preparse(config, args):
+ """(deprecated) modify command line arguments before option parsing. """
+
+@hookspec(firstresult=True)
+def pytest_cmdline_main(config):
+ """ called for performing the main command line action. The default
+ implementation will invoke the configure hooks and runtest_mainloop. """
+
+def pytest_load_initial_conftests(early_config, parser, args):
+ """ implements the loading of initial conftest files ahead
+ of command line option parsing. """
-def pytest_runtestloop(session):
- """ called for performing the main runtest loop
- (after collection finished). """
-pytest_runtestloop.firstresult = True
# -------------------------------------------------------------------------
# collection hooks
# -------------------------------------------------------------------------
+@hookspec(firstresult=True)
def pytest_collection(session):
""" perform the collection protocol for the given session. """
-pytest_collection.firstresult = True
def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order
@@ -84,16 +101,16 @@ def pytest_collection_modifyitems(session, config, items):
def pytest_collection_finish(session):
""" called after collection has been performed and modified. """
+@hookspec(firstresult=True)
def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling
more specific hooks.
"""
-pytest_ignore_collect.firstresult = True
+@hookspec(firstresult=True)
def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """
-pytest_collect_directory.firstresult = True
def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node
@@ -112,29 +129,29 @@ def pytest_collectreport(report):
def pytest_deselected(items):
""" called for test items deselected by keyword. """
+@hookspec(firstresult=True)
def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """
-pytest_make_collect_report.firstresult = True
# -------------------------------------------------------------------------
# Python test function related hooks
# -------------------------------------------------------------------------
+@hookspec(firstresult=True)
def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given path.
This hook will be called for each matching test module path.
The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module.
"""
-pytest_pycollect_makemodule.firstresult = True
+@hookspec(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None. """
-pytest_pycollect_makeitem.firstresult = True
+@hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """
-pytest_pyfunc_call.firstresult = True
def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function."""
@@ -142,9 +159,16 @@ def pytest_generate_tests(metafunc):
# -------------------------------------------------------------------------
# generic runtest related hooks
# -------------------------------------------------------------------------
-def pytest_itemstart(item, node=None):
+
+@hookspec(firstresult=True)
+def pytest_runtestloop(session):
+ """ called for performing the main runtest loop
+ (after collection finished). """
+
+def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """
+@hookspec(firstresult=True)
def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for
the given test item, including capturing exceptions and calling
@@ -152,13 +176,12 @@ def pytest_runtest_protocol(item, nextitem):
:arg item: test item for which the runtest protocol is performed.
- :arg nexitem: the scheduled-to-be-next test item (or None if this
- is the end my friend). This argument is passed on to
- :py:func:`pytest_runtest_teardown`.
+ :arg nextitem: the scheduled-to-be-next test item (or None if this
+ is the end my friend). This argument is passed on to
+ :py:func:`pytest_runtest_teardown`.
:return boolean: True if no further hook implementations should be invoked.
"""
-pytest_runtest_protocol.firstresult = True
def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """
@@ -172,18 +195,18 @@ def pytest_runtest_call(item):
def pytest_runtest_teardown(item, nextitem):
""" called after ``pytest_runtest_call``.
- :arg nexitem: the scheduled-to-be-next test item (None if no further
- test item is scheduled). This argument can be used to
- perform exact teardowns, i.e. calling just enough finalizers
- so that nextitem only needs to call setup-functions.
+ :arg nextitem: the scheduled-to-be-next test item (None if no further
+ test item is scheduled). This argument can be used to
+ perform exact teardowns, i.e. calling just enough finalizers
+ so that nextitem only needs to call setup-functions.
"""
+@hookspec(firstresult=True)
def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and
:py:class:`_pytest.runner.CallInfo`.
"""
-pytest_runtest_makereport.firstresult = True
def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to
@@ -199,6 +222,9 @@ def pytest_sessionstart(session):
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
+def pytest_unconfigure(config):
+ """ called before test process is exited. """
+
# -------------------------------------------------------------------------
# hooks for customising the assert methods
@@ -220,28 +246,32 @@ def pytest_assertrepr_compare(config, op, left, right):
def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal reporting."""
+@hookspec(firstresult=True)
def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting."""
-pytest_report_teststatus.firstresult = True
def pytest_terminal_summary(terminalreporter):
""" add additional section in terminal summary reporting. """
+
+@hookspec(historic=True)
+def pytest_logwarning(message, code, nodeid, fslocation):
+ """ process a warning specified by a message, a code string,
+ a nodeid and fslocation (both of which may be None
+ if the warning is not tied to a partilar node/location)."""
+
# -------------------------------------------------------------------------
# doctest hooks
# -------------------------------------------------------------------------
+@hookspec(firstresult=True)
def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest"""
-pytest_doctest_prepare_content.firstresult = True
# -------------------------------------------------------------------------
# error handling and internal debugging hooks
# -------------------------------------------------------------------------
-def pytest_plugin_registered(plugin, manager):
- """ a new pytest plugin got registered. """
-
def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """
@@ -249,11 +279,17 @@ def pytest_keyboard_interrupt(excinfo):
""" called for keyboard interrupt. """
def pytest_exception_interact(node, call, report):
- """ (experimental, new in 2.4) called when
- an exception was raised which can potentially be
+ """called when an exception was raised which can potentially be
interactively handled.
This hook is only called if an exception was raised
- that is not an internal exception like "skip.Exception".
+ that is not an internal exception like ``skip.Exception``.
"""
+def pytest_enter_pdb(config):
+ """ called upon pdb.set_trace(), can be used by plugins to take special
+ action just before the python debugger enters in interactive mode.
+
+ :arg config: pytest config object
+ :type config: _pytest.config.Config
+ """
diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py
index 2d330870f1..f4de1343ed 100644
--- a/_pytest/junitxml.py
+++ b/_pytest/junitxml.py
@@ -1,33 +1,32 @@
-""" report test results in JUnit-XML format, for use with Hudson and build integration servers.
+"""
+ report test results in JUnit-XML format,
+ for use with Jenkins and build integration servers.
+
Based on initial code from Ross Lawley.
"""
+# Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
+# src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
import py
import os
import re
import sys
import time
+import pytest
# Python 2.X and 3.X compatibility
-try:
- unichr(65)
-except NameError:
+if sys.version_info[0] < 3:
+ from codecs import open
+else:
unichr = chr
-try:
- unicode('A')
-except NameError:
unicode = str
-try:
- long(1)
-except NameError:
long = int
class Junit(py.xml.Namespace):
pass
-
# We need to get the subset of the invalid unicode ranges according to
# XML 1.0 which are valid in this python build. Hence we calculate
# this dynamically instead of hardcoding it. The spec range of valid
@@ -35,21 +34,21 @@ class Junit(py.xml.Namespace):
# | [#x10000-#x10FFFF]
_legal_chars = (0x09, 0x0A, 0x0d)
_legal_ranges = (
- (0x20, 0x7E),
- (0x80, 0xD7FF),
- (0xE000, 0xFFFD),
- (0x10000, 0x10FFFF),
+ (0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF),
)
-_legal_xml_re = [unicode("%s-%s") % (unichr(low), unichr(high))
- for (low, high) in _legal_ranges
- if low < sys.maxunicode]
+_legal_xml_re = [
+ unicode("%s-%s") % (unichr(low), unichr(high))
+ for (low, high) in _legal_ranges if low < sys.maxunicode
+]
_legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re
-illegal_xml_re = re.compile(unicode('[^%s]') %
- unicode('').join(_legal_xml_re))
+illegal_xml_re = re.compile(unicode('[^%s]') % unicode('').join(_legal_xml_re))
del _legal_chars
del _legal_ranges
del _legal_xml_re
+_py_ext_re = re.compile(r"\.py$")
+
+
def bin_xml_escape(arg):
def repl(matchobj):
i = ord(matchobj.group())
@@ -57,173 +56,332 @@ def bin_xml_escape(arg):
return unicode('#x%02X') % i
else:
return unicode('#x%04X') % i
+
return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
-def pytest_addoption(parser):
- group = parser.getgroup("terminal reporting")
- group.addoption('--junitxml', '--junit-xml', action="store",
- dest="xmlpath", metavar="path", default=None,
- help="create junit-xml style report file at given path.")
- group.addoption('--junitprefix', '--junit-prefix', action="store",
- metavar="str", default=None,
- help="prepend prefix to classnames in junit-xml output")
-def pytest_configure(config):
- xmlpath = config.option.xmlpath
- # prevent opening xmllog on slave nodes (xdist)
- if xmlpath and not hasattr(config, 'slaveinput'):
- config._xml = LogXML(xmlpath, config.option.junitprefix)
- config.pluginmanager.register(config._xml)
+class _NodeReporter(object):
+ def __init__(self, nodeid, xml):
-def pytest_unconfigure(config):
- xml = getattr(config, '_xml', None)
- if xml:
- del config._xml
- config.pluginmanager.unregister(xml)
+ self.id = nodeid
+ self.xml = xml
+ self.add_stats = self.xml.add_stats
+ self.duration = 0
+ self.properties = []
+ self.nodes = []
+ self.testcase = None
+ self.attrs = {}
+ def append(self, node):
+ self.xml.add_stats(type(node).__name__)
+ self.nodes.append(node)
-def mangle_testnames(names):
- names = [x.replace(".py", "") for x in names if x != '()']
- names[0] = names[0].replace("/", '.')
- return names
+ def add_property(self, name, value):
+ self.properties.append((str(name), bin_xml_escape(value)))
-class LogXML(object):
- def __init__(self, logfile, prefix):
- logfile = os.path.expanduser(os.path.expandvars(logfile))
- self.logfile = os.path.normpath(os.path.abspath(logfile))
- self.prefix = prefix
- self.tests = []
- self.passed = self.skipped = 0
- self.failed = self.errors = 0
+ def make_properties_node(self):
+ """Return a Junit node containing custom properties, if any.
+ """
+ if self.properties:
+ return Junit.properties([
+ Junit.property(name=name, value=value)
+ for name, value in self.properties
+ ])
+ return ''
- def _opentestcase(self, report):
- names = mangle_testnames(report.nodeid.split("::"))
+ def record_testreport(self, testreport):
+ assert not self.testcase
+ names = mangle_test_address(testreport.nodeid)
classnames = names[:-1]
- if self.prefix:
- classnames.insert(0, self.prefix)
- self.tests.append(Junit.testcase(
- classname=".".join(classnames),
- name=bin_xml_escape(names[-1]),
- time=getattr(report, 'duration', 0)
- ))
+ if self.xml.prefix:
+ classnames.insert(0, self.xml.prefix)
+ attrs = {
+ "classname": ".".join(classnames),
+ "name": bin_xml_escape(names[-1]),
+ "file": testreport.location[0],
+ }
+ if testreport.location[1] is not None:
+ attrs["line"] = testreport.location[1]
+ self.attrs = attrs
- def _write_captured_output(self, report):
- sec = dict(report.sections)
- for name in ('out', 'err'):
- content = sec.get("Captured std%s" % name)
- if content:
- tag = getattr(Junit, 'system-'+name)
- self.append(tag(bin_xml_escape(content)))
+ def to_xml(self):
+ testcase = Junit.testcase(time=self.duration, **self.attrs)
+ testcase.append(self.make_properties_node())
+ for node in self.nodes:
+ testcase.append(node)
+ return testcase
+
+ def _add_simple(self, kind, message, data=None):
+ data = bin_xml_escape(data)
+ node = kind(data, message=message)
+ self.append(node)
- def append(self, obj):
- self.tests[-1].append(obj)
+ def _write_captured_output(self, report):
+ for capname in ('out', 'err'):
+ allcontent = ""
+ for name, content in report.get_sections("Captured std%s" %
+ capname):
+ allcontent += content
+ if allcontent:
+ tag = getattr(Junit, 'system-' + capname)
+ self.append(tag(bin_xml_escape(allcontent)))
def append_pass(self, report):
- self.passed += 1
+ self.add_stats('passed')
self._write_captured_output(report)
def append_failure(self, report):
- #msg = str(report.longrepr.reprtraceback.extraline)
+ # msg = str(report.longrepr.reprtraceback.extraline)
if hasattr(report, "wasxfail"):
- self.append(
- Junit.skipped(message="xfail-marked test passes unexpectedly"))
- self.skipped += 1
+ self._add_simple(
+ Junit.skipped,
+ "xfail-marked test passes unexpectedly")
else:
- fail = Junit.failure(message="test failure")
+ if hasattr(report.longrepr, "reprcrash"):
+ message = report.longrepr.reprcrash.message
+ elif isinstance(report.longrepr, (unicode, str)):
+ message = report.longrepr
+ else:
+ message = str(report.longrepr)
+ message = bin_xml_escape(message)
+ fail = Junit.failure(message=message)
fail.append(bin_xml_escape(report.longrepr))
self.append(fail)
- self.failed += 1
self._write_captured_output(report)
- def append_collect_failure(self, report):
- #msg = str(report.longrepr.reprtraceback.extraline)
- self.append(Junit.failure(bin_xml_escape(report.longrepr),
- message="collection failure"))
- self.errors += 1
+ def append_collect_error(self, report):
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ self.append(Junit.error(bin_xml_escape(report.longrepr),
+ message="collection failure"))
def append_collect_skipped(self, report):
- #msg = str(report.longrepr.reprtraceback.extraline)
- self.append(Junit.skipped(bin_xml_escape(report.longrepr),
- message="collection skipped"))
- self.skipped += 1
+ self._add_simple(
+ Junit.skipped, "collection skipped", report.longrepr)
def append_error(self, report):
- self.append(Junit.error(bin_xml_escape(report.longrepr),
- message="test setup failure"))
- self.errors += 1
+ self._add_simple(
+ Junit.error, "test setup failure", report.longrepr)
+ self._write_captured_output(report)
def append_skipped(self, report):
if hasattr(report, "wasxfail"):
- self.append(Junit.skipped(bin_xml_escape(report.wasxfail),
- message="expected test failure"))
+ self._add_simple(
+ Junit.skipped, "expected test failure", report.wasxfail
+ )
else:
filename, lineno, skipreason = report.longrepr
if skipreason.startswith("Skipped: "):
skipreason = bin_xml_escape(skipreason[9:])
self.append(
- Junit.skipped("%s:%s: %s" % report.longrepr,
+ Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
type="pytest.skip",
- message=skipreason
- ))
- self.skipped += 1
+ message=skipreason))
self._write_captured_output(report)
+ def finalize(self):
+ data = self.to_xml().unicode(indent=0)
+ self.__dict__.clear()
+ self.to_xml = lambda: py.xml.raw(data)
+
+
+@pytest.fixture
+def record_xml_property(request):
+ """Fixture that adds extra xml properties to the tag for the calling test.
+ The fixture is callable with (name, value), with value being automatically
+ xml-encoded.
+ """
+ request.node.warn(
+ code='C3',
+ message='record_xml_property is an experimental feature',
+ )
+ xml = getattr(request.config, "_xml", None)
+ if xml is not None:
+ node_reporter = xml.node_reporter(request.node.nodeid)
+ return node_reporter.add_property
+ else:
+ def add_property_noop(name, value):
+ pass
+
+ return add_property_noop
+
+
+def pytest_addoption(parser):
+ group = parser.getgroup("terminal reporting")
+ group.addoption(
+ '--junitxml', '--junit-xml',
+ action="store",
+ dest="xmlpath",
+ metavar="path",
+ default=None,
+ help="create junit-xml style report file at given path.")
+ group.addoption(
+ '--junitprefix', '--junit-prefix',
+ action="store",
+ metavar="str",
+ default=None,
+ help="prepend prefix to classnames in junit-xml output")
+
+
+def pytest_configure(config):
+ xmlpath = config.option.xmlpath
+ # prevent opening xmllog on slave nodes (xdist)
+ if xmlpath and not hasattr(config, 'slaveinput'):
+ config._xml = LogXML(xmlpath, config.option.junitprefix)
+ config.pluginmanager.register(config._xml)
+
+
+def pytest_unconfigure(config):
+ xml = getattr(config, '_xml', None)
+ if xml:
+ del config._xml
+ config.pluginmanager.unregister(xml)
+
+
+def mangle_test_address(address):
+ path, possible_open_bracket, params = address.partition('[')
+ names = path.split("::")
+ try:
+ names.remove('()')
+ except ValueError:
+ pass
+ # convert file path to dotted path
+ names[0] = names[0].replace("/", '.')
+ names[0] = _py_ext_re.sub("", names[0])
+ # put any params back
+ names[-1] += possible_open_bracket + params
+ return names
+
+
+class LogXML(object):
+ def __init__(self, logfile, prefix):
+ logfile = os.path.expanduser(os.path.expandvars(logfile))
+ self.logfile = os.path.normpath(os.path.abspath(logfile))
+ self.prefix = prefix
+ self.stats = dict.fromkeys([
+ 'error',
+ 'passed',
+ 'failure',
+ 'skipped',
+ ], 0)
+ self.node_reporters = {} # nodeid -> _NodeReporter
+ self.node_reporters_ordered = []
+
+ def finalize(self, report):
+ nodeid = getattr(report, 'nodeid', report)
+ # local hack to handle xdist report order
+ slavenode = getattr(report, 'node', None)
+ reporter = self.node_reporters.pop((nodeid, slavenode))
+ if reporter is not None:
+ reporter.finalize()
+
+ def node_reporter(self, report):
+ nodeid = getattr(report, 'nodeid', report)
+ # local hack to handle xdist report order
+ slavenode = getattr(report, 'node', None)
+
+ key = nodeid, slavenode
+
+ if key in self.node_reporters:
+ # TODO: breasks for --dist=each
+ return self.node_reporters[key]
+ reporter = _NodeReporter(nodeid, self)
+ self.node_reporters[key] = reporter
+ self.node_reporters_ordered.append(reporter)
+ return reporter
+
+ def add_stats(self, key):
+ if key in self.stats:
+ self.stats[key] += 1
+
+ def _opentestcase(self, report):
+ reporter = self.node_reporter(report)
+ reporter.record_testreport(report)
+ return reporter
+
def pytest_runtest_logreport(self, report):
+ """handle a setup/call/teardown report, generating the appropriate
+ xml tags as necessary.
+
+ note: due to plugins like xdist, this hook may be called in interlaced
+ order with reports from other nodes. for example:
+
+ usual call order:
+ -> setup node1
+ -> call node1
+ -> teardown node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+
+ possible call order in xdist:
+ -> setup node1
+ -> call node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+ -> teardown node1
+ """
if report.passed:
- if report.when == "call": # ignore setup/teardown
- self._opentestcase(report)
- self.append_pass(report)
+ if report.when == "call": # ignore setup/teardown
+ reporter = self._opentestcase(report)
+ reporter.append_pass(report)
elif report.failed:
- self._opentestcase(report)
- if report.when != "call":
- self.append_error(report)
+ reporter = self._opentestcase(report)
+ if report.when == "call":
+ reporter.append_failure(report)
else:
- self.append_failure(report)
+ reporter.append_error(report)
elif report.skipped:
- self._opentestcase(report)
- self.append_skipped(report)
+ reporter = self._opentestcase(report)
+ reporter.append_skipped(report)
+ self.update_testcase_duration(report)
+ if report.when == "teardown":
+ self.finalize(report)
+
+ def update_testcase_duration(self, report):
+ """accumulates total duration for nodeid from given report and updates
+ the Junit.testcase with the new total if already created.
+ """
+ reporter = self.node_reporter(report)
+ reporter.duration += getattr(report, 'duration', 0.0)
def pytest_collectreport(self, report):
if not report.passed:
- self._opentestcase(report)
+ reporter = self._opentestcase(report)
if report.failed:
- self.append_collect_failure(report)
+ reporter.append_collect_error(report)
else:
- self.append_collect_skipped(report)
+ reporter.append_collect_skipped(report)
def pytest_internalerror(self, excrepr):
- self.errors += 1
- data = bin_xml_escape(excrepr)
- self.tests.append(
- Junit.testcase(
- Junit.error(data, message="internal error"),
- classname="pytest",
- name="internal"))
+ reporter = self.node_reporter('internal')
+ reporter.attrs.update(classname="pytest", name='internal')
+ reporter._add_simple(Junit.error, 'internal error', excrepr)
def pytest_sessionstart(self):
self.suite_start_time = time.time()
def pytest_sessionfinish(self):
- if py.std.sys.version_info[0] < 3:
- logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8')
- else:
- logfile = open(self.logfile, 'w', encoding='utf-8')
-
+ dirname = os.path.dirname(os.path.abspath(self.logfile))
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+ logfile = open(self.logfile, 'w', encoding='utf-8')
suite_stop_time = time.time()
suite_time_delta = suite_stop_time - self.suite_start_time
- numtests = self.passed + self.failed
+
+ numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped']
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
logfile.write(Junit.testsuite(
- self.tests,
+ [x.to_xml() for x in self.node_reporters_ordered],
name="pytest",
- errors=self.errors,
- failures=self.failed,
- skips=self.skipped,
+ errors=self.stats['error'],
+ failures=self.stats['failure'],
+ skips=self.stats['skipped'],
tests=numtests,
- time="%.3f" % suite_time_delta,
- ).unicode(indent=0))
+ time="%.3f" % suite_time_delta, ).unicode(indent=0))
logfile.close()
def pytest_terminal_summary(self, terminalreporter):
- terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
+ terminalreporter.write_sep("-",
+ "generated xml file: %s" % (self.logfile))
diff --git a/_pytest/main.py b/_pytest/main.py
index 3ebfc4ddeb..8654d7af62 100644
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -1,14 +1,19 @@
""" core implementation of testing process: init, session, runtest loop. """
+import imp
+import os
+import re
+import sys
+import _pytest
+import _pytest._code
import py
-import pytest, _pytest
-import os, sys, imp
+import pytest
try:
from collections import MutableMapping as MappingMixin
except ImportError:
from UserDict import DictMixin as MappingMixin
-from _pytest.runner import collect_one_node, Skipped
+from _pytest.runner import collect_one_node
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@@ -18,12 +23,15 @@ EXIT_TESTSFAILED = 1
EXIT_INTERRUPTED = 2
EXIT_INTERNALERROR = 3
EXIT_USAGEERROR = 4
+EXIT_NOTESTSCOLLECTED = 5
-name_re = py.std.re.compile("^[a-zA-Z_]\w*$")
+name_re = re.compile("^[a-zA-Z_]\w*$")
def pytest_addoption(parser):
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
- type="args", default=('.*', 'CVS', '_darcs', '{arch}'))
+ type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg'])
+ parser.addini("testpaths", "directories to search for tests when no files or directories are given in the command line.",
+ type="args", default=[])
#parser.addini("dirpatterns",
# "patterns specifying possible locations of test files",
# type="linelist", default=["**/test_*.txt",
@@ -38,6 +46,8 @@ def pytest_addoption(parser):
help="exit after first num failures or errors.")
group._addoption('--strict', action="store_true",
help="run pytest in strict mode, warnings become errors.")
+ group._addoption("-c", metavar="file", type=str, dest="inifilename",
+ help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
group = parser.getgroup("collect", "collection")
group.addoption('--collectonly', '--collect-only', action="store_true",
@@ -51,6 +61,9 @@ def pytest_addoption(parser):
group.addoption('--confcutdir', dest="confcutdir", default=None,
metavar="dir",
help="only load conftest.py's relative to specified dir.")
+ group.addoption('--noconftest', action="store_true",
+ dest="noconftest", default=False,
+ help="Don't load any conftest.py files.")
group = parser.getgroup("debugconfig",
"test session debugging and configuration")
@@ -74,38 +87,32 @@ def wrap_session(config, doit):
initstate = 0
try:
try:
- config.do_configure()
+ config._do_configure()
initstate = 1
config.hook.pytest_sessionstart(session=session)
initstate = 2
- doit(config, session)
+ session.exitstatus = doit(config, session) or 0
except pytest.UsageError:
- args = sys.exc_info()[1].args
- for msg in args:
- sys.stderr.write("ERROR: %s\n" %(msg,))
- session.exitstatus = EXIT_USAGEERROR
+ raise
except KeyboardInterrupt:
- excinfo = py.code.ExceptionInfo()
+ excinfo = _pytest._code.ExceptionInfo()
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
session.exitstatus = EXIT_INTERRUPTED
except:
- excinfo = py.code.ExceptionInfo()
+ excinfo = _pytest._code.ExceptionInfo()
config.notify_exception(excinfo, config.option)
session.exitstatus = EXIT_INTERNALERROR
if excinfo.errisinstance(SystemExit):
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
- else:
- if session._testsfailed:
- session.exitstatus = EXIT_TESTSFAILED
+
finally:
+ excinfo = None # Explicitly break reference cycle.
session.startdir.chdir()
if initstate >= 2:
config.hook.pytest_sessionfinish(
session=session,
exitstatus=session.exitstatus)
- if initstate >= 1:
- config.do_unconfigure()
- config.pluginmanager.ensure_shutdown()
+ config._ensure_unconfigure()
return session.exitstatus
def pytest_cmdline_main(config):
@@ -117,6 +124,11 @@ def _main(config, session):
config.hook.pytest_collection(session=session)
config.hook.pytest_runtestloop(session=session)
+ if session.testsfailed:
+ return EXIT_TESTSFAILED
+ elif session.testscollected == 0:
+ return EXIT_NOTESTSCOLLECTED
+
def pytest_collection(session):
return session.perform_collect()
@@ -144,23 +156,21 @@ def pytest_ignore_collect(path, config):
p = path.dirpath()
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
ignore_paths = ignore_paths or []
- excludeopt = config.getvalue("ignore")
+ excludeopt = config.getoption("ignore")
if excludeopt:
ignore_paths.extend([py.path.local(x) for x in excludeopt])
return path in ignore_paths
-class HookProxy:
- def __init__(self, fspath, config):
+class FSHookProxy:
+ def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath
- self.config = config
+ self.pm = pm
+ self.remove_mods = remove_mods
def __getattr__(self, name):
- hookmethod = getattr(self.config.hook, name)
-
- def call_matching_hooks(**kwargs):
- plugins = self.config._getmatchingplugins(self.fspath)
- return hookmethod.pcall(plugins, **kwargs)
- return call_matching_hooks
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+ self.__dict__[name] = x
+ return x
def compatproperty(name):
def fget(self):
@@ -233,17 +243,12 @@ class Node(object):
# used for storing artificial fixturedefs for direct parametrization
self._name2pseudofixturedef = {}
- #self.extrainit()
@property
def ihook(self):
""" fspath sensitive hook proxy used to call pytest hooks"""
return self.session.gethookproxy(self.fspath)
- #def extrainit(self):
- # """"extra initialization after Node is initialized. Implemented
- # by some subclasses. """
-
Module = compatproperty("Module")
Class = compatproperty("Class")
Instance = compatproperty("Instance")
@@ -263,6 +268,20 @@ class Node(object):
return "<%s %r>" %(self.__class__.__name__,
getattr(self, 'name', None))
+ def warn(self, code, message):
+ """ generate a warning with the given code and message for this
+ item. """
+ assert isinstance(code, str)
+ fslocation = getattr(self, "location", None)
+ if fslocation is None:
+ fslocation = getattr(self, "fspath", None)
+ else:
+ fslocation = "%s:%s" % fslocation[:2]
+
+ self.ihook.pytest_logwarning.call_historic(kwargs=dict(
+ code=code, message=message,
+ nodeid=self.nodeid, fslocation=fslocation))
+
# methods for ordering nodes
@property
def nodeid(self):
@@ -297,7 +316,7 @@ class Node(object):
except py.builtin._sysex:
raise
except:
- failure = py.std.sys.exc_info()
+ failure = sys.exc_info()
setattr(self, exattrname, failure)
raise
setattr(self, attrname, res)
@@ -346,9 +365,6 @@ class Node(object):
def listnames(self):
return [x.name for x in self.listchain()]
- def getplugins(self):
- return self.config._getmatchingplugins(self.fspath)
-
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
@@ -372,20 +388,24 @@ class Node(object):
fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr()
+ tbfilter = True
if self.config.option.fulltrace:
style="long"
else:
self._prunetraceback(excinfo)
- # XXX should excinfo.getrepr record all data and toterminal()
- # process it?
+ tbfilter = False # prunetraceback already does it
+ if style == "auto":
+ style = "long"
+ # XXX should excinfo.getrepr record all data and toterminal() process it?
if style is None:
if self.config.option.tbstyle == "short":
style = "short"
else:
style = "long"
+
return excinfo.getrepr(funcargs=True,
showlocals=self.config.option.showlocals,
- style=style)
+ style=style, tbfilter=tbfilter)
repr_failure = _repr_failure_py
@@ -394,10 +414,6 @@ class Collector(Node):
and thus iteratively build a tree.
"""
- # the set of exceptions to interpret as "Skip the whole module" during
- # collection
- skip_exceptions = (Skipped,)
-
class CollectError(Exception):
""" an error during collection, contains a custom message. """
@@ -439,9 +455,7 @@ class FSCollector(Collector):
self.fspath = fspath
def _makeid(self):
- if self == self.session:
- return "."
- relpath = self.session.fspath.bestrelpath(self.fspath)
+ relpath = self.fspath.relto(self.config.rootdir)
if os.sep != "/":
relpath = relpath.replace(os.sep, "/")
return relpath
@@ -455,6 +469,14 @@ class Item(Node):
"""
nextitem = None
+ def __init__(self, name, parent=None, config=None, session=None):
+ super(Item, self).__init__(name, parent, config, session)
+ self._report_sections = []
+
+ def add_report_section(self, when, key, content):
+ if content:
+ self._report_sections.append((when, key, content))
+
def reportinfo(self):
return self.fspath, None, ""
@@ -478,39 +500,64 @@ class Item(Node):
class NoMatch(Exception):
""" raised if matching cannot locate a matching names. """
+class Interrupted(KeyboardInterrupt):
+ """ signals an interrupted test run. """
+ __module__ = 'builtins' # for py3
+
class Session(FSCollector):
- class Interrupted(KeyboardInterrupt):
- """ signals an interrupted test run. """
- __module__ = 'builtins' # for py3
+ Interrupted = Interrupted
def __init__(self, config):
- FSCollector.__init__(self, py.path.local(), parent=None,
+ FSCollector.__init__(self, config.rootdir, parent=None,
config=config, session=self)
- self.config.pluginmanager.register(self, name="session", prepend=True)
- self._testsfailed = 0
+ self._fs2hookproxy = {}
+ self.testsfailed = 0
+ self.testscollected = 0
self.shouldstop = False
self.trace = config.trace.root.get("collection")
self._norecursepatterns = config.getini("norecursedirs")
self.startdir = py.path.local()
+ self.config.pluginmanager.register(self, name="session")
+
+ def _makeid(self):
+ return ""
+ @pytest.hookimpl(tryfirst=True)
def pytest_collectstart(self):
if self.shouldstop:
raise self.Interrupted(self.shouldstop)
+ @pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'):
- self._testsfailed += 1
+ self.testsfailed += 1
maxfail = self.config.getvalue("maxfail")
- if maxfail and self._testsfailed >= maxfail:
+ if maxfail and self.testsfailed >= maxfail:
self.shouldstop = "stopping after %d failures" % (
- self._testsfailed)
+ self.testsfailed)
pytest_collectreport = pytest_runtest_logreport
def isinitpath(self, path):
return path in self._initialpaths
def gethookproxy(self, fspath):
- return HookProxy(fspath, self.config)
+ try:
+ return self._fs2hookproxy[fspath]
+ except KeyError:
+ # check if we have the common case of running
+ # hooks with all conftest.py filesall conftest.py
+ pm = self.config.pluginmanager
+ my_conftestmodules = pm._getconftestmodules(fspath)
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ if remove_mods:
+ # one or more conftests are not in use at this fspath
+ proxy = FSHookProxy(fspath, pm, remove_mods)
+ else:
+ # all plugis are active for this fspath
+ proxy = self.config.hook
+
+ self._fs2hookproxy[fspath] = proxy
+ return proxy
def perform_collect(self, args=None, genitems=True):
hook = self.config.hook
@@ -520,6 +567,7 @@ class Session(FSCollector):
config=self.config, items=items)
finally:
hook.pytest_collection_finish(session=self)
+ self.testscollected = len(items)
return items
def _perform_collect(self, args, genitems):
@@ -632,7 +680,7 @@ class Session(FSCollector):
arg = self._tryconvertpyarg(arg)
parts = str(arg).split("::")
relpath = parts[0].replace("/", os.sep)
- path = self.fspath.join(relpath, abs=True)
+ path = self.config.invocation_dir.join(relpath, abs=True)
if not path.check():
if self.config.option.pyargs:
msg = "file or package not found: "
@@ -670,7 +718,8 @@ class Session(FSCollector):
if rep.passed:
has_matched = False
for x in rep.result:
- if x.name == name:
+ # TODO: remove parametrized workaround once collection structure contains parametrization
+ if x.name == name or x.name.split("[")[0] == name:
resultnodes.extend(self.matchnodes([x], nextnames))
has_matched = True
# XXX accept IDs that don't have "()" for class instances
@@ -693,5 +742,3 @@ class Session(FSCollector):
for x in self.genitems(subnode):
yield x
node.ihook.pytest_collectreport(report=rep)
-
-
diff --git a/_pytest/mark.py b/_pytest/mark.py
index 6b66b18761..d8b60def36 100644
--- a/_pytest/mark.py
+++ b/_pytest/mark.py
@@ -1,5 +1,10 @@
""" generic mechanism for marking and selecting python functions. """
-import py
+import inspect
+
+
+class MarkerError(Exception):
+
+ """Error in use of a pytest marker/attribute."""
def pytest_namespace():
@@ -38,24 +43,30 @@ def pytest_addoption(parser):
def pytest_cmdline_main(config):
+ import _pytest.config
if config.option.markers:
- config.do_configure()
- tw = py.io.TerminalWriter()
+ config._do_configure()
+ tw = _pytest.config.create_terminal_writer(config)
for line in config.getini("markers"):
name, rest = line.split(":", 1)
tw.write("@pytest.mark.%s:" % name, bold=True)
tw.line(rest)
tw.line()
- config.do_unconfigure()
+ config._ensure_unconfigure()
return 0
pytest_cmdline_main.tryfirst = True
def pytest_collection_modifyitems(items, config):
- keywordexpr = config.option.keyword
+ keywordexpr = config.option.keyword.lstrip()
matchexpr = config.option.markexpr
if not keywordexpr and not matchexpr:
return
+ # pytest used to allow "-" for negating
+ # but today we just allow "-" at the beginning, use "not" instead
+ # we probably remove "-" alltogether soon
+ if keywordexpr.startswith("-"):
+ keywordexpr = "not " + keywordexpr[1:]
selectuntil = False
if keywordexpr[-1:] == ":":
selectuntil = True
@@ -122,7 +133,6 @@ def matchkeyword(colitem, keywordexpr):
Additionally, matches on names in the 'extra_keyword_matches' set of
any item, as well as names directly assigned to test functions.
"""
- keywordexpr = keywordexpr.replace("-", "not ")
mapped_names = set()
# Add the names of the current item and any parent items
@@ -159,7 +169,7 @@ class MarkGenerator:
""" Factory for :class:`MarkDecorator` objects - exposed as
a ``pytest.mark`` singleton instance. Example::
- import py
+ import pytest
@pytest.mark.slowtest
def test_function():
pass
@@ -244,15 +254,17 @@ class MarkDecorator:
otherwise add *args/**kwargs in-place to mark information. """
if args and not kwargs:
func = args[0]
- if len(args) == 1 and (istestfunc(func) or
- hasattr(func, '__bases__')):
- if hasattr(func, '__bases__'):
+ is_class = inspect.isclass(func)
+ if len(args) == 1 and (istestfunc(func) or is_class):
+ if is_class:
if hasattr(func, 'pytestmark'):
- l = func.pytestmark
- if not isinstance(l, list):
- func.pytestmark = [l, self]
- else:
- l.append(self)
+ mark_list = func.pytestmark
+ if not isinstance(mark_list, list):
+ mark_list = [mark_list]
+ # always work on a copy to avoid updating pytestmark
+ # from a superclass by accident
+ mark_list = mark_list + [self]
+ func.pytestmark = mark_list
else:
func.pytestmark = [self]
else:
@@ -279,7 +291,7 @@ class MarkInfo:
#: positional argument list, empty if none specified
self.args = args
#: keyword argument dictionary, empty if nothing specified
- self.kwargs = kwargs
+ self.kwargs = kwargs.copy()
self._arglist = [(args, kwargs.copy())]
def __repr__(self):
diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py
index be131bd933..d4c169d37a 100644
--- a/_pytest/monkeypatch.py
+++ b/_pytest/monkeypatch.py
@@ -1,8 +1,13 @@
""" monkeypatching and mocking functionality. """
import os, sys
+import re
+
from py.builtin import _basestring
+RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
+
+
def pytest_funcarg__monkeypatch(request):
"""The returned ``monkeypatch`` funcarg provides these
helper methods to modify objects, dictionaries or os.environ::
@@ -26,50 +31,79 @@ def pytest_funcarg__monkeypatch(request):
return mpatch
+def resolve(name):
+ # simplified from zope.dottedname
+ parts = name.split('.')
-def derive_importpath(import_path):
- import pytest
+ used = parts.pop(0)
+ found = __import__(used)
+ for part in parts:
+ used += '.' + part
+ try:
+ found = getattr(found, part)
+ except AttributeError:
+ pass
+ else:
+ continue
+ # we use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions on python 3
+ try:
+ __import__(used)
+ except ImportError as ex:
+ # str is used for py2 vs py3
+ expected = str(ex).split()[-1]
+ if expected == used:
+ raise
+ else:
+ raise ImportError(
+ 'import error in %s: %s' % (used, ex)
+ )
+ found = annotated_getattr(found, part, used)
+ return found
+
+
+def annotated_getattr(obj, name, ann):
+ try:
+ obj = getattr(obj, name)
+ except AttributeError:
+ raise AttributeError(
+ '%r object at %s has no attribute %r' % (
+ type(obj).__name__, ann, name
+ )
+ )
+ return obj
+
+
+def derive_importpath(import_path, raising):
if not isinstance(import_path, _basestring) or "." not in import_path:
raise TypeError("must be absolute import path string, not %r" %
(import_path,))
- rest = []
- target = import_path
- while target:
- try:
- obj = __import__(target, None, None, "__doc__")
- except ImportError:
- if "." not in target:
- __tracebackhide__ = True
- pytest.fail("could not import any sub part: %s" %
- import_path)
- target, name = target.rsplit(".", 1)
- rest.append(name)
- else:
- assert rest
- try:
- while len(rest) > 1:
- attr = rest.pop()
- obj = getattr(obj, attr)
- attr = rest[0]
- getattr(obj, attr)
- except AttributeError:
- __tracebackhide__ = True
- pytest.fail("object %r has no attribute %r" % (obj, attr))
- return attr, obj
+ module, attr = import_path.rsplit('.', 1)
+ target = resolve(module)
+ if raising:
+ annotated_getattr(target, attr, ann=module)
+ return attr, target
+class Notset:
+ def __repr__(self):
+ return "<notset>"
+
+
+notset = Notset()
-notset = object()
class monkeypatch:
- """ object keeping a record of setattr/item/env/syspath changes. """
+ """ Object keeping a record of setattr/item/env/syspath changes. """
+
def __init__(self):
self._setattr = []
self._setitem = []
self._cwd = None
+ self._savesyspath = None
def setattr(self, target, name, value=notset, raising=True):
- """ set attribute value on target, memorizing the old value.
+ """ Set attribute value on target, memorizing the old value.
By default raise AttributeError if the attribute did not exist.
For convenience you can specify a string as ``target`` which
@@ -88,31 +122,31 @@ class monkeypatch:
if value is notset:
if not isinstance(target, _basestring):
raise TypeError("use setattr(target, name, value) or "
- "setattr(target, value) with target being a dotted "
- "import string")
+ "setattr(target, value) with target being a dotted "
+ "import string")
value = name
- name, target = derive_importpath(target)
+ name, target = derive_importpath(target, raising)
oldval = getattr(target, name, notset)
if raising and oldval is notset:
- raise AttributeError("%r has no attribute %r" %(target, name))
+ raise AttributeError("%r has no attribute %r" % (target, name))
# avoid class descriptors like staticmethod/classmethod
if inspect.isclass(target):
oldval = target.__dict__.get(name, notset)
- self._setattr.insert(0, (target, name, oldval))
+ self._setattr.append((target, name, oldval))
setattr(target, name, value)
def delattr(self, target, name=notset, raising=True):
- """ delete attribute ``name`` from ``target``, by default raise
+ """ Delete attribute ``name`` from ``target``, by default raise
AttributeError it the attribute did not previously exist.
If no ``name`` is specified and ``target`` is a string
it will be interpreted as a dotted import path with the
last part being the attribute name.
- If raising is set to false, the attribute is allowed to not
- pre-exist.
+ If ``raising`` is set to False, no exception will be raised if the
+ attribute is missing.
"""
__tracebackhide__ = True
if name is notset:
@@ -120,32 +154,35 @@ class monkeypatch:
raise TypeError("use delattr(target, name) or "
"delattr(target) with target being a dotted "
"import string")
- name, target = derive_importpath(target)
+ name, target = derive_importpath(target, raising)
if not hasattr(target, name):
if raising:
raise AttributeError(name)
else:
- self._setattr.insert(0, (target, name,
- getattr(target, name, notset)))
+ self._setattr.append((target, name, getattr(target, name, notset)))
delattr(target, name)
def setitem(self, dic, name, value):
- """ set dictionary entry ``name`` to value. """
- self._setitem.insert(0, (dic, name, dic.get(name, notset)))
+ """ Set dictionary entry ``name`` to value. """
+ self._setitem.append((dic, name, dic.get(name, notset)))
dic[name] = value
def delitem(self, dic, name, raising=True):
- """ delete ``name`` from dict, raise KeyError if it doesn't exist."""
+ """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
+
+ If ``raising`` is set to False, no exception will be raised if the
+ key is missing.
+ """
if name not in dic:
if raising:
raise KeyError(name)
else:
- self._setitem.insert(0, (dic, name, dic.get(name, notset)))
+ self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name]
def setenv(self, name, value, prepend=None):
- """ set environment variable ``name`` to ``value``. if ``prepend``
+ """ Set environment variable ``name`` to ``value``. If ``prepend``
is a character, read the current environment variable value
and prepend the ``value`` adjoined with the ``prepend`` character."""
value = str(value)
@@ -154,18 +191,23 @@ class monkeypatch:
self.setitem(os.environ, name, value)
def delenv(self, name, raising=True):
- """ delete ``name`` from environment, raise KeyError it not exists."""
+ """ Delete ``name`` from the environment. Raise KeyError it does not
+ exist.
+
+ If ``raising`` is set to False, no exception will be raised if the
+ environment variable is missing.
+ """
self.delitem(os.environ, name, raising=raising)
def syspath_prepend(self, path):
- """ prepend ``path`` to ``sys.path`` list of import locations. """
- if not hasattr(self, '_savesyspath'):
+ """ Prepend ``path`` to ``sys.path`` list of import locations. """
+ if self._savesyspath is None:
self._savesyspath = sys.path[:]
sys.path.insert(0, str(path))
def chdir(self, path):
- """ change the current working directory to the specified path
- path can be a string or a py.path.local object
+ """ Change the current working directory to the specified path.
+ Path can be a string or a py.path.local object.
"""
if self._cwd is None:
self._cwd = os.getcwd()
@@ -175,27 +217,37 @@ class monkeypatch:
os.chdir(path)
def undo(self):
- """ undo previous changes. This call consumes the
- undo stack. Calling it a second time has no effect unless
- you do more monkeypatching after the undo call."""
- for obj, name, value in self._setattr:
+ """ Undo previous changes. This call consumes the
+ undo stack. Calling it a second time has no effect unless
+ you do more monkeypatching after the undo call.
+
+ There is generally no need to call `undo()`, since it is
+ called automatically during tear-down.
+
+ Note that the same `monkeypatch` fixture is used across a
+ single test function invocation. If `monkeypatch` is used both by
+ the test function itself and one of the test fixtures,
+ calling `undo()` will undo all of the changes made in
+ both functions.
+ """
+ for obj, name, value in reversed(self._setattr):
if value is not notset:
setattr(obj, name, value)
else:
delattr(obj, name)
self._setattr[:] = []
- for dictionary, name, value in self._setitem:
+ for dictionary, name, value in reversed(self._setitem):
if value is notset:
try:
del dictionary[name]
except KeyError:
- pass # was already deleted, so we have the desired state
+ pass # was already deleted, so we have the desired state
else:
dictionary[name] = value
self._setitem[:] = []
- if hasattr(self, '_savesyspath'):
+ if self._savesyspath is not None:
sys.path[:] = self._savesyspath
- del self._savesyspath
+ self._savesyspath = None
if self._cwd is not None:
os.chdir(self._cwd)
diff --git a/_pytest/nose.py b/_pytest/nose.py
index ecfc2316bb..0387468686 100644
--- a/_pytest/nose.py
+++ b/_pytest/nose.py
@@ -1,20 +1,30 @@
""" run test suites written for nose. """
-import pytest, py
import sys
+
+import py
+import pytest
from _pytest import unittest
-def pytest_runtest_makereport(__multicall__, item, call):
- SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
- if SkipTest:
- if call.excinfo and call.excinfo.errisinstance(SkipTest):
- # let's substitute the excinfo with a pytest.skip one
- call2 = call.__class__(lambda:
- pytest.skip(str(call.excinfo.value)), call.when)
- call.excinfo = call2.excinfo
+def get_skip_exceptions():
+ skip_classes = set()
+ for module_name in ('unittest', 'unittest2', 'nose'):
+ mod = sys.modules.get(module_name)
+ if hasattr(mod, 'SkipTest'):
+ skip_classes.add(mod.SkipTest)
+ return tuple(skip_classes)
+
+
+def pytest_runtest_makereport(item, call):
+ if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
+ # let's substitute the excinfo with a pytest.skip one
+ call2 = call.__class__(lambda:
+ pytest.skip(str(call.excinfo.value)), call.when)
+ call.excinfo = call2.excinfo
-@pytest.mark.trylast
+
+@pytest.hookimpl(trylast=True)
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):
@@ -38,13 +48,8 @@ def teardown_nose(item):
# #call_optional(item._nosegensetup, 'teardown')
# del item.parent._nosegensetup
+
def pytest_make_collect_report(collector):
- SkipTest = getattr(sys.modules.get('unittest', None), 'SkipTest', None)
- if SkipTest is not None:
- collector.skip_exceptions += (SkipTest,)
- SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
- if SkipTest is not None:
- collector.skip_exceptions += (SkipTest,)
if isinstance(collector, pytest.Generator):
call_optional(collector.obj, 'setup')
diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py
index cc9d0b5f05..4ec62d0228 100644
--- a/_pytest/pastebin.py
+++ b/_pytest/pastebin.py
@@ -1,10 +1,8 @@
""" submit failure or test session information to a pastebin service. """
-import py, sys
+import pytest
+import sys
+import tempfile
-class url:
- base = "http://bpaste.net"
- xmlrpc = base + "/xmlrpc/"
- show = base + "/show/"
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
@@ -13,55 +11,82 @@ def pytest_addoption(parser):
choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.")
-def pytest_configure(__multicall__, config):
- import tempfile
- __multicall__.execute()
+@pytest.hookimpl(trylast=True)
+def pytest_configure(config):
+ import py
if config.option.pastebin == "all":
- config._pastebinfile = tempfile.TemporaryFile('w+')
tr = config.pluginmanager.getplugin('terminalreporter')
- oldwrite = tr._tw.write
- def tee_write(s, **kwargs):
- oldwrite(s, **kwargs)
- config._pastebinfile.write(str(s))
- tr._tw.write = tee_write
+ # if no terminal reporter plugin is present, nothing we can do here;
+ # this can happen when this function executes in a slave node
+ # when using pytest-xdist, for example
+ if tr is not None:
+ # pastebin file will be utf-8 encoded binary file
+ config._pastebinfile = tempfile.TemporaryFile('w+b')
+ oldwrite = tr._tw.write
+ def tee_write(s, **kwargs):
+ oldwrite(s, **kwargs)
+ if py.builtin._istext(s):
+ s = s.encode('utf-8')
+ config._pastebinfile.write(s)
+ tr._tw.write = tee_write
def pytest_unconfigure(config):
if hasattr(config, '_pastebinfile'):
+ # get terminal contents and delete file
config._pastebinfile.seek(0)
sessionlog = config._pastebinfile.read()
config._pastebinfile.close()
del config._pastebinfile
- proxyid = getproxy().newPaste("python", sessionlog)
- pastebinurl = "%s%s" % (url.show, proxyid)
- sys.stderr.write("pastebin session-log: %s\n" % pastebinurl)
+ # undo our patching in the terminal reporter
tr = config.pluginmanager.getplugin('terminalreporter')
del tr._tw.__dict__['write']
+ # write summary
+ tr.write_sep("=", "Sending information to Paste Service")
+ pastebinurl = create_new_paste(sessionlog)
+ tr.write_line("pastebin session-log: %s\n" % pastebinurl)
-def getproxy():
+def create_new_paste(contents):
+ """
+ Creates a new paste using bpaste.net service.
+
+ :contents: paste contents as utf-8 encoded bytes
+ :returns: url to the pasted contents
+ """
+ import re
if sys.version_info < (3, 0):
- from xmlrpclib import ServerProxy
+ from urllib import urlopen, urlencode
+ else:
+ from urllib.request import urlopen
+ from urllib.parse import urlencode
+
+ params = {
+ 'code': contents,
+ 'lexer': 'python3' if sys.version_info[0] == 3 else 'python',
+ 'expiry': '1week',
+ }
+ url = 'https://bpaste.net'
+ response = urlopen(url, data=urlencode(params).encode('ascii')).read()
+ m = re.search(r'href="/raw/(\w+)"', response.decode('utf-8'))
+ if m:
+ return '%s/show/%s' % (url, m.group(1))
else:
- from xmlrpc.client import ServerProxy
- return ServerProxy(url.xmlrpc).pastes
+ return 'bad response: ' + response
def pytest_terminal_summary(terminalreporter):
+ import _pytest.config
if terminalreporter.config.option.pastebin != "failed":
return
tr = terminalreporter
if 'failed' in tr.stats:
terminalreporter.write_sep("=", "Sending information to Paste Service")
- if tr.config.option.debug:
- terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,))
- serverproxy = getproxy()
for rep in terminalreporter.stats.get('failed'):
try:
msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
except AttributeError:
msg = tr._getfailureheadline(rep)
- tw = py.io.TerminalWriter(stringio=True)
+ tw = _pytest.config.create_terminal_writer(terminalreporter.config, stringio=True)
rep.toterminal(tw)
s = tw.stringio.getvalue()
assert len(s)
- proxyid = serverproxy.newPaste("python", s)
- pastebinurl = "%s%s" % (url.show, proxyid)
+ pastebinurl = create_new_paste(s)
tr.write_line("%s --> %s" %(msg, pastebinurl))
diff --git a/_pytest/pdb.py b/_pytest/pdb.py
index 6405773f8e..84c920d172 100644
--- a/_pytest/pdb.py
+++ b/_pytest/pdb.py
@@ -1,8 +1,11 @@
""" interactive debugging with PDB, the Python Debugger. """
-
-import pytest, py
+from __future__ import absolute_import
+import pdb
import sys
+import pytest
+
+
def pytest_addoption(parser):
group = parser.getgroup("general")
group._addoption('--pdb',
@@ -16,50 +19,43 @@ def pytest_configure(config):
if config.getvalue("usepdb"):
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
- old_trace = py.std.pdb.set_trace
+ old = (pdb.set_trace, pytestPDB._pluginmanager)
def fin():
- py.std.pdb.set_trace = old_trace
- py.std.pdb.set_trace = pytest.set_trace
+ pdb.set_trace, pytestPDB._pluginmanager = old
+ pytestPDB._config = None
+ pdb.set_trace = pytest.set_trace
+ pytestPDB._pluginmanager = config.pluginmanager
+ pytestPDB._config = config
config._cleanup.append(fin)
class pytestPDB:
""" Pseudo PDB that defers to the real pdb. """
- item = None
- collector = None
+ _pluginmanager = None
+ _config = None
def set_trace(self):
""" invoke PDB set_trace debugging, dropping any IO capturing. """
+ import _pytest.config
frame = sys._getframe().f_back
- item = self.item or self.collector
-
- if item is not None:
- capman = item.config.pluginmanager.getplugin("capturemanager")
- out, err = capman.suspendcapture()
- if hasattr(item, 'outerr'):
- item.outerr = (item.outerr[0] + out, item.outerr[1] + err)
- tw = py.io.TerminalWriter()
+ if self._pluginmanager is not None:
+ capman = self._pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspendcapture(in_=True)
+ tw = _pytest.config.create_terminal_writer(self._config)
tw.line()
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
- py.std.pdb.Pdb().set_trace(frame)
-
-def pdbitem(item):
- pytestPDB.item = item
-pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem
-
-@pytest.mark.tryfirst
-def pytest_make_collect_report(__multicall__, collector):
- try:
- pytestPDB.collector = collector
- return __multicall__.execute()
- finally:
- pytestPDB.collector = None
+ self._pluginmanager.hook.pytest_enter_pdb(config=self._config)
+ pdb.Pdb().set_trace(frame)
-def pytest_runtest_makereport():
- pytestPDB.item = None
class PdbInvoke:
def pytest_exception_interact(self, node, call, report):
- return _enter_pdb(node, call.excinfo, report)
+ capman = node.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ out, err = capman.suspendcapture(in_=True)
+ sys.stdout.write(out)
+ sys.stdout.write(err)
+ _enter_pdb(node, call.excinfo, report)
def pytest_internalerror(self, excrepr, excinfo):
for line in str(excrepr).split("\n"):
@@ -87,7 +83,8 @@ def _enter_pdb(node, excinfo, rep):
def _postmortem_traceback(excinfo):
# A doctest.UnexpectedException is not useful for post_mortem.
# Use the underlying exception instead:
- if isinstance(excinfo.value, py.std.doctest.UnexpectedException):
+ from doctest import UnexpectedException
+ if isinstance(excinfo.value, UnexpectedException):
return excinfo.value.exc_info[2]
else:
return excinfo._excinfo[2]
@@ -101,7 +98,6 @@ def _find_last_non_hidden_frame(stack):
def post_mortem(t):
- pdb = py.std.pdb
class Pdb(pdb.Pdb):
def get_stack(self, f, t):
stack, i = pdb.Pdb.get_stack(self, f, t)
diff --git a/_pytest/pytester.py b/_pytest/pytester.py
index 5442a13987..faed7f581c 100644
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -1,27 +1,34 @@
""" (disabled by default) support for testing pytest and pytest plugins. """
-
-import py, pytest
-import sys, os
import codecs
+import gc
+import os
+import platform
import re
+import subprocess
+import sys
import time
+import traceback
from fnmatch import fnmatch
-from _pytest.main import Session, EXIT_OK
-from py.builtin import print_
-from _pytest.core import HookRelay
+from py.builtin import print_
-def get_public_names(l):
- """Only return names from iterator l without a leading underscore."""
- return [x for x in l if x[0] != "_"]
+from _pytest._code import Source
+import py
+import pytest
+from _pytest.main import Session, EXIT_OK
def pytest_addoption(parser):
- group = parser.getgroup("pylib")
- group.addoption('--no-tools-on-path',
- action="store_true", dest="notoolsonpath", default=False,
- help=("discover tools on PATH instead of going through py.cmdline.")
- )
+ # group = parser.getgroup("pytester", "pytester (self-tests) options")
+ parser.addoption('--lsof',
+ action="store_true", dest="lsof", default=False,
+ help=("run FD checks if lsof is available"))
+
+ parser.addoption('--runpytest', default="inprocess", dest="runpytest",
+ choices=("inprocess", "subprocess", ),
+ help=("run pytest sub runs in tests using an 'inprocess' "
+ "or 'subprocess' (python -m main) method"))
+
def pytest_configure(config):
# This might be called multiple times. Only take the first.
@@ -32,7 +39,124 @@ def pytest_configure(config):
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
-def pytest_funcarg___pytest(request):
+ if config.getvalue("lsof"):
+ checker = LsofFdLeakChecker()
+ if checker.matching_platform():
+ config.pluginmanager.register(checker)
+
+
+class LsofFdLeakChecker(object):
+ def get_open_files(self):
+ out = self._exec_lsof()
+ open_files = self._parse_lsof_output(out)
+ return open_files
+
+ def _exec_lsof(self):
+ pid = os.getpid()
+ return py.process.cmdexec("lsof -Ffn0 -p %d" % pid)
+
+ def _parse_lsof_output(self, out):
+ def isopen(line):
+ return line.startswith('f') and ("deleted" not in line and
+ 'mem' not in line and "txt" not in line and 'cwd' not in line)
+
+ open_files = []
+
+ for line in out.split("\n"):
+ if isopen(line):
+ fields = line.split('\0')
+ fd = fields[0][1:]
+ filename = fields[1][1:]
+ if filename.startswith('/'):
+ open_files.append((fd, filename))
+
+ return open_files
+
+ def matching_platform(self):
+ try:
+ py.process.cmdexec("lsof -v")
+ except (py.process.cmdexec.Error, UnicodeDecodeError):
+ # cmdexec may raise UnicodeDecodeError on Windows systems
+ # with locale other than english:
+ # https://bitbucket.org/pytest-dev/py/issues/66
+ return False
+ else:
+ return True
+
+ @pytest.hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_runtest_item(self, item):
+ lines1 = self.get_open_files()
+ yield
+ if hasattr(sys, "pypy_version_info"):
+ gc.collect()
+ lines2 = self.get_open_files()
+
+ new_fds = set([t[0] for t in lines2]) - set([t[0] for t in lines1])
+ leaked_files = [t for t in lines2 if t[0] in new_fds]
+ if leaked_files:
+ error = []
+ error.append("***** %s FD leakage detected" % len(leaked_files))
+ error.extend([str(f) for f in leaked_files])
+ error.append("*** Before:")
+ error.extend([str(f) for f in lines1])
+ error.append("*** After:")
+ error.extend([str(f) for f in lines2])
+ error.append(error[0])
+ error.append("*** function %s:%s: %s " % item.location)
+ pytest.fail("\n".join(error), pytrace=False)
+
+
+# XXX copied from execnet's conftest.py - needs to be merged
+winpymap = {
+ 'python2.7': r'C:\Python27\python.exe',
+ 'python2.6': r'C:\Python26\python.exe',
+ 'python3.1': r'C:\Python31\python.exe',
+ 'python3.2': r'C:\Python32\python.exe',
+ 'python3.3': r'C:\Python33\python.exe',
+ 'python3.4': r'C:\Python34\python.exe',
+ 'python3.5': r'C:\Python35\python.exe',
+}
+
+def getexecutable(name, cache={}):
+ try:
+ return cache[name]
+ except KeyError:
+ executable = py.path.local.sysfind(name)
+ if executable:
+ if name == "jython":
+ import subprocess
+ popen = subprocess.Popen([str(executable), "--version"],
+ universal_newlines=True, stderr=subprocess.PIPE)
+ out, err = popen.communicate()
+ if not err or "2.5" not in err:
+ executable = None
+ if "2.5.2" in err:
+ executable = None # http://bugs.jython.org/issue1790
+ cache[name] = executable
+ return executable
+
+@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4",
+ 'pypy', 'pypy3'])
+def anypython(request):
+ name = request.param
+ executable = getexecutable(name)
+ if executable is None:
+ if sys.platform == "win32":
+ executable = winpymap.get(name, None)
+ if executable:
+ executable = py.path.local(executable)
+ if executable.check():
+ return executable
+ pytest.skip("no suitable %s found" % (name,))
+ return executable
+
+# used at least by pytest-xdist plugin
+@pytest.fixture
+def _pytest(request):
+ """ Return a helper which offers a gethookrecorder(hook)
+ method which returns a HookRecorder instance which helps
+ to make assertions about called hooks.
+ """
return PytestArg(request)
class PytestArg:
@@ -41,15 +165,18 @@ class PytestArg:
def gethookrecorder(self, hook):
hookrecorder = HookRecorder(hook._pm)
- hookrecorder.start_recording(hook._hookspecs)
self.request.addfinalizer(hookrecorder.finish_recording)
return hookrecorder
+
+def get_public_names(l):
+ """Only return names from iterator l without a leading underscore."""
+ return [x for x in l if x[0] != "_"]
+
+
class ParsedCall:
- def __init__(self, name, locals):
- assert '_name' not in locals
- self.__dict__.update(locals)
- self.__dict__.pop('self')
+ def __init__(self, name, kwargs):
+ self.__dict__.update(kwargs)
self._name = name
def __repr__(self):
@@ -57,72 +184,40 @@ class ParsedCall:
del d['_name']
return "<ParsedCall %r(**%r)>" %(self._name, d)
+
class HookRecorder:
+ """Record all hooks called in a plugin manager.
+
+ This wraps all the hook calls in the plugin manager, recording
+ each call before propagating the normal calls.
+
+ """
+
def __init__(self, pluginmanager):
self._pluginmanager = pluginmanager
self.calls = []
- self._recorders = {}
-
- def start_recording(self, hookspecs):
- if not isinstance(hookspecs, (list, tuple)):
- hookspecs = [hookspecs]
- for hookspec in hookspecs:
- assert hookspec not in self._recorders
- class RecordCalls:
- _recorder = self
- for name, method in vars(hookspec).items():
- if name[0] != "_":
- setattr(RecordCalls, name, self._makecallparser(method))
- recorder = RecordCalls()
- self._recorders[hookspec] = recorder
- self._pluginmanager.register(recorder)
- self.hook = HookRelay(hookspecs, pm=self._pluginmanager,
- prefix="pytest_")
+
+ def before(hook_name, hook_impls, kwargs):
+ self.calls.append(ParsedCall(hook_name, kwargs))
+
+ def after(outcome, hook_name, hook_impls, kwargs):
+ pass
+
+ self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
def finish_recording(self):
- for recorder in self._recorders.values():
- if self._pluginmanager.isregistered(recorder):
- self._pluginmanager.unregister(recorder)
- self._recorders.clear()
-
- def _makecallparser(self, method):
- name = method.__name__
- args, varargs, varkw, default = py.std.inspect.getargspec(method)
- if not args or args[0] != "self":
- args.insert(0, 'self')
- fspec = py.std.inspect.formatargspec(args, varargs, varkw, default)
- # we use exec because we want to have early type
- # errors on wrong input arguments, using
- # *args/**kwargs delays this and gives errors
- # elsewhere
- exec (py.code.compile("""
- def %(name)s%(fspec)s:
- self._recorder.calls.append(
- ParsedCall(%(name)r, locals()))
- """ % locals()))
- return locals()[name]
+ self._undo_wrapping()
def getcalls(self, names):
if isinstance(names, str):
names = names.split()
- for name in names:
- for cls in self._recorders:
- if name in vars(cls):
- break
- else:
- raise ValueError("callname %r not found in %r" %(
- name, self._recorders.keys()))
- l = []
- for call in self.calls:
- if call._name in names:
- l.append(call)
- return l
+ return [call for call in self.calls if call._name in names]
- def contains(self, entries):
+ def assert_contains(self, entries):
__tracebackhide__ = True
i = 0
entries = list(entries)
- backlocals = py.std.sys._getframe(1).f_locals
+ backlocals = sys._getframe(1).f_locals
while entries:
name, check = entries.pop(0)
for ind, call in enumerate(self.calls[i:]):
@@ -154,19 +249,100 @@ class HookRecorder:
assert len(l) == 1, (name, l)
return l[0]
+ # functionality for test reports
+
+ def getreports(self,
+ names="pytest_runtest_logreport pytest_collectreport"):
+ return [x.report for x in self.getcalls(names)]
+
+ def matchreport(self, inamepart="",
+ names="pytest_runtest_logreport pytest_collectreport", when=None):
+ """ return a testreport whose dotted import path matches """
+ l = []
+ for rep in self.getreports(names=names):
+ try:
+ if not when and rep.when != "call" and rep.passed:
+ # setup/teardown passing reports - let's ignore those
+ continue
+ except AttributeError:
+ pass
+ if when and getattr(rep, 'when', None) != when:
+ continue
+ if not inamepart or inamepart in rep.nodeid.split("::"):
+ l.append(rep)
+ if not l:
+ raise ValueError("could not find test report matching %r: "
+ "no test reports at all!" % (inamepart,))
+ if len(l) > 1:
+ raise ValueError(
+ "found 2 or more testreports matching %r: %s" %(inamepart, l))
+ return l[0]
+
+ def getfailures(self,
+ names='pytest_runtest_logreport pytest_collectreport'):
+ return [rep for rep in self.getreports(names) if rep.failed]
+
+ def getfailedcollections(self):
+ return self.getfailures('pytest_collectreport')
-def pytest_funcarg__linecomp(request):
+ def listoutcomes(self):
+ passed = []
+ skipped = []
+ failed = []
+ for rep in self.getreports(
+ "pytest_collectreport pytest_runtest_logreport"):
+ if rep.passed:
+ if getattr(rep, "when", None) == "call":
+ passed.append(rep)
+ elif rep.skipped:
+ skipped.append(rep)
+ elif rep.failed:
+ failed.append(rep)
+ return passed, skipped, failed
+
+ def countoutcomes(self):
+ return [len(x) for x in self.listoutcomes()]
+
+ def assertoutcome(self, passed=0, skipped=0, failed=0):
+ realpassed, realskipped, realfailed = self.listoutcomes()
+ assert passed == len(realpassed)
+ assert skipped == len(realskipped)
+ assert failed == len(realfailed)
+
+ def clear(self):
+ self.calls[:] = []
+
+
+@pytest.fixture
+def linecomp(request):
return LineComp()
+
def pytest_funcarg__LineMatcher(request):
return LineMatcher
-def pytest_funcarg__testdir(request):
- tmptestdir = TmpTestdir(request)
- return tmptestdir
-rex_outcome = re.compile("(\d+) (\w+)")
+@pytest.fixture
+def testdir(request, tmpdir_factory):
+ return Testdir(request, tmpdir_factory)
+
+
+rex_outcome = re.compile("(\d+) ([\w-]+)")
class RunResult:
+ """The result of running a command.
+
+ Attributes:
+
+ :ret: The return value.
+ :outlines: List of lines captured from stdout.
+ :errlines: List of lines captures from stderr.
+ :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
+ reconstruct stdout or the commonly used
+ ``stdout.fnmatch_lines()`` method.
+ :stderrr: :py:class:`LineMatcher` of stderr.
+ :duration: Duration in seconds.
+
+ """
def __init__(self, ret, outlines, errlines, duration):
self.ret = ret
self.outlines = outlines
@@ -176,6 +352,8 @@ class RunResult:
self.duration = duration
def parseoutcomes(self):
+ """ Return a dictionary of outcomestring->num from parsing
+ the terminal output that the test process produced."""
for line in reversed(self.outlines):
if 'seconds' in line:
outcomes = rex_outcome.findall(line)
@@ -185,13 +363,41 @@ class RunResult:
d[cat] = int(num)
return d
-class TmpTestdir:
- def __init__(self, request):
+ def assert_outcomes(self, passed=0, skipped=0, failed=0):
+ """ assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run."""
+ d = self.parseoutcomes()
+ assert passed == d.get("passed", 0)
+ assert skipped == d.get("skipped", 0)
+ assert failed == d.get("failed", 0)
+
+
+
+class Testdir:
+ """Temporary test directory with tools to test/run py.test itself.
+
+ This is based on the ``tmpdir`` fixture but provides a number of
+ methods which aid with testing py.test itself. Unless
+ :py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
+ current working directory.
+
+ Attributes:
+
+ :tmpdir: The :py:class:`py.path.local` instance of the temporary
+ directory.
+
+ :plugins: A list of plugins to use with :py:meth:`parseconfig` and
+ :py:meth:`runpytest`. Initially this is an empty list but
+ plugins can be added to the list. The type of items to add to
+ the list depend on the method which uses them so refer to them
+ for details.
+
+ """
+
+ def __init__(self, request, tmpdir_factory):
self.request = request
- self.Config = request.config.__class__
- self._pytest = request.getfuncargvalue("_pytest")
# XXX remove duplication with tmpdir plugin
- basetmp = request.config._tmpdirhandler.ensuretemp("testdir")
+ basetmp = tmpdir_factory.ensuretemp("testdir")
name = request.function.__name__
for i in range(100):
try:
@@ -201,37 +407,58 @@ class TmpTestdir:
break
self.tmpdir = tmpdir
self.plugins = []
- self._syspathremove = []
+ self._savesyspath = (list(sys.path), list(sys.meta_path))
+ self._savemodulekeys = set(sys.modules)
self.chdir() # always chdir
self.request.addfinalizer(self.finalize)
+ method = self.request.config.getoption("--runpytest")
+ if method == "inprocess":
+ self._runpytest_method = self.runpytest_inprocess
+ elif method == "subprocess":
+ self._runpytest_method = self.runpytest_subprocess
def __repr__(self):
- return "<TmpTestdir %r>" % (self.tmpdir,)
+ return "<Testdir %r>" % (self.tmpdir,)
def finalize(self):
- for p in self._syspathremove:
- py.std.sys.path.remove(p)
+ """Clean up global state artifacts.
+
+ Some methods modify the global interpreter state and this
+ tries to clean this up. It does not remove the temporary
+ directory however so it can be looked at after the test run
+ has finished.
+
+ """
+ sys.path[:], sys.meta_path[:] = self._savesyspath
if hasattr(self, '_olddir'):
self._olddir.chdir()
- # delete modules that have been loaded from tmpdir
- for name, mod in list(sys.modules.items()):
- if mod:
- fn = getattr(mod, '__file__', None)
- if fn and fn.startswith(str(self.tmpdir)):
- del sys.modules[name]
-
- def getreportrecorder(self, obj):
- if hasattr(obj, 'config'):
- obj = obj.config
- if hasattr(obj, 'hook'):
- obj = obj.hook
- assert hasattr(obj, '_hookspecs'), obj
- reprec = ReportRecorder(obj)
- reprec.hookrecorder = self._pytest.gethookrecorder(obj)
- reprec.hook = reprec.hookrecorder.hook
+ self.delete_loaded_modules()
+
+ def delete_loaded_modules(self):
+ """Delete modules that have been loaded during a test.
+
+ This allows the interpreter to catch module changes in case
+ the module is re-imported.
+ """
+ for name in set(sys.modules).difference(self._savemodulekeys):
+ # it seems zope.interfaces is keeping some state
+ # (used by twisted related tests)
+ if name != "zope.interface":
+ del sys.modules[name]
+
+ def make_hook_recorder(self, pluginmanager):
+ """Create a new :py:class:`HookRecorder` for a PluginManager."""
+ assert not hasattr(pluginmanager, "reprec")
+ pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
+ self.request.addfinalizer(reprec.finish_recording)
return reprec
def chdir(self):
+ """Cd into the temporary directory.
+
+ This is done automatically upon instantiation.
+
+ """
old = self.tmpdir.chdir()
if not hasattr(self, '_olddir'):
self._olddir = old
@@ -246,60 +473,127 @@ class TmpTestdir:
ret = None
for name, value in items:
p = self.tmpdir.join(name).new(ext=ext)
- source = py.builtin._totext(py.code.Source(value)).strip()
- content = source.encode("utf-8") # + "\n"
+ source = Source(value)
+ def my_totext(s, encoding="utf-8"):
+ if py.builtin._isbytes(s):
+ s = py.builtin._totext(s, encoding=encoding)
+ return s
+ source_unicode = "\n".join([my_totext(line) for line in source.lines])
+ source = py.builtin._totext(source_unicode)
+ content = source.strip().encode("utf-8") # + "\n"
#content = content.rstrip() + "\n"
p.write(content, "wb")
if ret is None:
ret = p
return ret
-
def makefile(self, ext, *args, **kwargs):
+ """Create a new file in the testdir.
+
+ ext: The extension the file should use, including the dot.
+ E.g. ".py".
+
+ args: All args will be treated as strings and joined using
+ newlines. The result will be written as contents to the
+ file. The name of the file will be based on the test
+ function requesting this fixture.
+ E.g. "testdir.makefile('.txt', 'line1', 'line2')"
+
+ kwargs: Each keyword is the name of a file, while the value of
+ it will be written as contents of the file.
+ E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
+
+ """
return self._makefile(ext, args, kwargs)
def makeconftest(self, source):
+ """Write a contest.py file with 'source' as contents."""
return self.makepyfile(conftest=source)
def makeini(self, source):
+ """Write a tox.ini file with 'source' as contents."""
return self.makefile('.ini', tox=source)
def getinicfg(self, source):
+ """Return the pytest section from the tox.ini config file."""
p = self.makeini(source)
return py.iniconfig.IniConfig(p)['pytest']
def makepyfile(self, *args, **kwargs):
+ """Shortcut for .makefile() with a .py extension."""
return self._makefile('.py', args, kwargs)
def maketxtfile(self, *args, **kwargs):
+ """Shortcut for .makefile() with a .txt extension."""
return self._makefile('.txt', args, kwargs)
def syspathinsert(self, path=None):
+ """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
+
+ This is undone automatically after the test.
+ """
if path is None:
path = self.tmpdir
- py.std.sys.path.insert(0, str(path))
- self._syspathremove.append(str(path))
+ sys.path.insert(0, str(path))
+ # a call to syspathinsert() usually means that the caller
+ # wants to import some dynamically created files.
+ # with python3 we thus invalidate import caches.
+ self._possibly_invalidate_import_caches()
+
+ def _possibly_invalidate_import_caches(self):
+ # invalidate caches if we can (py33 and above)
+ try:
+ import importlib
+ except ImportError:
+ pass
+ else:
+ if hasattr(importlib, "invalidate_caches"):
+ importlib.invalidate_caches()
def mkdir(self, name):
+ """Create a new (sub)directory."""
return self.tmpdir.mkdir(name)
def mkpydir(self, name):
+ """Create a new python package.
+
+ This creates a (sub)direcotry with an empty ``__init__.py``
+ file so that is recognised as a python package.
+
+ """
p = self.mkdir(name)
p.ensure("__init__.py")
return p
Session = Session
def getnode(self, config, arg):
+ """Return the collection node of a file.
+
+ :param config: :py:class:`_pytest.config.Config` instance, see
+ :py:meth:`parseconfig` and :py:meth:`parseconfigure` to
+ create the configuration.
+
+ :param arg: A :py:class:`py.path.local` instance of the file.
+
+ """
session = Session(config)
assert '::' not in str(arg)
p = py.path.local(arg)
- x = session.fspath.bestrelpath(p)
config.hook.pytest_sessionstart(session=session)
- res = session.perform_collect([x], genitems=False)[0]
+ res = session.perform_collect([str(p)], genitems=False)[0]
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
return res
def getpathnode(self, path):
+ """Return the collection node of a file.
+
+ This is like :py:meth:`getnode` but uses
+ :py:meth:`parseconfigure` to create the (configured) py.test
+ Config instance.
+
+ :param path: A :py:class:`py.path.local` instance of the file.
+
+ """
config = self.parseconfigure(path)
session = Session(config)
x = session.fspath.bestrelpath(path)
@@ -309,6 +603,12 @@ class TmpTestdir:
return res
def genitems(self, colitems):
+ """Generate all test items from a collection node.
+
+ This recurses into the collection node and returns a list of
+ all the test items contained within.
+
+ """
session = colitems[0].session
result = []
for colitem in colitems:
@@ -316,6 +616,14 @@ class TmpTestdir:
return result
def runitem(self, source):
+ """Run the "test_func" Item.
+
+ The calling test instance (the class which contains the test
+ method) must provide a ``.getrunner()`` method which should
+ return a runner which can run the test protocol for a single
+ item, like e.g. :py:func:`_pytest.runner.runtestprotocol`.
+
+ """
# used from runner functional tests
item = self.getitem(source)
# the test class where we are called from wants to provide the runner
@@ -324,72 +632,176 @@ class TmpTestdir:
return runner(item)
def inline_runsource(self, source, *cmdlineargs):
+ """Run a test module in process using ``pytest.main()``.
+
+ This run writes "source" into a temporary file and runs
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder`
+ instance for the result.
+
+ :param source: The source code of the test module.
+
+ :param cmdlineargs: Any extra command line arguments to use.
+
+ :return: :py:class:`HookRecorder` instance of the result.
+
+ """
p = self.makepyfile(source)
l = list(cmdlineargs) + [p]
return self.inline_run(*l)
- def inline_runsource1(self, *args):
- args = list(args)
- source = args.pop()
- p = self.makepyfile(source)
- l = list(args) + [p]
- reprec = self.inline_run(*l)
- reports = reprec.getreports("pytest_runtest_logreport")
- assert len(reports) == 3, reports # setup/call/teardown
- return reports[1]
-
def inline_genitems(self, *args):
- return self.inprocess_run(list(args) + ['--collectonly'])
+ """Run ``pytest.main(['--collectonly'])`` in-process.
+
+ Retuns a tuple of the collected items and a
+ :py:class:`HookRecorder` instance.
+
+ This runs the :py:func:`pytest.main` function to run all of
+ py.test inside the test process itself like
+ :py:meth:`inline_run`. However the return value is a tuple of
+ the collection items and a :py:class:`HookRecorder` instance.
+
+ """
+ rec = self.inline_run("--collect-only", *args)
+ items = [x.item for x in rec.getcalls("pytest_itemcollected")]
+ return items, rec
- def inline_run(self, *args):
- items, rec = self.inprocess_run(args)
- return rec
+ def inline_run(self, *args, **kwargs):
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
- def inprocess_run(self, args, plugins=None):
+ This runs the :py:func:`pytest.main` function to run all of
+ py.test inside the test process itself. This means it can
+ return a :py:class:`HookRecorder` instance which gives more
+ detailed results from then run then can be done by matching
+ stdout/stderr from :py:meth:`runpytest`.
+
+ :param args: Any command line arguments to pass to
+ :py:func:`pytest.main`.
+
+ :param plugin: (keyword-only) Extra plugin instances the
+ ``pytest.main()`` instance should use.
+
+ :return: A :py:class:`HookRecorder` instance.
+
+ """
rec = []
- items = []
class Collect:
def pytest_configure(x, config):
- rec.append(self.getreportrecorder(config))
- def pytest_itemcollected(self, item):
- items.append(item)
- if not plugins:
- plugins = []
+ rec.append(self.make_hook_recorder(config.pluginmanager))
+
+ plugins = kwargs.get("plugins") or []
plugins.append(Collect())
ret = pytest.main(list(args), plugins=plugins)
- reprec = rec[0]
+ self.delete_loaded_modules()
+ if len(rec) == 1:
+ reprec = rec.pop()
+ else:
+ class reprec:
+ pass
reprec.ret = ret
- assert len(rec) == 1
- return items, reprec
- def parseconfig(self, *args):
+ # typically we reraise keyboard interrupts from the child run
+ # because it's our user requesting interruption of the testing
+ if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
+ calls = reprec.getcalls("pytest_keyboard_interrupt")
+ if calls and calls[-1].excinfo.type == KeyboardInterrupt:
+ raise KeyboardInterrupt()
+ return reprec
+
+ def runpytest_inprocess(self, *args, **kwargs):
+ """ Return result of running pytest in-process, providing a similar
+ interface to what self.runpytest() provides. """
+ if kwargs.get("syspathinsert"):
+ self.syspathinsert()
+ now = time.time()
+ capture = py.io.StdCapture()
+ try:
+ try:
+ reprec = self.inline_run(*args, **kwargs)
+ except SystemExit as e:
+ class reprec:
+ ret = e.args[0]
+ except Exception:
+ traceback.print_exc()
+ class reprec:
+ ret = 3
+ finally:
+ out, err = capture.reset()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ res = RunResult(reprec.ret,
+ out.split("\n"), err.split("\n"),
+ time.time()-now)
+ res.reprec = reprec
+ return res
+
+ def runpytest(self, *args, **kwargs):
+ """ Run pytest inline or in a subprocess, depending on the command line
+ option "--runpytest" and return a :py:class:`RunResult`.
+
+ """
+ args = self._ensure_basetemp(args)
+ return self._runpytest_method(*args, **kwargs)
+
+ def _ensure_basetemp(self, args):
args = [str(x) for x in args]
for x in args:
if str(x).startswith('--basetemp'):
+ #print ("basedtemp exists: %s" %(args,))
break
else:
args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
+ #print ("added basetemp: %s" %(args,))
+ return args
+
+ def parseconfig(self, *args):
+ """Return a new py.test Config instance from given commandline args.
+
+ This invokes the py.test bootstrapping code in _pytest.config
+ to create a new :py:class:`_pytest.core.PluginManager` and
+ call the pytest_cmdline_parse hook to create new
+ :py:class:`_pytest.config.Config` instance.
+
+ If :py:attr:`plugins` has been populated they should be plugin
+ modules which will be registered with the PluginManager.
+
+ """
+ args = self._ensure_basetemp(args)
+
import _pytest.config
config = _pytest.config._prepareconfig(args, self.plugins)
# we don't know what the test will do with this half-setup config
# object and thus we make sure it gets unconfigured properly in any
# case (otherwise capturing could still be active, for example)
- def ensure_unconfigure():
- if hasattr(config.pluginmanager, "_config"):
- config.pluginmanager.do_unconfigure(config)
- config.pluginmanager.ensure_shutdown()
-
- self.request.addfinalizer(ensure_unconfigure)
+ self.request.addfinalizer(config._ensure_unconfigure)
return config
def parseconfigure(self, *args):
+ """Return a new py.test configured Config instance.
+
+ This returns a new :py:class:`_pytest.config.Config` instance
+ like :py:meth:`parseconfig`, but also calls the
+ pytest_configure hook.
+
+ """
config = self.parseconfig(*args)
- config.do_configure()
- self.request.addfinalizer(lambda:
- config.do_unconfigure())
+ config._do_configure()
+ self.request.addfinalizer(config._ensure_unconfigure)
return config
def getitem(self, source, funcname="test_func"):
+ """Return the test item for a test function.
+
+ This writes the source to a python file and runs py.test's
+ collection on the resulting module, returning the test item
+ for the requested function name.
+
+ :param source: The module source.
+
+ :param funcname: The name of the test function for which the
+ Item must be returned.
+
+ """
items = self.getitems(source)
for item in items:
if item.name == funcname:
@@ -398,11 +810,33 @@ class TmpTestdir:
funcname, source, items)
def getitems(self, source):
+ """Return all test items collected from the module.
+
+ This writes the source to a python file and runs py.test's
+ collection on the resulting module, returning all test items
+ contained within.
+
+ """
modcol = self.getmodulecol(source)
return self.genitems([modcol])
def getmodulecol(self, source, configargs=(), withinit=False):
- kw = {self.request.function.__name__: py.code.Source(source).strip()}
+ """Return the module collection node for ``source``.
+
+ This writes ``source`` to a file using :py:meth:`makepyfile`
+ and then runs the py.test collection on it, returning the
+ collection node for the test module.
+
+ :param source: The source code of the module to collect.
+
+ :param configargs: Any extra arguments to pass to
+ :py:meth:`parseconfigure`.
+
+ :param withinit: Whether to also write a ``__init__.py`` file
+ to the temporarly directory to ensure it is a package.
+
+ """
+ kw = {self.request.function.__name__: Source(source).strip()}
path = self.makepyfile(**kw)
if withinit:
self.makepyfile(__init__ = "#")
@@ -411,27 +845,54 @@ class TmpTestdir:
return node
def collect_by_name(self, modcol, name):
+ """Return the collection node for name from the module collection.
+
+ This will search a module collection node for a collection
+ node matching the given name.
+
+ :param modcol: A module collection node, see
+ :py:meth:`getmodulecol`.
+
+ :param name: The name of the node to return.
+
+ """
for colitem in modcol._memocollect():
if colitem.name == name:
return colitem
def popen(self, cmdargs, stdout, stderr, **kw):
+ """Invoke subprocess.Popen.
+
+ This calls subprocess.Popen making sure the current working
+ directory is the PYTHONPATH.
+
+ You probably want to use :py:meth:`run` instead.
+
+ """
env = os.environ.copy()
env['PYTHONPATH'] = os.pathsep.join(filter(None, [
str(os.getcwd()), env.get('PYTHONPATH', '')]))
kw['env'] = env
- #print "env", env
- return py.std.subprocess.Popen(cmdargs,
- stdout=stdout, stderr=stderr, **kw)
+ return subprocess.Popen(cmdargs,
+ stdout=stdout, stderr=stderr, **kw)
def run(self, *cmdargs):
+ """Run a command with arguments.
+
+ Run a process using subprocess.Popen saving the stdout and
+ stderr.
+
+ Returns a :py:class:`RunResult`.
+
+ """
return self._run(*cmdargs)
def _run(self, *cmdargs):
cmdargs = [str(x) for x in cmdargs]
p1 = self.tmpdir.join("stdout")
p2 = self.tmpdir.join("stderr")
- print_("running", cmdargs, "curdir=", py.path.local())
+ print_("running:", ' '.join(cmdargs))
+ print_(" in:", str(py.path.local()))
f1 = codecs.open(str(p1), "w", encoding="utf8")
f2 = codecs.open(str(p2), "w", encoding="utf8")
try:
@@ -461,38 +922,35 @@ class TmpTestdir:
except UnicodeEncodeError:
print("couldn't print to %s because of encoding" % (fp,))
- def runpybin(self, scriptname, *args):
- fullargs = self._getpybinargs(scriptname) + args
- return self.run(*fullargs)
+ def _getpytestargs(self):
+ # we cannot use "(sys.executable,script)"
+ # because on windows the script is e.g. a py.test.exe
+ return (sys.executable, _pytest_fullpath,) # noqa
- def _getpybinargs(self, scriptname):
- if not self.request.config.getvalue("notoolsonpath"):
- # XXX we rely on script referring to the correct environment
- # we cannot use "(py.std.sys.executable,script)"
- # because on windows the script is e.g. a py.test.exe
- return (py.std.sys.executable, _pytest_fullpath,) # noqa
- else:
- pytest.skip("cannot run %r with --no-tools-on-path" % scriptname)
+ def runpython(self, script):
+ """Run a python script using sys.executable as interpreter.
- def runpython(self, script, prepend=True):
- if prepend:
- s = self._getsysprepend()
- if s:
- script.write(s + "\n" + script.read())
+ Returns a :py:class:`RunResult`.
+ """
return self.run(sys.executable, script)
- def _getsysprepend(self):
- if self.request.config.getvalue("notoolsonpath"):
- s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath())
- else:
- s = ""
- return s
-
def runpython_c(self, command):
- command = self._getsysprepend() + command
- return self.run(py.std.sys.executable, "-c", command)
+ """Run python -c "command", return a :py:class:`RunResult`."""
+ return self.run(sys.executable, "-c", command)
- def runpytest(self, *args):
+ def runpytest_subprocess(self, *args, **kwargs):
+ """Run py.test as a subprocess with given arguments.
+
+ Any plugins added to the :py:attr:`plugins` list will added
+ using the ``-p`` command line option. Addtionally
+ ``--basetemp`` is used put any temporary files and directories
+ in a numbered directory prefixed with "runpytest-" so they do
+ not conflict with the normal numberd pytest location for
+ temporary files and directories.
+
+ Returns a :py:class:`RunResult`.
+
+ """
p = py.path.local.make_numbered_dir(prefix="runpytest-",
keep=None, rootdir=self.tmpdir)
args = ('--basetemp=%s' % p, ) + args
@@ -505,19 +963,30 @@ class TmpTestdir:
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
args = ('-p', plugins[0]) + args
- return self.runpybin("py.test", *args)
+ args = self._getpytestargs() + args
+ return self.run(*args)
def spawn_pytest(self, string, expect_timeout=10.0):
- if self.request.config.getvalue("notoolsonpath"):
- pytest.skip("--no-tools-on-path prevents running pexpect-spawn tests")
+ """Run py.test using pexpect.
+
+ This makes sure to use the right py.test and sets up the
+ temporary directory locations.
+
+ The pexpect child is returned.
+
+ """
basetemp = self.tmpdir.mkdir("pexpect")
- invoke = " ".join(map(str, self._getpybinargs("py.test")))
+ invoke = " ".join(map(str, self._getpytestargs()))
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
return self.spawn(cmd, expect_timeout=expect_timeout)
def spawn(self, cmd, expect_timeout=10.0):
+ """Run a command using pexpect.
+
+ The pexpect child is returned.
+ """
pexpect = pytest.importorskip("pexpect", "3.0")
- if hasattr(sys, 'pypy_version_info') and '64' in py.std.platform.machine():
+ if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
pytest.skip("pypy-64 bit not supported")
if sys.platform == "darwin":
pytest.xfail("pexpect does not work reliably on darwin?!")
@@ -536,86 +1005,6 @@ def getdecoded(out):
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
py.io.saferepr(out),)
-class ReportRecorder(object):
- def __init__(self, hook):
- self.hook = hook
- self.pluginmanager = hook._pm
- self.pluginmanager.register(self)
-
- def getcall(self, name):
- return self.hookrecorder.getcall(name)
-
- def popcall(self, name):
- return self.hookrecorder.popcall(name)
-
- def getcalls(self, names):
- """ return list of ParsedCall instances matching the given eventname. """
- return self.hookrecorder.getcalls(names)
-
- # functionality for test reports
-
- def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
- return [x.report for x in self.getcalls(names)]
-
- def matchreport(self, inamepart="",
- names="pytest_runtest_logreport pytest_collectreport", when=None):
- """ return a testreport whose dotted import path matches """
- l = []
- for rep in self.getreports(names=names):
- try:
- if not when and rep.when != "call" and rep.passed:
- # setup/teardown passing reports - let's ignore those
- continue
- except AttributeError:
- pass
- if when and getattr(rep, 'when', None) != when:
- continue
- if not inamepart or inamepart in rep.nodeid.split("::"):
- l.append(rep)
- if not l:
- raise ValueError("could not find test report matching %r: no test reports at all!" %
- (inamepart,))
- if len(l) > 1:
- raise ValueError("found more than one testreport matching %r: %s" %(
- inamepart, l))
- return l[0]
-
- def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'):
- return [rep for rep in self.getreports(names) if rep.failed]
-
- def getfailedcollections(self):
- return self.getfailures('pytest_collectreport')
-
- def listoutcomes(self):
- passed = []
- skipped = []
- failed = []
- for rep in self.getreports(
- "pytest_collectreport pytest_runtest_logreport"):
- if rep.passed:
- if getattr(rep, "when", None) == "call":
- passed.append(rep)
- elif rep.skipped:
- skipped.append(rep)
- elif rep.failed:
- failed.append(rep)
- return passed, skipped, failed
-
- def countoutcomes(self):
- return [len(x) for x in self.listoutcomes()]
-
- def assertoutcome(self, passed=0, skipped=0, failed=0):
- realpassed, realskipped, realfailed = self.listoutcomes()
- assert passed == len(realpassed)
- assert skipped == len(realskipped)
- assert failed == len(realfailed)
-
- def clear(self):
- self.hookrecorder.calls[:] = []
-
- def unregister(self):
- self.pluginmanager.unregister(self)
- self.hookrecorder.finish_recording()
class LineComp:
def __init__(self):
@@ -632,21 +1021,39 @@ class LineComp:
lines1 = val.split("\n")
return LineMatcher(lines1).fnmatch_lines(lines2)
+
class LineMatcher:
+ """Flexible matching of text.
+
+ This is a convenience class to test large texts like the output of
+ commands.
+
+ The constructor takes a list of lines without their trailing
+ newlines, i.e. ``text.splitlines()``.
+
+ """
+
def __init__(self, lines):
self.lines = lines
def str(self):
+ """Return the entire original text."""
return "\n".join(self.lines)
def _getlines(self, lines2):
if isinstance(lines2, str):
- lines2 = py.code.Source(lines2)
- if isinstance(lines2, py.code.Source):
+ lines2 = Source(lines2)
+ if isinstance(lines2, Source):
lines2 = lines2.strip().lines
return lines2
def fnmatch_lines_random(self, lines2):
+ """Check lines exist in the output.
+
+ The argument is a list of lines which have to occur in the
+ output, in any order. Each line can contain glob whildcards.
+
+ """
lines2 = self._getlines(lines2)
for line in lines2:
for x in self.lines:
@@ -657,14 +1064,26 @@ class LineMatcher:
raise ValueError("line %r not found in output" % line)
def get_lines_after(self, fnline):
+ """Return all lines following the given line in the text.
+
+ The given line can contain glob wildcards.
+ """
for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline):
return self.lines[i+1:]
raise ValueError("line %r not found in output" % fnline)
def fnmatch_lines(self, lines2):
+ """Search the text for matching lines.
+
+ The argument is a list of lines which have to match and can
+ use glob wildcards. If they do not match an pytest.fail() is
+ called. The matches and non-matches are also printed on
+ stdout.
+
+ """
def show(arg1, arg2):
- py.builtin.print_(arg1, arg2, file=py.std.sys.stderr)
+ py.builtin.print_(arg1, arg2, file=sys.stderr)
lines2 = self._getlines(lines2)
lines1 = self.lines[:]
nextline = None
diff --git a/_pytest/python.py b/_pytest/python.py
index a882dec70e..21d78aea33 100644
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -1,26 +1,93 @@
""" Python test discovery, setup and run of test functions. """
-import py
+import fnmatch
+import functools
import inspect
+import re
+import types
import sys
+
+import py
import pytest
-from _pytest.mark import MarkDecorator
-from py._code.code import TerminalRepr
+from _pytest._code.code import TerminalRepr
+from _pytest.mark import MarkDecorator, MarkerError
+
+try:
+ import enum
+except ImportError: # pragma: no cover
+ # Only available in Python 3.4+ or as a backport
+ enum = None
import _pytest
-cutdir = py.path.local(_pytest.__file__).dirpath()
+import _pytest._pluggy as pluggy
+
+cutdir2 = py.path.local(_pytest.__file__).dirpath()
+cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
+
NoneType = type(None)
NOTSET = object()
-
+isfunction = inspect.isfunction
+isclass = inspect.isclass
callable = py.builtin.callable
+# used to work around a python2 exception info leak
+exc_clear = getattr(sys, 'exc_clear', lambda: None)
+# The type of re.compile objects is not exposed in Python.
+REGEX_TYPE = type(re.compile(''))
+
+_PY3 = sys.version_info > (3, 0)
+_PY2 = not _PY3
+
+
+if hasattr(inspect, 'signature'):
+ def _format_args(func):
+ return str(inspect.signature(func))
+else:
+ def _format_args(func):
+ return inspect.formatargspec(*inspect.getargspec(func))
+
+if sys.version_info[:2] == (2, 6):
+ def isclass(object):
+ """ Return true if the object is a class. Overrides inspect.isclass for
+ python 2.6 because it will return True for objects which always return
+ something on __getattr__ calls (see #1035).
+ Backport of https://hg.python.org/cpython/rev/35bf8f7a8edc
+ """
+ return isinstance(object, (type, types.ClassType))
-def getfslineno(obj):
- # xxx let decorators etc specify a sane ordering
+def _has_positional_arg(func):
+ return func.__code__.co_argcount
+
+
+def filter_traceback(entry):
+ # entry.path might sometimes return a str object when the entry
+ # points to dynamically generated code
+ # see https://bitbucket.org/pytest-dev/py/issues/71
+ raw_filename = entry.frame.code.raw.co_filename
+ is_generated = '<' in raw_filename and '>' in raw_filename
+ if is_generated:
+ return False
+ # entry.path might point to an inexisting file, in which case it will
+ # alsso return a str object. see #1133
+ p = py.path.local(entry.path)
+ return p != cutdir1 and not p.relto(cutdir2)
+
+
+def get_real_func(obj):
+ """ gets the real function object of the (possibly) wrapped object by
+ functools.wraps or functools.partial.
+ """
while hasattr(obj, "__wrapped__"):
obj = obj.__wrapped__
+ if isinstance(obj, functools.partial):
+ obj = obj.func
+ return obj
+
+def getfslineno(obj):
+ # xxx let decorators etc specify a sane ordering
+ obj = get_real_func(obj)
if hasattr(obj, 'place_as'):
obj = obj.place_as
- fslineno = py.code.getfslineno(obj)
+ fslineno = _pytest._code.getfslineno(obj)
assert isinstance(fslineno[1], int), obj
return fslineno
@@ -33,6 +100,17 @@ def getimfunc(func):
except AttributeError:
return func
+def safe_getattr(object, name, default):
+ """ Like getattr but return default upon any Exception.
+
+ Attribute access can potentially fail for 'evil' Python objects.
+ See issue214
+ """
+ try:
+ return getattr(object, name, default)
+ except Exception:
+ return default
+
class FixtureFunctionMarker:
def __init__(self, scope, params,
@@ -44,7 +122,7 @@ class FixtureFunctionMarker:
self.ids = ids
def __call__(self, function):
- if inspect.isclass(function):
+ if isclass(function):
raise ValueError(
"class fixtures not supported (may be in the future)")
function._pytestfixturefunction = self
@@ -123,12 +201,19 @@ def pytest_addoption(parser):
parser.addini("usefixtures", type="args", default=[],
help="list of default fixtures to be used with this project")
parser.addini("python_files", type="args",
- default=('test_*.py', '*_test.py'),
+ default=['test_*.py', '*_test.py'],
help="glob-style file patterns for Python test module discovery")
- parser.addini("python_classes", type="args", default=("Test",),
- help="prefixes for Python test class discovery")
- parser.addini("python_functions", type="args", default=("test",),
- help="prefixes for Python test function and method discovery")
+ parser.addini("python_classes", type="args", default=["Test",],
+ help="prefixes or glob names for Python test class discovery")
+ parser.addini("python_functions", type="args", default=["test",],
+ help="prefixes or glob names for Python test function and "
+ "method discovery")
+
+ group.addoption("--import-mode", default="prepend",
+ choices=["prepend", "append"], dest="importmode",
+ help="prepend/append to sys.path when importing test modules, "
+ "default is to prepend.")
+
def pytest_cmdline_main(config):
if config.option.showfixtures:
@@ -137,6 +222,13 @@ def pytest_cmdline_main(config):
def pytest_generate_tests(metafunc):
+ # those alternative spellings are common - raise a specific error to alert
+ # the user
+ alt_spellings = ['parameterize', 'parametrise', 'parameterise']
+ for attr in alt_spellings:
+ if hasattr(metafunc.function, attr):
+ msg = "{0} has '{1}', spelling should be 'parametrize'"
+ raise MarkerError(msg.format(metafunc.function.__name__, attr))
try:
markers = metafunc.function.parametrize
except AttributeError:
@@ -163,7 +255,7 @@ def pytest_configure(config):
def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session)
-@pytest.mark.trylast
+@pytest.hookimpl(trylast=True)
def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
@@ -182,17 +274,18 @@ def pytestconfig(request):
return request.config
-def pytest_pyfunc_call(__multicall__, pyfuncitem):
- if not __multicall__.execute():
- testfunction = pyfuncitem.obj
- if pyfuncitem._isyieldedfunction():
- testfunction(*pyfuncitem._args)
- else:
- funcargs = pyfuncitem.funcargs
- testargs = {}
- for arg in pyfuncitem._fixtureinfo.argnames:
- testargs[arg] = funcargs[arg]
- testfunction(**testargs)
+@pytest.hookimpl(trylast=True)
+def pytest_pyfunc_call(pyfuncitem):
+ testfunction = pyfuncitem.obj
+ if pyfuncitem._isyieldedfunction():
+ testfunction(*pyfuncitem._args)
+ else:
+ funcargs = pyfuncitem.funcargs
+ testargs = {}
+ for arg in pyfuncitem._fixtureinfo.argnames:
+ testargs[arg] = funcargs[arg]
+ testfunction(**testargs)
+ return True
def pytest_collect_file(path, parent):
ext = path.ext
@@ -209,26 +302,37 @@ def pytest_collect_file(path, parent):
def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)
-def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
- res = __multicall__.execute()
+@pytest.hookimpl(hookwrapper=True)
+def pytest_pycollect_makeitem(collector, name, obj):
+ outcome = yield
+ res = outcome.get_result()
if res is not None:
- return res
- if inspect.isclass(obj):
- #if hasattr(collector.obj, 'unittest'):
- # return # we assume it's a mixin class for a TestCase derived one
- if collector.classnamefilter(name):
+ raise StopIteration
+ # nothing was collected elsewhere, let's do it here
+ if isclass(obj):
+ if collector.istestclass(obj, name):
Class = collector._getcustomclass("Class")
- return Class(name, parent=collector)
- elif collector.funcnamefilter(name) and hasattr(obj, '__call__') and \
- getfixturemarker(obj) is None:
- if is_generator(obj):
- return Generator(name, parent=collector)
- else:
- return list(collector._genfunctions(name, obj))
+ outcome.force_result(Class(name, parent=collector))
+ elif collector.istestfunction(obj, name):
+ # mock seems to store unbound methods (issue473), normalize it
+ obj = getattr(obj, "__func__", obj)
+ # We need to try and unwrap the function if it's a functools.partial
+ # or a funtools.wrapped.
+ # We musn't if it's been wrapped with mock.patch (python 2 only)
+ if not (isfunction(obj) or isfunction(get_real_func(obj))):
+ collector.warn(code="C2", message=
+ "cannot collect %r because it is not a function."
+ % name, )
+ elif getattr(obj, "__test__", True):
+ if is_generator(obj):
+ res = Generator(name, parent=collector)
+ else:
+ res = list(collector._genfunctions(name, obj))
+ outcome.force_result(res)
def is_generator(func):
try:
- return py.code.getrawcode(func).co_flags & 32 # generator function
+ return _pytest._code.getrawcode(func).co_flags & 32 # generator function
except AttributeError: # builtin functions have no bytecode
# assume them to not be generators
return False
@@ -281,12 +385,13 @@ class PyobjMixin(PyobjContext):
def reportinfo(self):
# XXX caching?
obj = self.obj
- if hasattr(obj, 'compat_co_firstlineno'):
+ compat_co_firstlineno = getattr(obj, 'compat_co_firstlineno', None)
+ if isinstance(compat_co_firstlineno, int):
# nose compatibility
fspath = sys.modules[obj.__module__].__file__
if fspath.endswith(".pyc"):
fspath = fspath[:-1]
- lineno = obj.compat_co_firstlineno
+ lineno = compat_co_firstlineno
else:
fspath, lineno = getfslineno(obj)
modpath = self.getmodpath()
@@ -296,16 +401,49 @@ class PyobjMixin(PyobjContext):
class PyCollector(PyobjMixin, pytest.Collector):
def funcnamefilter(self, name):
- for prefix in self.config.getini("python_functions"):
- if name.startswith(prefix):
- return True
+ return self._matches_prefix_or_glob_option('python_functions', name)
+
+ def isnosetest(self, obj):
+ """ Look for the __test__ attribute, which is applied by the
+ @nose.tools.istest decorator
+ """
+ # We explicitly check for "is True" here to not mistakenly treat
+ # classes with a custom __getattr__ returning something truthy (like a
+ # function) as test classes.
+ return safe_getattr(obj, '__test__', False) is True
def classnamefilter(self, name):
- for prefix in self.config.getini("python_classes"):
- if name.startswith(prefix):
+ return self._matches_prefix_or_glob_option('python_classes', name)
+
+ def istestfunction(self, obj, name):
+ return (
+ (self.funcnamefilter(name) or self.isnosetest(obj)) and
+ safe_getattr(obj, "__call__", False) and getfixturemarker(obj) is None
+ )
+
+ def istestclass(self, obj, name):
+ return self.classnamefilter(name) or self.isnosetest(obj)
+
+ def _matches_prefix_or_glob_option(self, option_name, name):
+ """
+ checks if the given name matches the prefix or glob-pattern defined
+ in ini configuration.
+ """
+ for option in self.config.getini(option_name):
+ if name.startswith(option):
return True
+ # check that name looks like a glob-string before calling fnmatch
+ # because this is called for every name in each collected module,
+ # and fnmatch is somewhat expensive to call
+ elif ('*' in option or '?' in option or '[' in option) and \
+ fnmatch.fnmatch(name, option):
+ return True
+ return False
def collect(self):
+ if not getattr(self.obj, "__test__", True):
+ return []
+
# NB. we avoid random getattrs and peek in the __dict__ instead
# (XXX originally introduced from a PyPy need, still true?)
dicts = [getattr(self.obj, '__dict__', {})]
@@ -314,7 +452,7 @@ class PyCollector(PyobjMixin, pytest.Collector):
seen = {}
l = []
for dic in dicts:
- for name, obj in dic.items():
+ for name, obj in list(dic.items()):
if name in seen:
continue
seen[name] = True
@@ -341,15 +479,20 @@ class PyCollector(PyobjMixin, pytest.Collector):
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module)
- gentesthook = self.config.hook.pytest_generate_tests
- extra = [module]
- if cls is not None:
- extra.append(cls())
- plugins = self.getplugins() + extra
- gentesthook.pcall(plugins, metafunc=metafunc)
+ methods = []
+ if hasattr(module, "pytest_generate_tests"):
+ methods.append(module.pytest_generate_tests)
+ if hasattr(cls, "pytest_generate_tests"):
+ methods.append(cls().pytest_generate_tests)
+ if methods:
+ self.ihook.pytest_generate_tests.call_extra(methods,
+ dict(metafunc=metafunc))
+ else:
+ self.ihook.pytest_generate_tests(metafunc=metafunc)
+
Function = self._getcustomclass("Function")
if not metafunc._calls:
- yield Function(name, parent=self)
+ yield Function(name, parent=self, fixtureinfo=fixtureinfo)
else:
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
add_funcarg_pseudo_fixture_def(self, metafunc, fm)
@@ -358,6 +501,7 @@ class PyCollector(PyobjMixin, pytest.Collector):
subname = "%s[%s]" %(name, callspec.id)
yield Function(name=subname, parent=self,
callspec=callspec, callobj=funcobj,
+ fixtureinfo=fixtureinfo,
keywords={callspec.id:True})
def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
@@ -422,6 +566,19 @@ class FuncFixtureInfo:
self.names_closure = names_closure
self.name2fixturedefs = name2fixturedefs
+
+def _marked(func, mark):
+ """ Returns True if :func: is already marked with :mark:, False otherwise.
+ This can happen if marker is applied to class and the test file is
+ invoked more than once.
+ """
+ try:
+ func_mark = getattr(func, mark.name)
+ except AttributeError:
+ return False
+ return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
+
+
def transfer_markers(funcobj, cls, mod):
# XXX this should rather be code in the mark plugin or the mark
# plugin should merge with the python plugin.
@@ -432,9 +589,11 @@ def transfer_markers(funcobj, cls, mod):
continue
if isinstance(pytestmark, list):
for mark in pytestmark:
- mark(funcobj)
+ if not _marked(funcobj, mark):
+ mark(funcobj)
else:
- pytestmark(funcobj)
+ if not _marked(funcobj, pytestmark):
+ pytestmark(funcobj)
class Module(pytest.File, PyCollector):
""" Collector for test classes and functions. """
@@ -447,11 +606,12 @@ class Module(pytest.File, PyCollector):
def _importtestmodule(self):
# we assume we are only called once per module
+ importmode = self.config.getoption("--import-mode")
try:
- mod = self.fspath.pyimport(ensuresyspath=True)
+ mod = self.fspath.pyimport(ensuresyspath=importmode)
except SyntaxError:
- excinfo = py.code.ExceptionInfo()
- raise self.CollectError(excinfo.getrepr(style="short"))
+ raise self.CollectError(
+ _pytest._code.ExceptionInfo().getrepr(style="short"))
except self.fspath.ImportMismatchError:
e = sys.exc_info()[1]
raise self.CollectError(
@@ -476,7 +636,7 @@ class Module(pytest.File, PyCollector):
#XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, its probably a pytest style one
# so we pass the current module object
- if inspect.getargspec(setup_module)[0]:
+ if _has_positional_arg(setup_module):
setup_module(self.obj)
else:
setup_module()
@@ -487,7 +647,7 @@ class Module(pytest.File, PyCollector):
#XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, it's probably a pytest style one
# so we pass the current module object
- if inspect.getargspec(fin)[0]:
+ if _has_positional_arg(fin):
finalizer = lambda: fin(self.obj)
else:
finalizer = fin
@@ -498,10 +658,9 @@ class Class(PyCollector):
""" Collector for test methods. """
def collect(self):
if hasinit(self.obj):
- pytest.skip("class %s.%s with __init__ won't get collected" % (
- self.obj.__module__,
- self.obj.__name__,
- ))
+ self.warn("C1", "cannot collect test class %r because it has a "
+ "__init__ constructor" % self.obj.__name__)
+ return []
return [self._getcustomclass("Instance")(name="()", parent=self)]
def setup(self):
@@ -558,27 +717,39 @@ class FunctionMixin(PyobjMixin):
def _prunetraceback(self, excinfo):
if hasattr(self, '_obj') and not self.config.option.fulltrace:
- code = py.code.Code(self.obj)
+ code = _pytest._code.Code(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
traceback = excinfo.traceback
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
if ntraceback == traceback:
ntraceback = ntraceback.cut(path=path)
if ntraceback == traceback:
- ntraceback = ntraceback.cut(excludepath=cutdir)
+ #ntraceback = ntraceback.cut(excludepath=cutdir2)
+ ntraceback = ntraceback.filter(filter_traceback)
+ if not ntraceback:
+ ntraceback = traceback
+
excinfo.traceback = ntraceback.filter()
+ # issue364: mark all but first and last frames to
+ # only show a single-line message for each frame
+ if self.config.option.tbstyle == "auto":
+ if len(excinfo.traceback) > 2:
+ for entry in excinfo.traceback[1:-1]:
+ entry.set_repr_style('short')
def _repr_failure_py(self, excinfo, style="long"):
if excinfo.errisinstance(pytest.fail.Exception):
if not excinfo.value.pytrace:
- return str(excinfo.value)
+ return py._builtin._totext(excinfo.value)
return super(FunctionMixin, self)._repr_failure_py(excinfo,
style=style)
def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated"
- return self._repr_failure_py(excinfo,
- style=self.config.option.tbstyle)
+ style = self.config.option.tbstyle
+ if style == "auto":
+ style = "long"
+ return self._repr_failure_py(excinfo, style=style)
class Generator(FunctionMixin, PyCollector):
@@ -692,15 +863,14 @@ class CallSpec2(object):
def id(self):
return "-".join(map(str, filter(None, self._idlist)))
- def setmulti(self, valtype, argnames, valset, id, keywords, scopenum,
+ def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum,
param_index):
for arg,val in zip(argnames, valset):
self._checkargnotcontained(arg)
- getattr(self, valtype)[arg] = val
+ valtype_for_arg = valtypes[arg]
+ getattr(self, valtype_for_arg)[arg] = val
self.indices[arg] = param_index
self._arg2scopenum[arg] = scopenum
- if val is _notexists:
- self._emptyparamspecified = True
self._idlist.append(id)
self.keywords.update(keywords)
@@ -727,6 +897,27 @@ class FuncargnamesCompatAttr:
return self.fixturenames
class Metafunc(FuncargnamesCompatAttr):
+ """
+ Metafunc objects are passed to the ``pytest_generate_tests`` hook.
+ They help to inspect a test function and to generate tests according to
+ test configuration or values specified in the class or module where a
+ test function is defined.
+
+ :ivar fixturenames: set of fixture names required by the test function
+
+ :ivar function: underlying python test function
+
+ :ivar cls: class object where the test function is defined in or ``None``.
+
+ :ivar module: the module object where the test function is defined in.
+
+ :ivar config: access to the :class:`_pytest.config.Config` object for the
+ test session.
+
+ :ivar funcargnames:
+ .. deprecated:: 2.3
+ Use ``fixturenames`` instead.
+ """
def __init__(self, function, fixtureinfo, config, cls=None, module=None):
self.config = config
self.module = module
@@ -734,7 +925,6 @@ class Metafunc(FuncargnamesCompatAttr):
self.fixturenames = fixtureinfo.names_closure
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
self.cls = cls
- self.module = module
self._calls = []
self._ids = py.builtin.set()
@@ -743,26 +933,33 @@ class Metafunc(FuncargnamesCompatAttr):
""" Add new invocations to the underlying test function using the list
of argvalues for the given argnames. Parametrization is performed
during the collection phase. If you need to setup expensive resources
- see about setting indirect=True to do it rather at test setup time.
+ see about setting indirect to do it rather at test setup time.
:arg argnames: a comma-separated string denoting one or more argument
names, or a list/tuple of argument strings.
:arg argvalues: The list of argvalues determines how often a
test is invoked with different argument values. If only one
- argname was specified argvalues is a list of simple values. If N
+ argname was specified argvalues is a list of values. If N
argnames were specified, argvalues must be a list of N-tuples,
where each tuple-element specifies a value for its respective
argname.
- :arg indirect: if True each argvalue corresponding to an argname will
+ :arg indirect: The list of argnames or boolean. A list of arguments'
+ names (subset of argnames). If True the list contains all names from
+ the argnames. Each argvalue corresponding to an argname in this list will
be passed as request.param to its respective argname fixture
function so that it can perform more expensive setups during the
setup phase of a test rather than at collection time.
- :arg ids: list of string ids each corresponding to the argvalues so
- that they are part of the test id. If no ids are provided they will
- be generated automatically from the argvalues.
+ :arg ids: list of string ids, or a callable.
+ If strings, each is corresponding to the argvalues so that they are
+ part of the test id.
+ If callable, it should take one argument (a single argvalue) and return
+ a string or return None. If None, the automatically generated id for that
+ argument will be used.
+ If no ids are provided they will be generated automatically from
+ the argvalues.
:arg scope: if specified it denotes the scope of the parameters.
The scope is used for grouping tests by parameter instances.
@@ -791,28 +988,50 @@ class Metafunc(FuncargnamesCompatAttr):
argvalues = [(val,) for val in argvalues]
if not argvalues:
argvalues = [(_notexists,) * len(argnames)]
+ # we passed a empty list to parameterize, skip that test
+ #
+ fs, lineno = getfslineno(self.function)
+ newmark = pytest.mark.skip(
+ reason="got empty parameter set %r, function %s at %s:%d" % (
+ argnames, self.function.__name__, fs, lineno))
+ newmarks = newkeywords.setdefault(0, {})
+ newmarks[newmark.markname] = newmark
+
if scope is None:
scope = "function"
scopenum = scopes.index(scope)
- if not indirect:
- #XXX should we also check for the opposite case?
- for arg in argnames:
- if arg not in self.fixturenames:
- raise ValueError("%r uses no fixture %r" %(
+ valtypes = {}
+ for arg in argnames:
+ if arg not in self.fixturenames:
+ raise ValueError("%r uses no fixture %r" %(self.function, arg))
+
+ if indirect is True:
+ valtypes = dict.fromkeys(argnames, "params")
+ elif indirect is False:
+ valtypes = dict.fromkeys(argnames, "funcargs")
+ elif isinstance(indirect, (tuple, list)):
+ valtypes = dict.fromkeys(argnames, "funcargs")
+ for arg in indirect:
+ if arg not in argnames:
+ raise ValueError("indirect given to %r: fixture %r doesn't exist" %(
self.function, arg))
- valtype = indirect and "params" or "funcargs"
+ valtypes[arg] = "params"
+ idfn = None
+ if callable(ids):
+ idfn = ids
+ ids = None
if ids and len(ids) != len(argvalues):
raise ValueError('%d tests specified with %d ids' %(
len(argvalues), len(ids)))
if not ids:
- ids = idmaker(argnames, argvalues)
+ ids = idmaker(argnames, argvalues, idfn)
newcalls = []
for callspec in self._calls or [CallSpec2(self)]:
for param_index, valset in enumerate(argvalues):
assert len(valset) == len(argnames)
newcallspec = callspec.copy(self)
- newcallspec.setmulti(valtype, argnames, valset, ids[param_index],
+ newcallspec.setmulti(valtypes, argnames, valset, ids[param_index],
newkeywords.get(param_index, {}), scopenum,
param_index)
newcalls.append(newcallspec)
@@ -854,49 +1073,107 @@ class Metafunc(FuncargnamesCompatAttr):
cs.setall(funcargs, id, param)
self._calls.append(cs)
-def idmaker(argnames, argvalues):
- idlist = []
- for valindex, valset in enumerate(argvalues):
- this_id = []
- for nameindex, val in enumerate(valset):
- if not isinstance(val, (float, int, str, bool, NoneType)):
- this_id.append(str(argnames[nameindex])+str(valindex))
- else:
- this_id.append(str(val))
- idlist.append("-".join(this_id))
- return idlist
+
+if _PY3:
+ import codecs
+
+ def _escape_bytes(val):
+ """
+ If val is pure ascii, returns it as a str(), otherwise escapes
+ into a sequence of escaped bytes:
+ b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6'
+
+ note:
+ the obvious "v.decode('unicode-escape')" will return
+ valid utf-8 unicode if it finds them in the string, but we
+ want to return escaped bytes for any byte, even if they match
+ a utf-8 string.
+ """
+ if val:
+ # source: http://goo.gl/bGsnwC
+ encoded_bytes, _ = codecs.escape_encode(val)
+ return encoded_bytes.decode('ascii')
+ else:
+ # empty bytes crashes codecs.escape_encode (#1087)
+ return ''
+else:
+ def _escape_bytes(val):
+ """
+ In py2 bytes and str are the same type, so return it unchanged if it
+ is a full ascii string, otherwise escape it into its binary form.
+ """
+ try:
+ return val.decode('ascii')
+ except UnicodeDecodeError:
+ return val.encode('string-escape')
+
+
+def _idval(val, argname, idx, idfn):
+ if idfn:
+ try:
+ s = idfn(val)
+ if s:
+ return s
+ except Exception:
+ pass
+
+ if isinstance(val, bytes):
+ return _escape_bytes(val)
+ elif isinstance(val, (float, int, str, bool, NoneType)):
+ return str(val)
+ elif isinstance(val, REGEX_TYPE):
+ return _escape_bytes(val.pattern) if isinstance(val.pattern, bytes) else val.pattern
+ elif enum is not None and isinstance(val, enum.Enum):
+ return str(val)
+ elif isclass(val) and hasattr(val, '__name__'):
+ return val.__name__
+ elif _PY2 and isinstance(val, unicode):
+ # special case for python 2: if a unicode string is
+ # convertible to ascii, return it as an str() object instead
+ try:
+ return str(val)
+ except UnicodeError:
+ # fallthrough
+ pass
+ return str(argname)+str(idx)
+
+def _idvalset(idx, valset, argnames, idfn):
+ this_id = [_idval(val, argname, idx, idfn)
+ for val, argname in zip(valset, argnames)]
+ return "-".join(this_id)
+
+def idmaker(argnames, argvalues, idfn=None):
+ ids = [_idvalset(valindex, valset, argnames, idfn)
+ for valindex, valset in enumerate(argvalues)]
+ if len(set(ids)) < len(ids):
+ # user may have provided a bad idfn which means the ids are not unique
+ ids = [str(i) + testid for i, testid in enumerate(ids)]
+ return ids
def showfixtures(config):
from _pytest.main import wrap_session
return wrap_session(config, _showfixtures_main)
def _showfixtures_main(config, session):
+ import _pytest.config
session.perform_collect()
curdir = py.path.local()
- if session.items:
- nodeid = session.items[0].nodeid
- else:
- part = session._initialparts[0]
- nodeid = "::".join(map(str, [curdir.bestrelpath(part[0])] + part[1:]))
- nodeid.replace(session.fspath.sep, "/")
-
- tw = py.io.TerminalWriter()
+ tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")
fm = session._fixturemanager
available = []
- for argname in fm._arg2fixturedefs:
- fixturedefs = fm.getfixturedefs(argname, nodeid)
+ for argname, fixturedefs in fm._arg2fixturedefs.items():
assert fixturedefs is not None
if not fixturedefs:
continue
- fixturedef = fixturedefs[-1]
- loc = getlocation(fixturedef.func, curdir)
- available.append((len(fixturedef.baseid),
- fixturedef.func.__module__,
- curdir.bestrelpath(loc),
- fixturedef.argname, fixturedef))
+ for fixturedef in fixturedefs:
+ loc = getlocation(fixturedef.func, curdir)
+ available.append((len(fixturedef.baseid),
+ fixturedef.func.__module__,
+ curdir.bestrelpath(loc),
+ fixturedef.argname, fixturedef))
available.sort()
currentmodule = None
@@ -916,7 +1193,7 @@ def _showfixtures_main(config, session):
loc = getlocation(fixturedef.func, curdir)
doc = fixturedef.func.__doc__ or ""
if doc:
- for line in doc.split("\n"):
+ for line in doc.strip().split("\n"):
tw.line(" " + line.strip())
else:
tw.line(" %s: no docstring available" %(loc,),
@@ -932,11 +1209,11 @@ def getlocation(function, curdir):
# builtin pytest.raises helper
-def raises(ExpectedException, *args, **kwargs):
- """ assert that a code block/function call raises @ExpectedException
+def raises(expected_exception, *args, **kwargs):
+ """ assert that a code block/function call raises ``expected_exception``
and raise a failure exception otherwise.
- This helper produces a ``py.code.ExceptionInfo()`` object.
+ This helper produces a ``ExceptionInfo()`` object (see below).
If using Python 2.5 or above, you may use this function as a
context manager::
@@ -944,6 +1221,28 @@ def raises(ExpectedException, *args, **kwargs):
>>> with raises(ZeroDivisionError):
... 1/0
+ .. note::
+
+ When using ``pytest.raises`` as a context manager, it's worthwhile to
+ note that normal context manager rules apply and that the exception
+ raised *must* be the final line in the scope of the context manager.
+ Lines of code after that, within the scope of the context manager will
+ not be executed. For example::
+
+ >>> with raises(OSError) as exc_info:
+ assert 1 == 1 # this will execute as expected
+ raise OSError(errno.EEXISTS, 'directory exists')
+ assert exc_info.value.errno == errno.EEXISTS # this will not execute
+
+ Instead, the following approach must be taken (note the difference in
+ scope)::
+
+ >>> with raises(OSError) as exc_info:
+ assert 1 == 1 # this will execute as expected
+ raise OSError(errno.EEXISTS, 'directory exists')
+
+ assert exc_info.value.errno == errno.EEXISTS # this will now execute
+
Or you can specify a callable by passing a to-be-called lambda::
>>> raises(ZeroDivisionError, lambda: 1/0)
@@ -963,31 +1262,42 @@ def raises(ExpectedException, *args, **kwargs):
>>> raises(ZeroDivisionError, "f(0)")
<ExceptionInfo ...>
- Performance note:
- -----------------
+ .. autoclass:: _pytest._code.ExceptionInfo
+ :members:
- Similar to caught exception objects in Python, explicitly clearing local
- references to returned ``py.code.ExceptionInfo`` objects can help the Python
- interpreter speed up its garbage collection.
+ .. note::
+ Similar to caught exception objects in Python, explicitly clearing
+ local references to returned ``ExceptionInfo`` objects can
+ help the Python interpreter speed up its garbage collection.
- Clearing those references breaks a reference cycle (``ExceptionInfo`` -->
- caught exception --> frame stack raising the exception --> current frame
- stack --> local variables --> ``ExceptionInfo``) which makes Python keep all
- objects referenced from that cycle (including all local variables in the
- current frame) alive until the next cyclic garbage collection run. See the
- official Python ``try`` statement documentation for more detailed
- information.
+ Clearing those references breaks a reference cycle
+ (``ExceptionInfo`` --> caught exception --> frame stack raising
+ the exception --> current frame stack --> local variables -->
+ ``ExceptionInfo``) which makes Python keep all objects referenced
+ from that cycle (including all local variables in the current
+ frame) alive until the next cyclic garbage collection run. See the
+ official Python ``try`` statement documentation for more detailed
+ information.
"""
__tracebackhide__ = True
- if ExpectedException is AssertionError:
+ if expected_exception is AssertionError:
# we want to catch a AssertionError
# replace our subclass with the builtin one
- # see https://bitbucket.org/hpk42/pytest/issue/176/pytestraises
- from _pytest.assertion.util import BuiltinAssertionError as ExpectedException
+ # see https://github.com/pytest-dev/pytest/issues/176
+ from _pytest.assertion.util import BuiltinAssertionError \
+ as expected_exception
+ msg = ("exceptions must be old-style classes or"
+ " derived from BaseException, not %s")
+ if isinstance(expected_exception, tuple):
+ for exc in expected_exception:
+ if not isclass(exc):
+ raise TypeError(msg % type(exc))
+ elif not isclass(expected_exception):
+ raise TypeError(msg % type(expected_exception))
if not args:
- return RaisesContext(ExpectedException)
+ return RaisesContext(expected_exception)
elif isinstance(args[0], str):
code, = args
assert isinstance(code, str)
@@ -996,35 +1306,42 @@ def raises(ExpectedException, *args, **kwargs):
loc.update(kwargs)
#print "raises frame scope: %r" % frame.f_locals
try:
- code = py.code.Source(code).compile()
+ code = _pytest._code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc)
# XXX didn'T mean f_globals == f_locals something special?
# this is destroyed here ...
- except ExpectedException:
- return py.code.ExceptionInfo()
+ except expected_exception:
+ return _pytest._code.ExceptionInfo()
else:
func = args[0]
try:
func(*args[1:], **kwargs)
- except ExpectedException:
- return py.code.ExceptionInfo()
- pytest.fail("DID NOT RAISE")
+ except expected_exception:
+ return _pytest._code.ExceptionInfo()
+ pytest.fail("DID NOT RAISE {0}".format(expected_exception))
class RaisesContext(object):
- def __init__(self, ExpectedException):
- self.ExpectedException = ExpectedException
+ def __init__(self, expected_exception):
+ self.expected_exception = expected_exception
self.excinfo = None
def __enter__(self):
- self.excinfo = object.__new__(py.code.ExceptionInfo)
+ self.excinfo = object.__new__(_pytest._code.ExceptionInfo)
return self.excinfo
def __exit__(self, *tp):
__tracebackhide__ = True
if tp[0] is None:
pytest.fail("DID NOT RAISE")
+ if sys.version_info < (2, 7):
+ # py26: on __exit__() exc_value often does not contain the
+ # exception value.
+ # http://bugs.python.org/issue7853
+ if not isinstance(tp[1], BaseException):
+ exc_type, value, traceback = tp
+ tp = exc_type, exc_type(value), traceback
self.excinfo.__init__(tp)
- return issubclass(self.excinfo.type, self.ExpectedException)
+ return issubclass(self.excinfo.type, self.expected_exception)
#
# the basic pytest Function item
@@ -1036,28 +1353,27 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
"""
_genid = None
def __init__(self, name, parent, args=None, config=None,
- callspec=None, callobj=NOTSET, keywords=None, session=None):
+ callspec=None, callobj=NOTSET, keywords=None, session=None,
+ fixtureinfo=None):
super(Function, self).__init__(name, parent, config=config,
session=session)
self._args = args
if callobj is not NOTSET:
self.obj = callobj
- for name, val in (py.builtin._getfuncdict(self.obj) or {}).items():
- self.keywords[name] = val
+ self.keywords.update(self.obj.__dict__)
if callspec:
- for name, val in callspec.keywords.items():
- self.keywords[name] = val
- if keywords:
- for name, val in keywords.items():
- self.keywords[name] = val
-
- isyield = self._isyieldedfunction()
- self._fixtureinfo = fi = self.session._fixturemanager.getfixtureinfo(
- self.parent, self.obj, self.cls, funcargs=not isyield)
- self.fixturenames = fi.names_closure
- if callspec is not None:
self.callspec = callspec
+ self.keywords.update(callspec.keywords)
+ if keywords:
+ self.keywords.update(keywords)
+
+ if fixtureinfo is None:
+ fixtureinfo = self.session._fixturemanager.getfixtureinfo(
+ self.parent, self.obj, self.cls,
+ funcargs=not self._isyieldedfunction())
+ self._fixtureinfo = fixtureinfo
+ self.fixturenames = fixtureinfo.names_closure
self._initrequest()
def _initrequest(self):
@@ -1099,15 +1415,6 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
def setup(self):
- # check if parametrization happend with an empty list
- try:
- self.callspec._emptyparamspecified
- except AttributeError:
- pass
- else:
- fs, lineno = self._getfslineno()
- pytest.skip("got empty parameter set, function %s at %s:%d" %(
- self.function.__name__, fs, lineno))
super(Function, self).setup()
fillfixtures(self)
@@ -1142,7 +1449,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
self._pyfuncitem = pyfuncitem
#: fixture for which this request is being performed
self.fixturename = None
- #: Scope string, one of "function", "cls", "module", "session"
+ #: Scope string, one of "function", "class", "module", "session"
self.scope = "function"
self._funcargs = {}
self._fixturedefs = {}
@@ -1224,7 +1531,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
return self._pyfuncitem.session
def addfinalizer(self, finalizer):
- """add finalizer/teardown function to be called after the
+ """ add finalizer/teardown function to be called after the
last test within the requesting test context finished
execution. """
# XXX usually this method is shadowed by fixturedef specific ones
@@ -1281,12 +1588,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
try:
val = cache[cachekey]
except KeyError:
- __tracebackhide__ = True
- if scopemismatch(self.scope, scope):
- raise ScopeMismatchError("You tried to access a %r scoped "
- "resource with a %r scoped request object" %(
- (scope, self.scope)))
- __tracebackhide__ = False
+ self._check_scope(self.fixturename, self.scope, scope)
val = setup()
cache[cachekey] = val
if teardown is not None:
@@ -1316,13 +1618,16 @@ class FixtureRequest(FuncargnamesCompatAttr):
except FixtureLookupError:
if argname == "request":
class PseudoFixtureDef:
- cached_result = (self, [0])
+ cached_result = (self, [0], None)
+ scope = "function"
return PseudoFixtureDef
raise
- result = self._getfuncargvalue(fixturedef)
- self._funcargs[argname] = result
- self._fixturedefs[argname] = fixturedef
- return fixturedef
+ # remove indent to prevent the python3 exception
+ # from leaking into the call
+ result = self._getfuncargvalue(fixturedef)
+ self._funcargs[argname] = result
+ self._fixturedefs[argname] = fixturedef
+ return fixturedef
def _get_fixturestack(self):
current = self
@@ -1358,17 +1663,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
# check if a higher-level scoped fixture accesses a lower level one
- if scope is not None:
- __tracebackhide__ = True
- if scopemismatch(self.scope, scope):
- # try to report something helpful
- lines = subrequest._factorytraceback()
- raise ScopeMismatchError("You tried to access the %r scoped "
- "fixture %r with a %r scoped request object, "
- "involved factories\n%s" %(
- (scope, argname, self.scope, "\n".join(lines))))
- __tracebackhide__ = False
+ subrequest._check_scope(argname, self.scope, scope)
+ # clear sys.exc_info before invoking the fixture (python bug?)
+ # if its not explicitly cleared it will leak into the call
+ exc_clear()
try:
# call the fixture function
val = fixturedef.execute(request=subrequest)
@@ -1378,13 +1677,25 @@ class FixtureRequest(FuncargnamesCompatAttr):
subrequest.node)
return val
+ def _check_scope(self, argname, invoking_scope, requested_scope):
+ if argname == "request":
+ return
+ if scopemismatch(invoking_scope, requested_scope):
+ # try to report something helpful
+ lines = self._factorytraceback()
+ pytest.fail("ScopeMismatch: You tried to access the %r scoped "
+ "fixture %r with a %r scoped request object, "
+ "involved factories\n%s" %(
+ (requested_scope, argname, invoking_scope, "\n".join(lines))),
+ pytrace=False)
+
def _factorytraceback(self):
lines = []
for fixturedef in self._get_fixturestack():
factory = fixturedef.func
fs, lineno = getfslineno(factory)
p = self._pyfuncitem.session.fspath.bestrelpath(fs)
- args = inspect.formatargspec(*inspect.getargspec(factory))
+ args = _format_args(factory)
lines.append("%s:%d: def %s%s" %(
p, lineno, factory.__name__, args))
return lines
@@ -1438,6 +1749,7 @@ scopenum_function = scopes.index("function")
def scopemismatch(currentscope, newscope):
return scopes.index(newscope) > scopes.index(currentscope)
+
class FixtureLookupError(LookupError):
""" could not return a requested Fixture (missing or invalid). """
def __init__(self, argname, request, msg=None):
@@ -1453,17 +1765,23 @@ class FixtureLookupError(LookupError):
stack.extend(map(lambda x: x.func, self.fixturestack))
msg = self.msg
if msg is not None:
- stack = stack[:-1] # the last fixture raise an error, let's present
- # it at the requesting side
+ # the last fixture raise an error, let's present
+ # it at the requesting side
+ stack = stack[:-1]
for function in stack:
fspath, lineno = getfslineno(function)
- lines, _ = inspect.getsourcelines(function)
- addline("file %s, line %s" % (fspath, lineno+1))
- for i, line in enumerate(lines):
- line = line.rstrip()
- addline(" " + line)
- if line.lstrip().startswith('def'):
- break
+ try:
+ lines, _ = inspect.getsourcelines(get_real_func(function))
+ except (IOError, IndexError):
+ error_msg = "file %s, line %s: source code not available"
+ addline(error_msg % (fspath, lineno+1))
+ else:
+ addline("file %s, line %s" % (fspath, lineno+1))
+ for i, line in enumerate(lines):
+ line = line.rstrip()
+ addline(" " + line)
+ if line.lstrip().startswith('def'):
+ break
if msg is None:
fm = self.request._fixturemanager
@@ -1536,21 +1854,13 @@ class FixtureManager:
self.session = session
self.config = session.config
self._arg2fixturedefs = {}
- self._seenplugins = set()
self._holderobjseen = set()
self._arg2finish = {}
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
session.config.pluginmanager.register(self, "funcmanage")
- self._nodename2fixtureinfo = {}
def getfixtureinfo(self, node, func, cls, funcargs=True):
- # node is the "collection node" for "func"
- key = (node, func)
- try:
- return self._nodename2fixtureinfo[key]
- except KeyError:
- pass
if funcargs and not hasattr(node, "nofuncargs"):
if cls is not None:
startindex = 1
@@ -1566,34 +1876,23 @@ class FixtureManager:
fm = node.session._fixturemanager
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames,
node)
- fixtureinfo = FuncFixtureInfo(argnames, names_closure,
- arg2fixturedefs)
- self._nodename2fixtureinfo[key] = fixtureinfo
- return fixtureinfo
+ return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
- ### XXX this hook should be called for historic events like pytest_configure
- ### so that we don't have to do the below pytest_configure hook
def pytest_plugin_registered(self, plugin):
- if plugin in self._seenplugins:
- return
nodeid = None
try:
p = py.path.local(plugin.__file__)
except AttributeError:
pass
else:
+ # construct the base nodeid which is later used to check
+ # what fixtures are visible for particular tests (as denoted
+ # by their test id)
if p.basename.startswith("conftest.py"):
- nodeid = p.dirpath().relto(self.session.fspath)
+ nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != "/":
nodeid = nodeid.replace(p.sep, "/")
self.parsefactories(plugin, nodeid)
- self._seenplugins.add(plugin)
-
- @pytest.mark.tryfirst
- def pytest_configure(self, config):
- plugins = config.pluginmanager.getplugins()
- for plugin in plugins:
- self.pytest_plugin_registered(plugin)
def _getautousenames(self, nodeid):
""" return a tuple of fixture names to be used. """
@@ -1642,13 +1941,20 @@ class FixtureManager:
def pytest_generate_tests(self, metafunc):
for argname in metafunc.fixturenames:
faclist = metafunc._arg2fixturedefs.get(argname)
- if faclist is None:
- continue # will raise FixtureLookupError at setup time
- for fixturedef in faclist:
+ if faclist:
+ fixturedef = faclist[-1]
if fixturedef.params is not None:
- metafunc.parametrize(argname, fixturedef.params,
- indirect=True, scope=fixturedef.scope,
- ids=fixturedef.ids)
+ func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]])
+ # skip directly parametrized arguments
+ argnames = func_params[0]
+ if not isinstance(argnames, (tuple, list)):
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
+ if argname not in func_params and argname not in argnames:
+ metafunc.parametrize(argname, fixturedef.params,
+ indirect=True, scope=fixturedef.scope,
+ ids=fixturedef.ids)
+ else:
+ continue # will raise FixtureLookupError at setup time
def pytest_collection_modifyitems(self, items):
# separate parametrized setups
@@ -1666,14 +1972,14 @@ class FixtureManager:
autousenames = []
for name in dir(holderobj):
obj = getattr(holderobj, name, None)
- if not callable(obj):
- continue
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
# or are "@pytest.fixture" marked
marker = getfixturemarker(obj)
if marker is None:
if not name.startswith(self._argprefix):
continue
+ if not callable(obj):
+ continue
marker = defaultfuncargprefixmarker
name = name[len(self._argprefix):]
elif not isinstance(marker, FixtureFunctionMarker):
@@ -1718,7 +2024,7 @@ class FixtureManager:
def fail_fixturefunc(fixturefunc, msg):
fs, lineno = getfslineno(fixturefunc)
location = "%s:%s" % (fs, lineno+1)
- source = py.code.Source(fixturefunc)
+ source = _pytest._code.Source(fixturefunc)
pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
pytrace=False)
@@ -1773,13 +2079,15 @@ class FixtureDef:
self._finalizer.append(finalizer)
def finish(self):
- while self._finalizer:
- func = self._finalizer.pop()
- func()
try:
- del self.cached_result
- except AttributeError:
- pass
+ while self._finalizer:
+ func = self._finalizer.pop()
+ func()
+ finally:
+ # even if finalization fails, we invalidate
+ # the cached fixture value
+ if hasattr(self, "cached_result"):
+ del self.cached_result
def execute(self, request):
# get required arguments and register our own finish()
@@ -1787,7 +2095,8 @@ class FixtureDef:
kwargs = {}
for argname in self.argnames:
fixturedef = request._get_active_fixturedef(argname)
- result, arg_cache_key = fixturedef.cached_result
+ result, arg_cache_key, exc = fixturedef.cached_result
+ request._check_scope(argname, request.scope, fixturedef.scope)
kwargs[argname] = result
if argname != "request":
fixturedef.addfinalizer(self.finish)
@@ -1795,22 +2104,24 @@ class FixtureDef:
my_cache_key = request.param_index
cached_result = getattr(self, "cached_result", None)
if cached_result is not None:
- #print argname, "Found cached_result", cached_result
- #print argname, "param_index", param_index
- result, cache_key = cached_result
+ result, cache_key, err = cached_result
if my_cache_key == cache_key:
- #print request.fixturename, "CACHE HIT", repr(my_cache_key)
- return result
- #print request.fixturename, "CACHE MISS"
+ if err is not None:
+ py.builtin._reraise(*err)
+ else:
+ return result
# we have a previous but differently parametrized fixture instance
# so we need to tear it down before creating a new one
self.finish()
assert not hasattr(self, "cached_result")
+ fixturefunc = self.func
+
if self.unittest:
- result = self.func(request.instance, **kwargs)
+ if request.instance is not None:
+ # bind the unbound method to the TestCase instance
+ fixturefunc = self.func.__get__(request.instance)
else:
- fixturefunc = self.func
# the fixture function needs to be bound to the actual
# request.instance so that code working with "self" behaves
# as expected.
@@ -1818,27 +2129,52 @@ class FixtureDef:
fixturefunc = getimfunc(self.func)
if fixturefunc != self.func:
fixturefunc = fixturefunc.__get__(request.instance)
+
+ try:
result = call_fixture_func(fixturefunc, request, kwargs,
self.yieldctx)
- self.cached_result = (result, my_cache_key)
+ except Exception:
+ self.cached_result = (None, my_cache_key, sys.exc_info())
+ raise
+ self.cached_result = (result, my_cache_key, None)
return result
def __repr__(self):
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
(self.argname, self.scope, self.baseid))
+def num_mock_patch_args(function):
+ """ return number of arguments used up by mock arguments (if any) """
+ patchings = getattr(function, "patchings", None)
+ if not patchings:
+ return 0
+ mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
+ if mock is not None:
+ return len([p for p in patchings
+ if not p.attribute_name and p.new is mock.DEFAULT])
+ return len(patchings)
+
+
def getfuncargnames(function, startindex=None):
# XXX merge with main.py's varnames
- #assert not inspect.isclass(function)
+ #assert not isclass(function)
realfunction = function
while hasattr(realfunction, "__wrapped__"):
realfunction = realfunction.__wrapped__
if startindex is None:
startindex = inspect.ismethod(function) and 1 or 0
if realfunction != function:
- startindex += len(getattr(function, "patchings", []))
+ startindex += num_mock_patch_args(function)
function = realfunction
- argnames = inspect.getargs(py.code.getrawcode(function))[0]
+ if isinstance(function, functools.partial):
+ argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0]
+ partial = function
+ argnames = argnames[len(partial.args):]
+ if partial.keywords:
+ for kw in partial.keywords:
+ argnames.remove(kw)
+ else:
+ argnames = inspect.getargs(_pytest._code.getrawcode(function))[0]
defaults = getattr(function, 'func_defaults',
getattr(function, '__defaults__', None)) or ()
numdefaults = len(defaults)
diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py
index 987ff8f973..a89474c036 100644
--- a/_pytest/recwarn.py
+++ b/_pytest/recwarn.py
@@ -1,9 +1,16 @@
""" recording warnings during test function execution. """
+import inspect
+
+import _pytest._code
import py
import sys
+import warnings
+import pytest
+
-def pytest_funcarg__recwarn(request):
+@pytest.yield_fixture
+def recwarn(request):
"""Return a WarningsRecorder instance that provides these methods:
* ``pop(category=None)``: return last warning matching the category.
@@ -12,88 +19,203 @@ def pytest_funcarg__recwarn(request):
See http://docs.python.org/library/warnings.html for information
on warning categories.
"""
- if sys.version_info >= (2,7):
- import warnings
- oldfilters = warnings.filters[:]
- warnings.simplefilter('default')
- def reset_filters():
- warnings.filters[:] = oldfilters
- request.addfinalizer(reset_filters)
wrec = WarningsRecorder()
- request.addfinalizer(wrec.finalize)
- return wrec
+ with wrec:
+ warnings.simplefilter('default')
+ yield wrec
+
def pytest_namespace():
- return {'deprecated_call': deprecated_call}
+ return {'deprecated_call': deprecated_call,
+ 'warns': warns}
+
-def deprecated_call(func, *args, **kwargs):
- """ assert that calling ``func(*args, **kwargs)``
- triggers a DeprecationWarning.
+def deprecated_call(func=None, *args, **kwargs):
+ """ assert that calling ``func(*args, **kwargs)`` triggers a
+ ``DeprecationWarning`` or ``PendingDeprecationWarning``.
+
+ This function can be used as a context manager::
+
+ >>> with deprecated_call():
+ ... myobject.deprecated_method()
+
+ Note: we cannot use WarningsRecorder here because it is still subject
+ to the mechanism that prevents warnings of the same type from being
+ triggered twice for the same module. See #1190.
"""
- warningmodule = py.std.warnings
- l = []
- oldwarn_explicit = getattr(warningmodule, 'warn_explicit')
- def warn_explicit(*args, **kwargs):
- l.append(args)
- oldwarn_explicit(*args, **kwargs)
- oldwarn = getattr(warningmodule, 'warn')
- def warn(*args, **kwargs):
- l.append(args)
- oldwarn(*args, **kwargs)
-
- warningmodule.warn_explicit = warn_explicit
- warningmodule.warn = warn
+ if not func:
+ return WarningsChecker(expected_warning=DeprecationWarning)
+
+ categories = []
+
+ def warn_explicit(message, category, *args, **kwargs):
+ categories.append(category)
+ old_warn_explicit(message, category, *args, **kwargs)
+
+ def warn(message, category=None, *args, **kwargs):
+ if isinstance(message, Warning):
+ categories.append(message.__class__)
+ else:
+ categories.append(category)
+ old_warn(message, category, *args, **kwargs)
+
+ old_warn = warnings.warn
+ old_warn_explicit = warnings.warn_explicit
+ warnings.warn_explicit = warn_explicit
+ warnings.warn = warn
try:
ret = func(*args, **kwargs)
finally:
- warningmodule.warn_explicit = warn_explicit
- warningmodule.warn = warn
- if not l:
- #print warningmodule
+ warnings.warn_explicit = old_warn_explicit
+ warnings.warn = old_warn
+ deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
+ if not any(issubclass(c, deprecation_categories) for c in categories):
__tracebackhide__ = True
- raise AssertionError("%r did not produce DeprecationWarning" %(func,))
+ raise AssertionError("%r did not produce DeprecationWarning" % (func,))
return ret
-class RecordedWarning:
- def __init__(self, message, category, filename, lineno, line):
+def warns(expected_warning, *args, **kwargs):
+ """Assert that code raises a particular class of warning.
+
+ Specifically, the input @expected_warning can be a warning class or
+ tuple of warning classes, and the code must return that warning
+ (if a single class) or one of those warnings (if a tuple).
+
+ This helper produces a list of ``warnings.WarningMessage`` objects,
+ one for each warning raised.
+
+ This function can be used as a context manager, or any of the other ways
+ ``pytest.raises`` can be used::
+
+ >>> with warns(RuntimeWarning):
+ ... warnings.warn("my warning", RuntimeWarning)
+ """
+ wcheck = WarningsChecker(expected_warning)
+ if not args:
+ return wcheck
+ elif isinstance(args[0], str):
+ code, = args
+ assert isinstance(code, str)
+ frame = sys._getframe(1)
+ loc = frame.f_locals.copy()
+ loc.update(kwargs)
+
+ with wcheck:
+ code = _pytest._code.Source(code).compile()
+ py.builtin.exec_(code, frame.f_globals, loc)
+ else:
+ func = args[0]
+ with wcheck:
+ return func(*args[1:], **kwargs)
+
+
+class RecordedWarning(object):
+ def __init__(self, message, category, filename, lineno, file, line):
self.message = message
self.category = category
self.filename = filename
self.lineno = lineno
+ self.file = file
self.line = line
-class WarningsRecorder:
- def __init__(self):
- warningmodule = py.std.warnings
- self.list = []
- def showwarning(message, category, filename, lineno, line=0):
- self.list.append(RecordedWarning(
- message, category, filename, lineno, line))
- try:
- self.old_showwarning(message, category,
- filename, lineno, line=line)
- except TypeError:
- # < python2.6
- self.old_showwarning(message, category, filename, lineno)
- self.old_showwarning = warningmodule.showwarning
- warningmodule.showwarning = showwarning
+
+class WarningsRecorder(object):
+ """A context manager to record raised warnings.
+
+ Adapted from `warnings.catch_warnings`.
+ """
+
+ def __init__(self, module=None):
+ self._module = sys.modules['warnings'] if module is None else module
+ self._entered = False
+ self._list = []
+
+ @property
+ def list(self):
+ """The list of recorded warnings."""
+ return self._list
+
+ def __getitem__(self, i):
+ """Get a recorded warning by index."""
+ return self._list[i]
+
+ def __iter__(self):
+ """Iterate through the recorded warnings."""
+ return iter(self._list)
+
+ def __len__(self):
+ """The number of recorded warnings."""
+ return len(self._list)
def pop(self, cls=Warning):
- """ pop the first recorded warning, raise exception if not exists."""
- for i, w in enumerate(self.list):
+ """Pop the first recorded warning, raise exception if not exists."""
+ for i, w in enumerate(self._list):
if issubclass(w.category, cls):
- return self.list.pop(i)
+ return self._list.pop(i)
__tracebackhide__ = True
- assert 0, "%r not found in %r" %(cls, self.list)
-
- #def resetregistry(self):
- # import warnings
- # warnings.onceregistry.clear()
- # warnings.__warningregistry__.clear()
+ raise AssertionError("%r not found in warning list" % cls)
def clear(self):
- self.list[:] = []
+ """Clear the list of recorded warnings."""
+ self._list[:] = []
+
+ def __enter__(self):
+ if self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError("Cannot enter %r twice" % self)
+ self._entered = True
+ self._filters = self._module.filters
+ self._module.filters = self._filters[:]
+ self._showwarning = self._module.showwarning
+
+ def showwarning(message, category, filename, lineno,
+ file=None, line=None):
+ self._list.append(RecordedWarning(
+ message, category, filename, lineno, file, line))
+
+ # still perform old showwarning functionality
+ self._showwarning(
+ message, category, filename, lineno, file=file, line=line)
+
+ self._module.showwarning = showwarning
+
+ # allow the same warning to be raised more than once
+
+ self._module.simplefilter('always')
+ return self
+
+ def __exit__(self, *exc_info):
+ if not self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError("Cannot exit %r without entering first" % self)
+ self._module.filters = self._filters
+ self._module.showwarning = self._showwarning
+
+
+class WarningsChecker(WarningsRecorder):
+ def __init__(self, expected_warning=None, module=None):
+ super(WarningsChecker, self).__init__(module=module)
+
+ msg = ("exceptions must be old-style classes or "
+ "derived from Warning, not %s")
+ if isinstance(expected_warning, tuple):
+ for exc in expected_warning:
+ if not inspect.isclass(exc):
+ raise TypeError(msg % type(exc))
+ elif inspect.isclass(expected_warning):
+ expected_warning = (expected_warning,)
+ elif expected_warning is not None:
+ raise TypeError(msg % type(expected_warning))
+
+ self.expected_warning = expected_warning
+
+ def __exit__(self, *exc_info):
+ super(WarningsChecker, self).__exit__(*exc_info)
- def finalize(self):
- py.std.warnings.showwarning = self.old_showwarning
+ # only check if we're not currently handling an exception
+ if all(a is None for a in exc_info):
+ if self.expected_warning is not None:
+ if not any(r.category in self.expected_warning for r in self):
+ __tracebackhide__ = True
+ pytest.fail("DID NOT WARN")
diff --git a/_pytest/resultlog.py b/_pytest/resultlog.py
index 0c100552f0..3670f0214c 100644
--- a/_pytest/resultlog.py
+++ b/_pytest/resultlog.py
@@ -3,6 +3,7 @@ text file.
"""
import py
+import os
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "resultlog plugin options")
@@ -14,6 +15,9 @@ def pytest_configure(config):
resultlog = config.option.resultlog
# prevent opening resultlog on slave nodes (xdist)
if resultlog and not hasattr(config, 'slaveinput'):
+ dirname = os.path.dirname(os.path.abspath(resultlog))
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
logfile = open(resultlog, 'w', 1) # line buffered
config._resultlog = ResultLog(config, logfile)
config.pluginmanager.register(config._resultlog)
diff --git a/_pytest/runner.py b/_pytest/runner.py
index 5392481178..cde94c8c89 100644
--- a/_pytest/runner.py
+++ b/_pytest/runner.py
@@ -1,10 +1,12 @@
""" basic collect and runtest protocol implementations """
+import bdb
+import sys
+from time import time
import py
import pytest
-import sys
-from time import time
-from py._code.code import TerminalRepr
+from _pytest._code.code import TerminalRepr, ExceptionInfo
+
def pytest_namespace():
return {
@@ -85,7 +87,17 @@ def pytest_runtest_setup(item):
item.session._setupstate.prepare(item)
def pytest_runtest_call(item):
- item.runtest()
+ try:
+ item.runtest()
+ except Exception:
+ # Store trace info to allow postmortem debugging
+ type, value, tb = sys.exc_info()
+ tb = tb.tb_next # Skip *this* frame
+ sys.last_type = type
+ sys.last_value = value
+ sys.last_traceback = tb
+ del tb # Get rid of it in this namespace
+ raise
def pytest_runtest_teardown(item, nextitem):
item.session._setupstate.teardown_exact(item, nextitem)
@@ -118,7 +130,7 @@ def check_interactive_exception(call, report):
return call.excinfo and not (
hasattr(report, "wasxfail") or
call.excinfo.errisinstance(skip.Exception) or
- call.excinfo.errisinstance(py.std.bdb.BdbQuit))
+ call.excinfo.errisinstance(bdb.BdbQuit))
def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when
@@ -135,14 +147,13 @@ class CallInfo:
self.when = when
self.start = time()
try:
- try:
- self.result = func()
- except KeyboardInterrupt:
- raise
- except:
- self.excinfo = py.code.ExceptionInfo()
- finally:
+ self.result = func()
+ except KeyboardInterrupt:
self.stop = time()
+ raise
+ except:
+ self.excinfo = ExceptionInfo()
+ self.stop = time()
def __repr__(self):
if self.excinfo:
@@ -167,9 +178,13 @@ class BaseReport(object):
self.__dict__.update(kw)
def toterminal(self, out):
- longrepr = self.longrepr
if hasattr(self, 'node'):
out.line(getslaveinfoline(self.node))
+
+ longrepr = self.longrepr
+ if longrepr is None:
+ return
+
if hasattr(longrepr, 'toterminal'):
longrepr.toterminal(out)
else:
@@ -178,6 +193,11 @@ class BaseReport(object):
except UnicodeEncodeError:
out.line("<unprintable longrepr>")
+ def get_sections(self, prefix):
+ for name, content in self.sections:
+ if name.startswith(prefix):
+ yield prefix, content
+
passed = property(lambda x: x.outcome == "passed")
failed = property(lambda x: x.outcome == "failed")
skipped = property(lambda x: x.outcome == "skipped")
@@ -191,11 +211,12 @@ def pytest_runtest_makereport(item, call):
duration = call.stop-call.start
keywords = dict([(x,1) for x in item.keywords])
excinfo = call.excinfo
+ sections = []
if not call.excinfo:
outcome = "passed"
longrepr = None
else:
- if not isinstance(excinfo, py.code.ExceptionInfo):
+ if not isinstance(excinfo, ExceptionInfo):
outcome = "failed"
longrepr = excinfo
elif excinfo.errisinstance(pytest.skip.Exception):
@@ -209,16 +230,18 @@ def pytest_runtest_makereport(item, call):
else: # exception in setup or teardown
longrepr = item._repr_failure_py(excinfo,
style=item.config.option.tbstyle)
+ for rwhen, key, content in item._report_sections:
+ sections.append(("Captured %s %s" %(key, rwhen), content))
return TestReport(item.nodeid, item.location,
keywords, outcome, longrepr, when,
- duration=duration)
+ sections, duration)
class TestReport(BaseReport):
""" Basic test report object (also used for setup and teardown calls if
they fail).
"""
- def __init__(self, nodeid, location,
- keywords, outcome, longrepr, when, sections=(), duration=0, **extra):
+ def __init__(self, nodeid, location, keywords, outcome,
+ longrepr, when, sections=(), duration=0, **extra):
#: normalized collection node id
self.nodeid = nodeid
@@ -267,7 +290,9 @@ def pytest_make_collect_report(collector):
if not call.excinfo:
outcome = "passed"
else:
- if call.excinfo.errisinstance(collector.skip_exceptions):
+ from _pytest import nose
+ skip_exceptions = (Skipped,) + nose.get_skip_exceptions()
+ if call.excinfo.errisinstance(skip_exceptions):
outcome = "skipped"
r = collector._repr_failure_py(call.excinfo, "line").reprcrash
longrepr = (str(r.path), r.lineno, r.message)
@@ -284,7 +309,8 @@ def pytest_make_collect_report(collector):
class CollectReport(BaseReport):
- def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
+ def __init__(self, nodeid, outcome, longrepr, result,
+ sections=(), **extra):
self.nodeid = nodeid
self.outcome = outcome
self.longrepr = longrepr
@@ -318,7 +344,7 @@ class SetupState(object):
is called at the end of teardown_all().
"""
assert colitem and not isinstance(colitem, tuple)
- assert callable(finalizer)
+ assert py.builtin.callable(finalizer)
#assert colitem in self.stack # some unit tests don't setup stack :/
self._finalizers.setdefault(colitem, []).append(finalizer)
@@ -409,7 +435,10 @@ class OutcomeException(Exception):
def __repr__(self):
if self.msg:
- return str(self.msg)
+ val = self.msg
+ if isinstance(val, bytes):
+ val = py._builtin._totext(val, errors='replace')
+ return val
return "<%s instance>" %(self.__class__.__name__,)
__str__ = __repr__
@@ -448,7 +477,7 @@ def skip(msg=""):
skip.Exception = Skipped
def fail(msg="", pytrace=True):
- """ explicitely fail an currently-executing test with the given Message.
+ """ explicitly fail an currently-executing test with the given Message.
:arg pytrace: if false the msg represents the full failure information
and no python traceback will be reported.
@@ -462,8 +491,6 @@ def importorskip(modname, minversion=None):
""" return imported module if it has at least "minversion" as its
__version__ attribute. If no minversion is specified the a skip
is only triggered if the module can not be imported.
- Note that version comparison only works with simple version strings
- like "1.2.3" but not "1.2.3.dev1" or others.
"""
__tracebackhide__ = True
compile(modname, '', 'eval') # to catch syntaxerrors
@@ -475,9 +502,14 @@ def importorskip(modname, minversion=None):
if minversion is None:
return mod
verattr = getattr(mod, '__version__', None)
- def intver(verstring):
- return [int(x) for x in verstring.split(".")]
- if verattr is None or intver(verattr) < intver(minversion):
- skip("module %r has __version__ %r, required is: %r" %(
- modname, verattr, minversion))
+ if minversion is not None:
+ try:
+ from pkg_resources import parse_version as pv
+ except ImportError:
+ skip("we have a required version for %r but can not import "
+ "no pkg_resources to parse version strings." %(modname,))
+ if verattr is None or pv(verattr) < pv(minversion):
+ skip("module %r has __version__ %r, required is: %r" %(
+ modname, verattr, minversion))
return mod
+
diff --git a/_pytest/skipping.py b/_pytest/skipping.py
index a370b64e4a..18e038d2c8 100644
--- a/_pytest/skipping.py
+++ b/_pytest/skipping.py
@@ -1,7 +1,12 @@
""" support for skip/xfail functions and markers. """
-
-import py, pytest
+import os
import sys
+import traceback
+
+import py
+import pytest
+from _pytest.mark import MarkInfo, MarkDecorator
+
def pytest_addoption(parser):
group = parser.getgroup("general")
@@ -9,6 +14,13 @@ def pytest_addoption(parser):
action="store_true", dest="runxfail", default=False,
help="run tests even if they are marked xfail")
+ parser.addini("xfail_strict", "default for the strict parameter of xfail "
+ "markers when not given explicitly (default: "
+ "False)",
+ default=False,
+ type="bool")
+
+
def pytest_configure(config):
if config.option.runxfail:
old = pytest.xfail
@@ -19,6 +31,11 @@ def pytest_configure(config):
setattr(pytest, "xfail", nop)
config.addinivalue_line("markers",
+ "skip(reason=None): skip the given test function with an optional reason. "
+ "Example: skip(reason=\"no way of currently testing this\") skips the "
+ "test."
+ )
+ config.addinivalue_line("markers",
"skipif(condition): skip the given test function if eval(condition) "
"results in a True value. Evaluation happens within the "
"module global context. Example: skipif('sys.platform == \"win32\"') "
@@ -26,25 +43,31 @@ def pytest_configure(config):
"http://pytest.org/latest/skipping.html"
)
config.addinivalue_line("markers",
- "xfail(condition, reason=None, run=True): mark the the test function "
- "as an expected failure if eval(condition) has a True value. "
- "Optionally specify a reason for better reporting and run=False if "
- "you don't even want to execute the test function. See "
- "http://pytest.org/latest/skipping.html"
+ "xfail(condition, reason=None, run=True, raises=None, strict=False): "
+ "mark the the test function as an expected failure if eval(condition) "
+ "has a True value. Optionally specify a reason for better reporting "
+ "and run=False if you don't even want to execute the test function. "
+ "If only specific exception(s) are expected, you can list them in "
+ "raises, and if the test fails in other ways, it will be reported as "
+ "a true failure. See http://pytest.org/latest/skipping.html"
)
+
def pytest_namespace():
return dict(xfail=xfail)
+
class XFailed(pytest.fail.Exception):
""" raised from an explicit call to pytest.xfail() """
+
def xfail(reason=""):
""" xfail an executing test or setup functions with the given reason."""
__tracebackhide__ = True
raise XFailed(reason)
xfail.Exception = XFailed
+
class MarkEvaluator:
def __init__(self, item, name):
self.item = item
@@ -52,7 +75,8 @@ class MarkEvaluator:
@property
def holder(self):
- return self.item.keywords.get(self.name, None)
+ return self.item.keywords.get(self.name)
+
def __bool__(self):
return bool(self.holder)
__nonzero__ = __bool__
@@ -60,18 +84,22 @@ class MarkEvaluator:
def wasvalid(self):
return not hasattr(self, 'exc')
+ def invalidraise(self, exc):
+ raises = self.get('raises')
+ if not raises:
+ return
+ return not isinstance(exc, raises)
+
def istrue(self):
try:
return self._istrue()
- except KeyboardInterrupt:
- raise
- except:
+ except Exception:
self.exc = sys.exc_info()
if isinstance(self.exc[1], SyntaxError):
msg = [" " * (self.exc[1].offset + 4) + "^",]
msg.append("SyntaxError: invalid syntax")
else:
- msg = py.std.traceback.format_exception_only(*self.exc[:2])
+ msg = traceback.format_exception_only(*self.exc[:2])
pytest.fail("Error evaluating %r expression\n"
" %s\n"
"%s"
@@ -79,7 +107,7 @@ class MarkEvaluator:
pytrace=False)
def _getglobals(self):
- d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config}
+ d = {'os': os, 'sys': sys, 'config': self.item.config}
func = self.item.obj
try:
d.update(func.__globals__)
@@ -88,24 +116,38 @@ class MarkEvaluator:
return d
def _istrue(self):
+ if hasattr(self, 'result'):
+ return self.result
if self.holder:
d = self._getglobals()
- if self.holder.args:
+ if self.holder.args or 'condition' in self.holder.kwargs:
self.result = False
- for expr in self.holder.args:
- self.expr = expr
- if isinstance(expr, py.builtin._basestring):
- result = cached_eval(self.item.config, expr, d)
- else:
- if self.get("reason") is None:
- # XXX better be checked at collection time
- pytest.fail("you need to specify reason=STRING "
- "when using booleans as conditions.")
- result = bool(expr)
- if result:
- self.result = True
+ # "holder" might be a MarkInfo or a MarkDecorator; only
+ # MarkInfo keeps track of all parameters it received in an
+ # _arglist attribute
+ if hasattr(self.holder, '_arglist'):
+ arglist = self.holder._arglist
+ else:
+ arglist = [(self.holder.args, self.holder.kwargs)]
+ for args, kwargs in arglist:
+ if 'condition' in kwargs:
+ args = (kwargs['condition'],)
+ for expr in args:
self.expr = expr
- break
+ if isinstance(expr, py.builtin._basestring):
+ result = cached_eval(self.item.config, expr, d)
+ else:
+ if "reason" not in kwargs:
+ # XXX better be checked at collection time
+ msg = "you need to specify reason=STRING " \
+ "when using booleans as conditions."
+ pytest.fail(msg)
+ result = bool(expr)
+ if result:
+ self.result = True
+ self.reason = kwargs.get('reason', None)
+ self.expr = expr
+ return self.result
else:
self.result = True
return getattr(self, 'result', False)
@@ -114,7 +156,7 @@ class MarkEvaluator:
return self.holder.kwargs.get(attr, default)
def getexplanation(self):
- expl = self.get('reason', None)
+ expl = getattr(self, 'reason', None) or self.get('reason', None)
if not expl:
if not hasattr(self, 'expr'):
return ""
@@ -123,62 +165,95 @@ class MarkEvaluator:
return expl
-@pytest.mark.tryfirst
+@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup(item):
- if not isinstance(item, pytest.Function):
- return
- evalskip = MarkEvaluator(item, 'skipif')
- if evalskip.istrue():
- pytest.skip(evalskip.getexplanation())
+ # Check if skip or skipif are specified as pytest marks
+
+ skipif_info = item.keywords.get('skipif')
+ if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
+ eval_skipif = MarkEvaluator(item, 'skipif')
+ if eval_skipif.istrue():
+ item._evalskip = eval_skipif
+ pytest.skip(eval_skipif.getexplanation())
+
+ skip_info = item.keywords.get('skip')
+ if isinstance(skip_info, (MarkInfo, MarkDecorator)):
+ item._evalskip = True
+ if 'reason' in skip_info.kwargs:
+ pytest.skip(skip_info.kwargs['reason'])
+ elif skip_info.args:
+ pytest.skip(skip_info.args[0])
+ else:
+ pytest.skip("unconditional skip")
+
item._evalxfail = MarkEvaluator(item, 'xfail')
check_xfail_no_run(item)
+
+@pytest.mark.hookwrapper
def pytest_pyfunc_call(pyfuncitem):
check_xfail_no_run(pyfuncitem)
+ outcome = yield
+ passed = outcome.excinfo is None
+ if passed:
+ check_strict_xfail(pyfuncitem)
+
def check_xfail_no_run(item):
+ """check xfail(run=False)"""
if not item.config.option.runxfail:
evalxfail = item._evalxfail
if evalxfail.istrue():
if not evalxfail.get('run', True):
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
-def pytest_runtest_makereport(__multicall__, item, call):
- if not isinstance(item, pytest.Function):
- return
+
+def check_strict_xfail(pyfuncitem):
+ """check xfail(strict=True) for the given PASSING test"""
+ evalxfail = pyfuncitem._evalxfail
+ if evalxfail.istrue():
+ strict_default = pyfuncitem.config.getini('xfail_strict')
+ is_strict_xfail = evalxfail.get('strict', strict_default)
+ if is_strict_xfail:
+ del pyfuncitem._evalxfail
+ explanation = evalxfail.getexplanation()
+ pytest.fail('[XPASS(strict)] ' + explanation, pytrace=False)
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_makereport(item, call):
+ outcome = yield
+ rep = outcome.get_result()
+ evalxfail = getattr(item, '_evalxfail', None)
+ evalskip = getattr(item, '_evalskip', None)
# unitttest special case, see setting of _unexpectedsuccess
- if hasattr(item, '_unexpectedsuccess'):
- rep = __multicall__.execute()
- if rep.when == "call":
- # we need to translate into how pytest encodes xpass
- rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
- rep.outcome = "failed"
- return rep
- if not (call.excinfo and
- call.excinfo.errisinstance(pytest.xfail.Exception)):
- evalxfail = getattr(item, '_evalxfail', None)
- if not evalxfail:
- return
- if call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
- if not item.config.getvalue("runxfail"):
- rep = __multicall__.execute()
- rep.wasxfail = "reason: " + call.excinfo.value.msg
- rep.outcome = "skipped"
- return rep
- rep = __multicall__.execute()
- evalxfail = item._evalxfail
- if not rep.skipped:
- if not item.config.option.runxfail:
- if evalxfail.wasvalid() and evalxfail.istrue():
- if call.excinfo:
- rep.outcome = "skipped"
- elif call.when == "call":
- rep.outcome = "failed"
- else:
- return rep
+ if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
+ # we need to translate into how pytest encodes xpass
+ rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
+ rep.outcome = "failed"
+ elif item.config.option.runxfail:
+ pass # don't interefere
+ elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
+ rep.wasxfail = "reason: " + call.excinfo.value.msg
+ rep.outcome = "skipped"
+ elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \
+ evalxfail.istrue():
+ if call.excinfo:
+ if evalxfail.invalidraise(call.excinfo.value):
+ rep.outcome = "failed"
+ else:
+ rep.outcome = "skipped"
rep.wasxfail = evalxfail.getexplanation()
- return rep
- return rep
+ elif call.when == "call":
+ rep.outcome = "failed" # xpass outcome
+ rep.wasxfail = evalxfail.getexplanation()
+ elif evalskip is not None and rep.skipped and type(rep.longrepr) is tuple:
+ # skipped by mark.skipif; change the location of the failure
+ # to point to the item definition, otherwise it will display
+ # the location of where the skip exception was raised within pytest
+ filename, line, reason = rep.longrepr
+ filename, line = item.location[:2]
+ rep.longrepr = filename, line, reason
# called by terminalreporter progress reporting
def pytest_report_teststatus(report):
@@ -186,7 +261,7 @@ def pytest_report_teststatus(report):
if report.skipped:
return "xfailed", "x", "xfail"
elif report.failed:
- return "xpassed", "X", "XPASS"
+ return "xpassed", "X", ("XPASS", {'yellow': True})
# called by the terminalreporter instance/plugin
def pytest_terminal_summary(terminalreporter):
@@ -211,6 +286,9 @@ def pytest_terminal_summary(terminalreporter):
show_skipped(terminalreporter, lines)
elif char == "E":
show_simple(terminalreporter, lines, 'error', "ERROR %s")
+ elif char == 'p':
+ show_simple(terminalreporter, lines, 'passed', "PASSED %s")
+
if lines:
tr._tw.sep("=", "short test summary info")
for line in lines:
@@ -220,14 +298,14 @@ def show_simple(terminalreporter, lines, stat, format):
failed = terminalreporter.stats.get(stat)
if failed:
for rep in failed:
- pos = rep.nodeid
- lines.append(format %(pos, ))
+ pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
+ lines.append(format %(pos,))
def show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed")
if xfailed:
for rep in xfailed:
- pos = rep.nodeid
+ pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
lines.append("XFAIL %s" % (pos,))
if reason:
@@ -237,7 +315,7 @@ def show_xpassed(terminalreporter, lines):
xpassed = terminalreporter.stats.get("xpassed")
if xpassed:
for rep in xpassed:
- pos = rep.nodeid
+ pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
lines.append("XPASS %s %s" %(pos, reason))
@@ -247,9 +325,8 @@ def cached_eval(config, expr, d):
try:
return config._evalcache[expr]
except KeyError:
- #import sys
- #print >>sys.stderr, ("cache-miss: %r" % expr)
- exprcode = py.code.compile(expr, mode="eval")
+ import _pytest._code
+ exprcode = _pytest._code.compile(expr, mode="eval")
config._evalcache[expr] = x = eval(exprcode, d)
return x
diff --git a/_pytest/standalonetemplate.py b/_pytest/standalonetemplate.py
index b67bf20f31..484d5d1b25 100755
--- a/_pytest/standalonetemplate.py
+++ b/_pytest/standalonetemplate.py
@@ -1,5 +1,24 @@
#! /usr/bin/env python
+# Hi There!
+# You may be wondering what this giant blob of binary data here is, you might
+# even be worried that we're up to something nefarious (good for you for being
+# paranoid!). This is a base64 encoding of a zip file, this zip file contains
+# a fully functional basic pytest script.
+#
+# Pytest is a thing that tests packages, pytest itself is a package that some-
+# one might want to install, especially if they're looking to run tests inside
+# some package they want to install. Pytest has a lot of code to collect and
+# execute tests, and other such sort of "tribal knowledge" that has been en-
+# coded in its code base. Because of this we basically include a basic copy
+# of pytest inside this blob. We do this because it let's you as a maintainer
+# or application developer who wants people who don't deal with python much to
+# easily run tests without installing the complete pytest package.
+#
+# If you're wondering how this is created: you can create it yourself if you
+# have a complete pytest installation by using this command on the command-
+# line: ``py.test --genscript=runtests.py``.
+
sources = """
@SOURCES@"""
@@ -49,6 +68,11 @@ class DictImporter(object):
return res
if __name__ == "__main__":
+ try:
+ import pkg_resources # noqa
+ except ImportError:
+ sys.stderr.write("ERROR: setuptools not installed\n")
+ sys.exit(2)
if sys.version_info >= (3, 0):
exec("def do_exec(co, loc): exec(co, loc)\n")
import pickle
@@ -61,6 +85,5 @@ if __name__ == "__main__":
importer = DictImporter(sources)
sys.meta_path.insert(0, importer)
-
entry = "@ENTRY@"
do_exec(entry, locals()) # noqa
diff --git a/_pytest/terminal.py b/_pytest/terminal.py
index 1b9f8905fb..825f553ef2 100644
--- a/_pytest/terminal.py
+++ b/_pytest/terminal.py
@@ -2,9 +2,16 @@
This is a good source for looking at the various reporting hooks.
"""
+from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
+ EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
import pytest
import py
import sys
+import time
+import platform
+
+import _pytest._pluggy as pluggy
+
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general")
@@ -15,7 +22,8 @@ def pytest_addoption(parser):
group._addoption('-r',
action="store", dest="reportchars", default=None, metavar="chars",
help="show extra test summary info as specified by chars (f)ailed, "
- "(E)error, (s)skipped, (x)failed, (X)passed.")
+ "(E)error, (s)skipped, (x)failed, (X)passed (w)pytest-warnings "
+ "(p)passed, (P)passed with output, (a)all except pP.")
group._addoption('-l', '--showlocals',
action="store_true", dest="showlocals", default=False,
help="show locals in tracebacks (disabled by default).")
@@ -23,9 +31,9 @@ def pytest_addoption(parser):
action="store", dest="report", default=None, metavar="opts",
help="(deprecated, use -r)")
group._addoption('--tb', metavar="style",
- action="store", dest="tbstyle", default='long',
- choices=['long', 'short', 'no', 'line', 'native'],
- help="traceback print mode (long/short/line/native/no).")
+ action="store", dest="tbstyle", default='auto',
+ choices=['auto', 'long', 'short', 'no', 'line', 'native'],
+ help="traceback print mode (auto/long/short/line/native/no).")
group._addoption('--fulltrace', '--full-trace',
action="store_true", default=False,
help="don't cut any tracebacks (default is to cut).")
@@ -49,7 +57,7 @@ def getreportopt(config):
optvalue = config.option.report
if optvalue:
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
- file=py.std.sys.stderr)
+ file=sys.stderr)
if optvalue:
for setting in optvalue.split(","):
setting = setting.strip()
@@ -60,8 +68,10 @@ def getreportopt(config):
reportchars = config.option.reportchars
if reportchars:
for char in reportchars:
- if char not in reportopts:
+ if char not in reportopts and char != 'a':
reportopts += char
+ elif char == 'a':
+ reportopts = 'fEsxXw'
return reportopts
def pytest_report_teststatus(report):
@@ -75,8 +85,17 @@ def pytest_report_teststatus(report):
letter = "f"
return report.outcome, letter, report.outcome.upper()
+class WarningReport:
+ def __init__(self, code, message, nodeid=None, fslocation=None):
+ self.code = code
+ self.message = message
+ self.nodeid = nodeid
+ self.fslocation = fslocation
+
+
class TerminalReporter:
def __init__(self, config, file=None):
+ import _pytest.config
self.config = config
self.verbosity = self.config.option.verbose
self.showheader = self.verbosity >= 0
@@ -85,28 +104,26 @@ class TerminalReporter:
self._numcollected = 0
self.stats = {}
- self.startdir = self.curdir = py.path.local()
+ self.startdir = py.path.local()
if file is None:
- file = py.std.sys.stdout
- self._tw = self.writer = py.io.TerminalWriter(file)
- if self.config.option.color == 'yes':
- self._tw.hasmarkup = True
- if self.config.option.color == 'no':
- self._tw.hasmarkup = False
+ file = sys.stdout
+ self._tw = self.writer = _pytest.config.create_terminal_writer(config,
+ file)
self.currentfspath = None
self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup
+ self.isatty = file.isatty()
def hasopt(self, char):
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
return char in self.reportchars
- def write_fspath_result(self, fspath, res):
+ def write_fspath_result(self, nodeid, res):
+ fspath = self.config.rootdir.join(nodeid.split("::")[0])
if fspath != self.currentfspath:
self.currentfspath = fspath
- #fspath = self.startdir.bestrelpath(fspath)
+ fspath = self.startdir.bestrelpath(fspath)
self._tw.line()
- #relpath = self.startdir.bestrelpath(fspath)
self._tw.write(fspath + " ")
self._tw.write(res)
@@ -128,7 +145,8 @@ class TerminalReporter:
self._tw.write(content, **markup)
def write_line(self, line, **markup):
- line = str(line)
+ if not py.builtin._istext(line):
+ line = py.builtin.text(line, errors="replace")
self.ensure_newline()
self._tw.line(line, **markup)
@@ -147,10 +165,18 @@ class TerminalReporter:
self._tw.line(msg, **kw)
def pytest_internalerror(self, excrepr):
- for line in str(excrepr).split("\n"):
+ for line in py.builtin.text(excrepr).split("\n"):
self.write_line("INTERNALERROR> " + line)
return 1
+ def pytest_logwarning(self, code, fslocation, message, nodeid):
+ warnings = self.stats.setdefault("warnings", [])
+ if isinstance(fslocation, tuple):
+ fslocation = "%s:%d" % fslocation
+ warning = WarningReport(code=code, fslocation=fslocation,
+ message=message, nodeid=nodeid)
+ warnings.append(warning)
+
def pytest_plugin_registered(self, plugin):
if self.config.option.traceconfig:
msg = "PLUGIN registered: %s" % (plugin,)
@@ -165,12 +191,12 @@ class TerminalReporter:
def pytest_runtest_logstart(self, nodeid, location):
# ensure that the path is printed before the
# 1st test of a module starts running
- fspath = nodeid.split("::")[0]
if self.showlongtestinfo:
- line = self._locationline(fspath, *location)
+ line = self._locationline(nodeid, *location)
self.write_ensure_prefix(line, "")
elif self.showfspath:
- self.write_fspath_result(fspath, "")
+ fsid = nodeid.split("::")[0]
+ self.write_fspath_result(fsid, "")
def pytest_runtest_logreport(self, report):
rep = report
@@ -183,7 +209,7 @@ class TerminalReporter:
return
if self.verbosity <= 0:
if not hasattr(rep, 'node') and self.showfspath:
- self.write_fspath_result(rep.fspath, letter)
+ self.write_fspath_result(rep.nodeid, letter)
else:
self._tw.write(letter)
else:
@@ -196,7 +222,7 @@ class TerminalReporter:
markup = {'red':True}
elif rep.skipped:
markup = {'yellow':True}
- line = self._locationline(str(rep.fspath), *rep.location)
+ line = self._locationline(rep.nodeid, *rep.location)
if not hasattr(rep, 'node'):
self.write_ensure_prefix(line, word, **markup)
#self._tw.write(word, **markup)
@@ -209,7 +235,7 @@ class TerminalReporter:
self.currentfspath = -2
def pytest_collection(self):
- if not self.hasmarkup and self.config.option.verbose >= 1:
+ if not self.isatty and self.config.option.verbose >= 1:
self.write("collecting ... ", bold=True)
def pytest_collectreport(self, report):
@@ -219,8 +245,8 @@ class TerminalReporter:
self.stats.setdefault("skipped", []).append(report)
items = [x for x in report.result if isinstance(x, pytest.Item)]
self._numcollected += len(items)
- if self.hasmarkup:
- #self.write_fspath_result(report.fspath, 'E')
+ if self.isatty:
+ #self.write_fspath_result(report.nodeid, 'E')
self.report_collect()
def report_collect(self, final=False):
@@ -238,7 +264,7 @@ class TerminalReporter:
line += " / %d errors" % errors
if skipped:
line += " / %d skipped" % skipped
- if self.hasmarkup:
+ if self.isatty:
if final:
line += " \n"
self.rewrite(line, bold=True)
@@ -248,18 +274,19 @@ class TerminalReporter:
def pytest_collection_modifyitems(self):
self.report_collect(True)
- @pytest.mark.trylast
+ @pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session):
- self._sessionstarttime = py.std.time.time()
+ self._sessionstarttime = time.time()
if not self.showheader:
return
self.write_sep("=", "test session starts", bold=True)
- verinfo = ".".join(map(str, sys.version_info[:3]))
+ verinfo = platform.python_version()
msg = "platform %s -- Python %s" % (sys.platform, verinfo)
if hasattr(sys, 'pypy_version_info'):
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
- msg += " -- py-%s -- pytest-%s" % (py.__version__, pytest.__version__)
+ msg += ", pytest-%s, py-%s, pluggy-%s" % (
+ pytest.__version__, py.__version__, pluggy.__version__)
if self.verbosity > 0 or self.config.option.debug or \
getattr(self.config.option, 'pastebin', None):
msg += " -- " + str(sys.executable)
@@ -271,15 +298,17 @@ class TerminalReporter:
self.write_line(line)
def pytest_report_header(self, config):
- plugininfo = config.pluginmanager._plugin_distinfo
+ inifile = ""
+ if config.inifile:
+ inifile = config.rootdir.bestrelpath(config.inifile)
+ lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
+
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
- l = []
- for dist, plugin in plugininfo:
- name = dist.project_name
- if name.startswith("pytest-"):
- name = name[7:]
- l.append(name)
- return "plugins: %s" % ", ".join(l)
+
+ lines.append(
+ "plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
+ return lines
def pytest_collection_finish(self, session):
if self.config.option.collectonly:
@@ -328,15 +357,21 @@ class TerminalReporter:
indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col))
- def pytest_sessionfinish(self, exitstatus, __multicall__):
- __multicall__.execute()
+ @pytest.hookimpl(hookwrapper=True)
+ def pytest_sessionfinish(self, exitstatus):
+ outcome = yield
+ outcome.get_result()
self._tw.line("")
- if exitstatus in (0, 1, 2, 4):
+ summary_exit_codes = (
+ EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
+ EXIT_NOTESTSCOLLECTED)
+ if exitstatus in summary_exit_codes:
+ self.config.hook.pytest_terminal_summary(terminalreporter=self)
self.summary_errors()
self.summary_failures()
- self.summary_hints()
- self.config.hook.pytest_terminal_summary(terminalreporter=self)
- if exitstatus == 2:
+ self.summary_warnings()
+ self.summary_passes()
+ if exitstatus == EXIT_INTERRUPTED:
self._report_keyboardinterrupt()
del self._keyboardinterrupt_memo
self.summary_deselected()
@@ -357,22 +392,27 @@ class TerminalReporter:
if self.config.option.fulltrace:
excrepr.toterminal(self._tw)
else:
+ self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True)
excrepr.reprcrash.toterminal(self._tw)
- def _locationline(self, collect_fspath, fspath, lineno, domain):
+ def _locationline(self, nodeid, fspath, lineno, domain):
+ def mkrel(nodeid):
+ line = self.config.cwd_relative_nodeid(nodeid)
+ if domain and line.endswith(domain):
+ line = line[:-len(domain)]
+ l = domain.split("[")
+ l[0] = l[0].replace('.', '::') # don't replace '.' in params
+ line += "[".join(l)
+ return line
# collect_fspath comes from testid which has a "/"-normalized path
- if fspath and fspath.replace("\\", "/") != collect_fspath:
- fspath = "%s <- %s" % (collect_fspath, fspath)
+
if fspath:
- line = str(fspath)
- if lineno is not None:
- lineno += 1
- line += ":" + str(lineno)
- if domain:
- line += ": " + str(domain)
+ res = mkrel(nodeid).replace("::()", "") # parens-normalization
+ if nodeid.split("::")[0] != fspath.replace("\\", "/"):
+ res += " <- " + self.startdir.bestrelpath(fspath)
else:
- line = "[location]"
- return line + " "
+ res = "[location]"
+ return res + " "
def _getfailureheadline(self, rep):
if hasattr(rep, 'location'):
@@ -400,10 +440,27 @@ class TerminalReporter:
l.append(x)
return l
- def summary_hints(self):
- if self.config.option.traceconfig:
- for hint in self.config.pluginmanager._hints:
- self._tw.line("hint: %s" % hint)
+ def summary_warnings(self):
+ if self.hasopt("w"):
+ warnings = self.stats.get("warnings")
+ if not warnings:
+ return
+ self.write_sep("=", "pytest-warning summary")
+ for w in warnings:
+ self._tw.line("W%s %s %s" % (w.code,
+ w.fslocation, w.message))
+
+ def summary_passes(self):
+ if self.config.option.tbstyle != "no":
+ if self.hasopt("P"):
+ reports = self.getreports('passed')
+ if not reports:
+ return
+ self.write_sep("=", "PASSES")
+ for rep in reports:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg)
+ self._outrep_summary(rep)
def summary_failures(self):
if self.config.option.tbstyle != "no":
@@ -417,7 +474,8 @@ class TerminalReporter:
self.write_line(line)
else:
msg = self._getfailureheadline(rep)
- self.write_sep("_", msg)
+ markup = {'red': True, 'bold': True}
+ self.write_sep("_", msg, **markup)
self._outrep_summary(rep)
def summary_errors(self):
@@ -447,26 +505,10 @@ class TerminalReporter:
self._tw.line(content)
def summary_stats(self):
- session_duration = py.std.time.time() - self._sessionstarttime
-
- keys = "failed passed skipped deselected xfailed xpassed".split()
- for key in self.stats.keys():
- if key not in keys:
- keys.append(key)
- parts = []
- for key in keys:
- if key: # setup/teardown reports have an empty key, ignore them
- val = self.stats.get(key, None)
- if val:
- parts.append("%d %s" % (len(val), key))
- line = ", ".join(parts)
+ session_duration = time.time() - self._sessionstarttime
+ (line, color) = build_summary_stats_line(self.stats)
msg = "%s in %.2f seconds" % (line, session_duration)
-
- markup = {'bold': True}
- if 'failed' in self.stats or 'error' in self.stats:
- markup = {'red': True, 'bold': True}
- else:
- markup = {'green': True, 'bold': True}
+ markup = {color: True, 'bold': True}
if self.verbosity >= 0:
self.write_sep("=", msg, **markup)
@@ -502,3 +544,50 @@ def flatten(l):
else:
yield x
+def build_summary_stats_line(stats):
+ keys = ("failed passed skipped deselected "
+ "xfailed xpassed warnings error").split()
+ key_translation = {'warnings': 'pytest-warnings'}
+ unknown_key_seen = False
+ for key in stats.keys():
+ if key not in keys:
+ if key: # setup/teardown reports have an empty key, ignore them
+ keys.append(key)
+ unknown_key_seen = True
+ parts = []
+ for key in keys:
+ val = stats.get(key, None)
+ if val:
+ key_name = key_translation.get(key, key)
+ parts.append("%d %s" % (len(val), key_name))
+
+ if parts:
+ line = ", ".join(parts)
+ else:
+ line = "no tests ran"
+
+ if 'failed' in stats or 'error' in stats:
+ color = 'red'
+ elif 'warnings' in stats or unknown_key_seen:
+ color = 'yellow'
+ elif 'passed' in stats:
+ color = 'green'
+ else:
+ color = 'yellow'
+
+ return (line, color)
+
+
+def _plugin_nameversions(plugininfo):
+ l = []
+ for plugin, dist in plugininfo:
+ # gets us name and version!
+ name = '{dist.project_name}-{dist.version}'.format(dist=dist)
+ # questionable convenience, but it keeps things short
+ if name.startswith("pytest-"):
+ name = name[7:]
+ # we decided to print python package names
+ # they can have more than one plugin
+ if name not in l:
+ l.append(name)
+ return l
diff --git a/_pytest/tmpdir.py b/_pytest/tmpdir.py
index 8eb0cdec04..ebc48dbe5b 100644
--- a/_pytest/tmpdir.py
+++ b/_pytest/tmpdir.py
@@ -1,8 +1,17 @@
""" support for providing temporary directories to test functions. """
-import pytest, py
+import re
+
+import pytest
+import py
from _pytest.monkeypatch import monkeypatch
-class TempdirHandler:
+
+class TempdirFactory:
+ """Factory for temporary directories under the common base temp directory.
+
+ The base directory can be configured using the ``--basetemp`` option.
+ """
+
def __init__(self, config):
self.config = config
self.trace = config.trace.get("tmpdir")
@@ -18,6 +27,10 @@ class TempdirHandler:
return self.getbasetemp().ensure(string, dir=dir)
def mktemp(self, basename, numbered=True):
+ """Create a subdirectory of the base temporary directory and return it.
+ If ``numbered``, ensure the directory is unique by adding a number
+ prefix greater than any existing one.
+ """
basetemp = self.getbasetemp()
if not numbered:
p = basetemp.mkdir(basename)
@@ -39,7 +52,17 @@ class TempdirHandler:
basetemp.remove()
basetemp.mkdir()
else:
- basetemp = py.path.local.make_numbered_dir(prefix='pytest-')
+ temproot = py.path.local.get_temproot()
+ user = get_user()
+ if user:
+ # use a sub-directory in the temproot to speed-up
+ # make_numbered_dir() call
+ rootdir = temproot.join('pytest-of-%s' % user)
+ else:
+ rootdir = temproot
+ rootdir.ensure(dir=1)
+ basetemp = py.path.local.make_numbered_dir(prefix='pytest-',
+ rootdir=rootdir)
self._basetemp = t = basetemp.realpath()
self.trace("new basetemp", t)
return t
@@ -47,15 +70,44 @@ class TempdirHandler:
def finish(self):
self.trace("finish")
+
+def get_user():
+ """Return the current user name, or None if getuser() does not work
+ in the current environment (see #1010).
+ """
+ import getpass
+ try:
+ return getpass.getuser()
+ except (ImportError, KeyError):
+ return None
+
+# backward compatibility
+TempdirHandler = TempdirFactory
+
+
def pytest_configure(config):
+ """Create a TempdirFactory and attach it to the config object.
+
+ This is to comply with existing plugins which expect the handler to be
+ available at pytest_configure time, but ideally should be moved entirely
+ to the tmpdir_factory session fixture.
+ """
mp = monkeypatch()
- t = TempdirHandler(config)
+ t = TempdirFactory(config)
config._cleanup.extend([mp.undo, t.finish])
mp.setattr(config, '_tmpdirhandler', t, raising=False)
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
+
+@pytest.fixture(scope='session')
+def tmpdir_factory(request):
+ """Return a TempdirFactory instance for the test session.
+ """
+ return request.config._tmpdirhandler
+
+
@pytest.fixture
-def tmpdir(request):
+def tmpdir(request, tmpdir_factory):
"""return a temporary directory path object
which is unique to each test function invocation,
created as a sub directory of the base temporary
@@ -63,9 +115,9 @@ def tmpdir(request):
path object.
"""
name = request.node.name
- name = py.std.re.sub("[\W]", "_", name)
+ name = re.sub("[\W]", "_", name)
MAXVAL = 30
if len(name) > MAXVAL:
name = name[:MAXVAL]
- x = request.config._tmpdirhandler.mktemp(name, numbered=True)
+ x = tmpdir_factory.mktemp(name, numbered=True)
return x
diff --git a/_pytest/unittest.py b/_pytest/unittest.py
index e6fefbd430..8120e94fbf 100644
--- a/_pytest/unittest.py
+++ b/_pytest/unittest.py
@@ -1,33 +1,32 @@
""" discovery and running of std-library "unittest" style tests. """
-import pytest, py
+from __future__ import absolute_import
+
import sys
+import traceback
+import pytest
# for transfering markers
+import _pytest._code
from _pytest.python import transfer_markers
-
-
-def is_unittest(obj):
- """Is obj a subclass of unittest.TestCase?"""
- unittest = sys.modules.get('unittest')
- if unittest is None:
- return # nobody can have derived unittest.TestCase
- try:
- return issubclass(obj, unittest.TestCase)
- except KeyboardInterrupt:
- raise
- except:
- return False
+from _pytest.skipping import MarkEvaluator
def pytest_pycollect_makeitem(collector, name, obj):
- if is_unittest(obj):
- return UnitTestCase(name, parent=collector)
+ # has unittest been imported and is obj a subclass of its TestCase?
+ try:
+ if not issubclass(obj, sys.modules["unittest"].TestCase):
+ return
+ except Exception:
+ return
+ # yes, so let's collect it
+ return UnitTestCase(name, parent=collector)
class UnitTestCase(pytest.Class):
- nofuncargs = True # marker for fixturemanger.getfixtureinfo()
- # to declare that our children do not support funcargs
- #
+ # marker for fixturemanger.getfixtureinfo()
+ # to declare that our children do not support funcargs
+ nofuncargs = True
+
def setup(self):
cls = self.obj
if getattr(cls, '__unittest_skip__', False):
@@ -41,10 +40,13 @@ class UnitTestCase(pytest.Class):
super(UnitTestCase, self).setup()
def collect(self):
+ from unittest import TestLoader
+ cls = self.obj
+ if not getattr(cls, "__test__", True):
+ return
self.session._fixturemanager.parsefactories(self, unittest=True)
- loader = py.std.unittest.TestLoader()
+ loader = TestLoader()
module = self.getparent(pytest.Module).obj
- cls = self.obj
foundsomething = False
for name in loader.getTestCaseNames(self.obj):
x = getattr(self.obj, name)
@@ -67,12 +69,26 @@ class TestCaseFunction(pytest.Function):
def setup(self):
self._testcase = self.parent.obj(self.name)
+ self._fix_unittest_skip_decorator()
self._obj = getattr(self._testcase, self.name)
if hasattr(self._testcase, 'setup_method'):
self._testcase.setup_method(self._obj)
if hasattr(self, "_request"):
self._request._fillfixtures()
+ def _fix_unittest_skip_decorator(self):
+ """
+ The @unittest.skip decorator calls functools.wraps(self._testcase)
+ The call to functools.wraps() fails unless self._testcase
+ has a __name__ attribute. This is usually automatically supplied
+ if the test is a function or method, but we need to add manually
+ here.
+
+ See issue #1169
+ """
+ if sys.version_info[0] == 2:
+ setattr(self._testcase, "__name__", self.name)
+
def teardown(self):
if hasattr(self._testcase, 'teardown_method'):
self._testcase.teardown_method(self._obj)
@@ -84,11 +100,11 @@ class TestCaseFunction(pytest.Function):
# unwrap potential exception info (see twisted trial support below)
rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo)
try:
- excinfo = py.code.ExceptionInfo(rawexcinfo)
+ excinfo = _pytest._code.ExceptionInfo(rawexcinfo)
except TypeError:
try:
try:
- l = py.std.traceback.format_exception(*rawexcinfo)
+ l = traceback.format_exception(*rawexcinfo)
l.insert(0, "NOTE: Incompatible Exception Representation, "
"displaying natively:\n\n")
pytest.fail("".join(l), pytrace=False)
@@ -100,7 +116,7 @@ class TestCaseFunction(pytest.Function):
except KeyboardInterrupt:
raise
except pytest.fail.Exception:
- excinfo = py.code.ExceptionInfo()
+ excinfo = _pytest._code.ExceptionInfo()
self.__dict__.setdefault('_excinfo', []).append(excinfo)
def addError(self, testcase, rawexcinfo):
@@ -112,6 +128,8 @@ class TestCaseFunction(pytest.Function):
try:
pytest.skip(reason)
except pytest.skip.Exception:
+ self._evalskip = MarkEvaluator(self, 'SkipTest')
+ self._evalskip.result = True
self._addexcinfo(sys.exc_info())
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
@@ -139,7 +157,7 @@ class TestCaseFunction(pytest.Function):
if traceback:
excinfo.traceback = traceback
-@pytest.mark.tryfirst
+@pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call):
if isinstance(item, TestCaseFunction):
if item._excinfo:
@@ -150,30 +168,33 @@ def pytest_runtest_makereport(item, call):
pass
# twisted trial support
-def pytest_runtest_protocol(item, __multicall__):
- if isinstance(item, TestCaseFunction):
- if 'twisted.trial.unittest' in sys.modules:
- ut = sys.modules['twisted.python.failure']
- Failure__init__ = ut.Failure.__init__.im_func
- check_testcase_implements_trial_reporter()
- def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
- captureVars=None):
- if exc_value is None:
- self._rawexcinfo = sys.exc_info()
- else:
- if exc_type is None:
- exc_type = type(exc_value)
- self._rawexcinfo = (exc_type, exc_value, exc_tb)
- try:
- Failure__init__(self, exc_value, exc_type, exc_tb,
- captureVars=captureVars)
- except TypeError:
- Failure__init__(self, exc_value, exc_type, exc_tb)
- ut.Failure.__init__ = excstore
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_protocol(item):
+ if isinstance(item, TestCaseFunction) and \
+ 'twisted.trial.unittest' in sys.modules:
+ ut = sys.modules['twisted.python.failure']
+ Failure__init__ = ut.Failure.__init__
+ check_testcase_implements_trial_reporter()
+ def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
+ captureVars=None):
+ if exc_value is None:
+ self._rawexcinfo = sys.exc_info()
+ else:
+ if exc_type is None:
+ exc_type = type(exc_value)
+ self._rawexcinfo = (exc_type, exc_value, exc_tb)
try:
- return __multicall__.execute()
- finally:
- ut.Failure.__init__ = Failure__init__
+ Failure__init__(self, exc_value, exc_type, exc_tb,
+ captureVars=captureVars)
+ except TypeError:
+ Failure__init__(self, exc_value, exc_type, exc_tb)
+ ut.Failure.__init__ = excstore
+ yield
+ ut.Failure.__init__ = Failure__init__
+ else:
+ yield
+
def check_testcase_implements_trial_reporter(done=[]):
if done: