Skip to content

Commit 96be57a

Browse files
Apply comments, rebase and a few improvements
1 parent 485c555 commit 96be57a

File tree

4 files changed

+198
-129
lines changed

4 files changed

+198
-129
lines changed

src/_pytest/fixtures.py

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
from _pytest.scope import _ScopeName
6464
from _pytest.scope import HIGH_SCOPES
6565
from _pytest.scope import Scope
66-
from _pytest.stash import StashKey
6766

6867

6968
if TYPE_CHECKING:
@@ -147,89 +146,6 @@ def get_scope_node(
147146
assert_never(scope)
148147

149148

150-
# Used for storing artificial fixturedefs for direct parametrization.
151-
name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
152-
153-
154-
def add_funcarg_pseudo_fixture_def(
155-
collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
156-
) -> None:
157-
import _pytest.python
158-
159-
# This function will transform all collected calls to functions
160-
# if they use direct funcargs (i.e. direct parametrization)
161-
# because we want later test execution to be able to rely on
162-
# an existing FixtureDef structure for all arguments.
163-
# XXX we can probably avoid this algorithm if we modify CallSpec2
164-
# to directly care for creating the fixturedefs within its methods.
165-
if not metafunc._calls[0].funcargs:
166-
# This function call does not have direct parametrization.
167-
return
168-
# Collect funcargs of all callspecs into a list of values.
169-
arg2params: Dict[str, List[object]] = {}
170-
arg2scope: Dict[str, Scope] = {}
171-
for callspec in metafunc._calls:
172-
for argname, argvalue in callspec.funcargs.items():
173-
assert argname not in callspec.params
174-
callspec.params[argname] = argvalue
175-
arg2params_list = arg2params.setdefault(argname, [])
176-
callspec.indices[argname] = len(arg2params_list)
177-
arg2params_list.append(argvalue)
178-
if argname not in arg2scope:
179-
scope = callspec._arg2scope.get(argname, Scope.Function)
180-
arg2scope[argname] = scope
181-
callspec.funcargs.clear()
182-
183-
# Register artificial FixtureDef's so that later at test execution
184-
# time we can rely on a proper FixtureDef to exist for fixture setup.
185-
arg2fixturedefs = metafunc._arg2fixturedefs
186-
for argname, valuelist in arg2params.items():
187-
# If we have a scope that is higher than function, we need
188-
# to make sure we only ever create an according fixturedef on
189-
# a per-scope basis. We thus store and cache the fixturedef on the
190-
# node related to the scope.
191-
scope = arg2scope[argname]
192-
node = None
193-
if scope is not Scope.Function:
194-
node = get_scope_node(collector, scope)
195-
if node is None:
196-
# If used class scope and there is no class, use module-level
197-
# collector (for now).
198-
if scope is Scope.Class:
199-
assert isinstance(collector, _pytest.python.Module)
200-
node = collector
201-
# If used package scope and there is no package, use session
202-
# (for now).
203-
elif scope is Scope.Package:
204-
node = collector.session
205-
else:
206-
assert False, f"Unhandled missing scope: {scope}"
207-
if node is None:
208-
name2pseudofixturedef = None
209-
else:
210-
default: Dict[str, FixtureDef[Any]] = {}
211-
name2pseudofixturedef = node.stash.setdefault(
212-
name2pseudofixturedef_key, default
213-
)
214-
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
215-
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
216-
else:
217-
fixturedef = FixtureDef(
218-
fixturemanager=fixturemanager,
219-
baseid="",
220-
argname=argname,
221-
func=get_direct_param_fixture_func,
222-
scope=arg2scope[argname],
223-
params=valuelist,
224-
unittest=False,
225-
ids=None,
226-
_ispytest=True,
227-
)
228-
arg2fixturedefs[argname] = [fixturedef]
229-
if name2pseudofixturedef is not None:
230-
name2pseudofixturedef[argname] = fixturedef
231-
232-
233149
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
234150
"""Return fixturemarker or None if it doesn't exist or raised
235151
exceptions."""
@@ -355,10 +271,6 @@ def reorder_items_atscope(
355271
return items_done
356272

357273

358-
def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
359-
return request.param
360-
361-
362274
@dataclasses.dataclass(frozen=True)
363275
class FuncFixtureInfo:
364276
"""Fixture-related information for a fixture-requesting item (e.g. test

src/_pytest/python.py

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pathlib import Path
1515
from typing import Any
1616
from typing import Callable
17+
from typing import cast
1718
from typing import Dict
1819
from typing import final
1920
from typing import Generator
@@ -59,7 +60,10 @@
5960
from _pytest.deprecated import check_ispytest
6061
from _pytest.deprecated import INSTANCE_COLLECTOR
6162
from _pytest.deprecated import NOSE_SUPPORT_METHOD
63+
from _pytest.fixtures import FixtureDef
64+
from _pytest.fixtures import FixtureRequest
6265
from _pytest.fixtures import FuncFixtureInfo
66+
from _pytest.fixtures import get_scope_node
6367
from _pytest.main import Session
6468
from _pytest.mark import MARK_GEN
6569
from _pytest.mark import ParameterSet
@@ -77,6 +81,7 @@
7781
from _pytest.pathlib import visit
7882
from _pytest.scope import _ScopeName
7983
from _pytest.scope import Scope
84+
from _pytest.stash import StashKey
8085
from _pytest.warning_types import PytestCollectionWarning
8186
from _pytest.warning_types import PytestReturnNotNoneWarning
8287
from _pytest.warning_types import PytestUnhandledCoroutineWarning
@@ -493,13 +498,8 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
493498
if not metafunc._calls:
494499
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
495500
else:
496-
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
497-
fm = self.session._fixturemanager
498-
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
499-
500-
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
501-
# with direct parametrization, so make sure we update what the
502-
# function really needs.
501+
# Dynamic direct parametrization may have shadowed some fixtures,
502+
# so make sure we update what the function really needs.
503503
fixtureinfo.prune_dependency_tree()
504504

505505
for callspec in metafunc._calls:
@@ -1116,11 +1116,8 @@ class CallSpec2:
11161116
and stored in item.callspec.
11171117
"""
11181118

1119-
# arg name -> arg value which will be passed to the parametrized test
1120-
# function (direct parameterization).
1121-
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
1122-
# arg name -> arg value which will be passed to a fixture of the same name
1123-
# (indirect parametrization).
1119+
# arg name -> arg value which will be passed to a fixture or pseudo-fixture
1120+
# of the same name. (indirect or direct parametrization respectively)
11241121
params: Dict[str, object] = dataclasses.field(default_factory=dict)
11251122
# arg name -> arg index.
11261123
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
@@ -1134,32 +1131,23 @@ class CallSpec2:
11341131
def setmulti(
11351132
self,
11361133
*,
1137-
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
11381134
argnames: Iterable[str],
11391135
valset: Iterable[object],
11401136
id: str,
11411137
marks: Iterable[Union[Mark, MarkDecorator]],
11421138
scope: Scope,
11431139
param_index: int,
11441140
) -> "CallSpec2":
1145-
funcargs = self.funcargs.copy()
11461141
params = self.params.copy()
11471142
indices = self.indices.copy()
11481143
arg2scope = self._arg2scope.copy()
11491144
for arg, val in zip(argnames, valset):
1150-
if arg in params or arg in funcargs:
1145+
if arg in params:
11511146
raise ValueError(f"duplicate {arg!r}")
1152-
valtype_for_arg = valtypes[arg]
1153-
if valtype_for_arg == "params":
1154-
params[arg] = val
1155-
elif valtype_for_arg == "funcargs":
1156-
funcargs[arg] = val
1157-
else:
1158-
assert_never(valtype_for_arg)
1147+
params[arg] = val
11591148
indices[arg] = param_index
11601149
arg2scope[arg] = scope
11611150
return CallSpec2(
1162-
funcargs=funcargs,
11631151
params=params,
11641152
indices=indices,
11651153
_arg2scope=arg2scope,
@@ -1178,6 +1166,14 @@ def id(self) -> str:
11781166
return "-".join(self._idlist)
11791167

11801168

1169+
def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
1170+
return request.param
1171+
1172+
1173+
# Used for storing artificial fixturedefs for direct parametrization.
1174+
name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]()
1175+
1176+
11811177
@final
11821178
class Metafunc:
11831179
"""Objects passed to the :hook:`pytest_generate_tests` hook.
@@ -1319,8 +1315,6 @@ def parametrize(
13191315

13201316
self._validate_if_using_arg_names(argnames, indirect)
13211317

1322-
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
1323-
13241318
# Use any already (possibly) generated ids with parametrize Marks.
13251319
if _param_mark and _param_mark._param_ids_from:
13261320
generated_ids = _param_mark._param_ids_from._param_ids_generated
@@ -1335,6 +1329,59 @@ def parametrize(
13351329
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
13361330
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
13371331

1332+
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1333+
# artificial FixtureDef's so that later at test execution time we can rely
1334+
# on a proper FixtureDef to exist for fixture setup.
1335+
arg2fixturedefs = self._arg2fixturedefs
1336+
node = None
1337+
# If we have a scope that is higher than function, we need
1338+
# to make sure we only ever create an according fixturedef on
1339+
# a per-scope basis. We thus store and cache the fixturedef on the
1340+
# node related to the scope.
1341+
if scope_ is not Scope.Function:
1342+
collector = cast(nodes.Node, self.definition.parent)
1343+
node = get_scope_node(collector, scope_)
1344+
if node is None:
1345+
# If used class scope and there is no class, use module-level
1346+
# collector (for now).
1347+
if scope_ is Scope.Class:
1348+
assert isinstance(collector, _pytest.python.Module)
1349+
node = collector
1350+
# If used package scope and there is no package, use session
1351+
# (for now).
1352+
elif scope_ is Scope.Package:
1353+
node = collector.session
1354+
else:
1355+
assert_never(scope_) # type: ignore[arg-type]
1356+
if node is None:
1357+
name2pseudofixturedef = None
1358+
else:
1359+
default: Dict[str, FixtureDef[Any]] = {}
1360+
name2pseudofixturedef = node.stash.setdefault(
1361+
name2pseudofixturedef_key, default
1362+
)
1363+
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
1364+
for argname in argnames:
1365+
if arg_values_types[argname] == "params":
1366+
continue
1367+
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
1368+
fixturedef = name2pseudofixturedef[argname]
1369+
else:
1370+
fixturedef = FixtureDef(
1371+
fixturemanager=self.definition.session._fixturemanager,
1372+
baseid="",
1373+
argname=argname,
1374+
func=get_direct_param_fixture_func,
1375+
scope=scope_,
1376+
params=None,
1377+
unittest=False,
1378+
ids=None,
1379+
_ispytest=True,
1380+
)
1381+
if name2pseudofixturedef is not None:
1382+
name2pseudofixturedef[argname] = fixturedef
1383+
arg2fixturedefs[argname] = [fixturedef]
1384+
13381385
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
13391386
# more than once) then we accumulate those calls generating the cartesian product
13401387
# of all calls.
@@ -1344,7 +1391,6 @@ def parametrize(
13441391
zip(ids, parametersets)
13451392
):
13461393
newcallspec = callspec.setmulti(
1347-
valtypes=arg_values_types,
13481394
argnames=argnames,
13491395
valset=param_set.values,
13501396
id=param_id,

testing/example_scripts/issue_519.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ def checked_order():
2222
assert order == [
2323
("issue_519.py", "fix1", "arg1v1"),
2424
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
25-
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
2625
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
26+
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
2727
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
2828
("issue_519.py", "fix1", "arg1v2"),
2929
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
30-
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
3130
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
31+
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
3232
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
3333
]
3434

0 commit comments

Comments
 (0)