From 91410e27ede8f8b701ef1a593e91371339de0b04 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:43:30 +0000 Subject: [PATCH 01/14] CurrentDocument --- sphinx/builders/__init__.py | 11 +- sphinx/directives/__init__.py | 17 +- sphinx/directives/code.py | 7 +- sphinx/directives/patches.py | 5 +- sphinx/domains/c/__init__.py | 48 ++--- sphinx/domains/changeset.py | 4 +- sphinx/domains/cpp/__init__.py | 67 +++--- sphinx/environment/__init__.py | 231 +++++++++++++++++++-- sphinx/ext/autodoc/__init__.py | 14 +- sphinx/ext/autodoc/typehints.py | 5 +- sphinx/ext/duration.py | 5 +- sphinx/ext/intersphinx/_resolve.py | 4 +- sphinx/testing/restructuredtext.py | 4 +- sphinx/util/docutils.py | 4 +- tests/test_environment/test_environment.py | 6 +- tests/test_extensions/autodoc_util.py | 3 +- tests/test_extensions/test_ext_autodoc.py | 14 +- tests/test_markup/test_markup.py | 2 +- tests/test_util/test_util_i18n.py | 4 +- 19 files changed, 327 insertions(+), 128 deletions(-) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 1420417380d..71734bfb513 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -13,7 +13,12 @@ from docutils import nodes from docutils.utils import DependencyList -from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment +from sphinx.environment import ( + CONFIG_CHANGED_REASON, + CONFIG_OK, + BuildEnvironment, + CurrentDocument, +) from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import SphinxError from sphinx.locale import __ @@ -615,7 +620,7 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None: filename = str(self.env.doc2path(docname)) filetype = get_filetype(self.app.config.source_suffix, filename) publisher = self.app.registry.get_publisher(self.app, filetype) - self.env.temp_data['_parser'] = publisher.parser + self.env.current_document._parser = publisher.parser # record_dependencies is mutable even though it is in settings, # explicitly re-initialise for each document publisher.settings.record_dependencies = DependencyList() @@ -635,7 +640,7 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None: self.env.all_docs[docname] = time.time_ns() // 1_000 # cleanup - self.env.temp_data.clear() + self.env.current_document = CurrentDocument() self.env.ref_context.clear() self.write_doctree(docname, doctree, _cache=_cache) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index c45232c92e7..603bdc72b86 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -284,9 +284,9 @@ def run(self) -> list[Node]: # needed for association of version{added,changed} directives object_name: ObjDescT = self.names[0] if isinstance(object_name, tuple): - self.env.temp_data['object'] = str(object_name[0]) + self.env.current_document.obj_desc_name = str(object_name[0]) else: - self.env.temp_data['object'] = str(object_name) + self.env.current_document.obj_desc_name = str(object_name) self.before_content() content_children = self.parse_content_to_nodes(allow_section_headings=True) content_node = addnodes.desc_content('', *content_children) @@ -296,7 +296,7 @@ def run(self) -> list[Node]: 'object-description-transform', self.domain, self.objtype, content_node ) DocFieldTransformer(self).transform_all(content_node) - self.env.temp_data['object'] = None + self.env.current_document.obj_desc_name = '' self.after_content() if node['no-typesetting']: @@ -335,7 +335,7 @@ def run(self) -> list[Node]: ) if role: docutils.register_role('', role) # type: ignore[arg-type] - self.env.temp_data['default_role'] = role_name + self.env.current_document.default_role = role_name else: literal_block = nodes.literal_block(self.block_text, self.block_text) reporter = self.state.reporter @@ -362,13 +362,8 @@ class DefaultDomain(SphinxDirective): def run(self) -> list[Node]: domain_name = self.arguments[0].lower() - # if domain_name not in env.domains: - # # try searching by label - # for domain in env.domains.sorted(): - # if domain.label.lower() == domain_name: - # domain_name = domain.name - # break - self.env.temp_data['default_domain'] = self.env.domains.get(domain_name) + default_domain = self.env.domains.get(domain_name) + self.env.current_document.default_domain = default_domain return [] diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index b68f6e1a729..00925debe7d 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -48,7 +48,7 @@ def run(self) -> list[Node]: linenothreshold = self.options.get('linenothreshold', sys.maxsize) force = 'force' in self.options - self.env.temp_data['highlight_language'] = language + self.env.current_document.highlight_language = language return [ addnodes.highlightlang( lang=language, force=force, linenothreshold=linenothreshold @@ -159,8 +159,9 @@ def run(self) -> list[Node]: # no highlight language specified. Then this directive refers the current # highlight setting via ``highlight`` directive or ``highlight_language`` # configuration. - literal['language'] = self.env.temp_data.get( - 'highlight_language', self.config.highlight_language + literal['language'] = ( + self.env.current_document.highlight_language + or self.config.highlight_language ) extra_args = literal['highlight_args'] = {} if hl_lines is not None: diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index ff2989520d8..9b60bc047a1 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -115,8 +115,9 @@ def run(self) -> list[Node]: # no highlight language specified. Then this directive refers the current # highlight setting via ``highlight`` directive or ``highlight_language`` # configuration. - node['language'] = self.env.temp_data.get( - 'highlight_language', self.config.highlight_language + node['language'] = ( + self.env.current_document.highlight_language + or self.config.highlight_language ) if 'number-lines' in self.options: diff --git a/sphinx/domains/c/__init__.py b/sphinx/domains/c/__init__.py index 28d77227eee..02996096bac 100644 --- a/sphinx/domains/c/__init__.py +++ b/sphinx/domains/c/__init__.py @@ -207,19 +207,19 @@ def describe_signature(self, signode: TextElement, ast: ASTDeclaration, def run(self) -> list[Node]: env = self.state.document.settings.env # from ObjectDescription.run - if 'c:parent_symbol' not in env.temp_data: + if 'c:parent_symbol' not in env.current_document: root = env.domaindata['c']['root_symbol'] - env.temp_data['c:parent_symbol'] = root + env.current_document['c:parent_symbol'] = root env.ref_context['c:parent_key'] = root.get_lookup_key() # When multiple declarations are made in the same directive # they need to know about each other to provide symbol lookup for function parameters. # We use last_symbol to store the latest added declaration in a directive. - env.temp_data['c:last_symbol'] = None + env.current_document['c:last_symbol'] = None return super().run() def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: - parentSymbol: Symbol = self.env.temp_data['c:parent_symbol'] + parentSymbol: Symbol = self.env.current_document['c:parent_symbol'] max_len = (self.env.config.c_maximum_signature_line_length or self.env.config.maximum_signature_line_length @@ -239,7 +239,7 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: # the possibly inner declarations. name = _make_phony_error_name() symbol = parentSymbol.add_name(name) - self.env.temp_data['c:last_symbol'] = symbol + self.env.current_document['c:last_symbol'] = symbol raise ValueError from e try: @@ -248,15 +248,15 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None - symbol.siblingAbove = self.env.temp_data['c:last_symbol'] + symbol.siblingAbove = self.env.current_document['c:last_symbol'] if symbol.siblingAbove is not None: assert symbol.siblingAbove.siblingBelow is None symbol.siblingAbove.siblingBelow = symbol - self.env.temp_data['c:last_symbol'] = symbol + self.env.current_document['c:last_symbol'] = symbol except _DuplicateSymbolError as e: # Assume we are actually in the old symbol, # instead of the newly created duplicate. - self.env.temp_data['c:last_symbol'] = e.symbol + self.env.current_document['c:last_symbol'] = e.symbol msg = __("Duplicate C declaration, also defined at %s:%s.\n" "Declaration is '.. c:%s:: %s'.") logger.warning( @@ -278,15 +278,15 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: return ast def before_content(self) -> None: - lastSymbol: Symbol = self.env.temp_data['c:last_symbol'] + lastSymbol: Symbol = self.env.current_document['c:last_symbol'] assert lastSymbol - self.oldParentSymbol = self.env.temp_data['c:parent_symbol'] + self.oldParentSymbol = self.env.current_document['c:parent_symbol'] self.oldParentKey: LookupKey = self.env.ref_context['c:parent_key'] - self.env.temp_data['c:parent_symbol'] = lastSymbol + self.env.current_document['c:parent_symbol'] = lastSymbol self.env.ref_context['c:parent_key'] = lastSymbol.get_lookup_key() def after_content(self) -> None: - self.env.temp_data['c:parent_symbol'] = self.oldParentSymbol + self.env.current_document['c:parent_symbol'] = self.oldParentSymbol self.env.ref_context['c:parent_key'] = self.oldParentKey @@ -375,8 +375,8 @@ def run(self) -> list[Node]: name = _make_phony_error_name() symbol = rootSymbol.add_name(name) stack = [symbol] - self.env.temp_data['c:parent_symbol'] = symbol - self.env.temp_data['c:namespace_stack'] = stack + self.env.current_document['c:parent_symbol'] = symbol + self.env.current_document['c:namespace_stack'] = stack self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -400,14 +400,14 @@ def run(self) -> list[Node]: except DefinitionError as e: logger.warning(e, location=self.get_location()) name = _make_phony_error_name() - oldParent = self.env.temp_data.get('c:parent_symbol', None) + oldParent = self.env.current_document.get('c:parent_symbol', None) if not oldParent: oldParent = self.env.domaindata['c']['root_symbol'] symbol = oldParent.add_name(name) - stack = self.env.temp_data.get('c:namespace_stack', []) + stack = self.env.current_document.get('c:namespace_stack', []) stack.append(symbol) - self.env.temp_data['c:parent_symbol'] = symbol - self.env.temp_data['c:namespace_stack'] = stack + self.env.current_document['c:parent_symbol'] = symbol + self.env.current_document['c:namespace_stack'] = stack self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -420,7 +420,7 @@ class CNamespacePopObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - stack = self.env.temp_data.get('c:namespace_stack', None) + stack = self.env.current_document.get('c:namespace_stack', None) if not stack or len(stack) == 0: logger.warning("C namespace pop on empty stack. Defaulting to global scope.", location=self.get_location()) @@ -431,8 +431,8 @@ def run(self) -> list[Node]: symbol = stack[-1] else: symbol = self.env.domaindata['c']['root_symbol'] - self.env.temp_data['c:parent_symbol'] = symbol - self.env.temp_data['c:namespace_stack'] = stack + self.env.current_document['c:parent_symbol'] = symbol + self.env.current_document['c:namespace_stack'] = stack self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -451,9 +451,9 @@ def __init__( self.aliasOptions = aliasOptions self.document = document if env is not None: - if 'c:parent_symbol' not in env.temp_data: + if 'c:parent_symbol' not in env.current_document: root = env.domaindata['c']['root_symbol'] - env.temp_data['c:parent_symbol'] = root + env.current_document['c:parent_symbol'] = root env.ref_context['c:parent_key'] = root.get_lookup_key() self.parentKey = env.ref_context['c:parent_key'] else: @@ -659,7 +659,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: location=self.get_location()) # see below return [addnodes.desc_inline('c', text, text, classes=[self.class_type])], [] - parentSymbol = self.env.temp_data.get('c:parent_symbol', None) + parentSymbol = self.env.current_document.get('c:parent_symbol', None) if parentSymbol is None: parentSymbol = self.env.domaindata['c']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, diff --git a/sphinx/domains/changeset.py b/sphinx/domains/changeset.py index a27c5f571a8..25f8f8e5b16 100644 --- a/sphinx/domains/changeset.py +++ b/sphinx/domains/changeset.py @@ -41,7 +41,7 @@ class ChangeSet(NamedTuple): docname: str lineno: int module: str | None - descname: str | None + descname: str content: str @@ -123,7 +123,7 @@ def changesets(self) -> dict[str, list[ChangeSet]]: def note_changeset(self, node: addnodes.versionmodified) -> None: version = node['version'] module = self.env.ref_context.get('py:module') - objname = self.env.temp_data.get('object') + objname = self.env.current_document.obj_desc_name changeset = ChangeSet(node['type'], self.env.docname, node.line, # type: ignore[arg-type] module, objname, node.astext()) self.changesets.setdefault(version, []).append(changeset) diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index 743aa0d1018..2c737814e6c 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -285,9 +285,9 @@ def describe_signature(self, signode: desc_signature, def run(self) -> list[Node]: env = self.state.document.settings.env # from ObjectDescription.run - if 'cpp:parent_symbol' not in env.temp_data: + if 'cpp:parent_symbol' not in env.current_document: root = env.domaindata['cpp']['root_symbol'] - env.temp_data['cpp:parent_symbol'] = root + env.current_document['cpp:parent_symbol'] = root env.ref_context['cpp:parent_key'] = root.get_lookup_key() # The lookup keys assume that no nested scopes exists inside overloaded functions. @@ -301,7 +301,7 @@ def run(self) -> list[Node]: # :cpp:any:`boom` # # So we disallow any signatures inside functions. - parentSymbol = env.temp_data['cpp:parent_symbol'] + parentSymbol = env.current_document['cpp:parent_symbol'] parentDecl = parentSymbol.declaration if parentDecl is not None and parentDecl.objectType == 'function': msg = ("C++ declarations inside functions are not supported. " @@ -310,16 +310,16 @@ def run(self) -> list[Node]: logger.warning(msg, location=self.get_location()) name = _make_phony_error_name() symbol = parentSymbol.add_name(name) - env.temp_data['cpp:last_symbol'] = symbol + env.current_document['cpp:last_symbol'] = symbol return [] # When multiple declarations are made in the same directive # they need to know about each other to provide symbol lookup for function parameters. # We use last_symbol to store the latest added declaration in a directive. - env.temp_data['cpp:last_symbol'] = None + env.current_document['cpp:last_symbol'] = None return super().run() def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: - parentSymbol: Symbol = self.env.temp_data['cpp:parent_symbol'] + parentSymbol: Symbol = self.env.current_document['cpp:parent_symbol'] max_len = (self.env.config.cpp_maximum_signature_line_length or self.env.config.maximum_signature_line_length @@ -339,7 +339,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: # the possibly inner declarations. name = _make_phony_error_name() symbol = parentSymbol.add_name(name) - self.env.temp_data['cpp:last_symbol'] = symbol + self.env.current_document['cpp:last_symbol'] = symbol raise ValueError from e try: @@ -348,15 +348,15 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None - symbol.siblingAbove = self.env.temp_data['cpp:last_symbol'] + symbol.siblingAbove = self.env.current_document['cpp:last_symbol'] if symbol.siblingAbove is not None: assert symbol.siblingAbove.siblingBelow is None symbol.siblingAbove.siblingBelow = symbol - self.env.temp_data['cpp:last_symbol'] = symbol + self.env.current_document['cpp:last_symbol'] = symbol except _DuplicateSymbolError as e: # Assume we are actually in the old symbol, # instead of the newly created duplicate. - self.env.temp_data['cpp:last_symbol'] = e.symbol + self.env.current_document['cpp:last_symbol'] = e.symbol msg = __("Duplicate C++ declaration, also defined at %s:%s.\n" "Declaration is '.. cpp:%s:: %s'.") logger.warning( @@ -379,25 +379,26 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: return ast def before_content(self) -> None: - lastSymbol: Symbol = self.env.temp_data['cpp:last_symbol'] + lastSymbol: Symbol = self.env.current_document['cpp:last_symbol'] assert lastSymbol - self.oldParentSymbol = self.env.temp_data['cpp:parent_symbol'] + self.oldParentSymbol = self.env.current_document['cpp:parent_symbol'] self.oldParentKey: LookupKey = self.env.ref_context['cpp:parent_key'] - self.env.temp_data['cpp:parent_symbol'] = lastSymbol + self.env.current_document['cpp:parent_symbol'] = lastSymbol self.env.ref_context['cpp:parent_key'] = lastSymbol.get_lookup_key() - self.env.temp_data['cpp:domain_name'] = ( - *self.env.temp_data.get('cpp:domain_name', ()), + self.env.current_document['cpp:domain_name'] = ( + *self.env.current_document.get('cpp:domain_name', ()), lastSymbol.identOrOp._stringify(str), ) def after_content(self) -> None: - self.env.temp_data['cpp:parent_symbol'] = self.oldParentSymbol + self.env.current_document['cpp:parent_symbol'] = self.oldParentSymbol self.env.ref_context['cpp:parent_key'] = self.oldParentKey - self.env.temp_data['cpp:domain_name'] = self.env.temp_data['cpp:domain_name'][:-1] + old_cpp_domain_name = self.env.current_document['cpp:domain_name'][:-1] + self.env.current_document['cpp:domain_name'] = old_cpp_domain_name def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: return tuple(s.identOrOp._stringify(str) for s in - self.env.temp_data['cpp:last_symbol'].get_full_nested_name().names) + self.env.current_document['cpp:last_symbol'].get_full_nested_name().names) def _toc_entry_name(self, sig_node: desc_signature) -> str: if not sig_node.get('_toc_parts'): @@ -411,7 +412,9 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: parens = '' *parents, name = sig_node['_toc_parts'] if config.toc_object_entries_show_parents == 'domain': - return '::'.join((*self.env.temp_data.get('cpp:domain_name', ()), name + parens)) + return '::'.join( + (*self.env.current_document.get('cpp:domain_name', ()), name + parens) + ) if config.toc_object_entries_show_parents == 'hide': return name + parens if config.toc_object_entries_show_parents == 'all': @@ -511,8 +514,8 @@ def run(self) -> list[Node]: ast = ASTNamespace(name, None) symbol = rootSymbol.add_name(ast.nestedName, ast.templatePrefix) stack = [symbol] - self.env.temp_data['cpp:parent_symbol'] = symbol - self.env.temp_data['cpp:namespace_stack'] = stack + self.env.current_document['cpp:parent_symbol'] = symbol + self.env.current_document['cpp:namespace_stack'] = stack self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -537,14 +540,14 @@ def run(self) -> list[Node]: logger.warning(e, location=self.get_location()) name = _make_phony_error_name() ast = ASTNamespace(name, None) - oldParent = self.env.temp_data.get('cpp:parent_symbol', None) + oldParent = self.env.current_document.get('cpp:parent_symbol', None) if not oldParent: oldParent = self.env.domaindata['cpp']['root_symbol'] symbol = oldParent.add_name(ast.nestedName, ast.templatePrefix) - stack = self.env.temp_data.get('cpp:namespace_stack', []) + stack = self.env.current_document.get('cpp:namespace_stack', []) stack.append(symbol) - self.env.temp_data['cpp:parent_symbol'] = symbol - self.env.temp_data['cpp:namespace_stack'] = stack + self.env.current_document['cpp:parent_symbol'] = symbol + self.env.current_document['cpp:namespace_stack'] = stack self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -557,7 +560,7 @@ class CPPNamespacePopObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - stack = self.env.temp_data.get('cpp:namespace_stack', None) + stack = self.env.current_document.get('cpp:namespace_stack', None) if not stack or len(stack) == 0: logger.warning("C++ namespace pop on empty stack. Defaulting to global scope.", location=self.get_location()) @@ -568,8 +571,8 @@ def run(self) -> list[Node]: symbol = stack[-1] else: symbol = self.env.domaindata['cpp']['root_symbol'] - self.env.temp_data['cpp:parent_symbol'] = symbol - self.env.temp_data['cpp:namespace_stack'] = stack + self.env.current_document['cpp:parent_symbol'] = symbol + self.env.current_document['cpp:namespace_stack'] = stack self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -582,9 +585,9 @@ def __init__(self, sig: str, aliasOptions: dict, self.sig = sig self.aliasOptions = aliasOptions if env is not None: - if 'cpp:parent_symbol' not in env.temp_data: + if 'cpp:parent_symbol' not in env.current_document: root = env.domaindata['cpp']['root_symbol'] - env.temp_data['cpp:parent_symbol'] = root + env.current_document['cpp:parent_symbol'] = root env.ref_context['cpp:parent_key'] = root.get_lookup_key() self.parentKey = env.ref_context['cpp:parent_key'] else: @@ -771,7 +774,7 @@ def run(self) -> list[Node]: self.before_content() content_node = addnodes.desc_content('', *self.parse_content_to_nodes()) node.append(content_node) - self.env.temp_data['object'] = None + self.env.current_document.obj_desc_name = '' self.after_content() return [node] @@ -828,7 +831,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: location=self.get_location()) # see below return [addnodes.desc_inline('cpp', text, text, classes=[self.class_type])], [] - parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) + parentSymbol = self.env.current_document.get('cpp:parent_symbol', None) if parentSymbol is None: parentSymbol = self.env.domaindata['cpp']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index b17c1ee453b..b0a8b2e7eb5 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -6,7 +6,7 @@ import os import pickle from collections import defaultdict -from copy import copy +from copy import deepcopy from typing import TYPE_CHECKING from sphinx import addnodes @@ -192,10 +192,10 @@ def __init__(self, app: Sphinx) -> None: self.original_image_uri: dict[_StrPath, str] = {} # temporary data storage while reading a document - self.temp_data: dict[str, Any] = {} + self.current_document: CurrentDocument = CurrentDocument() # context for cross-references (e.g. current module or class) - # this is similar to temp_data, but will for example be copied to - # attributes of "any" cross references + # this is similar to ``self.current_document``, + # but will for example be copied to attributes of "any" cross references self.ref_context: dict[str, Any] = {} # search index data @@ -422,7 +422,13 @@ def relfn2path(self, filename: str, docname: str | None = None) -> tuple[str, st if filename.startswith('/'): abs_fn = (self.srcdir / filename[1:]).resolve() else: - doc_dir = self.doc2path(docname or self.docname, base=False).parent + if not docname: + if self.docname: + docname = self.docname + else: + msg = 'docname' + raise KeyError(msg) + doc_dir = self.doc2path(docname, base=False).parent abs_fn = (self.srcdir / doc_dir / filename).resolve() rel_fn = _relative_path(abs_fn, self.srcdir) @@ -554,32 +560,42 @@ def check_dependents(self, app: Sphinx, already: set[str]) -> Iterator[str]: def prepare_settings(self, docname: str) -> None: """Prepare to set up environment for reading.""" - self.temp_data['docname'] = docname - # defaults to the global default, but can be re-set in a document - self.temp_data['default_role'] = self.config.default_role - self.temp_data['default_domain'] = self.domains.get(self.config.primary_domain) + self.current_document = CurrentDocument( + docname=docname, + # defaults to the global default, but can be re-set in a document + default_role=self.config.default_role, + default_domain=self.domains.get(self.config.primary_domain), + ) # utilities to use while reading a document + @property + def temp_data(self) -> CurrentDocument: + """Returns the temporary data storage for the current document. + + Kept for backwards compatibility. + """ + return self.current_document + @property def docname(self) -> str: """Returns the docname of the document currently being parsed.""" - return self.temp_data['docname'] + return self.current_document.docname @property def parser(self) -> Parser: """Returns the parser being used for to parse the current document.""" - return self.temp_data['_parser'] + if (parser := self.current_document._parser) is not None: + return parser + msg = 'parser' + raise KeyError(msg) def new_serialno(self, category: str = '') -> int: """Return a serial number, e.g. for index entry targets. The number is guaranteed to be unique in the current document. """ - key = category + 'serialno' - cur = self.temp_data.get(key, 0) - self.temp_data[key] = cur + 1 - return cur + return self.current_document.new_serial_number(category) def note_dependency( self, filename: str | os.PathLike[str], *, docname: str | None = None @@ -722,17 +738,19 @@ def resolve_references( def apply_post_transforms(self, doctree: nodes.document, docname: str) -> None: """Apply all post-transforms.""" + backup = self.current_document + new = deepcopy(backup) + new.docname = docname try: # set env.docname during applying post-transforms - backup = copy(self.temp_data) - self.temp_data['docname'] = docname + self.current_document = new transformer = SphinxTransformer(doctree) transformer.set_environment(self) transformer.add_transforms(self.app.registry.get_post_transforms()) transformer.apply_transforms() finally: - self.temp_data = backup + self.current_document = backup # allow custom references to be resolved self.events.emit('doctree-resolved', doctree, docname) @@ -842,3 +860,180 @@ def _check_toc_parents(toctree_includes: dict[str, list[str]]) -> None: type='toc', subtype='multiple_toc_parents', ) + + +class CurrentDocument: + """Temporary data storage while reading a document.""" + + __slots__ = ( + '_parser', + '_serial_numbers', + '_ext_props', + 'autodoc_annotations', + 'default_domain', + 'default_role', + 'docname', + 'highlight_language', + 'obj_desc_name', + 'reading_started_at', + ) + + def __init__( + self, + *, + docname: str = '', + default_role: str = '', + default_domain: Domain | None = None, + ) -> None: + #: The docname of the document currently being parsed. + self.docname = docname + + #: The default role for the current document. + #: Set by the ``.. default-role::`` directive. + self.default_role: str = default_role + + #: The default domain for the current document. + #: Set by the ``.. default-domain::`` directive. + self.default_domain: Domain | None = default_domain + + #: The parser being used to parse the current document. + self._parser: Parser | None = None + + #: The default language for syntax highlighting. + #: Set by the ``.. highlight::`` directive to override + #: the ``highlight_language`` config value. + self.highlight_language: str = '' + + #: The current object's name. + #: Used in the Changes builder. + self.obj_desc_name: str = '' + + #: Records type hints of Python objects in the current document. + #: Used in ``sphinx.ext.autodoc.typehints``. + #: Maps object names to maps of attribute names -> type hints. + self.autodoc_annotations: dict[str, dict[str, str]] = {} + + #: Records the time when reading begain for the current document. + #: Used in ``sphinx.ext.duration``. + self.reading_started_at: float = 0.0 + + # Used for generating unique serial numbers. + self._serial_numbers: dict[str, int] = {} + + # Stores properties relating to the current document set by extensions. + self._ext_props: dict[str, Any] = {} + + def new_serial_number(self, category: str = '', /) -> int: + """Return a serial number, e.g. for index entry targets. + + The number is guaranteed to be unique in the current document. + """ + current = self._serial_numbers.get(category, 0) + self._serial_numbers[category] = current + 1 + return current + + # Mapping interface: + + def __getitem__(self, item: str) -> Any: + if item == 'annotations': + return self.autodoc_annotations + if item == 'object': + return self.obj_desc_name + if item == 'started_at': + return self.reading_started_at + if item in { + '_parser', + 'autodoc_annotations', + 'default_domain', + 'default_role', + 'docname', + 'highlight_language', + 'obj_desc_name', + 'reading_started_at', + }: + return getattr(self, item) + return self._ext_props[item] + + def __setitem__(self, key: str, value: Any) -> None: + if key == 'annotations': + self.autodoc_annotations = value + elif key == 'object': + self.obj_desc_name = value + elif key == 'started_at': + self.reading_started_at = value + elif key in { + '_parser', + 'autodoc_annotations', + 'default_domain', + 'default_role', + 'docname', + 'highlight_language', + 'obj_desc_name', + 'reading_started_at', + }: + setattr(self, key, value) + else: + self._ext_props[key] = value + + def __delitem__(self, key: str) -> None: + del self._ext_props[key] + + def __contains__(self, item: str) -> bool: + if item in { + 'annotations', + 'object', + 'started_at', + }: + return True + if item in { + '_parser', + '_serial_numbers', + '_ext_props', + 'autodoc_annotations', + 'default_domain', + 'default_role', + 'docname', + 'highlight_language', + 'obj_desc_name', + 'reading_started_at', + }: + return True + return item in self._ext_props + + def get(self, key: str, default: Any | None = None) -> Any | None: + try: + return self[key] + except KeyError: + return default + + def pop(self, key: str, default: Any | None = None) -> Any | None: + if key == 'annotations': + key = 'autodoc_annotations' + elif key == 'object': + key = 'obj_desc_name' + elif key == 'started_at': + key = 'reading_started_at' + try: + blank: str | float | dict[str, dict[str, str]] | None = { + '_parser': None, + 'autodoc_annotations': {}, + 'default_domain': None, + 'default_role': '', + 'docname': '', + 'highlight_language': '', + 'obj_desc_name': '', + 'reading_started_at': 0.0, + }[key] + except KeyError: + pass + else: + value = getattr(self, key) + setattr(self, key, blank) + return value + return self._ext_props.pop(key, default) + + def setdefault(self, key: str, default: Any | None = None) -> Any | None: + return self._ext_props.setdefault(key, default) + + def clear(self) -> None: + CurrentDocument.__init__(self) # NoQA: PLC2801 diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 60c31e2542e..a8b96fe6cd2 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -821,9 +821,9 @@ def document_members(self, all_members: bool = False) -> None: *self.options.members*. """ # set current namespace for finding members - self.env.temp_data['autodoc:module'] = self.modname + self.env.current_document['autodoc:module'] = self.modname if self.objpath: - self.env.temp_data['autodoc:class'] = self.objpath[0] + self.env.current_document['autodoc:class'] = self.objpath[0] want_all = (all_members or self.options.inherited_members or @@ -856,8 +856,8 @@ def document_members(self, all_members: bool = False) -> None: check_module=members_check_module and not isattr) # reset current objects - self.env.temp_data['autodoc:module'] = None - self.env.temp_data['autodoc:class'] = None + self.env.current_document['autodoc:module'] = None + self.env.current_document['autodoc:class'] = None def sort_members(self, documenters: list[tuple[Documenter, bool]], order: str) -> list[tuple[Documenter, bool]]: @@ -1159,7 +1159,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a toplevel object without explicit module, # it can be contained in another auto directive ... - modname = self.env.temp_data.get('autodoc:module') + modname = self.env.current_document.get('autodoc:module') # ... or in the scope of a module directive if not modname: modname = self.env.ref_context.get('py:module') @@ -1184,7 +1184,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a class-level object without path, # there must be a current class, either from a parent # auto directive ... - mod_cls_ = self.env.temp_data.get('autodoc:class') + mod_cls_ = self.env.current_document.get('autodoc:class') # ... or from a class directive if mod_cls_ is None: mod_cls_ = self.env.ref_context.get('py:class') @@ -1196,7 +1196,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, parents = [cls] # if the module name is still missing, get it like above if not modname: - modname = self.env.temp_data.get('autodoc:module') + modname = self.env.current_document.get('autodoc:module') if not modname: modname = self.env.ref_context.get('py:module') # ... else, it stays None, which means invalid diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index a2b05aac705..faf08d2971a 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -30,8 +30,7 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, try: if callable(obj): - annotations = app.env.temp_data.setdefault('annotations', {}) - annotation = annotations.setdefault(name, {}) + annotation = app.env.current_document.autodoc_annotations.setdefault(name, {}) sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases) for param in sig.parameters.values(): if param.annotation is not param.empty: @@ -58,7 +57,7 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element # signature node does not have valid context info for the target object return - annotations = app.env.temp_data.get('annotations', {}) + annotations = app.env.current_document.autodoc_annotations if annotations.get(fullname, {}): field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)] if field_lists == []: diff --git a/sphinx/ext/duration.py b/sphinx/ext/duration.py index 6739f676ff9..cc21cb6afef 100644 --- a/sphinx/ext/duration.py +++ b/sphinx/ext/duration.py @@ -63,13 +63,12 @@ def on_builder_inited(app: Sphinx) -> None: def on_source_read(app: Sphinx, docname: str, content: list[str]) -> None: """Start to measure reading duration.""" - app.env.temp_data['started_at'] = time.monotonic() + app.env.current_document.reading_started_at = time.monotonic() def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None: """Record a reading duration.""" - started_at = app.env.temp_data['started_at'] - duration = time.monotonic() - started_at + duration = time.monotonic() - app.env.current_document.reading_started_at domain = app.env.domains['duration'] domain.note_reading_duration(duration) diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py index 0dbab63dc69..474de8428e5 100644 --- a/sphinx/ext/intersphinx/_resolve.py +++ b/sphinx/ext/intersphinx/_resolve.py @@ -398,7 +398,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: # the user did not specify a domain, # so we check first the default (if available) then standard domains domains: list[Domain] = [] - if default_domain := self.env.temp_data.get('default_domain'): + if default_domain := self.env.current_document.default_domain: domains.append(default_domain) if ( std_domain := self.env.domains.standard_domain @@ -505,7 +505,7 @@ def get_role_name(self, name: str) -> tuple[str, str] | None: names = name.split(':') if len(names) == 1: # role - default_domain = self.env.temp_data.get('default_domain') + default_domain = self.env.current_document.default_domain domain = default_domain.name if default_domain else None role = names[0] elif len(names) == 2: diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py index 620e8483492..a4e89719d0f 100644 --- a/sphinx/testing/restructuredtext.py +++ b/sphinx/testing/restructuredtext.py @@ -10,7 +10,7 @@ def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document: """Parse a string as reStructuredText with Sphinx application.""" try: - app.env.temp_data['docname'] = docname + app.env.current_document.docname = docname reader = SphinxStandaloneReader() reader.setup(app) parser = RSTParser() @@ -30,4 +30,4 @@ def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document: }, ) finally: - app.env.temp_data.pop('docname', None) + app.env.current_document.docname = '' diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 87823bd69fd..6448828b6b6 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -285,7 +285,7 @@ def lookup_domain_element(self, type: str, name: str) -> Any: ) # else look in the default domain else: - def_domain = self.env.temp_data.get('default_domain') + def_domain = self.env.current_document.default_domain if def_domain is not None: element = getattr(def_domain, type)(name) if element is not None: @@ -587,7 +587,7 @@ def __call__( if name: self.name = name.lower() else: - self.name = self.env.temp_data.get('default_role', '') + self.name = self.env.current_document.default_role if not self.name: self.name = self.env.config.default_role if not self.name: diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py index 10e98584342..d762fa55670 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -183,14 +183,14 @@ def test_env_relfn2path(app): assert absfn == str(app.srcdir / 'logo.jpg') # omit docname (w/ current docname) - app.env.temp_data['docname'] = 'subdir/document' + app.env.current_document.docname = 'subdir/document' relfn, absfn = app.env.relfn2path('images/logo.jpg') assert Path(relfn) == Path('subdir/images/logo.jpg') assert absfn == str(app.srcdir / 'subdir' / 'images' / 'logo.jpg') # omit docname (w/o current docname) - app.env.temp_data.clear() - with pytest.raises(KeyError): + app.env.current_document.clear() + with pytest.raises(KeyError, match=r"^'docname'$"): app.env.relfn2path('images/logo.jpg') diff --git a/tests/test_extensions/autodoc_util.py b/tests/test_extensions/autodoc_util.py index 7c4da07970e..3d08c739300 100644 --- a/tests/test_extensions/autodoc_util.py +++ b/tests/test_extensions/autodoc_util.py @@ -22,7 +22,8 @@ def do_autodoc( options: dict[str, Any] | None = None, ) -> StringList: options = {} if options is None else options.copy() - app.env.temp_data.setdefault('docname', 'index') # set dummy docname + if not app.env.current_document.docname: + app.env.current_document.docname = 'index' # set dummy docname doccls = app.registry.documenters[objtype] docoptions = process_documenter_options(doccls, app.config, options) state = Mock() diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index eb775aba081..02a521fdcdf 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -95,9 +95,9 @@ def verify(objtype, name, result): 'test_ext_autodoc.raises(exc) -> None', ('test_ext_autodoc', ['raises'], 'exc', 'None'), ) - directive.env.temp_data['autodoc:module'] = 'test_ext_autodoc' + directive.env.current_document['autodoc:module'] = 'test_ext_autodoc' verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) - del directive.env.temp_data['autodoc:module'] + del directive.env.current_document['autodoc:module'] directive.env.ref_context['py:module'] = 'test_ext_autodoc' verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None)) @@ -111,7 +111,7 @@ def verify(objtype, name, result): ) directive.env.ref_context['py:module'] = 'sphinx.testing.util' directive.env.ref_context['py:class'] = 'Foo' - directive.env.temp_data['autodoc:class'] = 'SphinxTestApp' + directive.env.current_document['autodoc:class'] = 'SphinxTestApp' verify( 'method', 'cleanup', @@ -526,7 +526,7 @@ def test_autodoc_exception(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_warnings(app): - app.env.temp_data['docname'] = 'dummy' + app.env.current_document.docname = 'dummy' # can't import module do_autodoc(app, 'module', 'unknown') @@ -1299,7 +1299,7 @@ def test_autodoc_module_member_order(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_module_scope(app): - app.env.temp_data['autodoc:module'] = 'target' + app.env.current_document['autodoc:module'] = 'target' actual = do_autodoc(app, 'attribute', 'Class.mdocattr') assert list(actual) == [ '', @@ -1314,8 +1314,8 @@ def test_autodoc_module_scope(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_class_scope(app): - app.env.temp_data['autodoc:module'] = 'target' - app.env.temp_data['autodoc:class'] = 'Class' + app.env.current_document['autodoc:module'] = 'target' + app.env.current_document['autodoc:class'] = 'Class' actual = do_autodoc(app, 'attribute', 'mdocattr') assert list(actual) == [ '', diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py index 9eb0b83fa89..2af8227a2e2 100644 --- a/tests/test_markup/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -36,7 +36,7 @@ def settings(app): settings = optparser.get_default_values() settings.smart_quotes = True settings.env = app.builder.env - settings.env.temp_data['docname'] = 'dummy' + settings.env.current_document.docname = 'dummy' settings.contentsname = 'dummy' domain_context = sphinx_domains(settings.env) domain_context.enable() diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 95d0909f90a..9018936cc5c 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -117,7 +117,7 @@ def test_format_date_timezone(): @pytest.mark.sphinx('html', testroot='root') def test_get_filename_for_language(app): get_filename = i18n.get_image_filename_for_language - app.env.temp_data['docname'] = 'index' + app.env.current_document.docname = 'index' # language is en app.env.config.language = 'en' @@ -156,7 +156,7 @@ def test_get_filename_for_language(app): assert get_filename('foo.png', app.env) == '/en/foo.png' # docpath (for a document in the sub directory) - app.env.temp_data['docname'] = 'subdir/index' + app.env.current_document.docname = 'subdir/index' assert get_filename('foo.png', app.env) == '/subdir/en/foo.png' From a98f18d0c94b2929dce3005ac857404a79818cce Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:18:25 +0000 Subject: [PATCH 02/14] privatise --- sphinx/builders/__init__.py | 4 ++-- sphinx/environment/__init__.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 99fb68b05d3..e493d11cf59 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -17,7 +17,7 @@ CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironment, - CurrentDocument, + _CurrentDocument, ) from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import SphinxError @@ -640,7 +640,7 @@ def read_doc(self, docname: str, *, _cache: bool = True) -> None: self.env.all_docs[docname] = time.time_ns() // 1_000 # cleanup - self.env.current_document = CurrentDocument() + self.env.current_document = _CurrentDocument() self.env.ref_context.clear() self.write_doctree(docname, doctree, _cache=_cache) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index b0a8b2e7eb5..23318ca6fbd 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -192,7 +192,7 @@ def __init__(self, app: Sphinx) -> None: self.original_image_uri: dict[_StrPath, str] = {} # temporary data storage while reading a document - self.current_document: CurrentDocument = CurrentDocument() + self.current_document: _CurrentDocument = _CurrentDocument() # context for cross-references (e.g. current module or class) # this is similar to ``self.current_document``, # but will for example be copied to attributes of "any" cross references @@ -560,7 +560,7 @@ def check_dependents(self, app: Sphinx, already: set[str]) -> Iterator[str]: def prepare_settings(self, docname: str) -> None: """Prepare to set up environment for reading.""" - self.current_document = CurrentDocument( + self.current_document = _CurrentDocument( docname=docname, # defaults to the global default, but can be re-set in a document default_role=self.config.default_role, @@ -570,7 +570,7 @@ def prepare_settings(self, docname: str) -> None: # utilities to use while reading a document @property - def temp_data(self) -> CurrentDocument: + def temp_data(self) -> _CurrentDocument: """Returns the temporary data storage for the current document. Kept for backwards compatibility. @@ -862,7 +862,7 @@ def _check_toc_parents(toctree_includes: dict[str, list[str]]) -> None: ) -class CurrentDocument: +class _CurrentDocument: """Temporary data storage while reading a document.""" __slots__ = ( @@ -1036,4 +1036,4 @@ def setdefault(self, key: str, default: Any | None = None) -> Any | None: return self._ext_props.setdefault(key, default) def clear(self) -> None: - CurrentDocument.__init__(self) # NoQA: PLC2801 + _CurrentDocument.__init__(self) # NoQA: PLC2801 From f64b3f18ffddce0ac862328c8bef1372ce26b7c6 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:32:18 +0000 Subject: [PATCH 03/14] Add autodoc:{class,module} --- sphinx/environment/__init__.py | 23 +++++++++++++++++++++++ sphinx/ext/autodoc/__init__.py | 14 +++++++------- tests/test_extensions/test_ext_autodoc.py | 13 +++++++------ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 23318ca6fbd..a7c0bb425dd 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -870,6 +870,8 @@ class _CurrentDocument: '_serial_numbers', '_ext_props', 'autodoc_annotations', + 'autodoc_class', + 'autodoc_module', 'default_domain', 'default_role', 'docname', @@ -913,6 +915,9 @@ def __init__( #: Maps object names to maps of attribute names -> type hints. self.autodoc_annotations: dict[str, dict[str, str]] = {} + self.autodoc_class: str = '' + self.autodoc_module: str = '' + #: Records the time when reading begain for the current document. #: Used in ``sphinx.ext.duration``. self.reading_started_at: float = 0.0 @@ -937,6 +942,10 @@ def new_serial_number(self, category: str = '', /) -> int: def __getitem__(self, item: str) -> Any: if item == 'annotations': return self.autodoc_annotations + if item == 'autodoc:class': + return self.autodoc_class + if item == 'autodoc:module': + return self.autodoc_module if item == 'object': return self.obj_desc_name if item == 'started_at': @@ -957,6 +966,10 @@ def __getitem__(self, item: str) -> Any: def __setitem__(self, key: str, value: Any) -> None: if key == 'annotations': self.autodoc_annotations = value + elif key == 'autodoc:class': + self.autodoc_class = value + elif key == 'autodoc:module': + self.autodoc_module = value elif key == 'object': self.obj_desc_name = value elif key == 'started_at': @@ -981,6 +994,8 @@ def __delitem__(self, key: str) -> None: def __contains__(self, item: str) -> bool: if item in { 'annotations', + 'autodoc:class', + 'autodoc:module', 'object', 'started_at', }: @@ -990,6 +1005,8 @@ def __contains__(self, item: str) -> bool: '_serial_numbers', '_ext_props', 'autodoc_annotations', + 'autodoc_class', + 'autodoc_module', 'default_domain', 'default_role', 'docname', @@ -1009,6 +1026,10 @@ def get(self, key: str, default: Any | None = None) -> Any | None: def pop(self, key: str, default: Any | None = None) -> Any | None: if key == 'annotations': key = 'autodoc_annotations' + if key == 'autodoc:class': + key = 'autodoc_class' + if key == 'autodoc:module': + key = 'autodoc_module' elif key == 'object': key = 'obj_desc_name' elif key == 'started_at': @@ -1017,6 +1038,8 @@ def pop(self, key: str, default: Any | None = None) -> Any | None: blank: str | float | dict[str, dict[str, str]] | None = { '_parser': None, 'autodoc_annotations': {}, + 'autodoc_class': '', + 'autodoc_module': '', 'default_domain': None, 'default_role': '', 'docname': '', diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index a8b96fe6cd2..2b09f28d792 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -821,9 +821,9 @@ def document_members(self, all_members: bool = False) -> None: *self.options.members*. """ # set current namespace for finding members - self.env.current_document['autodoc:module'] = self.modname + self.env.current_document.autodoc_class = self.modname if self.objpath: - self.env.current_document['autodoc:class'] = self.objpath[0] + self.env.current_document.autodoc_class = self.objpath[0] want_all = (all_members or self.options.inherited_members or @@ -856,8 +856,8 @@ def document_members(self, all_members: bool = False) -> None: check_module=members_check_module and not isattr) # reset current objects - self.env.current_document['autodoc:module'] = None - self.env.current_document['autodoc:class'] = None + self.env.current_document.autodoc_module = '' + self.env.current_document.autodoc_class = '' def sort_members(self, documenters: list[tuple[Documenter, bool]], order: str) -> list[tuple[Documenter, bool]]: @@ -1159,7 +1159,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a toplevel object without explicit module, # it can be contained in another auto directive ... - modname = self.env.current_document.get('autodoc:module') + modname = self.env.current_document.autodoc_module # ... or in the scope of a module directive if not modname: modname = self.env.ref_context.get('py:module') @@ -1184,7 +1184,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a class-level object without path, # there must be a current class, either from a parent # auto directive ... - mod_cls_ = self.env.current_document.get('autodoc:class') + mod_cls_ = self.env.current_document.autodoc_class # ... or from a class directive if mod_cls_ is None: mod_cls_ = self.env.ref_context.get('py:class') @@ -1196,7 +1196,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, parents = [cls] # if the module name is still missing, get it like above if not modname: - modname = self.env.current_document.get('autodoc:module') + modname = self.env.current_document.autodoc_module if not modname: modname = self.env.ref_context.get('py:module') # ... else, it stays None, which means invalid diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 02a521fdcdf..89d8d037269 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -95,9 +95,10 @@ def verify(objtype, name, result): 'test_ext_autodoc.raises(exc) -> None', ('test_ext_autodoc', ['raises'], 'exc', 'None'), ) - directive.env.current_document['autodoc:module'] = 'test_ext_autodoc' + directive.env.current_document.autodoc_module = 'test_ext_autodoc' verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) - del directive.env.current_document['autodoc:module'] + directive.env.current_document.autodoc_module = '' + directive.env.ref_context['py:module'] = 'test_ext_autodoc' verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None)) verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None)) @@ -111,7 +112,7 @@ def verify(objtype, name, result): ) directive.env.ref_context['py:module'] = 'sphinx.testing.util' directive.env.ref_context['py:class'] = 'Foo' - directive.env.current_document['autodoc:class'] = 'SphinxTestApp' + directive.env.current_document.autodoc_class = 'SphinxTestApp' verify( 'method', 'cleanup', @@ -1299,7 +1300,7 @@ def test_autodoc_module_member_order(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_module_scope(app): - app.env.current_document['autodoc:module'] = 'target' + app.env.current_document.autodoc_module = 'target' actual = do_autodoc(app, 'attribute', 'Class.mdocattr') assert list(actual) == [ '', @@ -1314,8 +1315,8 @@ def test_autodoc_module_scope(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_class_scope(app): - app.env.current_document['autodoc:module'] = 'target' - app.env.current_document['autodoc:class'] = 'Class' + app.env.current_document.autodoc_module = 'target' + app.env.current_document.autodoc_class = 'Class' actual = do_autodoc(app, 'attribute', 'mdocattr') assert list(actual) == [ '', From f1322d418b6c48f5f25d1245b550297f257a60ee Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:37:25 +0000 Subject: [PATCH 04/14] fixup! Add autodoc:{class,module} --- sphinx/environment/__init__.py | 5 +++++ sphinx/ext/autodoc/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index a7c0bb425dd..1e53abff3d5 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -915,7 +915,12 @@ def __init__( #: Maps object names to maps of attribute names -> type hints. self.autodoc_annotations: dict[str, dict[str, str]] = {} + #: The current Python class name. + #: Used in ``sphinx.ext.autodoc``. self.autodoc_class: str = '' + + #: The current Python module name. + #: Used in ``sphinx.ext.autodoc``. self.autodoc_module: str = '' #: Records the time when reading begain for the current document. diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 2b09f28d792..d266ffdfc9c 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -821,7 +821,7 @@ def document_members(self, all_members: bool = False) -> None: *self.options.members*. """ # set current namespace for finding members - self.env.current_document.autodoc_class = self.modname + self.env.current_document.autodoc_module = self.modname if self.objpath: self.env.current_document.autodoc_class = self.objpath[0] From e72a3d7a2950376a23b2de58a7f092a8586d9ded Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:52:42 +0000 Subject: [PATCH 05/14] fixup! Add autodoc:{class,module} --- sphinx/ext/autodoc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index d266ffdfc9c..b2cf5fbcf49 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1186,7 +1186,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # auto directive ... mod_cls_ = self.env.current_document.autodoc_class # ... or from a class directive - if mod_cls_ is None: + if not mod_cls_: mod_cls_ = self.env.ref_context.get('py:class') # ... if still None, there's no way to know if mod_cls_ is None: From 58c16d181608b6b2b4069f95e4d98a15b8ae0aa1 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:51:31 +0000 Subject: [PATCH 06/14] fixup! Add autodoc:{class,module} --- sphinx/ext/autodoc/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index b2cf5fbcf49..9f6b9e25d16 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1184,14 +1184,13 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a class-level object without path, # there must be a current class, either from a parent # auto directive ... - mod_cls_ = self.env.current_document.autodoc_class + mod_cls = self.env.current_document.autodoc_class # ... or from a class directive - if not mod_cls_: - mod_cls_ = self.env.ref_context.get('py:class') - # ... if still None, there's no way to know - if mod_cls_ is None: + if not mod_cls: + mod_cls = self.env.ref_context.get('py:class', '') + # ... if still falsy, there's no way to know + if not mod_cls: return None, [] - mod_cls = mod_cls_ modname, sep, cls = mod_cls.rpartition('.') parents = [cls] # if the module name is still missing, get it like above From 11e23d5d38b43f7d24450b7f46556ff508d8c1e7 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 02:55:15 +0000 Subject: [PATCH 07/14] post-merge --- sphinx/directives/__init__.py | 2 +- sphinx/domains/cpp/__init__.py | 9 ++++----- sphinx/ext/autodoc/typehints.py | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index a02e070c204..1488d4ee391 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -296,7 +296,7 @@ def run(self) -> list[Node]: 'object-description-transform', self.domain, self.objtype, content_node ) DocFieldTransformer(self).transform_all(content_node) - self.env.temp_data['object'] = None + self.env.current_document.obj_desc_name = '' self.after_content() if node['no-typesetting']: diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index d6c098e720b..4493018ab97 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -430,9 +430,10 @@ def after_content(self) -> None: current_document['cpp:domain_name'] = current_document['cpp:domain_name'][:-1] def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + last_symbol: Symbol = self.env.current_document['cpp:last_symbol'] return tuple( s.identOrOp._stringify(str) - for s in self.env.current_document['cpp:last_symbol'].get_full_nested_name().names + for s in last_symbol.get_full_nested_name().names ) def _toc_entry_name(self, sig_node: desc_signature) -> str: @@ -447,12 +448,10 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: parens = '' *parents, name = sig_node['_toc_parts'] if config.toc_object_entries_show_parents == 'domain': - return '::'.join( - ( + return '::'.join(( *self.env.current_document.get('cpp:domain_name', ()), name + parens, - ) - ) + )) if config.toc_object_entries_show_parents == 'hide': return name + parens if config.toc_object_entries_show_parents == 'all': diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index e9f7b4e29ae..f2b5b2567a9 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -38,7 +38,8 @@ def record_typehints( try: if callable(obj): - annotation = app.env.current_document.autodoc_annotations.setdefault(name, {}) + current_document = app.env.current_document + annotation = current_document.autodoc_annotations.setdefault(name, {}) sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases) for param in sig.parameters.values(): if param.annotation is not param.empty: From 5d559f322091e6dde33c2f95ce643eae6ec1e1a2 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 04:01:52 +0000 Subject: [PATCH 08/14] Refactor _CurrentDocument --- sphinx/environment/__init__.py | 128 +++++++++------------------------ 1 file changed, 33 insertions(+), 95 deletions(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 431fb5f8165..a2639789ba2 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator - from typing import Any, Literal + from typing import Any, Final, Literal from docutils import nodes from docutils.nodes import Node @@ -884,6 +884,26 @@ class _CurrentDocument: 'reading_started_at', ) + # Map of old-style temp_data keys to _CurrentDocument attributes + __attr_map: Final = { + '_parser': '_parser', + 'annotations': 'autodoc_annotations', + 'autodoc:class': 'autodoc_class', + 'autodoc:module': 'autodoc_module', + 'default_domain': 'default_domain', + 'default_role': 'default_role', + 'docname': 'docname', + 'highlight_language': 'highlight_language', + 'object': 'obj_desc_name', + 'started_at': 'reading_started_at', + } + + # Attributes that should reset to None if popped. + __attr_default_none: Final = frozenset({ + '_parser', + 'default_domain', + }) + def __init__( self, *, @@ -949,51 +969,13 @@ def new_serial_number(self, category: str = '', /) -> int: # Mapping interface: def __getitem__(self, item: str) -> Any: - if item == 'annotations': - return self.autodoc_annotations - if item == 'autodoc:class': - return self.autodoc_class - if item == 'autodoc:module': - return self.autodoc_module - if item == 'object': - return self.obj_desc_name - if item == 'started_at': - return self.reading_started_at - if item in { - '_parser', - 'autodoc_annotations', - 'default_domain', - 'default_role', - 'docname', - 'highlight_language', - 'obj_desc_name', - 'reading_started_at', - }: - return getattr(self, item) + if item in self.__attr_map: + getattr(self, self.__attr_map[item]) return self._ext_props[item] def __setitem__(self, key: str, value: Any) -> None: - if key == 'annotations': - self.autodoc_annotations = value - elif key == 'autodoc:class': - self.autodoc_class = value - elif key == 'autodoc:module': - self.autodoc_module = value - elif key == 'object': - self.obj_desc_name = value - elif key == 'started_at': - self.reading_started_at = value - elif key in { - '_parser', - 'autodoc_annotations', - 'default_domain', - 'default_role', - 'docname', - 'highlight_language', - 'obj_desc_name', - 'reading_started_at', - }: - setattr(self, key, value) + if key in self.__attr_map: + setattr(self, self.__attr_map[key], value) else: self._ext_props[key] = value @@ -1001,30 +983,7 @@ def __delitem__(self, key: str) -> None: del self._ext_props[key] def __contains__(self, item: str) -> bool: - if item in { - 'annotations', - 'autodoc:class', - 'autodoc:module', - 'object', - 'started_at', - }: - return True - if item in { - '_parser', - '_serial_numbers', - '_ext_props', - 'autodoc_annotations', - 'autodoc_class', - 'autodoc_module', - 'default_domain', - 'default_role', - 'docname', - 'highlight_language', - 'obj_desc_name', - 'reading_started_at', - }: - return True - return item in self._ext_props + return item in self.__attr_map or item in self._ext_props def get(self, key: str, default: Any | None = None) -> Any | None: try: @@ -1033,34 +992,13 @@ def get(self, key: str, default: Any | None = None) -> Any | None: return default def pop(self, key: str, default: Any | None = None) -> Any | None: - if key == 'annotations': - key = 'autodoc_annotations' - if key == 'autodoc:class': - key = 'autodoc_class' - if key == 'autodoc:module': - key = 'autodoc_module' - elif key == 'object': - key = 'obj_desc_name' - elif key == 'started_at': - key = 'reading_started_at' - try: - blank: str | float | dict[str, dict[str, str]] | None = { - '_parser': None, - 'autodoc_annotations': {}, - 'autodoc_class': '', - 'autodoc_module': '', - 'default_domain': None, - 'default_role': '', - 'docname': '', - 'highlight_language': '', - 'obj_desc_name': '', - 'reading_started_at': 0.0, - }[key] - except KeyError: - pass - else: - value = getattr(self, key) - setattr(self, key, blank) + if key in self.__attr_map: + value = getattr(self, self.__attr_map[key]) + if key in self.__attr_default_none: + default = None + else: + default = type(value)() # set key to type's default + setattr(self, self.__attr_map[key], default) return value return self._ext_props.pop(key, default) From a11b635b1780496ae996b259b7351a859eb848c1 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 04:23:21 +0000 Subject: [PATCH 09/14] Convert C/C++ map-style to attributes --- sphinx/domains/c/__init__.py | 50 ++++++++++++-------------- sphinx/domains/cpp/__init__.py | 66 ++++++++++++++++------------------ sphinx/environment/__init__.py | 57 +++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 64 deletions(-) diff --git a/sphinx/domains/c/__init__.py b/sphinx/domains/c/__init__.py index 47d0f14076a..992d95b932d 100644 --- a/sphinx/domains/c/__init__.py +++ b/sphinx/domains/c/__init__.py @@ -220,19 +220,19 @@ def describe_signature( def run(self) -> list[Node]: env = self.state.document.settings.env # from ObjectDescription.run - if 'c:parent_symbol' not in env.current_document: + if env.current_document.c_parent_symbol is None: root = env.domaindata['c']['root_symbol'] - env.current_document['c:parent_symbol'] = root + env.current_document.c_parent_symbol = root env.ref_context['c:parent_key'] = root.get_lookup_key() # When multiple declarations are made in the same directive # they need to know about each other to provide symbol lookup for function parameters. # We use last_symbol to store the latest added declaration in a directive. - env.current_document['c:last_symbol'] = None + env.current_document.c_last_symbol = None return super().run() def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: - parent_symbol: Symbol = self.env.current_document['c:parent_symbol'] + parent_symbol: Symbol = self.env.current_document.c_parent_symbol max_len = ( self.env.config.c_maximum_signature_line_length @@ -254,7 +254,7 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: # the possibly inner declarations. name = _make_phony_error_name() symbol = parent_symbol.add_name(name) - self.env.current_document['c:last_symbol'] = symbol + self.env.current_document.c_last_symbol = symbol raise ValueError from e try: @@ -264,15 +264,15 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None - symbol.siblingAbove = self.env.current_document['c:last_symbol'] + symbol.siblingAbove = self.env.current_document.c_last_symbol if symbol.siblingAbove is not None: assert symbol.siblingAbove.siblingBelow is None symbol.siblingAbove.siblingBelow = symbol - self.env.current_document['c:last_symbol'] = symbol + self.env.current_document.c_last_symbol = symbol except _DuplicateSymbolError as e: # Assume we are actually in the old symbol, # instead of the newly created duplicate. - self.env.current_document['c:last_symbol'] = e.symbol + self.env.current_document.c_last_symbol = e.symbol msg = __( 'Duplicate C declaration, also defined at %s:%s.\n' "Declaration is '.. c:%s:: %s'." @@ -298,15 +298,15 @@ def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration: return ast def before_content(self) -> None: - last_symbol: Symbol = self.env.current_document['c:last_symbol'] + last_symbol: Symbol = self.env.current_document.c_last_symbol assert last_symbol - self.oldParentSymbol = self.env.current_document['c:parent_symbol'] + self.oldParentSymbol = self.env.current_document.c_parent_symbol self.oldParentKey: LookupKey = self.env.ref_context['c:parent_key'] - self.env.current_document['c:parent_symbol'] = last_symbol + self.env.current_document.c_parent_symbol = last_symbol self.env.ref_context['c:parent_key'] = last_symbol.get_lookup_key() def after_content(self) -> None: - self.env.current_document['c:parent_symbol'] = self.oldParentSymbol + self.env.current_document.c_parent_symbol = self.oldParentSymbol self.env.ref_context['c:parent_key'] = self.oldParentKey @@ -410,8 +410,8 @@ def run(self) -> list[Node]: name = _make_phony_error_name() symbol = root_symbol.add_name(name) stack = [symbol] - self.env.current_document['c:parent_symbol'] = symbol - self.env.current_document['c:namespace_stack'] = stack + self.env.current_document.c_parent_symbol = symbol + self.env.current_document.c_namespace_stack = stack self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -435,14 +435,12 @@ def run(self) -> list[Node]: except DefinitionError as e: logger.warning(e, location=self.get_location()) name = _make_phony_error_name() - old_parent = self.env.current_document.get('c:parent_symbol', None) + old_parent = self.env.current_document.c_parent_symbol if not old_parent: old_parent = self.env.domaindata['c']['root_symbol'] symbol = old_parent.add_name(name) - stack = self.env.current_document.get('c:namespace_stack', []) - stack.append(symbol) - self.env.current_document['c:parent_symbol'] = symbol - self.env.current_document['c:namespace_stack'] = stack + self.env.current_document.c_namespace_stack.append(symbol) + self.env.current_document.c_parent_symbol = symbol self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -455,21 +453,19 @@ class CNamespacePopObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - stack = self.env.current_document.get('c:namespace_stack', None) - if not stack or len(stack) == 0: + stack = self.env.current_document.c_namespace_stack + if len(stack) == 0: logger.warning( 'C namespace pop on empty stack. Defaulting to global scope.', location=self.get_location(), ) - stack = [] else: stack.pop() if len(stack) > 0: symbol = stack[-1] else: symbol = self.env.domaindata['c']['root_symbol'] - self.env.current_document['c:parent_symbol'] = symbol - self.env.current_document['c:namespace_stack'] = stack + self.env.current_document.c_parent_symbol = symbol self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() return [] @@ -488,9 +484,9 @@ def __init__( self.aliasOptions = aliasOptions self.document = document if env is not None: - if 'c:parent_symbol' not in env.current_document: + if env.current_document.c_parent_symbol is None: root = env.domaindata['c']['root_symbol'] - env.current_document['c:parent_symbol'] = root + env.current_document.c_parent_symbol = root env.ref_context['c:parent_key'] = root.get_lookup_key() self.parentKey = env.ref_context['c:parent_key'] else: @@ -735,7 +731,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: # see below node = addnodes.desc_inline('c', text, text, classes=[self.class_type]) return [node], [] - parent_symbol = self.env.current_document.get('c:parent_symbol', None) + parent_symbol = self.env.current_document.c_parent_symbol if parent_symbol is None: parent_symbol = self.env.domaindata['c']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, diff --git a/sphinx/domains/cpp/__init__.py b/sphinx/domains/cpp/__init__.py index 4493018ab97..b696e529b43 100644 --- a/sphinx/domains/cpp/__init__.py +++ b/sphinx/domains/cpp/__init__.py @@ -309,9 +309,9 @@ def describe_signature( def run(self) -> list[Node]: env = self.state.document.settings.env # from ObjectDescription.run - if 'cpp:parent_symbol' not in env.current_document: + if env.current_document.cpp_parent_symbol is None: root = env.domaindata['cpp']['root_symbol'] - env.current_document['cpp:parent_symbol'] = root + env.current_document.cpp_parent_symbol = root env.ref_context['cpp:parent_key'] = root.get_lookup_key() # The lookup keys assume that no nested scopes exists inside overloaded functions. @@ -325,7 +325,7 @@ def run(self) -> list[Node]: # :cpp:any:`boom` # # So we disallow any signatures inside functions. - parent_symbol = env.current_document['cpp:parent_symbol'] + parent_symbol = env.current_document.cpp_parent_symbol parent_decl = parent_symbol.declaration if parent_decl is not None and parent_decl.objectType == 'function': msg = ( @@ -336,16 +336,16 @@ def run(self) -> list[Node]: logger.warning(msg, location=self.get_location()) name = _make_phony_error_name() symbol = parent_symbol.add_name(name) - env.current_document['cpp:last_symbol'] = symbol + env.current_document.cpp_last_symbol = symbol return [] # When multiple declarations are made in the same directive # they need to know about each other to provide symbol lookup for function parameters. # We use last_symbol to store the latest added declaration in a directive. - env.current_document['cpp:last_symbol'] = None + env.current_document.cpp_last_symbol = None return super().run() def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: - parent_symbol: Symbol = self.env.current_document['cpp:parent_symbol'] + parent_symbol: Symbol = self.env.current_document.cpp_parent_symbol max_len = ( self.env.config.cpp_maximum_signature_line_length @@ -367,7 +367,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: # the possibly inner declarations. name = _make_phony_error_name() symbol = parent_symbol.add_name(name) - self.env.current_document['cpp:last_symbol'] = symbol + self.env.current_document.cpp_last_symbol = symbol raise ValueError from e try: @@ -377,15 +377,15 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: # append the new declaration to the sibling list assert symbol.siblingAbove is None assert symbol.siblingBelow is None - symbol.siblingAbove = self.env.current_document['cpp:last_symbol'] + symbol.siblingAbove = self.env.current_document.cpp_last_symbol if symbol.siblingAbove is not None: assert symbol.siblingAbove.siblingBelow is None symbol.siblingAbove.siblingBelow = symbol - self.env.current_document['cpp:last_symbol'] = symbol + self.env.current_document.cpp_last_symbol = symbol except _DuplicateSymbolError as e: # Assume we are actually in the old symbol, # instead of the newly created duplicate. - self.env.current_document['cpp:last_symbol'] = e.symbol + self.env.current_document.cpp_last_symbol = e.symbol msg = __( 'Duplicate C++ declaration, also defined at %s:%s.\n' "Declaration is '.. cpp:%s:: %s'." @@ -412,25 +412,25 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration: return ast def before_content(self) -> None: - last_symbol: Symbol = self.env.current_document['cpp:last_symbol'] + last_symbol: Symbol = self.env.current_document.cpp_last_symbol assert last_symbol - self.oldParentSymbol = self.env.current_document['cpp:parent_symbol'] + self.oldParentSymbol = self.env.current_document.cpp_parent_symbol self.oldParentKey: LookupKey = self.env.ref_context['cpp:parent_key'] - self.env.current_document['cpp:parent_symbol'] = last_symbol + self.env.current_document.cpp_parent_symbol = last_symbol self.env.ref_context['cpp:parent_key'] = last_symbol.get_lookup_key() - self.env.current_document['cpp:domain_name'] = ( - *self.env.current_document.get('cpp:domain_name', ()), + self.env.current_document.cpp_domain_name = ( + *self.env.current_document.cpp_domain_name, last_symbol.identOrOp._stringify(str), ) def after_content(self) -> None: - current_document = self.env.current_document - current_document['cpp:parent_symbol'] = self.oldParentSymbol + self.env.current_document.cpp_parent_symbol = self.oldParentSymbol self.env.ref_context['cpp:parent_key'] = self.oldParentKey - current_document['cpp:domain_name'] = current_document['cpp:domain_name'][:-1] + cpp_domain_name = self.env.current_document.cpp_domain_name + self.env.current_document.cpp_domain_name = cpp_domain_name[:-1] def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: - last_symbol: Symbol = self.env.current_document['cpp:last_symbol'] + last_symbol: Symbol = self.env.current_document.cpp_last_symbol return tuple( s.identOrOp._stringify(str) for s in last_symbol.get_full_nested_name().names @@ -449,7 +449,7 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str: *parents, name = sig_node['_toc_parts'] if config.toc_object_entries_show_parents == 'domain': return '::'.join(( - *self.env.current_document.get('cpp:domain_name', ()), + *self.env.current_document.cpp_domain_name, name + parens, )) if config.toc_object_entries_show_parents == 'hide': @@ -556,8 +556,8 @@ def run(self) -> list[Node]: ast = ASTNamespace(name, None) symbol = root_symbol.add_name(ast.nestedName, ast.templatePrefix) stack = [symbol] - self.env.current_document['cpp:parent_symbol'] = symbol - self.env.current_document['cpp:namespace_stack'] = stack + self.env.current_document.cpp_parent_symbol = symbol + self.env.current_document.cpp_namespace_stack = stack self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -582,14 +582,12 @@ def run(self) -> list[Node]: logger.warning(e, location=self.get_location()) name = _make_phony_error_name() ast = ASTNamespace(name, None) - old_parent = self.env.current_document.get('cpp:parent_symbol', None) + old_parent = self.env.current_document.cpp_parent_symbol if not old_parent: old_parent = self.env.domaindata['cpp']['root_symbol'] symbol = old_parent.add_name(ast.nestedName, ast.templatePrefix) - stack = self.env.current_document.get('cpp:namespace_stack', []) - stack.append(symbol) - self.env.current_document['cpp:parent_symbol'] = symbol - self.env.current_document['cpp:namespace_stack'] = stack + self.env.current_document.cpp_namespace_stack.append(symbol) + self.env.current_document.cpp_parent_symbol = symbol self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -602,21 +600,19 @@ class CPPNamespacePopObject(SphinxDirective): option_spec: ClassVar[OptionSpec] = {} def run(self) -> list[Node]: - stack = self.env.current_document.get('cpp:namespace_stack', None) - if not stack or len(stack) == 0: + stack = self.env.current_document.cpp_namespace_stack + if len(stack) == 0: logger.warning( 'C++ namespace pop on empty stack. Defaulting to global scope.', location=self.get_location(), ) - stack = [] else: stack.pop() if len(stack) > 0: symbol = stack[-1] else: symbol = self.env.domaindata['cpp']['root_symbol'] - self.env.current_document['cpp:parent_symbol'] = symbol - self.env.current_document['cpp:namespace_stack'] = stack + self.env.current_document.cpp_parent_symbol = symbol self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key() return [] @@ -633,9 +629,9 @@ def __init__( self.sig = sig self.aliasOptions = aliasOptions if env is not None: - if 'cpp:parent_symbol' not in env.current_document: + if env.current_document.cpp_parent_symbol is None: root = env.domaindata['cpp']['root_symbol'] - env.current_document['cpp:parent_symbol'] = root + env.current_document.cpp_parent_symbol = root env.ref_context['cpp:parent_key'] = root.get_lookup_key() self.parentKey = env.ref_context['cpp:parent_key'] else: @@ -913,7 +909,7 @@ def run(self) -> tuple[list[Node], list[system_message]]: # see below node = addnodes.desc_inline('cpp', text, text, classes=[self.class_type]) return [node], [] - parent_symbol = self.env.current_document.get('cpp:parent_symbol', None) + parent_symbol = self.env.current_document.cpp_parent_symbol if parent_symbol is None: parent_symbol = self.env.domaindata['cpp']['root_symbol'] # ...most if not all of these classes should really apply to the individual references, diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index a2639789ba2..ca13bb97cde 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -7,7 +7,7 @@ import pickle from collections import defaultdict from copy import deepcopy -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from sphinx import addnodes from sphinx.domains._domains_container import _DomainsContainer @@ -32,7 +32,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterable, Iterator - from typing import Any, Final, Literal + from typing import Any, Literal from docutils import nodes from docutils.nodes import Node @@ -42,6 +42,8 @@ from sphinx.builders import Builder from sphinx.config import Config from sphinx.domains import Domain + from sphinx.domains.c._symbol import Symbol as CSymbol + from sphinx.domains.cpp._symbol import Symbol as CPPSymbol from sphinx.events import EventManager from sphinx.project import Project from sphinx.util._pathlib import _StrPath @@ -876,6 +878,13 @@ class _CurrentDocument: 'autodoc_annotations', 'autodoc_class', 'autodoc_module', + 'c_last_symbol', + 'c_namespace_stack', + 'c_parent_symbol', + 'cpp_domain_name', + 'cpp_last_symbol', + 'cpp_namespace_stack', + 'cpp_parent_symbol', 'default_domain', 'default_role', 'docname', @@ -890,6 +899,13 @@ class _CurrentDocument: 'annotations': 'autodoc_annotations', 'autodoc:class': 'autodoc_class', 'autodoc:module': 'autodoc_module', + 'c:last_symbol': 'c_last_symbol', + 'c:namespace_stack': 'c_namespace_stack', + 'c:parent_symbol': 'c_parent_symbol', + 'cpp:domain_name': 'cpp_domain_name', + 'cpp:last_symbol': 'cpp_last_symbol', + 'cpp:namespace_stack': 'cpp_namespace_stack', + 'cpp:parent_symbol': 'cpp_parent_symbol', 'default_domain': 'default_domain', 'default_role': 'default_role', 'docname': 'docname', @@ -901,6 +917,10 @@ class _CurrentDocument: # Attributes that should reset to None if popped. __attr_default_none: Final = frozenset({ '_parser', + 'c:last_symbol', + 'c:parent_symbol', + 'cpp:last_symbol', + 'cpp:parent_symbol', 'default_domain', }) @@ -947,6 +967,37 @@ def __init__( #: Used in ``sphinx.ext.autodoc``. self.autodoc_module: str = '' + #: The most-recently added declaration in a directive. + #: Used in the C Domain. + self.c_last_symbol: CSymbol | None = None + + #: The stack of namespace scopes, altered by the ``.. c:namespace::`` + #: and ``.. c:namespace-(push|pop)::``directives. + #: Used in the C Domain. + self.c_namespace_stack: list[CSymbol] = [] + + #: The parent declaration. + #: Used in the C Domain. + self.c_parent_symbol: CSymbol | None = None + + #: A stack of the string representation of declarations, + #: used to format the table of contents entry. + #: Used in the C++ Domain. + self.cpp_domain_name: tuple[str, ...] = () + + #: The most-recently added declaration in a directive. + #: Used in the C++ Domain. + self.cpp_last_symbol: CPPSymbol | None = None + + #: The stack of namespace scopes, altered by the ``.. cpp:namespace::`` + #: and ``.. cpp:namespace-(push|pop)::``directives. + #: Used in the C++ Domain. + self.cpp_namespace_stack: list[CPPSymbol] = [] + + #: The parent declaration. + #: Used in the C++ Domain. + self.cpp_parent_symbol: CPPSymbol | None = None + #: Records the time when reading begain for the current document. #: Used in ``sphinx.ext.duration``. self.reading_started_at: float = 0.0 @@ -983,6 +1034,8 @@ def __delitem__(self, key: str) -> None: del self._ext_props[key] def __contains__(self, item: str) -> bool: + if item in {'c:parent_symbol', 'cpp:parent_symbol'}: + return getattr(self, item) is not None return item in self.__attr_map or item in self._ext_props def get(self, key: str, default: Any | None = None) -> Any | None: From 2d2c7cd5c30b2232cdebbcbe4023eceed3d45415 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 04:33:50 +0000 Subject: [PATCH 10/14] address review comments --- sphinx/environment/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index ca13bb97cde..105c39309f3 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -874,7 +874,7 @@ class _CurrentDocument: __slots__ = ( '_parser', '_serial_numbers', - '_ext_props', + '_extension_data', 'autodoc_annotations', 'autodoc_class', 'autodoc_module', @@ -1006,12 +1006,12 @@ def __init__( self._serial_numbers: dict[str, int] = {} # Stores properties relating to the current document set by extensions. - self._ext_props: dict[str, Any] = {} + self._extension_data: dict[str, Any] = {} def new_serial_number(self, category: str = '', /) -> int: """Return a serial number, e.g. for index entry targets. - The number is guaranteed to be unique in the current document. + The number is guaranteed to be unique in the current document & category. """ current = self._serial_numbers.get(category, 0) self._serial_numbers[category] = current + 1 @@ -1022,21 +1022,21 @@ def new_serial_number(self, category: str = '', /) -> int: def __getitem__(self, item: str) -> Any: if item in self.__attr_map: getattr(self, self.__attr_map[item]) - return self._ext_props[item] + return self._extension_data[item] def __setitem__(self, key: str, value: Any) -> None: if key in self.__attr_map: setattr(self, self.__attr_map[key], value) else: - self._ext_props[key] = value + self._extension_data[key] = value def __delitem__(self, key: str) -> None: - del self._ext_props[key] + del self._extension_data[key] def __contains__(self, item: str) -> bool: if item in {'c:parent_symbol', 'cpp:parent_symbol'}: return getattr(self, item) is not None - return item in self.__attr_map or item in self._ext_props + return item in self.__attr_map or item in self._extension_data def get(self, key: str, default: Any | None = None) -> Any | None: try: @@ -1053,10 +1053,10 @@ def pop(self, key: str, default: Any | None = None) -> Any | None: default = type(value)() # set key to type's default setattr(self, self.__attr_map[key], default) return value - return self._ext_props.pop(key, default) + return self._extension_data.pop(key, default) def setdefault(self, key: str, default: Any | None = None) -> Any | None: - return self._ext_props.setdefault(key, default) + return self._extension_data.setdefault(key, default) def clear(self) -> None: _CurrentDocument.__init__(self) # NoQA: PLC2801 From da96228a0c5c26620a439685af79c7d72a1c285f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 05:25:00 +0000 Subject: [PATCH 11/14] doc --- doc/extdev/envapi.rst | 42 ++++++++++++++++++++++++++++++++++ sphinx/environment/__init__.py | 9 ++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/doc/extdev/envapi.rst b/doc/extdev/envapi.rst index 971e506d093..1982146a20e 100644 --- a/doc/extdev/envapi.rst +++ b/doc/extdev/envapi.rst @@ -47,6 +47,48 @@ Build environment API .. autoattribute:: parser + **Per-document attributes** + + .. attribute:: current_document + + Temporary data storage while reading a document. + + .. important:: + Only the following attributes constitute the public API. + The type itself and any methods or other attributes remain private, + experimental, and will be changed or removed without notice. + + .. attribute:: current_document.docname + :type: str + + The document name ('docname') for the current document. + + .. attribute:: current_document.default_role + :type: str + + The default role for the current document. + Set by the :dudir:`default-role` directive. + + .. attribute:: current_document.default_domain + :type: Domain | None + + The default domain for the current document. + Set by the :rst:dir:`default-domain` directive. + + .. attribute:: current_document.highlight_language + :type: str + + The default language for syntax highlighting. + Set by the :rst:dir:`highlight` directive to override + the :confval:`highlight_language` config value. + + .. attribute:: current_document._parser + :type: Parser | None + + *This attribute is experimental and may be changed without notice.* + + The parser being used to parse the current document. + **Utility methods** .. automethod:: doc2path diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 105c39309f3..d4f7efc6682 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -869,7 +869,12 @@ def _check_toc_parents(toctree_includes: dict[str, list[str]]) -> None: class _CurrentDocument: - """Temporary data storage while reading a document.""" + """Temporary data storage while reading a document. + + This class is only for internal use. Please don't use this in your extensions. + It will be removed or changed without notice. + The only stable API is via ``env.current_document``. + """ __slots__ = ( '_parser', @@ -932,7 +937,7 @@ def __init__( default_domain: Domain | None = None, ) -> None: #: The docname of the document currently being parsed. - self.docname = docname + self.docname: str = docname #: The default role for the current document. #: Set by the ``.. default-role::`` directive. From 4a7859c8d6556d220766640f190c5829b573510c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 23:04:05 +0000 Subject: [PATCH 12/14] fixes --- sphinx/environment/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index d4f7efc6682..e4d1f5118fa 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -1026,7 +1026,7 @@ def new_serial_number(self, category: str = '', /) -> int: def __getitem__(self, item: str) -> Any: if item in self.__attr_map: - getattr(self, self.__attr_map[item]) + return getattr(self, self.__attr_map[item]) return self._extension_data[item] def __setitem__(self, key: str, value: Any) -> None: @@ -1036,7 +1036,7 @@ def __setitem__(self, key: str, value: Any) -> None: self._extension_data[key] = value def __delitem__(self, key: str) -> None: - del self._extension_data[key] + self.pop(key, default=None) def __contains__(self, item: str) -> bool: if item in {'c:parent_symbol', 'cpp:parent_symbol'}: From a09be4993d77f24d23e11bdfd3123a73d408f91c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 3 Jan 2025 23:06:58 +0000 Subject: [PATCH 13/14] mapping interface --- doc/extdev/envapi.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/extdev/envapi.rst b/doc/extdev/envapi.rst index 1982146a20e..d57374448cc 100644 --- a/doc/extdev/envapi.rst +++ b/doc/extdev/envapi.rst @@ -53,6 +53,10 @@ Build environment API Temporary data storage while reading a document. + Extensions may use the mapping interface provided by + ``env.current_document`` to store data relating to the current document, + but should use a unique prefix to avoid name clashes. + .. important:: Only the following attributes constitute the public API. The type itself and any methods or other attributes remain private, From 8fbb4b40449d23a00f8be392b7aa62a661d30d08 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:09:20 +0000 Subject: [PATCH 14/14] all other methods --- sphinx/environment/__init__.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index e4d1f5118fa..74b75d6921b 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -1043,14 +1043,34 @@ def __contains__(self, item: str) -> bool: return getattr(self, item) is not None return item in self.__attr_map or item in self._extension_data + def __iter__(self) -> Iterator[str]: + return iter(self.keys()) + + def __len__(self) -> int: + return len(self.__attr_map) + len(self._extension_data) + + def keys(self) -> Iterable[str]: + return frozenset(self.__attr_map.keys() | self._extension_data.keys()) + + def items(self) -> Iterable[tuple[str, Any]]: + for key in self.keys(): + yield key, self[key] + + def values(self) -> Iterable[Any]: + for key in self.keys(): + yield self[key] + def get(self, key: str, default: Any | None = None) -> Any | None: try: return self[key] except KeyError: return default - def pop(self, key: str, default: Any | None = None) -> Any | None: + __sentinel = object() + + def pop(self, key: str, default: Any | None = __sentinel) -> Any | None: if key in self.__attr_map: + # the keys in __attr_map always exist, so ``default`` is ignored value = getattr(self, self.__attr_map[key]) if key in self.__attr_default_none: default = None @@ -1058,6 +1078,8 @@ def pop(self, key: str, default: Any | None = None) -> Any | None: default = type(value)() # set key to type's default setattr(self, self.__attr_map[key], default) return value + if default is self.__sentinel: + return self._extension_data.pop(key) return self._extension_data.pop(key, default) def setdefault(self, key: str, default: Any | None = None) -> Any | None: @@ -1065,3 +1087,9 @@ def setdefault(self, key: str, default: Any | None = None) -> Any | None: def clear(self) -> None: _CurrentDocument.__init__(self) # NoQA: PLC2801 + + def update(self, other: Iterable[tuple[str, Any]] = (), /, **kwargs: Any) -> None: + other_dict = dict(other) if not isinstance(other, dict) else other + for dct in other_dict, kwargs: + for key, value in dct.items(): + self[key] = value