diff --git a/_pytest/python.py b/_pytest/python.py index 5f9da87f3..62cf3d936 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -886,6 +886,7 @@ class FuncargRequest: self._currentarg = None self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid + self.scope = "function" def _getfaclist(self, argname): faclist = self._name2factory.get(argname, None) @@ -982,6 +983,9 @@ class FuncargRequest: try: val = cache[cachekey] except KeyError: + __tracebackhide__ = True + check_scope(self.scope, scope) + __tracebackhide__ = False val = setup() cache[cachekey] = val if teardown is not None: @@ -1007,7 +1011,6 @@ class FuncargRequest: factorylist = self._getfaclist(argname) funcargfactory = factorylist.pop() node = self._pyfuncitem - oldarg = self._currentarg mp = monkeypatch() mp.setattr(self, '_currentarg', argname) try: @@ -1016,11 +1019,24 @@ class FuncargRequest: pass else: mp.setattr(self, 'param', param, raising=False) - try: - self._funcargs[argname] = val = funcargfactory(request=self) - return val - finally: - mp.undo() + + # implemenet funcarg marker scope + marker = getattr(funcargfactory, "funcarg", None) + scope = None + if marker is not None: + scope = marker.kwargs.get("scope") + if scope is not None: + __tracebackhide__ = True + check_scope(self.scope, scope) + __tracebackhide__ = False + mp.setattr(self, "scope", scope) + val = self.cached_setup(lambda: funcargfactory(request=self), + scope=scope) + else: + val = funcargfactory(request=self) + mp.undo() + self._funcargs[argname] = val + return val def _getscopeitem(self, scope): if scope == "function": @@ -1039,7 +1055,7 @@ class FuncargRequest: def addfinalizer(self, finalizer): """add finalizer function to be called after test function finished execution. """ - self._addfinalizer(finalizer, scope="function") + self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) @@ -1049,3 +1065,15 @@ class FuncargRequest: def __repr__(self): return "" %(self._pyfuncitem) +class ScopeMismatchError(Exception): + """ A funcarg factory tries to access a funcargvalue/factory + which has a lower scope (e.g. a Session one calls a function one) + """ +scopes = "session module class function".split() +def check_scope(currentscope, newscope): + __tracebackhide__ = True + i_currentscope = scopes.index(currentscope) + i_newscope = scopes.index(newscope) + if i_newscope > i_currentscope: + raise ScopeMismatchError("You tried to access a %r scoped funcarg " + "from a %r scoped one." % (newscope, currentscope)) diff --git a/testing/test_python.py b/testing/test_python.py index f3316f31e..03c305ae1 100644 --- a/testing/test_python.py +++ b/testing/test_python.py @@ -58,7 +58,7 @@ class TestClass: ]) def test_setup_teardown_class_as_classmethod(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile(test_mod1=""" class TestClassMethod: @classmethod def setup_class(cls): @@ -1698,3 +1698,110 @@ class TestFuncargMarker: reprec = testdir.inline_run() reprec.assertoutcome(passed=4) + def test_scope_session(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + l.append(1) + return 1 + + def test_1(arg): + assert arg == 1 + def test_2(arg): + assert arg == 1 + assert len(l) == 1 + class TestClass: + def test3(self, arg): + assert arg == 1 + assert len(l) == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=3) + + def test_scope_module_uses_session(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + l.append(1) + return 1 + + def test_1(arg): + assert arg == 1 + def test_2(arg): + assert arg == 1 + assert len(l) == 1 + class TestClass: + def test3(self, arg): + assert arg == 1 + assert len(l) == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=3) + + def test_scope_module_and_finalizer(self, testdir): + testdir.makeconftest(""" + import pytest + finalized = [] + created = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + created.append(1) + assert request.scope == "module" + request.addfinalizer(lambda: finalized.append(1)) + def pytest_funcarg__created(request): + return len(created) + def pytest_funcarg__finalized(request): + return len(finalized) + """) + testdir.makepyfile( + test_mod1=""" + def test_1(arg, created, finalized): + assert created == 1 + assert finalized == 0 + def test_2(arg, created, finalized): + assert created == 1 + assert finalized == 0""", + test_mod2=""" + def test_3(arg, created, finalized): + assert created == 2 + assert finalized == 1""", + test_mode3=""" + def test_4(arg, created, finalized): + assert created == 3 + assert finalized == 2 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=4) + + @pytest.mark.parametrize("method", [ + 'request.getfuncargvalue("arg")', + 'request.cached_setup(lambda: None, scope="function")', + ], ids=["getfuncargvalue", "cached_setup"]) + def test_scope_mismatch(self, testdir, method): + testdir.makeconftest(""" + import pytest + finalized = [] + created = [] + @pytest.mark.funcarg(scope="function") + def pytest_funcarg__arg(request): + pass + """) + testdir.makepyfile( + test_mod1=""" + import pytest + @pytest.mark.funcarg(scope="session") + def pytest_funcarg__arg(request): + %s + def test_1(arg): + pass + """ % method) + result = testdir.runpytest() + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*ScopeMismatch*You tried*function*from*session*", + ]) +