Skip to content

Commit ae07985

Browse files
authored
Merge pull request #1711 from nicoddemus/invocation-scoped-fixtures
Invocation scoped fixtures
2 parents 832ada1 + 05f3422 commit ae07985

11 files changed

+475
-62
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,17 @@ time or change existing behaviors in order to make them less surprising/more use
135135
never fail because tuples are always truthy and are usually a mistake
136136
(see `#1562`_). Thanks `@kvas-it`_, for the PR.
137137

138+
* Experimentally introduce new ``"invocation"`` fixture scope. At invocation scope a
139+
fixture function is cached in the same way as the fixture or test function that requests it.
140+
You can now use the builtin ``monkeypatch`` fixture from ``session``-scoped fixtures
141+
where previously you would get an error that you can not use a ``function``-scoped fixture from a
142+
``session``-scoped one.*
143+
Thanks `@nicoddemus`_ for the PR.
144+
138145
* Allow passing a custom debugger class (e.g. ``--pdbcls=IPython.core.debugger:Pdb``).
139146
Thanks to `@anntzer`_ for the PR.
140147

148+
141149
*
142150

143151
*

_pytest/capture.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def capsys(request):
161161
captured output available via ``capsys.readouterr()`` method calls
162162
which return a ``(out, err)`` tuple.
163163
"""
164-
if "capfd" in request._funcargs:
164+
if "capfd" in request.fixturenames:
165165
raise request.raiseerror(error_capsysfderror)
166166
request.node._capfuncarg = c = CaptureFixture(SysCapture, request)
167167
return c
@@ -172,7 +172,7 @@ def capfd(request):
172172
captured output available via ``capfd.readouterr()`` method calls
173173
which return a ``(out, err)`` tuple.
174174
"""
175-
if "capsys" in request._funcargs:
175+
if "capsys" in request.fixturenames:
176176
request.raiseerror(error_capsysfderror)
177177
if not hasattr(os, 'dup'):
178178
pytest.skip("capfd funcarg needs os.dup")

_pytest/fixtures.py

Lines changed: 99 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,6 @@ def __init__(self, argnames, names_closure, name2fixturedefs):
260260
self.name2fixturedefs = name2fixturedefs
261261

262262

263-
264-
265263
class FixtureRequest(FuncargnamesCompatAttr):
266264
""" A request for a fixture from a test or fixture function.
267265
@@ -276,34 +274,51 @@ def __init__(self, pyfuncitem):
276274
self.fixturename = None
277275
#: Scope string, one of "function", "class", "module", "session"
278276
self.scope = "function"
279-
self._funcargs = {}
280-
self._fixturedefs = {}
277+
# rename both attributes below because their key has changed; better an attribute error
278+
# than subtle key misses; also backward incompatibility
279+
self._fixture_values = {} # (argname, scope) -> fixture value
280+
self._fixture_defs = {} # (argname, scope) -> FixtureDef
281281
fixtureinfo = pyfuncitem._fixtureinfo
282282
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
283283
self._arg2index = {}
284-
self.fixturenames = fixtureinfo.names_closure
285284
self._fixturemanager = pyfuncitem.session._fixturemanager
286285

286+
@property
287+
def fixturenames(self):
288+
# backward incompatible note: now a readonly property
289+
return list(self._pyfuncitem._fixtureinfo.names_closure)
290+
287291
@property
288292
def node(self):
289293
""" underlying collection node (depends on current request scope)"""
290294
return self._getscopeitem(self.scope)
291295

292296

293-
def _getnextfixturedef(self, argname):
294-
fixturedefs = self._arg2fixturedefs.get(argname, None)
297+
def _getnextfixturedef(self, argname, scope):
298+
def trygetfixturedefs(argname):
299+
fixturedefs = self._arg2fixturedefs.get(argname, None)
300+
if fixturedefs is None:
301+
fixturedefs = self._arg2fixturedefs.get(argname + ':' + scope, None)
302+
return fixturedefs
303+
304+
fixturedefs = trygetfixturedefs(argname)
295305
if fixturedefs is None:
296306
# we arrive here because of a a dynamic call to
297307
# getfixturevalue(argname) usage which was naturally
298308
# not known at parsing/collection time
299-
fixturedefs = self._fixturemanager.getfixturedefs(
300-
argname, self._pyfuncitem.parent.nodeid)
301-
self._arg2fixturedefs[argname] = fixturedefs
309+
parentid = self._pyfuncitem.parent.nodeid
310+
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
311+
if fixturedefs:
312+
self._arg2fixturedefs[argname] = fixturedefs
313+
fixturedefs_by_argname = self._fixturemanager.getfixturedefs_multiple_scopes(argname, parentid)
314+
if fixturedefs_by_argname:
315+
self._arg2fixturedefs.update(fixturedefs_by_argname)
316+
fixturedefs = trygetfixturedefs(argname)
302317
# fixturedefs list is immutable so we maintain a decreasing index
303-
index = self._arg2index.get(argname, 0) - 1
318+
index = self._arg2index.get((argname, scope), 0) - 1
304319
if fixturedefs is None or (-index > len(fixturedefs)):
305320
raise FixtureLookupError(argname, self)
306-
self._arg2index[argname] = index
321+
self._arg2index[(argname, scope)] = index
307322
return fixturedefs[index]
308323

309324
@property
@@ -442,10 +457,10 @@ def getfuncargvalue(self, argname):
442457

443458
def _get_active_fixturedef(self, argname):
444459
try:
445-
return self._fixturedefs[argname]
460+
return self._fixture_defs[(argname, self.scope)]
446461
except KeyError:
447462
try:
448-
fixturedef = self._getnextfixturedef(argname)
463+
fixturedef = self._getnextfixturedef(argname, self.scope)
449464
except FixtureLookupError:
450465
if argname == "request":
451466
class PseudoFixtureDef:
@@ -456,8 +471,8 @@ class PseudoFixtureDef:
456471
# remove indent to prevent the python3 exception
457472
# from leaking into the call
458473
result = self._getfixturevalue(fixturedef)
459-
self._funcargs[argname] = result
460-
self._fixturedefs[argname] = fixturedef
474+
self._fixture_values[(argname, self.scope)] = result
475+
self._fixture_defs[(argname, self.scope)] = fixturedef
461476
return fixturedef
462477

463478
def _get_fixturestack(self):
@@ -578,11 +593,10 @@ def __init__(self, request, scope, param, param_index, fixturedef):
578593
self._fixturedef = fixturedef
579594
self.addfinalizer = fixturedef.addfinalizer
580595
self._pyfuncitem = request._pyfuncitem
581-
self._funcargs = request._funcargs
582-
self._fixturedefs = request._fixturedefs
596+
self._fixture_values = request._fixture_values
597+
self._fixture_defs = request._fixture_defs
583598
self._arg2fixturedefs = request._arg2fixturedefs
584599
self._arg2index = request._arg2index
585-
self.fixturenames = request.fixturenames
586600
self._fixturemanager = request._fixturemanager
587601

588602
def __repr__(self):
@@ -622,7 +636,7 @@ def formatrepr(self):
622636
fspath, lineno = getfslineno(function)
623637
try:
624638
lines, _ = inspect.getsourcelines(get_real_func(function))
625-
except (IOError, IndexError):
639+
except (IOError, IndexError, TypeError):
626640
error_msg = "file %s, line %s: source code not available"
627641
addline(error_msg % (fspath, lineno+1))
628642
else:
@@ -636,9 +650,9 @@ def formatrepr(self):
636650
if msg is None:
637651
fm = self.request._fixturemanager
638652
available = []
639-
for name, fixturedef in fm._arg2fixturedefs.items():
640-
parentid = self.request._pyfuncitem.parent.nodeid
641-
faclist = list(fm._matchfactories(fixturedef, parentid))
653+
parentid = self.request._pyfuncitem.parent.nodeid
654+
for name, fixturedefs in fm._arg2fixturedefs.items():
655+
faclist = list(fm._matchfactories(fixturedefs, parentid))
642656
if faclist:
643657
available.append(name)
644658
msg = "fixture %r not found" % (self.argname,)
@@ -749,7 +763,7 @@ def execute(self, request):
749763
assert not hasattr(self, "cached_result")
750764

751765
ihook = self._fixturemanager.session.ihook
752-
ihook.pytest_fixture_setup(fixturedef=self, request=request)
766+
return ihook.pytest_fixture_setup(fixturedef=self, request=request)
753767

754768
def __repr__(self):
755769
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
@@ -984,10 +998,12 @@ def getfixtureclosure(self, fixturenames, parentnode):
984998

985999
parentid = parentnode.nodeid
9861000
fixturenames_closure = self._getautousenames(parentid)
1001+
9871002
def merge(otherlist):
9881003
for arg in otherlist:
9891004
if arg not in fixturenames_closure:
9901005
fixturenames_closure.append(arg)
1006+
9911007
merge(fixturenames)
9921008
arg2fixturedefs = {}
9931009
lastlen = -1
@@ -1000,6 +1016,11 @@ def merge(otherlist):
10001016
if fixturedefs:
10011017
arg2fixturedefs[argname] = fixturedefs
10021018
merge(fixturedefs[-1].argnames)
1019+
fixturedefs_by_argname = self.getfixturedefs_multiple_scopes(argname, parentid)
1020+
if fixturedefs_by_argname:
1021+
arg2fixturedefs.update(fixturedefs_by_argname)
1022+
for fixturedefs in fixturedefs_by_argname.values():
1023+
merge(fixturedefs[-1].argnames)
10031024
return fixturenames_closure, arg2fixturedefs
10041025

10051026
def pytest_generate_tests(self, metafunc):
@@ -1018,7 +1039,7 @@ def pytest_generate_tests(self, metafunc):
10181039
indirect=True, scope=fixturedef.scope,
10191040
ids=fixturedef.ids)
10201041
else:
1021-
continue # will raise FixtureLookupError at setup time
1042+
continue # will raise FixtureLookupError at setup time
10221043

10231044
def pytest_collection_modifyitems(self, items):
10241045
# separate parametrized setups
@@ -1057,25 +1078,43 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
10571078
msg = 'fixtures cannot have "pytest_funcarg__" prefix ' \
10581079
'and be decorated with @pytest.fixture:\n%s' % name
10591080
assert not name.startswith(self._argprefix), msg
1060-
fixturedef = FixtureDef(self, nodeid, name, obj,
1061-
marker.scope, marker.params,
1062-
unittest=unittest, ids=marker.ids)
1063-
faclist = self._arg2fixturedefs.setdefault(name, [])
1064-
if fixturedef.has_location:
1065-
faclist.append(fixturedef)
1081+
1082+
def new_fixture_def(name, scope):
1083+
"""Create and registers a new FixtureDef with given name and scope."""
1084+
fixture_def = FixtureDef(self, nodeid, name, obj,
1085+
scope, marker.params,
1086+
unittest=unittest, ids=marker.ids)
1087+
1088+
faclist = self._arg2fixturedefs.setdefault(name, [])
1089+
if fixture_def.has_location:
1090+
faclist.append(fixture_def)
1091+
else:
1092+
# fixturedefs with no location are at the front
1093+
# so this inserts the current fixturedef after the
1094+
# existing fixturedefs from external plugins but
1095+
# before the fixturedefs provided in conftests.
1096+
i = len([f for f in faclist if not f.has_location])
1097+
faclist.insert(i, fixture_def)
1098+
if marker.autouse:
1099+
autousenames.append(name)
1100+
1101+
if marker.scope == 'invocation':
1102+
for new_scope in scopes:
1103+
new_fixture_def(name + ':{0}'.format(new_scope), new_scope)
10661104
else:
1067-
# fixturedefs with no location are at the front
1068-
# so this inserts the current fixturedef after the
1069-
# existing fixturedefs from external plugins but
1070-
# before the fixturedefs provided in conftests.
1071-
i = len([f for f in faclist if not f.has_location])
1072-
faclist.insert(i, fixturedef)
1073-
if marker.autouse:
1074-
autousenames.append(name)
1105+
new_fixture_def(name, marker.scope)
1106+
10751107
if autousenames:
10761108
self._nodeid_and_autousenames.append((nodeid or '', autousenames))
10771109

10781110
def getfixturedefs(self, argname, nodeid):
1111+
"""
1112+
Gets a list of fixtures which are applicable to the given node id.
1113+
1114+
:param str argname: name of the fixture to search for
1115+
:param str nodeid: full node id of the requesting test.
1116+
:return: list[FixtureDef]
1117+
"""
10791118
try:
10801119
fixturedefs = self._arg2fixturedefs[argname]
10811120
except KeyError:
@@ -1087,3 +1126,24 @@ def _matchfactories(self, fixturedefs, nodeid):
10871126
for fixturedef in fixturedefs:
10881127
if nodeid.startswith(fixturedef.baseid):
10891128
yield fixturedef
1129+
1130+
def getfixturedefs_multiple_scopes(self, argname, nodeid):
1131+
"""
1132+
Gets multiple scoped fixtures which are applicable to the given nodeid. Multiple scoped
1133+
fixtures are created by "invocation" scoped fixtures and have argnames in
1134+
the form: "<argname>:<scope>" (for example "tmpdir:session").
1135+
1136+
:return: dict of "argname" -> [FixtureDef].
1137+
1138+
Arguments similar to ``getfixturedefs``.
1139+
"""
1140+
prefix = argname + ':'
1141+
fixturedefs_by_argname = dict((k, v) for k, v in self._arg2fixturedefs.items()
1142+
if k.startswith(prefix))
1143+
if fixturedefs_by_argname:
1144+
result = {}
1145+
for argname, fixturedefs in fixturedefs_by_argname.items():
1146+
result[argname] = tuple(self._matchfactories(fixturedefs, nodeid))
1147+
return result
1148+
else:
1149+
return None

_pytest/monkeypatch.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
1111

1212

13-
@pytest.fixture
13+
@pytest.fixture(scope='invocation')
1414
def monkeypatch(request):
1515
"""The returned ``monkeypatch`` fixture provides these
1616
helper methods to modify objects, dictionaries or os.environ::
@@ -25,9 +25,11 @@ def monkeypatch(request):
2525
monkeypatch.chdir(path)
2626
2727
All modifications will be undone after the requesting
28-
test function has finished. The ``raising``
28+
test function or fixture has finished. The ``raising``
2929
parameter determines if a KeyError or AttributeError
3030
will be raised if the set/deletion operation has no target.
31+
32+
This fixture is ``invocation``-scoped.
3133
"""
3234
mpatch = MonkeyPatch()
3335
request.addfinalizer(mpatch.undo)
@@ -97,7 +99,8 @@ def __repr__(self):
9799

98100

99101
class MonkeyPatch:
100-
""" Object keeping a record of setattr/item/env/syspath changes. """
102+
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
103+
"""
101104

