Skip to content

Commit 23b47b1

Browse files
author
Sylvain MARIE
committed
FixtureClosureNode is now able to properly handle ignore_args, and now supports that plugins append fixtures to the closure, such as pytest-asyncio. Added corresponding tests. Fixes #68
1 parent fbfc423 commit 23b47b1

File tree

4 files changed

+123
-31
lines changed

4 files changed

+123
-31
lines changed

pytest_cases/plugin.py

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,20 @@ def get_fixture_defs(self, fixname):
6161
class FixtureClosureNode(object):
6262
__slots__ = 'parent', 'fixture_defs', \
6363
'split_fixture_name', 'split_fixture_discarded_names', 'children', \
64-
'_as_list', 'all_fixture_defs'
64+
'_as_list', 'all_fixture_defs', 'fixture_defs_mgr'
65+
66+
def __init__(self,
67+
fixture_defs_mgr=None, # type: FixtureDefsCache
68+
parent_node=None # type: FixtureClosureNode
69+
):
70+
if fixture_defs_mgr is None:
71+
if parent_node is None:
72+
raise ValueError()
73+
fixture_defs_mgr = parent_node.fixture_defs_mgr
74+
else:
75+
assert isinstance(fixture_defs_mgr, FixtureDefsCache)
6576

66-
def __init__(self, parent_node=None):
77+
self.fixture_defs_mgr = fixture_defs_mgr
6778
self.parent = parent_node
6879

6980
# these will be set after closure has been built
@@ -129,7 +140,15 @@ def __setitem__(self, key, value):
129140
warn("WARNING the new order is not taken into account !!")
130141

131142
def append(self, item):
132-
warn("WARNING some code tries to add an item to the fixture tree, this will be IGNORED !! Item: %s" % item)
143+
"""
144+
This is now supported: we simply update the closure with the new item.
145+
`self.all_fixture_defs` and `self._as_list` are updated on the way
146+
so the resulting facades used by pytest are consistent after the update.
147+
148+
:param item:
149+
:return:
150+
"""
151+
self.build_closure((item, ))
133152

134153
def insert(self, index, object):
135154
warn("WARNING some code tries to insert an item in the fixture tree, this will be IGNORED !! "
@@ -224,10 +243,36 @@ def _get_all_fixture_defs(self):
224243
# ---- utils to build the closure
225244

226245
def build_closure(self,
227-
fixture_defs_mgr, # type: FixtureDefsCache
228-
initial_fixture_names # type: Iterable[str]
246+
initial_fixture_names, # type: Iterable[str]
247+
ignore_args=()
229248
):
230-
self._build_closure(fixture_defs_mgr, initial_fixture_names)
249+
"""
250+
Updates this Node with the fixture names provided as argument.
251+
Fixture names and definitions will be stored in self.fixture_defs.
252+
253+
If some fixtures are Union fixtures, this node will become a "split" node
254+
and have children. If new fixtures are added to the closure after that,
255+
they will be added to the child nodes rather than self.
256+
257+
Note: when this method is used on an existing (already filled) root node,
258+
all of its internal structures (self._as_list and self.all_fixture_defs) are updated accordingly so that the
259+
facades used by pytest are still consistent.
260+
261+
:param initial_fixture_names:
262+
:param ignore_args: arguments to keep in the names but not to put in the fixture defs, because they correspond
263+
"direct parametrize"
264+
:return:
265+
"""
266+
self._build_closure(self.fixture_defs_mgr, initial_fixture_names, ignore_args=ignore_args)
267+
268+
# update fixture defs
269+
if self.all_fixture_defs is None:
270+
self.all_fixture_defs = self._get_all_fixture_defs()
271+
else:
272+
self.all_fixture_defs.update(self._get_all_fixture_defs())
273+
274+
# mark the fixture list as to be rebuilt (automatic next time one iterates on self)
275+
self._as_list = None
231276

232277
def is_closure_built(self):
233278
return self.fixture_defs is not None
@@ -242,12 +287,15 @@ def already_knows_fixture(self, fixture_name):
242287
return self.parent.already_knows_fixture(fixture_name)
243288

244289
def _build_closure(self,
245-
fixture_defs_mgr, # type: FixtureDefsCache
246-
initial_fixture_names # type: Iterable[str]
290+
fixture_defs_mgr, # type: FixtureDefsCache
291+
initial_fixture_names, # type: Iterable[str]
292+
ignore_args
247293
):
248294
"""
249295
250-
:param arg2fixturedefs: set of fixtures already known by the parent node
296+
:param fixture_defs_mgr:
297+
:param initial_fixture_names:
298+
:param ignore_args: arguments to keep in the names but not to put in the fixture defs
251299
:return: nothing (the input arg2fixturedefs is modified)
252300
"""
253301

@@ -267,6 +315,11 @@ def _build_closure(self,
267315
if self.already_knows_fixture(fixname):
268316
continue
269317

318+
# new ignore_args option in pytest 4.6+
319+
if fixname in ignore_args:
320+
self.add_required_fixture(fixname, None)
321+
continue
322+
270323
# else grab the fixture definition(s) for this fixture name for this test node id
271324
fixturedefs = fixture_defs_mgr.get_fixture_defs(fixname)
272325
if not fixturedefs:
@@ -289,7 +342,7 @@ def _build_closure(self,
289342

290343
# propagate WITH the pending
291344
self.split_and_build(fixture_defs_mgr, fixname, fixturedefs, alternative_f_names,
292-
pending_fixture_names)
345+
pending_fixture_names, ignore_args=ignore_args)
293346

294347
# empty the pending
295348
pending_fixture_names = []
@@ -325,7 +378,8 @@ def split_and_build(self,
325378
split_fixture_name, # type: str
326379
split_fixture_defs, # type: Tuple[FixtureDefinition]
327380
alternative_fixture_names, # type: List[str]
328-
pending_fixtures_list #
381+
pending_fixtures_list, #
382+
ignore_args
329383
):
330384
""" Declares that this node contains a union with alternatives (child nodes=subtrees) """
331385

@@ -344,7 +398,7 @@ def split_and_build(self,
344398
# create the child nodes
345399
for f in alternative_fixture_names:
346400
# create the child node
347-
new_c = FixtureClosureNode(self)
401+
new_c = FixtureClosureNode(parent_node=self)
348402
self.children[f] = new_c
349403

350404
# set the discarded fixture names
@@ -354,9 +408,9 @@ def split_and_build(self,
354408
# create a copy of the pending fixtures list and prepend the fixture used
355409
pending_for_child = copy(pending_fixtures_list)
356410
# (a) first propagate all child's dependencies
357-
new_c._build_closure(fixture_defs_mgr, [f])
411+
new_c._build_closure(fixture_defs_mgr, [f], ignore_args=ignore_args)
358412
# (b) then the ones required by parent
359-
new_c._build_closure(fixture_defs_mgr, pending_for_child)
413+
new_c._build_closure(fixture_defs_mgr, pending_for_child, ignore_args=ignore_args)
360414

361415
def has_split(self):
362416
return self.split_fixture_name is not None
@@ -522,8 +576,8 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
522576
print("Creating closure for %s:" % parentid)
523577

524578
fixture_defs_mger = FixtureDefsCache(fm, parentid)
525-
fixturenames_closure_node = FixtureClosureNode()
526-
fixturenames_closure_node.build_closure(fixture_defs_mger, _init_fixnames)
579+
fixturenames_closure_node = FixtureClosureNode(fixture_defs_mgr=fixture_defs_mger)
580+
fixturenames_closure_node.build_closure(_init_fixnames, ignore_args=ignore_args)
527581

528582
if _DEBUG:
529583
print("Closure for %s completed:" % parentid)
@@ -533,29 +587,30 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
533587
fixturenames_closure_node.to_list()
534588

535589
# FINALLY compare with the previous behaviour TODO remove when in 'production' ?
536-
if len(ignore_args) == 0:
537-
assert fixturenames_closure_node.get_all_fixture_defs() == ref_arg2fixturedefs
538-
# if fixturenames_closure_node.has_split():
539-
# # order might be changed
540-
# assert set((str(f) for f in fixturenames_closure_node)) == set(ref_fixturenames)
541-
# else:
542-
# # same order
543-
# if len(p_markers) < 2:
544-
# assert list(fixturenames_closure_node) == ref_fixturenames
545-
# else:
546-
# NOW different order happens all the time because of the "prepend" strategy in the closure building
547-
# which makes much more sense/intuition.
548-
assert set((str(f) for f in fixturenames_closure_node)) == set(ref_fixturenames)
590+
arg2fixturedefs = fixturenames_closure_node.get_all_fixture_defs()
591+
# if len(ignore_args) == 0:
592+
assert dict(arg2fixturedefs) == ref_arg2fixturedefs
593+
# if fixturenames_closure_node.has_split():
594+
# # order might be changed
595+
# assert set((str(f) for f in fixturenames_closure_node)) == set(ref_fixturenames)
596+
# else:
597+
# # same order
598+
# if len(p_markers) < 2:
599+
# assert list(fixturenames_closure_node) == ref_fixturenames
600+
# else:
601+
# NOW different order happens all the time because of the "prepend" strategy in the closure building
602+
# which makes much more sense/intuition.
603+
assert set((str(f) for f in fixturenames_closure_node)) == set(ref_fixturenames)
549604

550605
# and store our closure in the node
551606
# note as an alternative we could return a custom object in place of the ref_fixturenames
552607
# store_union_closure_in_node(fixturenames_closure_node, parentnode)
553608

554609
if LooseVersion(pytest.__version__) >= LooseVersion('3.7.0'):
555610
our_initial_names = sorted_fixturenames # initial_names
556-
return our_initial_names, fixturenames_closure_node, ref_arg2fixturedefs
611+
return our_initial_names, fixturenames_closure_node, arg2fixturedefs
557612
else:
558-
return fixturenames_closure_node, ref_arg2fixturedefs
613+
return fixturenames_closure_node, arg2fixturedefs
559614

560615

561616
# ------------ hack to store and retrieve our custom "closure" object

pytest_cases/tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,32 @@
1+
import sys
2+
import pytest
3+
4+
15
pytest_plugins = ["pytester"]
26
# In order to run meta-tests, see https://docs.pytest.org/en/latest/writing_plugins.html
7+
8+
9+
@pytest.hookimpl(trylast=True)
10+
def pytest_configure(config):
11+
"""
12+
In python 2, add
13+
14+
--ignore-glob='**/*py35*.py'
15+
16+
:param config:
17+
:return:
18+
"""
19+
if sys.version_info < (3, 5):
20+
print("Python < 3.5: ignoring test files containing 'py35'")
21+
OPT = ['**/*py35*.py']
22+
if config.option.ignore_glob is None:
23+
config.option.ignore_glob = OPT
24+
else:
25+
config.option.ignore_glob += OPT
26+
27+
# assert config.getoption('--ignore-glob') == OPT
28+
29+
30+
@pytest.fixture
31+
def global_fixture():
32+
return 'global'

pytest_cases/tests/issues/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import pytest
2+
3+
4+
@pytest.mark.asyncio
5+
@pytest.mark.parametrize('a', [0])
6+
async def test_x(a):
7+
assert True

0 commit comments

Comments
 (0)