Add pytest.fixture(indirect=...) argument

closes #10101
This commit is contained in:
Michał 'Khorne' Lowas-Rzechonek 2022-06-20 12:41:41 +02:00
parent 4414c4adae
commit 720c4585b5
7 changed files with 163 additions and 11 deletions

View File

@ -238,6 +238,7 @@ Michael Goerz
Michael Krebs
Michael Seifert
Michal Wajszczuk
Michał Lowas-Rzechonek
Michał Zięba
Mihai Capotă
Mike Hoyle (hoylemd)

View File

@ -0,0 +1,2 @@
Add an optional ``indirect`` argument for ``pytest.fixture`` to make all
parametrizations of that fixture indirect by default.

View File

@ -372,6 +372,24 @@ test:
This can be used, for example, to do more expensive setup at test run time in
the fixture, rather than having to run those setup steps at collection time.
It's also possible to configure this on the fixture level, making all tests
using that fixture indirectly parametrized by default:
.. code-block:: python
import pytest
@pytest.fixture(indirect=True)
def fixt(request):
return request.param * 3
@pytest.mark.parametrize("fixt", ["a", "b"])
def test_indirect(fixt):
assert len(fixt) == 3
.. regendoc:wipe
Apply indirect on particular arguments

View File

@ -212,6 +212,7 @@ def add_funcarg_pseudo_fixture_def(
func=get_direct_param_fixture_func,
scope=arg2scope[argname],
params=valuelist,
indirect=False,
unittest=False,
ids=None,
)
@ -943,6 +944,7 @@ class FixtureDef(Generic[FixtureValue]):
func: "_FixtureFunc[FixtureValue]",
scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None],
params: Optional[Sequence[object]],
indirect: bool = False,
unittest: bool = False,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
@ -987,6 +989,8 @@ class FixtureDef(Generic[FixtureValue]):
# assign to the parameter values, or a callable to generate an ID given
# a parameter value.
self.ids = ids
# Whether the fixture should be always indirectly parametrized
self.indirect = indirect
# The names requested by the fixtures.
self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
# Whether the fixture was collected from a unittest TestCase class.
@ -1174,6 +1178,7 @@ class FixtureFunctionMarker:
converter=_ensure_immutable_ids,
)
name: Optional[str] = None
indirect: bool = False
def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function):
@ -1241,6 +1246,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
name: Optional[str] = None,
indirect: bool = False,
) -> Union[FixtureFunctionMarker, FixtureFunction]:
"""Decorator to mark a fixture factory function.
@ -1298,6 +1304,7 @@ def fixture(
autouse=autouse,
ids=ids,
name=name,
indirect=indirect,
)
# Direct decoration.
@ -1417,7 +1424,11 @@ class FixtureManager:
p_argnames, _ = ParameterSet._parse_parametrize_args(
*marker.args, **marker.kwargs
)
parametrize_argnames.extend(p_argnames)
for argname in p_argnames:
fixturedefs = self.getfixturedefs(argname, node.nodeid) or []
if any(f.indirect for f in fixturedefs):
continue
parametrize_argnames.append(argname)
return parametrize_argnames
@ -1614,10 +1625,14 @@ class FixtureManager:
func=obj,
scope=marker.scope,
params=marker.params,
indirect=marker.indirect,
unittest=unittest,
ids=marker.ids,
)
if marker.indirect:
fixture_def.params = fixture_def.params or []
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)

View File

@ -1194,8 +1194,8 @@ class Metafunc:
#: Underlying Python test function.
self.function = definition.obj
#: Set of fixture names required by the test function.
self.fixturenames = fixtureinfo.names_closure
#: Set of fixtures required by the test function
self.fixtureinfo = fixtureinfo
#: Class object where the test function is defined in or ``None``.
self.cls = cls
@ -1205,6 +1205,10 @@ class Metafunc:
# Result of parametrize().
self._calls: List[CallSpec2] = []
@property
def fixturenames(self):
return self.fixtureinfo.names_closure
def parametrize(
self,
argnames: Union[str, List[str], Tuple[str, ...]],
@ -1300,8 +1304,23 @@ class Metafunc:
else:
scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
if not isinstance(indirect, (bool, Sequence)):
fail(
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
type=type(indirect).__name__, func=self.function.__name__
),
pytrace=False,
)
self._validate_if_using_arg_names(argnames, indirect)
if indirect is not True: # False, or a list
indirect = list(indirect or [])
for argname in argnames:
fixturedefs = self.fixtureinfo.name2fixturedefs.get(argname, [])
if any(f.indirect for f in fixturedefs):
indirect.append(argname)
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
# Use any already (possibly) generated ids with parametrize Marks.
@ -1435,13 +1454,7 @@ class Metafunc:
pytrace=False,
)
valtypes[arg] = "params"
else:
fail(
"In {func}: expected Sequence or boolean for indirect, got {type}".format(
type=type(indirect).__name__, func=self.function.__name__
),
pytrace=False,
)
return valtypes
def _validate_if_using_arg_names(

View File

@ -4468,3 +4468,105 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None:
result.assert_outcomes(errors=1)
result.stdout.fnmatch_lines([expected])
assert result.ret == ExitCode.TESTS_FAILED
def test_fixture_indirect(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.fixture(indirect=True)
def indirect_sum(request):
return sum(request.param)
@pytest.fixture
def indirect_reversed(request):
return list(reversed(request.param))
"""
)
pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("indirect_sum, indirect_reversed, direct_list",
[
([1,2,3], [1,2,3], [1,2,3])
],
indirect=["indirect_reversed"]
)
def test_indirect_sum(indirect_sum, indirect_reversed, direct_list):
assert indirect_sum == 6
assert indirect_reversed == [3, 2, 1]
assert direct_list == [1, 2, 3]
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)
def test_fixture_indirect_always(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.fixture(indirect=True)
def indirect_sum(request):
return sum(request.param)
"""
)
pytester.makepyfile(
"""
import pytest
@pytest.mark.parametrize("indirect_sum",
[
([1,2,3])
],
indirect=[]
)
def test_indirect_sum(indirect_sum):
assert indirect_sum == 6
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)
def test_fixture_indirect_no_params(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.fixture(indirect=True)
def indirect_skip(request):
pass
"""
)
pytester.makepyfile(
"""
import pytest
def test_indirect_skip(indirect_skip):
pass
"""
)
result = pytester.runpytest()
result.assert_outcomes(skipped=1)
def test_fixture_indirect_default_params(pytester: Pytester) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.fixture(indirect=True, params=[1])
def indirect_default(request):
return request.param
"""
)
pytester.makepyfile(
"""
import pytest
def test_indirect_sum(indirect_default):
assert indirect_default == 1
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)

View File

@ -22,6 +22,7 @@ from _pytest import python
from _pytest.compat import _format_args
from _pytest.compat import getfuncargnames
from _pytest.compat import NOTSET
from _pytest.fixtures import FixtureDef
from _pytest.outcomes import fail
from _pytest.pytester import Pytester
from _pytest.python import IdMaker
@ -34,7 +35,7 @@ class TestMetafunc:
# on the funcarg level, so we don't need a full blown
# initialization.
class FuncFixtureInfoMock:
name2fixturedefs = None
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] = {}
def __init__(self, names):
self.names_closure = names