Skip to content

Conversation

@RonnyPfannschmidt
Copy link
Member

  • reconfigured ruff for upgrade
  • added Self type for the errors
  • fixed test warnings locations

@RonnyPfannschmidt RonnyPfannschmidt added the needs backport applied to PRs, indicates that it should be ported to the current bug-fix branch label Jun 17, 2024
@RonnyPfannschmidt RonnyPfannschmidt force-pushed the ronny/new-annotations-try-2 branch 2 times, most recently from 2ecf59e to d5d2a27 Compare June 17, 2024 15:37
@RonnyPfannschmidt RonnyPfannschmidt force-pushed the ronny/new-annotations-try-2 branch 3 times, most recently from 798c82c to 987207a Compare June 18, 2024 09:51
@RonnyPfannschmidt
Copy link
Member Author

the way sphinx autodoc fails on deferred type annotations has been most infurating

its better on python 3.12 but rtd is still on 3.9

@The-Compiler
Copy link
Member

Reviewed by reproducing the automated changes:

$ git checkout -b ronny-repro c46a3a9920b38164fea4e22ef99b4b66f42e77bf
$ git checkout ronny/new-annotations-try-2 -- pyproject.toml 
$ git commit -am pyproject
$ tox -e linting
$ git commit -am linting
$ git diff ronny-repro..ronny/new-annotations-try-2

Which leads to a more digestable diff:

 changelog/11797.bugfix.rst         |  1 +
 doc/en/conf.py                     | 11 ++++++++++-
 src/_pytest/_code/code.py          | 18 ++++++++++++------
 src/_pytest/assertion/__init__.py  |  2 --
 src/_pytest/capture.py             |  6 +++++-
 src/_pytest/config/__init__.py     | 74 ++++++++++++++++++++++++++++++++++----------------------------------------
 src/_pytest/hookspec.py            |  6 ++++++
 src/_pytest/main.py                |  1 +
 src/_pytest/mark/__init__.py       |  2 --
 src/_pytest/pytester.py            |  4 ++++
 src/_pytest/python.py              |  2 +-
 src/_pytest/python_api.py          | 16 +++++++++++-----
 src/_pytest/recwarn.py             |  8 +++++++-
 src/_pytest/reports.py             |  8 +++-----
 src/_pytest/runner.py              |  1 +
 src/_pytest/threadexception.py     |  9 ++++++++-
 src/_pytest/unraisableexception.py |  7 ++++++-
 testing/python/approx.py           | 40 ++++++++++++++++++++++++++++++++++++++++
 testing/python/fixtures.py         |  2 +-
 testing/test_warnings.py           |  4 ++--
 tox.ini                            | 11 +++++++++--
 21 files changed, 162 insertions(+), 71 deletions(-)
diff --git a/changelog/11797.bugfix.rst b/changelog/11797.bugfix.rst
new file mode 100644
index 000000000..94b72da00
--- /dev/null
+++ b/changelog/11797.bugfix.rst
@@ -0,0 +1 @@
+:func:`pytest.approx` now correctly handles :class:`Sequence <collections.abc.Sequence>`-like objects.
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 2904b141f..afef9b8f6 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -21,9 +21,11 @@
 from textwrap import dedent
 from typing import TYPE_CHECKING
 
-from _pytest import __version__ as version
+from _pytest import __version__ as full_version
 
 
+version = full_version.split("+")[0]
+
 if TYPE_CHECKING:
     import sphinx.application
 
@@ -38,6 +40,9 @@
 autodoc_member_order = "bysource"
 autodoc_typehints = "description"
 autodoc_typehints_description_target = "documented"
+
+
+autodoc2_packages = ["pytest", "_pytest"]
 todo_include_todos = 1
 
 latex_engine = "lualatex"
@@ -178,6 +183,7 @@
     ("py:class", "SubRequest"),
     ("py:class", "TerminalReporter"),
     ("py:class", "_pytest._code.code.TerminalRepr"),
