commit
						693c3b7f61
					
				|  | @ -0,0 +1 @@ | ||||||
|  | Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized. | ||||||
|  | @ -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. | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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 [] | ||||||
|  |  | ||||||
|  | @ -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)" | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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[]]*"]) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue