diff --git a/CHANGELOG b/CHANGELOG index 43e4cee77..e11c827c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -65,6 +65,10 @@ Unreleased - fix issue221 - handle importing of namespace-package with no __init__.py properly. +- refactor internal FixtureRequest handling to avoid monkeypatching. + One of the positive user-facing effects is that the "request" object + can now be used in closures. + Changes between 2.4.1 and 2.4.2 ----------------------------------- diff --git a/_pytest/python.py b/_pytest/python.py index d10100a99..d1287d92e 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -4,7 +4,6 @@ import inspect import sys import pytest from _pytest.mark import MarkDecorator -from _pytest.monkeypatch import monkeypatch from py._code.code import TerminalRepr import _pytest @@ -1083,14 +1082,12 @@ class FixtureRequest(FuncargnamesCompatAttr): self.fixturename = None #: Scope string, one of "function", "cls", "module", "session" self.scope = "function" - self.getparent = pyfuncitem.getparent self._funcargs = self._pyfuncitem.funcargs.copy() - self._fixtureinfo = fi = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fi.name2fixturedefs + fixtureinfo = pyfuncitem._fixtureinfo + self._arg2fixturedefs = fixtureinfo.name2fixturedefs self._arg2index = {} - self.fixturenames = self._fixtureinfo.names_closure + self.fixturenames = fixtureinfo.names_closure self._fixturemanager = pyfuncitem.session._fixturemanager - self._parentid = pyfuncitem.parent.nodeid self._fixturestack = [] @property @@ -1104,7 +1101,7 @@ class FixtureRequest(FuncargnamesCompatAttr): # we arrive here because of a getfuncargvalue(argname) usage which # was naturally not knowable at parsing/collection time fixturedefs = self._fixturemanager.getfixturedefs( - argname, self._parentid) + argname, self._pyfuncitem.parent.nodeid) self._arg2fixturedefs[argname] = fixturedefs # fixturedefs is immutable so we maintain a decreasing index index = self._arg2index.get(argname, 0) - 1 @@ -1270,29 +1267,21 @@ class FixtureRequest(FuncargnamesCompatAttr): return fixturedef.cached_result # set by fixturedef.execute() except AttributeError: pass - - # prepare request fixturename and param attributes before - # calling into fixture function + # prepare a subrequest object before calling fixture function + # (latter managed by fixturedef) argname = fixturedef.argname node = self._pyfuncitem - mp = monkeypatch() - mp.setattr(self, 'fixturename', argname) + scope = fixturedef.scope try: param = node.callspec.getparam(argname) except (AttributeError, ValueError): - pass + param = notset else: - 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 - try: - paramscopenum = node.callspec._arg2scopenum[argname] - except (KeyError, AttributeError): - pass - else: - if paramscopenum != scopenum_subfunction: + # if a parametrize invocation set a scope it will override + # the static scope defined with the fixture function + paramscopenum = node.callspec._arg2scopenum.get(argname) + if paramscopenum is not None and \ + paramscopenum != scopenum_subfunction: scope = scopes[paramscopenum] # check if a higher-level scoped fixture accesses a lower level one @@ -1302,31 +1291,29 @@ class FixtureRequest(FuncargnamesCompatAttr): # try to report something helpful lines = self._factorytraceback() raise ScopeMismatchError("You tried to access the %r scoped " - "funcarg %r with a %r scoped request object, " + "fixture %r with a %r scoped request object, " "involved factories\n%s" %( (scope, argname, self.scope, "\n".join(lines)))) __tracebackhide__ = False - mp.setattr(self, "scope", scope) - - # route request.addfinalizer to fixturedef - mp.setattr(self, "addfinalizer", fixturedef.addfinalizer) + else: + scope = self.scope + subrequest = SubRequest(self, argname, scope, param, + fixturedef.addfinalizer) try: # perform the fixture call - val = fixturedef.execute(request=self) + val = fixturedef.execute(request=subrequest) finally: # if the fixture function failed it might still have # registered finalizers so we can register - # prepare finalization according to scope # (XXX analyse exact finalizing mechanics / cleanup) self.session._setupstate.addfinalizer(fixturedef.finish, - self.node) + subrequest.node) self._fixturemanager.addargfinalizer(fixturedef.finish, argname) for subargname in fixturedef.argnames: # XXX all deps? self._fixturemanager.addargfinalizer(fixturedef.finish, subargname) - mp.undo() return val def _factorytraceback(self): @@ -1358,6 +1345,28 @@ class FixtureRequest(FuncargnamesCompatAttr): def __repr__(self): return "" %(self.node) +notset = object() +class SubRequest(FixtureRequest): + """ a sub request for handling getting a fixture from a + test function/fixture. """ + def __init__(self, request, argname, scope, param, addfinalizer): + self._parent_request = request + self.fixturename = argname + if param is not notset: + self.param = param + self.scope = scope + self.addfinalizer = addfinalizer + self._pyfuncitem = request._pyfuncitem + self._funcargs = request._funcargs + self._arg2fixturedefs = request._arg2fixturedefs + self._arg2index = request._arg2index + self.fixturenames = request.fixturenames + self._fixturemanager = request._fixturemanager + self._fixturestack = request._fixturestack + + def __repr__(self): + return "" % (self.fixturename, self.node) + class ScopeMismatchError(Exception): """ A fixture function tries to use a different fixture function which which has a lower scope (e.g. a Session one calls a function one) @@ -1399,8 +1408,8 @@ class FixtureLookupError(LookupError): fm = self.request._fixturemanager available = [] for name, fixturedef in fm._arg2fixturedefs.items(): - faclist = list(fm._matchfactories(fixturedef, - self.request._parentid)) + parentid = self.request._pyfuncitem.parent.nodeid + faclist = list(fm._matchfactories(fixturedef, parentid)) if faclist: available.append(name) msg = "fixture %r not found" % (self.argname,) @@ -1744,7 +1753,6 @@ class FixtureDef: func() # check neccesity of next commented call self._fixturemanager.removefinalizer(self.finish) - #print "finished", self try: del self.cached_result except AttributeError: diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 519db465a..baabc341e 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -473,7 +473,7 @@ class TestRequestBasic: assert l == ["module", "function", "class", "function", "method", "function"] """) - reprec = testdir.inline_run() + reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=3) def test_fixtures_sub_subdir_normalize_sep(self, testdir): @@ -1792,6 +1792,20 @@ class TestFixtureMarker: for test in ['test_a', 'test_b', 'test_c']: assert reprec.matchreport(test).passed + def test_request_is_clean(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.fixture(params=[1, 2]) + def fix(request): + request.addfinalizer(lambda: l.append(request.param)) + def test_fix(fix): + pass + """) + reprec = testdir.inline_run("-s") + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert l == [1,2] + def test_parametrize_separated_lifecycle(self, testdir): testdir.makepyfile(""" import pytest