Skip to content

Commit 03e19cd

Browse files
erheronnicoddemus
authored andcommitted
Force explicit declaration of args in parametrize
Every argname used in `parametrize` either must be declared explicitly in the python test function, or via `indirect` list Fix pytest-dev#5712
1 parent 2f0d0fb commit 03e19cd

File tree

7 files changed

+106
-6
lines changed

7 files changed

+106
-6
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ Vidar T. Fauske
273273
Virgil Dupras
274274
Vitaly Lashmanov
275275
Vlad Dragos
276+
Vladyslav Rachek
276277
Volodymyr Piskun
277278
Wei Lin
278279
Wil Cooley

changelog/5712.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Now all arguments to ``@pytest.mark.parametrize`` need to be explicitly declared in the function signature or via ``indirect``.
2+
Previously it was possible to omit an argument if a fixture with the same name existed, which was just an accident of implementation and was not meant to be a part of the API.

doc/en/example/parametrize.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,9 @@ The result of this test will be successful:
398398
399399
.. regendoc:wipe
400400
401+
Note, that each argument in `parametrize` list should be explicitly declared in corresponding
402+
python test function or via `indirect`.
403+
401404
Parametrizing test methods through per-class configuration
402405
--------------------------------------------------------------
403406

src/_pytest/fixtures.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,10 +1326,8 @@ def getfixtureinfo(self, node, func, cls, funcargs=True):
13261326
else:
13271327
argnames = ()
13281328

1329-
usefixtures = itertools.chain.from_iterable(
1330-
mark.args for mark in node.iter_markers(name="usefixtures")
1331-
)
1332-
initialnames = tuple(usefixtures) + argnames
1329+
usefixtures = get_use_fixtures_for_node(node)
1330+
initialnames = usefixtures + argnames
13331331
fm = node.session._fixturemanager
13341332
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
13351333
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
@@ -1526,3 +1524,13 @@ def _matchfactories(self, fixturedefs, nodeid):
15261524
for fixturedef in fixturedefs:
15271525
if nodes.ischildnode(fixturedef.baseid, nodeid):
15281526
yield fixturedef
1527+
1528+
1529+
def get_use_fixtures_for_node(node) -> Tuple[str, ...]:
1530+
"""Returns the names of all the usefixtures() marks on the given node"""
1531+
return tuple(
1532+
str(x)
1533+
for x in itertools.chain.from_iterable(
1534+
mark.args for mark in node.iter_markers(name="usefixtures")
1535+
)
1536+
)

src/_pytest/python.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,7 @@ def parametrize(
929929
ids=None,
930930
scope=None,
931931
*,
932-
_param_mark: Optional[Mark] = None
932+
_param_mark: Optional[Mark] = None,
933933
):
934934
""" Add new invocations to the underlying test function using the list
935935
of argvalues for the given argnames. Parametrization is performed
@@ -1002,6 +1002,8 @@ def parametrize(
10021002

10031003
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
10041004

1005+
self._validate_explicit_parameters(argnames, indirect)
1006+
10051007
# Use any already (possibly) generated ids with parametrize Marks.
10061008
if _param_mark and _param_mark._param_ids_from:
10071009
generated_ids = _param_mark._param_ids_from._param_ids_generated
@@ -1152,6 +1154,37 @@ def _validate_if_using_arg_names(self, argnames, indirect):
11521154
pytrace=False,
11531155
)
11541156

1157+
def _validate_explicit_parameters(self, argnames, indirect):
1158+
"""
1159+
The argnames in *parametrize* should either be declared explicitly via
1160+
indirect list or in the function signature
1161+
1162+
:param List[str] argnames: list of argument names passed to ``parametrize()``.
1163+
:param indirect: same ``indirect`` parameter of ``parametrize()``.
1164+
:raise ValueError: if validation fails
1165+
"""
1166+
if isinstance(indirect, bool) and indirect is True:
1167+
return
1168+
parametrized_argnames = list()
1169+
funcargnames = _pytest.compat.getfuncargnames(self.function)
1170+
if isinstance(indirect, Sequence):
1171+
for arg in argnames:
1172+
if arg not in indirect:
1173+
parametrized_argnames.append(arg)
1174+
elif indirect is False:
1175+
parametrized_argnames = argnames
1176+
1177+
usefixtures = fixtures.get_use_fixtures_for_node(self.definition)
1178+
1179+
for arg in parametrized_argnames:
1180+
if arg not in funcargnames and arg not in usefixtures:
1181+
func_name = self.function.__name__
1182+
msg = (
1183+
'In function "{func_name}":\n'
1184+
'Parameter "{arg}" should be declared explicitly via indirect or in function itself'
1185+
).format(func_name=func_name, arg=arg)
1186+
fail(msg, pytrace=False)
1187+
11551188

11561189
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
11571190
"""Find the most appropriate scope for a parametrized call based on its arguments.

testing/python/collect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def fix3():
463463
return '3'
464464
465465
@pytest.mark.parametrize('fix2', ['2'])
466-
def test_it(fix1):
466+
def test_it(fix1, fix2):
467467
assert fix1 == '21'
468468
assert not fix3_instantiated
469469
"""

testing/python/metafunc.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def __init__(self, names):
2828
class DefinitionMock(python.FunctionDefinition):
2929
obj = attr.ib()
3030

31+
def listchain(self):
32+
return []
33+
3134
names = fixtures.getfuncargnames(func)
3235
fixtureinfo = FixtureInfo(names)
3336
definition = DefinitionMock._create(func)
@@ -1877,3 +1880,53 @@ def test_converted_to_str(a, b):
18771880
"*= 6 passed in *",
18781881
]
18791882
)
1883+
1884+
def test_parametrize_explicit_parameters_func(self, testdir):
1885+
testdir.makepyfile(
1886+
"""
1887+
import pytest
1888+
1889+
1890+
@pytest.fixture
1891+
def fixture(arg):
1892+
return arg
1893+
1894+
@pytest.mark.parametrize("arg", ["baz"])
1895+
def test_without_arg(fixture):
1896+
assert "baz" == fixture
1897+
"""
1898+
)
1899+
result = testdir.runpytest()
1900+
result.assert_outcomes(error=1)
1901+
result.stdout.fnmatch_lines(
1902+
[
1903+
'*In function "test_without_arg"*',
1904+
'*Parameter "arg" should be declared explicitly via indirect*',
1905+
"*or in function itself*",
1906+
]
1907+
)
1908+
1909+
def test_parametrize_explicit_parameters_method(self, testdir):
1910+
testdir.makepyfile(
1911+
"""
1912+
import pytest
1913+
1914+
class Test:
1915+
@pytest.fixture
1916+
def test_fixture(self, argument):
1917+
return argument
1918+
1919+
@pytest.mark.parametrize("argument", ["foobar"])
1920+
def test_without_argument(self, test_fixture):
1921+
assert "foobar" == test_fixture
1922+
"""
1923+
)
1924+
result = testdir.runpytest()
1925+
result.assert_outcomes(error=1)
1926+
result.stdout.fnmatch_lines(
1927+
[
1928+
'*In function "test_without_argument"*',
1929+
'*Parameter "argument" should be declared explicitly via indirect*',
1930+
"*or in function itself*",
1931+
]
1932+
)

0 commit comments

Comments
 (0)