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