From e492b1d567f74588e712265dd8016b2156cf6afa Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Tue, 30 Jun 2020 00:05:12 +0300
Subject: [PATCH 1/3] python: don't pass entire Item for generating ID

Just the nodeid is enough for the error messages.
This removes an import cycle.
---
 src/_pytest/mark/structures.py |  7 ++-----
 src/_pytest/python.py          | 29 +++++++++++++++------------
 testing/python/metafunc.py     | 36 ++++++++--------------------------
 3 files changed, 27 insertions(+), 45 deletions(-)

diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index 2d756bb42..ca5c03e90 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -27,9 +27,6 @@ from _pytest.config import Config
 from _pytest.outcomes import fail
 from _pytest.warning_types import PytestUnknownMarkWarning
 
-if TYPE_CHECKING:
-    from _pytest.python import FunctionDefinition
-
 
 EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
 
@@ -159,7 +156,7 @@ class ParameterSet(
         argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
         func,
         config: Config,
-        function_definition: "FunctionDefinition",
+        nodeid: str,
     ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
         argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
         parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
@@ -177,7 +174,7 @@ class ParameterSet(
                     )
                     fail(
                         msg.format(
-                            nodeid=function_definition.nodeid,
+                            nodeid=nodeid,
                             values=param.values,
                             names=argnames,
                             names_len=len(argnames),
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 7fbd59add..65855ca23 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -980,7 +980,7 @@ class Metafunc:
             argvalues,
             self.function,
             self.config,
-            function_definition=self.definition,
+            nodeid=self.definition.nodeid,
         )
         del argvalues
 
@@ -1003,7 +1003,9 @@ class Metafunc:
             if generated_ids is not None:
                 ids = generated_ids
 
-        ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
+        ids = self._resolve_arg_ids(
+            argnames, ids, parameters, nodeid=self.definition.nodeid
+        )
 
         # Store used (possibly generated) ids with parametrize Marks.
         if _param_mark and _param_mark._param_ids_from and generated_ids is None:
@@ -1042,7 +1044,7 @@ class Metafunc:
             ]
         ],
         parameters: typing.Sequence[ParameterSet],
-        item,
+        nodeid: str,
     ) -> List[str]:
         """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
         to ``parametrize``.
@@ -1050,7 +1052,7 @@ class Metafunc:
         :param List[str] argnames: list of argument names passed to ``parametrize()``.
         :param ids: the ids parameter of the parametrized call (see docs).
         :param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
-        :param Item item: the item that generated this parametrized call.
+        :param str str: the nodeid of the item that generated this parametrized call.
         :rtype: List[str]
         :return: the list of ids for each argname given
         """
@@ -1063,7 +1065,7 @@ class Metafunc:
         else:
             idfn = None
             ids_ = self._validate_ids(ids, parameters, self.function.__name__)
