@@ -122,7 +122,7 @@ class FixtureClosureNode(object):
122122
123123 """
124124 __slots__ = 'parent' , 'fixture_defs_mgr' , \
125- 'fixture_defs' , 'split_fixture_name' , 'split_fixture_alternatives' , 'children'
125+ 'fixture_defs' , '_current_indices' , ' split_fixture_name' , 'split_fixture_alternatives' , 'children'
126126
127127 def __init__ (self ,
128128 fixture_defs_mgr = None , # type: FixtureDefsCache
@@ -138,8 +138,12 @@ def __init__(self,
138138 self .fixture_defs_mgr = fixture_defs_mgr
139139 self .parent = parent_node
140140
141+ # This is a temp variable used during closure construction
142+ self ._current_indices = None # type: dict[str, int]
143+
141144 # these will be set after closure has been built
142145 self .fixture_defs = None # type: OrderedDict
146+ # Note: fixture_defs contains dependencies of a node, with their definitions.
143147 self .split_fixture_name = None # type: str
144148 self .split_fixture_alternatives = []
145149 # we do not use a dict any more as several children can use the same union value (doubled unions)
@@ -238,6 +242,7 @@ def build_closure(self,
238242 ignore_args = ()
239243 ):
240244 """
245+ This method is for the root node only
241246 Updates this Node with the fixture names provided as argument.
242247 Fixture names and definitions will be stored in self.fixture_defs.
243248
@@ -250,8 +255,41 @@ def build_closure(self,
250255 to "direct parametrization"
251256 :return:
252257 """
258+ assert self .parent is None , "This should only be called on the root node, use _build_closure otherwise"
253259 self ._build_closure (self .fixture_defs_mgr , initial_fixture_names , ignore_args = ignore_args )
254260
261+ # We can now remove the temporary "current indices" from all nodes
262+ self ._clean_current_indices ()
263+
264+ def _clean_current_indices (self ):
265+ """Clean `self._current_indices` from all nodes. This variable is only used to remember the current fixture
266+ overridden variants used while walking the dependencies. Once closure is built it is useless."""
267+ for c in self .children :
268+ c ._clean_current_indices ()
269+ self ._current_indices = None
270+
271+ @property
272+ def root (self ):
273+ """Return the root of the fixture closure tree"""
274+ node = self
275+ while node .parent is not None :
276+ node = node .parent
277+ return node
278+
279+ def _get_current_index (self , argname , default = None ):
280+ """Equivalent of pytest 9 current_index.get(argname, default) but performed recursively till the top of the tree."""
281+ assert self ._current_indices is not None , "should never happen by design"
282+ idx = self ._current_indices .get (argname ) # without the default as we need to know if this worked
283+ if idx is not None :
284+ # Found
285+ return idx
286+ elif self .parent is not None :
287+ # Not found - Maybe the parent has it
288+ return self .parent ._get_current_index (argname , default )
289+ else :
290+ # Root reached and not found, return the default
291+ return default
292+
255293 def is_closure_built (self ):
256294 return self .fixture_defs is not None
257295
@@ -284,63 +322,97 @@ def _build_closure(self,
284322 if self .fixture_defs is None :
285323 self .fixture_defs = OrderedDict ()
286324
287- # -- then for all pending, add them with their dependencies
288- pending_fixture_names = list (initial_fixture_names )
289- while len (pending_fixture_names ) > 0 :
290- fixname = pending_fixture_names .pop (0 )
325+ # -- then for all fixture names, add them with their dependencies
291326
292- # if the fixture is already known in this node or above, do not care
293- if self .already_knows_fixture (fixname ):
294- continue
327+ # From Pytest9 on, see https://github.com/pytest-dev/pytest/pull/13789/changes
328+ # Track the index for each fixture name in the simulated stack.
329+ # Needed for handling override chains correctly, similar to _get_active_fixturedef.
330+ # Using negative indices: -1 is the most specific (last), -2 is second to last, etc.
331+ self ._current_indices = {}
295332
296- # new ignore_args option in pytest 4.6+. Not really a fixture but a test function parameter, it seems.
297- if fixname in ignore_args :
298- self .add_required_fixture (fixname , None )
299- continue
333+ def process_argname (argname : str ) -> None :
334+ # Optimization: if this version of the fixture name was already processed in this node or parent chain,
335+ # do not care.
336+ idx = self ._get_current_index (argname )
337+ if idx == - 1 :
338+ return
339+ elif idx is None :
340+ # Fixture was not processed OR has been registered without any fixture def
341+ # Check the latter
342+ if self .already_knows_fixture (argname ):
343+ return
344+
345+ # Add fixture name to the closure :
346+ # The equivalent of this section from pytest code will be done after the 'ignore_args' below for two reasons
347+ # - in case of fixture 'unions' we will not add them the same way
348+ # - in our closure tree nodes, the closure is not only containing the names but also the definitions (it is
349+ # a combination of fixturenames_closure and arg2fixturedefs)
350+ #
351+ # if argname not in fixturenames_closure:
352+ # fixturenames_closure.append(argname)
353+
354+ # New ignore_args option in pytest 4.6+. Not really a fixture but a test function parameter, it seems.
355+ if argname in ignore_args :
356+ self .add_required_fixture (argname , None ) # do not store any fixture def for it
357+ return
300358
301- # else grab the fixture definition(s) for this fixture name for this test node id
302- fixturedefs = fixture_defs_mgr .get_fixture_defs (fixname )
359+ # Finally the main processing
360+ # (a) Grab the fixture definition(s) for this fixture name for this test node id
361+ fixturedefs = fixture_defs_mgr .get_fixture_defs (argname )
303362 if not fixturedefs :
304- # fixture without definition: add it. This can happen with e.g. "requests", etc.
305- self .add_required_fixture (fixname , None )
306- continue
307- else :
308- # the actual definition is the last one
309- _fixdef = fixturedefs [- 1 ]
310- _params = _fixdef .params
311-
312- if _params is not None and is_fixture_union_params (_params ):
313- # create an UNION fixture
314-
315- # transform the _params into a list of names
316- alternative_f_names = UnionFixtureAlternative .to_list_of_fixture_names (_params )
363+ # Fixture not defined or not visible - This can happen with e.g. "requests", etc. - add it.
364+ self .add_required_fixture (argname , None ) # do not store any fixture def for it
365+ return
317366
318- # TO DO if only one name, simplify ? >> No, we leave such "optimization" to the end user
367+ # (b) Get the index of the override fixture definition version we need to manage.
368+ # Start with the last one (-1) = the one that will actually be used, but it may require some of the other
369+ # definitions because of a complex dependency chain.
370+ index = self ._get_current_index (argname , - 1 )
371+ if - index > len (fixturedefs ):
372+ # Exhausted the override chain (will error during runtest).
373+ return
374+ # Now grab that version of the fixture definition
375+ fixturedef = fixturedefs [index ]
376+
377+ # (c) introspect parameters to check: Is this fixture parametrized with a "union" of fixtures ?
378+ _params = fixturedef .params
379+ if _params is None or not is_fixture_union_params (_params ):
380+ # No : this is a standard fixture. Do the same as in pytest
381+
382+ # This is the place where we finally add the fixture name to the closure
383+ # if argname not in fixturenames_closure:
384+ # fixturenames_closure.append(argname)
385+ self .add_required_fixture (argname , fixturedefs )
386+
387+ # Now process all fixture dependencies, but in their analysis we will consider the "previous" override
388+ # so that (I guess) we can handle the situation a[overridden] -> b -> a[original] -> c
389+ self ._current_indices [argname ] = index - 1
390+ for dependency in fixturedef .argnames :
391+ process_argname (dependency )
392+ self ._current_indices [argname ] = index
393+ else :
394+ # This is a 'Union'-parametrized fixture - do not add it yet
395+ # It requires to split the current closure node into two branches before continuing the analysis
396+ # Indeed some fixture dependencies will be needed in some of the branches, while some others not.
319397
320- # if there are direct dependencies that are not the union members, add them to pending
321- non_member_dependencies = [f for f in _fixdef .argnames if f not in alternative_f_names ]
322- # currently we only have 'requests' in this list but future impl of fixture_union may act otherwise
323- pending_fixture_names += non_member_dependencies
398+ # transform the _params into a list of names
399+ alternative_f_names = UnionFixtureAlternative .to_list_of_fixture_names (_params )
324400
325- # propagate WITH the pending
326- self .split_and_build (fixture_defs_mgr , fixname , fixturedefs , alternative_f_names ,
327- pending_fixture_names , ignore_args = ignore_args )
401+ # TO DO if only one name, simplify ? >> No, we leave such "optimization" to the end user
328402
329- # empty the pending because all of them have been propagated on all children with their dependencies
330- pending_fixture_names = [ ]
331- continue
403+ # if there are direct dependencies that are not the union members, add them to pending
404+ non_member_dependencies = [ f for f in fixturedef . argnames if f not in alternative_f_names ]
405+ # currently we only have 'requests' in this list but future impl of fixture_union may act otherwise
332406
333- else :
334- # normal fixture
335- self .add_required_fixture (fixname , fixturedefs )
407+ # propagate WITH all non-member dependencies
408+ # but handle the situation where the union fixture was an override of another fixture (nasty!)
409+ self ._current_indices [argname ] = index - 1
410+ self .split_and_build (fixture_defs_mgr , argname , fixturedefs , alternative_f_names ,
411+ non_member_dependencies , ignore_args = ignore_args )
412+ self ._current_indices [argname ] = index
336413
337- # add all dependencies in the to do list
338- dependencies = _fixdef .argnames
339- # - append: was pytest default
340- # pending_fixture_names += dependencies
341- # - prepend: makes much more sense
342- pending_fixture_names = list (dependencies ) + pending_fixture_names
343- continue
414+ for name in initial_fixture_names :
415+ process_argname (name )
344416
345417 # ------ tools to add new fixture names during closure construction
346418
@@ -405,7 +477,8 @@ def add_required_fixture(self, new_fixture_name, new_fixture_defs):
405477 if self .already_knows_fixture (new_fixture_name ):
406478 return
407479 elif not self .has_split ():
408- # add_required_fixture locally
480+ # Add required fixture to the dependencies of this node
481+ # (fixture_defs is the dict of dependencies + their defs for this node)
409482 if new_fixture_name not in self .fixture_defs :
410483 self .fixture_defs [new_fixture_name ] = new_fixture_defs
411484 else :
@@ -424,10 +497,10 @@ def split_and_build(self,
424497 """ Declares that this node contains a union with alternatives (child nodes=subtrees) """
425498
426499 if self .has_split ():
427- raise ValueError ( "This should not happen anymore" )
428- # # propagate the split on the children: split each of them
429- # for n in self.children:
430- # n.split_and_build(fm, nodeid, split_fixture_name, split_fixture_defs, alternative_fixture_names )
500+ # Propagate the split on the children: split each of them
501+ for n in self . children :
502+ n . split_and_build ( fixture_defs_mgr , split_fixture_name , split_fixture_defs ,
503+ alternative_fixture_names , pending_fixtures_list , ignore_args )
431504 else :
432505 # add the split (union) name to known fixtures
433506 self .add_required_fixture (split_fixture_name , split_fixture_defs )
@@ -779,7 +852,9 @@ def _getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
779852 # (2) now let's do it by ourselves to support fixture unions
780853 _init_fixnames , super_closure , arg2fixturedefs = create_super_closure (fm , parentnode , fixturenames , ignore_args )
781854
782- # Compare with the previous behaviour TODO remove when in 'production' ?
855+ # Compare with the previous behaviour.
856+ # NOTE: do not remove these asserts when in 'production', as this proved effective to detect bugs related with
857+ # pytest modifications such as GH#374
783858 # NOTE different order happens all the time because of our "prepend" strategy in the closure building
784859 # which makes much more sense/intuition than pytest default
785860 assert set (super_closure ) == set (ref_fixturenames )
0 commit comments