diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b372cf94d1..41a0773e1af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,24 +56,22 @@ jobs: fail-fast: false matrix: name: [ - "windows-py39-unittest-asynctest", - "windows-py39-unittest-twisted24", - "windows-py39-unittest-twisted25", - "windows-py39-pluggy", - "windows-py39-xdist", - "windows-py310", + "windows-py310-unittest-asynctest", + "windows-py310-unittest-twisted24", + "windows-py310-unittest-twisted25", + "windows-py310-pluggy", + "windows-py310-xdist", "windows-py311", "windows-py312", "windows-py313", "windows-py314", - "ubuntu-py39-unittest-asynctest", - "ubuntu-py39-unittest-twisted24", - "ubuntu-py39-unittest-twisted25", - "ubuntu-py39-lsof-numpy-pexpect", - "ubuntu-py39-pluggy", - "ubuntu-py39-freeze", - "ubuntu-py39-xdist", + "ubuntu-py310-unittest-asynctest", + "ubuntu-py310-unittest-twisted24", + "ubuntu-py310-unittest-twisted25", + "ubuntu-py310-lsof-numpy-pexpect", + "ubuntu-py310-pluggy", + "ubuntu-py310-freeze", "ubuntu-py310-xdist", "ubuntu-py311", "ubuntu-py312", @@ -81,7 +79,6 @@ jobs: "ubuntu-py314", "ubuntu-pypy3-xdist", - "macos-py39", "macos-py310", "macos-py312", "macos-py313", @@ -93,35 +90,30 @@ jobs: include: # Use separate jobs for different unittest flavors (twisted, asynctest) to ensure proper coverage. - - name: "windows-py39-unittest-asynctest" - python: "3.9" + - name: "windows-py310-unittest-asynctest" + python: "3.10" os: windows-latest - tox_env: "py39-asynctest" + tox_env: "py310-asynctest" use_coverage: true - - name: "windows-py39-unittest-twisted24" - python: "3.9" + - name: "windows-py310-unittest-twisted24" + python: "3.10" os: windows-latest - tox_env: "py39-twisted24" + tox_env: "py310-twisted24" use_coverage: true - - name: "windows-py39-unittest-twisted25" - python: "3.9" + - name: "windows-py310-unittest-twisted25" + python: "3.10" os: windows-latest - tox_env: "py39-twisted25" + tox_env: "py310-twisted25" use_coverage: true - - name: "windows-py39-pluggy" - python: "3.9" - os: windows-latest - tox_env: "py39-pluggymain-pylib-xdist" - - - name: "windows-py39-xdist" - python: "3.9" + - name: "windows-py310-pluggy" + python: "3.10" os: windows-latest - tox_env: "py39-xdist" + tox_env: "py310-pluggymain-pylib-xdist" - - name: "windows-py310" + - name: "windows-py310-xdist" python: "3.10" os: windows-latest tox_env: "py310-xdist" @@ -147,44 +139,39 @@ jobs: tox_env: "py314" # Use separate jobs for different unittest flavors (twisted, asynctest) to ensure proper coverage. - - name: "ubuntu-py39-unittest-asynctest" - python: "3.9" + - name: "ubuntu-py310-unittest-asynctest" + python: "3.10" os: ubuntu-latest - tox_env: "py39-asynctest" + tox_env: "py310-asynctest" use_coverage: true - - name: "ubuntu-py39-unittest-twisted24" - python: "3.9" + - name: "ubuntu-py310-unittest-twisted24" + python: "3.10" os: ubuntu-latest - tox_env: "py39-twisted24" + tox_env: "py310-twisted24" use_coverage: true - - name: "ubuntu-py39-unittest-twisted25" - python: "3.9" + - name: "ubuntu-py310-unittest-twisted25" + python: "3.10" os: ubuntu-latest - tox_env: "py39-twisted25" + tox_env: "py310-twisted25" use_coverage: true - - name: "ubuntu-py39-lsof-numpy-pexpect" - python: "3.9" + - name: "ubuntu-py310-lsof-numpy-pexpect" + python: "3.10" os: ubuntu-latest - tox_env: "py39-lsof-numpy-pexpect" + tox_env: "py310-lsof-numpy-pexpect" use_coverage: true - - name: "ubuntu-py39-pluggy" - python: "3.9" - os: ubuntu-latest - tox_env: "py39-pluggymain-pylib-xdist" - - - name: "ubuntu-py39-freeze" - python: "3.9" + - name: "ubuntu-py310-pluggy" + python: "3.10" os: ubuntu-latest - tox_env: "py39-freeze" + tox_env: "py310-pluggymain-pylib-xdist" - - name: "ubuntu-py39-xdist" - python: "3.9" + - name: "ubuntu-py310-freeze" + python: "3.10" os: ubuntu-latest - tox_env: "py39-xdist" + tox_env: "py310-freeze" - name: "ubuntu-py310-xdist" python: "3.10" @@ -216,17 +203,11 @@ jobs: use_coverage: true - name: "ubuntu-pypy3-xdist" - python: "pypy-3.9" + python: "pypy-3.10" os: ubuntu-latest tox_env: "pypy3-xdist" - - name: "macos-py39" - python: "3.9" - os: macos-latest - tox_env: "py39-xdist" - use_coverage: true - - name: "macos-py310" python: "3.10" os: macos-latest @@ -254,7 +235,7 @@ jobs: - name: "doctesting" - python: "3.9" + python: "3.10" os: ubuntu-latest tox_env: "doctesting" use_coverage: true @@ -264,12 +245,12 @@ jobs: contains( fromJSON( '[ - "windows-py39-pluggy", + "windows-py310-pluggy", "windows-py313", - "ubuntu-py39-pluggy", - "ubuntu-py39-freeze", + "ubuntu-py310-pluggy", + "ubuntu-py310-freeze", "ubuntu-py313", - "macos-py39", + "macos-py310", "macos-py313" ]' ), diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b17b032dce2..4b7ea12fa4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: hooks: - id: pyupgrade args: - - "--py39-plus" + - "--py310-plus" # Manual because ruff does what pyupgrade does and the two are not out of sync # often enough to make launching pyupgrade everytime worth it stages: [manual] diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b79955e1c01..e98dd06fb5a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -197,7 +197,7 @@ Short version #. Follow `PEP-8 `_ for naming. #. Tests are run using ``tox``:: - tox -e linting,py39 + tox -e linting,py313 The test environments above are usually enough to cover most cases locally. @@ -269,24 +269,24 @@ Here is a simple overview, with pytest-specific bits: #. Run all the tests - You need to have Python 3.9 or later available in your system. Now + You need to have a supported Python version available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py39 + $ tox -e linting,py - This command will run tests via the "tox" tool against Python 3.9 - and also perform "lint" coding-style checks. + This command will run tests via the "tox" tool against your default Python + version and also perform "lint" coding-style checks. #. You can now edit your local working copy and run the tests again as necessary. Please follow `PEP-8 `_ for naming. - You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest + You can pass different options to ``tox``. For example, to run tests on Python 3.13 and pass options to pytest (e.g. enter pdb on failure) to pytest you can do:: - $ tox -e py39 -- --pdb + $ tox -e py313 -- --pdb - Or to only run tests in a particular test module on Python 3.9:: + Or to only run tests in a particular test module on Python 3.12:: - $ tox -e py39 -- testing/test_config.py + $ tox -e py312 -- testing/test_config.py When committing, ``pre-commit`` will re-format the files if necessary. diff --git a/README.rst b/README.rst index 091afc363da..bf9cd445884 100644 --- a/README.rst +++ b/README.rst @@ -97,7 +97,7 @@ Features - Can run `unittest `_ (or trial) test suites out of the box -- Python 3.9+ or PyPy3 +- Python 3.10+ or PyPy3 - Rich plugin architecture, with over 1300+ `external plugins `_ and thriving community diff --git a/changelog/13719.breaking.rst b/changelog/13719.breaking.rst new file mode 100644 index 00000000000..328c7dfcf2b --- /dev/null +++ b/changelog/13719.breaking.rst @@ -0,0 +1 @@ +Support for Python 3.9 is dropped following its end of life. diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index f54524213bc..c04a2868812 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -10,7 +10,7 @@ import pytest -pythonlist = ["python3.9", "python3.10", "python3.11"] +pythonlist = ["python3.11", "python3.12", "python3.13"] @pytest.fixture(params=pythonlist) diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 349711faaf4..4089e0e5867 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -9,8 +9,6 @@ Get Started Install ``pytest`` ---------------------------------------- -``pytest`` requires: Python 3.8+ or PyPy3. - 1. Run the following command in your command line: .. code-block:: bash diff --git a/doc/en/how-to/skipping.rst b/doc/en/how-to/skipping.rst index 09a19766f99..6584b1c7b24 100644 --- a/doc/en/how-to/skipping.rst +++ b/doc/en/how-to/skipping.rst @@ -84,14 +84,14 @@ It is also possible to skip the whole module using If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on an interpreter earlier than Python3.10: +when run on an interpreter earlier than Python3.13: .. code-block:: python import sys - @pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") + @pytest.mark.skipif(sys.version_info < (3, 13), reason="requires python3.13 or higher") def test_function(): ... If the condition evaluates to ``True`` during collection, the test function will be skipped, diff --git a/doc/en/index.rst b/doc/en/index.rst index 2b58bebc20f..5b139800e0d 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -46,8 +46,6 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. -``pytest`` requires: Python 3.8+ or PyPy3. - **PyPI package name**: :pypi:`pytest` A quick example @@ -104,7 +102,7 @@ Features - Can run :ref:`unittest ` (including trial) test suites out of the box -- Python 3.8+ or PyPy 3 +- Python 3.10+ or PyPy 3 - Rich plugin architecture, with over 1300+ :ref:`external plugins ` and thriving community diff --git a/pyproject.toml b/pyproject.toml index a277961ccb4..12c51078a8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ authors = [ { name = "Florian Bruhin" }, { name = "Others (See AUTHORS)" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", @@ -33,7 +33,6 @@ classifiers = [ "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -86,10 +85,10 @@ write_to = "src/_pytest/_version.py" [tool.black] # See https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#t-target-version -target-version = [ "py39", "py310", "py311", "py312", "py313" ] +target-version = [ "py310", "py311", "py312", "py313" ] [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 88 src = [ "src", @@ -520,7 +519,7 @@ files = [ mypy_path = [ "src", ] -python_version = "3.9" +python_version = "3.10" check_untyped_defs = true disallow_any_generics = true disallow_untyped_defs = true @@ -543,7 +542,7 @@ include = [ extraPaths = [ "src", ] -pythonVersion = "3.9" +pythonVersion = "3.10" typeCheckingMode = "basic" reportMissingImports = "none" reportMissingModuleSource = "none" diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index f1241f14136..06036d21956 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -30,9 +30,8 @@ from typing import Literal from typing import overload from typing import SupportsIndex -from typing import TYPE_CHECKING +from typing import TypeAlias from typing import TypeVar -from typing import Union import pluggy @@ -55,7 +54,7 @@ TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] -EXCEPTION_OR_MORE = Union[type[BaseException], tuple[type[BaseException], ...]] +EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...] class Code: @@ -469,7 +468,7 @@ def stringify_exception( notes = getattr(exc, "__notes__", []) except KeyError: # Workaround for https://github.com/python/cpython/issues/98778 on - # Python <= 3.9, and some 3.10 and 3.11 patch versions. + # some 3.10 and 3.11 patch versions. HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ()) if sys.version_info < (3, 12) and isinstance(exc, HTTPError): notes = [] @@ -853,15 +852,10 @@ def group_contains( return self._group_contains(self.value, expected_exception, match, depth) -if TYPE_CHECKING: - from typing_extensions import TypeAlias - - # Type alias for the `tbfilter` setting: - # bool: If True, it should be filtered using Traceback.filter() - # callable: A callable that takes an ExceptionInfo and returns the filtered traceback. - TracebackFilter: TypeAlias = Union[ - bool, Callable[[ExceptionInfo[BaseException]], Traceback] - ] +# Type alias for the `tbfilter` setting: +# bool: If True, it should be filtered using Traceback.filter() +# callable: A callable that takes an ExceptionInfo and returns the filtered traceback. +TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback] @dataclasses.dataclass diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index a8f7201a40f..280691f0b6c 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -26,7 +26,7 @@ def __init__(self, obj: object = None) -> None: elif isinstance(obj, Source): self.lines = obj.lines self.raw_lines = obj.raw_lines - elif isinstance(obj, (tuple, list)): + elif isinstance(obj, tuple | list): self.lines = deindent(x.rstrip("\n") for x in obj) self.raw_lines = list(x.rstrip("\n") for x in obj) elif isinstance(obj, str): @@ -155,9 +155,9 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None # AST's line numbers start indexing at 1. values: list[int] = [] for x in ast.walk(node): - if isinstance(x, (ast.stmt, ast.ExceptHandler)): + if isinstance(x, ast.stmt | ast.ExceptHandler): # The lineno points to the class/def, so need to include the decorators. - if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef): for d in x.decorator_list: values.append(d.lineno - 1) values.append(x.lineno - 1) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index fd808f8b3b7..68fe4994555 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -198,7 +198,8 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No indents = [""] * len(lines) source = "\n".join(lines) new_lines = self._highlight(source).splitlines() - for indent, new_line in zip(indents, new_lines): + # Would be better to strict=True but that fails some CI jobs. + for indent, new_line in zip(indents, new_lines, strict=False): self.line(indent + new_line) def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer: diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index e353c1a9b52..878fc7a538b 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -432,7 +432,7 @@ def relto(self, relpath): """Return a string which is the relative part of the path to the given 'relpath'. """ - if not isinstance(relpath, (str, LocalPath)): + if not isinstance(relpath, str | LocalPath): raise TypeError(f"{relpath!r}: not a string or path object") strrelpath = str(relpath) if strrelpath and strrelpath[-1] != self.sep: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b07f8b24b57..bff33ccf155 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -26,6 +26,17 @@ from typing import IO from typing import TYPE_CHECKING + +if sys.version_info >= (3, 12): + from importlib.resources.abc import TraversableResources +else: + from importlib.abc import TraversableResources +if sys.version_info < (3, 11): + from importlib.readers import FileReader +else: + from importlib.resources.readers import FileReader + + from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr_unlimited @@ -291,19 +302,8 @@ def get_data(self, pathname: str | bytes) -> bytes: with open(pathname, "rb") as f: return f.read() - if sys.version_info >= (3, 10): - if sys.version_info >= (3, 12): - from importlib.resources.abc import TraversableResources - else: - from importlib.abc import TraversableResources - - def get_resource_reader(self, name: str) -> TraversableResources: - if sys.version_info < (3, 11): - from importlib.readers import FileReader - else: - from importlib.resources.readers import FileReader - - return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) + def get_resource_reader(self, name: str) -> TraversableResources: + return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type] def _write_pyc_fp( @@ -496,7 +496,7 @@ def _call_reprcompare( expls: Sequence[str], each_obj: Sequence[object], ) -> str: - for i, res, expl in zip(range(len(ops)), results, expls): + for i, res, expl in zip(range(len(ops)), results, expls, strict=True): try: done = not res except Exception: @@ -729,21 +729,15 @@ def run(self, mod: ast.Module) -> None: else: lineno = item.lineno # Now actually insert the special imports. - if sys.version_info >= (3, 10): - aliases = [ - ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0), - ast.alias( - "_pytest.assertion.rewrite", - "@pytest_ar", - lineno=lineno, - col_offset=0, - ), - ] - else: - aliases = [ - ast.alias("builtins", "@py_builtins"), - ast.alias("_pytest.assertion.rewrite", "@pytest_ar"), - ] + aliases = [ + ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0), + ast.alias( + "_pytest.assertion.rewrite", + "@pytest_ar", + lineno=lineno, + col_offset=0, + ), + ] imports = [ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases ] @@ -754,7 +748,7 @@ def run(self, mod: ast.Module) -> None: nodes: list[ast.AST | Sentinel] = [mod] while nodes: node = nodes.pop() - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef): self.scope = tuple((*self.scope, node)) nodes.append(_SCOPE_END_MARKER) if node == _SCOPE_END_MARKER: @@ -1132,12 +1126,12 @@ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: if isinstance(comp.left, ast.NamedExpr): self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment] left_res, left_expl = self.visit(comp.left) - if isinstance(comp.left, (ast.Compare, ast.BoolOp)): + if isinstance(comp.left, ast.Compare | ast.BoolOp): left_expl = f"({left_expl})" res_variables = [self.variable() for i in range(len(comp.ops))] load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables] store_names = [ast.Name(v, ast.Store()) for v in res_variables] - it = zip(range(len(comp.ops)), comp.ops, comp.comparators) + it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True) expls: list[ast.expr] = [] syms: list[ast.expr] = [] results = [left_res] @@ -1150,7 +1144,7 @@ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: next_operand.target.id = self.variable() self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment] next_res, next_expl = self.visit(next_operand) - if isinstance(next_operand, (ast.Compare, ast.BoolOp)): + if isinstance(next_operand, ast.Compare | ast.BoolOp): next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c545e6cd20c..cc499f7186f 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -131,7 +131,7 @@ def isdict(x: Any) -> bool: def isset(x: Any) -> bool: - return isinstance(x, (set, frozenset)) + return isinstance(x, set | frozenset) def isnamedtuple(obj: Any) -> bool: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 7f30e72007e..4383f105af6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -256,7 +256,7 @@ def pytest_make_collect_report( self, collector: nodes.Collector ) -> Generator[None, CollectReport, CollectReport]: res = yield - if isinstance(collector, (Session, Directory)): + if isinstance(collector, Session | Directory): # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 15bfbb0613e..763803adbe7 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -5,7 +5,7 @@ import os from pathlib import Path import sys -from typing import TYPE_CHECKING +from typing import TypeAlias import iniconfig @@ -16,14 +16,9 @@ from _pytest.pathlib import safe_exists -if TYPE_CHECKING: - from typing import Union - - from typing_extensions import TypeAlias - - # Even though TOML supports richer data types, all values are converted to str/list[str] during - # parsing to maintain compatibility with the rest of the configuration system. - ConfigDict: TypeAlias = dict[str, Union[str, list[str]]] +# Even though TOML supports richer data types, all values are converted to str/list[str] during +# parsing to maintain compatibility with the rest of the configuration system. +ConfigDict: TypeAlias = dict[str, str | list[str]] def _parse_ini_config(path: Path) -> iniconfig.IniConfig: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 0dbef6056d7..cd255f5eeb6 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -324,7 +324,7 @@ def repr_failure( # type: ignore[override] Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None ) = None if isinstance( - excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) + excinfo.value, doctest.DocTestFailure | doctest.UnexpectedException ): failures = [excinfo.value] elif isinstance(excinfo.value, MultipleDoctestFailures): @@ -530,24 +530,6 @@ def _find_lineno(self, obj, source_lines): source_lines, ) - if sys.version_info < (3, 10): - - def _find( - self, tests, obj, name, module, source_lines, globs, seen - ) -> None: - """Override _find to work around issue in stdlib. - - https://github.com/pytest-dev/pytest/issues/3456 - https://github.com/python/cpython/issues/69718 - """ - if _is_mocked(obj): - return # pragma: no cover - with _patch_unwrap_mock_aware(): - # Type ignored because this is a private function. - super()._find( # type:ignore[misc] - tests, obj, name, module, source_lines, globs, seen - ) - if sys.version_info < (3, 13): def _from_module(self, module, object): @@ -657,7 +639,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: if len(wants) != len(gots): return got offset = 0 - for w, g in zip(wants, gots): + for w, g in zip(wants, gots, strict=True): fraction: str | None = w.group("fraction") exponent: str | None = w.group("exponent1") if exponent is None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bc5805aaea9..91f1b3a67f6 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -26,11 +26,9 @@ from typing import final from typing import Generic from typing import NoReturn -from typing import Optional from typing import overload from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union import warnings import _pytest @@ -88,26 +86,24 @@ # The type of the fixture function (type variable). FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object]) # The type of a fixture function (type alias generic in fixture value). -_FixtureFunc = Union[ - Callable[..., FixtureValue], Callable[..., Generator[FixtureValue]] -] +_FixtureFunc = Callable[..., FixtureValue] | Callable[..., Generator[FixtureValue]] # The type of FixtureDef.cached_result (type alias generic in fixture value). -_FixtureCachedResult = Union[ +_FixtureCachedResult = ( tuple[ # The result. FixtureValue, # Cache key. object, None, - ], - tuple[ + ] + | tuple[ None, # Cache key. object, # The exception and the original traceback. - tuple[BaseException, Optional[types.TracebackType]], - ], -] + tuple[BaseException, types.TracebackType | None], + ] +) @dataclasses.dataclass(frozen=True) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 068c7410a46..cd59069559e 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -7,7 +7,6 @@ from collections.abc import Iterable from collections.abc import Set as AbstractSet import dataclasses -from typing import Optional from typing import TYPE_CHECKING from .expression import Expression @@ -45,7 +44,7 @@ ] -old_mark_config_key = StashKey[Optional[Config]]() +old_mark_config_key = StashKey[Config | None]() def param( diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f9261076ad0..04c37796e10 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -18,7 +18,6 @@ from typing import overload from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union import warnings from .._code import getfslineno @@ -302,7 +301,7 @@ def combined_with(self, other: Mark) -> Mark: # A generic parameter designating an object to which a Mark may # be applied -- a test function (callable) or class. # Note: a lambda is not allowed, but this can't be represented. -Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) +Markable = TypeVar("Markable", bound=Callable[..., object] | type) @dataclasses.dataclass @@ -396,7 +395,7 @@ def __call__(self, *args: object, **kwargs: object): # For staticmethods/classmethods, the marks are eventually fetched from the # function object, not the descriptor, so unwrap. unwrapped_func = func - if isinstance(func, (staticmethod, classmethod)): + if isinstance(func, staticmethod | classmethod): unwrapped_func = func.__func__ if len(args) == 1 and (istestfunc(unwrapped_func) or is_class): store_mark(unwrapped_func, self.mark, stacklevel=3) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index d6b90687a6f..cd15434605d 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -348,7 +348,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: entries = find_prefixed(root, prefix) entries, entries2 = itertools.tee(entries) numbers = map(parse_num, extract_suffixes(entries2, prefix)) - for entry, number in zip(entries, numbers): + for entry, number in zip(entries, numbers, strict=True): if number <= max_delete: yield Path(entry) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8e4fb041532..3f9da026799 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -210,7 +210,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: def pytest_pycollect_makeitem( collector: Module | Class, name: str, obj: object ) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]: - assert isinstance(collector, (Class, Module)), type(collector) + assert isinstance(collector, Class | Module), type(collector) # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): if collector.istestclass(obj, name): @@ -358,7 +358,7 @@ def classnamefilter(self, name: str) -> bool: def istestfunction(self, obj: object, name: str) -> bool: if self.funcnamefilter(name) or self.isnosetest(obj): - if isinstance(obj, (staticmethod, classmethod)): + if isinstance(obj, staticmethod | classmethod): # staticmethods and classmethods need to be unwrapped. obj = safe_getattr(obj, "__func__", False) return callable(obj) and fixtures.getfixturemarker(obj) is None @@ -944,7 +944,9 @@ def _resolve_ids(self) -> Iterable[str | _HiddenParam]: # ID not provided - generate it. yield "-".join( self._idval(val, argname, idx) - for val, argname in zip(parameterset.values, self.argnames) + for val, argname in zip( + parameterset.values, self.argnames, strict=True + ) ) def _idval(self, val: object, argname: str, idx: int) -> str: @@ -989,9 +991,9 @@ def _idval_from_hook(self, val: object, argname: str) -> str | None: def _idval_from_value(self, val: object) -> str | None: """Try to make an ID for a parameter in a ParameterSet from its value, if the value type is supported.""" - if isinstance(val, (str, bytes)): + if isinstance(val, str | bytes): return _ascii_escaped_by_config(val, self.config) - elif val is None or isinstance(val, (float, int, bool, complex)): + elif val is None or isinstance(val, float | int | bool | complex): return str(val) elif isinstance(val, re.Pattern): return ascii_escaped(val.pattern) @@ -1079,7 +1081,7 @@ def setmulti( params = self.params.copy() indices = self.indices.copy() arg2scope = dict(self._arg2scope) - for arg, val in zip(argnames, valset): + for arg, val in zip(argnames, valset, strict=True): if arg in params: raise nodes.Collector.CollectError( f"{nodeid}: duplicate parametrization of {arg!r}" @@ -1336,7 +1338,7 @@ def parametrize( newcalls = [] for callspec in self._calls or [CallSpec2()]: for param_index, (param_id, param_set) in enumerate( - zip(ids, parametersets) + zip(ids, parametersets, strict=True) ): newcallspec = callspec.setmulti( argnames=argnames, diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 52e564bd809..8abd054a60a 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -113,7 +113,7 @@ def _check_type(self) -> None: def _recursive_sequence_map(f, x): """Recursively map a function over a sequence of arbitrary depth""" - if isinstance(x, (list, tuple)): + 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): @@ -245,7 +245,7 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: max_rel_diff = -math.inf different_ids = [] for (approx_key, approx_value), other_value in zip( - approx_side_as_map.items(), other_side.values() + approx_side_as_map.items(), other_side.values(), strict=True ): if approx_value != other_value: if approx_value.expected is not None and other_value is not None: @@ -327,7 +327,7 @@ def _repr_compare(self, other_side: Sequence[float]) -> list[str]: max_rel_diff = -math.inf different_ids = [] for i, (approx_value, other_value) in enumerate( - zip(approx_side_as_map, other_side) + zip(approx_side_as_map, other_side, strict=True) ): if approx_value != other_value: try: @@ -365,7 +365,7 @@ def __eq__(self, actual) -> bool: return super().__eq__(actual) def _yield_comparisons(self, actual): - return zip(actual, self.expected) + return zip(actual, self.expected, strict=True) def _check_type(self) -> None: __tracebackhide__ = True @@ -394,7 +394,7 @@ def __repr__(self) -> str: # handle complex numbers, e.g. (inf + 1j). if ( isinstance(self.expected, bool) - or (not isinstance(self.expected, (Complex, Decimal))) + or (not isinstance(self.expected, Complex | Decimal)) or math.isinf(abs(self.expected) or isinstance(self.expected, bool)) ): return str(self.expected) @@ -447,8 +447,8 @@ def is_bool(val: Any) -> bool: # __sub__, and __float__ are defined. Also, consider bool to be # non-numeric, even though it has the required arithmetic. if is_bool(self.expected) or not ( - isinstance(self.expected, (Complex, Decimal)) - and isinstance(actual, (Complex, Decimal)) + isinstance(self.expected, Complex | Decimal) + and isinstance(actual, Complex | Decimal) ): return False @@ -766,7 +766,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: cls = ApproxNumpy elif _is_sequence_like(expected): cls = ApproxSequenceLike - elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)): + elif isinstance(expected, Collection) and not isinstance(expected, str | bytes): msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" raise TypeError(msg) else: @@ -779,7 +779,7 @@ def _is_sequence_like(expected: object) -> bool: return ( hasattr(expected, "__getitem__") and isinstance(expected, Sized) - and not isinstance(expected, (str, bytes)) + and not isinstance(expected, str | bytes) ) diff --git a/src/_pytest/raises.py b/src/_pytest/raises.py index 78fae6ddcde..9066779a8af 100644 --- a/src/_pytest/raises.py +++ b/src/_pytest/raises.py @@ -29,9 +29,9 @@ # for some reason Sphinx does not play well with 'from types import TracebackType' import types + from typing import TypeGuard from typing_extensions import ParamSpec - from typing_extensions import TypeGuard from typing_extensions import TypeVar P = ParamSpec("P") diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 480ffae1f9c..2122c021020 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -459,7 +459,7 @@ def toterminal(self, out: TerminalWriter) -> None: def pytest_report_to_serializable( report: CollectReport | TestReport, ) -> dict[str, Any] | None: - if isinstance(report, (TestReport, CollectReport)): + if isinstance(report, TestReport | CollectReport): data = report._to_json() data["$report_type"] = report.__class__.__name__ return data diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 26e4e838b77..ec08025d897 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -262,7 +262,7 @@ def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> b if hasattr(report, "wasxfail"): # Exception was expected. return False - if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)): + if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit): # Special control flow exception. return False return True diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index ec118f2c92f..6ba30c4574c 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -10,7 +10,6 @@ import platform import sys import traceback -from typing import Optional from _pytest.config import Config from _pytest.config import hookimpl @@ -236,7 +235,7 @@ def evaluate_xfail_marks(item: Item) -> Xfail | None: # Saves the xfail mark evaluation. Can be refreshed during call if None. -xfailed_key = StashKey[Optional[Xfail]]() +xfailed_key = StashKey[Xfail | None]() @hookimpl(tryfirst=True) @@ -285,7 +284,7 @@ def pytest_runtest_makereport( raises = xfailed.raises if raises is None or ( ( - isinstance(raises, (type, tuple)) + isinstance(raises, type | tuple) and isinstance(call.excinfo.value, raises) ) or ( diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index b18ac56b811..341173ee6ac 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -14,7 +14,6 @@ import traceback import types from typing import TYPE_CHECKING -from typing import Union import _pytest._code from _pytest.compat import is_async_function @@ -43,10 +42,10 @@ import twisted.trial.unittest -_SysExcInfoType = Union[ - tuple[type[BaseException], BaseException, types.TracebackType], - tuple[None, None, None], -] +_SysExcInfoType = ( + tuple[type[BaseException], BaseException, types.TracebackType] + | tuple[None, None, None] +) def pytest_pycollect_makeitem( diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 7064d1daa9b..ab62e93b223 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -18,8 +18,7 @@ @contextlib.contextmanager def ignore_encoding_warning(): with warnings.catch_warnings(): - if sys.version_info >= (3, 10): - warnings.simplefilter("ignore", EncodingWarning) # noqa: F821 + warnings.simplefilter("ignore", EncodingWarning) yield diff --git a/testing/conftest.py b/testing/conftest.py index 25abce913ea..663c9d80b3e 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -111,7 +111,7 @@ def write(self, msg, **kw): def _write_source(self, lines, indents=()): if not indents: indents = [""] * len(lines) - for indent, line in zip(indents, lines): + for indent, line in zip(indents, lines, strict=True): self.line(indent + line) def line(self, line, **kw): diff --git a/testing/python/approx.py b/testing/python/approx.py index 06633b544ec..4756b90b267 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -77,7 +77,7 @@ def do_assert(lhs, rhs, expected_message, verbosity_level=0): ) for i, (obtained_line, expected_line) in enumerate( - zip(obtained_message, expected_message) + zip(obtained_message, expected_message, strict=True) ): regex = re.compile(expected_line) assert regex.match(obtained_line) is not None, ( diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index fb76fe6cf96..b58ddc6e162 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +from itertools import zip_longest import os from pathlib import Path import sys @@ -3043,7 +3044,7 @@ def test_4(modarg, arg): ] import pprint - pprint.pprint(list(zip(values, expected))) + pprint.pprint(list(zip_longest(values, expected))) assert values == expected def test_parametrized_fixture_teardown_order(self, pytester: Pytester) -> None: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 7ae26de3a18..010c22f5c0c 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -429,7 +429,7 @@ def test_idmaker_autoname(self) -> None: def test_idmaker_with_bytes_regex(self) -> None: result = IdMaker( - ("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None + ("a"), [pytest.param(re.compile(b"foo"))], None, None, None, None, None ).make_unique_parameterset_ids() assert result == ["foo"] diff --git a/testing/python/raises.py b/testing/python/raises.py index 40f9afea3ba..9e3fe304528 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -395,8 +395,8 @@ class NotAnException: def test_issue_11872(self) -> None: """Regression test for #11872. - urllib.error.HTTPError on Python<=3.9 raises KeyError instead of - AttributeError on invalid attribute access. + urllib.error.HTTPError on some Python 3.10/11 minor releases raises + KeyError instead of AttributeError on invalid attribute access. https://github.com/python/cpython/issues/98778 """ diff --git a/testing/python/raises_group.py b/testing/python/raises_group.py index 386e127a13d..6b9f98201dd 100644 --- a/testing/python/raises_group.py +++ b/testing/python/raises_group.py @@ -292,7 +292,7 @@ def test_catch_unwrapped_exceptions() -> None: # if users want one of several exception types they need to use a RaisesExc # (which the error message suggests) with RaisesGroup( - RaisesExc(check=lambda e: isinstance(e, (SyntaxError, ValueError))), + RaisesExc(check=lambda e: isinstance(e, SyntaxError | ValueError)), allow_unwrapped=True, ): raise ValueError diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index f13c71352de..18bc32dc86f 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -132,7 +132,7 @@ def test_location_is_set(self) -> None: if isinstance(node, ast.Import): continue for n in [node, *ast.iter_child_nodes(node)]: - assert isinstance(n, (ast.stmt, ast.expr)) + assert isinstance(n, ast.stmt | ast.expr) for location in [ (n.lineno, n.col_offset), (n.end_lineno, n.end_col_offset), @@ -2263,10 +2263,6 @@ def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: assert get_cache_dir(Path(source)) == Path(expected) - @pytest.mark.skipif( - sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), - reason="#9298", - ) def test_sys_pycache_prefix_integration( self, tmp_path, monkeypatch, pytester: Pytester ) -> None: diff --git a/testing/test_compat.py b/testing/test_compat.py index 3722bfcfb40..fa9e259647f 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: - from typing_extensions import Literal + from typing import Literal def test_real_func_loop_limit() -> None: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 9f18a90d100..4f9702149a8 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -661,8 +661,7 @@ def test_func(arg1): node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) tnodes = node.find_by_tag("testcase") - assert len(tnodes) == 3 - for tnode, char in zip(tnodes, "<&'"): + for tnode, char in zip(tnodes, "<&'", strict=True): tnode.assert_attr( classname="test_failure_escape", name=f"test_func[{char}]" ) diff --git a/testing/test_reports.py b/testing/test_reports.py index 7a893981838..5ffbde563b6 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -101,8 +101,7 @@ def test_repr_entry(): rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries - assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True) - for a_entry, rep_entry in zip(a_entries, rep_entries): + for a_entry, rep_entry in zip(a_entries, rep_entries, strict=True): assert isinstance(rep_entry, ReprEntry) assert rep_entry.reprfileloc is not None assert rep_entry.reprfuncargs is not None @@ -146,8 +145,7 @@ def test_repr_entry_native(): rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries - assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True) - for rep_entry, a_entry in zip(rep_entries, a_entries): + for rep_entry, a_entry in zip(rep_entries, a_entries, strict=True): assert isinstance(rep_entry, ReprEntryNative) assert rep_entry.lines == a_entry.lines diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 9a6c2c4b6aa..3a3b4057e45 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,7 +1,6 @@ # mypy: allow-untyped-defs from __future__ import annotations -import sys import textwrap from _pytest.pytester import Pytester @@ -1136,22 +1135,13 @@ def test_func(): """ ) result = pytester.runpytest() - markline = " ^" - pypy_version_info = getattr(sys, "pypy_version_info", None) - if pypy_version_info is not None: - markline = markline[7:] - - if sys.version_info >= (3, 10): - expected = [ - "*ERROR*test_nameerror*", - "*asd*", - "", - "During handling of the above exception, another exception occurred:", - ] - else: - expected = [ - "*ERROR*test_nameerror*", - ] + + expected = [ + "*ERROR*test_nameerror*", + "*asd*", + "", + "During handling of the above exception, another exception occurred:", + ] expected += [ "*evaluating*skipif*condition*", @@ -1159,7 +1149,7 @@ def test_func(): "*ERROR*test_syntax*", "*evaluating*xfail*condition*", " syntax error", - markline, + " ^", "SyntaxError: invalid syntax", "*1 pass*2 errors*", ] diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 2a3f4446a11..bacce108b42 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2677,7 +2677,7 @@ def test_len_dict(): [ "*short test summary info*", f"*{list(range(10))}*", - f"*{dict(zip(range(10), range(10)))}*", + f"*{dict(zip(range(10), range(10), strict=True))}*", ] ) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index b1c64dc9332..ff7ee4915c9 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -280,8 +280,7 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): ("call warning", "runtest", "test_warning_recorded_hook.py::test_func"), ("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"), ] - assert len(collected) == len(expected) # python < 3.10 zip(strict=True) - for collected_result, expected_result in zip(collected, expected): + for collected_result, expected_result in zip(collected, expected, strict=True): assert collected_result[0] == expected_result[0], str(collected) assert collected_result[1] == expected_result[1], str(collected) assert collected_result[2] == expected_result[2], str(collected) diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 8a316580a25..3ee2dfb3019 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -9,7 +9,6 @@ import contextlib from typing import Literal -from typing import Optional from typing_extensions import assert_type @@ -52,10 +51,10 @@ class Foo(TypedDict): def check_raises_is_a_context_manager(val: bool) -> None: with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: pass - assert_type(excinfo, Optional[pytest.ExceptionInfo[RuntimeError]]) + assert_type(excinfo, pytest.ExceptionInfo[RuntimeError] | None) # Issue #12941. def check_testreport_attributes(report: TestReport) -> None: assert_type(report.when, Literal["setup", "call", "teardown"]) - assert_type(report.location, tuple[str, Optional[int], str]) + assert_type(report.location, tuple[str, int | None, str]) diff --git a/testing/typing_raises_group.py b/testing/typing_raises_group.py index c7dd16991ac..081ffd59bca 100644 --- a/testing/typing_raises_group.py +++ b/testing/typing_raises_group.py @@ -1,8 +1,7 @@ from __future__ import annotations +from collections.abc import Callable import sys -from typing import Callable -from typing import Union from typing_extensions import assert_type @@ -160,10 +159,7 @@ def check_nested_raisesgroups_contextmanager() -> None: assert_type( excinfo.value.exceptions[0], # this union is because of how typeshed defines .exceptions - Union[ - ExceptionGroup[ValueError], - ExceptionGroup[ExceptionGroup[ValueError]], - ], + ExceptionGroup[ValueError] | ExceptionGroup[ExceptionGroup[ValueError]], ) @@ -240,8 +236,5 @@ def check_check_typing() -> None: # `BaseExceptiongroup` should perhaps be `ExceptionGroup`, but close enough assert_type( RaisesGroup(ValueError).check, - Union[ - Callable[[BaseExceptionGroup[ValueError]], bool], - None, - ], + Callable[[BaseExceptionGroup[ValueError]], bool] | None, ) diff --git a/tox.ini b/tox.ini index 3fe7865a289..fa86c9c4403 100644 --- a/tox.ini +++ b/tox.ini @@ -4,18 +4,17 @@ minversion = 3.20.0 distshare = {homedir}/.tox/distshare envlist = linting - py39 py310 py311 py312 py313 py314 pypy3 - py39-{pexpect,xdist,twisted24,twisted25,asynctest,numpy,pluggymain,pylib} + py310-{pexpect,xdist,twisted24,twisted25,asynctest,numpy,pluggymain,pylib} doctesting doctesting-coverage plugins - py39-freeze + py310-freeze docs docs-checklinks @@ -58,10 +57,11 @@ setenv = # See https://docs.python.org/3/library/io.html#io-encoding-warning # If we don't enable this, neither can any of our downstream users! - PYTHONWARNDEFAULTENCODING=1 + # pylib is not PYTHONWARNDEFAULTENCODING clean, so don't set for it. + !pylib: PYTHONWARNDEFAULTENCODING=1 # Configuration to run with coverage similar to CI, e.g. - # "tox -e py39-coverage". + # "tox -e py313-coverage". coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess coverage: COVERAGE_FILE={toxinidir}/.coverage @@ -182,7 +182,7 @@ commands = pytest pytest_twisted_integration.py pytest simple_integration.py --force-sugar --flakes -[testenv:py39-freeze] +[testenv:py310-freeze] description = test pytest frozen with `pyinstaller` under `{basepython}` changedir = testing/freeze