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

Allow ovewriting a parametrized fixture while reusing the parent fixture's value #7736

Merged

Conversation

nicoddemus
Copy link
Member

Fix #1953

@nicoddemus nicoddemus added the needs backport applied to PRs, indicates that it should be ported to the current bug-fix branch label Sep 10, 2020
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

The fix has a flaw in that if metafunc.parametrize happens, it still tries the super fixture, which leads to the parametrization possibly being attempted again. You can see it by changing the overriding fixture (in test_foo.py) to be parametrized itself, e.g. @pytest.fixture(params=[10, 20]). Then it crashes with ValueError: duplicate 'foo'.

The fix I think is just to stop when parametrizing. I would add the above case as an additional test either way.

BTW, when reviewing I refactored the function a bit to be more understandable (to me). You can apply it, if you think it's better of course. Based on master and contains the fix.

diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py
index bf77d09f1..36cf15899 100644
--- a/src/_pytest/fixtures.py
+++ b/src/_pytest/fixtures.py
@@ -47,6 +47,7 @@ from _pytest.config import _PluggyPlugin
 from _pytest.config import Config
 from _pytest.config.argparsing import Parser
 from _pytest.deprecated import FILLFUNCARGS
+from _pytest.mark import Mark
 from _pytest.mark import ParameterSet
 from _pytest.outcomes import fail
 from _pytest.outcomes import TEST_OUTCOME
@@ -1529,34 +1530,53 @@ class FixtureManager:
         return initialnames, fixturenames_closure, arg2fixturedefs
 
     def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
+        def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+            if "argnames" in mark.kwargs:
+                argnames = mark.kwargs[
+                    "argnames"
+                ]  # type: Union[str, Tuple[str, ...], List[str]]
+            else:
+                argnames = mark.args[0]
+            if not isinstance(argnames, (tuple, list)):
+                argnames = [x.strip() for x in argnames.split(",") if x.strip()]
+            return argnames
+
         for argname in metafunc.fixturenames:
-            faclist = metafunc._arg2fixturedefs.get(argname)
-            if faclist:
-                fixturedef = faclist[-1]
+            # Get the FixtureDefs for the argname.
+            fixture_defs = metafunc._arg2fixturedefs.get(argname)
+            if not fixture_defs:
+                # Will raise FixtureLookupError at setup time.
+                continue
+
+            # The test itself parametrizes using this argname, give it
+            # precedence.
+            if any(
+                argname in get_parametrize_mark_argnames(mark)
+                for mark in metafunc.definition.iter_markers("parametrize")
+            ):
+                break
+
+            # In the common case we only look at the fixture def with the
+            # closest scope (last in the list). But if the fixture overrides
+            # another fixture, while requesting the super fixture, keep going
+            # in case the super fixture is parametrized (#1953).
+            for fixturedef in reversed(fixture_defs):
+                # Fixture is parametrized, apply it and stop.
                 if fixturedef.params is not None:
-                    markers = list(metafunc.definition.iter_markers("parametrize"))
-                    for parametrize_mark in markers:
-                        if "argnames" in parametrize_mark.kwargs:
-                            argnames = parametrize_mark.kwargs["argnames"]
-                        else:
-                            argnames = parametrize_mark.args[0]
+                    metafunc.parametrize(
+                        argname,
+                        fixturedef.params,
+                        indirect=True,
+                        scope=fixturedef.scope,
+                        ids=fixturedef.ids,
+                    )
+                    break
 
-                        if not isinstance(argnames, (tuple, list)):
-                            argnames = [
-                                x.strip() for x in argnames.split(",") if x.strip()
-                            ]
-                        if argname in argnames:
-                            break
-                    else:
-                        metafunc.parametrize(
-                            argname,
-                            fixturedef.params,
-                            indirect=True,
-                            scope=fixturedef.scope,
-                            ids=fixturedef.ids,
-                        )
-            else:
-                continue  # Will raise FixtureLookupError at setup time.
+                # Not requesting the overridden super fixture, stop.
+                if argname not in fixturedef.argnames:
+                    break
+
+                # Try super fixture, if any.
 
     def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
         # Separate parametrized setups.

@@ -0,0 +1,20 @@
Fix error when overwriting a fixture, reusing the original fixture value, when the overwritten fixture is parameterized:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Fix error when overwriting a fixture, reusing the original fixture value, when the overwritten fixture is parameterized:
Fix error when overwriting a fixture, reusing the original fixture value, when the overwritten fixture is parametrized:

😮 😮

@bluetech
Copy link
Member

I think the changes would be too risky for a backport, so I prefer we don't backport it.

@nicoddemus
Copy link
Member Author

The fix has a flaw in that if metafunc.parametrize happens, it still tries the super fixture, which leads to the parametrization possibly being attempted again. You can see it by changing the overriding fixture (in test_foo.py) to be parametrized itself, e.g. @pytest.fixture(params=[10, 20]). Then it crashes with ValueError: duplicate 'foo'.

Ahh good catch, thanks! I will apply your improvements and add a new test (it really amazes me that, besides the huge number of test cases and scenarios we have, we still sometimes have gaps on them).

BTW, when reviewing I refactored the function a bit to be more understandable (to me). You can apply it, if you think it's better of course. Based on master and contains the fix.

Much clearer, thanks, I will apply them. 👍

I think the changes would be too risky for a backport, so I prefer we don't backport it.

Fair enough. 👍

@nicoddemus nicoddemus removed the needs backport applied to PRs, indicates that it should be ported to the current bug-fix branch label Sep 11, 2020
@nicoddemus nicoddemus force-pushed the extend-fixture-parametrize-1953 branch 2 times, most recently from fa1e088 to cf43a63 Compare September 11, 2020 17:20
argname in get_parametrize_mark_argnames(mark)
for mark in metafunc.definition.iter_markers("parametrize")
):
continue
Copy link
Member Author

Choose a reason for hiding this comment

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

Noticed this should be continue instead of break, otherwise we would skip evaluating the rest of the argnames for possible fixture parametrization (test_override_parametrize_fixture_and_indirect demonstrates this).

@nicoddemus
Copy link
Member Author

Applied the changes and tests as requested, thanks again for the great review @bluetech!

@nicoddemus nicoddemus force-pushed the extend-fixture-parametrize-1953 branch from 78b7219 to e36adba Compare September 11, 2020 19:53
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Great, LGTM!

src/_pytest/fixtures.py Show resolved Hide resolved
@nicoddemus nicoddemus merged commit ec58ae5 into pytest-dev:master Sep 12, 2020
@nicoddemus nicoddemus deleted the extend-fixture-parametrize-1953 branch September 12, 2020 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extending a fixture does not keep its params
2 participants