From b6d1adbdfc7c0a5c9e2e1ae1a699cb388fd56f4f Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 13 Sep 2023 09:41:01 +0200 Subject: [PATCH 01/10] tests: add test for issue #311 --- tests/cases/issues/issue_311/cases.py | 6 ++++++ tests/cases/issues/issue_311/conftest.py | 7 +++++++ .../cases/issues/issue_311/test_issue_311/conftest.py | 7 +++++++ .../issues/issue_311/test_issue_311/test_issue_311.py | 11 +++++++++++ 4 files changed, 31 insertions(+) create mode 100644 tests/cases/issues/issue_311/cases.py create mode 100644 tests/cases/issues/issue_311/conftest.py create mode 100644 tests/cases/issues/issue_311/test_issue_311/conftest.py create mode 100644 tests/cases/issues/issue_311/test_issue_311/test_issue_311.py diff --git a/tests/cases/issues/issue_311/cases.py b/tests/cases/issues/issue_311/cases.py new file mode 100644 index 00000000..cf72361a --- /dev/null +++ b/tests/cases/issues/issue_311/cases.py @@ -0,0 +1,6 @@ +from pytest_cases import parametrize + + +@parametrize(arg=(1,)) +def case_parametrized(arg): + return arg diff --git a/tests/cases/issues/issue_311/conftest.py b/tests/cases/issues/issue_311/conftest.py new file mode 100644 index 00000000..8dee3860 --- /dev/null +++ b/tests/cases/issues/issue_311/conftest.py @@ -0,0 +1,7 @@ +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 diff --git a/tests/cases/issues/issue_311/test_issue_311/conftest.py b/tests/cases/issues/issue_311/test_issue_311/conftest.py new file mode 100644 index 00000000..c5ca3dae --- /dev/null +++ b/tests/cases/issues/issue_311/test_issue_311/conftest.py @@ -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): + return arg diff --git a/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py new file mode 100644 index 00000000..044caa0f --- /dev/null +++ b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py @@ -0,0 +1,11 @@ +from pytest_cases import fixture, parametrize_with_cases + + +@fixture +@parametrize_with_cases('arg', cases='cases') +def function_scoped(arg): + return arg + + +def test_scope_mismatch(scope_mismatch): + assert scope_mismatch == 1 From 3975f3c2a1095aebb4ab2a0498777a45c5b28e60 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 13 Sep 2023 09:42:02 +0200 Subject: [PATCH 02/10] fix: propagate scope in ...ParamAlternative --- src/pytest_cases/fixture_parametrize_plus.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py index 644ebefd..6f92412c 100644 --- a/src/pytest_cases/fixture_parametrize_plus.py +++ b/src/pytest_cases/fixture_parametrize_plus.py @@ -303,6 +303,7 @@ def create(cls, i, # type: int argvalue, # type: Any id, # type: Union[str, Callable] + scope='function', # type: str hook=None, # type: Callable debug=False # type: bool ): @@ -349,7 +350,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 @@ -413,6 +414,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 ): @@ -480,8 +482,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 @@ -861,7 +863,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, - hook=hook, debug=debug) + scope=scope, hook=hook, debug=debug) else: # If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later _ids = ids[from_i:to_i] if explicit_ids_to_use else ids @@ -869,7 +871,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, - hook=hook, debug=debug) + scope=scope, hook=hook, debug=debug) def _create_fixture_ref_alt(union_name, test_func, i): # noqa From 5a061188882a1bd876d314d082bbd1fef203ef47 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 13 Sep 2023 09:45:18 +0200 Subject: [PATCH 03/10] 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. --- src/pytest_cases/case_parametrizer_new.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py index 361b742e..19e02917 100644 --- a/src/pytest_cases/case_parametrizer_new.py +++ b/src/pytest_cases/case_parametrizer_new.py @@ -557,6 +557,11 @@ 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. + if target_host.__name__ == 'conftest': + case_id = 'conftest_' + case_id + '_with_scope_' + scope + def name_changer(name, i): return name + '_' * i From 0d760611b0ec4de3c474cc9c85e80a18db392658 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 14 Sep 2023 07:24:02 +0200 Subject: [PATCH 04/10] 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. --- src/pytest_cases/case_parametrizer_new.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py index 19e02917..4d670fc0 100644 --- a/src/pytest_cases/case_parametrizer_new.py +++ b/src/pytest_cases/case_parametrizer_new.py @@ -559,8 +559,9 @@ def get_or_create_case_fixture(case_id, # type: str # If the fixture will be injected in a conftest, make sure its name # is unique. Include also its scope to avoid conflicts. See #311. - if target_host.__name__ == 'conftest': - case_id = 'conftest_' + case_id + '_with_scope_' + scope + if 'conftest' in target_host.__name__: + extra = target_host.__name__.replace('.', '_') + case_id = extra + '_' + case_id + '_with_scope_' + scope def name_changer(name, i): return name + '_' * i From 02484e8f7a3a6d74cce814e3367ca832df80e037 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 8 Nov 2023 17:59:18 +0100 Subject: [PATCH 05/10] 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. --- src/pytest_cases/fixture_parametrize_plus.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py index 5159f665..df743521 100644 --- a/src/pytest_cases/fixture_parametrize_plus.py +++ b/src/pytest_cases/fixture_parametrize_plus.py @@ -316,7 +316,7 @@ def create(cls, i, # type: int argvalue, # type: Any id, # type: Union[str, Callable] - scope='function', # type: str + scope=None, # type: str hook=None, # type: Callable debug=False # type: bool ): @@ -427,7 +427,7 @@ def create(cls, to_i, # type: int argvalues, # type: Any ids, # type: Union[Sequence[str], Callable] - scope='function', # type: str + scope="function", # type: str hook=None, # type: Callable debug=False # type: bool ): @@ -876,7 +876,8 @@ 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=scope, hook=hook, debug=debug) + scope=scope or "function", + hook=hook, debug=debug) else: # If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later _ids = ids[from_i:to_i] if explicit_ids_to_use else ids @@ -884,7 +885,8 @@ 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=scope, hook=hook, debug=debug) + scope=scope or "function", + hook=hook, debug=debug) def _create_fixture_ref_alt(union_name, test_func, i): # noqa From 484743a61a3898e2a2d91e16db2713101d9dae74 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 9 Nov 2023 09:37:53 +0100 Subject: [PATCH 06/10] More explicit None scope replacement As suggested by @smarie --- src/pytest_cases/fixture_parametrize_plus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py index df743521..13a73431 100644 --- a/src/pytest_cases/fixture_parametrize_plus.py +++ b/src/pytest_cases/fixture_parametrize_plus.py @@ -876,7 +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=scope or "function", + 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 @@ -885,7 +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=scope or "function", + scope="function" if scope is None else scope, hook=hook, debug=debug) From 2835d3c830d02d9dfc97c9755a68c62fbdda33e4 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 9 Nov 2023 10:05:42 +0100 Subject: [PATCH 07/10] Add note and example for conftest qualname --- src/pytest_cases/case_parametrizer_new.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py index 4d670fc0..63135a06 100644 --- a/src/pytest_cases/case_parametrizer_new.py +++ b/src/pytest_cases/case_parametrizer_new.py @@ -559,6 +559,15 @@ def get_or_create_case_fixture(case_id, # type: str # 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__: extra = target_host.__name__.replace('.', '_') case_id = extra + '_' + case_id + '_with_scope_' + scope From 8487bbabc5227df3d32be7b14e22596387eca53d Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 9 Nov 2023 11:10:59 +0100 Subject: [PATCH 08/10] Test correctness of scopes As suggested by @smarie --- tests/cases/issues/issue_311/conftest.py | 4 +- .../issue_311/test_issue_311/conftest.py | 2 +- .../test_issue_311/test_issue_311.py | 44 +++++++++++++++++-- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/cases/issues/issue_311/conftest.py b/tests/cases/issues/issue_311/conftest.py index 8dee3860..f6be43e3 100644 --- a/tests/cases/issues/issue_311/conftest.py +++ b/tests/cases/issues/issue_311/conftest.py @@ -4,4 +4,6 @@ @fixture(scope='session') @parametrize_with_cases('arg', cases='cases', scope='session') def scope_mismatch(arg): - return arg + return [arg] + +session_scoped = scope_mismatch diff --git a/tests/cases/issues/issue_311/test_issue_311/conftest.py b/tests/cases/issues/issue_311/test_issue_311/conftest.py index c5ca3dae..535ff9d2 100644 --- a/tests/cases/issues/issue_311/test_issue_311/conftest.py +++ b/tests/cases/issues/issue_311/test_issue_311/conftest.py @@ -4,4 +4,4 @@ @fixture(scope='class') @parametrize_with_cases('arg', cases='cases', scope='class') def class_scoped(arg): - return arg + return [arg] diff --git a/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py index 044caa0f..1e3ad83e 100644 --- a/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py +++ b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py @@ -4,8 +4,46 @@ @fixture @parametrize_with_cases('arg', cases='cases') def function_scoped(arg): - return arg + 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_scope_mismatch(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] From b8b32092dad59f29f5d17c1d7760f519a68c379a Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Thu, 9 Nov 2023 16:56:09 +0100 Subject: [PATCH 09/10] Add changelog for #317 --- docs/changelog.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 9c9feffe..5f286e96 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog + - Fixed `ScopeMismatch` with parametrized cases in non-trivial test + 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 From 9ed516dbdd1b9e547685cebdf813c2c535282296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylvain=20Mari=C3=A9?= Date: Fri, 10 Nov 2023 13:21:42 +0100 Subject: [PATCH 10/10] Update docs/changelog.md --- docs/changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 5f286e96..b583a8f2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,7 @@ # Changelog +### 3.8.1 - bugfixes + - Fixed `ScopeMismatch` with parametrized cases in non-trivial test trees. `scope` is now correctly handled for (i) `fixture` cases, and (ii) fixtures defined in `conftest.py` files at any depth. Fixes