-        return idmaker(argnames, parameters, idfn, ids_, self.config, item=item)
+        return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid)
 
     def _validate_ids(
         self,
@@ -1223,7 +1225,7 @@ def _idval(
     argname: str,
     idx: int,
     idfn: Optional[Callable[[object], Optional[object]]],
-    item,
+    nodeid: Optional[str],
     config: Optional[Config],
 ) -> str:
     if idfn:
@@ -1232,8 +1234,9 @@ def _idval(
             if generated_id is not None:
                 val = generated_id
         except Exception as e:
-            msg = "{}: error raised while trying to determine id of parameter '{}' at position {}"
-            msg = msg.format(item.nodeid, argname, idx)
+            prefix = "{}: ".format(nodeid) if nodeid is not None else ""
+            msg = "error raised while trying to determine id of parameter '{}' at position {}"
+            msg = prefix + msg.format(argname, idx)
             raise ValueError(msg) from e
     elif config:
         hook_id = config.hook.pytest_make_parametrize_id(
@@ -1263,7 +1266,7 @@ def _idvalset(
     argnames: Iterable[str],
     idfn: Optional[Callable[[object], Optional[object]]],
     ids: Optional[List[Union[None, str]]],
-    item,
+    nodeid: Optional[str],
     config: Optional[Config],
 ):
     if parameterset.id is not None:
@@ -1271,7 +1274,7 @@ def _idvalset(
     id = None if ids is None or idx >= len(ids) else ids[idx]
     if id is None:
         this_id = [
-            _idval(val, argname, idx, idfn, item=item, config=config)
+            _idval(val, argname, idx, idfn, nodeid=nodeid, config=config)
             for val, argname in zip(parameterset.values, argnames)
         ]
         return "-".join(this_id)
@@ -1285,10 +1288,12 @@ def idmaker(
     idfn: Optional[Callable[[object], Optional[object]]] = None,
     ids: Optional[List[Union[None, str]]] = None,
     config: Optional[Config] = None,
-    item=None,
+    nodeid: Optional[str] = None,
 ) -> List[str]:
     resolved_ids = [
-        _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item)
+        _idvalset(
+            valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid
+        )
         for valindex, parameterset in enumerate(parametersets)
     ]
 
diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py
index c4b5bd222..1f110d419 100644
--- a/testing/python/metafunc.py
+++ b/testing/python/metafunc.py
@@ -19,6 +19,7 @@ from _pytest import python
 from _pytest.outcomes import fail
 from _pytest.pytester import Testdir
 from _pytest.python import _idval
+from _pytest.python import idmaker
 
 
 class TestMetafunc:
@@ -35,10 +36,11 @@ class TestMetafunc:
         @attr.s
         class DefinitionMock(python.FunctionDefinition):
             obj = attr.ib()
+            _nodeid = attr.ib()
 
         names = fixtures.getfuncargnames(func)
         fixtureinfo = FuncFixtureInfoMock(names)  # type: Any
-        definition = DefinitionMock._create(func)  # type: Any
+        definition = DefinitionMock._create(func, "mock::nodeid")  # type: Any
         return python.Metafunc(definition, fixtureinfo, config)
 
     def test_no_funcargs(self) -> None:
@@ -270,7 +272,7 @@ class TestMetafunc:
         deadline=400.0
     )  # very close to std deadline and CI boxes are not reliable in CPU power
     def test_idval_hypothesis(self, value) -> None:
-        escaped = _idval(value, "a", 6, None, item=None, config=None)
+        escaped = _idval(value, "a", 6, None, nodeid=None, config=None)
         assert isinstance(escaped, str)
         escaped.encode("ascii")
 
@@ -292,7 +294,7 @@ class TestMetafunc:
             ),
         ]
         for val, expected in values:
-            assert _idval(val, "a", 6, None, item=None, config=None) == expected
+            assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
 
     def test_unicode_idval_with_config(self) -> None:
         """unittest for expected behavior to obtain ids with
@@ -321,7 +323,7 @@ class TestMetafunc:
             ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
         ]  # type: List[Tuple[str, Any, str]]
         for val, config, expected in values:
-            actual = _idval(val, "a", 6, None, item=None, config=config)
+            actual = _idval(val, "a", 6, None, nodeid=None, config=config)
             assert actual == expected
 
     def test_bytes_idval(self) -> None:
@@ -338,7 +340,7 @@ class TestMetafunc:
             ("αρά".encode(), "\\xce\\xb1\\xcf\\x81\\xce\\xac"),
         ]
         for val, expected in values:
-            assert _idval(val, "a", 6, idfn=None, item=None, config=None) == expected
+            assert _idval(val, "a", 6, idfn=None, nodeid=None, config=None) == expected
 
     def test_class_or_function_idval(self) -> None:
         """unittest for the expected behavior to obtain ids for parametrized
@@ -353,12 +355,10 @@ class TestMetafunc:
 
         values = [(TestClass, "TestClass"), (test_function, "test_function")]
         for val, expected in values:
-            assert _idval(val, "a", 6, None, item=None, config=None) == expected
+            assert _idval(val, "a", 6, None, nodeid=None, config=None) == expected
 
     def test_idmaker_autoname(self) -> None:
         """#250"""
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("a", "b"), [pytest.param("string", 1.0), pytest.param("st-ring", 2.0)]
         )
