Merge remote-tracking branch 'origin/master' into mm

This commit is contained in:
Anthony Sottile 2019-05-30 20:23:38 -07:00
commit fb3ae5eaa9
7 changed files with 104 additions and 7 deletions

View File

@ -0,0 +1 @@
Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized.

View File

@ -0,0 +1,2 @@
Show the test module being collected when emitting ``PytestCollectionWarning`` messages for
test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem.

View File

@ -1127,18 +1127,40 @@ class FixtureManager(object):
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
session.config.pluginmanager.register(self, "funcmanage") session.config.pluginmanager.register(self, "funcmanage")
def _get_direct_parametrize_args(self, node):
"""This function returns all the direct parametrization
arguments of a node, so we don't mistake them for fixtures
Check https://github.com/pytest-dev/pytest/issues/5036
This things are done later as well when dealing with parametrization
so this could be improved
"""
from _pytest.mark import ParameterSet
parametrize_argnames = []
for marker in node.iter_markers(name="parametrize"):
if not marker.kwargs.get("indirect", False):
p_argnames, _ = ParameterSet._parse_parametrize_args(
*marker.args, **marker.kwargs
)
parametrize_argnames.extend(p_argnames)
return parametrize_argnames
def getfixtureinfo(self, node, func, cls, funcargs=True): def getfixtureinfo(self, node, func, cls, funcargs=True):
if funcargs and not getattr(node, "nofuncargs", False): if funcargs and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, cls=cls) argnames = getfuncargnames(func, cls=cls)
else: else:
argnames = () argnames = ()
usefixtures = itertools.chain.from_iterable( usefixtures = itertools.chain.from_iterable(
mark.args for mark in node.iter_markers(name="usefixtures") mark.args for mark in node.iter_markers(name="usefixtures")
) )
initialnames = tuple(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 initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
) )
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
@ -1172,7 +1194,7 @@ class FixtureManager(object):
autousenames.extend(basenames) autousenames.extend(basenames)
return autousenames return autousenames
def getfixtureclosure(self, fixturenames, parentnode): def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()):
# collect the closure of all fixtures , starting with the given # collect the closure of all fixtures , starting with the given
# fixturenames as the initial set. As we have to visit all # fixturenames as the initial set. As we have to visit all
# factory definitions anyway, we also return an arg2fixturedefs # factory definitions anyway, we also return an arg2fixturedefs
@ -1200,6 +1222,8 @@ class FixtureManager(object):
while lastlen != len(fixturenames_closure): while lastlen != len(fixturenames_closure):
lastlen = len(fixturenames_closure) lastlen = len(fixturenames_closure)
for argname in fixturenames_closure: for argname in fixturenames_closure:
if argname in ignore_args:
continue
if argname in arg2fixturedefs: if argname in arg2fixturedefs:
continue continue
fixturedefs = self.getfixturedefs(argname, parentid) fixturedefs = self.getfixturedefs(argname, parentid)

View File

@ -103,8 +103,11 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
else: else:
return cls(parameterset, marks=[], id=None) return cls(parameterset, marks=[], id=None)
@classmethod @staticmethod
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): def _parse_parametrize_args(argnames, argvalues, **_):
"""It receives an ignored _ (kwargs) argument so this function can
take also calls from parametrize ignoring scope, indirect, and other
arguments..."""
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1 force_tuple = len(argnames) == 1
@ -113,6 +116,11 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
parameters = [ parameters = [
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
] ]
return argnames, parameters
@classmethod
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
argnames, parameters = cls._parse_parametrize_args(argnames, argvalues)
del argvalues del argvalues
if parameters: if parameters:

View File

@ -719,7 +719,8 @@ class Class(PyCollector):
self.warn( self.warn(
PytestCollectionWarning( PytestCollectionWarning(
"cannot collect test class %r because it has a " "cannot collect test class %r because it has a "
"__init__ constructor" % self.obj.__name__ "__init__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
) )
) )
return [] return []
@ -727,7 +728,8 @@ class Class(PyCollector):
self.warn( self.warn(
PytestCollectionWarning( PytestCollectionWarning(
"cannot collect test class %r because it has a " "cannot collect test class %r because it has a "
"__new__ constructor" % self.obj.__name__ "__new__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
) )
) )
return [] return []

View File

@ -146,7 +146,24 @@ class TestClass(object):
result = testdir.runpytest("-rw") result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
"*cannot collect test class 'TestClass1' because it has a __init__ constructor" "*cannot collect test class 'TestClass1' because it has "
"a __init__ constructor (from: test_class_with_init_warning.py)"
]
)
def test_class_with_new_warning(self, testdir):
testdir.makepyfile(
"""
class TestClass1(object):
def __new__(self):
pass
"""
)
result = testdir.runpytest("-rw")
result.stdout.fnmatch_lines(
[
"*cannot collect test class 'TestClass1' because it has "
"a __new__ constructor (from: test_class_with_new_warning.py)"
] ]
) )

View File

@ -3950,3 +3950,46 @@ def test_call_fixture_function_error():
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
assert fix() == 1 assert fix() == 1
def test_fixture_param_shadowing(testdir):
"""Parametrized arguments would be shadowed if a fixture with the same name also exists (#5036)"""
testdir.makepyfile(
"""
import pytest
@pytest.fixture(params=['a', 'b'])
def argroot(request):
return request.param
@pytest.fixture
def arg(argroot):
return argroot
# This should only be parametrized directly
@pytest.mark.parametrize("arg", [1])
def test_direct(arg):
assert arg == 1
# This should be parametrized based on the fixtures
def test_normal_fixture(arg):
assert isinstance(arg, str)
# Indirect should still work:
@pytest.fixture
def arg2(request):
return 2*request.param
@pytest.mark.parametrize("arg2", [1], indirect=True)
def test_indirect(arg2):
assert arg2 == 2
"""
)
# Only one test should have run
result = testdir.runpytest("-v")
result.assert_outcomes(passed=4)
result.stdout.fnmatch_lines(["*::test_direct[[]1[]]*"])
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"])
result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"])
result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"])