From cb455ddada013cd4107071699379801e309562ad Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Thu, 15 Aug 2024 21:49:50 +0100 Subject: [PATCH] Add per-event overloads to ``Sphinx.connect()`` (#12784) Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- doc/conf.py | 6 + doc/extdev/event_callbacks.rst | 8 +- sphinx/application.py | 347 ++++++++++++++++++++++++++++++++- sphinx/config.py | 6 +- 4 files changed, 350 insertions(+), 17 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index fc86fcd6d..aad73e59b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -186,12 +186,15 @@ nitpick_ignore = { ('js:func', 'number'), ('js:func', 'string'), ('py:attr', 'srcline'), + ('py:class', '_ConfigRebuild'), # sphinx.application.Sphinx.add_config_value ('py:class', '_StrPath'), # sphinx.environment.BuildEnvironment.doc2path ('py:class', 'Element'), # sphinx.domains.Domain ('py:class', 'Documenter'), # sphinx.application.Sphinx.add_autodocumenter ('py:class', 'IndexEntry'), # sphinx.domains.IndexEntry + ('py:class', 'Lexer'), # sphinx.application.Sphinx.add_lexer ('py:class', 'Node'), # sphinx.domains.Domain ('py:class', 'NullTranslations'), # gettext.NullTranslations + ('py:class', 'Path'), # sphinx.application.Sphinx.connect ('py:class', 'RoleFunction'), # sphinx.domains.Domain ('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes ('py:class', 'Theme'), # sphinx.application.TemplateBridge @@ -199,6 +202,8 @@ nitpick_ignore = { ('py:class', 'StringList'), # sphinx.utils.parsing.nested_parse_to_nodes ('py:class', 'system_message'), # sphinx.utils.docutils.SphinxDirective ('py:class', 'TitleGetter'), # sphinx.domains.Domain + ('py:class', 'todo_node'), # sphinx.application.Sphinx.connect + ('py:class', 'Transform'), # sphinx.application.Sphinx.add_transform ('py:class', 'XRefRole'), # sphinx.domains.Domain ('py:class', 'docutils.nodes.Element'), ('py:class', 'docutils.nodes.Node'), @@ -210,6 +215,7 @@ nitpick_ignore = { ('py:class', 'docutils.parsers.rst.states.Inliner'), ('py:class', 'docutils.transforms.Transform'), ('py:class', 'nodes.NodeVisitor'), + ('py:class', 'nodes.TextElement'), # sphinx.application.Sphinx.connect ('py:class', 'nodes.document'), ('py:class', 'nodes.reference'), ('py:class', 'pygments.lexer.Lexer'), diff --git a/doc/extdev/event_callbacks.rst b/doc/extdev/event_callbacks.rst index 1edade48f..04eae51be 100644 --- a/doc/extdev/event_callbacks.rst +++ b/doc/extdev/event_callbacks.rst @@ -107,10 +107,10 @@ Here is a more detailed list of these events. :param app: :class:`.Sphinx` :param env: :class:`.BuildEnvironment` - :param added: ``set[str]`` - :param changed: ``set[str]`` - :param removed: ``set[str]`` - :returns: ``list[str]`` of additional docnames to re-read + :param added: ``Set[str]`` + :param changed: ``Set[str]`` + :param removed: ``Set[str]`` + :returns: ``Sequence[str]`` of additional docnames to re-read Emitted when the environment determines which source files have changed and should be re-read. diff --git a/sphinx/application.py b/sphinx/application.py index 142e19294..ea1f79ba7 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -10,15 +10,11 @@ import os import pickle import sys from collections import deque -from collections.abc import Callable, Collection, Sequence # NoQA: TCH003 from io import StringIO from os import path -from typing import IO, TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, overload -from docutils.nodes import TextElement # NoQA: TCH002 from docutils.parsers.rst import Directive, roles -from docutils.transforms import Transform # NoQA: TCH002 -from pygments.lexer import Lexer # NoQA: TCH002 import sphinx from sphinx import locale, package_dir @@ -41,16 +37,22 @@ from sphinx.util.osutil import ensuredir, relpath from sphinx.util.tags import Tags if TYPE_CHECKING: - from typing import Final + from collections.abc import Callable, Collection, Iterable, Sequence, Set + from pathlib import Path + from typing import IO, Any, Final, Literal from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers import Parser + from docutils.transforms import Transform + from pygments.lexer import Lexer + from sphinx import addnodes from sphinx.builders import Builder from sphinx.domains import Domain, Index from sphinx.environment.collectors import EnvironmentCollector from sphinx.ext.autodoc import Documenter + from sphinx.ext.todo import todo_node from sphinx.extension import Extension from sphinx.roles import XRefRole from sphinx.search import SearchLanguage @@ -456,6 +458,329 @@ class Sphinx: req = f'{major}.{minor}' raise VersionRequirementError(req) + # ---- Core events ------------------------------------------------------- + + @overload + def connect( + self, + event: Literal['config-inited'], + callback: Callable[[Sphinx, Config], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['builder-inited'], + callback: Callable[[Sphinx], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-get-outdated'], + callback: Callable[ + [Sphinx, BuildEnvironment, Set[str], Set[str], Set[str]], Sequence[str] + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-before-read-docs'], + callback: Callable[[Sphinx, BuildEnvironment, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-purge-doc'], + callback: Callable[[Sphinx, BuildEnvironment, str], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['source-read'], + callback: Callable[[Sphinx, str, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['include-read'], + callback: Callable[[Sphinx, Path, str, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['doctree-read'], + callback: Callable[[Sphinx, nodes.document], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-merge-info'], + callback: Callable[ + [Sphinx, BuildEnvironment, list[str], BuildEnvironment], None + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-updated'], + callback: Callable[[Sphinx, BuildEnvironment], str], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-get-updated'], + callback: Callable[[Sphinx, BuildEnvironment], Iterable[str]], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-check-consistency'], + callback: Callable[[Sphinx, BuildEnvironment], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['write-started'], + callback: Callable[[Sphinx, Builder], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['doctree-resolved'], + callback: Callable[[Sphinx, nodes.document, str], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['missing-reference'], + callback: Callable[ + [Sphinx, BuildEnvironment, addnodes.pending_xref, nodes.TextElement], + nodes.reference | None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['warn-missing-reference'], + callback: Callable[[Sphinx, Domain, addnodes.pending_xref], bool | None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['build-finished'], + callback: Callable[[Sphinx, Exception | None], None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from builtin builders -------------------------------------- + + @overload + def connect( + self, + event: Literal['html-collect-pages'], + callback: Callable[[Sphinx], Iterable[tuple[str, dict[str, Any], str]]], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['html-page-context'], + callback: Callable[ + [Sphinx, str, str, dict[str, Any], nodes.document], str | None + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['linkcheck-process-uri'], + callback: Callable[[Sphinx, str], str | None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from builtin extensions-- ---------------------------------- + + @overload + def connect( + self, + event: Literal['object-description-transform'], + callback: Callable[[Sphinx, str, str, addnodes.desc_content], None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from first-party extensions -------------------------------- + + @overload + def connect( + self, + event: Literal['autodoc-process-docstring'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + dict[str, bool], + Sequence[str], + ], + None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-before-process-signature'], + callback: Callable[[Sphinx, Any, bool], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-process-signature'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + dict[str, bool], + str | None, + str | None, + ], + tuple[str | None, str | None] | None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-process-bases'], + callback: Callable[[Sphinx, str, Any, dict[str, bool], list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-skip-member'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + bool, + dict[str, bool], + ], + bool, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['todo-defined'], + callback: Callable[[Sphinx, todo_node], None], + priority: int = 500, + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['viewcode-find-source'], + callback: Callable[ + [Sphinx, str], + tuple[str, dict[str, tuple[Literal['class', 'def', 'other'], int, int]]], + ], + priority: int = 500, + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['viewcode-follow-imported'], + callback: Callable[[Sphinx, str, str], str | None], + priority: int = 500, + ) -> int: + ... + + # ---- Catch-all --------------------------------------------------------- + + @overload + def connect( + self, + event: str, + callback: Callable[..., Any], + priority: int = 500 + ) -> int: + ... + # event interface def connect(self, event: str, callback: Callable, priority: int = 500) -> int: """Register *callback* to be called when *event* is emitted. @@ -851,7 +1176,7 @@ class Sphinx: def add_object_type(self, directivename: str, rolename: str, indextemplate: str = '', parse_node: Callable | None = None, - ref_nodeclass: type[TextElement] | None = None, + ref_nodeclass: type[nodes.TextElement] | None = None, objname: str = '', doc_field_types: Sequence = (), override: bool = False, ) -> None: @@ -918,9 +1243,11 @@ class Sphinx: ref_nodeclass, objname, doc_field_types, override=override) - def add_crossref_type(self, directivename: str, rolename: str, indextemplate: str = '', - ref_nodeclass: type[TextElement] | None = None, objname: str = '', - override: bool = False) -> None: + def add_crossref_type( + self, directivename: str, rolename: str, indextemplate: str = '', + ref_nodeclass: type[nodes.TextElement] | None = None, objname: str = '', + override: bool = False, + ) -> None: """Register a new crossref object type. This method is very similar to :meth:`~Sphinx.add_object_type` except that the diff --git a/sphinx/config.py b/sphinx/config.py index bd5941e2c..bae92140d 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -23,7 +23,7 @@ else: if TYPE_CHECKING: import os - from collections.abc import Collection, Iterator, Sequence, Set + from collections.abc import Collection, Iterable, Iterator, Sequence, Set from typing import TypeAlias from sphinx.application import Sphinx @@ -739,8 +739,8 @@ def check_primary_domain(app: Sphinx, config: Config) -> None: config.primary_domain = None -def check_root_doc(app: Sphinx, env: BuildEnvironment, added: set[str], - changed: set[str], removed: set[str]) -> set[str]: +def check_root_doc(app: Sphinx, env: BuildEnvironment, added: Set[str], + changed: Set[str], removed: Set[str]) -> Iterable[str]: """Adjust root_doc to 'contents' to support an old project which does not have any root_doc setting. """