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

4 participants