mid-scale refactoring to make request API available directly on items.

This commit was slightly tricky because i want to backward
compatibility especially for the oejskit plugin which
uses Funcarg-filling for non-Function objects.
This commit is contained in:
holger krekel 2012-06-25 17:35:33 +02:00
parent 227d847216
commit 91b6f2bda8
16 changed files with 529 additions and 399 deletions

View File

@ -1,6 +1,19 @@
Changes between 2.2.4 and 2.2.5.dev
Changes between 2.2.4 and 2.3.0.dev
-----------------------------------
- merge FuncargRequest and Item API such that funcarg-functionality
is now natively available on the "item" object passed to the various
pytest_runtest hooks. This allows more sensitive behaviour
of e.g. the pytest-django plugin which previously had no full
access to all instantiated funcargs.
This internal API re-organisation is a fully backward compatible
change: existing factories accepting a "request" object will
get a Function "item" object which carries the same API. In fact,
the FuncargRequest API (or rather then a ResourceRequestAPI)
could be available for all collection and item nodes but this is
left for later consideration because it would render the documentation
invalid and the "funcarg" naming sounds odd in context of
directory, file, class, etc. nodes.
- catch unicode-issues when writing failure representations
to terminal to prevent the whole session from crashing
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
@ -23,6 +36,7 @@ Changes between 2.2.4 and 2.2.5.dev
pytest-django, trial and unittest integration.
- reporting refinements:
- pytest_report_header now receives a "startdir" so that
you can use startdir.bestrelpath(yourpath) to show
nice relative path

View File

@ -1,2 +1,2 @@
#
__version__ = '2.2.5.dev4'
__version__ = '2.3.0.dev1'

View File

