Skip to content

Commit d3cf756

Browse files
smarieSylvain MARIE
andauthored
[WIP] Support removal from fixture closure tree to improve compatibility with other plugins (#273)
* Initial test to reproduce the bug. * Improved compatibility with other `pytest` plugins, in particular `pytest-repeat`, by support removal from fixture closure tree. Fixed #269 * Improved readability of fix for future maintenance. Fixed test. Co-authored-by: Sylvain MARIE <[email protected]>
1 parent 2383167 commit d3cf756

File tree

3 files changed

+96
-11
lines changed

3 files changed

+96
-11
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### 3.6.12 - type hint fix
44

5+
- Improved compatibility with other `pytest` plugins, in particular `pytest-repeat`, by support removal from fixture closure tree. Fixed [#269](https://github.com/smarie/python-pytest-cases/issues/269).
56
- Fixed type hint errors detected by `pyright`. Fixed [#270](https://github.com/smarie/python-pytest-cases/issues/270)
67

78
### 3.6.11 - bugfix for pytest-xdist and `get_all_cases` API improvement

src/pytest_cases/plugin.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -633,23 +633,47 @@ def __setitem__(self, i, o):
633633
# full_replace = i == slice(None, None, None)
634634
# except: # noqa
635635
# full_replace = False
636+
637+
# Get the existing value(s) that we wish to replace
636638
ref = list(self)[i]
637639

638640
if o == ref:
639641
# no change at all: of course we accept.
640642
return
643+
644+
if not isinstance(i, slice):
645+
# In-place change of a single item: let's be conservative and reject for now
646+
# if i == 0:
647+
# self.remove(ref)
648+
# self.insert(0, o)
649+
# elif i == len(self) - 1:
650+
# self.remove(ref)
651+
# self.append(o)
652+
# else:
653+
raise NotImplementedError("Replacing an element in a super fixture closure is not currently implemented. "
654+
"Please report this issue to the `pytest-cases` project.")
641655
else:
642-
if isinstance(i, slice):
643-
if set(o) == set(ref):
644-
# a change is required in the order of fixtures. Ignore but continue
645-
warn("WARNING: An attempt was made to reorder a super fixture closure with unions. This is not yet "
646-
"supported since the partitions use subsets of the fixtures ; please report it so that we can "
647-
"find a suitable solution for your need.")
648-
return
649-
650-
# At least one change of fixture name is required: not supported, and reject as it is
651-
raise NotImplementedError("It is not possible to replace an element in a super fixture closure,"
652-
"as the partitions inside it do not have the same size")
656+
# Replacement of multiple items at once: support reordering (ignored) and removal (actually done)
657+
new_set = set(o)
658+
ref_set = set(ref)
659+
if new_set == ref_set:
660+
# A change is required in the order of fixtures. Ignore but continue
661+
warn("WARNING: An attempt was made to reorder a super fixture closure with unions. This is not yet "
662+
"supported since the partitions use subsets of the fixtures ; please report it so that we can "
663+
"find a suitable solution for your need.")
664+
return
665+
666+
added = new_set.difference(ref_set)
667+
removed = ref_set.difference(new_set)
668+
if len(added) == 0:
669+
# Pure removal: ok.
670+
self.remove_all(removed)
671+
return
672+
else:
673+
# self.append_all(added)
674+
# Rather be conservative for now
675+
raise NotImplementedError("Adding elements to a super fixture closure with a slice is not currently"
676+
"implemented. Please report this issue to the `pytest-cases` project.")
653677

654678
def __delitem__(self, i):
655679
self.remove(self[i])
@@ -695,6 +719,14 @@ def insert(self, index, fixture_name):
695719
# Finally update self.fixture_defs so that the "list" view reflects the changes in self.tree
696720
self._update_fixture_defs()
697721

722+
def append_all(self, fixture_names):
723+
"""Append various fixture names to the closure"""
724+
# appending is natively supported in our tree growing method
725+
self.tree.build_closure(tuple(fixture_names))
726+
727+
# Finally update self.fixture_defs so that the "list" view reflects the changes in self.tree
728+
self._update_fixture_defs()
729+
698730
def remove(self, value):
699731
"""
700732
Try to transparently support removal. Note: since the underlying structure is a tree,
@@ -709,6 +741,14 @@ def remove(self, value):
709741
# update fixture defs
710742
self._update_fixture_defs()
711743

744+
def remove_all(self, values):
745+
"""Multiple `remove` operations at once."""
746+
# remove in the tree
747+
self.tree.remove_fixtures(tuple(values))
748+
749+
# update fixture defs
750+
self._update_fixture_defs()
751+
712752

713753
def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
714754
"""
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import pytest
2+
from pytest_cases import fixture, parametrize
3+
4+
5+
@pytest.fixture
6+
def __my_repeat_step_number(request):
7+
return request.param
8+
9+
10+
@pytest.hookimpl(trylast=True)
11+
def pytest_generate_tests(metafunc):
12+
"""This hook and fixture above are similar to what happens in pytest-repeat.
13+
See https://github.com/smarie/python-pytest-cases/issues/269
14+
"""
15+
if metafunc.function is test_repeat:
16+
metafunc.fixturenames.append("__my_repeat_step_number")
17+
18+
def make_progress_id(i, n=2):
19+
return '{0}-{1}'.format(i + 1, n)
20+
21+
metafunc.parametrize(
22+
'__my_repeat_step_number',
23+
range(2),
24+
indirect=True,
25+
ids=make_progress_id
26+
)
27+
28+
29+
@fixture
30+
def my_fix():
31+
return 2
32+
33+
34+
@parametrize("arg", (my_fix,))
35+
def test_repeat(arg):
36+
assert arg == 2
37+
38+
39+
def test_synthesis(module_results_dct):
40+
"""Make sure that two tests were created."""
41+
assert list(module_results_dct) == [
42+
"test_repeat[my_fix-1-2]",
43+
"test_repeat[my_fix-2-2]"
44+
]

0 commit comments

Comments
 (0)