Skip to content

Commit 7751008

Browse files
committed
Implement invocation-scoped fixtures
1 parent 29289b4 commit 7751008

File tree

2 files changed

+82
-39
lines changed

2 files changed

+82
-39
lines changed

_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/python.py

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,34 +1838,51 @@ def __init__(self, pyfuncitem):
18381838
self.fixturename = None
18391839
#: Scope string, one of "function", "class", "module", "session"
18401840
self.scope = "function"
1841-
self._funcargs = {}
1842-
self._fixturedefs = {}
1841+
# rename both attributes below because their key has changed; better an attribute error
1842+
# than subtle key misses; also backward incompatibility
1843+
self._fixture_values = {} # (argname, scope) -> fixture value
1844+
self._fixture_defs = {} # (argname, scope) -> FixtureDef
18431845
fixtureinfo = pyfuncitem._fixtureinfo
18441846
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
18451847
self._arg2index = {}
1846-
self.fixturenames = fixtureinfo.names_closure
18471848
self._fixturemanager = pyfuncitem.session._fixturemanager
18481849

1850+
@property
1851+
def fixturenames(self):
1852+
# backward incompatible note: now a readonly property
1853+
return list(self._pyfuncitem._fixtureinfo.names_closure)
1854+
18491855
@property
18501856
def node(self):
18511857
""" underlying collection node (depends on current request scope)"""
18521858
return self._getscopeitem(self.scope)
18531859

18541860

1855-
def _getnextfixturedef(self, argname):
1856-
fixturedefs = self._arg2fixturedefs.get(argname, None)
1861+
def _getnextfixturedef(self, argname, scope):
1862+
def trygetfixturedefs(argname):
1863+
fixturedefs = self._arg2fixturedefs.get(argname, None)
1864+
if fixturedefs is None:
1865+
fixturedefs = self._arg2fixturedefs.get(argname + ':' + scope, None)
1866+
return fixturedefs
1867+
1868+
fixturedefs = trygetfixturedefs(argname)
18571869
if fixturedefs is None:
18581870
# we arrive here because of a a dynamic call to
18591871
# getfixturevalue(argname) usage which was naturally
18601872
# not known at parsing/collection time
1861-
fixturedefs = self._fixturemanager.getfixturedefs(
1862-
argname, self._pyfuncitem.parent.nodeid)
1863-
self._arg2fixturedefs[argname] = fixturedefs
1873+
parentid = self._pyfuncitem.parent.nodeid
1874+
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
1875+
if fixturedefs:
1876+
self._arg2fixturedefs[argname] = fixturedefs
1877+
fixturedefs_by_argname = self._fixturemanager.getfixturedefs_multiple_scopes(argname, parentid)
1878+
if fixturedefs_by_argname:
1879+
self._arg2fixturedefs.update(fixturedefs_by_argname)
1880+
fixturedefs = trygetfixturedefs(argname)
18641881
# fixturedefs list is immutable so we maintain a decreasing index
1865-
index = self._arg2index.get(argname, 0) - 1
1882+
index = self._arg2index.get((argname, scope), 0) - 1
18661883
if fixturedefs is None or (-index > len(fixturedefs)):
18671884
raise FixtureLookupError(argname, self)
1868-
self._arg2index[argname] = index
1885+
self._arg2index[(argname, scope)] = index
18691886
return fixturedefs[index]
18701887

18711888
@property
@@ -2004,10 +2021,10 @@ def getfuncargvalue(self, argname):
20042021

20052022
def _get_active_fixturedef(self, argname):
20062023
try:
2007-
return self._fixturedefs[argname]
2024+
return self._fixture_defs[(argname, self.scope)]
20082025
except KeyError:
20092026
try:
2010-
fixturedef = self._getnextfixturedef(argname)
2027+
fixturedef = self._getnextfixturedef(argname, self.scope)
20112028
except FixtureLookupError:
20122029
if argname == "request":
20132030
class PseudoFixtureDef:
@@ -2018,8 +2035,8 @@ class PseudoFixtureDef:
20182035
# remove indent to prevent the python3 exception
20192036
# from leaking into the call
20202037
result = self._getfixturevalue(fixturedef)
2021-
self._funcargs[argname] = result
2022-
self._fixturedefs[argname] = fixturedef
2038+
self._fixture_values[(argname, self.scope)] = result
2039+
self._fixture_defs[(argname, self.scope)] = fixturedef
20232040
return fixturedef
20242041

20252042
def _get_fixturestack(self):
@@ -2140,11 +2157,10 @@ def __init__(self, request, scope, param, param_index, fixturedef):
21402157
self._fixturedef = fixturedef
21412158
self.addfinalizer = fixturedef.addfinalizer
21422159
self._pyfuncitem = request._pyfuncitem
2143-
self._funcargs = request._funcargs
2144-
self._fixturedefs = request._fixturedefs
2160+
self._fixture_values = request._fixture_values
2161+
self._fixture_defs = request._fixture_defs
21452162
self._arg2fixturedefs = request._arg2fixturedefs
21462163
self._arg2index = request._arg2index
2147-
self.fixturenames = request.fixturenames
21482164
self._fixturemanager = request._fixturemanager
21492165