@ -119,7 +119,7 @@ class CaptureManager:
return "", ""
def activate_funcargs(self, pyfuncitem):
if hasattr(pyfuncitem, 'funcargs'):
if pyfuncitem.funcargs:
for name, capfuncarg in pyfuncitem.funcargs.items():
if name in ('capsys', 'capfd'):
assert not hasattr(self, '_capturing_funcarg')
@ -186,7 +186,7 @@ def pytest_funcarg__capsys(request):
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
if "capfd" in request._funcargs:
if "capfd" in request.funcargs:
raise request.LookupError(error_capsysfderror)
return CaptureFuncarg(py.io.StdCapture)
@ -195,7 +195,7 @@ def pytest_funcarg__capfd(request):
captured output available via ``capsys.readouterr()`` method calls
which return a ``(out, err)`` tuple.
"""
if "capsys" in request._funcargs:
if "capsys" in request.funcargs:
raise request.LookupError(error_capsysfderror)
if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup")

View File

@ -3,6 +3,8 @@
import py
import pytest, _pytest
import os, sys, imp
from _pytest.monkeypatch import monkeypatch
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
# exitcodes for the command line
@ -144,33 +146,162 @@ class HookProxy:
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)
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)
class Request(object):
_argprefix = "pytest_funcarg__"
class LookupError(LookupError):
""" error while performing funcarg factory lookup. """
def _initattr(self):
self._name2factory = {}
self._currentarg = None
@property
def _plugins(self):
extra = [obj for obj in (self.module, self.instance) if obj]
return self.getplugins() + extra
def _getscopeitem(self, scope):
if scope == "function":
return self
elif scope == "session":
return None
elif scope == "class":
x = self.getparent(pytest.Class)
if x is not None:
return x
scope = "module"
if scope == "module":
return self.getparent(pytest.Module)
raise ValueError("unknown scope %r" %(scope,))
def getfuncargvalue(self, argname):
""" Retrieve a named function argument value.
This function looks up a matching factory and invokes
it to obtain the return value. The factory receives
the same request object and can itself perform recursive
calls to this method, effectively allowing to make use of
multiple other funcarg values or to decorate values from
other name-matching factories.
"""
try:
return self.funcargs[argname]
except KeyError:
pass
except TypeError:
self.funcargs = getattr(self, "_funcargs", {})
if argname not in self._name2factory:
self._name2factory[argname] = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
#else: we are called recursively
if not self._name2factory[argname]:
self._raiselookupfailed(argname)
funcargfactory = self._name2factory[argname].pop()
oldarg = self._currentarg
mp = monkeypatch()
mp.setattr(self, '_currentarg', argname)
try:
param = self.callspec.getparam(argname)
except (AttributeError, ValueError):
pass
else:
mp.setattr(self, 'param', param, raising=False)
try:
self.funcargs[argname] = res = funcargfactory(self)
finally:
mp.undo()
return res
def addfinalizer(self, finalizer):
""" add a no-args finalizer function to be called when the underlying
node is torn down."""
self.session._setupstate.addfinalizer(finalizer, self)
def cached_setup(self, setup, teardown=None,
scope="module", extrakey=None):
""" Return a cached testing resource created by ``setup`` &
detroyed by a respective ``teardown(resource)`` call.
:arg teardown: function receiving a previously setup resource.
:arg setup: a no-argument function creating a resource.
:arg scope: a string value out of ``function``, ``class``, ``module``
or ``session`` indicating the caching lifecycle of the resource.
:arg extrakey: added to internal caching key.
"""
if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref?
colitem = self._getscopeitem(scope)
cachekey = (self._currentarg, colitem, extrakey)
cache = self.config._setupcache
try:
val = cache[cachekey]
except KeyError:
val = setup()
cache[cachekey] = val
if teardown is not None:
def finalizer():
del cache[cachekey]
teardown(val)
self.session._setupstate.addfinalizer(finalizer, colitem)
return val
def _raiselookupfailed(self, argname):
available = []
for plugin in self._plugins:
for name in vars(plugin):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self.reportinfo()
msg = "LookupError: no factory found for function argument %r" % (argname,)
msg += "\n available funcargs: %s" %(", ".join(available),)
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
raise self.LookupError(msg)
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)
#: keywords on this node (node name is always contained)
self.keywords = {self.name: True}
#: fspath sensitive hook proxy used to call pytest hooks
self.ihook = self.session.gethookproxy(self.fspath)
Module = compatproperty("Module")
Class = compatproperty("Class")
Instance = compatproperty("Instance")
@ -178,6 +309,11 @@ class Node(object):
File = compatproperty("File")
Item = compatproperty("Item")
module = pyobj_property("Module")
cls = pyobj_property("Class")
instance = pyobj_property("Instance")
def _getcustomclass(self, name):
cls = getattr(self, name)
if cls != getattr(pytest, name):
@ -193,12 +329,14 @@ class Node(object):
# 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
@ -338,15 +476,36 @@ class FSCollector(Collector):
class File(FSCollector):
""" base class for collecting tests from a file. """
class Item(Node):
class Item(Node, Request):
""" a basic test invocation item. Note that for a single function
there might be multiple test invocation items.
"""
nextitem = None
def __init__(self, name, parent=None, config=None, session=None):
super(Item, self).__init__(name, parent, config, session)
self._initattr()
self.funcargs = None # later set to a dict from fillfuncargs() or
# from getfuncargvalue(). Setting it to
# None prevents users from performing
# "name in item.funcargs" checks too early.
def reportinfo(self):
return self.fspath, None, ""
def applymarker(self, marker):
""" Apply a marker to this item. This method is
useful if you have several parametrized function
and want to mark a single one of them.
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``py.test.mark.NAME(...)``.
"""
if not isinstance(marker, pytest.mark.XYZ.__class__):
raise ValueError("%r is not a py.test.mark.* object")
self.keywords[marker.markname] = marker
@property
def location(self):
try:

View File

@ -318,7 +318,7 @@ class TmpTestdir:
# 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)

View File

@ -4,11 +4,27 @@ import inspect
import sys
import pytest
from py._code.code import TerminalRepr
from _pytest.monkeypatch import monkeypatch
from _pytest.main import Request, Item
import _pytest
cutdir = py.path.local(_pytest.__file__).dirpath()
def cached_property(f):
"""returns a cached property that is calculated by function f.
taken from http://code.activestate.com/recipes/576563-cached-property/"""
def get(self):
try:
return self._property_cache[f]
except AttributeError:
self._property_cache = {}
x = self._property_cache[f] = f(self)
return x
except KeyError:
x = self._property_cache[f] = f(self)
return x
return property(get)
def pytest_addoption(parser):
group = parser.getgroup("general")
group.addoption('--funcargs',
@ -60,13 +76,21 @@ def pytest_funcarg__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
if pyfuncitem._isyieldedfunction():
testfunction(*pyfuncitem._args)
else:
funcargs = pyfuncitem.funcargs
try:
funcargnames = pyfuncitem.funcargnames
except AttributeError:
funcargs = pyfuncitem.funcargs
else:
funcargs = {}
for name in funcargnames:
funcargs[name] = pyfuncitem.funcargs[name]
testfunction(**funcargs)
def pytest_collect_file(path, parent):
@ -110,6 +134,7 @@ def is_generator(func):
return False
class PyobjMixin(object):
def obj():
def fget(self):
try:
@ -232,12 +257,15 @@ class PyCollectorMixin(PyobjMixin, pytest.Collector):
gentesthook.pcall(plugins, metafunc=metafunc)
Function = self._getcustomclass("Function")
if not metafunc._calls:
return Function(name, parent=self)
return Function(name, parent=self,
funcargnames=metafunc.funcargnames)
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})
callspec=callspec, callobj=funcobj,
funcargnames=metafunc.funcargnames,
keywords={callspec.id:True})
l.append(function)
return l
@ -256,6 +284,7 @@ def transfer_markers(funcobj, cls, mod):
pytestmark(funcobj)
class Module(pytest.File, PyCollectorMixin):
""" Collector for test classes and functions. """
def _getobj(self):
return self._memoizedcall('_obj', self._importtestmodule)
@ -303,7 +332,7 @@ class Module(pytest.File, PyCollectorMixin):
self.obj.teardown_module()
class Class(PyCollectorMixin, pytest.Collector):
""" Collector for test methods. """
def collect(self):
return [self._getcustomclass("Instance")(name="()", parent=self)]
@ -373,7 +402,7 @@ class FunctionMixin(PyobjMixin):
excinfo.traceback = ntraceback.filter()
def _repr_failure_py(self, excinfo, style="long"):
if excinfo.errisinstance(FuncargRequest.LookupError):
if excinfo.errisinstance(Request.LookupError):
fspath, lineno, msg = self.reportinfo()
lines, _ = inspect.getsourcelines(self.obj)
for i, line in enumerate(lines):
@ -445,77 +474,6 @@ class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
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'):
fillfuncargs(self)
def __eq__(self, other):
try:
return (self.name == other.name and
self._args == other._args and
self.parent == other.parent and
self.obj == other.obj and
getattr(self, '_genid', None) ==
getattr(other, '_genid', None)
)
except AttributeError:
pass
return False
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash((self.parent, self.name))
def hasinit(obj):
init = getattr(obj, '__init__', None)
if init:
@ -535,10 +493,20 @@ def getfuncargnames(function, startindex=None):
return argnames[startindex:-numdefaults]
return argnames[startindex:]
def fillfuncargs(function):
def fillfuncargs(node):
""" fill missing funcargs. """
request = FuncargRequest(pyfuncitem=function)
request._fillfuncargs()
if not isinstance(node, Function):
node = FuncargRequest(pyfuncitem=node)
if node.funcargs is None:
node.funcargs = getattr(node, "_funcargs", {})
if not isinstance(node, Function) or not node._isyieldedfunction():
try:
funcargnames = node.funcargnames
except AttributeError:
funcargnames = getfuncargnames(node.function)
if funcargnames:
for argname in funcargnames:
node.getfuncargvalue(argname)
_notexists = object()
@ -697,195 +665,6 @@ class IDMaker:
l.append(str(val))
return "-".join(l)
class FuncargRequest:
""" A request for function arguments from a test function.
Note that there is an optional ``param`` attribute in case
there was an invocation to metafunc.addcall(param=...).
If no such call was done in a ``pytest_generate_tests``
hook, the attribute will not be present.
"""
_argprefix = "pytest_funcarg__"
_argname = None
class LookupError(LookupError):
""" error on performing funcarg request. """
def __init__(self, pyfuncitem):
self._pyfuncitem = pyfuncitem
if hasattr(pyfuncitem, '_requestparam'):
self.param = pyfuncitem._requestparam
extra = [obj for obj in (self.module, self.instance) if obj]
self._plugins = pyfuncitem.getplugins() + extra
self._funcargs = self._pyfuncitem.funcargs.copy()
self._name2factory = {}
self._currentarg = None
@property
def function(self):
""" function object of the test invocation. """
return self._pyfuncitem.obj
@property
def keywords(self):
""" keywords of the test function item.
.. versionadded:: 2.0
"""
return self._pyfuncitem.keywords
@property
def module(self):
""" module where the test function was collected. """
return self._pyfuncitem.getparent(pytest.Module).obj
@property
def cls(self):
""" class (can be None) where the test function was collected. """
clscol = self._pyfuncitem.getparent(pytest.Class)
if clscol:
return clscol.obj
@property
def instance(self):
""" instance (can be None) on which test function was collected. """
return py.builtin._getimself(self.function)
@property
def config(self):
""" the pytest config object associated with this request. """
return self._pyfuncitem.config
@property
def fspath(self):
""" the file system path of the test module which collected this test. """
return self._pyfuncitem.fspath
def _fillfuncargs(self):
argnames = getfuncargnames(self.function)
if argnames:
assert not getattr(self._pyfuncitem, '_args', None), (
"yielded functions cannot have funcargs")
for argname in argnames:
if argname not in self._pyfuncitem.funcargs:
self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname)
def applymarker(self, marker):
""" Apply a marker to a single test function invocation.
This method is useful if you don't want to have a keyword/marker
on all function invocations.
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
created by a call to ``py.test.mark.NAME(...)``.
"""
if not isinstance(marker, py.test.mark.XYZ.__class__):
raise ValueError("%r is not a py.test.mark.* object")
self._pyfuncitem.keywords[marker.markname] = marker
def cached_setup(self, setup, teardown=None, scope="module", extrakey=None):
""" Return a testing resource managed by ``setup`` &
``teardown`` calls. ``scope`` and ``extrakey`` determine when the
``teardown`` function will be called so that subsequent calls to
``setup`` would recreate the resource.
:arg teardown: function receiving a previously setup resource.
:arg setup: a no-argument function creating a resource.
:arg scope: a string value out of ``function``, ``class``, ``module``
or ``session`` indicating the caching lifecycle of the resource.
:arg extrakey: added to internal caching key of (funcargname, scope).
"""
if not hasattr(self.config, '_setupcache'):
self.config._setupcache = {} # XXX weakref?
cachekey = (self._currentarg, self._getscopeitem(scope), extrakey)
cache = self.config._setupcache
try:
val = cache[cachekey]
except KeyError:
val = setup()
cache[cachekey] = val
if teardown is not None:
def finalizer():
del cache[cachekey]
teardown(val)
self._addfinalizer(finalizer, scope=scope)
return val
def getfuncargvalue(self, argname):
""" Retrieve a function argument by name for this test
function invocation. This allows one function argument factory
to call another function argument factory. If there are two
funcarg factories for the same test function argument the first
factory may use ``getfuncargvalue`` to call the second one and
do something additional with the resource.
"""
try:
return self._funcargs[argname]
except KeyError:
pass
if argname not in self._name2factory:
self._name2factory[argname] = self.config.pluginmanager.listattr(
plugins=self._plugins,
attrname=self._argprefix + str(argname)
)
#else: we are called recursively
if not self._name2factory[argname]:
self._raiselookupfailed(argname)
funcargfactory = self._name2factory[argname].pop()
oldarg = self._currentarg
mp = monkeypatch()
mp.setattr(self, '_currentarg', argname)
try:
param = self._pyfuncitem.callspec.getparam(argname)
except (AttributeError, ValueError):
pass
else:
mp.setattr(self, 'param', param, raising=False)
try:
self._funcargs[argname] = res = funcargfactory(request=self)
finally:
mp.undo()
return res
def _getscopeitem(self, scope):
if scope == "function":
return self._pyfuncitem
elif scope == "session":
return None
elif scope == "class":
x = self._pyfuncitem.getparent(pytest.Class)
if x is not None:
return x
scope = "module"
if scope == "module":
return self._pyfuncitem.getparent(pytest.Module)
raise ValueError("unknown finalization scope %r" %(scope,))
def addfinalizer(self, finalizer):
"""add finalizer function to be called after test function
finished execution. """
self._addfinalizer(finalizer, scope="function")
def _addfinalizer(self, finalizer, scope):
colitem = self._getscopeitem(scope)
self._pyfuncitem.session._setupstate.addfinalizer(
finalizer=finalizer, colitem=colitem)
def __repr__(self):
return "<FuncargRequest for %r>" %(self._pyfuncitem)
def _raiselookupfailed(self, argname):
available = []
for plugin in self._plugins:
for name in vars(plugin):
if name.startswith(self._argprefix):
name = name[len(self._argprefix):]
if name not in available:
available.append(name)
fspath, lineno, msg = self._pyfuncitem.reportinfo()
msg = "LookupError: no factory found for function argument %r" % (argname,)
msg += "\n available funcargs: %s" %(", ".join(available),)
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
raise self.LookupError(msg)
def showfuncargs(config):
from _pytest.main import wrap_session
@ -903,8 +682,8 @@ def _showfuncargs_main(config, session):
for plugin in plugins:
available = []
for name, factory in vars(plugin).items():
if name.startswith(FuncargRequest._argprefix):
name = name[len(FuncargRequest._argprefix):]
if name.startswith(Request._argprefix):
name = name[len(Request._argprefix):]
if name not in available:
available.append([name, factory])
if available:
@ -1009,3 +788,131 @@ class RaisesContext(object):
self.excinfo.__init__(tp)
return issubclass(self.excinfo.type, self.ExpectedException)
#
# the basic py.test Function item
#
_dummy = object()
class Function(FunctionMixin, pytest.Item):
""" a Function Item is responsible for setting up and executing a
Python test function.
"""
_genid = None
def __init__(self, name, parent=None, args=None, config=None,
callspec=None, callobj=_dummy, keywords=None,
session=None, funcargnames=()):
super(Function, self).__init__(name, parent, config=config,
session=session)
self.funcargnames = funcargnames
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.param = callspec.param
if callobj is not _dummy:
self._obj = callobj
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
if keywords:
self.keywords.update(keywords)
@property
def function(self):
"underlying python 'function' object"
return getattr(self.obj, 'im_func', self.obj)
def _getobj(self):
name = self.name
i = name.find("[") # parametrization
if i != -1:
name = name[:i]
return getattr(self.parent.obj, name)
@property
def _pyfuncitem(self):
"(compatonly) for code expecting pytest-2.2 style request objects"
return self
def _isyieldedfunction(self):
return getattr(self, "_args", None) 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()
fillfuncargs(self)
def __eq__(self, other):
try:
return (self.name == other.name and
self._args == other._args and
self.parent == other.parent and
self.obj == other.obj and
getattr(self, '_genid', None) ==
getattr(other, '_genid', None)
)
except AttributeError:
pass
return False
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash((self.parent, self.name))
def itemapi_property(name, set=False):
prop = getattr(Function, name, None)
doc = getattr(prop, "__doc__", None)
def get(self):
return getattr(self._pyfuncitem, name)
if set:
def set(self, value):
setattr(self._pyfuncitem, name, value)
else:
set = None
return property(get, set, None, doc)
class FuncargRequest(Request):
""" (deprecated) helper interactions with a test function invocation.
Note that there is an optional ``param`` attribute in case
there was an invocation to metafunc.addcall(param=...).
If no such call was done in a ``pytest_generate_tests``
hook, the attribute will not be present.
"""
def __init__(self, pyfuncitem):
self._pyfuncitem = pyfuncitem
Request._initattr(self)
self.getplugins = self._pyfuncitem.getplugins
self.reportinfo = self._pyfuncitem.reportinfo
try:
self.param = self._pyfuncitem.param
except AttributeError:
pass
def __repr__(self):
return "<FuncargRequest for %r>" % (self._pyfuncitem.name)
_getscopeitem = itemapi_property("_getscopeitem")
funcargs = itemapi_property("funcargs", set=True)
keywords = itemapi_property("keywords")
module = itemapi_property("module")
cls = itemapi_property("cls")
instance = itemapi_property("instance")
config = itemapi_property("config")
session = itemapi_property("session")
fspath = itemapi_property("fspath")
applymarker = itemapi_property("applymarker")
@property
def function(self):
return self._pyfuncitem.obj

