Skip to content

Commit ac7f3c3

Browse files
michele-rivasmarie
andauthored
Fix ScopeMismatch with parametrized cases (#317)
* tests: add test for issue #311 * fix: propagate scope in ...ParamAlternative * fix: mangle fixture name for conftest Fixtures that are to be injected in conftest.py will be available globally. If we do not include this information in the name of the autogenerated fixtures, we may risk causing a conflict if another test/case/conftest uses parametrization on the same tests. * fix: avoid conflict also if __init__.py exists When conftest is in a package rather than in a mere folder, its name is "fully qualified". Perhaps there would be no need to actually add the scope in this case, but better safe than sorry. * Do not pass on None as scope Make sure that the default scope always is "function" so as to avoid issues with pytest <= 6, which 'translates' the scope string kwarg into an index from a list. The list does not contain None. * More explicit None scope replacement As suggested by @smarie * Add note and example for conftest qualname * Test correctness of scopes As suggested by @smarie * Add changelog for #317 * Update docs/changelog.md --------- Co-authored-by: Sylvain Marié <[email protected]>
1 parent c0f0a43 commit ac7f3c3

File tree

7 files changed

+101
-3
lines changed

7 files changed

+101
-3
lines changed

docs/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
### 3.8.1 - bugfixes
4+
5+
- Fixed `ScopeMismatch` with parametrized cases in non-trivial test
6+
trees. `scope` is now correctly handled for (i) `fixture` cases, and
7+
(ii) fixtures defined in `conftest.py` files at any depth. Fixes
8+
[#311](https://github.com/smarie/python-pytest-cases/issues/311). PR
9+
[#317](https://github.com/smarie/python-pytest-cases/pull/317) by [@michele-riva](https://github.com/michele-riva).
10+
311
### 3.8.0 - async, generators and strict-markers
412

513
- `@fixture` and `@parametrize` are now async and generator aware. Fixes

src/pytest_cases/case_parametrizer_new.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,21 @@ def get_or_create_case_fixture(case_id, # type: str
557557
# Fix the problem with "case_foo(foo)" leading to the generated fixture having the same name
558558
existing_fixture_names += fixtures_in_cases_module
559559

560+
# If the fixture will be injected in a conftest, make sure its name
561+
# is unique. Include also its scope to avoid conflicts. See #311.
562+
# Notice target_host.__name__ may just be 'conftest' when tests
563+
# are simple modules or a more complicated fully qualified name
564+
# when the test suite is a package (i.e., with __init__.py). For
565+
# example, target_host.__name__ would be 'tests.conftest' when
566+
# executing tests from within 'base' in the following tree:
567+
# base/
568+
# tests/
569+
# __init__.py
570+
# conftest.py
571+
if 'conftest' in target_host.__name__:
572+
extra = target_host.__name__.replace('.', '_')
573+
case_id = extra + '_' + case_id + '_with_scope_' + scope
574+
560575
def name_changer(name, i):
561576
return name + '_' * i
562577

src/pytest_cases/fixture_parametrize_plus.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ def create(cls,
316316
i, # type: int
317317
argvalue, # type: Any
318318
id, # type: Union[str, Callable]
319+
scope=None, # type: str
319320
hook=None, # type: Callable
320321
debug=False # type: bool
321322
):
@@ -362,7 +363,7 @@ def create(cls,
362363

363364
# Create the fixture. IMPORTANT auto_simplify=True : we create a NON-parametrized fixture.
364365
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=(argvalue,),
365-
hook=hook, auto_simplify=True, debug=debug)
366+
scope=scope, hook=hook, auto_simplify=True, debug=debug)
366367

367368
# Create the alternative
368369
argvals = (argvalue,) if nb_params == 1 else argvalue
@@ -426,6 +427,7 @@ def create(cls,
426427
to_i, # type: int
427428
argvalues, # type: Any
428429
ids, # type: Union[Sequence[str], Callable]
430+
scope="function", # type: str
429431
hook=None, # type: Callable
430432
debug=False # type: bool
431433
):
@@ -493,8 +495,8 @@ def create(cls,
493495
else:
494496
ids = [mini_idvalset(argnames, vals, i) for i, vals in enumerate(unmarked_argvalues)]
495497

496-
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids, hook=hook,
497-
debug=debug)
498+
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids,
499+
scope=scope, hook=hook, debug=debug)
498500

499501
# Create the corresponding alternative
500502
# note: as opposed to SingleParamAlternative, no need to move the custom id/marks to the ParamAlternative
@@ -874,6 +876,7 @@ def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa
874876
return SingleParamAlternative.create(new_fixture_host=fh, test_func=test_func,
875877
param_union_name=union_name, argnames=argnames, i=i,
876878
argvalue=marked_argvalues[i], id=_id,
879+
scope="function" if scope is None else scope,
877880
hook=hook, debug=debug)
878881
else:
879882
# If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later
@@ -882,6 +885,7 @@ def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa
882885
return MultiParamAlternative.create(new_fixture_host=fh, test_func=test_func,
883886
param_union_name=union_name, argnames=argnames, from_i=from_i,
884887
to_i=to_i, argvalues=marked_argvalues[from_i:to_i], ids=_ids,
888+
scope="function" if scope is None else scope,
885889
hook=hook, debug=debug)
886890

887891

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pytest_cases import parametrize
2+
3+
4+
@parametrize(arg=(1,))
5+
def case_parametrized(arg):
6+
return arg
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pytest_cases import fixture, parametrize_with_cases
2+
3+
4+
@fixture(scope='session')
5+
@parametrize_with_cases('arg', cases='cases', scope='session')
6+
def scope_mismatch(arg):
7+
return [arg]
8+
9+
session_scoped = scope_mismatch
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pytest_cases import fixture, parametrize_with_cases
2+
3+
4+
@fixture(scope='class')
5+
@parametrize_with_cases('arg', cases='cases', scope='class')
6+
def class_scoped(arg):
7+
return [arg]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from pytest_cases import fixture, parametrize_with_cases
2+
3+
4+
@fixture
5+
@parametrize_with_cases('arg', cases='cases')
6+
def function_scoped(arg):
7+
return [arg]
8+
9+
# This tests would fail with a ScopeMismatch
10+
# during collection before #317
11+
def test_scope_mismatch_collection(scope_mismatch):
12+
assert scope_mismatch == [1]
13+
14+
def test_scopes(session_scoped, function_scoped, class_scoped):
15+
session_scoped.append(2)
16+
function_scoped.append(2)
17+
class_scoped.append(2)
18+
assert session_scoped == [1, 2]
19+
assert function_scoped == [1, 2]
20+
assert class_scoped == [1, 2]
21+
22+
def test_scopes_again(session_scoped, function_scoped, class_scoped):
23+
session_scoped.append(3)
24+
function_scoped.append(3)
25+
class_scoped.append(3)
26+
assert session_scoped == [1, 2, 3]
27+
assert function_scoped == [1, 3]
28+
assert class_scoped == [1, 3]
29+
30+
31+
class TestScopesInClass:
32+
33+
def test_scopes_in_class(self, session_scoped,
34+
function_scoped, class_scoped):
35+
session_scoped.append(4)
36+
function_scoped.append(4)
37+
class_scoped.append(4)
38+
assert session_scoped == [1, 2, 3, 4]
39+
assert function_scoped == [1, 4]
40+
assert class_scoped == [1, 4]
41+
42+
def test_scopes_in_class_again(self, session_scoped,
43+
function_scoped, class_scoped):
44+
session_scoped.append(5)
45+
function_scoped.append(5)
46+
class_scoped.append(5)
47+
assert session_scoped == [1, 2, 3, 4, 5]
48+
assert function_scoped == [1, 5]
49+
assert class_scoped == [1, 4, 5]

0 commit comments

Comments
 (0)