Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 16 commits into from
Jan 4, 2025
46 changes: 46 additions & 0 deletions doc/extdev/envapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 __
Expand Down Expand Up @@ -620,7 +625,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()
Expand All @@ -640,7 +645,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()
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
self.env.current_document = _CurrentDocument()
self.env.ref_context.clear()

self.write_doctree(docname, doctree, _cache=_cache)
Expand Down
17 changes: 6 additions & 11 deletions sphinx/directives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.current_document.obj_desc_name = ''
self.after_content()

if node['no-typesetting']:
Expand Down Expand Up @@ -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
Expand All @@ -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 []


Expand Down
7 changes: 4 additions & 3 deletions sphinx/directives/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions sphinx/directives/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
50 changes: 23 additions & 27 deletions sphinx/domains/c/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand All @@ -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.temp_data['c:last_symbol'] = symbol
self.env.current_document.c_last_symbol = symbol
raise ValueError from e

try:
Expand All @@ -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.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'."
Expand All @@ -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.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


Expand Down Expand Up @@ -410,8 +410,8 @@ def run(self) -> list[Node]:
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 []

Expand All @@ -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.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 []

Expand All @@ -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 []

Expand All @@ -488,9 +484,9 @@ def __init__(
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:
Expand Down Expand Up @@ -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.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,
Expand Down
2 changes: 1 addition & 1 deletion sphinx/domains/changeset.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,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
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
changeset = ChangeSet(
node['type'],
self.env.docname,
Expand Down
Loading
Loading