[parametrize] enforce explicit argnames declaration (#6330)

Every argname used in `parametrize` either must
be declared explicitly in the python test function, or via
`indirect` list

Fix #5712
This commit is contained in:
Vladyslav Rachek 2020-02-07 00:20:25 +01:00 committed by GitHub
parent 39d9f7cff5
commit 9e262038c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 6 deletions

View File

@ -274,6 +274,7 @@ Vidar T. Fauske
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vlad Dragos Vlad Dragos
Vladyslav Rachek
Volodymyr Piskun Volodymyr Piskun
Wei Lin Wei Lin
Wil Cooley Wil Cooley

View File

@ -0,0 +1,2 @@
Now all arguments to ``@pytest.mark.parametrize`` need to be explicitly declared in the function signature or via ``indirect``.
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.

View File

@ -398,6 +398,9 @@ The result of this test will be successful:
.. regendoc:wipe .. regendoc:wipe
Note, that each argument in `parametrize` list should be explicitly declared in corresponding
python test function or via `indirect`.
Parametrizing test methods through per-class configuration Parametrizing test methods through per-class configuration
-------------------------------------------------------------- --------------------------------------------------------------

View File

@ -1,6 +1,5 @@
import functools import functools
import inspect import inspect
import itertools
import sys import sys
import warnings import warnings
from collections import defaultdict from collections import defaultdict
@ -1279,10 +1278,8 @@ class FixtureManager:
else: else:
argnames = () argnames = ()
usefixtures = itertools.chain.from_iterable( usefixtures = get_use_fixtures_for_node(node)
mark.args for mark in node.iter_markers(name="usefixtures") initialnames = usefixtures + argnames
)
initialnames = tuple(usefixtures) + argnames
fm = node.session._fixturemanager fm = node.session._fixturemanager
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
initialnames, node, ignore_args=self._get_direct_parametrize_args(node) initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
@ -1479,3 +1476,12 @@ class FixtureManager:
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if nodes.ischildnode(fixturedef.baseid, nodeid): if nodes.ischildnode(fixturedef.baseid, nodeid):
yield fixturedef yield fixturedef
def get_use_fixtures_for_node(node) -> Tuple[str, ...]:
"""Returns the names of all the usefixtures() marks on the given node"""
return tuple(
str(name)
for mark in node.iter_markers(name="usefixtures")
for name in mark.args
)

View File

@ -955,6 +955,8 @@ class Metafunc:
arg_values_types = self._resolve_arg_value_types(argnames, indirect) arg_values_types = self._resolve_arg_value_types(argnames, indirect)
self._validate_explicit_parameters(argnames, indirect)
# Use any already (possibly) generated ids with parametrize Marks. # Use any already (possibly) generated ids with parametrize Marks.
if _param_mark and _param_mark._param_ids_from: if _param_mark and _param_mark._param_ids_from:
generated_ids = _param_mark._param_ids_from._param_ids_generated generated_ids = _param_mark._param_ids_from._param_ids_generated
@ -1105,6 +1107,37 @@ class Metafunc:
pytrace=False, pytrace=False,
) )
def _validate_explicit_parameters(self, argnames, indirect):
"""
The argnames in *parametrize* should either be declared explicitly via
indirect list or in the function signature
:param List[str] argnames: list of argument names passed to ``parametrize()``.
:param indirect: same ``indirect`` parameter of ``parametrize()``.
:raise ValueError: if validation fails
"""
if isinstance(indirect, bool) and indirect is True:
return
parametrized_argnames = list()
funcargnames = _pytest.compat.getfuncargnames(self.function)
if isinstance(indirect, Sequence):
for arg in argnames:
if arg not in indirect:
parametrized_argnames.append(arg)
elif indirect is False:
parametrized_argnames = argnames
usefixtures = fixtures.get_use_fixtures_for_node(self.definition)
for arg in parametrized_argnames:
if arg not in funcargnames and arg not in usefixtures:
func_name = self.function.__name__
msg = (
'In function "{func_name}":\n'
'Parameter "{arg}" should be declared explicitly via indirect or in function itself'
).format(func_name=func_name, arg=arg)
fail(msg, pytrace=False)
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
"""Find the most appropriate scope for a parametrized call based on its arguments. """Find the most appropriate scope for a parametrized call based on its arguments.

View File

@ -463,7 +463,7 @@ class TestFunction:
return '3' return '3'
@pytest.mark.parametrize('fix2', ['2']) @pytest.mark.parametrize('fix2', ['2'])
def test_it(fix1): def test_it(fix1, fix2):
assert fix1 == '21' assert fix1 == '21'
assert not fix3_instantiated assert not fix3_instantiated
""" """

View File

@ -28,6 +28,9 @@ class TestMetafunc:
class DefinitionMock(python.FunctionDefinition): class DefinitionMock(python.FunctionDefinition):
obj = attr.ib() obj = attr.ib()
def listchain(self):
return []
names = fixtures.getfuncargnames(func) names = fixtures.getfuncargnames(func)
fixtureinfo = FixtureInfo(names) fixtureinfo = FixtureInfo(names)
definition = DefinitionMock._create(func) definition = DefinitionMock._create(func)
@ -1877,3 +1880,51 @@ class TestMarkersWithParametrization:
"*= 6 passed in *", "*= 6 passed in *",
] ]
) )
def test_parametrize_explicit_parameters_func(self, testdir):
testdir.makepyfile(
"""
import pytest
@pytest.fixture
def fixture(arg):
return arg
@pytest.mark.parametrize("arg", ["baz"])
def test_without_arg(fixture):
assert "baz" == fixture
"""
)
result = testdir.runpytest()
result.assert_outcomes(error=1)
result.stdout.fnmatch_lines(
[
'*In function "test_without_arg"*',
'*Parameter "arg" should be declared explicitly via indirect or in function itself*',
]
)
def test_parametrize_explicit_parameters_method(self, testdir):
testdir.makepyfile(
"""
import pytest
class Test:
@pytest.fixture
def test_fixture(self, argument):
return argument
@pytest.mark.parametrize("argument", ["foobar"])
def test_without_argument(self, test_fixture):
assert "foobar" == test_fixture
"""
)
result = testdir.runpytest()
result.assert_outcomes(error=1)
result.stdout.fnmatch_lines(
[
'*In function "test_without_argument"*',
'*Parameter "argument" should be declared explicitly via indirect or in function itself*',
]
)