@@ -373,14 +373,10 @@ class TestMetafunc:
         assert result == ["a0-\\xc3\\xb4"]
 
     def test_idmaker_with_bytes_regex(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(("a"), [pytest.param(re.compile(b"foo"), 1.0)])
         assert result == ["foo"]
 
     def test_idmaker_native_strings(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("a", "b"),
             [
@@ -414,8 +410,6 @@ class TestMetafunc:
         ]
 
     def test_idmaker_non_printable_characters(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("s", "n"),
             [
@@ -430,8 +424,6 @@ class TestMetafunc:
         assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
 
     def test_idmaker_manual_ids_must_be_printable(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("s",),
             [
@@ -442,8 +434,6 @@ class TestMetafunc:
         assert result == ["hello \\x00", "hello \\x05"]
 
     def test_idmaker_enum(self) -> None:
-        from _pytest.python import idmaker
-
         enum = pytest.importorskip("enum")
         e = enum.Enum("Foo", "one, two")
         result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
@@ -451,7 +441,6 @@ class TestMetafunc:
 
     def test_idmaker_idfn(self) -> None:
         """#351"""
-        from _pytest.python import idmaker
 
         def ids(val: object) -> Optional[str]:
             if isinstance(val, Exception):
@@ -471,7 +460,6 @@ class TestMetafunc:
 
     def test_idmaker_idfn_unique_names(self) -> None:
         """#351"""
-        from _pytest.python import idmaker
 
         def ids(val: object) -> str:
             return "a"
@@ -492,7 +480,6 @@ class TestMetafunc:
         disable_test_id_escaping_and_forfeit_all_rights_to_community_support
         option. (#5294)
         """
-        from _pytest.python import idmaker
 
         class MockConfig:
             def __init__(self, config):
@@ -525,7 +512,6 @@ class TestMetafunc:
         disable_test_id_escaping_and_forfeit_all_rights_to_community_support
         option. (#5294)
         """
-        from _pytest.python import idmaker
 
         class MockConfig:
             def __init__(self, config):
@@ -607,16 +593,12 @@ class TestMetafunc:
         )
 
     def test_idmaker_with_ids(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("a", "b"), [pytest.param(1, 2), pytest.param(3, 4)], ids=["a", None]
         )
         assert result == ["a", "3-4"]
 
     def test_idmaker_with_paramset_id(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("a", "b"),
             [pytest.param(1, 2, id="me"), pytest.param(3, 4, id="you")],
@@ -625,8 +607,6 @@ class TestMetafunc:
         assert result == ["me", "you"]
 
     def test_idmaker_with_ids_unique_names(self) -> None:
-        from _pytest.python import idmaker
-
         result = idmaker(
             ("a"), map(pytest.param, [1, 2, 3, 4, 5]), ids=["a", "a", "b", "c", "b"]
         )

From 40c355f8c3554a2002976d9f5426ca53293ccf59 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Mon, 29 Jun 2020 23:31:01 +0300
Subject: [PATCH 2/3] python: pytest_pycollect_makeitem doesn't need to be a
 hookwrapper

A trylast is more appropriate for this usecase.

hookwrappers are more complicated and more expensive than regular
hookimpls, so better avoided.
---
 src/_pytest/python.py | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 65855ca23..c5cd14bdc 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -209,16 +209,12 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module":
     return mod
 
 
-@hookimpl(hookwrapper=True)
-def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj):
-    outcome = yield
-    res = outcome.get_result()
-    if res is not None:
-        return
+@hookimpl(trylast=True)
+def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
     # nothing was collected elsewhere, let's do it here
     if safe_isclass(obj):
         if collector.istestclass(obj, name):
-            outcome.force_result(Class.from_parent(collector, name=name, obj=obj))
+            return Class.from_parent(collector, name=name, obj=obj)
     elif collector.istestfunction(obj, name):
         # mock seems to store unbound methods (issue473), normalize it
         obj = getattr(obj, "__func__", obj)
@@ -245,7 +241,7 @@ def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj):
                 res.warn(PytestCollectionWarning(reason))
             else:
                 res = list(collector._genfunctions(name, obj))
-            outcome.force_result(res)
+            return res
 
 
 class PyobjMixin:

From ae83dbd4cfd08c027d9d9771db8aaa0474836ac6 Mon Sep 17 00:00:00 2001
From: Ran Benita <ran@unusedvar.com>
Date: Tue, 16 Jun 2020 12:50:09 +0300
Subject: [PATCH 3/3] python: remove ancient Function.repr_failure(outerr)
 parameter

This has been asserted like this since 04e9197fd6138adaf953ba8fef370
(i.e. 11 years, pytest 1.0), seems safe to simply remove at this point.
---
 src/_pytest/python.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index c5cd14bdc..751e17407 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -1590,9 +1590,8 @@ class Function(PyobjMixin, nodes.Item):
 
     # TODO: Type ignored -- breaks Liskov Substitution.
     def repr_failure(  # type: ignore[override] # noqa: F821
-        self, excinfo: ExceptionInfo[BaseException], outerr: None = None
+        self, excinfo: ExceptionInfo[BaseException],
     ) -> Union[str, TerminalRepr]:
-        assert outerr is None, "XXX outerr usage is deprecated"
         style = self.config.getoption("tbstyle", "auto")
         if style == "auto":
             style = "long"