21502166
def __repr__(self):
@@ -2184,7 +2200,7 @@ def formatrepr(self):
21842200
fspath, lineno = getfslineno(function)
21852201
try:
21862202
lines, _ = inspect.getsourcelines(get_real_func(function))
2187-
except (IOError, IndexError):
2203+
except (IOError, IndexError, TypeError):
21882204
error_msg = "file %s, line %s: source code not available"
21892205
addline(error_msg % (fspath, lineno+1))
21902206
else:
@@ -2198,9 +2214,9 @@ def formatrepr(self):
21982214
if msg is None:
21992215
fm = self.request._fixturemanager
22002216
available = []
2201-
for name, fixturedef in fm._arg2fixturedefs.items():
2202-
parentid = self.request._pyfuncitem.parent.nodeid
2203-
faclist = list(fm._matchfactories(fixturedef, parentid))
2217+
parentid = self.request._pyfuncitem.parent.nodeid
2218+
for name, fixturedefs in fm._arg2fixturedefs.items():
2219+
faclist = list(fm._matchfactories(fixturedefs, parentid))
22042220
if faclist:
22052221
available.append(name)
22062222
msg = "fixture %r not found" % (self.argname,)
@@ -2348,6 +2364,11 @@ def merge(otherlist):
23482364
if fixturedefs:
23492365
arg2fixturedefs[argname] = fixturedefs
23502366
merge(fixturedefs[-1].argnames)
2367+
fixturedefs_by_argname = self.getfixturedefs_multiple_scopes(argname, parentid)
2368+
if fixturedefs_by_argname:
2369+
arg2fixturedefs.update(fixturedefs_by_argname)
2370+
for fixturedefs in fixturedefs_by_argname.values():
2371+
merge(fixturedefs[-1].argnames)
23512372
return fixturenames_closure, arg2fixturedefs
23522373

23532374
def pytest_generate_tests(self, metafunc):
@@ -2366,7 +2387,7 @@ def pytest_generate_tests(self, metafunc):
23662387
indirect=True, scope=fixturedef.scope,
23672388
ids=fixturedef.ids)
23682389
else:
2369-
continue # will raise FixtureLookupError at setup time
2390+
continue # will raise FixtureLookupError at setup time
23702391

23712392
def pytest_collection_modifyitems(self, items):
23722393
# separate parametrized setups
@@ -2402,21 +2423,31 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
24022423
if marker.name:
24032424
name = marker.name
24042425
assert not name.startswith(self._argprefix), name
2405-
fixturedef = FixtureDef(self, nodeid, name, obj,
2406-
marker.scope, marker.params,
2407-
unittest=unittest, ids=marker.ids)
2408-
faclist = self._arg2fixturedefs.setdefault(name, [])
2409-
if fixturedef.has_location:
2410-
faclist.append(fixturedef)
2426+
2427+
def new_fixture_def(name, scope):
2428+
fixture_def = FixtureDef(self, nodeid, name, obj,
2429+
scope, marker.params,
2430+
unittest=unittest, ids=marker.ids)
2431+
2432+
faclist = self._arg2fixturedefs.setdefault(name, [])
2433+
if fixture_def.has_location:
2434+
faclist.append(fixture_def)
2435+
else:
2436+
# fixturedefs with no location are at the front
2437+
# so this inserts the current fixturedef after the
2438+
# existing fixturedefs from external plugins but
2439+
# before the fixturedefs provided in conftests.
2440+
i = len([f for f in faclist if not f.has_location])
2441+
faclist.insert(i, fixture_def)
2442+
if marker.autouse:
2443+
autousenames.append(name)
2444+
2445+
if marker.scope == 'invocation':
2446+
for new_scope in scopes:
2447+
new_fixture_def(name + ':{0}'.format(new_scope), new_scope)
24112448
else:
2412-
# fixturedefs with no location are at the front
2413-
# so this inserts the current fixturedef after the
2414-
# existing fixturedefs from external plugins but
2415-
# before the fixturedefs provided in conftests.
2416-
i = len([f for f in faclist if not f.has_location])
2417-
faclist.insert(i, fixturedef)
2418-
if marker.autouse:
2419-
autousenames.append(name)
2449+
new_fixture_def(name, marker.scope)
2450+
24202451
if autousenames:
24212452
self._nodeid_and_autousenames.append((nodeid or '', autousenames))
24222453

@@ -2433,6 +2464,18 @@ def _matchfactories(self, fixturedefs, nodeid):
24332464
if nodeid.startswith(fixturedef.baseid):
24342465
yield fixturedef
24352466

2467+
def getfixturedefs_multiple_scopes(self, argname, nodeid):
2468+
prefix = argname + ':'
2469+
fixturedefs_by_argname = dict((k, v) for k, v in self._arg2fixturedefs.items()
2470+
if k.startswith(prefix))
2471+
if fixturedefs_by_argname:
2472+
result = {}
2473+
for argname, fixturedefs in fixturedefs_by_argname.items():
2474+
result[argname] = tuple(self._matchfactories(fixturedefs, nodeid))
2475+
return result
2476+
else:
2477+
return None
2478+
24362479

24372480
def fail_fixturefunc(fixturefunc, msg):
24382481
fs, lineno = getfslineno(fixturefunc)
@@ -2518,7 +2561,7 @@ def execute(self, request):
25182561
assert not hasattr(self, "cached_result")
25192562

25202563
ihook = self._fixturemanager.session.ihook
2521-
ihook.pytest_fixture_setup(fixturedef=self, request=request)
2564+
return ihook.pytest_fixture_setup(fixturedef=self, request=request)
25222565

25232566
def __repr__(self):
25242567
return ("<FixtureDef name=%r scope=%r baseid=%r >" %

0 commit comments

Comments
 (0)