[pypy-commit] pypy pytest-update: sync pylib and pytest with the latest versions
RonnyPfannschmidt
noreply at buildbot.pypy.org
Fri Feb 15 09:14:26 CET 2013
Author: Ronny Pfannschmidt <Ronny.Pfannschmidt at gmx.de>
Branch: pytest-update
Changeset: r61244:ca87ef6535e0
Date: 2013-02-15 09:13 +0100
http://bitbucket.org/pypy/pypy/changeset/ca87ef6535e0/
Log: sync pylib and pytest with the latest versions
diff too long, truncating to 2000 out of 4863 lines
diff --git a/_pytest/__init__.py b/_pytest/__init__.py
--- a/_pytest/__init__.py
+++ b/_pytest/__init__.py
@@ -1,2 +1,2 @@
#
-__version__ = '2.2.4.dev2'
+__version__ = '2.3.5dev6'
diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py
--- a/_pytest/assertion/__init__.py
+++ b/_pytest/assertion/__init__.py
@@ -50,7 +50,7 @@
hook = None
if mode == "rewrite":
hook = rewrite.AssertionRewritingHook()
- sys.meta_path.append(hook)
+ sys.meta_path.insert(0, hook)
warn_about_missing_assertion(mode)
config._assertstate = AssertionState(config, mode)
config._assertstate.hook = hook
@@ -73,8 +73,12 @@
def callbinrepr(op, left, right):
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 len(''.join(new_expl[1:])) > 80*8 and item.config.option.verbose < 2:
+ new_expl[1:] = ['Detailed information truncated, use "-vv" to see']
res = '\n~'.join(new_expl)
if item.config.getvalue("assertmode") == "rewrite":
# The result will be fed back a python % formatting
diff --git a/_pytest/assertion/newinterpret.py b/_pytest/assertion/newinterpret.py
--- a/_pytest/assertion/newinterpret.py
+++ b/_pytest/assertion/newinterpret.py
@@ -11,7 +11,7 @@
from _pytest.assertion.reinterpret import BuiltinAssertionError
-if sys.platform.startswith("java") and sys.version_info < (2, 5, 2):
+if sys.platform.startswith("java"):
# See http://bugs.jython.org/issue1497
_exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict",
"ListComp", "GeneratorExp", "Yield", "Compare", "Call",
diff --git a/_pytest/assertion/oldinterpret.py b/_pytest/assertion/oldinterpret.py
--- a/_pytest/assertion/oldinterpret.py
+++ b/_pytest/assertion/oldinterpret.py
@@ -526,10 +526,13 @@
# 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)")
diff --git a/_pytest/assertion/reinterpret.py b/_pytest/assertion/reinterpret.py
--- a/_pytest/assertion/reinterpret.py
+++ b/_pytest/assertion/reinterpret.py
@@ -44,4 +44,3 @@
from _pytest.assertion.newinterpret import interpret as reinterpret
else:
reinterpret = reinterpret_old
-
diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py
--- a/_pytest/assertion/rewrite.py
+++ b/_pytest/assertion/rewrite.py
@@ -34,13 +34,13 @@
PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1])
del ver, impl
-PYC_EXT = ".py" + "c" if __debug__ else "o"
+PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
class AssertionRewritingHook(object):
- """Import hook which rewrites asserts."""
+ """PEP302 Import hook which rewrites asserts."""
def __init__(self):
self.session = None
@@ -95,7 +95,8 @@
finally:
self.session = sess
else:
- state.trace("matched test file (was specified on cmdline): %r" % (fn,))
+ state.trace("matched test file (was specified on cmdline): %r" %
+ (fn,))
# The requested module looks like a test file, so rewrite it. This is
# the most magical part of the process: load the source, rewrite the
# asserts, and load the rewritten source. We also cache the rewritten
@@ -121,14 +122,14 @@
# because we're in a zip file.
write = False
elif e == errno.EACCES:
- state.trace("read only directory: %r" % (fn_pypath.dirname,))
+ state.trace("read only directory: %r" % fn_pypath.dirname)
write = False
else:
raise
cache_name = fn_pypath.basename[:-3] + PYC_TAIL
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...
+ # 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)
if co is None:
state.trace("rewriting %r" % (fn,))
@@ -160,10 +161,11 @@
return sys.modules[name]
def _write_pyc(co, source_path, 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.)
+ # 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")
@@ -240,9 +242,8 @@
except EnvironmentError:
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) != 8 or data[:4] != imp.get_magic() or
+ struct.unpack("<l", data[4:])[0] != mtime):
return None
co = marshal.load(fp)
if not isinstance(co, types.CodeType):
@@ -261,6 +262,9 @@
_saferepr = py.io.saferepr
from _pytest.assertion.util import format_explanation as _format_explanation
+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) + ")"
@@ -280,35 +284,35 @@
unary_map = {
- ast.Not : "not %s",
- ast.Invert : "~%s",
- ast.USub : "-%s",
- ast.UAdd : "+%s"
+ ast.Not: "not %s",
+ ast.Invert: "~%s",
+ ast.USub: "-%s",
+ ast.UAdd: "+%s"
}
binop_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"
+ ast.BitOr: "|",
+ ast.BitXor: "^",
+ ast.BitAnd: "&",
+ ast.LShift: "<<",
+ ast.RShift: ">>",
+ ast.Add: "+",
+ ast.Sub: "-",
+ ast.Mult: "*",
+ ast.Div: "/",
+ ast.FloorDiv: "//",
+ ast.Mod: "%%", # escaped for string formatting
+ 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"
}
@@ -341,7 +345,7 @@
lineno = 0
for item in mod.body:
if (expect_docstring and isinstance(item, ast.Expr) and
- isinstance(item.value, ast.Str)):
+ isinstance(item.value, ast.Str)):
doc = item.value.s
if "PYTEST_DONT_REWRITE" in doc:
# The module has disabled assertion rewriting.
@@ -462,7 +466,8 @@
body.append(raise_)
# Clear temporary variables by setting them to None.
if self.variables:
- variables = [ast.Name(name, ast.Store()) for name in self.variables]
+ variables = [ast.Name(name, ast.Store())
+ for name in self.variables]
clear = ast.Assign(variables, ast.Name("None", ast.Load()))
self.statements.append(clear)
# Fix line numbers.
@@ -471,11 +476,12 @@
return self.statements
def visit_Name(self, name):
- # Check if the name is local or not.
+ # 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)
- globs = ast.Call(self.builtin("globals"), [], [], None, None)
- ops = [ast.In(), ast.IsNot()]
- test = ast.Compare(ast.Str(name.id), ops, [locs, globs])
+ 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])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
return name, self.explanation_param(expr)
@@ -548,7 +554,8 @@
new_kwarg, expl = self.visit(call.kwargs)
arg_expls.append("**" + expl)
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
- new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg)
+ new_call = ast.Call(new_func, new_args, new_kwargs,
+ new_star, new_kwarg)
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)
diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py
--- a/_pytest/assertion/util.py
+++ b/_pytest/assertion/util.py
@@ -116,16 +116,11 @@
excinfo = py.code.ExceptionInfo()
explanation = ['(pytest_assertion plugin: representation of '
'details failed. Probably an object has a faulty __repr__.)',
- str(excinfo)
- ]
-
+ str(excinfo)]
if not explanation:
return None
- # Don't include pageloads of data, should be configurable
- if len(''.join(explanation)) > 80*8:
- explanation = ['Detailed information too verbose, truncated']
return [summary] + explanation
diff --git a/_pytest/capture.py b/_pytest/capture.py
--- a/_pytest/capture.py
+++ b/_pytest/capture.py
@@ -119,22 +119,20 @@
return "", ""
def activate_funcargs(self, pyfuncitem):
- if not hasattr(pyfuncitem, 'funcargs'):
- return
- assert not hasattr(self, '_capturing_funcargs')
- self._capturing_funcargs = capturing_funcargs = []
- for name, capfuncarg in pyfuncitem.funcargs.items():
- if name in ('capsys', 'capfd'):
- capturing_funcargs.append(capfuncarg)
- capfuncarg._start()
+ 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()
def deactivate_funcargs(self):
- capturing_funcargs = getattr(self, '_capturing_funcargs', None)
- if capturing_funcargs is not None:
- while capturing_funcargs:
- capfuncarg = capturing_funcargs.pop()
- capfuncarg._finalize()
- del self._capturing_funcargs
+ 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)
@@ -169,9 +167,12 @@
@pytest.mark.tryfirst
def pytest_runtest_makereport(self, __multicall__, item, call):
- self.deactivate_funcargs()
+ 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])
if not rep.passed:
addouterr(rep, outerr)
if not rep.passed or rep.when == "teardown":
@@ -179,23 +180,29 @@
item.outerr = outerr
return rep
+error_capsysfderror = "cannot use capsys and capfd at the same time"
+
def pytest_funcarg__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.
"""
- return CaptureFuncarg(py.io.StdCapture)
+ if "capfd" in request._funcargs:
+ raise request.raiseerror(error_capsysfderror)
+ return CaptureFixture(py.io.StdCapture)
def pytest_funcarg__capfd(request):
"""enables capturing of writes to file descriptors 1 and 2 and makes
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
+ if "capsys" in request._funcargs:
+ request.raiseerror(error_capsysfderror)
if not hasattr(os, 'dup'):
- py.test.skip("capfd funcarg needs os.dup")
- return CaptureFuncarg(py.io.StdCaptureFD)
+ pytest.skip("capfd funcarg needs os.dup")
+ return CaptureFixture(py.io.StdCaptureFD)
-class CaptureFuncarg:
+class CaptureFixture:
def __init__(self, captureclass):
self.capture = captureclass(now=False)
@@ -204,11 +211,15 @@
def _finalize(self):
if hasattr(self, 'capture'):
- self.capture.reset()
+ outerr = self._outerr = self.capture.reset()
del self.capture
+ return outerr
def readouterr(self):
- return self.capture.readouterr()
+ try:
+ return self.capture.readouterr()
+ except AttributeError:
+ return self._outerr
def close(self):
self._finalize()
diff --git a/_pytest/config.py b/_pytest/config.py
--- a/_pytest/config.py
+++ b/_pytest/config.py
@@ -19,7 +19,7 @@
fin()
class Parser:
- """ Parser for command line arguments. """
+ """ Parser for command line arguments and ini-file values. """
def __init__(self, usage=None, processopt=None):
self._anonymous = OptionGroup("custom options", parser=self)
@@ -35,15 +35,17 @@
if option.dest:
self._processopt(option)
- def addnote(self, note):
- self._notes.append(note)
-
def getgroup(self, name, description="", after=None):
""" get (or create) a named option Group.
- :name: unique name of the option group.
+ :name: name of the option group.
:description: long description for --help output.
:after: name of other group, used for ordering --help output.
+
+ The returned group object has an ``addoption`` method with the same
+ signature as :py:func:`parser.addoption
+ <_pytest.config.Parser.addoption>` but will be shown in the
+ respective group in the output of ``pytest. --help``.
"""
for group in self._groups:
if group.name == name:
@@ -57,7 +59,19 @@
return group
def addoption(self, *opts, **attrs):
- """ add an optparse-style option. """
+ """ register a command line option.
+
+ :opts: option names, can be short or long options.
+ :attrs: same attributes which the ``add_option()`` function of the
+ `optparse library
+ <http://docs.python.org/library/optparse.html#module-optparse>`_
+ accepts.
+
+ After command line parsing options are available on the pytest config
+ object via ``config.option.NAME`` where ``NAME`` is usually set
+ by passing a ``dest`` attribute, for example
+ ``addoption("--long", dest="NAME", ...)``.
+ """
self._anonymous.addoption(*opts, **attrs)
def parse(self, args):
@@ -78,7 +92,15 @@
return args
def addini(self, name, help, type=None, default=None):
- """ add an ini-file option with the given name and description. """
+ """ register an ini-file option.
+
+ :name: name of the ini-variable
+ :type: type of the variable, can be ``pathlist``, ``args`` or ``linelist``.
+ :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")
self._inidict[name] = (help, type, default)
self._ininames.append(name)
@@ -154,20 +176,24 @@
p = current.join(opt1[len(opt)+1:], abs=1)
self._confcutdir = p
break
- for arg in args + [current]:
+ foundanchor = False
+ for arg in args:
if hasattr(arg, 'startswith') and arg.startswith("--"):
continue
anchor = current.join(arg, abs=1)
- if anchor.check(): # we found some file object
- self._path2confmods[None] = self.getconftestmodules(anchor)
- # let's also consider test* dirs
- if anchor.check(dir=1):
- for x in anchor.listdir("test*"):
- if x.check(dir=1):
- self.getconftestmodules(x)
- break
- else:
- assert 0, "no root of filesystem?"
+ 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):
""" return a list of imported conftest modules for the given path. """
@@ -245,8 +271,8 @@
class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """
def __init__(self, pluginmanager=None):
- #: command line option values, usually added via parser.addoption(...)
- #: or parser.getgroup(...).addoption(...) calls
+ #: access to command line option as attributes.
+ #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead
self.option = CmdOptions()
self._parser = Parser(
usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]",
@@ -258,6 +284,7 @@
self._conftest = Conftest(onimport=self._onimportconftest)
self.hook = self.pluginmanager.hook
self._inicache = {}
+ self._opt2dest = {}
self._cleanup = []
@classmethod
@@ -278,6 +305,9 @@
self.pluginmanager.consider_conftest(conftestmodule)
def _processopt(self, opt):
+ for name in opt._short_opts + opt._long_opts:
+ self._opt2dest[name] = opt.dest
+
if hasattr(opt, 'default') and opt.dest:
if not hasattr(self.option, opt.dest):
setattr(self.option, opt.dest, opt.default)
@@ -356,8 +386,9 @@
x.append(line) # modifies the cached list inline
def getini(self, name):
- """ return configuration value from an ini file. If the
- specified name hasn't been registered through a prior ``parse.addini``
+ """ return configuration value from an :ref:`ini file <inifiles>`. If the
+ specified name hasn't been registered through a prior
+ :py:func:`parser.addini <pytest.config.Parser.addini>`
call (usually from a plugin), a ValueError is raised. """
try:
return self._inicache[name]
@@ -411,8 +442,22 @@
self._checkconftest(name)
return self._conftest.rget(name, path)
+ def getoption(self, name):
+ """ 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.
+ """
+ name = self._opt2dest.get(name, name)
+ try:
+ return getattr(self.option, name)
+ except AttributeError:
+ raise ValueError("no option named %r" % (name,))
+
def getvalue(self, name, path=None):
- """ return ``name`` value looked set from command line options.
+ """ 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.
@@ -434,6 +479,11 @@
except KeyError:
py.test.skip("no %r value found" %(name,))
+def exists(path, ignore=EnvironmentError):
+ try:
+ return path.check()
+ except ignore:
+ return False
def getcfg(args, inibasenames):
args = [x for x in args if not str(x).startswith("-")]
@@ -444,20 +494,9 @@
for base in arg.parts(reverse=True):
for inibasename in inibasenames:
p = base.join(inibasename)
- if p.check():
+ if exists(p):
iniconfig = py.iniconfig.IniConfig(p)
if 'pytest' in iniconfig.sections:
return iniconfig['pytest']
return {}
-def findupwards(current, basename):
- current = py.path.local(current)
- while 1:
- p = current.join(basename)
- if p.check():
- return p
- p = current.dirpath()
- if p == current:
- return
- current = p
-
diff --git a/_pytest/core.py b/_pytest/core.py
--- a/_pytest/core.py
+++ b/_pytest/core.py
@@ -24,12 +24,28 @@
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:
- if args:
- indent = " " * self.indent
- content = " ".join(map(str, args))
- self.writer("%s%s [%s]\n" %(indent, content, ":".join(tags)))
+ 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:
@@ -79,10 +95,11 @@
self.import_plugin(spec)
def register(self, plugin, name=None, prepend=False):
- assert not self.isregistered(plugin), plugin
+ if self._name2plugin.get(name, None) == -1:
+ return
name = name or getattr(plugin, '__name__', str(id(plugin)))
- if name in self._name2plugin:
- return False
+ if self.isregistered(plugin, name):
+ raise ValueError("Plugin already registered: %s=%s" %(name, plugin))
#self.trace("registering", name, plugin)
self._name2plugin[name] = plugin
self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self})
@@ -321,13 +338,18 @@
name = importspec
try:
mod = "_pytest." + name
- return __import__(mod, None, None, '__doc__')
+ __import__(mod)
+ return sys.modules[mod]
except ImportError:
#e = py.std.sys.exc_info()[1]
#if str(e).find(name) == -1:
# raise
pass #
- return __import__(importspec, None, None, '__doc__')
+ try:
+ __import__(importspec)
+ except ImportError:
+ raise ImportError(importspec)
+ return sys.modules[importspec]
class MultiCall:
""" execute a call into multiple python functions/methods. """
@@ -460,16 +482,15 @@
pluginmanager=_pluginmanager, args=args)
def main(args=None, plugins=None):
- """ returned exit code integer, after an in-process testing run
- with the given command line arguments, preloading an optional list
- of passed in plugin objects. """
- try:
- config = _prepareconfig(args, plugins)
- exitstatus = config.hook.pytest_cmdline_main(config=config)
- except UsageError:
- e = sys.exc_info()[1]
- sys.stderr.write("ERROR: %s\n" %(e.args[0],))
- exitstatus = 3
+ """ return exit code, after performing an in-process test run.
+
+ :arg args: list of command line arguments.
+
+ :arg plugins: list of plugin objects to be auto-registered during
+ initialization.
+ """
+ config = _prepareconfig(args, plugins)
+ exitstatus = config.hook.pytest_cmdline_main(config=config)
return exitstatus
class UsageError(Exception):
diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py
--- a/_pytest/helpconfig.py
+++ b/_pytest/helpconfig.py
@@ -79,6 +79,8 @@
tw.line() ; tw.line()
#tw.sep("=")
+ tw.line("to see available markers type: py.test --markers")
+ tw.line("to see available fixtures type: py.test --fixtures")
return
tw.line("conftest.py options:")
diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py
--- a/_pytest/hookspec.py
+++ b/_pytest/hookspec.py
@@ -23,8 +23,28 @@
"""modify command line arguments before option parsing. """
def pytest_addoption(parser):
- """add optparse-style options and ini-style config values via calls
- to ``parser.addoption`` and ``parser.addini(...)``.
+ """register optparse-style options and ini-style config values.
+
+ This function must be implemented in a :ref:`plugin <pluginorder>` and is
+ called once at the beginning of a test run.
+
+ :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(...)
+ <_pytest.config.Parser.addini>`.
+
+ Options can later be accessed through the
+ :py:class:`config <_pytest.config.Config>` object, respectively:
+
+ - :py:func:`config.getoption(name) <_pytest.config.Config.getoption>` to
+ retrieve the value of a command line option.
+
+ - :py:func:`config.getini(name) <_pytest.config.Config.getini>` to retrieve
+ a value read from an ini-style file.
+
+ The config object is passed around on many internal objects via the ``.config``
+ attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
+ via (deprecated) ``pytest.config``.
"""
def pytest_cmdline_main(config):
@@ -33,7 +53,7 @@
pytest_cmdline_main.firstresult = True
def pytest_configure(config):
- """ called after command line options have been parsed.
+ """ called after command line options have been parsed
and all plugins and initial conftest files been loaded.
"""
@@ -193,7 +213,7 @@
# hooks for influencing reporting (invoked from _pytest_terminal)
# -------------------------------------------------------------------------
-def pytest_report_header(config):
+def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal reporting."""
def pytest_report_teststatus(report):
diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py
--- a/_pytest/junitxml.py
+++ b/_pytest/junitxml.py
@@ -57,7 +57,7 @@
return unicode('#x%02X') % i
else:
return unicode('#x%04X') % i
- return illegal_xml_re.sub(repl, py.xml.escape(arg))
+ return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
@@ -81,19 +81,22 @@
config.pluginmanager.unregister(xml)
+def mangle_testnames(names):
+ names = [x.replace(".py", "") for x in names if x != '()']
+ names[0] = names[0].replace("/", '.')
+ return names
+
class LogXML(object):
def __init__(self, logfile, prefix):
logfile = os.path.expanduser(os.path.expandvars(logfile))
- self.logfile = os.path.normpath(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 _opentestcase(self, report):
- names = report.nodeid.split("::")
- names[0] = names[0].replace("/", '.')
- names = [x.replace(".py", "") for x in names if x != "()"]
+ names = mangle_testnames(report.nodeid.split("::"))
classnames = names[:-1]
if self.prefix:
classnames.insert(0, self.prefix)
@@ -111,7 +114,7 @@
def append_failure(self, report):
#msg = str(report.longrepr.reprtraceback.extraline)
- if "xfail" in report.keywords:
+ if hasattr(report, "wasxfail"):
self.append(
Junit.skipped(message="xfail-marked test passes unexpectedly"))
self.skipped += 1
@@ -145,8 +148,8 @@
self.errors += 1
def append_skipped(self, report):
- if "xfail" in report.keywords:
- self.append(Junit.skipped(str(report.keywords['xfail']),
+ if hasattr(report, "wasxfail"):
+ self.append(Junit.skipped(str(report.wasxfail),
message="expected test failure"))
else:
filename, lineno, skipreason = report.longrepr
diff --git a/_pytest/main.py b/_pytest/main.py
--- a/_pytest/main.py
+++ b/_pytest/main.py
@@ -2,7 +2,15 @@
import py
import pytest, _pytest
+import inspect
import os, sys, imp
+try:
+ from collections import MutableMapping as MappingMixin
+except ImportError:
+ from UserDict import DictMixin as MappingMixin
+
+from _pytest.mark import MarkInfo
+
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
# exitcodes for the command line
@@ -10,6 +18,7 @@
EXIT_TESTSFAILED = 1
EXIT_INTERRUPTED = 2
EXIT_INTERNALERROR = 3
+EXIT_USAGEERROR = 4
name_re = py.std.re.compile("^[a-zA-Z_]\w*$")
@@ -28,7 +37,6 @@
group._addoption('--maxfail', metavar="num",
action="store", type="int", dest="maxfail", default=0,
help="exit after first num failures or errors.")
-
group._addoption('--strict', action="store_true",
help="run pytest in strict mode, warnings become errors.")
@@ -65,30 +73,36 @@
session.exitstatus = EXIT_OK
initstate = 0
try:
- config.pluginmanager.do_configure(config)
- initstate = 1
- config.hook.pytest_sessionstart(session=session)
- initstate = 2
- doit(config, session)
- except pytest.UsageError:
- raise
- except KeyboardInterrupt:
- excinfo = py.code.ExceptionInfo()
- config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
- session.exitstatus = EXIT_INTERRUPTED
- except:
- excinfo = py.code.ExceptionInfo()
- config.pluginmanager.notify_exception(excinfo, config.option)
- session.exitstatus = EXIT_INTERNALERROR
- if excinfo.errisinstance(SystemExit):
- sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
- if initstate >= 2:
- config.hook.pytest_sessionfinish(session=session,
- exitstatus=session.exitstatus or (session._testsfailed and 1))
- if not session.exitstatus and session._testsfailed:
- session.exitstatus = EXIT_TESTSFAILED
- if initstate >= 1:
- config.pluginmanager.do_unconfigure(config)
+ try:
+ config.pluginmanager.do_configure(config)
+ initstate = 1
+ config.hook.pytest_sessionstart(session=session)
+ initstate = 2
+ doit(config, session)
+ except pytest.UsageError:
+ msg = sys.exc_info()[1].args[0]
+ sys.stderr.write("ERROR: %s\n" %(msg,))
+ session.exitstatus = EXIT_USAGEERROR
+ except KeyboardInterrupt:
+ excinfo = py.code.ExceptionInfo()
+ config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
+ session.exitstatus = EXIT_INTERRUPTED
+ except:
+ excinfo = py.code.ExceptionInfo()
+ config.pluginmanager.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:
+ if initstate >= 2:
+ config.hook.pytest_sessionfinish(
+ session=session,
+ exitstatus=session.exitstatus)
+ if initstate >= 1:
+ config.pluginmanager.do_unconfigure(config)
return session.exitstatus
def pytest_cmdline_main(config):
@@ -106,11 +120,18 @@
def pytest_runtestloop(session):
if session.config.option.collectonly:
return True
+
+ def getnextitem(i):
+ # this is a function to avoid python2
+ # keeping sys.exc_info set when calling into a test
+ # python2 keeps sys.exc_info till the frame is left
+ try:
+ return session.items[i+1]
+ except IndexError:
+ return None
+
for i, item in enumerate(session.items):
- try:
- nextitem = session.items[i+1]
- except IndexError:
- nextitem = None
+ nextitem = getnextitem(i)
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
if session.shouldstop:
raise session.Interrupted(session.shouldstop)
@@ -129,8 +150,10 @@
def __init__(self, fspath, config):
self.fspath = fspath
self.config = config
+
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)
@@ -138,31 +161,71 @@
def compatproperty(name):
def fget(self):
+ # deprecated - use pytest.name
return getattr(pytest, name)
- return property(fget, None, None,
- "deprecated attribute %r, use pytest.%s" % (name,name))
+
+ return property(fget)
+
+class NodeKeywords(MappingMixin):
+ def __init__(self, node):
+ parent = node.parent
+ bases = parent and (parent.keywords._markers,) or ()
+ self._markers = type("dynmarker", bases, {node.name: True})
+
+ def __getitem__(self, key):
+ try:
+ return getattr(self._markers, key)
+ except AttributeError:
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ setattr(self._markers, key, value)
+
+ def __delitem__(self, key):
+ delattr(self._markers, key)
+
+ def __iter__(self):
+ return iter(self.keys())
+
+ def __len__(self):
+ return len(self.keys())
+
+ def keys(self):
+ return dir(self._markers)
class Node(object):
- """ base class for all Nodes in the collection tree.
+ """ base class for Collector and Item the test collection tree.
Collector subclasses have children, Items are terminal nodes."""
def __init__(self, name, parent=None, config=None, session=None):
- #: a unique name with the scope of the parent
+ #: a unique name within the scope of the parent node
self.name = name
#: the parent collector node.
self.parent = parent
- #: the test config object
+ #: the pytest config object
self.config = config or parent.config
- #: the collection this node is part of
+ #: the session this node is part of
self.session = session or parent.session
- #: filesystem path where this node was collected from
+ #: filesystem path where this node was collected from (can be None)
self.fspath = getattr(parent, 'fspath', None)
- self.ihook = self.session.gethookproxy(self.fspath)
- self.keywords = {self.name: True}
+
+ #: keywords/markers collected from all scopes
+ self.keywords = NodeKeywords(self)
+
+ #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")
@@ -180,25 +243,28 @@
return cls
def __repr__(self):
- return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None))
+ return "<%s %r>" %(self.__class__.__name__,
+ getattr(self, 'name', None))
# methods for ordering nodes
@property
def nodeid(self):
+ """ a ::-separated string denoting its collection tree address. """
try:
return self._nodeid
except AttributeError:
self._nodeid = x = self._makeid()
return x
+
def _makeid(self):
return self.parent.nodeid + "::" + self.name
def __eq__(self, other):
if not isinstance(other, Node):
return False
- return self.__class__ == other.__class__ and \
- self.name == other.name and self.parent == other.parent
+ return (self.__class__ == other.__class__ and
+ self.name == other.name and self.parent == other.parent)
def __ne__(self, other):
return not self == other
@@ -257,6 +323,9 @@
pass
def _repr_failure_py(self, excinfo, style=None):
+ fm = self.session._fixturemanager
+ if excinfo.errisinstance(fm.FixtureLookupError):
+ return excinfo.value.formatrepr()
if self.config.option.fulltrace:
style="long"
else:
@@ -365,9 +434,9 @@
__module__ = 'builtins' # for py3
def __init__(self, config):
- super(Session, self).__init__(py.path.local(), parent=None,
- config=config, session=self)
- assert self.config.pluginmanager.register(self, name="session", prepend=True)
+ FSCollector.__init__(self, py.path.local(), parent=None,
+ config=config, session=self)
+ self.config.pluginmanager.register(self, name="session", prepend=True)
self._testsfailed = 0
self.shouldstop = False
self.trace = config.trace.root.get("collection")
@@ -378,7 +447,7 @@
raise self.Interrupted(self.shouldstop)
def pytest_runtest_logreport(self, report):
- if report.failed and 'xfail' not in getattr(report, 'keywords', []):
+ if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1
maxfail = self.config.getvalue("maxfail")
if maxfail and self._testsfailed >= maxfail:
@@ -453,7 +522,7 @@
if path.check(dir=1):
assert not names, "invalid arg %r" %(arg,)
for path in path.visit(fil=lambda x: x.check(file=1),
- rec=self._recurse, bf=True, sort=True):
+ rec=self._recurse, bf=True, sort=True):
for x in self._collectfile(path):
yield x
else:
@@ -465,13 +534,13 @@
ihook = self.gethookproxy(path)
if not self.isinitpath(path):
if ihook.pytest_ignore_collect(path=path, config=self.config):
- return ()
+ return ()
return ihook.pytest_collect_file(path=path, parent=self)
def _recurse(self, path):
ihook = self.gethookproxy(path.dirpath())
if ihook.pytest_ignore_collect(path=path, config=self.config):
- return
+ return
for pat in self._norecursepatterns:
if path.check(fnmatch=pat):
return False
@@ -574,3 +643,12 @@
for x in self.genitems(subnode):
yield x
node.ihook.pytest_collectreport(report=rep)
+
+def getfslineno(obj):
+ # xxx let decorators etc specify a sane ordering
+ if hasattr(obj, 'place_as'):
+ obj = obj.place_as
+ fslineno = py.code.getfslineno(obj)
+ assert isinstance(fslineno[1], int), obj
+ return fslineno
+
diff --git a/_pytest/mark.py b/_pytest/mark.py
--- a/_pytest/mark.py
+++ b/_pytest/mark.py
@@ -8,11 +8,12 @@
group = parser.getgroup("general")
group._addoption('-k',
action="store", dest="keyword", default='', metavar="KEYWORDEXPR",
- help="only run tests which match given keyword expression. "
- "An expression consists of space-separated terms. "
- "Each term must match. Precede a term with '-' to negate. "
- "Terminate expression with ':' to make the first match match "
- "all subsequent tests (usually file-order). ")
+ help="only run tests which match the given expression. "
+ "An expression is a python evaluatable expression "
+ "where all names are substring-matched against test names "
+ "and keywords. Example: -k 'test_method or test_other' "
+ "matches all test functions whose name contains "
+ "'test_method' or 'test_other'.")
group._addoption("-m",
action="store", dest="markexpr", default="", metavar="MARKEXPR",
@@ -51,7 +52,7 @@
remaining = []
deselected = []
for colitem in items:
- if keywordexpr and skipbykeyword(colitem, keywordexpr):
+ if keywordexpr and not matchkeyword(colitem, keywordexpr):
deselected.append(colitem)
else:
if selectuntil:
@@ -72,45 +73,26 @@
def __getitem__(self, name):
return name in self._mydict
+class SubstringDict:
+ def __init__(self, mydict):
+ self._mydict = mydict
+ def __getitem__(self, name):
+ for key in self._mydict:
+ if name in key:
+ return True
+ return False
+
def matchmark(colitem, matchexpr):
- return eval(matchexpr, {}, BoolDict(colitem.obj.__dict__))
+ return eval(matchexpr, {}, BoolDict(colitem.keywords))
+
+def matchkeyword(colitem, keywordexpr):
+ keywordexpr = keywordexpr.replace("-", "not ")
+ return eval(keywordexpr, {}, SubstringDict(colitem.keywords))
def pytest_configure(config):
if config.option.strict:
pytest.mark._config = config
-def skipbykeyword(colitem, keywordexpr):
- """ return True if they given keyword expression means to
- skip this collector/item.
- """
- if not keywordexpr:
- return
-
- itemkeywords = getkeywords(colitem)
- for key in filter(None, keywordexpr.split()):
- eor = key[:1] == '-'
- if eor:
- key = key[1:]
- if not (eor ^ matchonekeyword(key, itemkeywords)):
- return True
-
-def getkeywords(node):
- keywords = {}
- while node is not None:
- keywords.update(node.keywords)
- node = node.parent
- return keywords
-
-
-def matchonekeyword(key, itemkeywords):
- for elem in key.split("."):
- for kw in itemkeywords:
- if elem in kw:
- break
- else:
- return False
- return True
-
class MarkGenerator:
""" Factory for :class:`MarkDecorator` objects - exposed as
a ``py.test.mark`` singleton instance. Example::
diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py
--- a/_pytest/monkeypatch.py
+++ b/_pytest/monkeypatch.py
@@ -1,6 +1,6 @@
""" monkeypatching and mocking functionality. """
-import os, sys
+import os, sys, inspect
def pytest_funcarg__monkeypatch(request):
"""The returned ``monkeypatch`` funcarg provides these
@@ -39,6 +39,10 @@
oldval = getattr(obj, name, notset)
if raising and oldval is notset:
raise AttributeError("%r has no attribute %r" %(obj, name))
+
+ # avoid class descriptors like staticmethod/classmethod
+ if inspect.isclass(obj):
+ oldval = obj.__dict__.get(name, notset)
self._setattr.insert(0, (obj, name, oldval))
setattr(obj, name, value)
diff --git a/_pytest/nose.py b/_pytest/nose.py
--- a/_pytest/nose.py
+++ b/_pytest/nose.py
@@ -3,6 +3,8 @@
import pytest, py
import inspect
import sys
+from _pytest import unittest
+
def pytest_runtest_makereport(__multicall__, item, call):
SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None)
@@ -15,7 +17,7 @@
@pytest.mark.trylast
def pytest_runtest_setup(item):
- if isinstance(item, (pytest.Function)):
+ if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):
gen = item.parent
if not hasattr(gen, '_nosegensetup'):
@@ -28,7 +30,7 @@
call_optional(item.parent.obj, 'setup')
def pytest_runtest_teardown(item):
- if isinstance(item, pytest.Function):
+ if is_potential_nosetest(item):
if not call_optional(item.obj, 'teardown'):
call_optional(item.parent.obj, 'teardown')
#if hasattr(item.parent, '_nosegensetup'):
@@ -39,9 +41,18 @@
if isinstance(collector, pytest.Generator):
call_optional(collector.obj, 'setup')
+
+def is_potential_nosetest(item):
+ # extra check needed since we do not do nose style setup/teardown
+ # on direct unittest style classes
+ return isinstance(item, pytest.Function) and \
+ not isinstance(item, unittest.TestCaseFunction)
+
+
def call_optional(obj, name):
method = getattr(obj, name, None)
- if method:
+ isfixture = hasattr(method, "_pytestfixturefunction")
+ if method is not None and not isfixture and py.builtin.callable(method):
# If there's any problems allow the exception to raise rather than
# silently ignoring them
method()
diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py
--- a/_pytest/pastebin.py
+++ b/_pytest/pastebin.py
@@ -2,7 +2,7 @@
import py, sys
class url:
- base = "http://paste.pocoo.org"
+ base = "http://bpaste.net"
xmlrpc = base + "/xmlrpc/"
show = base + "/show/"
@@ -11,7 +11,7 @@
group._addoption('--pastebin', metavar="mode",
action='store', dest="pastebin", default=None,
type="choice", choices=['failed', 'all'],
- help="send failed|all info to Pocoo pastebin service.")
+ help="send failed|all info to bpaste.net pastebin service.")
def pytest_configure(__multicall__, config):
import tempfile
diff --git a/_pytest/pdb.py b/_pytest/pdb.py
--- a/_pytest/pdb.py
+++ b/_pytest/pdb.py
@@ -50,7 +50,7 @@
def pytest_runtest_makereport():
pytestPDB.item = None
-
+
class PdbInvoke:
@pytest.mark.tryfirst
def pytest_runtest_makereport(self, item, call, __multicall__):
@@ -59,7 +59,7 @@
call.excinfo.errisinstance(pytest.skip.Exception) or \
call.excinfo.errisinstance(py.std.bdb.BdbQuit):
return rep
- if "xfail" in rep.keywords:
+ if hasattr(rep, "wasxfail"):
return rep
# we assume that the above execute() suspended capturing
# XXX we re-use the TerminalReporter's terminalwriter
diff --git a/_pytest/pytester.py b/_pytest/pytester.py
--- a/_pytest/pytester.py
+++ b/_pytest/pytester.py
@@ -2,6 +2,7 @@
import py, pytest
import sys, os
+import codecs
import re
import inspect
import time
@@ -194,10 +195,7 @@
except py.error.EEXIST:
continue
break
- # we need to create another subdir
- # because Directory.collect() currently loads
- # conftest.py from sibling directories
- self.tmpdir = tmpdir.mkdir(name)
+ self.tmpdir = tmpdir
self.plugins = []
self._syspathremove = []
self.chdir() # always chdir
@@ -244,8 +242,10 @@
ret = None
for name, value in items:
p = self.tmpdir.join(name).new(ext=ext)
- source = py.builtin._totext(py.code.Source(value)).lstrip()
- p.write(source.encode("utf-8"), "wb")
+ source = py.builtin._totext(py.code.Source(value)).strip()
+ content = source.encode("utf-8") # + "\n"
+ #content = content.rstrip() + "\n"
+ p.write(content, "wb")
if ret is None:
ret = p
return ret
@@ -318,7 +318,7 @@
# used from runner functional tests
item = self.getitem(source)
# the test class where we are called from wants to provide the runner
- testclassinstance = py.builtin._getimself(self.request.function)
+ testclassinstance = self.request.instance
runner = testclassinstance.getrunner()
return runner(item)
@@ -355,7 +355,7 @@
if not plugins:
plugins = []
plugins.append(Collect())
- ret = self.pytestmain(list(args), plugins=[Collect()])
+ ret = self.pytestmain(list(args), plugins=plugins)
reprec = rec[0]
reprec.ret = ret
assert len(rec) == 1
@@ -388,10 +388,12 @@
return config
def getitem(self, source, funcname="test_func"):
- for item in self.getitems(source):
+ items = self.getitems(source)
+ for item in items:
if item.name == funcname:
return item
- assert 0, "%r item not found in module:\n%s" %(funcname, source)
+ assert 0, "%r item not found in module:\n%s\nitems: %s" %(
+ funcname, source, items)
def getitems(self, source):
modcol = self.getmodulecol(source)
@@ -417,7 +419,8 @@
str(os.getcwd()), env.get('PYTHONPATH', '')]))
kw['env'] = env
#print "env", env
- return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
+ return py.std.subprocess.Popen(cmdargs,
+ stdout=stdout, stderr=stderr, **kw)
def pytestmain(self, *args, **kwargs):
class ResetCapturing:
@@ -438,28 +441,35 @@
p1 = self.tmpdir.join("stdout")
p2 = self.tmpdir.join("stderr")
print_("running", cmdargs, "curdir=", py.path.local())
- f1 = p1.open("wb")
- f2 = p2.open("wb")
- now = time.time()
- popen = self.popen(cmdargs, stdout=f1, stderr=f2,
- close_fds=(sys.platform != "win32"))
- ret = popen.wait()
- f1.close()
- f2.close()
- out = p1.read("rb")
- out = getdecoded(out).splitlines()
- err = p2.read("rb")
- err = getdecoded(err).splitlines()
- def dump_lines(lines, fp):
- try:
- for line in lines:
- py.builtin.print_(line, file=fp)
- except UnicodeEncodeError:
- print("couldn't print to %s because of encoding" % (fp,))
- dump_lines(out, sys.stdout)
- dump_lines(err, sys.stderr)
+ f1 = codecs.open(str(p1), "w", encoding="utf8")
+ f2 = codecs.open(str(p2), "w", encoding="utf8")
+ try:
+ now = time.time()
+ popen = self.popen(cmdargs, stdout=f1, stderr=f2,
+ close_fds=(sys.platform != "win32"))
+ ret = popen.wait()
+ finally:
+ f1.close()
+ f2.close()
+ f1 = codecs.open(str(p1), "r", encoding="utf8")
+ f2 = codecs.open(str(p2), "r", encoding="utf8")
+ try:
+ out = f1.read().splitlines()
+ err = f2.read().splitlines()
+ finally:
+ f1.close()
+ f2.close()
+ self._dump_lines(out, sys.stdout)
+ self._dump_lines(err, sys.stderr)
return RunResult(ret, out, err, time.time()-now)
+ def _dump_lines(self, lines, fp):
+ try:
+ for line in lines:
+ py.builtin.print_(line, file=fp)
+ 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)
@@ -520,6 +530,8 @@
pytest.skip("pypy-64 bit not supported")
if sys.platform == "darwin":
pytest.xfail("pexpect does not work reliably on darwin?!")
+ if sys.platform.startswith("freebsd"):
+ pytest.xfail("pexpect does not work reliably on freebsd")
logfile = self.tmpdir.join("spawn.out")
child = pexpect.spawn(cmd, logfile=logfile.open("w"))
child.timeout = expect_timeout
diff --git a/_pytest/python.py b/_pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -3,17 +3,85 @@
import inspect
import sys
import pytest
+from _pytest.main import getfslineno
+from _pytest.monkeypatch import monkeypatch
from py._code.code import TerminalRepr
-from _pytest.monkeypatch import monkeypatch
import _pytest
cutdir = py.path.local(_pytest.__file__).dirpath()
+callable = py.builtin.callable
+
+def getimfunc(func):
+ try:
+ return func.__func__
+ except AttributeError:
+ try:
+ return func.im_func
+ except AttributeError:
+ return func
+
+
+class FixtureFunctionMarker:
+ def __init__(self, scope, params, autouse=False):
+ self.scope = scope
+ self.params = params
+ self.autouse = autouse
+
+ def __call__(self, function):
+ if inspect.isclass(function):
+ raise ValueError("class fixtures not supported (may be in the future)")
+ function._pytestfixturefunction = self
+ return function
+
+
+def fixture(scope="function", params=None, autouse=False):
+ """ (return a) decorator to mark a fixture factory function.
+
+ This decorator can be used (with or or without parameters) to define
+ a fixture function. The name of the fixture function can later be
+ referenced to cause its invocation ahead of running tests: test
+ modules or classes can use the pytest.mark.usefixtures(fixturename)
+ marker. Test functions can directly use fixture names as input
+ arguments in which case the fixture instance returned from the fixture
+ function will be injected.
+
+ :arg scope: the scope for which this fixture is shared, one of
+ "function" (default), "class", "module", "session".
+
+ :arg params: an optional list of parameters which will cause multiple
+ invocations of the fixture function and all of the tests
+ using it.
+
+ :arg autouse: if True, the fixture func is activated for all tests that
+ can see it. If False (the default) then an explicit
+ reference is needed to activate the fixture.
+ """
+ if callable(scope) and params is None and autouse == False:
+ # direct decoration
+ return FixtureFunctionMarker("function", params, autouse)(scope)
+ else:
+ return FixtureFunctionMarker(scope, params, autouse=autouse)
+
+defaultfuncargprefixmarker = fixture()
+
+def pyobj_property(name):
+ def get(self):
+ node = self.getparent(getattr(pytest, name))
+ if node is not None:
+ return node.obj
+ doc = "python %s object this node was collected from (can be None)." % (
+ name.lower(),)
+ return property(get, None, None, doc)
+
+
def pytest_addoption(parser):
group = parser.getgroup("general")
- group.addoption('--funcargs',
- action="store_true", dest="showfuncargs", default=False,
- help="show available function arguments, sorted by plugin")
+ group.addoption('--fixtures', '--funcargs',
+ action="store_true", dest="showfixtures", default=False,
+ help="show available fixtures, sorted by plugin appearance")
+ 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'),
help="glob-style file patterns for Python test module discovery")
@@ -23,18 +91,18 @@
help="prefixes for Python test function and method discovery")
def pytest_cmdline_main(config):
- if config.option.showfuncargs:
- showfuncargs(config)
+ if config.option.showfixtures:
+ showfixtures(config)
return 0
def pytest_generate_tests(metafunc):
try:
- param = metafunc.function.parametrize
+ markers = metafunc.function.parametrize
except AttributeError:
return
- for p in param:
- metafunc.parametrize(*p.args, **p.kwargs)
+ for marker in markers:
+ metafunc.parametrize(*marker.args, **marker.kwargs)
def pytest_configure(config):
config.addinivalue_line("markers",
@@ -42,24 +110,35 @@
"times passing in multiple different argument value sets. Example: "
"@parametrize('arg1', [1,2]) would lead to two calls of the decorated "
"test function, one with arg1=1 and another with arg1=2."
+ " see http://pytest.org/latest/parametrize.html for more info and "
+ "examples."
+ )
+ config.addinivalue_line("markers",
+ "usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
+ "all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures "
)
+def pytest_sessionstart(session):
+ session._fixturemanager = FixtureManager(session)
@pytest.mark.trylast
def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
+ 'fixture': fixture,
'raises' : raises,
'collect': {
'Module': Module, 'Class': Class, 'Instance': Instance,
'Function': Function, 'Generator': Generator,
- '_fillfuncargs': fillfuncargs}
+ '_fillfuncargs': fillfixtures}
}
-def pytest_funcarg__pytestconfig(request):
+ at fixture()
+def pytestconfig(request):
""" the pytest config object with access to command line opts."""
return request.config
+
def pytest_pyfunc_call(__multicall__, pyfuncitem):
if not __multicall__.execute():
testfunction = pyfuncitem.obj
@@ -67,7 +146,10 @@
testfunction(*pyfuncitem._args)
else:
funcargs = pyfuncitem.funcargs
- testfunction(**funcargs)
+ testargs = {}
+ for arg in pyfuncitem._fixtureinfo.argnames:
+ testargs[arg] = funcargs[arg]
+ testfunction(**testargs)
def pytest_collect_file(path, parent):
ext = path.ext
@@ -79,8 +161,8 @@
break
else:
return
- return parent.ihook.pytest_pycollect_makemodule(
- path=path, parent=parent)
+ ihook = parent.session.gethookproxy(path)
+ return ihook.pytest_pycollect_makemodule(path=path, parent=parent)
def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)
@@ -100,7 +182,7 @@
if is_generator(obj):
return Generator(name, parent=collector)
else:
- return collector._genfunctions(name, obj)
+ return list(collector._genfunctions(name, obj))
def is_generator(func):
try:
@@ -109,7 +191,12 @@
# assume them to not be generators
return False
-class PyobjMixin(object):
+class PyobjContext(object):
+ module = pyobj_property("Module")
+ cls = pyobj_property("Class")
+ instance = pyobj_property("Instance")
+
+class PyobjMixin(PyobjContext):
def obj():
def fget(self):
try:
@@ -147,18 +234,7 @@
return s.replace(".[", "[")
def _getfslineno(self):
- try:
- return self._fslineno
- except AttributeError:
- pass
- obj = self.obj
- # xxx let decorators etc specify a sane ordering
- if hasattr(obj, 'place_as'):
- obj = obj.place_as
-
- self._fslineno = py.code.getfslineno(obj)
- assert isinstance(self._fslineno[1], int), obj
- return self._fslineno
+ return getfslineno(self.obj)
def reportinfo(self):
# XXX caching?
@@ -168,17 +244,15 @@
fspath = sys.modules[obj.__module__].__file__
if fspath.endswith(".pyc"):
fspath = fspath[:-1]
- #assert 0
- #fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
lineno = obj.compat_co_firstlineno
modpath = obj.__module__
else:
- fspath, lineno = self._getfslineno()
+ fspath, lineno = getfslineno(obj)
modpath = self.getmodpath()
assert isinstance(lineno, int)
return fspath, lineno, modpath
-class PyCollectorMixin(PyobjMixin, pytest.Collector):
+class PyCollector(PyobjMixin, pytest.Collector):
def funcnamefilter(self, name):
for prefix in self.config.getini("python_functions"):
@@ -203,17 +277,17 @@
if name in seen:
continue
seen[name] = True
- if name[0] != "_":
- res = self.makeitem(name, obj)
- if res is None:
- continue
- if not isinstance(res, list):
- res = [res]
- l.extend(res)
+ res = self.makeitem(name, obj)
+ if res is None:
+ continue
+ if not isinstance(res, list):
+ res = [res]
+ l.extend(res)
l.sort(key=lambda item: item.reportinfo()[:2])
return l
def makeitem(self, name, obj):
+ #assert self.ihook.fspath == self.fspath, self
return self.ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj)
@@ -222,8 +296,10 @@
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
transfer_markers(funcobj, cls, module)
- metafunc = Metafunc(funcobj, config=self.config,
- cls=cls, module=module)
+ fm = self.session._fixturemanager
+ 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:
@@ -232,14 +308,20 @@
gentesthook.pcall(plugins, metafunc=metafunc)
Function = self._getcustomclass("Function")
if not metafunc._calls:
- return Function(name, parent=self)
- l = []
- for callspec in metafunc._calls:
- subname = "%s[%s]" %(name, callspec.id)
- function = Function(name=subname, parent=self,
- callspec=callspec, callobj=funcobj, keywords={callspec.id:True})
- l.append(function)
- return l
+ yield Function(name, parent=self)
+ else:
+ for callspec in metafunc._calls:
+ subname = "%s[%s]" %(name, callspec.id)
+ yield Function(name=subname, parent=self,
+ callspec=callspec, callobj=funcobj,
+ keywords={callspec.id:True})
+
+
+class FuncFixtureInfo:
+ def __init__(self, argnames, names_closure, name2fixturedefs):
+ self.argnames = argnames
+ self.names_closure = names_closure
+ self.name2fixturedefs = name2fixturedefs
def transfer_markers(funcobj, cls, mod):
# XXX this should rather be code in the mark plugin or the mark
@@ -255,10 +337,15 @@
else:
pytestmark(funcobj)
-class Module(pytest.File, PyCollectorMixin):
+class Module(pytest.File, PyCollector):
+ """ Collector for test classes and functions. """
def _getobj(self):
return self._memoizedcall('_obj', self._importtestmodule)
+ def collect(self):
+ self.session._fixturemanager.parsefactories(self)
+ return super(Module, self).collect()
+
def _importtestmodule(self):
# we assume we are only called once per module
try:
@@ -283,45 +370,54 @@
return mod
def setup(self):
- if hasattr(self.obj, 'setup_module'):
+ setup_module = xunitsetup(self.obj, "setup_module")
+ if setup_module is not None:
#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(self.obj.setup_module)[0]:
- self.obj.setup_module(self.obj)
+ if inspect.getargspec(setup_module)[0]:
+ setup_module(self.obj)
else:
- self.obj.setup_module()
+ setup_module()
def teardown(self):
- if hasattr(self.obj, 'teardown_module'):
+ teardown_module = xunitsetup(self.obj, 'teardown_module')
+ if teardown_module is not None:
#XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, its probably a py.test style one
# so we pass the current module object
- if inspect.getargspec(self.obj.teardown_module)[0]:
- self.obj.teardown_module(self.obj)
+ if inspect.getargspec(teardown_module)[0]:
+ teardown_module(self.obj)
else:
- self.obj.teardown_module()
+ teardown_module()
-class Class(PyCollectorMixin, pytest.Collector):
-
+class Class(PyCollector):
+ """ Collector for test methods. """
def collect(self):
return [self._getcustomclass("Instance")(name="()", parent=self)]
def setup(self):
- setup_class = getattr(self.obj, 'setup_class', None)
+ setup_class = xunitsetup(self.obj, 'setup_class')
if setup_class is not None:
setup_class = getattr(setup_class, 'im_func', setup_class)
+ setup_class = getattr(setup_class, '__func__', setup_class)
setup_class(self.obj)
def teardown(self):
- teardown_class = getattr(self.obj, 'teardown_class', None)
+ teardown_class = xunitsetup(self.obj, 'teardown_class')
if teardown_class is not None:
teardown_class = getattr(teardown_class, 'im_func', teardown_class)
+ teardown_class = getattr(teardown_class, '__func__', teardown_class)
teardown_class(self.obj)
-class Instance(PyCollectorMixin, pytest.Collector):
+class Instance(PyCollector):
def _getobj(self):
- return self.parent.obj()
+ obj = self.parent.obj()
+ return obj
+
+ def collect(self):
+ self.session._fixturemanager.parsefactories(self)
+ return super(Instance, self).collect()
def newinstance(self):
self.obj = self._getobj()
@@ -330,6 +426,7 @@
class FunctionMixin(PyobjMixin):
""" mixin for the code common to Function and Generator.
"""
+
def setup(self):
""" perform setup for this test function. """
if hasattr(self, '_preservedparent'):
@@ -343,7 +440,7 @@
name = 'setup_method'
else:
name = 'setup_function'
- setup_func_or_method = getattr(obj, name, None)
+ setup_func_or_method = xunitsetup(obj, name)
if setup_func_or_method is not None:
setup_func_or_method(self.obj)
@@ -354,7 +451,7 @@
else:
name = 'teardown_function'
obj = self.parent.obj
- teardown_func_or_meth = getattr(obj, name, None)
+ teardown_func_or_meth = xunitsetup(obj, name)
if teardown_func_or_meth is not None:
teardown_func_or_meth(self.obj)
@@ -371,13 +468,6 @@
excinfo.traceback = ntraceback.filter()
def _repr_failure_py(self, excinfo, style="long"):
- if excinfo.errisinstance(FuncargRequest.LookupError):
- fspath, lineno, msg = self.reportinfo()
- lines, _ = inspect.getsourcelines(self.obj)
- for i, line in enumerate(lines):
- if line.strip().startswith('def'):
- return FuncargLookupErrorRepr(fspath, lineno,
- lines[:i+1], str(excinfo.value))
if excinfo.errisinstance(pytest.fail.Exception):
if not excinfo.value.pytrace:
return str(excinfo.value)
@@ -389,24 +479,8 @@
return self._repr_failure_py(excinfo,
style=self.config.option.tbstyle)
-class FuncargLookupErrorRepr(TerminalRepr):
- def __init__(self, filename, firstlineno, deflines, errorstring):
- self.deflines = deflines
- self.errorstring = errorstring
- self.filename = filename
- self.firstlineno = firstlineno
- def toterminal(self, tw):
- tw.line()
- for line in self.deflines:
- tw.line(" " + line.strip())
- for line in self.errorstring.split("\n"):
- tw.line(" " + line.strip(), red=True)
- tw.line()
- tw.line("%s:%d" % (self.filename, self.firstlineno+1))
-
-
-class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
+class Generator(FunctionMixin, PyCollector):
def collect(self):
# test generators are seen as collectors but they also
# invoke setup/teardown on popular request
@@ -418,7 +492,7 @@
seen = {}
for i, x in enumerate(self.obj()):
name, call, args = self.getcallargs(x)
- if not py.builtin.callable(call):
+ if not callable(call):
raise TypeError("%r yielded non callable test %r" %(self.obj, call,))
if name is None:
name = "[%d]" % i
@@ -443,77 +517,6 @@
return name, call, args
-#
-# Test Items
-#
-_dummy = object()
-class Function(FunctionMixin, pytest.Item):
- """ a Function Item is responsible for setting up
- and executing a Python callable test object.
- """
- _genid = None
- def __init__(self, name, parent=None, args=None, config=None,
- callspec=None, callobj=_dummy, keywords=None, session=None):
- super(Function, self).__init__(name, parent,
- config=config, session=session)
- self._args = args
- if self._isyieldedfunction():
- assert not callspec, (
- "yielded functions (deprecated) cannot have funcargs")
- else:
- if callspec is not None:
- self.callspec = callspec
- self.funcargs = callspec.funcargs or {}
- self._genid = callspec.id
- if hasattr(callspec, "param"):
- self._requestparam = callspec.param
- else:
- self.funcargs = {}
- if callobj is not _dummy:
- self._obj = callobj
- self.function = getattr(self.obj, 'im_func', self.obj)
- self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
- if keywords:
- self.keywords.update(keywords)
-
- def _getobj(self):
- name = self.name
- i = name.find("[") # parametrization
- if i != -1:
- name = name[:i]
- return getattr(self.parent.obj, name)
-
- def _isyieldedfunction(self):
- return self._args is not None
-
- def runtest(self):
- """ execute the underlying test function. """
- self.ihook.pytest_pyfunc_call(pyfuncitem=self)
-
- def setup(self):
- super(Function, self).setup()
- if hasattr(self, 'funcargs'):
More information about the pypy-commit
mailing list