View File

@ -1,4 +1,6 @@
""" (disabled by default) create result information in a plain text file. """
""" log machine-parseable test session result information in a plain
text file.
"""
import py

View File

@ -54,15 +54,15 @@ def pytest_configure(config):
mp.setattr(config, '_tmpdirhandler', t, raising=False)
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
def pytest_funcarg__tmpdir(request):
def pytest_funcarg__tmpdir(item):
"""return a temporary directory path object
which is unique to each test function invocation,
created as a sub directory of the base temporary
directory. The returned object is a `py.path.local`_
path object.
"""
name = request._pyfuncitem.name
name = item.name
name = py.std.re.sub("[\W]", "_", name)
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
x = item.config._tmpdirhandler.mktemp(name, numbered=True)
return x

View File

@ -17,7 +17,7 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
version = release = "2.2.4.3"
version = release = "2.3.0.dev1"
import sys, os
@ -26,6 +26,8 @@ import sys, os
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
autodoc_member_order = "bysource"
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -53,6 +55,7 @@ project = u'pytest'
copyright = u'2011, holger krekel et alii'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
@ -78,7 +81,7 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
@ -87,6 +90,8 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []

View File

@ -11,26 +11,27 @@ Injecting objects into test functions (funcargs)
Dependency injection through function arguments
=================================================
py.test lets you inject objects into test functions and precisely
control their life cycle in relation to the test execution. It is
also possible to run a test function multiple times with different objects.
py.test lets you inject objects into test invocations and precisely
control their life cycle in relation to the overall test execution. Moreover,
you can run a test function multiple times injecting different objects.
The basic mechanism for injecting objects is also called the
*funcarg mechanism* because objects are ultimately injected
by calling a test function with it as an argument. Unlike the
classical xUnit approach *funcargs* relate more to `Dependency Injection`_
because they help to de-couple test code from objects required for
them to execute.
them to execute. At test writing time you do not need to care for the
details of how your required resources are constructed or if they
live through a function, class, module or session scope.
.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection
To create a value with which to call a test function a factory function
is called which gets full access to the test function context and can
register finalizers or invoke lifecycle-caching helpers. The factory
can be implemented in same test class or test module, or in a
per-directory ``conftest.py`` file or even in an external plugin. This
allows full de-coupling of test code and objects needed for test
execution.
can be implemented in same test class or test module, in a
per-directory ``conftest.py`` file or in an external plugin. This
allows total de-coupling of test and setup code.
A test function may be invoked multiple times in which case we
speak of :ref:`parametrized testing <parametrizing-tests>`. This can be
@ -38,7 +39,7 @@ very useful if you want to test e.g. against different database backends
or with multiple numerical arguments sets and want to reuse the same set
of test functions.
py.test comes with :ref:`builtinfuncargs` and there are some refined usages in the examples section.
py.test comes with some :ref:`builtinfuncargs` and there are some refined usages in the examples section.
.. _funcarg:
@ -55,10 +56,8 @@ Let's look at a simple self-contained test module::
assert myfuncarg == 17
This test function needs an injected object named ``myfuncarg``.
py.test will discover and call the factory named
``pytest_funcarg__myfuncarg`` within the same module in this case.
Running the test looks like this::
py.test will automatically discover and call the ``pytest_funcarg__myfuncarg``
factory. Running the test looks like this::
$ py.test test_simplefactory.py
=========================== test session starts ============================
@ -79,9 +78,9 @@ Running the test looks like this::
test_simplefactory.py:5: AssertionError
========================= 1 failed in 0.01 seconds =========================
This means that indeed the test function was called with a ``myfuncarg``
argument value of ``42`` and the assert fails. Here is how py.test
comes to call the test function this way:
This shows that the test function was called with a ``myfuncarg``
argument value of ``42`` and the assert fails as expected. Here is
how py.test comes to call the test function this way:
1. py.test :ref:`finds <test discovery>` the ``test_function`` because
of the ``test_`` prefix. The test function needs a function argument
@ -99,13 +98,22 @@ Note that if you misspell a function argument or want
to use one that isn't available, you'll see an error
with a list of available function arguments.
You can always issue::
.. Note::
py.test --funcargs test_simplefactory.py
You can always issue::
to see available function arguments (which you can also
think of as "resources").
py.test --funcargs test_simplefactory.py
to see available function arguments.
The request object passed to factories
-----------------------------------------
Each funcarg factory receives a :py:class:`~_pytest.main.Request` object which
provides methods to manage caching and finalization in the context of the
test invocation as well as several attributes of the the underlying test item. In fact, as of version pytest-2.3, the request API is implemented on all Item
objects and therefore the request object has general :py:class:`Node attributes and methods <_pytest.main.Node>` attributes. This is a backward compatible
change so no changes are neccessary for pre-2.3 funcarg factories.
.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
@ -116,27 +124,6 @@ think of as "resources").
.. _`funcarg factory`:
.. _factory:
The funcarg **request** object
=============================================
Each funcarg factory receives a **request** object tied to a specific test
function call. A request object is passed to a funcarg factory and provides
access to test configuration and context:
.. autoclass:: _pytest.python.FuncargRequest()
:members: function,cls,module,keywords,config
.. _`useful caching and finalization helpers`:
.. automethod:: FuncargRequest.addfinalizer
.. automethod:: FuncargRequest.cached_setup
.. automethod:: FuncargRequest.applymarker
.. automethod:: FuncargRequest.getfuncargvalue
.. _`test generators`:
.. _`parametrizing-tests`:
.. _`parametrized test functions`:

