1515from typing import Dict
1616from typing import Generator
1717from typing import Generic
18+ from typing import Hashable
1819from typing import Iterable
1920from typing import Iterator
2021from typing import List
@@ -146,78 +147,58 @@ def get_scope_node(
146147 assert_never (scope )
147148
148149
149- # Used for storing artificial fixturedefs for direct parametrization.
150- name2pseudofixturedef_key = StashKey [Dict [str , "FixtureDef[Any]" ]]()
150+ def resolve_unique_values_and_their_indices_in_parametersets (
151+ argnames : Sequence [str ],
152+ parametersets : Sequence [ParameterSet ],
153+ ) -> Tuple [Dict [str , List [object ]], List [Tuple [int ]]]:
154+ """Resolve unique values and their indices in parameter sets. The index of a value
155+ is determined by when it appears in the possible values for the first time.
156+ For example, given ``argnames`` and ``parametersets`` below, the result would be:
157+
158+ ::
159+
160+ argnames = ["A", "B", "C"]
161+ parametersets = [("a1", "b1", "c1"), ("a1", "b2", "c1"), ("a1", "b3", "c2")]
162+ result[0] = {"A": ["a1"], "B": ["b1", "b2", "b3"], "C": ["c1", "c2"]}
163+ result[1] = [(0, 0, 0), (0, 1, 0), (0, 2, 1)]
164+
165+ result is used in reordering `indirect`ly parametrized with multiple
166+ parameters or directly parametrized tests to keep items using the same fixture or
167+ pseudo-fixture values respectively, close together.
168+
169+ :param argnames:
170+ Argument names passed to ``parametrize()``.
171+ :param parametersets:
172+ The parameter sets, each containing a set of values corresponding
173+ to ``argnames``.
174+ :returns:
175+ Tuple of unique parameter values and their indices in parametersets.
176+ """
177+ indices = []
178+ argname_value_indices_for_hashable_ones : Dict [str , Dict [object , int ]] = defaultdict (dict )
179+ argvalues_count : Dict [str , int ] = defaultdict (lambda : 0 )
180+ unique_values : Dict [str , List [object ]] = defaultdict (list )
181+ for i , argname in enumerate (argnames ):
182+ argname_indices = []
183+ for parameterset in parametersets :
184+ value = parameterset .values [i ]
185+ try :
186+ argname_indices .append (argname_value_indices_for_hashable_ones [argname ][value ])
187+ except KeyError : # New unique value
188+ argname_value_indices_for_hashable_ones [argname ][value ] = argvalues_count [argname ]
189+ argname_indices .append (argvalues_count [argname ])
190+ argvalues_count [argname ] += 1
191+ unique_values [argname ].append (value )
192+ except TypeError : # `value` is not hashable
193+ argname_indices .append (argvalues_count [argname ])
194+ argvalues_count [argname ] += 1
195+ unique_values [argname ].append (value )
196+ indices .append (argname_indices )
197+ return unique_values , list (zip (* indices ))
151198
152199
153- def add_funcarg_pseudo_fixture_def (
154- collector : nodes .Collector , metafunc : "Metafunc" , fixturemanager : "FixtureManager"
155- ) -> None :
156- # This function will transform all collected calls to functions
157- # if they use direct funcargs (i.e. direct parametrization)
158- # because we want later test execution to be able to rely on
159- # an existing FixtureDef structure for all arguments.
160- # XXX we can probably avoid this algorithm if we modify CallSpec2
161- # to directly care for creating the fixturedefs within its methods.
162- if not metafunc ._calls [0 ].funcargs :
163- # This function call does not have direct parametrization.
164- return
165- # Collect funcargs of all callspecs into a list of values.
166- arg2params : Dict [str , List [object ]] = {}
167- arg2scope : Dict [str , Scope ] = {}
168- for callspec in metafunc ._calls :
169- for argname , argvalue in callspec .funcargs .items ():
170- assert argname not in callspec .params
171- callspec .params [argname ] = argvalue
172- arg2params_list = arg2params .setdefault (argname , [])
173- callspec .indices [argname ] = len (arg2params_list )
174- arg2params_list .append (argvalue )
175- if argname not in arg2scope :
176- scope = callspec ._arg2scope .get (argname , Scope .Function )
177- arg2scope [argname ] = scope
178- callspec .funcargs .clear ()
179-
180- # Register artificial FixtureDef's so that later at test execution
181- # time we can rely on a proper FixtureDef to exist for fixture setup.
182- arg2fixturedefs = metafunc ._arg2fixturedefs
183- for argname , valuelist in arg2params .items ():
184- # If we have a scope that is higher than function, we need
185- # to make sure we only ever create an according fixturedef on
186- # a per-scope basis. We thus store and cache the fixturedef on the
187- # node related to the scope.
188- scope = arg2scope [argname ]
189- node = None
190- if scope is not Scope .Function :
191- node = get_scope_node (collector , scope )
192- if node is None :
193- assert scope is Scope .Class and isinstance (
194- collector , _pytest .python .Module
195- )
196- # Use module-level collector for class-scope (for now).
197- node = collector
198- if node is None :
199- name2pseudofixturedef = None
200- else :
201- default : Dict [str , FixtureDef [Any ]] = {}
202- name2pseudofixturedef = node .stash .setdefault (
203- name2pseudofixturedef_key , default
204- )
205- if name2pseudofixturedef is not None and argname in name2pseudofixturedef :
206- arg2fixturedefs [argname ] = [name2pseudofixturedef [argname ]]
207- else :
208- fixturedef = FixtureDef (
209- fixturemanager = fixturemanager ,
210- baseid = "" ,
211- argname = argname ,
212- func = get_direct_param_fixture_func ,
213- scope = arg2scope [argname ],
214- params = valuelist ,
215- unittest = False ,
216- ids = None ,
217- )
218- arg2fixturedefs [argname ] = [fixturedef ]
219- if name2pseudofixturedef is not None :
220- name2pseudofixturedef [argname ] = fixturedef
200+ # Used for storing artificial fixturedefs for direct parametrization.
201+ name2pseudofixturedef_key = StashKey [Dict [str , "FixtureDef[Any]" ]]()
221202
222203
223204def getfixturemarker (obj : object ) -> Optional ["FixtureFunctionMarker" ]:
@@ -229,38 +210,58 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
229210 )
230211
231212
232- # Parametrized fixture key, helper alias for code below.
233- _Key = Tuple [object , ...]
213+ @dataclasses .dataclass (frozen = True )
214+ class FixtureArgKey :
215+ argname : str
216+ param_index : Optional [int ]
217+ param_value : Optional [Hashable ]
218+ scoped_item_path : Optional [Path ]
219+ item_cls : Optional [type ]
220+
221+
222+ def get_fixture_arg_key (item : nodes .Item , argname : str , scope : Scope ) -> FixtureArgKey :
223+ param_index = None
224+ param_value = None
225+ if hasattr (item , 'callspec' ) and argname in item .callspec .params :
226+ # Fixture is parametrized.
227+ if isinstance (item .callspec .params [argname ], Hashable ):
228+ param_value = item .callspec .params [argname ]
229+ else :
230+ param_index = item .callspec .indices [argname ]
234231
232+ if scope is Scope .Session :
233+ scoped_item_path = None
234+ elif scope is Scope .Package :
235+ scoped_item_path = item .path .parent
236+ elif scope in (Scope .Module , Scope .Class ):
237+ scoped_item_path = item .path
238+ else :
239+ assert_never (scope )
240+
241+ if scope is Scope .Class and type (item ).__name__ != "DoctestItem" :
242+ item_cls = item .cls # type: ignore[attr-defined]
243+ else :
244+ item_cls = None
245+
246+ return FixtureArgKey (argname , param_index , param_value , scoped_item_path , item_cls )
247+
235248
236- def get_parametrized_fixture_keys (item : nodes .Item , scope : Scope ) -> Iterator [_Key ]:
249+ def get_fixture_keys (item : nodes .Item , scope : Scope ) -> Iterator [FixtureArgKey ]:
237250 """Return list of keys for all parametrized arguments which match
238251 the specified scope."""
239252 assert scope is not Scope .Function
240- try :
241- callspec = item .callspec # type: ignore[attr-defined]
242- except AttributeError :
243- pass
244- else :
245- cs : CallSpec2 = callspec
246- # cs.indices.items() is random order of argnames. Need to
253+ if hasattr (item , '_fixtureinfo' ):
247254 # sort this so that different calls to
248- # get_parametrized_fixture_keys will be deterministic.
249- for argname , param_index in sorted (cs .indices .items ()):
250- if cs ._arg2scope [argname ] != scope :
255+ # get_fixture_keys will be deterministic.
256+ for argname , fixture_def in sorted (item ._fixtureinfo .name2fixturedefs .items ()):
257+ # In the case item is parametrized on the `argname` with
258+ # a scope, it overrides that of the fixture.
259+ if hasattr (item , 'callspec' ) and argname in item .callspec ._arg2scope :
260+ if item .callspec ._arg2scope [argname ] != scope :
261+ continue
262+ elif fixture_def [- 1 ]._scope != scope :
251263 continue
252- if scope is Scope .Session :
253- key : _Key = (argname , param_index )
254- elif scope is Scope .Package :
255- key = (argname , param_index , item .path .parent )
256- elif scope is Scope .Module :
257- key = (argname , param_index , item .path )
258- elif scope is Scope .Class :
259- item_cls = item .cls # type: ignore[attr-defined]
260- key = (argname , param_index , item .path , item_cls )
261- else :
262- assert_never (scope )
263- yield key
264+ yield get_fixture_arg_key (item , argname , scope )
264265
265266
266267# Algorithm for sorting on a per-parametrized resource setup basis.
@@ -270,44 +271,66 @@ def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_K
270271
271272
272273def reorder_items (items : Sequence [nodes .Item ]) -> List [nodes .Item ]:
273- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [_Key , None ]]] = {}
274- items_by_argkey : Dict [Scope , Dict [_Key , Deque [nodes .Item ]]] = {}
274+ argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]] = {}
275+ items_by_argkey : Dict [Scope , Dict [FixtureArgKey , Deque [nodes .Item ]]] = {}
275276 for scope in HIGH_SCOPES :
276- d : Dict [nodes .Item , Dict [_Key , None ]] = {}
277+ d : Dict [nodes .Item , Dict [FixtureArgKey , None ]] = {}
277278 argkeys_cache [scope ] = d
278- item_d : Dict [_Key , Deque [nodes .Item ]] = defaultdict (deque )
279+ item_d : Dict [FixtureArgKey , Deque [nodes .Item ]] = defaultdict (deque )
279280 items_by_argkey [scope ] = item_d
280281 for item in items :
281- keys = dict .fromkeys (get_parametrized_fixture_keys (item , scope ), None )
282+ keys = dict .fromkeys (get_fixture_keys (item , scope ), None )
282283 if keys :
283284 d [item ] = keys
284285 for key in keys :
285286 item_d [key ].append (item )
286287 items_dict = dict .fromkeys (items , None )
287- return list (
288+ reordered_items = list (
288289 reorder_items_atscope (items_dict , argkeys_cache , items_by_argkey , Scope .Session )
289290 )
291+ for scope in reversed (HIGH_SCOPES ):
292+ for key in items_by_argkey [scope ]:
293+ last_item_dependent_on_key = items_by_argkey [scope ][key ].pop ()
294+ fixturedef = last_item_dependent_on_key ._fixtureinfo .name2fixturedefs [key .argname ][- 1 ]
295+ if fixturedef .is_pseudo :
296+ continue
297+ last_item_dependent_on_key .teardown = functools .partial (
298+ lambda other_finalizers , new_finalizer : [finalizer () for finalizer in (new_finalizer , other_finalizers )],
299+ last_item_dependent_on_key .teardown ,
300+ functools .partial (fixturedef .finish , last_item_dependent_on_key ._request )
301+ )
302+ return reordered_items
290303
291304
292305def fix_cache_order (
293306 item : nodes .Item ,
294- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [_Key , None ]]],
295- items_by_argkey : Dict [Scope , Dict [_Key , "Deque[nodes.Item]" ]],
307+ argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]],
308+ items_by_argkey : Dict [Scope , Dict [FixtureArgKey , "Deque[nodes.Item]" ]],
309+ ignore : Set [Optional [FixtureArgKey ]],
310+ current_scope : Scope
296311) -> None :
297312 for scope in HIGH_SCOPES :
313+ if current_scope < scope :
314+ continue
298315 for key in argkeys_cache [scope ].get (item , []):
316+ if key in ignore :
317+ continue
299318 items_by_argkey [scope ][key ].appendleft (item )
319+ # Make sure last dependent item on a key
320+ # remains updated while reordering.
321+ if items_by_argkey [scope ][key ][- 1 ] == item :
322+ items_by_argkey [scope ][key ].pop ()
300323
301324
302325def reorder_items_atscope (
303326 items : Dict [nodes .Item , None ],
304- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [_Key , None ]]],
305- items_by_argkey : Dict [Scope , Dict [_Key , "Deque[nodes.Item]" ]],
327+ argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]],
328+ items_by_argkey : Dict [Scope , Dict [FixtureArgKey , "Deque[nodes.Item]" ]],
306329 scope : Scope ,
307330) -> Dict [nodes .Item , None ]:
308331 if scope is Scope .Function or len (items ) < 3 :
309332 return items
310- ignore : Set [Optional [_Key ]] = set ()
333+ ignore : Set [Optional [FixtureArgKey ]] = set ()
311334 items_deque = deque (items )
312335 items_done : Dict [nodes .Item , None ] = {}
313336 scoped_items_by_argkey = items_by_argkey [scope ]
@@ -332,7 +355,7 @@ def reorder_items_atscope(
332355 i for i in scoped_items_by_argkey [slicing_argkey ] if i in items
333356 ]
334357 for i in reversed (matching_items ):
335- fix_cache_order (i , argkeys_cache , items_by_argkey )
358+ fix_cache_order (i , argkeys_cache , items_by_argkey , ignore , scope )
336359 items_deque .appendleft (i )
337360 break
338361 if no_argkey_group :
@@ -345,10 +368,6 @@ def reorder_items_atscope(
345368 return items_done
346369
347370
348- def get_direct_param_fixture_func (request : "FixtureRequest" ) -> Any :
349- return request .param
350-
351-
352371@dataclasses .dataclass
353372class FuncFixtureInfo :
354373 __slots__ = ("argnames" , "initialnames" , "names_closure" , "name2fixturedefs" )
@@ -891,7 +910,7 @@ def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
891910
892911
893912def call_fixture_func (
894- fixturefunc : "_FixtureFunc[FixtureValue]" , request : FixtureRequest , kwargs
913+ fixturefunc : "_FixtureFunc[FixtureValue]" , request : SubRequest , kwargs
895914) -> FixtureValue :
896915 if is_generator (fixturefunc ):
897916 fixturefunc = cast (
@@ -963,6 +982,7 @@ def __init__(
963982 ids : Optional [
964983 Union [Tuple [Optional [object ], ...], Callable [[Any ], Optional [object ]]]
965984 ] = None ,
985+ is_pseudo : bool = False ,
966986 ) -> None :
967987 self ._fixturemanager = fixturemanager
968988 # The "base" node ID for the fixture.
@@ -1014,6 +1034,9 @@ def __init__(
10141034 self .cached_result : Optional [_FixtureCachedResult [FixtureValue ]] = None
10151035 self ._finalizers : List [Callable [[], object ]] = []
10161036
1037+ # Whether fixture is a pseudo-fixture made in direct parametrizations.
1038+ self .is_pseudo = is_pseudo
1039+
10171040 @property
10181041 def scope (self ) -> "_ScopeName" :
10191042 """Scope string, one of "function", "class", "module", "package", "session"."""
@@ -1572,6 +1595,9 @@ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
15721595 # another fixture, while requesting the super fixture, keep going
15731596 # in case the super fixture is parametrized (#1953).
15741597 for fixturedef in reversed (fixture_defs ):
1598+ # Skip pseudo-fixtures
1599+ if fixturedef .is_pseudo :
1600+ continue
15751601 # Fixture is parametrized, apply it and stop.
15761602 if fixturedef .params is not None :
15771603 metafunc .parametrize (
0 commit comments