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