View File

@ -296,7 +296,6 @@ into interactive debugging when a test failure occurs.
The :py:mod:`_pytest.terminal` reported specifically uses
the reporting hook to print information about a test run.
Collection hooks
------------------------------
@ -327,37 +326,44 @@ test execution:
.. autofunction: pytest_runtest_logreport
Reference of important objects involved in hooks
Reference of objects involved in hooks
===========================================================
.. autoclass:: _pytest.config.Config
.. autoclass:: _pytest.main.Request()
:members:
.. autoclass:: _pytest.config.Parser
.. autoclass:: _pytest.config.Config()
:members:
.. autoclass:: _pytest.main.Node(name, parent)
.. autoclass:: _pytest.config.Parser()
:members:
..
.. autoclass:: _pytest.main.File(fspath, parent)
:members:
.. autoclass:: _pytest.main.Item(name, parent)
:members:
.. autoclass:: _pytest.python.Module(name, parent)
:members:
.. autoclass:: _pytest.python.Class(name, parent)
:members:
.. autoclass:: _pytest.python.Function(name, parent)
:members:
.. autoclass:: _pytest.runner.CallInfo
.. autoclass:: _pytest.main.Node()
:members:
.. autoclass:: _pytest.runner.TestReport
.. autoclass:: _pytest.main.Collector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Item()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Module()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Class()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Function()
:members:
:show-inheritance:
.. autoclass:: _pytest.runner.CallInfo()
:members:
.. autoclass:: _pytest.runner.TestReport()
:members:

