diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cc1b8ca669f..293d3e4de6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,6 @@ jobs: fail-fast: false matrix: python: - - "3.9" - "3.10" - "3.11" - "3.12" diff --git a/.ruff.toml b/.ruff.toml index 4451e8eb538..2b0e3434aa9 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,4 @@ -target-version = "py39" # Pin Ruff to Python 3.9 +target-version = "py310" # Pin Ruff to Python 3.10 line-length = 95 output-format = "full" @@ -419,8 +419,8 @@ select = [ ] # these tests need old ``typing`` generic aliases -"tests/test_util/test_util_typing.py" = ["UP006", "UP035"] -"tests/test_util/typing_test_data.py" = ["FA100", "UP006", "UP035"] +"tests/test_util/test_util_typing.py" = ["UP006", "UP007", "UP035"] +"tests/test_util/typing_test_data.py" = ["FA100", "UP006", "UP007", "UP035"] "utils/*" = [ "T201", # whitelist ``print`` for stdout messages diff --git a/CHANGES.rst b/CHANGES.rst index a3e927b387f..59e84cde4f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Release 8.0.0 (in development) Dependencies ------------ +* #12633: Drop Python 3.9 support. + Incompatible changes -------------------- diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index eac35606a3f..aa72255f973 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -174,19 +174,19 @@ 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.10:: +* To run unit tests for a specific Python version, such as Python 3.12:: - tox -e py310 + tox -e py312 * To run unit tests for a specific Python version and turn on deprecation warnings so they're shown in the test output:: - PYTHONWARNINGS=error tox -e py310 + PYTHONWARNINGS=error tox -e py312 * Arguments to ``pytest`` can be passed via ``tox``, e.g., in order to run a particular test:: - tox -e py310 tests/test_module.py::test_new_feature + tox -e py312 tests/test_module.py::test_new_feature You can also test by installing dependencies in your local environment:: diff --git a/doc/tutorial/deploying.rst b/doc/tutorial/deploying.rst index c2695933969..c950d37afc5 100644 --- a/doc/tutorial/deploying.rst +++ b/doc/tutorial/deploying.rst @@ -246,7 +246,7 @@ After you have published your sources on GitLab, create a file named pages: stage: deploy - image: python:3.9-slim + image: python:3.12-slim before_script: - apt-get update && apt-get install make --no-install-recommends -y - python -m pip install sphinx furo diff --git a/doc/usage/extensions/doctest.rst b/doc/usage/extensions/doctest.rst index a5fc2c7fc3b..f257509de22 100644 --- a/doc/usage/extensions/doctest.rst +++ b/doc/usage/extensions/doctest.rst @@ -79,10 +79,10 @@ a comma-separated list of group names. * ``pyversion``, a string option, can be used to specify the required Python version for the example to be tested. For instance, in the following case - the example will be tested only for Python versions greater than 3.10:: + the example will be tested only for Python versions greater than 3.12:: .. doctest:: - :pyversion: > 3.10 + :pyversion: > 3.12 The following operands are supported: diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index 7be688928c8..67da3d78603 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 py39-sphinx + $ sudo port install py312-sphinx To set up the executable paths, use the ``port select`` command: :: - $ sudo port select --set python python39 - $ sudo port select --set sphinx py39-sphinx + $ sudo port select --set python python312 + $ sudo port select --set sphinx py312-sphinx For more information, refer to the `package overview`__. -__ https://www.macports.org/ports.php?by=library&substr=py39-sphinx +__ https://www.macports.org/ports.php?by=library&substr=py312-sphinx Windows ~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 7284c83d35f..54d596c3082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,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.9" +requires-python = ">=3.10" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ @@ -30,7 +30,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", @@ -71,7 +70,6 @@ dependencies = [ "imagesize>=1.3", "requests>=2.30.0", "packaging>=23.0", - "importlib-metadata>=6.0; python_version < '3.10'", "tomli>=2; python_version < '3.11'", "colorama>=0.4.6; sys_platform == 'win32'", ] @@ -91,7 +89,6 @@ lint = [ "types-Pillow==10.2.0.20240520", "types-Pygments==2.18.0.20240506", "types-requests>=2.30.0", # align with requests - "importlib-metadata>=6.0", # for mypy (Python<=3.9) "tomli>=2", # for mypy (Python<=3.10) "pytest>=6.0", ] @@ -211,7 +208,7 @@ exclude = [ ] check_untyped_defs = true disallow_incomplete_defs = true -python_version = "3.9" +python_version = "3.10" show_column_numbers = true show_error_context = true strict_optional = true diff --git a/sphinx/_cli/__init__.py b/sphinx/_cli/__init__.py index 3168b383d1d..d4455e2d94e 100644 --- a/sphinx/_cli/__init__.py +++ b/sphinx/_cli/__init__.py @@ -79,7 +79,7 @@ def format_help(self) -> str: ] if commands := list(_load_subcommand_descriptions()): - command_max_length = min(max(map(len, next(zip(*commands), ()))), 22) + command_max_length = min(max(map(len, next(zip(*commands, strict=True), ()))), 22) help_fragments += [ '\n', bold(underline(__('Commands:'))), diff --git a/sphinx/application.py b/sphinx/application.py index 3935e70451a..a1589fb230c 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -10,10 +10,10 @@ import pickle import sys from collections import deque -from collections.abc import Collection, Sequence # NoQA: TCH003 +from collections.abc import Callable, Collection, Sequence # NoQA: TCH003 from io import StringIO from os import path -from typing import IO, TYPE_CHECKING, Any, Callable, Literal +from typing import IO, TYPE_CHECKING, Any, Literal from docutils.nodes import TextElement # NoQA: TCH002 from docutils.parsers.rst import Directive, roles diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index aef01561539..c5a59fee79e 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -87,9 +87,9 @@ def _stable_hash(obj: Any) -> str: """ if isinstance(obj, dict): obj = sorted(map(_stable_hash, obj.items())) - if isinstance(obj, (list, tuple, set, frozenset)): + if isinstance(obj, list | tuple | set | frozenset): obj = sorted(map(_stable_hash, obj)) - elif isinstance(obj, (type, types.FunctionType)): + elif isinstance(obj, type | types.FunctionType): # The default repr() of functions includes the ID, which is not ideal. # We use the fully qualified name instead. obj = f'{obj.__module__}.{obj.__qualname__}' @@ -734,7 +734,7 @@ def write_genindex(self) -> None: 'genindex-split.html') self.handle_page('genindex-all', genindexcontext, 'genindex.html') - for (key, entries), count in zip(genindex, indexcounts): + for (key, entries), count in zip(genindex, indexcounts, strict=True): ctx = {'key': key, 'entries': entries, 'count': count, 'genindexentries': genindex} self.handle_page('genindex-' + key, ctx, diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py index 83599d8261d..c85dce53018 100644 --- a/sphinx/builders/latex/transforms.py +++ b/sphinx/builders/latex/transforms.py @@ -417,7 +417,7 @@ def depart_caption(self, node: nodes.caption) -> None: self.unrestrict(node) def visit_title(self, node: nodes.title) -> None: - if isinstance(node.parent, (nodes.section, nodes.table)): + if isinstance(node.parent, nodes.section | nodes.table): self.restrict(node) def depart_title(self, node: nodes.title) -> None: diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index a9440db04cd..8d4a4562ffa 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -27,8 +27,8 @@ from sphinx.util.nodes import get_node_line if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable + from collections.abc import Callable, Iterator + from typing import Any from requests import Response diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index cea8c7ccf31..4ae4556ca68 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -8,7 +8,7 @@ import sys import time from os import path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any # try to import readline, unix specific enhancement try: @@ -36,7 +36,7 @@ from sphinx.util.template import SphinxRenderer if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence EXTENSIONS = { 'autodoc': __('automatically insert docstrings from modules'), diff --git a/sphinx/config.py b/sphinx/config.py index 2bb7fb27a2b..dc129540a4b 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -8,7 +8,7 @@ import types import warnings from os import getenv, path -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import TYPE_CHECKING, Any, Literal, NamedTuple from sphinx.deprecation import RemovedInSphinx90Warning from sphinx.errors import ConfigError, ExtensionError @@ -66,7 +66,7 @@ def is_serializable(obj: object, *, _seen: frozenset[int] = frozenset()) -> bool is_serializable(key, _seen=seen) and is_serializable(value, _seen=seen) for key, value in obj.items() ) - elif isinstance(obj, (list, tuple, set, frozenset)): + elif isinstance(obj, list | tuple | set | frozenset): seen = _seen | {id(obj)} return all(is_serializable(item, _seen=seen) for item in obj) @@ -87,13 +87,13 @@ def __init__(self, *candidates: str | bool | None) -> None: self.candidates = candidates def match(self, value: str | list | tuple) -> bool: - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): return all(item in self.candidates for item in value) else: return value in self.candidates -_OptValidTypes = Union[tuple[()], tuple[type, ...], frozenset[type], ENUM] +_OptValidTypes = tuple[()] | tuple[type, ...] | frozenset[type] | ENUM class _Opt: @@ -549,7 +549,7 @@ def _validate_valid_types( ) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM: if not valid_types: return () - if isinstance(valid_types, (frozenset, ENUM)): + if isinstance(valid_types, frozenset | ENUM): return valid_types if isinstance(valid_types, type): return frozenset((valid_types,)) @@ -584,7 +584,7 @@ def convert_source_suffix(app: Sphinx, config: Config) -> None: config.source_suffix = {source_suffix: 'restructuredtext'} logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."), source_suffix, config.source_suffix) - elif isinstance(source_suffix, (list, tuple)): + elif isinstance(source_suffix, list | tuple): # if list, considers as all of them are default filetype config.source_suffix = dict.fromkeys(source_suffix, 'restructuredtext') logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."), diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 543594233d7..3ed9243774b 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -8,7 +8,8 @@ import copy from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, cast +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, NamedTuple, cast from docutils.nodes import Element, Node, system_message @@ -153,7 +154,7 @@ def generate(self, docnames: Iterable[str] | None = None, raise NotImplementedError -TitleGetter = Callable[[Node], Optional[str]] +TitleGetter = Callable[[Node], str | None] class Domain: diff --git a/sphinx/domains/c/_ast.py b/sphinx/domains/c/_ast.py index 6082a56fead..8d582bcce6f 100644 --- a/sphinx/domains/c/_ast.py +++ b/sphinx/domains/c/_ast.py @@ -18,13 +18,12 @@ ) if TYPE_CHECKING: - from docutils.nodes import Element, Node, TextElement from sphinx.domains.c._symbol import Symbol from sphinx.environment import BuildEnvironment -DeclarationType = Union[ +DeclarationType = Union[ # NoQA: UP007 "ASTStruct", "ASTUnion", "ASTEnum", "ASTEnumerator", "ASTType", "ASTTypeWithInit", "ASTMacro", ] diff --git a/sphinx/domains/c/_parser.py b/sphinx/domains/c/_parser.py index 8c6582521a2..42211988414 100644 --- a/sphinx/domains/c/_parser.py +++ b/sphinx/domains/c/_parser.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from sphinx.domains.c._ast import ( ASTAlignofExpr, @@ -53,7 +53,6 @@ ASTTypeWithInit, ASTUnaryOpExpr, ASTUnion, - DeclarationType, ) from sphinx.domains.c._ids import ( _expression_assignment_ops, @@ -80,7 +79,9 @@ ) if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence + + from sphinx.domains.c._ast import DeclarationType class DefinitionParser(BaseParser): diff --git a/sphinx/domains/cpp/_ast.py b/sphinx/domains/cpp/_ast.py index 141d5112c8e..0ed178ee540 100644 --- a/sphinx/domains/cpp/_ast.py +++ b/sphinx/domains/cpp/_ast.py @@ -30,7 +30,6 @@ ) if TYPE_CHECKING: - from docutils.nodes import Element, TextElement from sphinx.addnodes import desc_signature diff --git a/sphinx/domains/cpp/_parser.py b/sphinx/domains/cpp/_parser.py index ee6e8baf2ef..452e4c9c8a7 100644 --- a/sphinx/domains/cpp/_parser.py +++ b/sphinx/domains/cpp/_parser.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from sphinx.domains.cpp._ast import ( ASTAlignofExpr, @@ -127,7 +127,7 @@ ) if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence logger = logging.getLogger(__name__) diff --git a/sphinx/domains/cpp/_symbol.py b/sphinx/domains/cpp/_symbol.py index 14c8f5fe672..ef5a405881a 100644 --- a/sphinx/domains/cpp/_symbol.py +++ b/sphinx/domains/cpp/_symbol.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, NoReturn +from typing import TYPE_CHECKING, Any, NoReturn from sphinx.domains.cpp._ast import ( ASTDeclaration, @@ -17,7 +17,7 @@ from sphinx.util import logging if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from sphinx.environment import BuildEnvironment diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index f136cfda106..afa36062d6b 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -74,7 +74,7 @@ def get_equation_number_for(self, labelid: str) -> int | None: def process_doc(self, env: BuildEnvironment, docname: str, document: nodes.document) -> None: def math_node(node: Node) -> bool: - return isinstance(node, (nodes.math, nodes.math_block)) + return isinstance(node, nodes.math | nodes.math_block) self.data['has_equations'][docname] = any(document.findall(math_node)) diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py index b9ee24d36e0..bdd1fdc970c 100644 --- a/sphinx/domains/python/_object.py +++ b/sphinx/domains/python/_object.py @@ -25,7 +25,6 @@ ) if TYPE_CHECKING: - from docutils.nodes import Node from docutils.parsers.rst.states import Inliner diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 008367b566a..629cad3a5cd 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -4,7 +4,7 @@ import re from copy import copy -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Final, cast +from typing import TYPE_CHECKING, Any, ClassVar, Final, cast from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -23,7 +23,7 @@ from sphinx.util.parsing import nested_parse_to_nodes if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator from sphinx.application import Sphinx from sphinx.builders import Builder @@ -402,7 +402,7 @@ def run(self) -> list[Node]: in_comment = False was_empty = True messages: list[Node] = [] - for line, (source, lineno) in zip(self.content, self.content.items): + for line, (source, lineno) in zip(self.content, self.content.items, strict=True): # empty line -> add to last definition if not line: if in_definition and entries: @@ -814,13 +814,12 @@ def process_doc( if not sectname: continue else: - if (isinstance(node, (nodes.definition_list, - nodes.field_list)) and + if (isinstance(node, nodes.definition_list | nodes.field_list) and node.children): node = cast(nodes.Element, node.children[0]) - if isinstance(node, (nodes.field, nodes.definition_list_item)): + if isinstance(node, nodes.field | nodes.definition_list_item): node = cast(nodes.Element, node.children[0]) - if isinstance(node, (nodes.term, nodes.field_name)): + if isinstance(node, nodes.term | nodes.field_name): sectname = clean_astext(node) else: toctree = next(node.findall(addnodes.toctree), None) @@ -1114,7 +1113,7 @@ def get_numfig_title(self, node: Node) -> str | None: return title_getter(elem) else: for subnode in elem: - if isinstance(subnode, (nodes.caption, nodes.title)): + if isinstance(subnode, nodes.caption | nodes.title): return clean_astext(subnode) return None diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 4539c1eab6a..f99fa8489d9 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -9,7 +9,7 @@ from collections import defaultdict from copy import copy from os import path -from typing import TYPE_CHECKING, Any, Callable, NoReturn +from typing import TYPE_CHECKING, Any, NoReturn from sphinx import addnodes from sphinx.environment.adapters import toctree as toctree_adapters @@ -23,7 +23,7 @@ from sphinx.util.osutil import canon_path, os_path if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from pathlib import Path from docutils import nodes diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 4009e9ef881..777002a895a 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -13,16 +13,14 @@ from sphinx.util.index_entries import _split_into if TYPE_CHECKING: - from typing import Literal, Optional, Union - - from typing_extensions import TypeAlias + from typing import Literal, TypeAlias from sphinx.builders import Builder from sphinx.environment import BuildEnvironment - _IndexEntryTarget: TypeAlias = tuple[Optional[str], Union[str, Literal[False]]] + _IndexEntryTarget: TypeAlias = tuple[str | None, str | Literal[False]] _IndexEntryTargets: TypeAlias = list[_IndexEntryTarget] - _IndexEntryCategoryKey: TypeAlias = Optional[str] + _IndexEntryCategoryKey: TypeAlias = str | None _IndexEntrySubItems: TypeAlias = dict[ str, tuple[_IndexEntryTargets, _IndexEntryCategoryKey], diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index 42c0bb49560..64ad4942a5f 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -404,7 +404,7 @@ def _toctree_standard_entry( def _toctree_add_classes(node: Element, depth: int, docname: str) -> None: """Add 'toctree-l%d' and 'current' classes to the toctree.""" for subnode in node.children: - if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)): + if isinstance(subnode, addnodes.compact_paragraph | nodes.list_item): # for