+    ("py:class", "TerminalRepr"),
     ("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
     ("py:class", "_pytest.logging.LogCaptureHandler"),
     ("py:class", "_pytest.mark.structures.ParameterSet"),
@@ -199,13 +205,16 @@
     ("py:class", "_PluggyPlugin"),
     # TypeVars
     ("py:class", "_pytest._code.code.E"),
+    ("py:class", "E"),  # due to delayed annotation
     ("py:class", "_pytest.fixtures.FixtureFunction"),
     ("py:class", "_pytest.nodes._NodeType"),
+    ("py:class", "_NodeType"),  # due to delayed annotation
     ("py:class", "_pytest.python_api.E"),
     ("py:class", "_pytest.recwarn.T"),
     ("py:class", "_pytest.runner.TResult"),
     ("py:obj", "_pytest.fixtures.FixtureValue"),
     ("py:obj", "_pytest.stash.T"),
+    ("py:class", "_ScopeName"),
 ]
 
 
diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py
index d30e80436..9400e834a 100644
--- a/src/_pytest/_code/code.py
+++ b/src/_pytest/_code/code.py
@@ -30,7 +30,10 @@
 from typing import Pattern
 from typing import Sequence
 from typing import SupportsIndex
+from typing import Tuple
+from typing import Type
 from typing import TypeVar
+from typing import Union
 
 import pluggy
 
@@ -53,6 +56,10 @@
 
 TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
 
+EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]]
+
+type_alias = type  #  to sidestep shadowing
+
 
 class Code:
     """Wrapper around Python code objects."""
@@ -592,9 +599,7 @@ def exconly(self, tryshort: bool = False) -> str:
                 text = text[len(self._striptext) :]
         return text
 
