diff --git a/AUTHORS b/AUTHORS index b0073c54f..f291b2467 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,6 +26,7 @@ Daniel Nuri Dave Hunt David Mohr Eduardo Schettino +Elizaveta Shashkova Eric Siegerman Florian Bruhin Edison Gustavo Muenz diff --git a/CHANGELOG b/CHANGELOG index bc4c74aa3..206132441 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -123,6 +123,10 @@ - fix issue890: changed extension of all documentation files from ``txt`` to ``rst``. Thanks to Abhijeet for the PR. +- fix issue714: add ability to apply indirect=True parameter on particular argnames. + Thanks Elizaveta239. + + 2.7.3 (compared to 2.7.2) ----------------------------- diff --git a/_pytest/python.py b/_pytest/python.py index 2a47edca6..8057a6580 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -779,11 +779,12 @@ class CallSpec2(object): def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtype, argnames, valset, id, keywords, scopenum, + def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum, param_index): for arg,val in zip(argnames, valset): self._checkargnotcontained(arg) - getattr(self, valtype)[arg] = val + valtype_for_arg = valtypes[arg] + getattr(self, valtype_for_arg)[arg] = val self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum if val is _notexists: @@ -850,7 +851,7 @@ class Metafunc(FuncargnamesCompatAttr): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed during the collection phase. If you need to setup expensive resources - see about setting indirect=True to do it rather at test setup time. + see about setting indirect to do it rather at test setup time. :arg argnames: a comma-separated string denoting one or more argument names, or a list/tuple of argument strings. @@ -862,7 +863,9 @@ class Metafunc(FuncargnamesCompatAttr): where each tuple-element specifies a value for its respective argname. - :arg indirect: if True each argvalue corresponding to an argname will + :arg indirect: The list of argnames or boolean. A list of arguments' + names (subset of argnames). If True the list contains all names from + the argnames. Each argvalue corresponding to an argname in this list will be passed as request.param to its respective argname fixture function so that it can perform more expensive setups during the setup phase of a test rather than at collection time. @@ -907,13 +910,23 @@ class Metafunc(FuncargnamesCompatAttr): if scope is None: scope = "function" scopenum = scopes.index(scope) - if not indirect: + valtypes = {} + if indirect is True: + valtypes = dict.fromkeys(argnames, "params") + elif indirect is False: + valtypes = dict.fromkeys(argnames, "funcargs") #XXX should we also check for the opposite case? for arg in argnames: if arg not in self.fixturenames: raise ValueError("%r uses no fixture %r" %( self.function, arg)) - valtype = indirect and "params" or "funcargs" + elif isinstance(indirect, (tuple, list)): + valtypes = dict.fromkeys(argnames, "funcargs") + for arg in indirect: + if arg not in argnames: + raise ValueError("indirect given to %r: fixture %r doesn't exist" %( + self.function, arg)) + valtypes[arg] = "params" idfn = None if callable(ids): idfn = ids @@ -928,7 +941,7 @@ class Metafunc(FuncargnamesCompatAttr): for param_index, valset in enumerate(argvalues): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) - newcallspec.setmulti(valtype, argnames, valset, ids[param_index], + newcallspec.setmulti(valtypes, argnames, valset, ids[param_index], newkeywords.get(param_index, {}), scopenum, param_index) newcalls.append(newcallspec) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index f80643b5b..3d0c778f5 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -280,6 +280,46 @@ The first invocation with ``db == "DB1"`` passed while the second with ``db == " .. regendoc:wipe +Apply indirect on particular arguments +--------------------------------------------------- + +Very often parametrization uses more than one argument name. There is opportunity to apply ``indirect`` +parameter on particular arguments. It can be done by passing list or tuple of +arguments' names to ``indirect``. In the example below there is a function ``test_indirect`` which uses +two fixtures: ``x`` and ``y``. Here we give to indirect the list, which contains the name of the +fixture ``x``. The indirect parameter will be applied to this argument only, and the value ``a`` +will be passed to respective fixture function. + + # content of test_indirect_list.py + + import pytest + @pytest.fixture(scope='function') + def x(request): + return request.param * 3 + + @pytest.fixture(scope='function') + def y(request): + return request.param * 2 + + @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=['x']) + def test_indirect(x,y): + assert x == 'aaa' + assert y == 'b' + +The result of this test will be successful: + + $ py.test test_indirect_list.py --collect-only + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3, pytest-2.8.0.dev4, py-1.4.30, pluggy-0.3.0 + rootdir: /home/elizabeth/work/pytest, inifile: tox.ini + collected 1 items + + + + =============================== in 0.02 seconds =============================== + +.. regendoc:wipe + Parametrizing test methods through per-class configuration -------------------------------------------------------------- diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 310f7e2f8..c9f5523ab 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -208,6 +208,7 @@ class TestMetafunc: assert metafunc._calls[0].id == "0-2" assert metafunc._calls[1].id == "0-3" + @pytest.mark.issue714 def test_parametrize_indirect(self): def func(x, y): pass metafunc = self.Metafunc(func) @@ -220,6 +221,65 @@ class TestMetafunc: assert metafunc._calls[0].params == dict(x=1,y=2, unnamed=1) assert metafunc._calls[1].params == dict(x=1,y=3, unnamed=1) + @pytest.mark.issue714 + def test_parametrize_indirect_list(self): + def func(x, y): pass + metafunc = self.Metafunc(func) + metafunc.parametrize('x, y', [('a', 'b')], indirect=['x']) + assert metafunc._calls[0].funcargs == dict(y='b') + assert metafunc._calls[0].params == dict(x='a') + + @pytest.mark.issue714 + def test_parametrize_indirect_list_all(self): + def func(x, y): pass + metafunc = self.Metafunc(func) + metafunc.parametrize('x, y', [('a', 'b')], indirect=['x', 'y']) + assert metafunc._calls[0].funcargs == {} + assert metafunc._calls[0].params == dict(x='a', y='b') + + @pytest.mark.issue714 + def test_parametrize_indirect_list_empty(self): + def func(x, y): pass + metafunc = self.Metafunc(func) + metafunc.parametrize('x, y', [('a', 'b')], indirect=[]) + assert metafunc._calls[0].funcargs == dict(x='a', y='b') + assert metafunc._calls[0].params == {} + + @pytest.mark.issue714 + def test_parametrize_indirect_list_functional(self, testdir): + """ + Test parametrization with 'indirect' parameter applied on + particular arguments. + + :param testdir: the instance of Testdir class, a temporary + test directory. + """ + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope='function') + def x(request): + return request.param * 3 + @pytest.fixture(scope='function') + def y(request): + return request.param * 2 + @pytest.mark.parametrize('x, y', [('a', 'b')], indirect=['x']) + def test_simple(x,y): + assert len(x) == 3 + assert len(y) == 1 + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*test_simple*a-b*", + "*1 passed*", + ]) + + @pytest.mark.issue714 + def test_parametrize_indirect_list_error(self, testdir): + def func(x, y): pass + metafunc = self.Metafunc(func) + with pytest.raises(ValueError): + metafunc.parametrize('x, y', [('a', 'b')], indirect=['x', 'z']) + def test_addcalls_and_parametrize_indirect(self): def func(x, y): pass metafunc = self.Metafunc(func)