View File

@ -24,7 +24,7 @@ def main():
name='pytest',
description='py.test: simple powerful testing with Python',
long_description = long_description,
version='2.2.5.dev4',
version='2.3.0.dev1',
url='http://pytest.org',
license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -12,33 +12,25 @@ def interpret(expr):
class TestBinReprIntegration:
pytestmark = needsnewassert
def pytest_funcarg__hook(self, request):
class MockHook(object):
def __init__(self):
self.called = False
self.args = tuple()
self.kwargs = dict()
def __call__(self, op, left, right):
self.called = True
self.op = op
self.left = left
self.right = right
mockhook = MockHook()
monkeypatch = request.getfuncargvalue("monkeypatch")
monkeypatch.setattr(util, '_reprcompare', mockhook)
return mockhook
def test_pytest_assertrepr_compare_called(self, hook):
interpret('assert 0 == 1')
assert hook.called
def test_pytest_assertrepr_compare_args(self, hook):
interpret('assert [0, 1] == [0, 2]')
assert hook.op == '=='
assert hook.left == [0, 1]
assert hook.right == [0, 2]
def test_pytest_assertrepr_compare_called(self, testdir):
testdir.makeconftest("""
l = []
def pytest_assertrepr_compare(op, left, right):
l.append((op, left, right))
def pytest_funcarg__l(request):
return l
""")
testdir.makepyfile("""
def test_hello():
assert 0 == 1
def test_check(l):
assert l == [("==", 0, 1)]
""")
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines([
"*test_hello*FAIL*",
"*test_check*PASS*",
])
def callequal(left, right):
return plugin.pytest_assertrepr_compare('==', left, right)