-    def errisinstance(
-        self, exc: type[BaseException] | tuple[type[BaseException], ...]
-    ) -> bool:
+    def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
         """Return True if the exception is an instance of exc.
 
         Consider using ``isinstance(excinfo.value, exc)`` instead.
@@ -617,7 +622,8 @@ def getrepr(
         showlocals: bool = False,
         style: TracebackStyle = "long",
         abspath: bool = False,
-        tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
+        tbfilter: bool
+        | Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
         funcargs: bool = False,
         truncate_locals: bool = True,
         truncate_args: bool = True,
@@ -722,7 +728,7 @@ def match(self, regexp: str | Pattern[str]) -> Literal[True]:
     def _group_contains(
         self,
         exc_group: BaseExceptionGroup[BaseException],
-        expected_exception: type[BaseException] | tuple[type[BaseException], ...],
+        expected_exception: EXCEPTION_OR_MORE,
         match: str | Pattern[str] | None,
         target_depth: int | None = None,
         current_depth: int = 1,
@@ -751,7 +757,7 @@ def _group_contains(
 
     def group_contains(
         self,
-        expected_exception: type[BaseException] | tuple[type[BaseException], ...],
+        expected_exception: EXCEPTION_OR_MORE,
         *,
         match: str | Pattern[str] | None = None,
         depth: int | None = None,
diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py
index 357833054..f2f1d029b 100644
--- a/src/_pytest/assertion/__init__.py
+++ b/src/_pytest/assertion/__init__.py
@@ -6,8 +6,6 @@
 import sys
 from typing import Any
 from typing import Generator
-from typing import List
-from typing import Optional
 from typing import TYPE_CHECKING
 
 from _pytest.assertion import rewrite
diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py
index 4272db454..c4dfcc275 100644
--- a/src/_pytest/capture.py
+++ b/src/_pytest/capture.py
@@ -26,6 +26,10 @@
 from typing import TextIO
 from typing import TYPE_CHECKING
 
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
 from _pytest.config import Config
 from _pytest.config import hookimpl
 from _pytest.config.argparsing import Parser
@@ -254,7 +258,7 @@ def writelines(self, lines: Iterable[str]) -> None:
     def writable(self) -> bool:
         return False
 
-    def __enter__(self) -> DontReadFromInput:
+    def __enter__(self) -> Self:
         return self
 
     def __exit__(
diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py
index 7180d2067..23a2c4797 100644
--- a/src/_pytest/config/__init__.py
+++ b/src/_pytest/config/__init__.py
@@ -13,7 +13,7 @@
 import importlib.metadata
 import inspect
 import os
-from pathlib import Path
+import pathlib
 import re
 import shlex
 import sys
@@ -23,22 +23,16 @@
 from typing import Any
 from typing import Callable
 from typing import cast
-from typing import Dict
 from typing import Final
 from typing import final
 from typing import Generator
 from typing import IO
 from typing import Iterable
 from typing import Iterator
-from typing import List
-from typing import Optional
 from typing import Sequence
-from typing import Set
 from typing import TextIO
-from typing import Tuple
 from typing import Type
 from typing import TYPE_CHECKING
-from typing import Union
 import warnings
 
 import pluggy
@@ -120,7 +114,7 @@ class ExitCode(enum.IntEnum):
 class ConftestImportFailure(Exception):
     def __init__(
         self,
-        path: Path,
+        path: pathlib.Path,
         *,
         cause: Exception,
     ) -> None:
@@ -296,7 +290,7 @@ def get_config(
         invocation_params=Config.InvocationParams(
             args=args or (),
             plugins=plugins,
-            dir=Path.cwd(),
+            dir=pathlib.Path.cwd(),
         ),
     )
 
@@ -353,7 +347,7 @@ def _prepareconfig(
         raise
 
 
-def _get_directory(path: Path) -> Path:
+def _get_directory(path: pathlib.Path) -> pathlib.Path:
     """Get the directory of a path - itself if already a directory."""
     if path.is_file():
         return path.parent
@@ -414,9 +408,9 @@ def __init__(self) -> None:
         # All conftest modules applicable for a directory.
         # This includes the directory's own conftest modules as well
         # as those of its parent directories.
-        self._dirpath2confmods: dict[Path, list[types.ModuleType]] = {}
+        self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
         # Cutoff directory above which conftests are no longer discovered.
-        self._confcutdir: Path | None = None
+        self._confcutdir: pathlib.Path | None = None
         # If set, conftest loading is skipped.
         self._noconftest = False
 
@@ -550,12 +544,12 @@ def pytest_configure(self, config: Config) -> None:
     #
     def _set_initial_conftests(
         self,
-        args: Sequence[str | Path],
+        args: Sequence[str | pathlib.Path],
         pyargs: bool,
         noconftest: bool,
-        rootpath: Path,
-        confcutdir: Path | None,
-        invocation_dir: Path,
+        rootpath: pathlib.Path,
+        confcutdir: pathlib.Path | None,
+        invocation_dir: pathlib.Path,
         importmode: ImportMode | str,
         *,
         consider_namespace_packages: bool,
@@ -599,7 +593,7 @@ def _set_initial_conftests(
                 consider_namespace_packages=consider_namespace_packages,
             )
 
-    def _is_in_confcutdir(self, path: Path) -> bool:
+    def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
         """Whether to consider the given path to load conftests from."""
         if self._confcutdir is None:
             return True
@@ -616,9 +610,9 @@ def _is_in_confcutdir(self, path: Path) -> bool:
 
     def _try_load_conftest(
         self,
-        anchor: Path,
+        anchor: pathlib.Path,
         importmode: str | ImportMode,
-        rootpath: Path,
+        rootpath: pathlib.Path,
         *,
         consider_namespace_packages: bool,
     ) -> None:
@@ -641,9 +635,9 @@ def _try_load_conftest(
 
     def _loadconftestmodules(
         self,
-        path: Path,
+        path: pathlib.Path,
         importmode: str | ImportMode,
-        rootpath: Path,
+        rootpath: pathlib.Path,
         *,
         consider_namespace_packages: bool,
     ) -> None:
@@ -671,14 +665,14 @@ def _loadconftestmodules(
                     clist.append(mod)
         self._dirpath2confmods[directory] = clist
 
-    def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
+    def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
         directory = self._get_directory(path)
         return self._dirpath2confmods.get(directory, ())
 
     def _rget_with_confmod(
         self,
         name: str,
-        path: Path,
+        path: pathlib.Path,
     ) -> tuple[types.ModuleType, Any]:
         modules = self._getconftestmodules(path)
         for mod in reversed(modules):
@@ -690,9 +684,9 @@ def _rget_with_confmod(
 
     def _importconftest(
         self,
-        conftestpath: Path,
+        conftestpath: pathlib.Path,
         importmode: str | ImportMode,
-        rootpath: Path,
+        rootpath: pathlib.Path,
         *,
         consider_namespace_packages: bool,
     ) -> types.ModuleType:
@@ -744,7 +738,7 @@ def _importconftest(
     def _check_non_top_pytest_plugins(
         self,
         mod: types.ModuleType,
-        conftestpath: Path,
+        conftestpath: pathlib.Path,
     ) -> None:
         if (
             hasattr(mod, "pytest_plugins")
@@ -1001,15 +995,15 @@ class InvocationParams:
         """The command-line arguments as passed to :func:`pytest.main`."""
         plugins: Sequence[str | _PluggyPlugin] | None
         """Extra plugins, might be `None`."""
-        dir: Path
-        """The directory from which :func:`pytest.main` was invoked."""
+        dir: pathlib.Path
+        """The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path"""
 
         def __init__(
             self,
             *,
             args: Iterable[str],
             plugins: Sequence[str | _PluggyPlugin] | None,
-            dir: Path,
+            dir: pathlib.Path,
         ) -> None:
             object.__setattr__(self, "args", tuple(args))
             object.__setattr__(self, "plugins", plugins)
@@ -1043,7 +1037,7 @@ def __init__(
 
         if invocation_params is None:
             invocation_params = self.InvocationParams(
-                args=(), plugins=None, dir=Path.cwd()
+                args=(), plugins=None, dir=pathlib.Path.cwd()
             )
 
         self.option = argparse.Namespace()
@@ -1094,7 +1088,7 @@ def __init__(
         self.args: list[str] = []
 
     @property
-    def rootpath(self) -> Path:
+    def rootpath(self) -> pathlib.Path:
         """The path to the :ref:`rootdir <rootdir>`.
 
         :type: pathlib.Path
