Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ScopeMismatch with parametrized cases #317

Merged
merged 11 commits into from
Nov 10, 2023
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

- Fixed `ScopeMismatch` with parametrized cases in non-trivial test
smarie marked this conversation as resolved.
Show resolved Hide resolved
trees. `scope` is now correctly handled for (i) `fixture` cases, and
(ii) fixtures defined in `conftest.py` files at any depth. Fixes
[#311](https://github.com/smarie/python-pytest-cases/issues/311). PR
[#317](https://github.com/smarie/python-pytest-cases/pull/317) by [@michele-riva](https://github.com/michele-riva).

### 3.8.0 - async, generators and strict-markers

- `@fixture` and `@parametrize` are now async and generator aware. Fixes
Expand Down
15 changes: 15 additions & 0 deletions src/pytest_cases/case_parametrizer_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,21 @@ def get_or_create_case_fixture(case_id, # type: str
# Fix the problem with "case_foo(foo)" leading to the generated fixture having the same name
existing_fixture_names += fixtures_in_cases_module

# If the fixture will be injected in a conftest, make sure its name
# is unique. Include also its scope to avoid conflicts. See #311.
# Notice target_host.__name__ may just be 'conftest' when tests
# are simple modules or a more complicated fully qualified name
# when the test suite is a package (i.e., with __init__.py). For
# example, target_host.__name__ would be 'tests.conftest' when
# executing tests from within 'base' in the following tree:
# base/
# tests/
# __init__.py
# conftest.py
if 'conftest' in target_host.__name__:
michele-riva marked this conversation as resolved.
Show resolved Hide resolved
extra = target_host.__name__.replace('.', '_')
case_id = extra + '_' + case_id + '_with_scope_' + scope

def name_changer(name, i):
return name + '_' * i

Expand Down
10 changes: 7 additions & 3 deletions src/pytest_cases/fixture_parametrize_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ def create(cls,
i, # type: int
argvalue, # type: Any
id, # type: Union[str, Callable]
scope=None, # type: str
hook=None, # type: Callable
debug=False # type: bool
):
Expand Down Expand Up @@ -362,7 +363,7 @@ def create(cls,

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

# Create the alternative
argvals = (argvalue,) if nb_params == 1 else argvalue
Expand Down Expand Up @@ -426,6 +427,7 @@ def create(cls,
to_i, # type: int
argvalues, # type: Any
ids, # type: Union[Sequence[str], Callable]
scope="function", # type: str
hook=None, # type: Callable
debug=False # type: bool
):
Expand Down Expand Up @@ -493,8 +495,8 @@ def create(cls,
else:
ids = [mini_idvalset(argnames, vals, i) for i, vals in enumerate(unmarked_argvalues)]

_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids, hook=hook,
debug=debug)
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids,
scope=scope, hook=hook, debug=debug)

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


Expand Down
6 changes: 6 additions & 0 deletions tests/cases/issues/issue_311/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pytest_cases import parametrize


@parametrize(arg=(1,))
def case_parametrized(arg):
return arg
9 changes: 9 additions & 0 deletions tests/cases/issues/issue_311/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pytest_cases import fixture, parametrize_with_cases


@fixture(scope='session')
@parametrize_with_cases('arg', cases='cases', scope='session')
def scope_mismatch(arg):
return [arg]

session_scoped = scope_mismatch
7 changes: 7 additions & 0 deletions tests/cases/issues/issue_311/test_issue_311/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pytest_cases import fixture, parametrize_with_cases


@fixture(scope='class')
@parametrize_with_cases('arg', cases='cases', scope='class')
def class_scoped(arg):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture does not appear to be used in the test. To be sure that the test works (and not only the pytest initial collection/setup), I would recommend to use this fixture somewhere in the test. In particular, the test does not at all validate that the scope is actually used.

This might require to create a second test, and to wrap both in a class..

return [arg]
49 changes: 49 additions & 0 deletions tests/cases/issues/issue_311/test_issue_311/test_issue_311.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from pytest_cases import fixture, parametrize_with_cases


@fixture
@parametrize_with_cases('arg', cases='cases')
def function_scoped(arg):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for the class-scoped: could you imagine a way to include this fixture in the test too, and in particular a way to be sure that the scope is correctly taken into account for all 3 ?

return [arg]

# This tests would fail with a ScopeMismatch
# during collection before #317
def test_scope_mismatch_collection(scope_mismatch):
assert scope_mismatch == [1]

def test_scopes(session_scoped, function_scoped, class_scoped):
session_scoped.append(2)
function_scoped.append(2)
class_scoped.append(2)
assert session_scoped == [1, 2]
assert function_scoped == [1, 2]
assert class_scoped == [1, 2]

def test_scopes_again(session_scoped, function_scoped, class_scoped):
session_scoped.append(3)
function_scoped.append(3)
class_scoped.append(3)
assert session_scoped == [1, 2, 3]
assert function_scoped == [1, 3]
assert class_scoped == [1, 3]


class TestScopesInClass:

def test_scopes_in_class(self, session_scoped,
function_scoped, class_scoped):
session_scoped.append(4)
function_scoped.append(4)
class_scoped.append(4)
assert session_scoped == [1, 2, 3, 4]
assert function_scoped == [1, 4]
assert class_scoped == [1, 4]

def test_scopes_in_class_again(self, session_scoped,
function_scoped, class_scoped):
session_scoped.append(5)
function_scoped.append(5)
class_scoped.append(5)
assert session_scoped == [1, 2, 3, 4, 5]
assert function_scoped == [1, 5]
assert class_scoped == [1, 4, 5]
Loading