2121from typing import Iterable
2222from typing import Iterator
2323from typing import List
24+ from typing import Mapping
2425from typing import MutableMapping
2526from typing import NoReturn
2627from typing import Optional
28+ from typing import OrderedDict
2729from typing import overload
2830from typing import Sequence
2931from typing import Set
7678
7779
7880if TYPE_CHECKING :
79- from typing import Deque
80-
8181 from _pytest .main import Session
8282 from _pytest .python import CallSpec2
8383 from _pytest .python import Function
@@ -161,6 +161,12 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
161161 )
162162
163163
164+ # Algorithm for sorting on a per-parametrized resource setup basis.
165+ # It is called for Session scope first and performs sorting
166+ # down to the lower scopes such as to minimize number of "high scope"
167+ # setups and teardowns.
168+
169+
164170@dataclasses .dataclass (frozen = True )
165171class FixtureArgKey :
166172 argname : str
@@ -169,97 +175,95 @@ class FixtureArgKey:
169175 item_cls : Optional [type ]
170176
171177
172- def get_parametrized_fixture_keys (
178+ _V = TypeVar ("_V" )
179+ OrderedSet = Dict [_V , None ]
180+
181+
182+ def get_parametrized_fixture_argkeys (
173183 item : nodes .Item , scope : Scope
174184) -> Iterator [FixtureArgKey ]:
175185 """Return list of keys for all parametrized arguments which match
176186 the specified scope."""
177187 assert scope is not Scope .Function
188+
178189 try :
179190 callspec : CallSpec2 = item .callspec # type: ignore[attr-defined]
180191 except AttributeError :
181192 return
193+
194+ item_cls = None
195+ if scope is Scope .Session :
196+ scoped_item_path = None
197+ elif scope is Scope .Package :
198+ # Package key = module's directory.
199+ scoped_item_path = item .path .parent
200+ elif scope is Scope .Module :
201+ scoped_item_path = item .path
202+ elif scope is Scope .Class :
203+ scoped_item_path = item .path
204+ item_cls = item .cls # type: ignore[attr-defined]
205+ else :
206+ assert_never (scope )
207+
182208 for argname in callspec .indices :
183209 if callspec ._arg2scope [argname ] != scope :
184210 continue
185-
186- item_cls = None
187- if scope is Scope .Session :
188- scoped_item_path = None
189- elif scope is Scope .Package :
190- # Package key = module's directory.
191- scoped_item_path = item .path .parent
192- elif scope is Scope .Module :
193- scoped_item_path = item .path
194- elif scope is Scope .Class :
195- scoped_item_path = item .path
196- item_cls = item .cls # type: ignore[attr-defined]
197- else :
198- assert_never (scope )
199-
200211 param_index = callspec .indices [argname ]
201212 yield FixtureArgKey (argname , param_index , scoped_item_path , item_cls )
202213
203214
204- # Algorithm for sorting on a per-parametrized resource setup basis.
205- # It is called for Session scope first and performs sorting
206- # down to the lower scopes such as to minimize number of "high scope"
207- # setups and teardowns.
208-
209-
210215def reorder_items (items : Sequence [nodes .Item ]) -> List [nodes .Item ]:
211- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]] = {}
212- items_by_argkey : Dict [Scope , Dict [FixtureArgKey , Deque [nodes .Item ]]] = {}
216+ argkeys_by_item : Dict [Scope , Dict [nodes .Item , OrderedSet [FixtureArgKey ]]] = {}
217+ items_by_argkey : Dict [
218+ Scope , Dict [FixtureArgKey , OrderedDict [nodes .Item , None ]]
219+ ] = {}
213220 for scope in HIGH_SCOPES :
214- scoped_argkeys_cache = argkeys_cache [scope ] = {}
215- scoped_items_by_argkey = items_by_argkey [scope ] = defaultdict (deque )
221+ scoped_argkeys_by_item = argkeys_by_item [scope ] = {}
222+ scoped_items_by_argkey = items_by_argkey [scope ] = defaultdict (OrderedDict )
216223 for item in items :
217- keys = dict .fromkeys (get_parametrized_fixture_keys (item , scope ), None )
218- if keys :
219- scoped_argkeys_cache [item ] = keys
220- for key in keys :
221- scoped_items_by_argkey [key ].append (item )
222- items_dict = dict .fromkeys (items , None )
224+ argkeys = dict .fromkeys (get_parametrized_fixture_argkeys (item , scope ))
225+ if argkeys :
226+ scoped_argkeys_by_item [item ] = argkeys
227+ for argkey in argkeys :
228+ scoped_items_by_argkey [argkey ][item ] = None
229+
230+ items_set = dict .fromkeys (items )
223231 return list (
224- reorder_items_atscope (items_dict , argkeys_cache , items_by_argkey , Scope .Session )
232+ reorder_items_atscope (
233+ items_set , argkeys_by_item , items_by_argkey , Scope .Session
234+ )
225235 )
226236
227237
228- def fix_cache_order (
229- item : nodes .Item ,
230- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]],
231- items_by_argkey : Dict [Scope , Dict [FixtureArgKey , "Deque[nodes.Item]" ]],
232- ) -> None :
233- for scope in HIGH_SCOPES :
234- for key in argkeys_cache [scope ].get (item , []):
235- items_by_argkey [scope ][key ].appendleft (item )
236-
237-
238238def reorder_items_atscope (
239- items : Dict [nodes .Item , None ],
240- argkeys_cache : Dict [Scope , Dict [nodes .Item , Dict [FixtureArgKey , None ]]],
241- items_by_argkey : Dict [Scope , Dict [FixtureArgKey , "Deque[nodes.Item]" ]],
239+ items : OrderedSet [nodes .Item ],
240+ argkeys_by_item : Mapping [Scope , Mapping [nodes .Item , OrderedSet [FixtureArgKey ]]],
241+ items_by_argkey : Mapping [
242+ Scope , Mapping [FixtureArgKey , OrderedDict [nodes .Item , None ]]
243+ ],
242244 scope : Scope ,
243- ) -> Dict [nodes .Item , None ]:
245+ ) -> OrderedSet [nodes .Item ]:
244246 if scope is Scope .Function or len (items ) < 3 :
245247 return items
246- ignore : Set [Optional [FixtureArgKey ]] = set ()
247- items_deque = deque (items )
248- items_done : Dict [nodes .Item , None ] = {}
248+
249249 scoped_items_by_argkey = items_by_argkey [scope ]
250- scoped_argkeys_cache = argkeys_cache [scope ]
250+ scoped_argkeys_by_item = argkeys_by_item [scope ]
251+
252+ ignore : Set [FixtureArgKey ] = set ()
253+ items_deque = deque (items )
254+ items_done : OrderedSet [nodes .Item ] = {}
251255 while items_deque :
252- no_argkey_group : Dict [nodes .Item , None ] = {}
256+ no_argkey_items : OrderedSet [nodes .Item ] = {}
253257 slicing_argkey = None
254258 while items_deque :
255259 item = items_deque .popleft ()
256- if item in items_done or item in no_argkey_group :
260+ if item in items_done or item in no_argkey_items :
257261 continue
258262 argkeys = dict .fromkeys (
259- ( k for k in scoped_argkeys_cache .get (item , []) if k not in ignore ), None
263+ k for k in scoped_argkeys_by_item .get (item , ()) if k not in ignore
260264 )
261265 if not argkeys :
262- no_argkey_group [item ] = None
266+ no_argkey_items [item ] = None
263267 else :
264268 slicing_argkey , _ = argkeys .popitem ()
265269 # We don't have to remove relevant items from later in the
@@ -268,16 +272,23 @@ def reorder_items_atscope(
268272 i for i in scoped_items_by_argkey [slicing_argkey ] if i in items
269273 ]
270274 for i in reversed (matching_items ):
271- fix_cache_order (i , argkeys_cache , items_by_argkey )
272275 items_deque .appendleft (i )
276+ # Fix items_by_argkey order.
277+ for other_scope in HIGH_SCOPES :
278+ other_scoped_items_by_argkey = items_by_argkey [other_scope ]
279+ for argkey in argkeys_by_item [other_scope ].get (i , ()):
280+ other_scoped_items_by_argkey [argkey ][i ] = None
281+ other_scoped_items_by_argkey [argkey ].move_to_end (
282+ i , last = False
283+ )
273284 break
274- if no_argkey_group :
275- no_argkey_group = reorder_items_atscope (
276- no_argkey_group , argkeys_cache , items_by_argkey , scope .next_lower ()
285+ if no_argkey_items :
286+ reordered_no_argkey_items = reorder_items_atscope (
287+ no_argkey_items , argkeys_by_item , items_by_argkey , scope .next_lower ()
277288 )
278- for item in no_argkey_group :
279- items_done [ item ] = None
280- ignore .add (slicing_argkey )
289+ items_done . update ( reordered_no_argkey_items )
290+ if slicing_argkey is not None :
291+ ignore .add (slicing_argkey )
281292 return items_done
282293
283294
0 commit comments