1414from pathlib import Path
1515from typing import Any
1616from typing import Callable
17+ from typing import cast
1718from typing import Dict
1819from typing import final
1920from typing import Generator
5960from _pytest .deprecated import check_ispytest
6061from _pytest .deprecated import INSTANCE_COLLECTOR
6162from _pytest .deprecated import NOSE_SUPPORT_METHOD
63+ from _pytest .fixtures import FixtureDef
64+ from _pytest .fixtures import FixtureRequest
6265from _pytest .fixtures import FuncFixtureInfo
66+ from _pytest .fixtures import get_scope_node
6367from _pytest .main import Session
6468from _pytest .mark import MARK_GEN
6569from _pytest .mark import ParameterSet
7781from _pytest .pathlib import visit
7882from _pytest .scope import _ScopeName
7983from _pytest .scope import Scope
84+ from _pytest .stash import StashKey
8085from _pytest .warning_types import PytestCollectionWarning
8186from _pytest .warning_types import PytestReturnNotNoneWarning
8287from _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
11821178class 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 ,
0 commit comments