Create a new type for the current document's environment state (#13151)

This commit is contained in:
Adam Turner 2025-01-04 00:28:03 +00:00 committed by GitHub
parent c7eaf175e6
commit e65bbb96ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 427 additions and 139 deletions

View File

@ -47,6 +47,52 @@ Build environment API
.. autoattribute:: parser
**Per-document attributes**
.. attribute:: current_document
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,
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

View File

@ -13,7 +13,12 @@ from typing import TYPE_CHECKING, Any, Literal, final
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 __
@ -620,7 +625,7 @@ class Builder:
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()
@ -640,7 +645,7 @@ class Builder:
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)

View File

@ -284,9 +284,9 @@ class ObjectDescription(SphinxDirective, Generic[ObjDescT]):
# 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 @@ class ObjectDescription(SphinxDirective, Generic[ObjDescT]):
'object-description-transform', self.domain, self.objtype, content_node
)
DocFieldTransformer(self).transform_all(content_node)
self.env.temp_data['object'] = ''
self.env.current_document.obj_desc_name = ''
self.after_content()
if node['no-typesetting']:
@ -335,7 +335,7 @@ class DefaultRole(SphinxDirective):
)
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 []

View File

@ -48,7 +48,7 @@ class Highlight(SphinxDirective):
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 @@ class CodeBlock(SphinxDirective):
# 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:

View File

@ -115,8 +115,9 @@ class Code(SphinxDirective):
# 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:

View File

@ -220,19 +220,19 @@ class CObject(ObjectDescription[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 env.current_document.c_parent_symbol is None:
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:
parent_symbol: Symbol = self.env.temp_data['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 @@ class CObject(ObjectDescription[ASTDeclaration]):
# the possibly inner declarations.
name = _make_phony_error_name()
symbol = parent_symbol.add_name(name)
self.env.temp_data['c:last_symbol'] = symbol
self.env.current_document.c_last_symbol = symbol
raise ValueError from e
try:
@ -264,15 +264,15 @@ class CObject(ObjectDescription[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'."
@ -298,15 +298,15 @@ class CObject(ObjectDescription[ASTDeclaration]):
return ast
def before_content(self) -> None:
last_symbol: Symbol = self.env.temp_data['c:last_symbol']
last_symbol: Symbol = self.env.current_document.c_last_symbol
assert last_symbol
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'] = 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.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
@ -410,8 +410,8 @@ class CNamespaceObject(SphinxDirective):
name = _make_phony_error_name()
symbol = root_symbol.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 []
@ -435,14 +435,12 @@ class CNamespacePushObject(SphinxDirective):
except DefinitionError as e:
logger.warning(e, location=self.get_location())
name = _make_phony_error_name()
old_parent = self.env.temp_data.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.temp_data.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_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.temp_data.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.temp_data['c:parent_symbol'] = symbol
self.env.temp_data['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 @@ class AliasNode(nodes.Element):
self.aliasOptions = aliasOptions
self.document = document
if env is not None:
if 'c:parent_symbol' not in env.temp_data:
if env.current_document.c_parent_symbol is None:
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:
@ -735,7 +731,7 @@ class CExprRole(SphinxRole):
# see below
node = addnodes.desc_inline('c', text, text, classes=[self.class_type])
return [node], []
parent_symbol = self.env.temp_data.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,

View File

@ -133,7 +133,7 @@ class ChangeSetDomain(Domain):
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,

View File

@ -309,9 +309,9 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
def run(self) -> list[Node]:
env = self.state.document.settings.env # from ObjectDescription.run
if 'cpp:parent_symbol' not in env.temp_data:
if env.current_document.cpp_parent_symbol is None:
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.
@ -325,7 +325,7 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
# :cpp:any:`boom`
#
# So we disallow any signatures inside functions.
parent_symbol = env.temp_data['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 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
logger.warning(msg, location=self.get_location())
name = _make_phony_error_name()
symbol = parent_symbol.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:
parent_symbol: Symbol = self.env.temp_data['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 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
# the possibly inner declarations.
name = _make_phony_error_name()
symbol = parent_symbol.add_name(name)
self.env.temp_data['cpp:last_symbol'] = symbol
self.env.current_document.cpp_last_symbol = symbol
raise ValueError from e
try:
@ -377,15 +377,15 @@ class CPPObject(ObjectDescription[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'."
@ -412,27 +412,28 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
return ast
def before_content(self) -> None:
last_symbol: Symbol = self.env.temp_data['cpp:last_symbol']
last_symbol: Symbol = self.env.current_document.cpp_last_symbol
assert last_symbol
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'] = 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.temp_data['cpp:domain_name'] = (
*self.env.temp_data.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:
temp_data = self.env.temp_data
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
temp_data['cpp:domain_name'] = temp_data['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
return tuple(
s.identOrOp._stringify(str)
for s in self.env.temp_data['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:
@ -448,7 +449,7 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
*parents, name = sig_node['_toc_parts']
if config.toc_object_entries_show_parents == 'domain':
return '::'.join((
*self.env.temp_data.get('cpp:domain_name', ()),
*self.env.current_document.cpp_domain_name,
name + parens,
))
if config.toc_object_entries_show_parents == 'hide':
@ -555,8 +556,8 @@ class CPPNamespaceObject(SphinxDirective):
ast = ASTNamespace(name, None)
symbol = root_symbol.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 []
@ -581,14 +582,12 @@ class CPPNamespacePushObject(SphinxDirective):
logger.warning(e, location=self.get_location())
name = _make_phony_error_name()
ast = ASTNamespace(name, None)
old_parent = self.env.temp_data.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.temp_data.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_namespace_stack.append(symbol)
self.env.current_document.cpp_parent_symbol = symbol
self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key()
return []
@ -601,21 +600,19 @@ class CPPNamespacePopObject(SphinxDirective):
option_spec: ClassVar[OptionSpec] = {}
def run(self) -> list[Node]:
stack = self.env.temp_data.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.temp_data['cpp:parent_symbol'] = symbol
self.env.temp_data['cpp:namespace_stack'] = stack
self.env.current_document.cpp_parent_symbol = symbol
self.env.ref_context['cpp:parent_key'] = symbol.get_lookup_key()
return []
@ -632,9 +629,9 @@ class AliasNode(nodes.Element):
self.sig = sig
self.aliasOptions = aliasOptions
if env is not None:
if 'cpp:parent_symbol' not in env.temp_data:
if env.current_document.cpp_parent_symbol is None:
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:
@ -844,7 +841,7 @@ class CPPAliasObject(ObjectDescription):
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]
@ -912,7 +909,7 @@ class CPPExprRole(SphinxRole):
# see below
node = addnodes.desc_inline('cpp', text, text, classes=[self.class_type])
return [node], []
parent_symbol = self.env.temp_data.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,

View File

@ -6,8 +6,8 @@ import functools
import os
import pickle
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING
from copy import deepcopy
from typing import TYPE_CHECKING, Final
from sphinx import addnodes
from sphinx.domains._domains_container import _DomainsContainer
@ -42,6 +42,8 @@ if TYPE_CHECKING:
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
@ -196,10 +198,10 @@ class BuildEnvironment:
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
@ -426,7 +428,13 @@ class BuildEnvironment:
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)
@ -558,32 +566,42 @@ class BuildEnvironment:
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
@ -726,17 +744,19 @@ class BuildEnvironment:
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)
@ -846,3 +866,230 @@ 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.
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',
'_serial_numbers',
'_extension_data',
'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',
'highlight_language',
'obj_desc_name',
'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',
'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',
'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',
'c:last_symbol',
'c:parent_symbol',
'cpp:last_symbol',
'cpp:parent_symbol',
'default_domain',
})
def __init__(
self,
*,
docname: str = '',
default_role: str = '',
default_domain: Domain | None = None,
) -> None:
#: The docname of the document currently being parsed.
self.docname: str = 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 = ''
#: 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
# Used for generating unique serial numbers.
self._serial_numbers: dict[str, int] = {}
# Stores properties relating to the current document set by extensions.
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 & category.
"""
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 in self.__attr_map:
return getattr(self, self.__attr_map[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._extension_data[key] = value
def __delitem__(self, key: str) -> None:
self.pop(key, default=None)
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._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
__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
else:
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:
return self._extension_data.setdefault(key, default)
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

View File

@ -878,9 +878,9 @@ class Documenter:
*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 self.options.members is ALL
@ -918,8 +918,8 @@ class Documenter:
)
# 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
@ -1261,7 +1261,7 @@ class ModuleLevelDocumenter(Documenter):
# 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')
@ -1287,19 +1287,18 @@ class ClassLevelDocumenter(Documenter):
# 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

View File

@ -38,8 +38,8 @@ def record_typehints(
try:
if callable(obj):
annotations = app.env.temp_data.setdefault('annotations', {})
annotation = 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:
@ -71,7 +71,7 @@ def merge_typehints(
# 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 == []:

View File

@ -66,13 +66,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)

View File

@ -398,7 +398,7 @@ class IntersphinxRole(SphinxRole):
# 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 @@ class IntersphinxRole(SphinxRole):
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:

View File

@ -17,7 +17,7 @@ if TYPE_CHECKING:
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()
@ -37,4 +37,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 = ''

View File

@ -285,7 +285,7 @@ class sphinx_domains(CustomReSTDispatcher):
)
# 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 @@ class SphinxRole:
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:

View File

@ -185,13 +185,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')

View File

@ -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()

View File

@ -95,9 +95,10 @@ def test_parse_name(app):
'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 test_parse_name(app):
)
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) == [
'',

View File

@ -38,7 +38,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()

View File

@ -116,7 +116,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'
@ -155,7 +155,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'