introduce metafunc.parametrize() and @pytest.mark.parametrize with examples. deprecate metafunc.addcall()

This commit is contained in:
holger krekel
2011-11-17 11:09:21 +00:00
parent 48a6a504b6
commit ec0565fac5
11 changed files with 528 additions and 234 deletions

View File

@@ -4,6 +4,7 @@ import inspect
import sys
import pytest
from py._code.code import TerminalRepr
from _pytest.monkeypatch import monkeypatch
import _pytest
cutdir = py.path.local(_pytest.__file__).dirpath()
@@ -26,6 +27,23 @@ def pytest_cmdline_main(config):
showfuncargs(config)
return 0
def pytest_generate_tests(metafunc):
try:
param = metafunc.function.parametrize
except AttributeError:
return
metafunc.parametrize(*param.args, **param.kwargs)
def pytest_configure(config):
config.addinivalue_line("markers",
"parametrize(argnames, argvalues): call a test function multiple "
"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."
)
@pytest.mark.trylast
def pytest_namespace():
raises.Exception = pytest.fail.Exception
@@ -425,6 +443,7 @@ class Function(FunctionMixin, pytest.Item):
"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"):
@@ -501,15 +520,59 @@ def fillfuncargs(function):
request._fillfuncargs()
_notexists = object()
class CallSpec:
def __init__(self, funcargs, id, param):
self.funcargs = funcargs
self.id = id
class CallSpec2(object):
def __init__(self, metafunc):
self.metafunc = metafunc
self.funcargs = {}
self._idlist = []
self.params = {}
self._globalid = _notexists
self._globalid_args = set()
self._globalparam = _notexists
def copy(self, metafunc):
cs = CallSpec2(self.metafunc)
cs.funcargs.update(self.funcargs)
cs.params.update(self.params)
cs._idlist = list(self._idlist)
cs._globalid = self._globalid
cs._globalid_args = self._globalid_args
cs._globalparam = self._globalparam
return cs
def _checkargnotcontained(self, arg):
if arg in self.params or arg in self.funcargs:
raise ValueError("duplicate %r" %(arg,))
def getparam(self, name):
try:
return self.params[name]
except KeyError:
if self._globalparam is _notexists:
raise ValueError(name)
return self._globalparam
@property
def id(self):
return "-".join(filter(None, self._idlist))
def setmulti(self, valtype, argnames, valset, id):
for arg,val in zip(argnames, valset):
self._checkargnotcontained(arg)
getattr(self, valtype)[arg] = val
self._idlist.append(id)
def setall(self, funcargs, id, param):
for x in funcargs:
self._checkargnotcontained(x)
self.funcargs.update(funcargs)
if id is not _notexists:
self._idlist.append(id)
if param is not _notexists:
self.param = param
def __repr__(self):
return "<CallSpec id=%r param=%r funcargs=%r>" %(
self.id, getattr(self, 'param', '?'), self.funcargs)
assert self._globalparam is _notexists
self._globalparam = param
class Metafunc:
def __init__(self, function, config=None, cls=None, module=None):
@@ -523,31 +586,69 @@ class Metafunc:
self._calls = []
self._ids = py.builtin.set()
def parametrize(self, argnames, argvalues, indirect=False, ids=None):
""" parametrize calls to the underlying test function during
the collection phase of a test run. parametrize may be called
multiple times for disjunct argnames sets.
:arg argnames: an argument name or a list of argument names
:arg argvalues: a list of values for a single argument if argnames
specified a single argument only or a list of tuples which specify
values for the multiple argument names.
:arg indirect: if True each argvalue corresponding to an argument will be
passed as request.param to the respective funcarg factory so that
it can perform more expensive setups during the setup phase of
a test rather than at collection time (which is the default).
:arg ids: list of string ids corresponding to the (list of) argvalues
so that they are part of the test id. If no ids are provided
they will be generated automatically from the argvalues.
"""
if not isinstance(argnames, (tuple, list)):
argnames = (argnames,)
argvalues = [(val,) for val in argvalues]
for arg in argnames:
if arg not in self.funcargnames:
raise ValueError("%r has no argument %r" %(self.function, arg))
valtype = indirect and "params" or "funcargs"
if not ids:
idmaker = IDMaker()
ids = list(map(idmaker, argvalues))
newcalls = []
for callspec in self._calls or [CallSpec2(self)]:
for i, valset in enumerate(argvalues):
assert len(valset) == len(argnames)
newcallspec = callspec.copy(self)
newcallspec.setmulti(valtype, argnames, valset, ids[i])
newcalls.append(newcallspec)
self._calls = newcalls
def addcall(self, funcargs=None, id=_notexists, param=_notexists):
""" add a new call to the underlying test function during the
collection phase of a test run. Note that request.addcall() is
""" (deprecated, use parametrize) add a new call to the underlying
test function during
the collection phase of a test run. Note that request.addcall() is
called during the test collection phase prior and independently
to actual test execution. Therefore you should perform setup
of resources in a funcarg factory which can be instrumented
with the ``param``.
to actual test execution. You should only use addcall()
if you need to specify multiple arguments of a test function
:arg funcargs: argument keyword dictionary used when invoking
the test function.
:arg id: used for reporting and identification purposes. If you
don't supply an `id` the length of the currently
list of calls to the test function will be used.
don't supply an `id` an automatic unique id will be generated.
:arg param: will be exposed to a later funcarg factory invocation
through the ``request.param`` attribute. It allows to
defer test fixture setup activities to when an actual
test is run.
:arg param: a parameter which will be exposed to a later funcarg factory
invocation through the ``request.param`` attribute.
"""
assert funcargs is None or isinstance(funcargs, dict)
if funcargs is not None:
for name in funcargs:
if name not in self.funcargnames:
pytest.fail("funcarg %r not used in this function." % name)
else:
funcargs = {}
if id is None:
raise ValueError("id=None not allowed")
if id is _notexists:
@@ -556,11 +657,26 @@ class Metafunc:
if id in self._ids:
raise ValueError("duplicate id %r" % id)
self._ids.add(id)
self._calls.append(CallSpec(funcargs, id, param))
cs = CallSpec2(self)
cs.setall(funcargs, id, param)
self._calls.append(cs)
class IDMaker:
def __init__(self):
self.counter = 0
def __call__(self, valset):
l = []
for val in valset:
if not isinstance(val, (int, str)):
val = "."+str(self.counter)
self.counter += 1
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``
@@ -693,11 +809,18 @@ class FuncargRequest:
self._raiselookupfailed(argname)
funcargfactory = self._name2factory[argname].pop()
oldarg = self._currentarg
self._currentarg = argname
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:
self._currentarg = oldarg
mp.undo()
return res
def _getscopeitem(self, scope):