4040from _pytest ._io import TerminalWriter
4141from _pytest ._io .saferepr import saferepr
4242from _pytest .compat import ascii_escaped
43- from _pytest .compat import assert_never
4443from _pytest .compat import get_default_arg_names
4544from _pytest .compat import get_real_func
4645from _pytest .compat import getimfunc
5958from _pytest .deprecated import check_ispytest
6059from _pytest .deprecated import INSTANCE_COLLECTOR
6160from _pytest .deprecated import NOSE_SUPPORT_METHOD
61+ from _pytest .fixtures import FixtureDef
62+ from _pytest .fixtures import FixtureRequest
6263from _pytest .fixtures import FuncFixtureInfo
64+ from _pytest .fixtures import get_scope_node
6365from _pytest .main import Session
6466from _pytest .mark import MARK_GEN
6567from _pytest .mark import ParameterSet
7779from _pytest .pathlib import visit
7880from _pytest .scope import _ScopeName
7981from _pytest .scope import Scope
82+ from _pytest .stash import StashKey
8083from _pytest .warning_types import PytestCollectionWarning
8184from _pytest .warning_types import PytestReturnNotNoneWarning
8285from _pytest .warning_types import PytestUnhandledCoroutineWarning
@@ -493,13 +496,11 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
493496 if not metafunc ._calls :
494497 yield Function .from_parent (self , name = name , fixtureinfo = fixtureinfo )
495498 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.
499+ # Direct parametrizations taking place in module/class-specific
500+ # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
501+ # we update what the function really needs a.k.a its fixture closure. Note that
502+ # direct parametrizations using `@pytest.mark.parametrize` have already been considered
503+ # into making the closure using `ignore_args` arg to `getfixtureclosure`.
503504 fixtureinfo .prune_dependency_tree ()
504505
505506 for callspec in metafunc ._calls :
@@ -1116,11 +1117,8 @@ class CallSpec2:
11161117 and stored in item.callspec.
11171118 """
11181119
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).
1120+ # arg name -> arg value which will be passed to a fixture or pseudo-fixture
1121+ # of the same name. (indirect or direct parametrization respectively)
11241122 params : Dict [str , object ] = dataclasses .field (default_factory = dict )
11251123 # arg name -> arg index.
11261124 indices : Dict [str , int ] = dataclasses .field (default_factory = dict )
@@ -1134,32 +1132,23 @@ class CallSpec2:
11341132 def setmulti (
11351133 self ,
11361134 * ,
1137- valtypes : Mapping [str , "Literal['params', 'funcargs']" ],
11381135 argnames : Iterable [str ],
11391136 valset : Iterable [object ],
11401137 id : str ,
11411138 marks : Iterable [Union [Mark , MarkDecorator ]],
11421139 scope : Scope ,
11431140 param_index : int ,
11441141 ) -> "CallSpec2" :
1145- funcargs = self .funcargs .copy ()
11461142 params = self .params .copy ()
11471143 indices = self .indices .copy ()
11481144 arg2scope = self ._arg2scope .copy ()
11491145 for arg , val in zip (argnames , valset ):
1150- if arg in params or arg in funcargs :
1146+ if arg in params :
11511147 raise ValueError (f"duplicate parametrization of { 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 )
1148+ params [arg ] = val
11591149 indices [arg ] = param_index
11601150 arg2scope [arg ] = scope
11611151 return CallSpec2 (
1162- funcargs = funcargs ,
11631152 params = params ,
11641153 indices = indices ,
11651154 _arg2scope = arg2scope ,
@@ -1178,6 +1167,14 @@ def id(self) -> str:
11781167 return "-" .join (self ._idlist )
11791168
11801169
1170+ def get_direct_param_fixture_func (request : FixtureRequest ) -> Any :
1171+ return request .param
1172+
1173+
1174+ # Used for storing pseudo fixturedefs for direct parametrization.
1175+ name2pseudofixturedef_key = StashKey [Dict [str , FixtureDef [Any ]]]()
1176+
1177+
11811178@final
11821179class Metafunc :
11831180 """Objects passed to the :hook:`pytest_generate_tests` hook.
@@ -1320,8 +1317,6 @@ def parametrize(
13201317
13211318 self ._validate_if_using_arg_names (argnames , indirect )
13221319
1323- arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
1324-
13251320 # Use any already (possibly) generated ids with parametrize Marks.
13261321 if _param_mark and _param_mark ._param_ids_from :
13271322 generated_ids = _param_mark ._param_ids_from ._param_ids_generated
@@ -1336,6 +1331,60 @@ def parametrize(
13361331 if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
13371332 object .__setattr__ (_param_mark ._param_ids_from , "_param_ids_generated" , ids )
13381333
1334+ # Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1335+ # artificial "pseudo" FixtureDef's so that later at test execution time we can
1336+ # rely on a proper FixtureDef to exist for fixture setup.
1337+ arg2fixturedefs = self ._arg2fixturedefs
1338+ node = None
1339+ # If we have a scope that is higher than function, we need
1340+ # to make sure we only ever create an according fixturedef on
1341+ # a per-scope basis. We thus store and cache the fixturedef on the
1342+ # node related to the scope.
1343+ if scope_ is not Scope .Function :
1344+ collector = self .definition .parent
1345+ assert collector is not None
1346+ node = get_scope_node (collector , scope_ )
1347+ if node is None :
1348+ # If used class scope and there is no class, use module-level
1349+ # collector (for now).
1350+ if scope_ is Scope .Class :
1351+ assert isinstance (collector , _pytest .python .Module )
1352+ node = collector
1353+ # If used package scope and there is no package, use session
1354+ # (for now).
1355+ elif scope_ is Scope .Package :
1356+ node = collector .session
1357+ else :
1358+ assert False , f"Unhandled missing scope: { scope } "
1359+ if node is None :
1360+ name2pseudofixturedef = None
1361+ else :
1362+ default : Dict [str , FixtureDef [Any ]] = {}
1363+ name2pseudofixturedef = node .stash .setdefault (
1364+ name2pseudofixturedef_key , default
1365+ )
1366+ arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
1367+ for argname in argnames :
1368+ if arg_values_types [argname ] == "params" :
1369+ continue
1370+ if name2pseudofixturedef is not None and argname in name2pseudofixturedef :
1371+ fixturedef = name2pseudofixturedef [argname ]
1372+ else :
1373+ fixturedef = FixtureDef (
1374+ fixturemanager = self .definition .session ._fixturemanager ,
1375+ baseid = "" ,
1376+ argname = argname ,
1377+ func = get_direct_param_fixture_func ,
1378+ scope = scope_ ,
1379+ params = None ,
1380+ unittest = False ,
1381+ ids = None ,
1382+ _ispytest = True ,
1383+ )
1384+ if name2pseudofixturedef is not None :
1385+ name2pseudofixturedef [argname ] = fixturedef
1386+ arg2fixturedefs [argname ] = [fixturedef ]
1387+
13391388 # Create the new calls: if we are parametrize() multiple times (by applying the decorator
13401389 # more than once) then we accumulate those calls generating the cartesian product
13411390 # of all calls.
@@ -1345,7 +1394,6 @@ def parametrize(
13451394 zip (ids , parametersets )
13461395 ):
13471396 newcallspec = callspec .setmulti (
1348- valtypes = arg_values_types ,
13491397 argnames = argnames ,
13501398 valset = param_set .values ,
13511399 id = param_id ,
0 commit comments