Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

from __future__ import annotations + migrate #12467

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=

doc/en/conf.py Outdated Show resolved Hide resolved
src/_pytest/config/__init__.py Show resolved Hide resolved
src/_pytest/hookspec.py Show resolved Hide resolved
src/_pytest/recwarn.py Outdated Show resolved Hide resolved
src/_pytest/_code/code.py Outdated Show resolved Hide resolved
@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
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

@RonnyPfannschmidt RonnyPfannschmidt merged commit bbe6b4a into pytest-dev:main Jun 21, 2024
28 checks passed


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