15
15
from typing import Dict
16
16
from typing import Generator
17
17
from typing import Generic
18
+ from typing import Hashable
18
19
from typing import Iterable
19
20
from typing import Iterator
20
21
from typing import List
@@ -146,78 +147,58 @@ def get_scope_node(
146
147
assert_never (scope )
147
148
148
149
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 ))
151
198
152
199
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]" ]]()
221
202
222
203
223
204
def getfixturemarker (obj : object ) -> Optional ["FixtureFunctionMarker" ]:
@@ -229,38 +210,58 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
229
210
)
230
211
231
212
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 ]
234
231
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
+
235
248
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 ]:
237
250
"""Return list of keys for all parametrized arguments which match
238
251
the specified scope."""
239
252
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' ):
247
254
# 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 :
251
263
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 )
264
265
265
266
266
267
# 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
270
271
271
272
272
273
def 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 ]]] = {}
275
276
for scope in HIGH_SCOPES :
276
- d : Dict [nodes .Item , Dict [_Key , None ]] = {}
277
+ d : Dict [nodes .Item , Dict [FixtureArgKey , None ]] = {}
277
278
argkeys_cache [scope ] = d
278
- item_d : Dict [_Key , Deque [nodes .Item ]] = defaultdict (deque )
279
+ item_d : Dict [FixtureArgKey , Deque [nodes .Item ]] = defaultdict (deque )
279
280
items_by_argkey [scope ] = item_d
280
281
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 )
282
283
if keys :
283
284
d [item ] = keys
284
285
for key in keys :
285
286
item_d [key ].append (item )
286
287
items_dict = dict .fromkeys (items , None )
287
- return list (
288
+ reordered_items = list (
288
289
reorder_items_atscope (items_dict , argkeys_cache , items_by_argkey , Scope .Session )
289
290
)
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
290
303
291
304
292
305
def fix_cache_order (
293
306
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
296
311
) -> None :
297
312
for scope in HIGH_SCOPES :
313
+ if current_scope < scope :
314
+ continue
298
315
for key in argkeys_cache [scope ].get (item , []):
316
+ if key in ignore :
317
+ continue
299
318
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 ()
300
323
301
324
302
325
def reorder_items_atscope (
303
326
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]" ]],
306
329
scope : Scope ,
307
330
) -> Dict [nodes .Item , None ]:
308
331
if scope is Scope .Function or len (items ) < 3 :
309
332
return items
310
- ignore : Set [Optional [_Key ]] = set ()
333
+ ignore : Set [Optional [FixtureArgKey ]] = set ()
311
334
items_deque = deque (items )
312
335
items_done : Dict [nodes .Item , None ] = {}
313
336
scoped_items_by_argkey = items_by_argkey [scope ]
@@ -332,7 +355,7 @@ def reorder_items_atscope(
332
355
i for i in scoped_items_by_argkey [slicing_argkey ] if i in items
333
356
]
334
357
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 )
336
359
items_deque .appendleft (i )
337
360
break
338
361
if no_argkey_group :
@@ -345,10 +368,6 @@ def reorder_items_atscope(
345
368
return items_done
346
369
347
370
348
- def get_direct_param_fixture_func (request : "FixtureRequest" ) -> Any :
349
- return request .param
350
-
351
-
352
371
@dataclasses .dataclass
353
372
class FuncFixtureInfo :
354
373
__slots__ = ("argnames" , "initialnames" , "names_closure" , "name2fixturedefs" )
@@ -891,7 +910,7 @@ def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
891
910
892
911
893
912
def call_fixture_func (
894
- fixturefunc : "_FixtureFunc[FixtureValue]" , request : FixtureRequest , kwargs
913
+ fixturefunc : "_FixtureFunc[FixtureValue]" , request : SubRequest , kwargs
895
914
) -> FixtureValue :
896
915
if is_generator (fixturefunc ):
897
916
fixturefunc = cast (
@@ -963,6 +982,7 @@ def __init__(
963
982
ids : Optional [
964
983
Union [Tuple [Optional [object ], ...], Callable [[Any ], Optional [object ]]]
965
984
] = None ,
985
+ is_pseudo : bool = False ,
966
986
) -> None :
967
987
self ._fixturemanager = fixturemanager
968
988
# The "base" node ID for the fixture.
@@ -1014,6 +1034,9 @@ def __init__(
1014
1034
self .cached_result : Optional [_FixtureCachedResult [FixtureValue ]] = None
1015
1035
self ._finalizers : List [Callable [[], object ]] = []
1016
1036
1037
+ # Whether fixture is a pseudo-fixture made in direct parametrizations.
1038
+ self .is_pseudo = is_pseudo
1039
+
1017
1040
@property
1018
1041
def scope (self ) -> "_ScopeName" :
1019
1042
"""Scope string, one of "function", "class", "module", "package", "session"."""
@@ -1572,6 +1595,9 @@ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
1572
1595
# another fixture, while requesting the super fixture, keep going
1573
1596
# in case the super fixture is parametrized (#1953).
1574
1597
for fixturedef in reversed (fixture_defs ):
1598
+ # Skip pseudo-fixtures
1599
+ if fixturedef .is_pseudo :
1600
+ continue
1575
1601
# Fixture is parametrized, apply it and stop.
1576
1602
if fixturedef .params is not None :
1577
1603
metafunc .parametrize (
0 commit comments