From 2fc7aede0b507b93d02fb1222fd5b2e3f82e3167 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 16:40:40 +0300 Subject: [PATCH 1/8] Request #714: Apply indirect=True on particular argnames --- _pytest/python.py | 22 ++++++++++++++++----- testing/python/metafunc.py | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 4623d3d04..41c5e5cf2 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -761,11 +761,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: @@ -844,7 +845,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. @@ -889,13 +892,22 @@ class Metafunc(FuncargnamesCompatAttr): if scope is None: scope = "function" scopenum = scopes.index(scope) + valtypes = {arg: "funcargs" for arg in argnames} if not indirect: #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" + else: + if not isinstance(indirect, (tuple, list)): + valtypes = {arg: "params" for arg in argnames} + else: + for arg in indirect: + if arg not in argnames: + raise ValueError("indirect: fixture %r doesn't exist" %( + arg)) + valtypes[arg] = "params" idfn = None if callable(ids): idfn = ids @@ -910,7 +922,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/testing/python/metafunc.py b/testing/python/metafunc.py index 7a6d1eebf..215e18b03 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -220,6 +220,46 @@ 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) + 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') + + 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') + + 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 == {} + + def test_parametrize_indirect_list_functional(self, testdir): + testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.parametrize('x, y', [('a', 'b')], indirect=['x']) + def pytest_funcarg__x(request): + return request.param * 3 + def pytest_funcarg__y(request): + return request.param * 2 + + 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*", + ]) + def test_addcalls_and_parametrize_indirect(self): def func(x, y): pass metafunc = self.Metafunc(func) From 984695359797c67f40ce86207aeca5bbba7ee030 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 17:28:27 +0300 Subject: [PATCH 2/8] Support for python 2.6 --- _pytest/python.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 41c5e5cf2..118eea406 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -892,7 +892,7 @@ class Metafunc(FuncargnamesCompatAttr): if scope is None: scope = "function" scopenum = scopes.index(scope) - valtypes = {arg: "funcargs" for arg in argnames} + valtypes = dict.fromkeys(argnames, "funcargs") if not indirect: #XXX should we also check for the opposite case? for arg in argnames: @@ -901,7 +901,7 @@ class Metafunc(FuncargnamesCompatAttr): self.function, arg)) else: if not isinstance(indirect, (tuple, list)): - valtypes = {arg: "params" for arg in argnames} + valtypes = dict.fromkeys(argnames, "params") else: for arg in indirect: if arg not in argnames: From 15b865f5022a944d3ec13036809bfb24dac3a7bd Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 20:30:23 +0300 Subject: [PATCH 3/8] Rewrite test using @pytest.fixture --- testing/python/metafunc.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 215e18b03..d696af7c5 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -243,13 +243,14 @@ class TestMetafunc: def test_parametrize_indirect_list_functional(self, testdir): testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.parametrize('x, y', [('a', 'b')], indirect=['x']) - def pytest_funcarg__x(request): + import pytest + @pytest.fixture(scope='function') + def x(request): return request.param * 3 - def pytest_funcarg__y(request): + @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 From 54a3d6210bf211f6a7add7b3f2a3f397d4a48ae3 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 20:39:31 +0300 Subject: [PATCH 4/8] Update CHANGELOG and AUTHORS --- AUTHORS | 1 + CHANGELOG | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS b/AUTHORS index f5cb30bb9..cf52e03a0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -25,6 +25,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 3bff6ee6f..ab27f7ee9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -101,6 +101,9 @@ - add ``file`` and ``line`` attributes to JUnit-XML output. +- fix issue714: add ability to apply indirect=True parameter on particular argnames. + + 2.7.3 (compared to 2.7.2) ----------------------------- From 63bac67fb5bb016eac1d1cb000f242086d6aaf23 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 23:06:24 +0300 Subject: [PATCH 5/8] Add test for bad value in argument 'indirect' --- testing/python/metafunc.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index d696af7c5..df07a96e2 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -261,6 +261,25 @@ class TestMetafunc: "*1 passed*", ]) + def test_parametrize_indirect_bad_arg(self, testdir): + 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', 'z']) + def test_simple(x,y): + assert len(x) == 3 + assert len(y) == 1 + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*indirect: fixture 'z' doesn't exist*", + ]) + def test_addcalls_and_parametrize_indirect(self): def func(x, y): pass metafunc = self.Metafunc(func) From db9809d6dc7a3f0d8a78825c84c19303c6b8bda6 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Sun, 2 Aug 2015 23:53:44 +0300 Subject: [PATCH 6/8] Update test for error in 'indirect' parameter --- testing/python/metafunc.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index df07a96e2..366f2c067 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -261,24 +261,11 @@ class TestMetafunc: "*1 passed*", ]) - def test_parametrize_indirect_bad_arg(self, testdir): - 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', 'z']) - def test_simple(x,y): - assert len(x) == 3 - assert len(y) == 1 - """) - result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*indirect: fixture 'z' doesn't exist*", - ]) + def test_parametrize_indirect_list_error(self, testdir): + def func(x, y): pass + metafunc = self.Metafunc(func) + pytest.raises(ValueError, lambda: + metafunc.parametrize('x, y', [('a', 'b')], indirect=['x', 'z'])) def test_addcalls_and_parametrize_indirect(self): def func(x, y): pass From 657ca97dbd6efbf6d732d0d56a77c84f679294d5 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Tue, 4 Aug 2015 00:02:03 +0300 Subject: [PATCH 7/8] Some refactorings after code review --- _pytest/python.py | 23 ++++++++++++----------- testing/python/metafunc.py | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 118eea406..622ed2a46 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -892,22 +892,23 @@ class Metafunc(FuncargnamesCompatAttr): if scope is None: scope = "function" scopenum = scopes.index(scope) - valtypes = dict.fromkeys(argnames, "funcargs") - 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)) - else: - if not isinstance(indirect, (tuple, list)): - valtypes = dict.fromkeys(argnames, "params") - else: - for arg in indirect: - if arg not in argnames: - raise ValueError("indirect: fixture %r doesn't exist" %( - arg)) - valtypes[arg] = "params" + 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 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 366f2c067..b4395c2c8 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,7 @@ 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) @@ -227,6 +229,7 @@ class TestMetafunc: 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) @@ -234,6 +237,7 @@ class TestMetafunc: 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) @@ -241,7 +245,15 @@ class TestMetafunc: 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') @@ -261,11 +273,12 @@ class TestMetafunc: "*1 passed*", ]) + @pytest.mark.issue714 def test_parametrize_indirect_list_error(self, testdir): def func(x, y): pass metafunc = self.Metafunc(func) - pytest.raises(ValueError, lambda: - metafunc.parametrize('x, y', [('a', 'b')], indirect=['x', 'z'])) + 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 From f7bacd169e1e84063c2a5ff38da2b087fc8111e5 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Tue, 4 Aug 2015 00:48:41 +0300 Subject: [PATCH 8/8] Update docs --- _pytest/python.py | 2 +- doc/en/example/parametrize.txt | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/_pytest/python.py b/_pytest/python.py index 622ed2a46..bf9be9f12 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -833,7 +833,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. diff --git a/doc/en/example/parametrize.txt b/doc/en/example/parametrize.txt index d897cf762..71c37699d 100644 --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -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 --------------------------------------------------------------