allow metafunc.parametrize(scope=...) calls to override the scope of a Fixture function definition. This is useful for cases where you want to dynamically

set scope and parametrization for a fixture instead of statically declaring
it on the fixture function.
This commit is contained in:
holger krekel 2012-10-06 21:01:13 +02:00
parent 55a8bfd174
commit d3893dd5d1
2 changed files with 47 additions and 12 deletions

View File

@ -628,7 +628,7 @@ class Metafunc(FuncargnamesCompatAttr):
self._arg2scopenum = {} self._arg2scopenum = {}
def parametrize(self, argnames, argvalues, indirect=False, ids=None, def parametrize(self, argnames, argvalues, indirect=False, ids=None,
scope="function"): scope=None):
""" Add new invocations to the underlying test function using the list """ Add new invocations to the underlying test function using the list
of argvalues for the given argnames. Parametrization is performed of argvalues for the given argnames. Parametrization is performed
during the collection phase. If you need to setup expensive resources during the collection phase. If you need to setup expensive resources
@ -648,6 +648,11 @@ class Metafunc(FuncargnamesCompatAttr):
:arg ids: list of string ids each corresponding to the argvalues so :arg ids: list of string ids each corresponding to the argvalues so
that they are part of the test id. If no ids are provided they will that they are part of the test id. If no ids are provided they will
be generated automatically from the argvalues. be generated automatically from the argvalues.
:arg scope: if specified: denotes the scope of the parameters.
The scope is used for sorting tests by parameters. It will
also override any fixture-function defined scope, allowing
to set a dynamic scope from test context and configuration.
""" """
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = (argnames,) argnames = (argnames,)
@ -656,7 +661,7 @@ class Metafunc(FuncargnamesCompatAttr):
argvalues = [(_notexists,) * len(argnames)] argvalues = [(_notexists,) * len(argnames)]
if scope is None: if scope is None:
scope = "function" scope = "subfunction"
scopenum = scopes.index(scope) scopenum = scopes.index(scope)
if not indirect: if not indirect:
#XXX should we also check for the opposite case? #XXX should we also check for the opposite case?
@ -893,9 +898,9 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
setattr(self.markers, name, val) setattr(self.markers, name, val)
# contstruct a list of all neccessary fixtures for this test function # contstruct a list of all neccessary fixtures for this test function
if hasattr(self.markers, "usefixtures"): try:
usefixtures = list(self.markers.usefixtures.args) usefixtures = list(self.markers.usefixtures.args)
else: except AttributeError:
usefixtures = [] usefixtures = []
self.fixturenames = (self.session._fixturemanager.getdefaultfixtures() + self.fixturenames = (self.session._fixturemanager.getdefaultfixtures() +
usefixtures + self._getfuncargnames()) usefixtures + self._getfuncargnames())
@ -938,10 +943,7 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
fs, lineno = self._getfslineno() fs, lineno = self._getfslineno()
pytest.skip("got empty parameter set, function %s at %s:%d" %( pytest.skip("got empty parameter set, function %s at %s:%d" %(
self.function.__name__, fs, lineno)) self.function.__name__, fs, lineno))
super(Function, self).setup() super(Function, self).setup()
#if hasattr(self, "_request"):
# self._request._callsetup()
fillfixtures(self) fillfixtures(self)
def __eq__(self, other): def __eq__(self, other):
@ -1103,7 +1105,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
def _fillfixtures(self): def _fillfixtures(self):
item = self._pyfuncitem item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames) fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames: for argname in fixturenames:
if argname not in item.funcargs: if argname not in item.funcargs:
item.funcargs[argname] = self.getfuncargvalue(argname) item.funcargs[argname] = self.getfuncargvalue(argname)
@ -1146,7 +1147,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
return val return val
def getfuncargvalue(self, argname): def getfuncargvalue(self, argname):
""" Retrieve a function argument by name for this test """ Retrieve a fixture function argument by name for this test
function invocation. This allows one function argument factory function invocation. This allows one function argument factory
to call another function argument factory. If there are two to call another function argument factory. If there are two
funcarg factories for the same test function argument the first funcarg factories for the same test function argument the first
@ -1181,8 +1182,8 @@ class FixtureRequest(FuncargnamesCompatAttr):
if fixturedef.active: if fixturedef.active:
return fixturedef.cached_result return fixturedef.cached_result
# prepare request scope and param attributes before # prepare request _currentarg and param attributes before
# calling into factory # calling into fixture function
argname = fixturedef.argname argname = fixturedef.argname
node = self._pyfuncitem node = self._pyfuncitem
mp = monkeypatch() mp = monkeypatch()
@ -1193,7 +1194,18 @@ class FixtureRequest(FuncargnamesCompatAttr):
pass pass
else: else:
mp.setattr(self, 'param', param, raising=False) mp.setattr(self, 'param', param, raising=False)
# if a parametrize invocation set a scope it will override
# the static scope defined with the fixture function
scope = fixturedef.scope scope = fixturedef.scope
try:
paramscopenum = node.callspec._arg2scopenum[argname]
except (KeyError, AttributeError):
pass
else:
if paramscopenum != scopenum_subfunction:
scope = scopes[paramscopenum]
if scope is not None: if scope is not None:
__tracebackhide__ = True __tracebackhide__ = True
if scopemismatch(self.scope, scope): if scopemismatch(self.scope, scope):
@ -1251,7 +1263,8 @@ class ScopeMismatchError(Exception):
which has a lower scope (e.g. a Session one calls a function one) which has a lower scope (e.g. a Session one calls a function one)
""" """
scopes = "session module class function".split() scopes = "session module class function subfunction".split()
scopenum_subfunction = scopes.index("subfunction")
def scopemismatch(currentscope, newscope): def scopemismatch(currentscope, newscope):
return scopes.index(newscope) > scopes.index(currentscope) return scopes.index(newscope) > scopes.index(currentscope)

View File

@ -1370,6 +1370,28 @@ class TestMetafuncFunctional:
"*1 passed*" "*1 passed*"
]) ])
@pytest.mark.parametrize(("scope", "length"),
[("module", 2), ("function", 4)])
def test_parametrize_scope_overrides(self, testdir, scope, length):
testdir.makepyfile("""
import pytest
l = []
def pytest_generate_tests(metafunc):
if "arg" in metafunc.funcargnames:
metafunc.parametrize("arg", [1,2], indirect=True,
scope=%r)
def pytest_funcarg__arg(request):
l.append(request.param)
return request.param
def test_hello(arg):
assert arg in (1,2)
def test_world(arg):
assert arg in (1,2)
def test_checklength():
assert len(l) == %d
""" % (scope, length))
reprec = testdir.inline_run()
reprec.assertoutcome(passed=5)
def test_conftest_funcargs_only_available_in_subdir(testdir): def test_conftest_funcargs_only_available_in_subdir(testdir):
sub1 = testdir.mkpydir("sub1") sub1 = testdir.mkpydir("sub1")