Skip to content

Commit 693c3b7

Browse files
authored
Merge pull request #5349 from asottile/mm
Merge master into features
2 parents c8d23c2 + fb3ae5e commit 693c3b7

File tree

7 files changed

+104
-7
lines changed

7 files changed

+104
-7
lines changed

changelog/5036.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.

changelog/5330.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
2+
test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.

src/_pytest/fixtures.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,18 +1127,40 @@ def __init__(self, session):
11271127
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
11281128
session.config.pluginmanager.register(self, "funcmanage")
11291129

1130+
def _get_direct_parametrize_args(self, node):
1131+
"""This function returns all the direct parametrization
1132+
arguments of a node, so we don't mistake them for fixtures
1133+
1134+
Check https://github.com/pytest-dev/pytest/issues/5036
1135+
1136+
This things are done later as well when dealing with parametrization
1137+
so this could be improved
1138+
"""
1139+
from _pytest.mark import ParameterSet
1140+
1141+
parametrize_argnames = []
1142+
for marker in node.iter_markers(name="parametrize"):
1143+
if not marker.kwargs.get("indirect", False):
1144+
p_argnames, _ = ParameterSet._parse_parametrize_args(
1145+
*marker.args, **marker.kwargs
1146+
)
1147+
parametrize_argnames.extend(p_argnames)
1148+
1149+
return parametrize_argnames
1150+
11301151
def getfixtureinfo(self, node, func, cls, funcargs=True):
11311152
if funcargs and not getattr(node, "nofuncargs", False):
11321153
argnames = getfuncargnames(func, cls=cls)
11331154
else:
11341155
argnames = ()
1156+
11351157
usefixtures = itertools.chain.from_iterable(
11361158
mark.args for mark in node.iter_markers(name="usefixtures")
11371159
)
11381160
initialnames = tuple(usefixtures) + argnames
11391161
fm = node.session._fixturemanager
11401162
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
1141-
initialnames, node
1163+
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
11421164
)
11431165
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
11441166

@@ -1172,7 +1194,7 @@ def _getautousenames(self, nodeid):
11721194
autousenames.extend(basenames)
11731195
return autousenames
11741196

1175-
def getfixtureclosure(self, fixturenames, parentnode):
1197+
def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()):
11761198
# collect the closure of all fixtures , starting with the given
11771199
# fixturenames as the initial set. As we have to visit all
11781200
# factory definitions anyway, we also return an arg2fixturedefs
@@ -1200,6 +1222,8 @@ def merge(otherlist):
12001222
while lastlen != len(fixturenames_closure):
12011223
lastlen = len(fixturenames_closure)
12021224
for argname in fixturenames_closure:
1225+
if argname in ignore_args:
1226+
continue
12031227
if argname in arg2fixturedefs:
12041228
continue
12051229
fixturedefs = self.getfixturedefs(argname, parentid)

src/_pytest/mark/structures.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,11 @@ def extract_from(cls, parameterset, force_tuple=False):
103103
else:
104104
return cls(parameterset, marks=[], id=None)
105105

106-
@classmethod
107-
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
106+
@staticmethod
107+
def _parse_parametrize_args(argnames, argvalues, **_):
108+
"""It receives an ignored _ (kwargs) argument so this function can
109+
take also calls from parametrize ignoring scope, indirect, and other
110+
arguments..."""
108111
if not isinstance(argnames, (tuple, list)):
109112
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
110113
force_tuple = len(argnames) == 1
@@ -113,6 +116,11 @@ def _for_parametrize(cls, argnames, argvalues, func, config, function_definition
113116
parameters = [
114117
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
115118
]
119+
return argnames, parameters
120+
121+
@classmethod
122+
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
123+
argnames, parameters = cls._parse_parametrize_args(argnames, argvalues)
116124
del argvalues
117125

118126
if parameters:

src/_pytest/python.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,15 +719,17 @@ def collect(self):
719719
self.warn(
720720
PytestCollectionWarning(
721721
"cannot collect test class %r because it has a "
722-
"__init__ constructor" % self.obj.__name__
722+
"__init__ constructor (from: %s)"
723+
% (self.obj.__name__, self.parent.nodeid)
723724
)
724725
)
725726
return []
726727
elif hasnew(self.obj):
727728
self.warn(
728729
PytestCollectionWarning(
729730
"cannot collect test class %r because it has a "
730-
"__new__ constructor" % self.obj.__name__
731+
"__new__ constructor (from: %s)"
732+
% (self.obj.__name__, self.parent.nodeid)
731733
)
732734
)
733735
return []

testing/python/collect.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,24 @@ def __init__(self):
146146
result = testdir.runpytest("-rw")
147147
result.stdout.fnmatch_lines(
148148
[
149-
"*cannot collect test class 'TestClass1' because it has a __init__ constructor"
149+
"*cannot collect test class 'TestClass1' because it has "
150+
"a __init__ constructor (from: test_class_with_init_warning.py)"
151+
]
152+
)
153+
154+
def test_class_with_new_warning(self, testdir):
155+
testdir.makepyfile(
156+
"""
157+
class TestClass1(object):
158+
def __new__(self):
159+
pass
160+
"""
161+
)
162+
result = testdir.runpytest("-rw")
163+
result.stdout.fnmatch_lines(
164+
[
165+
"*cannot collect test class 'TestClass1' because it has "
166+
"a __new__ constructor (from: test_class_with_new_warning.py)"
150167
]
151168
)
152169

testing/python/fixtures.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,3 +3950,46 @@ def fix():
39503950

39513951
with pytest.raises(pytest.fail.Exception):
39523952
assert fix() == 1
3953+
3954+
3955+
def test_fixture_param_shadowing(testdir):
3956+
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
3957+
testdir.makepyfile(
3958+
"""
3959+
import pytest
3960+
3961+
@pytest.fixture(params=['a', 'b'])
3962+
def argroot(request):
3963+
return request.param
3964+
3965+
@pytest.fixture
3966+
def arg(argroot):
3967+
return argroot
3968+
3969+
# This should only be parametrized directly
3970+
@pytest.mark.parametrize("arg", [1])
3971+
def test_direct(arg):
3972+
assert arg == 1
3973+
3974+
# This should be parametrized based on the fixtures
3975+
def test_normal_fixture(arg):
3976+
assert isinstance(arg, str)
3977+
3978+
# Indirect should still work:
3979+
3980+
@pytest.fixture
3981+
def arg2(request):
3982+
return 2*request.param
3983+
3984+
@pytest.mark.parametrize("arg2", [1], indirect=True)
3985+
def test_indirect(arg2):
3986+
assert arg2 == 2
3987+
"""
3988+
)
3989+
# Only one test should have run
3990+
result = testdir.runpytest("-v")
3991+
result.assert_outcomes(passed=4)
3992+
result.stdout.fnmatch_lines(["*::test_direct[[]1[]]*"])
3993+
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"])
3994+
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"])
3995+
result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"])

0 commit comments

Comments
 (0)