@@ -1104,11 +1098,9 @@ def rootpath(self) -> Path:
         return self._rootpath
 
     @property
-    def inipath(self) -> Path | None:
+    def inipath(self) -> pathlib.Path | None:
         """The path to the :ref:`configfile <configfiles>`.
 
-        :type: Optional[pathlib.Path]
-
         .. versionadded:: 6.1
         """
         return self._inipath
@@ -1319,8 +1311,8 @@ def _decide_args(
         args: list[str],
         pyargs: bool,
         testpaths: list[str],
-        invocation_dir: Path,
-        rootpath: Path,
+        invocation_dir: pathlib.Path,
+        rootpath: pathlib.Path,
         warn: bool,
     ) -> tuple[list[str], ArgsSource]:
         """Decide the args (initial paths/nodeids) to use given the relevant inputs.
@@ -1646,17 +1638,19 @@ def _getini(self, name: str):
         else:
             return self._getini_unknown_type(name, type, value)
 
-    def _getconftest_pathlist(self, name: str, path: Path) -> list[Path] | None:
+    def _getconftest_pathlist(
+        self, name: str, path: pathlib.Path
+    ) -> list[pathlib.Path] | None:
         try:
             mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
         except KeyError:
             return None
         assert mod.__file__ is not None
-        modpath = Path(mod.__file__).parent
-        values: list[Path] = []
+        modpath = pathlib.Path(mod.__file__).parent
+        values: list[pathlib.Path] = []
         for relroot in relroots:
             if isinstance(relroot, os.PathLike):
-                relroot = Path(relroot)
+                relroot = pathlib.Path(relroot)
             else:
                 relroot = relroot.replace("/", os.sep)
                 relroot = absolutepath(modpath / relroot)
diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py
index 13f4fddbd..996148999 100644
--- a/src/_pytest/hookspec.py
+++ b/src/_pytest/hookspec.py
@@ -321,6 +321,7 @@ def pytest_ignore_collect(
     Stops at first non-None result, see :ref:`firstresult`.
 
     :param collection_path: The path to analyze.
+    :type collection_path: pathlib.Path
     :param path: The path to analyze (deprecated).
     :param config: The pytest config object.
 
@@ -354,6 +355,7 @@ def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None:
     Stops at first non-None result, see :ref:`firstresult`.
 
     :param path: The path to analyze.
+    :type path: pathlib.Path
 
     See :ref:`custom directory collectors` for a simple example of use of this
     hook.
@@ -386,6 +388,7 @@ def pytest_collect_file(
     The new node needs to have the specified ``parent`` as a parent.
 
     :param file_path: The path to analyze.
+    :type file_path: pathlib.Path
     :param path: The path to collect (deprecated).
 
     .. versionchanged:: 7.0.0
@@ -507,6 +510,7 @@ def pytest_pycollect_makemodule(
     Stops at first non-None result, see :ref:`firstresult`.
 
     :param module_path: The path of the module to collect.
+    :type module_path: pathlib.Path
     :param path: The path of the module to collect (deprecated).
 
     .. versionchanged:: 7.0.0
@@ -1026,6 +1030,7 @@ def pytest_report_header(  # type:ignore[empty-body]
 
     :param config: The pytest config object.
     :param start_path: The starting dir.
+    :type start_path: pathlib.Path
     :param startdir: The starting dir (deprecated).
 
     .. note::
@@ -1069,6 +1074,7 @@ def pytest_report_collectionfinish(  # type:ignore[empty-body]
 
     :param config: The pytest config object.
     :param start_path: The starting dir.
+    :type start_path: pathlib.Path
     :param startdir: The starting dir (deprecated).
     :param items: List of pytest items that are going to be executed; this list should not be modified.
 
diff --git a/src/_pytest/main.py b/src/_pytest/main.py
index a19ddef58..47ebad471 100644
--- a/src/_pytest/main.py
+++ b/src/_pytest/main.py
@@ -510,6 +510,7 @@ def from_parent(  # type: ignore[override]
 
         :param parent: The parent collector of this Dir.
         :param path: The directory's path.
+        :type path: pathlib.Path
         """
         return super().from_parent(parent=parent, path=path)
 
diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py
index 68b79a11e..b8a309215 100644
--- a/src/_pytest/mark/__init__.py
+++ b/src/_pytest/mark/__init__.py
@@ -5,10 +5,8 @@
 import dataclasses
 from typing import AbstractSet
 from typing import Collection
-from typing import List
 from typing import Optional
 from typing import TYPE_CHECKING
-from typing import Union
 
 from .expression import Expression
 from .expression import ParseError
diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py
index e27648507..5c6ce5e88 100644
--- a/src/_pytest/pytester.py
+++ b/src/_pytest/pytester.py
@@ -909,6 +909,7 @@ def mkdir(self, name: str | os.PathLike[str]) -> Path:
             The name of the directory, relative to the pytester path.
         :returns:
             The created directory.
+        :rtype: pathlib.Path
         """
         p = self.path / name
         p.mkdir()
@@ -932,6 +933,7 @@ def copy_example(self, name: str | None = None) -> Path:
             The name of the file to copy.
         :return:
             Path to the copied directory (inside ``self.path``).
+        :rtype: pathlib.Path
         """
         example_dir_ = self._request.config.getini("pytester_example_dir")
         if example_dir_ is None:
@@ -1390,8 +1392,10 @@ def run(
             - Otherwise, it is passed through to :py:class:`subprocess.Popen`.
               For further information in this case, consult the document of the
               ``stdin`` parameter in :py:class:`subprocess.Popen`.
+        :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int
         :returns:
             The result.
+
         """
         __tracebackhide__ = True
 
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index 2904c3a1e..9182ce7df 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -1168,7 +1168,7 @@ def parametrize(
             If N argnames were specified, argvalues must be a list of
             N-tuples, where each tuple-element specifies a value for its
             respective argname.
-
+        :type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object]
         :param indirect:
             A list of arguments' names (subset of argnames) or a boolean.
             If True the list contains all names from the argnames. Each
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index d55575e4c..c1e851391 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -128,6 +128,8 @@ def _recursive_sequence_map(f, x):
     if isinstance(x, (list, tuple)):
         seq_type = type(x)
         return seq_type(_recursive_sequence_map(f, xi) for xi in x)
+    elif _is_sequence_like(x):
+        return [_recursive_sequence_map(f, xi) for xi in x]
     else:
         return f(x)
 
@@ -720,11 +722,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
     elif _is_numpy_array(expected):
         expected = _as_numpy_array(expected)
         cls = ApproxNumpy
-    elif (
-        hasattr(expected, "__getitem__")
-        and isinstance(expected, Sized)
-        and not isinstance(expected, (str, bytes))
-    ):
+    elif _is_sequence_like(expected):
         cls = ApproxSequenceLike
     elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
         msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
@@ -735,6 +733,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
     return cls(expected, rel, abs, nan_ok)
 
 
+def _is_sequence_like(expected: object) -> bool:
+    return (
+        hasattr(expected, "__getitem__")
+        and isinstance(expected, Sized)
+        and not isinstance(expected, (str, bytes))
+    )
+
+
 def _is_numpy_array(obj: object) -> bool:
     """
     Return true if the given object is implicitly convertible to ndarray,
diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py
index b2a369653..0e34fd0d2 100644
--- a/src/_pytest/recwarn.py
+++ b/src/_pytest/recwarn.py
@@ -13,7 +13,13 @@
 from typing import Iterator
 from typing import overload
 from typing import Pattern
+from typing import TYPE_CHECKING
 from typing import TypeVar
+
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
 import warnings
 
 from _pytest.deprecated import check_ispytest
@@ -222,7 +228,7 @@ def clear(self) -> None:
 
     # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__
     # -- it returns a List but we only emulate one.
-    def __enter__(self) -> WarningsRecorder:  # type: ignore
+    def __enter__(self) -> Self:
         if self._entered:
             __tracebackhide__ = True
             raise RuntimeError(f"Cannot enter {self!r} twice")
diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py
index ae7de506e..2f39adbfa 100644
--- a/src/_pytest/reports.py
+++ b/src/_pytest/reports.py
@@ -14,7 +14,6 @@
 from typing import Mapping
 from typing import NoReturn
 from typing import TYPE_CHECKING
-from typing import TypeVar
 
 from _pytest._code.code import ExceptionChainRepr
 from _pytest._code.code import ExceptionInfo
@@ -35,6 +34,8 @@
 
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from _pytest.runner import CallInfo
 
 
@@ -50,9 +51,6 @@ def getworkerinfoline(node):
         return s
 
 
-_R = TypeVar("_R", bound="BaseReport")
-
-
 class BaseReport:
     when: str | None
     location: tuple[str, int | None, str] | None
@@ -209,7 +207,7 @@ def _to_json(self) -> dict[str, Any]:
         return _report_to_json(self)
 
     @classmethod
-    def _from_json(cls: type[_R], reportdict: dict[str, object]) -> _R:
+    def _from_json(cls, reportdict: dict[str, object]) -> Self:
         """Create either a TestReport or CollectReport, depending on the calling class.
 
         It is the callers responsibility to know which class to pass here.
diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py
index bf30a7d2d..716c4948f 100644
--- a/src/_pytest/runner.py
+++ b/src/_pytest/runner.py
@@ -327,6 +327,7 @@ def from_call(
 
         :param func:
             The function to call. Called without arguments.
+        :type func: Callable[[], _pytest.runner.TResult]
         :param when:
             The phase in which the function is called.
         :param reraise:
diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py
index bfdcd381f..f525a7bca 100644
--- a/src/_pytest/threadexception.py
+++ b/src/_pytest/threadexception.py
@@ -6,11 +6,18 @@
 from typing import Any
 from typing import Callable
 from typing import Generator
+from typing import TYPE_CHECKING
 import warnings
 
 import pytest
 
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+type_alias = type
+
+
 # Copied from cpython/Lib/test/support/threading_helper.py, with modifications.
 class catch_threading_exception:
     """Context manager catching threading.Thread exception using
@@ -40,7 +47,7 @@ def __init__(self) -> None:
     def _hook(self, args: threading.ExceptHookArgs) -> None:
         self.args = args
 
-    def __enter__(self) -> catch_threading_exception:
+    def __enter__(self) -> Self:
         self._old_hook = threading.excepthook
         threading.excepthook = self._hook
         return self
diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py
index 24ab528ee..c191703a3 100644
--- a/src/_pytest/unraisableexception.py
+++ b/src/_pytest/unraisableexception.py
@@ -6,11 +6,16 @@
 from typing import Any
 from typing import Callable
 from typing import Generator
+from typing import TYPE_CHECKING
 import warnings
 
 import pytest
 
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+
 # Copied from cpython/Lib/test/support/__init__.py, with modifications.
 class catch_unraisable_exception:
     """Context manager catching unraisable exception using sys.unraisablehook.
@@ -42,7 +47,7 @@ def _hook(self, unraisable: sys.UnraisableHookArgs) -> None:
         # finalized. Storing unraisable.exc_value creates a reference cycle.
         self.unraisable = unraisable
 
-    def __enter__(self) -> catch_unraisable_exception:
+    def __enter__(self) -> Self:
         self._old_hook = sys.unraisablehook
         sys.unraisablehook = self._hook
         return self
diff --git a/testing/python/approx.py b/testing/python/approx.py
index 17a5d29bc..69743cdbe 100644
--- a/testing/python/approx.py
+++ b/testing/python/approx.py
@@ -953,6 +953,43 @@ def test_allow_ordered_sequences_only(self) -> None:
         with pytest.raises(TypeError, match="only supports ordered sequences"):
             assert {1, 2, 3} == approx({1, 2, 3})
 
+    def test_strange_sequence(self):
+        """https://github.com/pytest-dev/pytest/issues/11797"""
+        a = MyVec3(1, 2, 3)
+        b = MyVec3(0, 1, 2)
+
+        # this would trigger the error inside the test
+        pytest.approx(a, abs=0.5)._repr_compare(b)
+
+        assert b == pytest.approx(a, abs=2)
+        assert b != pytest.approx(a, abs=0.5)
+
+
+class MyVec3:  # incomplete
+    """sequence like"""
+
+    _x: int
+    _y: int
+    _z: int
+
+    def __init__(self, x: int, y: int, z: int):
+        self._x, self._y, self._z = x, y, z
+
+    def __repr__(self) -> str:
+        return f"<MyVec3 {self._x} {self._y} {self._z}>"
+
+    def __len__(self) -> int:
+        return 3
+
+    def __getitem__(self, key: int) -> int:
+        if key == 0:
+            return self._x
+        if key == 1:
+            return self._y
+        if key == 2:
+            return self._z
+        raise IndexError(key)
+
 
 class TestRecursiveSequenceMap:
     def test_map_over_scalar(self):
@@ -980,3 +1017,6 @@ def test_map_over_mixed_sequence(self):
             (5, 8),
             [(7)],
         ]