102105
def __init__(self):
103106
self._setattr = []

_pytest/python.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1506,4 +1506,3 @@ def setup(self):
15061506
super(Function, self).setup()
15071507
fixtures.fillfixtures(self)
15081508

1509-

doc/en/fixture.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ first execute with one instance and then finalizers are called
603603
before the next fixture instance is created. Among other things,
604604
this eases testing of applications which create and use global state.
605605

606-
The following example uses two parametrized funcargs, one of which is
606+
The following example uses two parametrized fixture, one of which is
607607
scoped on a per-module basis, and all the functions perform ``print`` calls
608608
to show the setup/teardown flow::
609609

@@ -863,6 +863,14 @@ All test methods in this TestClass will use the transaction fixture while
863863
other test classes or functions in the module will not use it unless
864864
they also add a ``transact`` reference.
865865

866+
invocation-scoped fixtures
867+
--------------------------
868+
869+
pytest 3.0 introduced a new advanced scope for fixtures: ``"invocation"``. Fixtures marked with
870+
this scope can be requested from any other scope, providing a version of the fixture for that scope.
871+
872+
See more in :ref:`invocation_scoped_fixture`.
873+
866874
Shifting (visibility of) fixture functions
867875
----------------------------------------------------
868876

0 commit comments

Comments
 (0)