re-introduce the old 2.2.4 FuncargRequest implementation as it is a better
base for implementing the new funcarg/setup api. Also Un-optimize funcargnames discovery for now.
This commit is contained in:
parent
4766497515
commit
c7ee6e71ab
|
@ -187,7 +187,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)
|
||||
|
||||
|
@ -196,7 +196,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")
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
|
||||
Implementation plan for resources
|
||||
------------------------------------------
|
||||
|
||||
1. Revert FuncargRequest to the old form, unmerge item/request
|
||||
2. make setup functions be discovered at collection time
|
||||
3. make funcarg factories be discovered at collection time
|
||||
4. Introduce funcarg marker
|
||||
5. Introduce funcarg scope parameter
|
||||
6. Introduce funcarg parametrize parameter
|
||||
7. (Introduce a pytest_fixture_protocol/setup_funcargs hook)
|
||||
|
||||
methods and data structures
|
||||
--------------------------------
|
||||
|
||||
A FuncarcDB holds all information about funcarg definitions,
|
||||
parametrization and the places where funcargs are required. It can
|
||||
answer the following questions:
|
||||
|
||||
* given a node and a funcargname, return a paramlist so that collection
|
||||
can perform parametrization (parametrized nodes?)
|
||||
* given a node (possibly containing a param), perform a funcargrequest
|
||||
and return the value
|
||||
* if funcargname is an empty string, it matches general setup.
|
||||
|
||||
pytest could perform 2-pass collection:
|
||||
- first perform normal collection (no parametrization at all!), populate
|
||||
FuncargDB
|
||||
- walk through the node tree and ask FuncargDB for each node for
|
||||
required funcargs and their parameters - clone subtrees (deepcopy) and
|
||||
substitute the un-parametrized node with parametrized ones
|
||||
|
||||
as a simple example, let's consider a tree where a test function requires
|
||||
a "abc" funcarg and its factory defines it as parametrized and scoped
|
||||
for Modules. When the 2nd collection pass asks FuncargDB to return
|
||||
params for the test module, it will know that the test functions in it
|
||||
requires "abc" and that is it parametrized and defined for module scope.
|
||||
Therefore parametrization of the module node is performed, substituting
|
||||
the node with multiple module nodes ("test_module.py[1]", ...).
|
||||
When test_module.py[1] is setup() it will call all its (parametrized)
|
||||
factories and populate a funcargs dictionary, mapping funcargnames to values.
|
||||
When a test function below test_module.py[1] is executed, it looks up
|
||||
its required arguments from the thus populated funcargs dictionary.
|
||||
|
||||
Let's add to this example a second funcarg "def" that has a per-function parametrization. When the 2nd collection pass asks FuncargDB to return
|
||||
params for the test function, it will know that the test functions in it
|
||||
requires "def" and that is it parametrized and defined for function scope.
|
||||
Therefore parametrization of the function node is performed, substituting
|
||||
the node with multiple function nodes ("test_function[1]", ...).
|
||||
|
||||
When test_function[1] is setup() it will call all its (parametrized)
|
||||
factories and populate a funcargs dictionary. The "def" will only appear
|
||||
in the funcargs dict seen by test_function[1]. When test_function[1]
|
||||
executes, it will use its funcargs.
|
||||
|
||||
|
||||
|
||||
|
||||
where
|
||||
|
||||
* ``nodeidbase`` is a basestring; for all nodeids matching
|
||||
startswith(nodeidbase) it defines a (scopecls, factorylist) tuple
|
||||
* ``scopecls`` is a node class for the which the factorylist s defined
|
||||
* ``param`` is a parametrizing parameter for the factorylist
|
||||
* ``factorylist`` is a list of factories which will be used to perform
|
||||
a funcarg request
|
||||
* the whole list is sorted by length of nodeidbase (longest first)
|
||||
|
||||
conftest loading:
|
||||
each funcarg-factory will populate FuncargDefs which keeps references
|
||||
to all definitions the funcarg2 marked function or pytest_funcarg__
|
||||
|
||||
|
||||
scope can be a string or a nodenames-tuple.
|
||||
|
||||
scopestring -> list of (funcargname, factorylist)
|
||||
|
||||
nodenames -> (funcargname, list of factories)
|
||||
|
||||
It needs to be a list because factories can decorate
|
||||
|
||||
For any given node and a required funcarg it is thus
|
||||
easy to lookup a list of matching factories.
|
||||
|
||||
When a test item is collected, it grows a dictionary
|
||||
(funcargname2factorycalllist). A factory lookup is performed
|
||||
for each required funcarg. The resulting factory call is stored
|
||||
with the item. If a function is parametrized multiple items are
|
||||
created with respective factory calls. Else if a factory is parametrized
|
||||
multiple items and calls to the factory function are created as well.
|
||||
|
||||
At setup time, an item populates a funcargs mapping, mapping names
|
||||
to values. If a value is funcarg factories are queried for a given item
|
||||
test functions and setup functions are put in a class
|
||||
which looks up required funcarg factories.
|
||||
|
||||
|
|
@ -177,11 +177,11 @@ class Node(object):
|
|||
#: fspath sensitive hook proxy used to call pytest hooks
|
||||
self.ihook = self.session.gethookproxy(self.fspath)
|
||||
|
||||
self.extrainit()
|
||||
#self.extrainit()
|
||||
|
||||
def extrainit(self):
|
||||
""""extra initialization after Node is initialized. Implemented
|
||||
by some subclasses. """
|
||||
#def extrainit(self):
|
||||
# """"extra initialization after Node is initialized. Implemented
|
||||
# by some subclasses. """
|
||||
|
||||
Module = compatproperty("Module")
|
||||
Class = compatproperty("Class")
|
||||
|
|
|
@ -33,126 +33,6 @@ def pyobj_property(name):
|
|||
name.lower(),)
|
||||
return property(get, None, None, doc)
|
||||
|
||||
class Request(object):
|
||||
_argprefix = "pytest_funcarg__"
|
||||
|
||||
class LookupError(LookupError):
|
||||
""" error while performing funcarg factory lookup. """
|
||||
|
||||
def extrainit(self):
|
||||
self._name2factory = {}
|
||||
self._currentarg = None
|
||||
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.
|
||||
|
||||
@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
|
||||
can itself perform recursive calls to this method,
|
||||
either for using multiple other funcarg values under the hood
|
||||
or to decorate values from other factories matching the same name.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
|
@ -222,6 +102,15 @@ def pytest_pyfunc_call(__multicall__, pyfuncitem):
|
|||
funcargs[name] = pyfuncitem.funcargs[name]
|
||||
testfunction(**funcargs)
|
||||
|
||||
def pytest_pyfunc_call(__multicall__, pyfuncitem):
|
||||
if not __multicall__.execute():
|
||||
testfunction = pyfuncitem.obj
|
||||
if pyfuncitem._isyieldedfunction():
|
||||
testfunction(*pyfuncitem._args)
|
||||
else:
|
||||
funcargs = pyfuncitem.funcargs
|
||||
testfunction(**funcargs)
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
ext = path.ext
|
||||
pb = path.purebasename
|
||||
|
@ -267,7 +156,7 @@ class PyobjContext(object):
|
|||
cls = pyobj_property("Class")
|
||||
instance = pyobj_property("Instance")
|
||||
|
||||
class PyobjMixin(Request, PyobjContext):
|
||||
class PyobjMixin(PyobjContext):
|
||||
def obj():
|
||||
def fget(self):
|
||||
try:
|
||||
|
@ -390,14 +279,12 @@ class PyCollector(PyobjMixin, pytest.Collector):
|
|||
gentesthook.pcall(plugins, metafunc=metafunc)
|
||||
Function = self._getcustomclass("Function")
|
||||
if not metafunc._calls:
|
||||
return Function(name, parent=self,
|
||||
funcargnames=metafunc.funcargnames)
|
||||
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,
|
||||
funcargnames=metafunc.funcargnames,
|
||||
keywords={callspec.id:True})
|
||||
l.append(function)
|
||||
return l
|
||||
|
@ -494,6 +381,7 @@ class Instance(PyCollector):
|
|||
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'):
|
||||
|
@ -535,7 +423,7 @@ class FunctionMixin(PyobjMixin):
|
|||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
def _repr_failure_py(self, excinfo, style="long"):
|
||||
if excinfo.errisinstance(Request.LookupError):
|
||||
if excinfo.errisinstance(FuncargRequest.LookupError):
|
||||
fspath, lineno, msg = self.reportinfo()
|
||||
lines, _ = inspect.getsourcelines(self.obj)
|
||||
for i, line in enumerate(lines):
|
||||
|
@ -626,10 +514,21 @@ def getfuncargnames(function, startindex=None):
|
|||
return argnames[startindex:-numdefaults]
|
||||
return argnames[startindex:]
|
||||
|
||||
def fillfuncargs(node):
|
||||
def fillfuncargs(function):
|
||||
""" fill missing funcargs. """
|
||||
if not isinstance(node, Function):
|
||||
node = OldFuncargRequest(pyfuncitem=node)
|
||||
#if not getattr(function, "_args", None) is not None:
|
||||
# request = FuncargRequest(pyfuncitem=function)
|
||||
# request._fillfuncargs()
|
||||
if getattr(function, "_args", None) is None:
|
||||
try:
|
||||
request = function._request
|
||||
except AttributeError:
|
||||
request = FuncargRequest(function)
|
||||
request._fillfuncargs()
|
||||
|
||||
def XXXfillfuncargs(node):
|
||||
""" fill missing funcargs. """
|
||||
node = FuncargRequest(node)
|
||||
if node.funcargs is None:
|
||||
node.funcargs = getattr(node, "_funcargs", {})
|
||||
if not isinstance(node, Function) or not node._isyieldedfunction():
|
||||
|
@ -815,8 +714,8 @@ def _showfuncargs_main(config, session):
|
|||
for plugin in plugins:
|
||||
available = []
|
||||
for name, factory in vars(plugin).items():
|
||||
if name.startswith(Request._argprefix):
|
||||
name = name[len(Request._argprefix):]
|
||||
if name.startswith(FuncargRequest._argprefix):
|
||||
name = name[len(FuncargRequest._argprefix):]
|
||||
if name not in available:
|
||||
available.append([name, factory])
|
||||
if available:
|
||||
|
@ -931,11 +830,9 @@ class Function(FunctionMixin, pytest.Item):
|
|||
"""
|
||||
_genid = None
|
||||
def __init__(self, name, parent=None, args=None, config=None,
|
||||
callspec=None, callobj=_dummy, keywords=None,
|
||||
session=None, funcargnames=()):
|
||||
callspec=None, callobj=_dummy, keywords=None, session=None):
|
||||
super(Function, self).__init__(name, parent, config=config,
|
||||
session=session)
|
||||
self.funcargnames = funcargnames
|
||||
self._args = args
|
||||
if self._isyieldedfunction():
|
||||
assert not callspec, (
|
||||
|
@ -943,12 +840,17 @@ class Function(FunctionMixin, pytest.Item):
|
|||
else:
|
||||
if callspec is not None:
|
||||
self.callspec = callspec
|
||||
self._funcargs = callspec.funcargs or {}
|
||||
self.funcargs = callspec.funcargs or {}
|
||||
self._genid = callspec.id
|
||||
if hasattr(callspec, "param"):
|
||||
self.param = callspec.param
|
||||
else:
|
||||
self.funcargs = {}
|
||||
self._request = req = FuncargRequest(self)
|
||||
if callobj is not _dummy:
|
||||
self._obj = callobj
|
||||
self.obj = callobj
|
||||
startindex = int(self.cls is not None)
|
||||
self.funcargnames = getfuncargnames(self.obj, startindex=startindex)
|
||||
|
||||
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
|
||||
if keywords:
|
||||
|
@ -1002,49 +904,196 @@ class Function(FunctionMixin, pytest.Item):
|
|||
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:
|
||||
""" A request for function arguments from a test function.
|
||||
|
||||
|
||||
class OldFuncargRequest(Request, PyobjContext):
|
||||
""" (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.
|
||||
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
|
||||
Request.extrainit(self)
|
||||
self.funcargs = pyfuncitem.funcargs
|
||||
self.getplugins = self._pyfuncitem.getplugins
|
||||
self.reportinfo = self._pyfuncitem.reportinfo
|
||||
self.getparent = self._pyfuncitem.getparent
|
||||
try:
|
||||
self.param = self._pyfuncitem.param
|
||||
except AttributeError:
|
||||
pass
|
||||
if hasattr(pyfuncitem, '_requestparam'):
|
||||
self.param = pyfuncitem._requestparam
|
||||
self.getparent = pyfuncitem.getparent
|
||||
self._funcargs = self._pyfuncitem.funcargs.copy()
|
||||
self._name2factory = {}
|
||||
self._currentarg = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<OldFuncargRequest for %r>" % (self._pyfuncitem.name)
|
||||
@cached_property
|
||||
def _plugins(self):
|
||||
extra = [obj for obj in (self.module, self.instance) if obj]
|
||||
return self._pyfuncitem.getplugins() + extra
|
||||
|
||||
_getscopeitem = itemapi_property("_getscopeitem")
|
||||
funcargs = itemapi_property("funcargs", set=True)
|
||||
keywords = itemapi_property("keywords")
|
||||
config = itemapi_property("config")
|
||||
session = itemapi_property("session")
|
||||
fspath = itemapi_property("fspath")
|
||||
applymarker = itemapi_property("applymarker")
|
||||
@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)
|
||||
|
|
|
@ -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(item):
|
||||
def pytest_funcarg__tmpdir(request):
|
||||
"""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 = item.name
|
||||
name = request._pyfuncitem.name
|
||||
name = py.std.re.sub("[\W]", "_", name)
|
||||
x = item.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||
return x
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ with a list of available function arguments.
|
|||
The request object passed to factories
|
||||
-----------------------------------------
|
||||
|
||||
Each funcarg factory receives a :py:class:`~_pytest.python.Request` object which
|
||||
Each funcarg factory receives a :py:class:`~_pytest.python.FuncargRequest` 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
|
||||
|
|
|
@ -331,7 +331,7 @@ test execution:
|
|||
Reference of objects involved in hooks
|
||||
===========================================================
|
||||
|
||||
.. autoclass:: _pytest.python.Request()
|
||||
.. autoclass:: _pytest.python.FuncargRequest()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.config.Config()
|
||||
|
|
|
@ -277,14 +277,18 @@ class TestFunction:
|
|||
def test_function_equality(self, testdir, tmpdir):
|
||||
config = testdir.parseconfigure()
|
||||
session = testdir.Session(config)
|
||||
def func1():
|
||||
pass
|
||||
def func2():
|
||||
pass
|
||||
f1 = pytest.Function(name="name", config=config,
|
||||
args=(1,), callobj=isinstance, session=session)
|
||||
args=(1,), callobj=func1, session=session)
|
||||
f2 = pytest.Function(name="name",config=config,
|
||||
args=(1,), callobj=py.builtin.callable, session=session)
|
||||
args=(1,), callobj=func2, session=session)
|
||||
assert not f1 == f2
|
||||
assert f1 != f2
|
||||
f3 = pytest.Function(name="name", config=config,
|
||||
args=(1,2), callobj=py.builtin.callable, session=session)
|
||||
args=(1,2), callobj=func2, session=session)
|
||||
assert not f3 == f2
|
||||
assert f3 != f2
|
||||
|
||||
|
@ -292,7 +296,7 @@ class TestFunction:
|
|||
assert f3 != f1
|
||||
|
||||
f1_b = pytest.Function(name="name", config=config,
|
||||
args=(1,), callobj=isinstance, session=session)
|
||||
args=(1,), callobj=func1, session=session)
|
||||
assert f1 == f1_b
|
||||
assert not f1 != f1_b
|
||||
|
||||
|
@ -307,10 +311,12 @@ class TestFunction:
|
|||
funcargs = {}
|
||||
id = "world"
|
||||
session = testdir.Session(config)
|
||||
def func():
|
||||
pass
|
||||
f5 = pytest.Function(name="name", config=config,
|
||||
callspec=callspec1, callobj=isinstance, session=session)
|
||||
callspec=callspec1, callobj=func, session=session)
|
||||
f5b = pytest.Function(name="name", config=config,
|
||||
callspec=callspec2, callobj=isinstance, session=session)
|
||||
callspec=callspec2, callobj=func, session=session)
|
||||
assert f5 != f5b
|
||||
assert not (f5 == f5b)
|
||||
|
||||
|
@ -549,7 +555,7 @@ class TestFillFuncArgs:
|
|||
return 42
|
||||
""")
|
||||
item = testdir.getitem("def test_func(some): pass")
|
||||
exc = pytest.raises(funcargs.OldFuncargRequest.LookupError,
|
||||
exc = pytest.raises(funcargs.FuncargRequest.LookupError,
|
||||
"funcargs.fillfuncargs(item)")
|
||||
s = str(exc.value)
|
||||
assert s.find("xyzsomething") != -1
|
||||
|
@ -624,7 +630,7 @@ class TestRequest:
|
|||
def pytest_funcarg__something(request): pass
|
||||
def test_func(something): pass
|
||||
""")
|
||||
req = funcargs.OldFuncargRequest(item)
|
||||
req = funcargs.FuncargRequest(item)
|
||||
assert req.function == item.obj
|
||||
assert req.keywords is item.keywords
|
||||
assert hasattr(req.module, 'test_func')
|
||||
|
@ -639,7 +645,7 @@ class TestRequest:
|
|||
def test_func(self, something):
|
||||
pass
|
||||
""")
|
||||
req = funcargs.OldFuncargRequest(item)
|
||||
req = funcargs.FuncargRequest(item)
|
||||
assert req.cls.__name__ == "TestB"
|
||||
assert req.instance.__class__ == req.cls
|
||||
|
||||
|
@ -653,7 +659,7 @@ class TestRequest:
|
|||
""")
|
||||
item1, = testdir.genitems([modcol])
|
||||
assert item1.name == "test_method"
|
||||
name2factory = funcargs.OldFuncargRequest(item1)._name2factory
|
||||
name2factory = funcargs.FuncargRequest(item1)._name2factory
|
||||
assert len(name2factory) == 1
|
||||
assert name2factory[0].__name__ == "pytest_funcarg__something"
|
||||
|
||||
|
@ -668,7 +674,7 @@ class TestRequest:
|
|||
def test_func(something):
|
||||
assert something == 2
|
||||
""")
|
||||
req = funcargs.OldFuncargRequest(item)
|
||||
req = funcargs.FuncargRequest(item)
|
||||
val = req.getfuncargvalue("something")
|
||||
assert val == 2
|
||||
|
||||
|
@ -680,7 +686,7 @@ class TestRequest:
|
|||
return l.pop()
|
||||
def test_func(something): pass
|
||||
""")
|
||||
req = funcargs.OldFuncargRequest(item)
|
||||
req = funcargs.FuncargRequest(item)
|
||||
pytest.raises(req.LookupError, req.getfuncargvalue, "notexists")
|
||||
val = req.getfuncargvalue("something")
|
||||
assert val == 1
|
||||
|
@ -691,7 +697,8 @@ class TestRequest:
|
|||
val2 = req.getfuncargvalue("other") # see about caching
|
||||
assert val2 == 2
|
||||
pytest._fillfuncargs(item)
|
||||
assert item.funcargs == {'something': 1, "other": 2}
|
||||
assert item.funcargs == {'something': 1}
|
||||
#assert item.funcargs == {'something': 1, "other": 2}
|
||||
|
||||
def test_request_addfinalizer(self, testdir):
|
||||
item = testdir.getitem("""
|
||||
|
@ -728,7 +735,7 @@ class TestRequest:
|
|||
def test_request_getmodulepath(self, testdir):
|
||||
modcol = testdir.getmodulecol("def test_somefunc(): pass")
|
||||
item, = testdir.genitems([modcol])
|
||||
req = funcargs.OldFuncargRequest(item)
|
||||
req = funcargs.FuncargRequest(item)
|
||||
assert req.fspath == modcol.fspath
|
||||
|
||||
def test_applymarker(testdir):
|
||||
|
@ -739,7 +746,7 @@ def test_applymarker(testdir):
|
|||
def test_func2(self, something):
|
||||
pass
|
||||
""")
|
||||
req1 = funcargs.OldFuncargRequest(item1)
|
||||
req1 = funcargs.FuncargRequest(item1)
|
||||
assert 'xfail' not in item1.keywords
|
||||
req1.applymarker(pytest.mark.xfail)
|
||||
assert 'xfail' in item1.keywords
|
||||
|
@ -757,7 +764,7 @@ class TestRequestCachedSetup:
|
|||
def test_func2(self, something):
|
||||
pass
|
||||
""")
|
||||
req1 = funcargs.OldFuncargRequest(item1)
|
||||
req1 = funcargs.FuncargRequest(item1)
|
||||
l = ["hello"]
|
||||
def setup():
|
||||
return l.pop()
|
||||
|
@ -766,7 +773,7 @@ class TestRequestCachedSetup:
|
|||
assert ret1 == "hello"
|
||||
ret1b = req1.cached_setup(setup)
|
||||
assert ret1 == ret1b
|
||||
req2 = funcargs.OldFuncargRequest(item2)
|
||||
req2 = funcargs.FuncargRequest(item2)
|
||||
ret2 = req2.cached_setup(setup)
|
||||
assert ret2 == ret1
|
||||
|
||||
|
@ -782,7 +789,7 @@ class TestRequestCachedSetup:
|
|||
def test_func2b(self, something):
|
||||
pass
|
||||
""")
|
||||
req1 = funcargs.OldFuncargRequest(item2)
|
||||
req1 = funcargs.FuncargRequest(item2)
|
||||
l = ["hello2", "hello"]
|
||||
def setup():
|
||||
return l.pop()
|
||||
|
@ -791,22 +798,22 @@ class TestRequestCachedSetup:
|
|||
# automatically turn "class" to "module" scope
|
||||
ret1 = req1.cached_setup(setup, scope="class")
|
||||
assert ret1 == "hello"
|
||||
req2 = funcargs.OldFuncargRequest(item2)
|
||||
req2 = funcargs.FuncargRequest(item2)
|
||||
ret2 = req2.cached_setup(setup, scope="class")
|
||||
assert ret2 == "hello"
|
||||
|
||||
req3 = funcargs.OldFuncargRequest(item3)
|
||||
req3 = funcargs.FuncargRequest(item3)
|
||||
ret3a = req3.cached_setup(setup, scope="class")
|
||||
ret3b = req3.cached_setup(setup, scope="class")
|
||||
assert ret3a == "hello2"
|
||||
assert ret3b == "hello2"
|
||||
req4 = funcargs.OldFuncargRequest(item4)
|
||||
req4 = funcargs.FuncargRequest(item4)
|
||||
ret4 = req4.cached_setup(setup, scope="class")
|
||||
assert ret4 == ret3a
|
||||
|
||||
def test_request_cachedsetup_extrakey(self, testdir):
|
||||
item1 = testdir.getitem("def test_func(): pass")
|
||||
req1 = funcargs.OldFuncargRequest(item1)
|
||||
req1 = funcargs.FuncargRequest(item1)
|
||||
l = ["hello", "world"]
|
||||
def setup():
|
||||
return l.pop()
|
||||
|
@ -821,7 +828,7 @@ class TestRequestCachedSetup:
|
|||
|
||||
def test_request_cachedsetup_cache_deletion(self, testdir):
|
||||
item1 = testdir.getitem("def test_func(): pass")
|
||||
req1 = funcargs.OldFuncargRequest(item1)
|
||||
req1 = funcargs.FuncargRequest(item1)
|
||||
l = []
|
||||
def setup():
|
||||
l.append("setup")
|
||||
|
@ -1093,9 +1100,9 @@ class TestMetafuncFunctional:
|
|||
def pytest_generate_tests(metafunc):
|
||||
metafunc.addcall(param=metafunc)
|
||||
|
||||
def pytest_funcarg__metafunc(item):
|
||||
assert item._genid == "0"
|
||||
return item.param
|
||||
def pytest_funcarg__metafunc(request):
|
||||
assert request._pyfuncitem._genid == "0"
|
||||
return request.param
|
||||
|
||||
def test_function(metafunc, pytestconfig):
|
||||
assert metafunc.config == pytestconfig
|
||||
|
@ -1591,6 +1598,7 @@ def test_issue117_sessionscopeteardown(testdir):
|
|||
])
|
||||
|
||||
class TestRequestAPI:
|
||||
@pytest.mark.xfail(reason="reverted refactoring")
|
||||
def test_addfinalizer_cachedsetup_getfuncargvalue(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
l = []
|
||||
|
@ -1615,10 +1623,11 @@ class TestRequestAPI:
|
|||
"*2 passed*",
|
||||
])
|
||||
|
||||
@pytest.mark.xfail(reason="consider item's funcarg access and error conditions")
|
||||
def test_runtest_setup_sees_filled_funcargs(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_runtest_setup(item):
|
||||
assert item.funcargs is None
|
||||
assert not hasattr(item, "_request")
|
||||
""")
|
||||
testdir.makepyfile("""
|
||||
def pytest_funcarg__a(request):
|
||||
|
|
Loading…
Reference in New Issue