+
+    def test_map_over_sequence_like(self):
+        assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3]
diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py
index 8d9dd49a8..bc091bb1f 100644
--- a/testing/python/fixtures.py
+++ b/testing/python/fixtures.py
@@ -4516,7 +4516,7 @@ def test_fixture_named_request(pytester: Pytester) -> None:
     result.stdout.fnmatch_lines(
         [
             "*'request' is a reserved word for fixtures, use another name:",
-            "  *test_fixture_named_request.py:6",
+            "  *test_fixture_named_request.py:8",
         ]
     )
 
diff --git a/testing/test_warnings.py b/testing/test_warnings.py
index 3ddcd8d8c..d4d0e0b7f 100644
--- a/testing/test_warnings.py
+++ b/testing/test_warnings.py
@@ -617,11 +617,11 @@ def test_group_warnings_by_message_summary(pytester: Pytester) -> None:
             f"*== {WARNINGS_SUMMARY_HEADER} ==*",
             "test_1.py: 21 warnings",
             "test_2.py: 1 warning",
-            "  */test_1.py:8: UserWarning: foo",
+            "  */test_1.py:10: UserWarning: foo",
             "    warnings.warn(UserWarning(msg))",
             "",
             "test_1.py: 20 warnings",
-            "  */test_1.py:8: UserWarning: bar",
+            "  */test_1.py:10: UserWarning: bar",
             "    warnings.warn(UserWarning(msg))",
             "",
             "-- Docs: *",
