diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a50c0415d62..a6c0c56458c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,6 @@ jobs: fail-fast: false matrix: python: - - "3.10" - "3.11" - "3.12" - "3.13" diff --git a/.ruff.toml b/.ruff.toml index 663382a98f7..ce53ebde69b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,4 @@ -target-version = "py310" # Pin Ruff to Python 3.10 +target-version = "py311" # Pin Ruff to Python 3.11 line-length = 88 output-format = "full" diff --git a/CHANGES.rst b/CHANGES.rst index 5cdc5e938a3..6cef031159d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Release 8.2.0 (in development) Dependencies ------------ +* #13000: Drop Python 3.10 support. + Incompatible changes -------------------- diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index de68cbf1dec..968f49e76cf 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -188,18 +188,18 @@ of targets and allows testing against multiple different Python environments: tox -av -* To run unit tests for a specific Python version, such as Python 3.12: +* To run unit tests for a specific Python version, such as Python 3.13: .. code-block:: shell - tox -e py312 + tox -e py313 * Arguments to :program:`pytest` can be passed via :program:`tox`, e.g., in order to run a particular test: .. code-block:: shell - tox -e py312 tests/test_module.py::test_new_feature + tox -e py313 tests/test_module.py::test_new_feature You can also test by installing dependencies in your local environment: diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index 67da3d78603..96d1594d6e6 100644 --- a/doc/usage/installation.rst +++ b/doc/usage/installation.rst @@ -152,18 +152,18 @@ Install either ``python3x-sphinx`` using :command:`port`: :: - $ sudo port install py312-sphinx + $ sudo port install py313-sphinx To set up the executable paths, use the ``port select`` command: :: - $ sudo port select --set python python312 - $ sudo port select --set sphinx py312-sphinx + $ sudo port select --set python python313 + $ sudo port select --set sphinx py313-sphinx For more information, refer to the `package overview`__. -__ https://www.macports.org/ports.php?by=library&substr=py312-sphinx +__ https://www.macports.org/ports.php?by=library&substr=py313-sphinx Windows ~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 0812e1145fd..61d0017e7e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ urls.Download = "https://pypi.org/project/Sphinx/" urls.Homepage = "https://www.sphinx-doc.org/" urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues" license.text = "BSD-2-Clause" -requires-python = ">=3.10" +requires-python = ">=3.11" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ @@ -31,7 +31,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -71,7 +70,6 @@ dependencies = [ "imagesize>=1.3", "requests>=2.30.0", "packaging>=23.0", - "tomli>=2; python_version < '3.11'", "colorama>=0.4.6; sys_platform == 'win32'", ] dynamic = ["version"] @@ -92,7 +90,6 @@ lint = [ "types-Pygments==2.18.0.20240506", "types-requests==2.32.0.20240914", # align with requests "types-urllib3==1.26.25.14", - "tomli>=2", # for mypy (Python<=3.10) "pyright==1.1.384", "pytest>=6.0", ] @@ -204,7 +201,7 @@ exclude = [ # tests/test_writers "^utils/convert_attestations\\.py$", ] -python_version = "3.10" +python_version = "3.11" strict = true show_column_numbers = true show_error_context = true diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index b590c423e85..af180a9262b 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -12,6 +12,7 @@ import os import subprocess import sys +from contextlib import chdir from os import path from typing import TYPE_CHECKING @@ -20,11 +21,6 @@ from sphinx.util.console import blue, bold, color_terminal, nocolor from sphinx.util.osutil import rmtree -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - if TYPE_CHECKING: from collections.abc import Sequence diff --git a/sphinx/config.py b/sphinx/config.py index 134a6e39467..8700ed30054 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -2,11 +2,11 @@ from __future__ import annotations -import sys import time import traceback import types import warnings +from contextlib import chdir from os import getenv, path from typing import TYPE_CHECKING, Any, Literal, NamedTuple @@ -16,11 +16,6 @@ from sphinx.util import logging from sphinx.util.osutil import fs_encoding -if sys.version_info >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - if TYPE_CHECKING: import os from collections.abc import Collection, Iterable, Iterator, Sequence, Set diff --git a/sphinx/domains/_domains_container.py b/sphinx/domains/_domains_container.py index 5353bcfd650..6b5c18bdb37 100644 --- a/sphinx/domains/_domains_container.py +++ b/sphinx/domains/_domains_container.py @@ -4,10 +4,9 @@ if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping, Set - from typing import Any, Final, Literal, NoReturn + from typing import Any, Final, Literal, NoReturn, Self from docutils import nodes - from typing_extensions import Self from sphinx.domains import Domain from sphinx.domains.c import CDomain diff --git a/sphinx/domains/c/_symbol.py b/sphinx/domains/c/_symbol.py index c70b5131610..01b159ac8be 100644 --- a/sphinx/domains/c/_symbol.py +++ b/sphinx/domains/c/_symbol.py @@ -12,8 +12,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator, Sequence - - from typing_extensions import Self + from typing import Self from sphinx.environment import BuildEnvironment diff --git a/sphinx/theming.py b/sphinx/theming.py index 6d9986ba3c2..6a4366413d6 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -10,6 +10,7 @@ import shutil import sys import tempfile +import tomllib from importlib.metadata import entry_points from os import path from typing import TYPE_CHECKING, Any @@ -22,17 +23,9 @@ from sphinx.util import logging from sphinx.util.osutil import ensuredir -if sys.version_info >= (3, 11): - import tomllib -else: - import tomli as tomllib - - if TYPE_CHECKING: from collections.abc import Callable - from typing import TypedDict - - from typing_extensions import Required + from typing import Required, TypedDict from sphinx.application import Sphinx diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 5fca3c811b5..da870ca074e 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -4,8 +4,7 @@ import os import re -import sys -from datetime import datetime, timezone +from datetime import datetime from os import path from typing import TYPE_CHECKING, NamedTuple @@ -60,10 +59,7 @@ def __call__( # NoQA: E704 Formatter: TypeAlias = DateFormatter | TimeFormatter | DatetimeFormatter -if sys.version_info[:2] >= (3, 11): - from datetime import UTC -else: - UTC = timezone.utc +from datetime import UTC logger = logging.getLogger(__name__) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 64f07a43249..c1b3fcb6444 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -17,7 +17,6 @@ from sphinx.locale import __ if TYPE_CHECKING: - from types import TracebackType from typing import Any # SEP separates path elements in the canonical file names @@ -173,31 +172,6 @@ def relpath( abspath = path.abspath -class _chdir: - """Remove this fall-back once support for Python 3.10 is removed.""" - - def __init__(self, target_dir: str, /) -> None: - self.path = target_dir - self._dirs: list[str] = [] - - def __enter__(self) -> None: - self._dirs.append(os.getcwd()) - os.chdir(self.path) - - def __exit__( - self, - type: type[BaseException] | None, - value: BaseException | None, - traceback: TracebackType | None, - /, - ) -> None: - os.chdir(self._dirs.pop()) - - -if sys.version_info[:2] < (3, 11): - cd = _chdir - - class FileAvoidWrite: """File-like object that buffers output and only writes if content changed. diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 35d444aa583..8f2a6ed8358 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -18,6 +18,7 @@ TypedDict, TypeVar, Union, + Unpack, ) from docutils import nodes @@ -121,8 +122,7 @@ def __call__( # NoQA: E704 # Readable file stream for inventory loading if TYPE_CHECKING: from types import TracebackType - - from typing_extensions import Self + from typing import Self _T_co = TypeVar('_T_co', str, bytes, covariant=True) @@ -221,19 +221,7 @@ def _is_annotated_form(obj: Any) -> TypeIs[Annotated[Any, ...]]: def _is_unpack_form(obj: Any) -> bool: """Check if the object is :class:`typing.Unpack` or equivalent.""" - if sys.version_info >= (3, 11): - from typing import Unpack - - # typing_extensions.Unpack != typing.Unpack for 3.11, but we assume - # that typing_extensions.Unpack should not be used in that case - return typing.get_origin(obj) is Unpack - - # Python 3.10 requires typing_extensions.Unpack - origin = typing.get_origin(obj) - return ( - getattr(origin, '__module__', None) == 'typing_extensions' - and origin.__name__ == 'Unpack' - ) + return typing.get_origin(obj) is Unpack def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: @@ -361,7 +349,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return rf'{text}\ [{args}]' elif isinstance(cls, typing._SpecialForm): return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] - elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: + elif cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): diff --git a/tests/test_builders/test_build_gettext.py b/tests/test_builders/test_build_gettext.py index 169cc3bf438..e9ed4afb90f 100644 --- a/tests/test_builders/test_build_gettext.py +++ b/tests/test_builders/test_build_gettext.py @@ -4,18 +4,13 @@ import os import re import subprocess -import sys +from contextlib import chdir from subprocess import CalledProcessError import pytest from sphinx.builders.gettext import Catalog, MsgOrigin -if sys.version_info[:2] >= (3, 11): - from contextlib import chdir -else: - from sphinx.util.osutil import _chdir as chdir - _MSGID_PATTERN = re.compile(r'msgid "((?:\n|.)*?)"\nmsgstr', re.MULTILINE) diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index ea6dc7475ca..841bfe56a2e 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -4,6 +4,7 @@ import os import re import subprocess +from contextlib import chdir from pathlib import Path from shutil import copyfile from subprocess import CalledProcessError @@ -20,11 +21,6 @@ from tests.utils import http_server -try: - from contextlib import chdir -except ImportError: - from sphinx.util.osutil import _chdir as chdir - STYLEFILES = [ 'article.cls', 'fancyhdr.sty', diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index f483eae8a65..f0aac81f27d 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -1571,13 +1571,11 @@ def brief(self, doc: str, *, indent: int = 0, **options: Any) -> list[str]: '(value, names=None, *values, module=None, ' 'qualname=None, type=None, start=1, boundary=None)' ) - elif sys.version_info[:2] >= (3, 11): + else: args = ( '(value, names=None, *, module=None, qualname=None, ' 'type=None, start=1, boundary=None)' ) - else: - args = '(value)' return self._node('class', self.name, doc, args=args, indent=indent, **options) diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index ea4d2f3d944..0298cdd3b55 100644 --- a/tests/test_extensions/test_ext_autodoc_configs.py +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -692,10 +692,6 @@ def test_mocked_module_imports(app): confoverrides={'autodoc_typehints': 'signature'}, ) def test_autodoc_typehints_signature(app): - if sys.version_info[:2] <= (3, 10): - type_o = '~typing.Any | None' - else: - type_o = '~typing.Any' if sys.version_info[:2] >= (3, 13): type_ppp = 'pathlib._local.PurePosixPath' else: @@ -732,7 +728,7 @@ def test_autodoc_typehints_signature(app): ' docstring', '', '', - '.. py:class:: Math(s: str, o: %s = None)' % type_o, + '.. py:class:: Math(s: str, o: ~typing.Any = None)', ' :module: target.typehints', '', '', @@ -1511,10 +1507,6 @@ def test_autodoc_typehints_description_and_type_aliases(app): confoverrides={'autodoc_typehints_format': 'fully-qualified'}, ) def test_autodoc_typehints_format_fully_qualified(app): - if sys.version_info[:2] <= (3, 10): - type_o = 'typing.Any | None' - else: - type_o = 'typing.Any' if sys.version_info[:2] >= (3, 13): type_ppp = 'pathlib._local.PurePosixPath' else: @@ -1550,7 +1542,7 @@ def test_autodoc_typehints_format_fully_qualified(app): ' docstring', '', '', - '.. py:class:: Math(s: str, o: %s = None)' % type_o, + '.. py:class:: Math(s: str, o: typing.Any = None)', ' :module: target.typehints', '', '', diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index a6bf97a2227..ca40a48fee7 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -1,6 +1,7 @@ """Test the autosummary extension.""" import sys +from contextlib import chdir from io import StringIO from unittest.mock import Mock, patch from xml.etree.ElementTree import Element @@ -25,11 +26,6 @@ from sphinx.testing.util import assert_node, etree_parse from sphinx.util.docutils import new_document -try: - from contextlib import chdir -except ImportError: - from sphinx.util.osutil import _chdir as chdir - html_warnfile = StringIO() diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 5bfa4adab63..46d2f88af6d 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -95,7 +95,7 @@ def test_format_date(): def test_format_date_timezone(): - dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.timezone.utc) + dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.UTC) if time.localtime(dt.timestamp()).tm_gmtoff == 0: raise pytest.skip('Local time zone is GMT') # NoQA: EM101 diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 33226371fb7..5e2fdb20cc1 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -6,7 +6,6 @@ import datetime import enum import functools -import sys import types from inspect import Parameter from typing import Callable, List, Optional, Union # NoQA: UP035 @@ -272,10 +271,7 @@ def test_signature_annotations(): # Space around '=' for defaults sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {}) -> None' - else: - assert sig_str == '(x: int = None, y: dict = {}) -> None' + assert sig_str == '(x: int = None, y: dict = {}) -> None' # Callable types sig = inspect.signature(mod.f8) @@ -355,18 +351,12 @@ def test_signature_annotations(): # show_return_annotation is False sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig, show_return_annotation=False) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {})' - else: - assert sig_str == '(x: int = None, y: dict = {})' + assert sig_str == '(x: int = None, y: dict = {})' # unqualified_typehints is True sig = inspect.signature(mod.f7) sig_str = stringify_signature(sig, unqualified_typehints=True) - if sys.version_info[:2] <= (3, 10): - assert sig_str == '(x: int | None = None, y: dict = {}) -> None' - else: - assert sig_str == '(x: int = None, y: dict = {}) -> None' + assert sig_str == '(x: int = None, y: dict = {}) -> None' # case: separator at head sig = inspect.signature(mod.f22) diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 1cc408a619e..314639efbe7 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -173,10 +173,7 @@ def test_restify_type_hints_containers(): ann_rst = restify(Tuple[str, ...]) assert ann_rst == ':py:class:`~typing.Tuple`\\ [:py:class:`str`, ...]' - if sys.version_info[:2] <= (3, 10): - assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`\\ [()]' - else: - assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`' + assert restify(Tuple[()]) == ':py:class:`~typing.Tuple`' assert restify(List[Dict[str, Tuple]]) == ( ':py:class:`~typing.List`\\ ' @@ -423,10 +420,9 @@ class X(t.TypedDict): assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect assert restify(UnpackCompat['X'], 'smart') == expect - if sys.version_info[:2] >= (3, 11): - expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' - assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect - assert restify(t.Unpack['X'], 'smart') == expect + expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' + assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect + assert restify(t.Unpack['X'], 'smart') == expect def test_restify_type_union_operator(): @@ -534,17 +530,9 @@ def test_stringify_type_hints_containers(): assert ann_str == 'typing.Tuple[str, ...]' assert stringify_annotation(Tuple[str, ...], 'smart') == '~typing.Tuple[str, ...]' - if sys.version_info[:2] <= (3, 10): - ann_str = stringify_annotation(Tuple[()], 'fully-qualified-except-typing') - assert ann_str == 'Tuple[()]' - assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple[()]' - assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple[()]' - else: - assert ( - stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == 'Tuple' - ) - assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple' - assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple' + assert stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == 'Tuple' + assert stringify_annotation(Tuple[()], 'fully-qualified') == 'typing.Tuple' + assert stringify_annotation(Tuple[()], 'smart') == '~typing.Tuple' ann_str = stringify_annotation( List[Dict[str, Tuple]], 'fully-qualified-except-typing' @@ -677,30 +665,13 @@ def test_stringify_Annotated(): def test_stringify_Unpack(): - from typing_extensions import Unpack as UnpackCompat - class X(t.TypedDict): x: int y: int label: str - if sys.version_info[:2] >= (3, 11): - # typing.Unpack is introduced in 3.11 but typing_extensions.Unpack only - # uses typing.Unpack in 3.12+, so the objects are not synchronised with - # each other, but we will assume that users use typing.Unpack. - import typing - - UnpackCompat = typing.Unpack # NoQA: F811 - assert stringify_annotation(UnpackCompat['X']) == 'Unpack[X]' - assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing.Unpack[X]' - else: - assert stringify_annotation(UnpackCompat['X']) == 'typing_extensions.Unpack[X]' - ann_str = stringify_annotation(UnpackCompat['X'], 'smart') - assert ann_str == '~typing_extensions.Unpack[X]' - - if sys.version_info[:2] >= (3, 11): - assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]' - assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]' + assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]' + assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]' def test_stringify_type_hints_string(): diff --git a/tox.ini b/tox.ini index 810cd46f9ec..6fd6e18ce97 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.2.0 -envlist = py{310,311,312,313} +envlist = py{311,312,313} [testenv] usedevelop = True @@ -19,7 +19,7 @@ passenv = BUILDER READTHEDOCS description = - py{310,311,312,313}: Run unit tests against {envname}. + py{311,312,313}: Run unit tests against {envname}. extras = test setenv =