Repository: smarie/python-pytest-cases (language: python)

Issue:
ScopeMismatch with parametrized cases
Hi, here's the last one. A bit hairier.

Example to reproduce:

***cases.py***
```python
from pytest_cases import parametrize

@parametrize(arg=(1,))
def case_parametrized(arg):
    return arg
```

***conftest.py***
```python
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
```

***test_scope_mismatch.py***
```python
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
```

Gives:
```
____________________________________________ ERROR at setup of test_scope_mismatch[parametrized-arg=1] _______________________________
ScopeMismatch: You tried to access the function scoped fixture parametrized with a session scoped request object, involved factories:
conftest.py:6:  def scope_mismatch(scope_mismatch_arg, request)
<makefun-gen-5>:1:  def scope_mismatch_arg(parametrized, request)
cases.py:6:  def case_parametrized(request)
```


Notice that this does **not** happen if the two fixtures are defined in the same module (either `conftest.py` or `test_scope_mismatch.py`). Also, this may be obscured by #309 or #310 if using AUTO cases.

It looks like the core of the issue is that the scope keyword argument is not passed to the .create calls here
https://github.com/smarie/python-pytest-cases/blob/ab3b7190d728b18512141b9f5f3a1c3dfc7cedf2/src/pytest_cases/fixture_parametrize_plus.py#L847-L872

In principle I can draft a PR.


Discussion / hints:
Sorry, this seems a bit more complicated than just passing on scope as I suggested above. It appears to be an issue of name conflict for case-parametrized fixtures that are defined within conftest.py. The name-conflict resolution here
https://github.com/smarie/python-pytest-cases/blob/ab3b7190d728b18512141b9f5f3a1c3dfc7cedf2/src/pytest_cases/case_parametrizer_new.py#L533-L565
does not actually fix the conflict because
- we're injecting the fixture in conftest itself;
- fixtures defined in conftest.py are available for all tests.

A solution would be to prepend "conftest_" to the `case_id` (either above, or in `fixture__creation.check_name_available`), but this only partly solves the issue. In fact, assume the following test-tree structure:
```
tests/
    conftest.py
    cases.py
    test_deeper/
        conftest.py
        test_scope_mismatch.py
```

with all the modules identical to my previous example but, test_deeper/conftest.py
```python
from pytest_cases import fixture, parametrize_with_cases

@fixture(scope='class')
@parametrize_with_cases('arg', cases='cases', scope='class')
def scope_mismatch_deeper(arg):
    return arg
```

While scope_mismatch_deeper is being parametrized, `target_host.__name__ == 'conftest'` (and not 'test_deeper.conftest') because test_deeper/conftest.py is currently being imported. This would simply duplicate the `'conftest_'+case_id` name, leading to another scope mismatch.

The ideal solution would be to have a `list_all_fixtures_in_conftests` function to be added to the existing_fixture_names. However, I know too little of pytest & pytest-cases to suggest an implementation.
Sorry for the spam. Perhaps a simpler solution than a `list_all_fixtures_in_conftests` would be to include also the scope in the base name to be checked (only if the fixture is injected in a conftest.py).

E.g.,
```python
if target_host.__name__ == 'conftest':
    case_id = 'conftest_' + case_id + '_with_scope_' + scope
```

This has the disadvantage of making names longer. If this sounds acceptable, I can also include it in the PR.

Fix patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 9c9feffe..b583a8f2 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,13 @@
 # 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
+   [#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


Test patch:
diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py
index 361b742e..63135a06 100644
--- a/src/pytest_cases/case_parametrizer_new.py
+++ b/src/pytest_cases/case_parametrizer_new.py
@@ -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__:
+        extra = target_host.__name__.replace('.', '_')
+        case_id = extra + '_' + case_id + '_with_scope_' + scope
+
     def name_changer(name, i):
         return name + '_' * i
 
diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py
index d70159c2..13a73431 100644
--- a/src/pytest_cases/fixture_parametrize_plus.py
+++ b/src/pytest_cases/fixture_parametrize_plus.py
@@ -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
                ):
@@ -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
@@ -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
                ):
@@ -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
@@ -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
@@ -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)
 
 
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..f6be43e3
--- /dev/null
+++ b/tests/cases/issues/issue_311/conftest.py
@@ -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
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..535ff9d2
--- /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..1e3ad83e
--- /dev/null
+++ b/tests/cases/issues/issue_311/test_issue_311/test_issue_311.py
@@ -0,0 +1,49 @@
+from pytest_cases import fixture, parametrize_with_cases
+
+
+@fixture
+@parametrize_with_cases('arg', cases='cases')
+def function_scoped(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_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]