diff --git a/tox.ini b/tox.ini
index 35b335a01..dff6e0017 100644
--- a/tox.ini
+++ b/tox.ini
@@ -81,18 +81,25 @@ setenv =
     PYTHONWARNDEFAULTENCODING=
 
 [testenv:docs]
-basepython = python3
+basepython = python3.9 # sync with rtd to get errors
 usedevelop = True
 deps =
     -r{toxinidir}/doc/en/requirements.txt
     # https://github.com/twisted/towncrier/issues/340
     towncrier<21.3.0
+
+
+
 commands =
     python scripts/towncrier-draft-to-file.py
     # the '-t changelog_towncrier_draft' tags makes sphinx include the draft
     # changelog in the docs; this does not happen on ReadTheDocs because it uses
     # the standard sphinx command so the 'changelog_towncrier_draft' is never set there
-    sphinx-build -W --keep-going -b html doc/en doc/en/_build/html -t changelog_towncrier_draft {posargs:}
+    sphinx-build \
+      -j auto \
+      -W --keep-going \
+      -b html doc/en doc/en/_build/html \
+      -t changelog_towncrier_draft {posargs:}
 setenv =
     # Sphinx is not clean of this warning.
     PYTHONWARNDEFAULTENCODING=

@RonnyPfannschmidt RonnyPfannschmidt force-pushed the ronny/new-annotations-try-2 branch 3 times, most recently from 44ad01d to d847844 Compare June 20, 2024 08:57
@RonnyPfannschmidt RonnyPfannschmidt force-pushed the ronny/new-annotations-try-2 branch from 34e0619 to 4e54f19 Compare June 20, 2024 09:04
@RonnyPfannschmidt
Copy link
Member Author

rebased

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jun 20, 2024
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, now I can finally forget about Union, Optional and friends :)

@RonnyPfannschmidt RonnyPfannschmidt removed the needs backport applied to PRs, indicates that it should be ported to the current bug-fix branch label Jun 20, 2024
@RonnyPfannschmidt
Copy link
Member Author

still working on the backport

from _pytest import __version__ as full_version


version = full_version.split("+")[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt this results in the |release| RST substitution having the value of 8.2 which might have a weird perception in changelog draft titles: https://docs.pytest.org/en/8.2.x/changelog.html#to-be-included-in-vrelease-if-present / https://docs.pytest.org/en/latest/changelog.html#to-be-included-in-vrelease-if-present.

Should this be something like +dev in the end but without a variable component, something static? Though, perhaps in RTD it'd be okay to use the long original version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I cut off too much

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I cut off too much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants