diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 620ff82abe5..e493d11cf59 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 d92678ab8f2..bad7dbc8f21 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'] = '' + self.env.temp_data['object'] = None 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 abf622d5bb8..25f8f8e5b16 100644 --- a/sphinx/domains/changeset.py +++ b/sphinx/domains/changeset.py @@ -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..1e53abff3d5 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,208 @@ 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', + 'autodoc_class', + 'autodoc_module', + '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]] = {} + + #: 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. + #: 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 == '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) + 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) + 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', + '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 + + 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' + 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) + 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..9f6b9e25d16 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 = '' + 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.temp_data.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,19 +1184,18 @@ 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.autodoc_class # ... or from a class directive - if mod_cls_ is None: - 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 if not modname: - modname = self.env.temp_data.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/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 f8fa00f4316..58a575f30ba 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 05427faf56f..d762fa55670 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -183,13 +183,13 @@ 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() + 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..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.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'] + 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.temp_data['autodoc:class'] = 'SphinxTestApp' + directive.env.current_document.autodoc_class = 'SphinxTestApp' verify( 'method', 'cleanup', @@ -526,7 +527,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 +1300,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 +1315,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'