View File

@ -690,8 +690,8 @@ class TestRequest:
assert val2 == 2
val2 = req.getfuncargvalue("other") # see about caching
assert val2 == 2
req._fillfuncargs()
assert item.funcargs == {'something': 1}
pytest._fillfuncargs(item)
assert item.funcargs == {'something': 1, "other": 2}
def test_request_addfinalizer(self, testdir):
item = testdir.getitem("""
@ -700,9 +700,8 @@ class TestRequest:
request.addfinalizer(lambda: teardownlist.append(1))
def test_func(something): pass
""")
req = funcargs.FuncargRequest(item)
req._pyfuncitem.session._setupstate.prepare(item) # XXX
req._fillfuncargs()
item.session._setupstate.prepare(item)
pytest._fillfuncargs(item)
# successively check finalization calls
teardownlist = item.getparent(pytest.Module).obj.teardownlist
ss = item.session._setupstate
@ -799,7 +798,8 @@ class TestRequestCachedSetup:
req3 = funcargs.FuncargRequest(item3)
ret3a = req3.cached_setup(setup, scope="class")
ret3b = req3.cached_setup(setup, scope="class")
assert ret3a == ret3b == "hello2"
assert ret3a == "hello2"
assert ret3b == "hello2"
req4 = funcargs.FuncargRequest(item4)
ret4 = req4.cached_setup(setup, scope="class")
assert ret4 == ret3a
@ -830,11 +830,12 @@ class TestRequestCachedSetup:
ret1 = req1.cached_setup(setup, teardown, scope="function")
assert l == ['setup']
# artificial call of finalizer
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
setupstate = req1._pyfuncitem.session._setupstate
setupstate._callfinalizers(item1)
assert l == ["setup", "teardown"]
ret2 = req1.cached_setup(setup, teardown, scope="function")
assert l == ["setup", "teardown", "setup"]
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
setupstate._callfinalizers(item1)
assert l == ["setup", "teardown", "setup", "teardown"]
def test_request_cached_setup_two_args(self, testdir):
@ -1092,9 +1093,9 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc):
metafunc.addcall(param=metafunc)
def pytest_funcarg__metafunc(request):
assert request._pyfuncitem._genid == "0"
return request.param
def pytest_funcarg__metafunc(item):
assert item._genid == "0"
return item.param
def test_function(metafunc, pytestconfig):
assert metafunc.config == pytestconfig
@ -1588,3 +1589,61 @@ def test_issue117_sessionscopeteardown(testdir):
"*3/x*",
"*ZeroDivisionError*",
])
class TestRequestAPI:
def test_addfinalizer_cachedsetup_getfuncargvalue(self, testdir):
testdir.makeconftest("""
l = []
def pytest_runtest_setup(item):
item.addfinalizer(lambda: l.append(1))
l2 = item.getfuncargvalue("l")
assert l2 is l
item.cached_setup(lambda: l.append(2), lambda val: l.append(3),
scope="function")
def pytest_funcarg__l(request):
return l
""")
testdir.makepyfile("""
def test_hello():
pass
def test_hello2(l):
assert l == [2, 3, 1, 2]
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*2 passed*",
])
def test_runtest_setup_sees_filled_funcargs(self, testdir):
testdir.makeconftest("""
def pytest_runtest_setup(item):
assert item.funcargs is None
""")
testdir.makepyfile("""
def pytest_funcarg__a(request):
return 1
def pytest_funcarg__b(request):
return request.getfuncargvalue("a") + 1
def test_hello(b):
assert b == 2
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])
result = testdir.makeconftest("""
import pytest
@pytest.mark.trylast
def pytest_runtest_setup(item):
assert item.funcargs == {"a": 1, "b": 2}
""")
result = testdir.runpytest()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])

View File

@ -2,7 +2,6 @@ import py, pytest
import os
from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler
from _pytest.python import FuncargRequest
def test_funcarg(testdir):
item = testdir.getitem("""
@ -11,12 +10,12 @@ def test_funcarg(testdir):
metafunc.addcall(id='b')
def test_func(tmpdir): pass
""", 'test_func[a]')
p = pytest_funcarg__tmpdir(FuncargRequest(item))
p = pytest_funcarg__tmpdir(item)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn.endswith("test_func_a_")
item.name = "qwe/\\abc"
p = pytest_funcarg__tmpdir(FuncargRequest(item))
p = pytest_funcarg__tmpdir(item)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn == "qwe__abc"

View File

@ -24,7 +24,7 @@ basepython=python2.7
deps=pytest-xdist
commands=
py.test -n3 -rfsxX \
--ignore .tox --junitxml={envlogdir}/junit-{envname}.xml []
--ignore .tox --junitxml={envlogdir}/junit-{envname}.xml testing
[testenv:trial]
changedir=.