diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 58e51e309..3ca005000 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 name: Install Python with: - python-version: "3.9" + python-version: "3.14" cache: "pip" - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 17119ee50..26b41e68c 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -25,7 +25,6 @@ jobs: name: Install Pythons with: python-version: | - 3.9 3.10 3.11 3.12 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8873b2125..fb7acaea0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: matrix: os: [Ubuntu, Windows, macOS] python_version: - ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.13t", "3.14t", "3.15t", "pypy3.9", "pypy3.10", "pypy3.11"] + ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.13t", "3.14t", "3.15t", "pypy3.10", "pypy3.11"] include: - os: Ubuntu python_version: "3.11.0" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7faca581d..faada1563 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,10 @@ Changelog *unreleased* ~~~~~~~~~~~~ +Removals: + +* Drop support for EOL Python 3.9. (:pull:`1263`) + Features: * Add a public :class:`~packaging.ranges.VersionRange` API and diff --git a/asv.conf.json b/asv.conf.json index 0acc4ef15..2af0057c7 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -15,5 +15,5 @@ "PYTHONHASHSEED": ["0"] } }, - "pythons": ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + "pythons": ["3.10", "3.11", "3.12", "3.13", "3.14"] } diff --git a/docs/development/getting-started.rst b/docs/development/getting-started.rst index d3e25af8c..f6c227449 100644 --- a/docs/development/getting-started.rst +++ b/docs/development/getting-started.rst @@ -30,7 +30,6 @@ each supported Python version and run the tests. For example: $ nox -s tests ... nox > Ran multiple sessions: - nox > * tests-3.9: success nox > * tests-3.10: success nox > * tests-3.11: success nox > * tests-3.12: success diff --git a/noxfile.py b/noxfile.py index 3122a1fde..e23ed9285 100644 --- a/noxfile.py +++ b/noxfile.py @@ -49,7 +49,6 @@ "3.13t", "3.14t", "3.15t", - "pypy3.9", "pypy3.10", "pypy3.11", ], @@ -675,7 +674,7 @@ def _bump(session: nox.Session, *, version: str, file: Path, kind: str) -> None: @contextlib.contextmanager def _replace_file( original_path: Path, -) -> Generator[tuple[IO[str], IO[str]], None, None]: +) -> Generator[tuple[IO[str], IO[str]]]: # Create a temporary file. fh, replacement_path = tempfile.mkstemp() diff --git a/pyproject.toml b/pyproject.toml index 6fe36f605..9fb8070ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Core utilities for Python packages" dynamic = ["version"] license = "Apache-2.0 OR BSD-2-Clause" readme = "README.rst" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [{name = "Donald Stufft", email = "donald@stufft.io"}] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -17,7 +17,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "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", @@ -110,7 +109,7 @@ markers = ["property: property-based tests (opt-in)"] strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unused_ignores = true -python_version = "3.9" +python_version = "3.10" files = ["src", "tests", "noxfile.py"] native_parser = true diff --git a/src/packaging/_manylinux.py b/src/packaging/_manylinux.py index 86de1e6d5..c5edbb78a 100644 --- a/src/packaging/_manylinux.py +++ b/src/packaging/_manylinux.py @@ -31,7 +31,7 @@ @contextlib.contextmanager -def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]: +def _parse_elf(path: str) -> Generator[ELFFile | None]: try: with open(path, "rb") as f: yield ELFFile(f) diff --git a/src/packaging/_parser.py b/src/packaging/_parser.py index 9331b4b4c..35920dcc9 100644 --- a/src/packaging/_parser.py +++ b/src/packaging/_parser.py @@ -86,9 +86,9 @@ def serialize(self) -> str: MarkerLogical = Literal["and", "or"] -MarkerVar = Union[Variable, Value] +MarkerVar = Variable | Value MarkerItem = tuple[MarkerVar, Op, MarkerVar] -MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]] +MarkerAtom = MarkerItem | Sequence["MarkerAtom"] MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]] diff --git a/src/packaging/_ranges.py b/src/packaging/_ranges.py index 5370ec87e..e312a1f7e 100644 --- a/src/packaging/_ranges.py +++ b/src/packaging/_ranges.py @@ -17,11 +17,10 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Sequence - from typing import Union # Total-order key for comparing two boundaries (boundary-vs-boundary only). # The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary. - _BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int] + _BoundaryOrderSuffix = tuple[int, int, int, int | float, int, int] _BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float] __all__ = [ @@ -172,7 +171,7 @@ def __repr__(self) -> str: if TYPE_CHECKING: - _VersionOrBoundary = Union[Version, BoundaryVersion, None] + _VersionOrBoundary = Version | BoundaryVersion | None @functools.total_ordering diff --git a/src/packaging/_tokenizer.py b/src/packaging/_tokenizer.py index db7cd17fb..64227eef0 100644 --- a/src/packaging/_tokenizer.py +++ b/src/packaging/_tokenizer.py @@ -179,7 +179,7 @@ def raise_syntax_error( @contextlib.contextmanager def enclosing_tokens( self, open_token: str, close_token: str, *, around: str - ) -> Generator[None, None, None]: + ) -> Generator[None]: if self.check(open_token): open_position = self.position self.read() diff --git a/src/packaging/errors.py b/src/packaging/errors.py index d1d47cf6c..018740d66 100644 --- a/src/packaging/errors.py +++ b/src/packaging/errors.py @@ -62,7 +62,7 @@ def finalize(self, msg: str) -> None: raise ExceptionGroup(msg, self.errors) @contextlib.contextmanager - def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]: + def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector]: """ Calls finalize if no uncollected errors were present. @@ -72,7 +72,7 @@ def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]: self.finalize(msg) @contextlib.contextmanager - def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]: + def collect(self, *err_cls: type[Exception]) -> typing.Generator[None]: """ Context manager to collect errors into the error list. diff --git a/src/packaging/licenses/_spdx.py b/src/packaging/licenses/_spdx.py index a277af282..5f98b80aa 100644 --- a/src/packaging/licenses/_spdx.py +++ b/src/packaging/licenses/_spdx.py @@ -1,4 +1,3 @@ - from __future__ import annotations from typing import TypedDict diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 51b603646..ce8bc3049 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -9,8 +9,9 @@ import os import platform import sys +from collections.abc import Callable from collections.abc import Set as AbstractSet -from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast +from typing import TYPE_CHECKING, Literal, TypedDict, cast from ._parser import MarkerAtom, MarkerList, Op, Value, Variable from ._parser import parse_marker as _parse_marker @@ -36,7 +37,7 @@ def __dir__() -> list[str]: return __all__ -Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] +Operator = Callable[[str, str | AbstractSet[str]], bool] EvaluateContext = Literal["metadata", "lock_file", "requirement"] """A ``typing.Literal`` enumerating valid marker evaluation contexts. diff --git a/src/packaging/metadata.py b/src/packaging/metadata.py index e113a3ce7..602587032 100644 --- a/src/packaging/metadata.py +++ b/src/packaging/metadata.py @@ -9,7 +9,6 @@ import typing from typing import ( Any, - Callable, Generic, Literal, TypedDict, @@ -21,6 +20,8 @@ from .errors import ExceptionGroup, _ErrorCollector if typing.TYPE_CHECKING: + from collections.abc import Callable + from .licenses import NormalizedLicenseExpression from .version import Version diff --git a/src/packaging/pylock.py b/src/packaging/pylock.py index b7976232d..68ae1426d 100644 --- a/src/packaging/pylock.py +++ b/src/packaging/pylock.py @@ -3,13 +3,12 @@ import dataclasses import logging import re -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass from datetime import datetime from typing import ( TYPE_CHECKING, Any, - Callable, Protocol, TypeVar, cast, diff --git a/src/packaging/ranges.py b/src/packaging/ranges.py index 8144a65c4..4fbf48e63 100644 --- a/src/packaging/ranges.py +++ b/src/packaging/ranges.py @@ -25,7 +25,6 @@ TYPE_CHECKING, Any, TypeVar, - Union, ) from ._ranges import ( @@ -58,7 +57,7 @@ __all__ = ["VersionRange"] T = TypeVar("T") -UnparsedVersion = Union[Version, str] +UnparsedVersion = Version | str UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) #: The most ``!=`` exclusion fragments (``!=V`` points or ``!=P.*`` prefixes) diff --git a/src/packaging/specifiers.py b/src/packaging/specifiers.py index a6a61af42..dd46b5643 100644 --- a/src/packaging/specifiers.py +++ b/src/packaging/specifiers.py @@ -16,10 +16,8 @@ from typing import ( TYPE_CHECKING, Any, - Callable, Final, TypeVar, - Union, ) from ._ranges import ( @@ -37,13 +35,8 @@ from .version import Version if TYPE_CHECKING: - import sys - from collections.abc import Iterable, Iterator, Sequence - - if sys.version_info >= (3, 10): - from typing import TypeGuard - else: - from typing_extensions import TypeGuard + from collections.abc import Callable, Iterable, Iterator, Sequence + from typing import TypeGuard from . import ranges from ._ranges import Interval @@ -75,7 +68,7 @@ def _validate_pre(pre: object, /) -> TypeGuard[bool | None]: T = TypeVar("T") -UnparsedVersion = Union[Version, str] +UnparsedVersion = Version | str UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) diff --git a/src/packaging/utils.py b/src/packaging/utils.py index 2f0ce2074..9be9ef361 100644 --- a/src/packaging/utils.py +++ b/src/packaging/utils.py @@ -5,7 +5,7 @@ from __future__ import annotations import re -from typing import NewType, Union, cast +from typing import NewType, cast from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag from .version import InvalidVersion, Version, _TrimmedRelease @@ -28,7 +28,7 @@ def __dir__() -> list[str]: return __all__ -BuildTag = Union[tuple[()], tuple[int, str]] +BuildTag = tuple[()] | tuple[int, str] """ A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair. diff --git a/src/packaging/version.py b/src/packaging/version.py index 5a964bd22..791c8ac71 100644 --- a/src/packaging/version.py +++ b/src/packaging/version.py @@ -12,14 +12,13 @@ import re import sys import typing +from collections.abc import Callable from typing import ( Any, - Callable, Literal, NamedTuple, SupportsInt, TypedDict, - Union, ) if typing.TYPE_CHECKING: @@ -66,14 +65,14 @@ def __dir__() -> list[str]: return __all__ -LocalType = tuple[Union[int, str], ...] +LocalType = tuple[int | str, ...] CmpLocalType = tuple[tuple[int, str], ...] CmpSuffix = tuple[int, int, int, int, int, int] -CmpKey = Union[ - tuple[int, tuple[int, ...], CmpSuffix], - tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType], -] +CmpKey = ( + tuple[int, tuple[int, ...], CmpSuffix] + | tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType] +) VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] @@ -431,8 +430,7 @@ def __init__(self, version: str) -> None: # propagate to the caller. if "" in version.split("."): raise InvalidVersion(f"Invalid version: {version!r}") from None - # TODO: remove "no cover" when Python 3.9 is dropped. - raise # pragma: no cover + raise self._epoch = 0 self._pre = None diff --git a/tasks/pickle_compat.py b/tasks/pickle_compat.py index 146ca95d8..fe321bae6 100644 --- a/tasks/pickle_compat.py +++ b/tasks/pickle_compat.py @@ -130,7 +130,7 @@ def verify(path: pathlib.Path, expected_version: str | None = None) -> int: return 1 for i, (loaded, expected) in enumerate( - zip(loaded_list, expected_list) # type: ignore[call-overload] + zip(loaded_list, expected_list, strict=False) # type: ignore[call-overload] ): if type(loaded) is not expected_cls: print( diff --git a/tests/test_dependency_groups.py b/tests/test_dependency_groups.py index 5c3e42bc4..4f291d1ce 100644 --- a/tests/test_dependency_groups.py +++ b/tests/test_dependency_groups.py @@ -19,12 +19,7 @@ from packaging.requirements import InvalidRequirement, Requirement if TYPE_CHECKING: - import sys - - if sys.version_info >= (3, 10): - from typing import TypeAlias - else: - from typing_extensions import TypeAlias + from typing import TypeAlias GroupsTable: TypeAlias = "dict[str, list[str | dict[str, str]]]" diff --git a/tests/test_manylinux.py b/tests/test_manylinux.py index 9c18a3430..f6b029fb3 100644 --- a/tests/test_manylinux.py +++ b/tests/test_manylinux.py @@ -33,7 +33,7 @@ @pytest.fixture(autouse=True) -def clear_lru_cache() -> Generator[None, None, None]: +def clear_lru_cache() -> Generator[None]: yield _get_glibc_version.cache_clear() _get_manylinux_module.cache_clear() diff --git a/tests/test_musllinux.py b/tests/test_musllinux.py index d90627262..3d4348f2c 100644 --- a/tests/test_musllinux.py +++ b/tests/test_musllinux.py @@ -37,7 +37,7 @@ class Proc(typing.NamedTuple): @pytest.fixture(autouse=True) -def clear_lru_cache() -> Generator[None, None, None]: +def clear_lru_cache() -> Generator[None]: yield _get_musl_version.cache_clear() diff --git a/tests/test_specifiers.py b/tests/test_specifiers.py index ce1555737..023234b8b 100644 --- a/tests/test_specifiers.py +++ b/tests/test_specifiers.py @@ -3090,7 +3090,7 @@ def test_range_bounds_hashable_and_equal(self) -> None: """Range bounds are hashable and support equality.""" a = Specifier(">1.0")._to_ranges() b = Specifier(">1.0")._to_ranges() - for (al, au), (bl, bu) in zip(a, b): + for (al, au), (bl, bu) in zip(a, b, strict=True): hash(al) hash(au) assert al == bl