Repository navigation
Parametrization with Variables Unexpectedly Changes Fixture Scope from Class-Level to Function-Level #12146
Description
Activity
Slightly simpler example:
import pytest @pytest.mark.usefixtures("fixt") class TestSmoke: def test_1(self): assert True def test_2(self): assert True
import pytest def pytest_generate_tests(metafunc): if "fixt" in metafunc.fixturenames: arg = "latest" metafunc.parametrize( "fixt", argvalues=[ ( arg, # "latest", ) ], indirect=True, scope="class", ) @pytest.fixture def fixt(request): yield
with
--setup-show:tests/test_app.py SETUP F fixt[('latest',)] tests/test_app.py::TestSmoke::test_1[fixt0] (fixtures used: fixt, request). TEARDOWN F fixt[('latest',)] SETUP F fixt[('latest',)] tests/test_app.py::TestSmoke::test_2[fixt0] (fixtures used: fixt, request). TEARDOWN F fixt[('latest',)]
but when commenting out
arg,and using"latest",instead:tests/test_app.py SETUP F fixt[('latest',)] tests/test_app.py::TestSmoke::test_1[fixt0] (fixtures used: fixt, request). tests/test_app.py::TestSmoke::test_2[fixt0] (fixtures used: fixt, request). TEARDOWN F fixt[('latest',)]
I'm... completely flabbergasted. How is this even possible‽
Reacted by Yogendra Singh@The-Compiler i suspect/fear we somewhere match object identities
when the string is passed as a constant, its likely the python interpreter render it as single tuple value instead of creating it
if that suspicion holds true, then can expect the behaviour as you observe
i jsut made a small test in ipython on python 3.12
In [1]: import dis In [2]: def fun(): ...: a = "" ...: return (a,) ...: In [3]: print(dis.dis(fun)) 1 0 RESUME 0 2 2 LOAD_CONST 1 ('') 4 STORE_FAST 0 (a) 3 6 LOAD_FAST 0 (a) 8 BUILD_TUPLE 1 10 RETURN_VALUE None In [4]: def fun2(): ...: return ("",) ...: In [5]: print(dis.dis(fun2)) 1 0 RESUME 0 2 2 RETURN_CONST 1 (('',)) Noneits possible #11257 fixes this
- addedtype: bugproblem that needs to be addressedproblem that needs to be addressedtopic: fixturesanything involving fixtures directly or indirectlyanything involving fixtures directly or indirectly
on Mar 23, 2024 Any update on this?
When
test2requests the value ofsetupfixture ,setupchecks if its cached param key (which is that oftest1) is the same as the one coming fromtest2. It does this check usingisoperator which tests exact object equality.In the
("chrome", "windows", "latest")scenario, as this value is a literal or const object (as showed above by @RonnyPfannschmidt ), Python reuses the same object created inpytest_generate_tests(test1)fortest2soistest passes and the cached value is returned.But in
(os_name, browser_name, browser_version)scenario, two separate objects are created fortest1andtest2so the check doesn't pass. As a result, the fixture tears down and re-execute itself.The check takes place here:
pytest/src/_pytest/fixtures.py
Lines 1056 to 1067 in 400b22d
my_cache_key = self.cache_key(request) if self.cached_result is not None: cache_key = self.cached_result[1] # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497). if my_cache_key is cache_key: if self.cached_result[2] is not None: exc, exc_tb = self.cached_result[2] raise exc.with_traceback(exc_tb) else: result = self.cached_result[0] return result Looks like this was fixed by #12600 (merged 2024-07-17), which replaced the
iscomparison of the fixture cache key with==— falling back toisfor objects that reject==(per #6497).Verified on current
main(67a174f, Python 3.11):- The-Compiler's reduced repro (
(arg,)with a localarg = "latest", class-scoped fixture, two tests) → fixture is set up once and torn down once for the whole class. - The original three-tuple case from the report → same behavior.
Likely safe to close as a duplicate of #6962.
- The-Compiler's reduced repro (
I have a
setupfixture that I've parameterized with class-level scope. When I directly specify the argument values as[("chrome", "windows", "latest")], it works fine, maintaining the class-level scope. However, when I try to use variables to set these argument values, I've noticed that the scope unexpectedly shifts to function-level.tests/test_app.py
Scenario 1 with hardcoded values
tests/conftest.py
Output
Scenario 2 with variable values
tests/conftest.py
Output