and

  • , indicate the depth level and recurse subnode['classes'].append(f'toctree-l{depth - 1}') _toctree_add_classes(subnode, depth, docname) @@ -442,7 +442,7 @@ def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tag copy = node.copy() for subnode in node.children: - if isinstance(subnode, (addnodes.compact_paragraph, nodes.list_item)): + if isinstance(subnode, addnodes.compact_paragraph | nodes.list_item): # for

    and

  • , just recurse copy.append(_toctree_copy(subnode, depth, maxdepth, collapse, tags)) elif isinstance(subnode, nodes.bullet_list): @@ -462,7 +462,7 @@ def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tag copy.append(_toctree_copy( child, depth, maxdepth, collapse, tags, # type: ignore[type-var] )) - elif isinstance(subnode, (nodes.reference, nodes.title)): + elif isinstance(subnode, nodes.reference | nodes.title): # deep copy references and captions sub_node_copy = subnode.copy() sub_node_copy.children = [child.deepcopy() for child in subnode.children] diff --git a/sphinx/events.py b/sphinx/events.py index 8a69b82a2fe..9a455d3d035 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -8,7 +8,7 @@ import contextlib from collections import defaultdict from operator import attrgetter -from typing import TYPE_CHECKING, Any, Callable, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from sphinx.errors import ExtensionError, SphinxError from sphinx.locale import __ @@ -16,6 +16,8 @@ from sphinx.util.inspect import safe_getattr if TYPE_CHECKING: + from collections.abc import Callable + from sphinx.application import Sphinx diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index d84258c4b25..173407401e5 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -10,9 +10,8 @@ import functools import operator import re -import sys from inspect import Parameter, Signature -from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar from docutils.statemachine import StringList @@ -40,7 +39,7 @@ ) if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Callable, Iterator, Sequence from types import ModuleType from sphinx.application import Sphinx @@ -612,7 +611,7 @@ def add_content(self, more_content: StringList | None) -> None: # add additional content (e.g. from document), if present if more_content: - for line, src in zip(more_content.data, more_content.items): + for line, src in zip(more_content.data, more_content.items, strict=True): self.add_line(line, src[0], src[1]) def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]: @@ -975,7 +974,7 @@ def add_content(self, more_content: StringList | None) -> None: super().add_content(None) self.indent = old_indent if more_content: - for line, src in zip(more_content.data, more_content.items): + for line, src in zip(more_content.data, more_content.items, strict=True): self.add_line(line, src[0], src[1]) @classmethod @@ -1450,7 +1449,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # Must be higher than FunctionDocumenter, ClassDocumenter, and # AttributeDocumenter as NewType can be an attribute and is a class - # after Python 3.10. Before 3.10 it is a kind of function object + # after Python 3.10. priority = 15 _signature_class: Any = None @@ -1740,24 +1739,6 @@ def get_doc(self) -> list[list[str]] | None: if isinstance(self.object, TypeVar): if self.object.__doc__ == TypeVar.__doc__: return [] - if sys.version_info[:2] < (3, 10): - if inspect.isNewType(self.object) or isinstance(self.object, TypeVar): - parts = self.modname.strip('.').split('.') - orig_objpath = self.objpath - for i in range(len(parts)): - new_modname = '.'.join(parts[:len(parts) - i]) - new_objpath = parts[len(parts) - i:] + orig_objpath - try: - analyzer = ModuleAnalyzer.for_module(new_modname) - analyzer.analyze() - key = ('', new_objpath[-1]) - comment = list(analyzer.attr_docs.get(key, [])) - if comment: - self.objpath = new_objpath - self.modname = new_modname - return [comment] - except PycodeError: - pass if self.doc_as_attr: # Don't show the docstring of the class when it is an alias. if self.get_variable_comment(): diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 57af4c5c626..66b42a67ddf 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from docutils import nodes from docutils.statemachine import StringList diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py index a9932b38b3d..8242934243e 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -102,9 +102,9 @@ def _is_lambda(x: Any, /) -> bool: def _get_arguments_inner(x: Any, /) -> ast.arguments | None: - if isinstance(x, (ast.AsyncFunctionDef, ast.FunctionDef, ast.Lambda)): + if isinstance(x, ast.AsyncFunctionDef | ast.FunctionDef | ast.Lambda): return x.args - if isinstance(x, (ast.Assign, ast.AnnAssign)): + if isinstance(x, ast.Assign | ast.AnnAssign): return _get_arguments_inner(x.value) return None diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index e6ba27439b2..4d2e5cf4a9b 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -11,7 +11,7 @@ import time from io import StringIO from os import path -from typing import TYPE_CHECKING, Any, Callable, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from docutils import nodes from docutils.parsers.rst import directives @@ -27,7 +27,7 @@ from sphinx.util.osutil import relpath if TYPE_CHECKING: - from collections.abc import Iterable, Sequence + from collections.abc import Callable, Iterable, Sequence from docutils.nodes import Element, Node, TextElement @@ -420,12 +420,12 @@ def test_doc(self, docname: str, doctree: Node) -> None: if self.config.doctest_test_doctest_blocks: def condition(node: Node) -> bool: - return (isinstance(node, (nodes.literal_block, nodes.comment)) and + return (isinstance(node, nodes.literal_block | nodes.comment) and 'testnodetype' in node) or \ isinstance(node, nodes.doctest_block) else: def condition(node: Node) -> bool: - return isinstance(node, (nodes.literal_block, nodes.comment)) \ + return isinstance(node, nodes.literal_block | nodes.comment) \ and 'testnodetype' in node for node in doctree.findall(condition): if self.skipped(node): # type: ignore[arg-type] diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 3fe7e04fd98..71049153dae 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -63,7 +63,7 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None: continue # ensure values are properly formatted - if not isinstance(value, (tuple, list)): + if not isinstance(value, (tuple | list)): errors += 1 msg = __( 'Invalid value `%r` in intersphinx_mapping[%r]. ' @@ -105,7 +105,7 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None: # ensure inventory locations are None or non-empty targets: list[InventoryLocation] = [] - for target in (inv if isinstance(inv, (tuple, list)) else (inv,)): + for target in (inv if isinstance(inv, (tuple | list)) else (inv,)): if target is None or target and isinstance(target, str): targets.append(target) else: diff --git a/sphinx/ext/intersphinx/_shared.py b/sphinx/ext/intersphinx/_shared.py index fed5844947d..c8c0689c145 100644 --- a/sphinx/ext/intersphinx/_shared.py +++ b/sphinx/ext/intersphinx/_shared.py @@ -7,8 +7,6 @@ from sphinx.util import logging if TYPE_CHECKING: - from typing import Optional - from sphinx.environment import BuildEnvironment from sphinx.util.typing import Inventory @@ -26,7 +24,7 @@ #: #: Empty strings are not expected and ``None`` indicates the default #: inventory file name :data:`~sphinx.builder.html.INVENTORY_FILENAME`. - InventoryLocation = Optional[str] + InventoryLocation = str | None #: Inventory cache entry. The integer field is the cache expiration time. InventoryCacheEntry = tuple[InventoryName, int, Inventory] diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 2ce3b2d429a..cc3c7f4ce1a 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -8,14 +8,14 @@ import re from functools import partial from itertools import starmap -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.typing import get_type_hints, stringify_annotation if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index 63c5b1930dd..9746d3eab84 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -4,7 +4,7 @@ from os import path from pprint import pformat -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from jinja2.sandbox import SandboxedEnvironment @@ -15,7 +15,7 @@ from sphinx.util.osutil import mtimes_of_files if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from jinja2.environment import Environment diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 81850bfe397..44640a93425 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import Iterable - from typing import Any, Callable + from collections.abc import Callable, Iterable + from typing import Any class _TranslationProxy: diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 7517d48d8b5..7ed107f4ab3 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -130,7 +130,7 @@ def visit_Call(self, node: ast.Call) -> str: def visit_Constant(self, node: ast.Constant) -> str: if node.value is Ellipsis: return "..." - elif isinstance(node.value, (int, float, complex)): + elif isinstance(node.value, int | float | complex): if self.code: return ast.get_source_segment(self.code, node) or repr(node.value) else: @@ -141,7 +141,7 @@ def visit_Constant(self, node: ast.Constant) -> str: def visit_Dict(self, node: ast.Dict) -> str: keys = (self.visit(k) for k in node.keys if k is not None) values = (self.visit(v) for v in node.values) - items = (k + ": " + v for k, v in zip(keys, values)) + items = (k + ": " + v for k, v in zip(keys, values, strict=True)) return "{" + ", ".join(items) + "}" def visit_Lambda(self, node: ast.Lambda) -> str: diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 2bfe25beb61..18bb993053b 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -108,7 +108,7 @@ def __eq__(self, other: Any) -> bool: return self.kind == other elif isinstance(other, str): return self.value == other - elif isinstance(other, (list, tuple)): + elif isinstance(other, list | tuple): return [self.kind, self.value] == list(other) elif other is None: return False @@ -404,7 +404,7 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: def visit_Expr(self, node: ast.Expr) -> None: """Handles Expr node and pick up a comment if string.""" - if (isinstance(self.previous, (ast.Assign, ast.AnnAssign)) and + if (isinstance(self.previous, ast.Assign | ast.AnnAssign) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)): try: targets = get_assign_targets(self.previous) diff --git a/sphinx/registry.py b/sphinx/registry.py index 3ae5fd1aae9..f802cff0829 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -2,16 +2,11 @@ from __future__ import annotations -import sys import traceback from importlib import import_module +from importlib.metadata import entry_points from types import MethodType -from typing import TYPE_CHECKING, Any, Callable - -if sys.version_info >= (3, 10): - from importlib.metadata import entry_points -else: - from importlib_metadata import entry_points +from typing import TYPE_CHECKING, Any from sphinx.domains import Domain, Index, ObjType from sphinx.domains.std import GenericObject, Target @@ -25,7 +20,7 @@ from sphinx.util.logging import prefixed_warnings if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Callable, Iterator, Sequence from docutils import nodes from docutils.core import Publisher diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 8244e692f80..3a9ee8ddb20 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -4,12 +4,13 @@ import shutil import sys import warnings -from typing import IO, TYPE_CHECKING, Any, Callable +from typing import IO, TYPE_CHECKING, Any from sphinx.deprecation import RemovedInSphinx90Warning if TYPE_CHECKING: import builtins + from collections.abc import Callable warnings.warn("'sphinx.testing.path' is deprecated. " "Use 'os.path' or 'pathlib' instead.", diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 1cc6c4a3a0b..53e55d6d5ce 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -43,7 +43,7 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> 'The node%s has %d child nodes, not one' % (xpath, len(node)) assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs) elif isinstance(cls, tuple): - assert isinstance(node, (list, nodes.Element)), \ + assert isinstance(node, list | nodes.Element), \ 'The node%s does not have any items' % xpath assert len(node) == len(cls), \ 'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls)) diff --git a/sphinx/theming.py b/sphinx/theming.py index bd7678fc6b4..b132c8bcd24 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -10,6 +10,7 @@ import shutil import sys import tempfile +from importlib.metadata import entry_points from os import path from typing import TYPE_CHECKING, Any from zipfile import ZipFile @@ -26,10 +27,6 @@ else: import tomli as tomllib -if sys.version_info >= (3, 10): - from importlib.metadata import entry_points -else: - from importlib_metadata import entry_points if TYPE_CHECKING: from collections.abc import Callable diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 12ca03a597e..fd7dd71faf5 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -22,10 +22,10 @@ if TYPE_CHECKING: from collections.abc import Iterator - from typing import Literal + from typing import Literal, TypeAlias from docutils.nodes import Node, Text - from typing_extensions import TypeAlias, TypeIs + from typing_extensions import TypeIs from sphinx.application import Sphinx from sphinx.config import Config @@ -247,7 +247,7 @@ class ApplySourceWorkaround(SphinxTransform): def apply(self, **kwargs: Any) -> None: for node in self.document.findall(): # type: Node - if isinstance(node, (nodes.TextElement, nodes.image, nodes.topic)): + if isinstance(node, nodes.TextElement | nodes.image | nodes.topic): apply_source_workaround(node) @@ -477,7 +477,7 @@ def _reorder_index_target_nodes(start_node: nodes.target) -> None: # as we want *consecutive* target & index nodes. node: nodes.Node for node in start_node.findall(descend=False, siblings=True): - if isinstance(node, (nodes.target, addnodes.index)): + if isinstance(node, nodes.target | addnodes.index): nodes_to_reorder.append(node) continue break # must be a consecutive run of target or index nodes diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index 53f38685a26..82624a9f109 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -3,8 +3,9 @@ from __future__ import annotations import re +from collections.abc import Callable from copy import deepcopy -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from docutils import nodes diff --git a/sphinx/util/display.py b/sphinx/util/display.py index f3ff8a5a255..f3aea633e34 100644 --- a/sphinx/util/display.py +++ b/sphinx/util/display.py @@ -7,9 +7,9 @@ from sphinx.util.console import bold, color_terminal if False: - from collections.abc import Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator from types import TracebackType - from typing import Any, Callable, TypeVar + from typing import Any, TypeVar from typing_extensions import ParamSpec @@ -21,7 +21,7 @@ def display_chunk(chunk: Any) -> str: - if isinstance(chunk, (list, tuple)): + if isinstance(chunk, list | tuple): if len(chunk) == 1: return str(chunk[0]) return f'{chunk[0]} .. {chunk[-1]}' diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index c277a59c51b..0ef44d2fd93 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -356,7 +356,7 @@ def transform(self, node: nodes.field_list) -> None: if is_typefield: # filter out only inline nodes; others will result in invalid # markup being written out - content = [n for n in content if isinstance(n, (nodes.Inline, nodes.Text))] + content = [n for n in content if isinstance(n, nodes.Inline | nodes.Text)] if content: types.setdefault(typename, {})[fieldarg] = content continue diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index c1a5ae2e353..ef9d236e22a 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -8,7 +8,7 @@ from contextlib import contextmanager from copy import copy from os import path -from typing import IO, TYPE_CHECKING, Any, Callable, cast +from typing import IO, TYPE_CHECKING, Any, cast import docutils from docutils import nodes @@ -27,7 +27,7 @@ report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(\\d+)?\\) ') if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator # NoQA: TCH003 from types import ModuleType from docutils.frontend import Values diff --git a/sphinx/util/fileutil.py b/sphinx/util/fileutil.py index 10bfc3bd610..9def09fca62 100644 --- a/sphinx/util/fileutil.py +++ b/sphinx/util/fileutil.py @@ -4,7 +4,7 @@ import os import posixpath -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from docutils.utils import relative_path @@ -12,6 +12,8 @@ from sphinx.util.osutil import copyfile, ensuredir if TYPE_CHECKING: + from collections.abc import Callable + from sphinx.util.template import BaseRenderer from sphinx.util.typing import PathMatcher diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index c14e3f099f0..8a947f5ca82 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: import datetime as dt from collections.abc import Iterator - from typing import Protocol, Union + from typing import Protocol from babel.core import Locale @@ -52,7 +52,7 @@ def __call__( # NoQA: E704 locale: str | Locale | None = ..., ) -> str: ... - Formatter = Union[DateFormatter, TimeFormatter, DatetimeFormatter] + Formatter = DateFormatter | TimeFormatter | DatetimeFormatter logger = logging.getLogger(__name__) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 28bba0c3032..c58178e4441 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -27,9 +27,9 @@ from collections.abc import Callable, Sequence from inspect import _ParameterKind from types import MethodType, ModuleType - from typing import Final, Protocol, Union + from typing import Final, Protocol, TypeAlias - from typing_extensions import TypeAlias, TypeIs + from typing_extensions import TypeIs class _SupportsGet(Protocol): def __get__(self, __instance: Any, __owner: type | None = ...) -> Any: ... # NoQA: E704 @@ -42,21 +42,21 @@ class _SupportsDelete(Protocol): # instance is contravariant but we do not need that precision def __delete__(self, __instance: Any) -> None: ... # NoQA: E704 - _RoutineType: TypeAlias = Union[ - types.FunctionType, - types.LambdaType, - types.MethodType, - types.BuiltinFunctionType, - types.BuiltinMethodType, - types.WrapperDescriptorType, - types.MethodDescriptorType, - types.ClassMethodDescriptorType, - ] - _SignatureType: TypeAlias = Union[ - Callable[..., Any], - staticmethod, - classmethod, - ] + _RoutineType: TypeAlias = ( + types.FunctionType + | types.LambdaType + | types.MethodType + | types.BuiltinFunctionType + | types.BuiltinMethodType + | types.WrapperDescriptorType + | types.MethodDescriptorType + | types.ClassMethodDescriptorType + ) + _SignatureType: TypeAlias = ( + Callable[..., Any] + | staticmethod + | classmethod + ) logger = logging.getLogger(__name__) @@ -128,20 +128,14 @@ def getall(obj: Any) -> Sequence[str] | None: __all__ = safe_getattr(obj, '__all__', None) if __all__ is None: return None - if isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__): + if isinstance(__all__, list | tuple) and all(isinstance(e, str) for e in __all__): return __all__ raise ValueError(__all__) def getannotations(obj: Any) -> Mapping[str, Any]: """Safely get the ``__annotations__`` attribute of an object.""" - if sys.version_info >= (3, 10, 0) or not isinstance(obj, type): - __annotations__ = safe_getattr(obj, '__annotations__', None) - else: - # Workaround for bugfix not available until python 3.10 as recommended by docs - # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older - __dict__ = safe_getattr(obj, '__dict__', {}) - __annotations__ = __dict__.get('__annotations__', None) + __annotations__ = safe_getattr(obj, '__annotations__', None) if isinstance(__annotations__, Mapping): return __annotations__ return {} @@ -198,7 +192,7 @@ def getslots(obj: Any) -> dict[str, Any] | dict[str, None] | None: return __slots__ elif isinstance(__slots__, str): return {__slots__: None} - elif isinstance(__slots__, (list, tuple)): + elif isinstance(__slots__, list | tuple): return dict.fromkeys(__slots__) else: raise ValueError @@ -206,11 +200,7 @@ def getslots(obj: Any) -> dict[str, Any] | dict[str, None] | None: def isNewType(obj: Any) -> bool: """Check the if object is a kind of :class:`~typing.NewType`.""" - if sys.version_info[:2] >= (3, 10): - return isinstance(obj, typing.NewType) - __module__ = safe_getattr(obj, '__module__', None) - __qualname__ = safe_getattr(obj, '__qualname__', None) - return __module__ == 'typing' and __qualname__ == 'NewType..new_type' + return isinstance(obj, typing.NewType) def isenumclass(x: Any) -> TypeIs[type[enum.Enum]]: @@ -237,7 +227,7 @@ def unpartial(obj: Any) -> Any: def ispartial(obj: Any) -> TypeIs[partial | partialmethod]: """Check if the object is a partial function or method.""" - return isinstance(obj, (partial, partialmethod)) + return isinstance(obj, partial | partialmethod) def isclassmethod( @@ -397,12 +387,12 @@ def _is_wrapped_coroutine(obj: Any) -> bool: def isproperty(obj: Any) -> TypeIs[property | cached_property]: """Check if the object is property (possibly cached).""" - return isinstance(obj, (property, cached_property)) + return isinstance(obj, property | cached_property) def isgenericalias(obj: Any) -> TypeIs[types.GenericAlias]: """Check if the object is a generic alias.""" - return isinstance(obj, (types.GenericAlias, typing._BaseGenericAlias)) # type: ignore[attr-defined] + return isinstance(obj, types.GenericAlias | typing._BaseGenericAlias) # type: ignore[attr-defined] def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any: @@ -852,11 +842,11 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> Signature: params: list[Parameter] = [] # positional-only arguments (introduced in Python 3.8) - for arg, defexpr in zip(args.posonlyargs, defaults): + for arg, defexpr in zip(args.posonlyargs, defaults, strict=False): params.append(_define(Parameter.POSITIONAL_ONLY, arg, code, defexpr=defexpr)) # normal arguments - for arg, defexpr in zip(args.args, defaults[pos_only_offset:]): + for arg, defexpr in zip(args.args, defaults[pos_only_offset:], strict=False): params.append(_define(Parameter.POSITIONAL_OR_KEYWORD, arg, code, defexpr=defexpr)) # variadic positional argument (no possible default expression) @@ -864,7 +854,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> Signature: params.append(_define(Parameter.VAR_POSITIONAL, args.vararg, code, defexpr=None)) # keyword-only arguments - for arg, defexpr in zip(args.kwonlyargs, args.kw_defaults): + for arg, defexpr in zip(args.kwonlyargs, args.kw_defaults, strict=False): params.append(_define(Parameter.KEYWORD_ONLY, arg, code, defexpr=defexpr)) # variadic keyword argument (no possible default expression) diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index bb0eca6a5cb..c48922cfb61 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -4,7 +4,7 @@ import os import re import zlib -from typing import IO, TYPE_CHECKING, Callable +from typing import IO, TYPE_CHECKING from sphinx.locale import __ from sphinx.util import logging @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from sphinx.builders import Builder from sphinx.environment import BuildEnvironment diff --git a/sphinx/util/matching.py b/sphinx/util/matching.py index 481ca12e144..79c56dd2e02 100644 --- a/sphinx/util/matching.py +++ b/sphinx/util/matching.py @@ -4,12 +4,12 @@ import os.path import re -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from sphinx.util.osutil import canon_path, path_stabilize if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator def _translate_pattern(pat: str) -> str: diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 37ce79c6340..53bfd2143b5 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -5,7 +5,7 @@ import contextlib import re import unicodedata -from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from docutils import nodes from docutils.nodes import Node @@ -16,7 +16,7 @@ from sphinx.util.parsing import _fresh_title_style_context if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Callable, Iterable, Iterator from docutils.nodes import Element from docutils.parsers.rst import Directive @@ -178,12 +178,12 @@ def apply_source_workaround(node: Element) -> None: return # workaround: some docutils nodes doesn't have source, line. - if (isinstance(node, ( - nodes.rubric, # #1305 rubric directive - nodes.line, # #1477 line node - nodes.image, # #3093 image directive in substitution - nodes.field_name, # #3335 field list syntax - ))): + if isinstance(node, ( + nodes.rubric # #1305 rubric directive + | nodes.line # #1477 line node + | nodes.image # #3093 image directive in substitution + | nodes.field_name # #3335 field list syntax + )): logger.debug('[i18n] PATCH: %r to have source and line: %s', get_full_module_name(node), repr_domxml(node)) try: diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index ea49e82cf4d..b99cea1a727 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -49,7 +49,7 @@ def relative_uri(base: str, to: str) -> str: b2 = base.split('#')[0].split(SEP) t2 = to.split('#')[0].split(SEP) # remove common segments (except the last segment) - for x, y in zip(b2[:-1], t2[:-1]): + for x, y in zip(b2[:-1], t2[:-1], strict=False): if x != y: break b2.pop(0) diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index 10f8c8945af..32912e014d7 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -6,7 +6,7 @@ import time import traceback from math import sqrt -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any try: import multiprocessing @@ -18,7 +18,7 @@ from sphinx.util import logging if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence logger = logging.getLogger(__name__) diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py index 4afbd37445e..7c64e940dfc 100644 --- a/sphinx/util/requests.py +++ b/sphinx/util/requests.py @@ -19,7 +19,7 @@ def _get_tls_cacert(url: str, certs: str | dict[str, str] | None) -> str | bool: """Get additional CA cert for a specific URL.""" if not certs: return True - elif isinstance(certs, (str, tuple)): + elif isinstance(certs, str | tuple): return certs else: hostname = urlsplit(url).netloc diff --git a/sphinx/util/template.py b/sphinx/util/template.py index 0ddc29e177f..2b38e3a8678 100644 --- a/sphinx/util/template.py +++ b/sphinx/util/template.py @@ -5,7 +5,7 @@ import os from functools import partial from os import path -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from jinja2 import TemplateNotFound from jinja2.loaders import BaseLoader @@ -17,7 +17,7 @@ from sphinx.util import rst, texescape if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence from jinja2.environment import Environment @@ -38,7 +38,7 @@ def render_string(self, source: str, context: dict[str, Any]) -> str: class FileRenderer(BaseRenderer): def __init__(self, search_path: Sequence[str | os.PathLike[str]]) -> None: - if isinstance(search_path, (str, os.PathLike)): + if isinstance(search_path, str | os.PathLike): search_path = [search_path] else: # filter "None" paths diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 49d071d9c0f..8f48fbdfc83 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -6,14 +6,13 @@ import sys import types import typing -from collections.abc import Sequence +from collections.abc import Callable, Sequence from contextvars import Context, ContextVar, Token from struct import Struct from typing import ( TYPE_CHECKING, Annotated, Any, - Callable, ForwardRef, TypedDict, TypeVar, @@ -25,9 +24,9 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Final, Literal, Protocol + from typing import Final, Literal, Protocol, TypeAlias - from typing_extensions import TypeAlias, TypeIs + from typing_extensions import TypeIs from sphinx.application import Sphinx @@ -41,10 +40,6 @@ 'smart', ] -if sys.version_info >= (3, 10): - from types import UnionType -else: - UnionType = None # classes that have an incorrect .__module__ attribute _INVALID_BUILTIN_CLASSES: Final[Mapping[object, str]] = { @@ -85,7 +80,7 @@ def is_invalid_builtin_class(obj: Any) -> bool: # Text like nodes which are initialized with text and rawsource -TextlikeNode = Union[nodes.Text, nodes.TextElement] +TextlikeNode = nodes.Text | nodes.TextElement # type of None NoneType = type(None) @@ -206,7 +201,7 @@ def _is_unpack_form(obj: Any) -> bool: # that typing_extensions.Unpack should not be used in that case return typing.get_origin(obj) is Unpack - # 3.9 and 3.10 require typing_extensions.Unpack + # Python 3.10 requires typing_extensions.Unpack origin = typing.get_origin(obj) return ( getattr(origin, '__module__', None) == 'typing_extensions' @@ -215,13 +210,11 @@ def _is_unpack_form(obj: Any) -> bool: def _typing_internal_name(obj: Any) -> str | None: - if sys.version_info[:2] >= (3, 10): - try: - return obj.__name__ - except AttributeError: - # e.g. ParamSpecArgs, ParamSpecKwargs - return '' - return getattr(obj, '_name', None) + try: + return obj.__name__ + except AttributeError: + # e.g. ParamSpecArgs, ParamSpecKwargs + return '' def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: @@ -291,11 +284,9 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return (f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' fr'\ [{args}, {meta}]') elif inspect.isNewType(cls): - if sys.version_info[:2] >= (3, 10): - # newtypes have correct module info since Python 3.10+ - return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' - return f':py:class:`{cls.__name__}`' - elif UnionType and isinstance(cls, UnionType): + # newtypes have correct module info since Python 3.10+ + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' + elif isinstance(cls, types.UnionType): # Union types (PEP 585) retain their definition order when they # are printed natively and ``None``-like types are kept as is. return ' | '.join(restify(a, mode) for a in cls.__args__) @@ -436,17 +427,14 @@ def stringify_annotation( return annotation_name return module_prefix + f'{annotation_module}.{annotation_name}' elif isNewType(annotation): - if sys.version_info[:2] >= (3, 10): - # newtypes have correct module info since Python 3.10+ - return module_prefix + f'{annotation_module}.{annotation_name}' - return annotation_name + return module_prefix + f'{annotation_module}.{annotation_name}' elif ismockmodule(annotation): return module_prefix + annotation_name elif ismock(annotation): return module_prefix + f'{annotation_module}.{annotation_name}' elif is_invalid_builtin_class(annotation): return module_prefix + _INVALID_BUILTIN_CLASSES[annotation] - elif _is_annotated_form(annotation): # for py39+ + elif _is_annotated_form(annotation): # for py310+ pass elif annotation_module == 'builtins' and annotation_qualname: args = getattr(annotation, '__args__', None) @@ -495,7 +483,7 @@ def stringify_annotation( elif hasattr(annotation, '__origin__'): # instantiated generic provided by a user qualname = stringify_annotation(annotation.__origin__, mode) - elif UnionType and isinstance(annotation, UnionType): # types.UnionType (for py3.10+) + elif isinstance(annotation, types.UnionType): qualname = 'types.UnionType' else: # we weren't able to extract the base type, appending arguments would @@ -505,7 +493,7 @@ def stringify_annotation( # Process the generic arguments (if any). # They must be a list or a tuple, otherwise they are considered 'broken'. annotation_args = getattr(annotation, '__args__', ()) - if annotation_args and isinstance(annotation_args, (list, tuple)): + if annotation_args and isinstance(annotation_args, list | tuple): if ( qualname in {'Union', 'types.UnionType'} and all(getattr(a, '__origin__', ...) is typing.Literal for a in annotation_args) @@ -525,7 +513,7 @@ def stringify_annotation( args = ', '.join(_format_literal_arg_stringify(a, mode=mode) for a in annotation_args) return f'{module_prefix}Literal[{args}]' - elif _is_annotated_form(annotation): # for py39+ + elif _is_annotated_form(annotation): # for py310+ args = stringify_annotation(annotation_args[0], mode) meta_args = [] for m in annotation.__metadata__: @@ -541,11 +529,6 @@ def stringify_annotation( else: meta_args.append(repr(m)) meta = ', '.join(meta_args) - if sys.version_info[:2] <= (3, 9): - if mode == 'smart': - return f'~typing.Annotated[{args}, {meta}]' - if mode == 'fully-qualified': - return f'typing.Annotated[{args}, {meta}]' if sys.version_info[:2] <= (3, 11): if mode == 'fully-qualified-except-typing': return f'Annotated[{args}, {meta}]' diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index e02f6e8243c..54f168e6a4d 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1369,7 +1369,7 @@ def visit_paragraph(self, node: Element) -> None: not isinstance(node.parent[index - 1], nodes.compound)): # insert blank line, if the paragraph follows a non-paragraph node in a compound self.body.append(r'\noindent' + CR) - elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)): + elif index == 1 and isinstance(node.parent, nodes.footnote | footnotetext): # don't insert blank line, if the paragraph is second child of a footnote # (first one is label node) pass @@ -2081,7 +2081,7 @@ def visit_block_quote(self, node: Element) -> None: done = 0 if len(node.children) == 1: child = node.children[0] - if isinstance(child, (nodes.bullet_list, nodes.enumerated_list)): + if isinstance(child, nodes.bullet_list | nodes.enumerated_list): done = 1 if not done: self.body.append(r'\begin{quote}' + CR) @@ -2092,7 +2092,7 @@ def depart_block_quote(self, node: Element) -> None: done = 0 if len(node.children) == 1: child = node.children[0] - if isinstance(child, (nodes.bullet_list, nodes.enumerated_list)): + if isinstance(child, nodes.bullet_list | nodes.enumerated_list): done = 1 if not done: self.body.append(r'\end{quote}' + CR) diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 3f9584f910d..b9953a5338a 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -297,7 +297,7 @@ def collect_node_menus(self) -> None: # try to find a suitable "Top" node title = self.document.next_node(nodes.title) top = title.parent if title else self.document - if not isinstance(top, (nodes.document, nodes.section)): + if not isinstance(top, nodes.document | nodes.section): top = self.document if top is not self.document: entries = node_menus[top['node_name']] @@ -625,7 +625,7 @@ def visit_title(self, node: Element) -> None: parent = node.parent if isinstance(parent, nodes.table): return - if isinstance(parent, (nodes.Admonition, nodes.sidebar, nodes.topic)): + if isinstance(parent, nodes.Admonition | nodes.sidebar | nodes.topic): raise nodes.SkipNode if not isinstance(parent, nodes.section): logger.warning(__('encountered title node not in section, topic, table, ' @@ -694,7 +694,7 @@ def depart_target(self, node: Element) -> None: def visit_reference(self, node: Element) -> None: # an xref's target is displayed in Info so we ignore a few # cases for the sake of appearance - if isinstance(node.parent, (nodes.title, addnodes.desc_type)): + if isinstance(node.parent, nodes.title | addnodes.desc_type): return if len(node) != 0 and isinstance(node[0], nodes.image): return @@ -987,7 +987,7 @@ def visit_term(self, node: Element) -> None: self.add_anchor(id, node) # anchors and indexes need to go in front for n in node[::]: - if isinstance(n, (addnodes.index, nodes.target)): + if isinstance(n, addnodes.index | nodes.target): n.walkabout(self) node.remove(n) self.body.append('\n%s ' % self.at_item_x) diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index d2dd2083654..de4c51228dd 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -6,7 +6,7 @@ import re import textwrap from collections.abc import Iterable, Iterator, Sequence -from itertools import chain, groupby +from itertools import chain, groupby, pairwise from typing import TYPE_CHECKING, Any, cast from docutils import nodes, writers @@ -221,10 +221,10 @@ def writesep(char: str = "-", lineno: int | None = None) -> str: tail = "+" if out[-1][0] == "-" else "|" glue = [ "+" if left[0] == "-" or right[0] == "-" else "|" - for left, right in zip(out, out[1:]) + for left, right in pairwise(out) ] glue.append(tail) - return head + "".join(chain.from_iterable(zip(out, glue))) + return head + "".join(chain.from_iterable(zip(out, glue, strict=False))) for lineno, line in enumerate(self.lines): if self.separator and lineno == self.separator: diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index e58044e2c67..cb7b2b69115 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -25,10 +25,9 @@ if TYPE_CHECKING: from collections.abc import Iterable - from typing import Union - CircularList = list[Union[int, 'CircularList']] - CircularDict = dict[str, Union[int, 'CircularDict']] + CircularList = list[int | 'CircularList'] + CircularDict = dict[str, int | 'CircularDict'] def check_is_serializable(subject: object, *, circular: bool) -> None: @@ -209,9 +208,7 @@ def check( assert isinstance(u, list) assert v.__class__ is u.__class__ - assert len(u) == len(v) - - for u_i, v_i in zip(u, v): + for u_i, v_i in zip(u, v, strict=True): counter[type(u)] += 1 check(u_i, v_i, counter=counter, guard=guard | {id(u), id(v)}) @@ -275,9 +272,7 @@ def check( assert isinstance(u, dict) assert v.__class__ is u.__class__ - assert len(u) == len(v) - - for u_i, v_i in zip(u, v): + for u_i, v_i in zip(u, v, strict=True): counter[type(u)] += 1 check(u[u_i], v[v_i], counter=counter, guard=guard | {id(u), id(v)}) return counter @@ -573,8 +568,7 @@ def test_nitpick_base(app, status, warning): app.build(force_all=True) warning = warning.getvalue().strip().split('\n') - assert len(warning) == len(nitpick_warnings) - for actual, expected in zip(warning, nitpick_warnings): + for actual, expected in zip(warning, nitpick_warnings, strict=True): assert expected in actual @@ -629,8 +623,7 @@ def test_nitpick_ignore_regex_fullmatch(app, status, warning): app.build(force_all=True) warning = warning.getvalue().strip().split('\n') - assert len(warning) == len(nitpick_warnings) - for actual, expected in zip(warning, nitpick_warnings): + for actual, expected in zip(warning, nitpick_warnings, strict=True): assert expected in actual diff --git a/tests/test_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index 3e68d60a65e..ee89ef18b49 100644 --- a/tests/test_extensions/test_ext_autodoc_autoclass.py +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -7,7 +7,6 @@ from __future__ import annotations import typing -from typing import Union import pytest @@ -305,7 +304,7 @@ def autodoc_process_bases(app, name, obj, options, bases): assert obj.__name__ == 'Quux' assert options == {'show-inheritance': True, 'members': []} - assert bases == [typing.List[Union[int, float]]] # NoQA: UP006 + assert bases == [typing.List[typing.Union[int, float]]] # NoQA: UP006, UP007 bases.pop() bases.extend([int, str]) diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index e3f034c3bc0..46f0b6ea762 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -153,7 +153,7 @@ def test_get_items_summary(make_app, app_params): def new_get_items(self, names, *args, **kwargs): results = orig_get_items(self, names, *args, **kwargs) - for name, result in zip(names, results): + for name, result in zip(names, results, strict=True): autosummary_items[name] = result # NoQA: PERF403 return results diff --git a/tests/test_extensions/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py index 5afd0f38eef..f719e9dd4a2 100644 --- a/tests/test_extensions/test_ext_napoleon_docstring.py +++ b/tests/test_extensions/test_ext_napoleon_docstring.py @@ -2421,7 +2421,7 @@ def test_tokenize_type_spec(self): [r"'with \'quotes\''"], ) - for spec, expected in zip(specs, tokens): + for spec, expected in zip(specs, tokens, strict=True): actual = _tokenize_type_spec(spec) assert expected == actual @@ -2440,7 +2440,7 @@ def test_recombine_set_tokens(self): ["{'F', 'C', 'N'}", ", ", "default", " ", "None"], ) - for tokens_, expected in zip(tokens, combined_tokens): + for tokens_, expected in zip(tokens, combined_tokens, strict=True): actual = _recombine_set_tokens(tokens_) assert expected == actual @@ -2456,7 +2456,7 @@ def test_recombine_set_tokens_invalid(self): ["{1, 2", ", ", "default", ": ", "None"], ) - for tokens_, expected in zip(tokens, combined_tokens): + for tokens_, expected in zip(tokens, combined_tokens, strict=True): actual = _recombine_set_tokens(tokens_) assert expected == actual @@ -2491,7 +2491,7 @@ def test_convert_numpy_type_spec(self): ":class:`pandas.DataFrame`, *optional*", ) - for spec, expected in zip(specs, converted): + for spec, expected in zip(specs, converted, strict=True): actual = _convert_numpy_type_spec(spec, translations=translations) assert expected == actual @@ -2569,7 +2569,7 @@ def test_token_type_invalid(self, warning): r".+: malformed string literal \(missing closing quote\):", r".+: malformed string literal \(missing opening quote\):", ) - for token, error in zip(tokens, errors): + for token, error in zip(tokens, errors, strict=True): try: _token_type(token) finally: @@ -2698,6 +2698,6 @@ def test_napoleon_keyword_and_paramtype(app, tmp_path): a_ = list(li.findall('.//a[@class="reference external"]')) assert len(a_) == 2 - for a, uri in zip(a_, ('list.html', 'int.html')): + for a, uri in zip(a_, ('list.html', 'int.html'), strict=True): assert a.attrib['href'] == f'127.0.0.1:5555/{uri}' assert a.attrib['title'] == '(in Intersphinx Test v42)' diff --git a/tests/test_transforms/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py index 4bd446b73b0..10b28740163 100644 --- a/tests/test_transforms/test_transforms_post_transforms.py +++ b/tests/test_transforms/test_transforms_post_transforms.py @@ -228,13 +228,13 @@ def test_custom_implementation( if ignore_sig_element_fallback_transform: # desc_sig_element is implemented or desc_sig_* nodes are properly handled (and left untouched) - for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1]): + for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1], strict=True): assert_node(node, node_type) assert not node.hasattr('_sig_node_type') assert mess == f'mark: {node_type.__name__!r}' else: # desc_sig_* nodes are converted into inline nodes - for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1]): + for node_type, node, mess in zip(self._builtin_sig_elements, document.children[:-1], stdout[:-1], strict=True): assert_node(node, nodes.inline, _sig_node_type=node_type.__name__) assert mess == f'generic visit: {nodes.inline.__name__!r}' diff --git a/tests/test_util/test_util_console.py b/tests/test_util/test_util_console.py index b617a333f10..3343091aede 100644 --- a/tests/test_util/test_util_console.py +++ b/tests/test_util/test_util_console.py @@ -53,7 +53,7 @@ def next_ansi_blocks(choices: Sequence[str], n: int) -> Sequence[str]: # # For instance ``next_ansi_blocks(['a', 'b'], 3) == ['a', 'b', 'a']``. stream = itertools.cycle(choices) - return list(map(operator.itemgetter(0), zip(stream, range(n)))) + return list(map(operator.itemgetter(0), zip(stream, range(n), strict=False))) # generate all permutations of length N for sigma in itertools.permutations(range(N), N): diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 764ca20d1de..9ddab510b99 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -91,7 +91,7 @@ def test_TypeAliasForwardRef(): alias = TypeAliasForwardRef('example') assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example' - alias = Optional[alias] + alias = Optional[alias] # NoQA: UP007 assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example | None' @@ -775,7 +775,7 @@ def test_isproperty(): def test_isgenericalias(): #: A list of int T = List[int] # NoQA: UP006 - S = list[Union[str, None]] + S = list[Union[str, None]] # NoQA: UP006, UP007 C = Callable[[int], None] # a generic alias not having a doccomment diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 956cffe9dec..c6a2e38e133 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -44,8 +44,6 @@ Union, ) -import pytest - from sphinx.ext.autodoc import mock from sphinx.util.typing import _INVALID_BUILTIN_CLASSES, restify, stringify_annotation @@ -274,12 +272,8 @@ def test_restify_type_hints_typevars(): assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util.test_util_typing.T`]" assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]" - if sys.version_info[:2] >= (3, 10): - assert restify(MyInt) == ":py:class:`tests.test_util.test_util_typing.MyInt`" - assert restify(MyInt, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyInt`" - else: - assert restify(MyInt) == ":py:class:`MyInt`" - assert restify(MyInt, "smart") == ":py:class:`MyInt`" + assert restify(MyInt) == ":py:class:`tests.test_util.test_util_typing.MyInt`" + assert restify(MyInt, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyInt`" def test_restify_type_hints_custom_class(): @@ -363,7 +357,6 @@ class X(t.TypedDict): assert restify(t.Unpack['X'], 'smart') == expect -@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_restify_type_union_operator(): assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined] assert restify(None | int) == ":py:obj:`None` | :py:class:`int`" # type: ignore[attr-defined] @@ -385,7 +378,6 @@ def test_restify_mock(): assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`' -@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') def test_restify_type_hints_paramspec(): from typing import ParamSpec P = ParamSpec('P') @@ -658,12 +650,8 @@ def test_stringify_type_hints_typevars(): assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util.test_util_typing.T]" assert stringify_annotation(list[T], "smart") == "list[~tests.test_util.test_util_typing.T]" - if sys.version_info[:2] >= (3, 10): - assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyInt" - assert stringify_annotation(MyInt, "smart") == "~tests.test_util.test_util_typing.MyInt" - else: - assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "MyInt" - assert stringify_annotation(MyInt, "smart") == "MyInt" + assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyInt" + assert stringify_annotation(MyInt, "smart") == "~tests.test_util.test_util_typing.MyInt" def test_stringify_type_hints_custom_class(): @@ -695,7 +683,6 @@ def test_stringify_type_Literal(): assert stringify_annotation(Literal[MyEnum.a], 'smart') == '~typing.Literal[MyEnum.a]' -@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_stringify_type_union_operator(): assert stringify_annotation(int | None) == "int | None" # type: ignore[attr-defined] assert stringify_annotation(int | None, "smart") == "int | None" # type: ignore[attr-defined] @@ -738,7 +725,6 @@ def test_stringify_type_ForwardRef(): assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'smart') == "~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]" # type: ignore[attr-defined] -@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') def test_stringify_type_hints_paramspec(): from typing import ParamSpec P = ParamSpec('P') diff --git a/tox.ini b/tox.ini index 8e37493ece6..b6f1daac1f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.2.0 -envlist = py{39,310,311,312,313} +envlist = py{310,311,312,313} [testenv] usedevelop = True @@ -19,7 +19,7 @@ passenv = BUILDER READTHEDOCS description = - py{39,310,311,312,313}: Run unit tests against {envname}. + py{310,311,312,313}: Run unit tests against {envname}. extras = test setenv = diff --git a/utils/babel_runner.py b/utils/babel_runner.py index 4cb35320ca3..d8d9a386a77 100644 --- a/utils/babel_runner.py +++ b/utils/babel_runner.py @@ -106,7 +106,10 @@ def run_extract() -> None: options = opt_dict with open(os.path.join(root, filename), 'rb') as fileobj: for lineno, message, comments, context in extract( - method, fileobj, KEYWORDS, options=options + method, # type: ignore[arg-type] + fileobj, + KEYWORDS, + options=options, ): filepath = os.path.join(input_path, relative_name) catalogue.add( @@ -217,7 +220,7 @@ def run_compile() -> None: for x in message.locations ): msgid = message.id - if isinstance(msgid, (list, tuple)): + if isinstance(msgid, list | tuple): msgid = msgid[0] js_catalogue[msgid] = message.string diff --git a/utils/bump_version.py b/utils/bump_version.py index 7275ccaa02a..63931c88420 100755 --- a/utils/bump_version.py +++ b/utils/bump_version.py @@ -8,9 +8,7 @@ import time from contextlib import contextmanager from pathlib import Path -from typing import TYPE_CHECKING - -from typing_extensions import TypeAlias +from typing import TYPE_CHECKING, TypeAlias if TYPE_CHECKING: from collections.abc import Iterator, Sequence