diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py
index be8bf0ccb2e..2ea8cd4f8ff 100644
--- a/sphinx/builders/html/__init__.py
+++ b/sphinx/builders/html/__init__.py
@@ -969,26 +969,24 @@ def has_wildcard(pattern: str) -> bool:
# user sidebar settings
html_sidebars = self.get_builder_config('sidebars', 'html')
- for pattern, patsidebars in html_sidebars.items():
+ msg = __('page %s matches two patterns in html_sidebars: %r and %r')
+ for pattern, pat_sidebars in html_sidebars.items():
if patmatch(pagename, pattern):
- if matched:
- if has_wildcard(pattern):
- # warn if both patterns contain wildcards
- if has_wildcard(matched):
- logger.warning(__('page %s matches two patterns in '
- 'html_sidebars: %r and %r'),
- pagename, matched, pattern)
- # else the already matched pattern is more specific
- # than the present one, because it contains no wildcard
- continue
+ if matched and has_wildcard(pattern):
+ # warn if both patterns contain wildcards
+ if has_wildcard(matched):
+ logger.warning(msg, pagename, matched)
+ # else the already matched pattern is more specific
+ # than the present one, because it contains no wildcard
+ continue
matched = pattern
- sidebars = patsidebars
+ sidebars = pat_sidebars
if len(sidebars) == 0:
# keep defaults
pass
- ctx['sidebars'] = sidebars
+ ctx['sidebars'] = list(sidebars)
# --------- these are overwritten by the serialization builder
diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py
index 41f9df1986c..b9ee24d36e0 100644
--- a/sphinx/domains/python/_object.py
+++ b/sphinx/domains/python/_object.py
@@ -89,6 +89,10 @@ def make_xref(
return result
+ _delimiters_re = re.compile(
+ r'(\s*[\[\]\(\),](?:\s*o[rf]\s)?\s*|\s+o[rf]\s+|\s*\|\s*|\.\.\.)'
+ )
+
def make_xrefs(
self,
rolename: str,
@@ -100,9 +104,7 @@ def make_xrefs(
inliner: Inliner | None = None,
location: Node | None = None,
) -> list[Node]:
- delims = r'(\s*[\[\]\(\),](?:\s*o[rf]\s)?\s*|\s+o[rf]\s+|\s*\|\s*|\.\.\.)'
- delims_re = re.compile(delims)
- sub_targets = re.split(delims, target)
+ sub_targets = self._delimiters_re.split(target)
split_contnode = bool(contnode and contnode.astext() == target)
@@ -112,13 +114,13 @@ def make_xrefs(
if split_contnode:
contnode = nodes.Text(sub_target)
- if in_literal or delims_re.match(sub_target):
+ if in_literal or self._delimiters_re.match(sub_target):
results.append(contnode or innernode(sub_target, sub_target)) # type: ignore[call-arg]
else:
results.append(self.make_xref(rolename, domain, sub_target,
innernode, contnode, env, inliner, location))
- if sub_target in ('Literal', 'typing.Literal', '~typing.Literal'):
+ if sub_target in {'Literal', 'typing.Literal', '~typing.Literal'}:
in_literal = True
return results
diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py
index d6582f4cb23..12ca03a597e 100644
--- a/sphinx/transforms/__init__.py
+++ b/sphinx/transforms/__init__.py
@@ -22,8 +22,10 @@
if TYPE_CHECKING:
from collections.abc import Iterator
+ from typing import Literal
from docutils.nodes import Node, Text
+ from typing_extensions import TypeAlias, TypeIs
from sphinx.application import Sphinx
from sphinx.config import Config
@@ -31,15 +33,22 @@
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import ExtensionMetadata
+ _DEFAULT_SUBSTITUTION_NAMES: TypeAlias = Literal[
+ 'version',
+ 'release',
+ 'today',
+ 'translation progress',
+ ]
+
logger = logging.getLogger(__name__)
-default_substitutions = {
+_DEFAULT_SUBSTITUTIONS = frozenset({
'version',
'release',
'today',
'translation progress',
-}
+})
class SphinxTransform(Transform):
@@ -105,20 +114,25 @@ class DefaultSubstitutions(SphinxTransform):
def apply(self, **kwargs: Any) -> None:
# only handle those not otherwise defined in the document
- to_handle = default_substitutions - set(self.document.substitution_defs)
+ to_handle = _DEFAULT_SUBSTITUTIONS - set(self.document.substitution_defs)
for ref in self.document.findall(nodes.substitution_reference):
- refname = ref['refname']
- if refname in to_handle:
- if refname == 'translation progress':
- # special handling: calculate translation progress
- text = _calculate_translation_progress(self.document)
- else:
- text = self.config[refname]
- if refname == 'today' and not text:
- # special handling: can also specify a strftime format
- text = format_date(self.config.today_fmt or _('%b %d, %Y'),
- language=self.config.language)
- ref.replace_self(nodes.Text(text))
+ if (name := ref['refname']) in to_handle:
+ ref.replace_self(self._handle_default_substitution(name))
+
+ def _handle_default_substitution(self, name: _DEFAULT_SUBSTITUTION_NAMES) -> nodes.Text:
+ if name == 'translation progress':
+ # special handling: calculate translation progress
+ return nodes.Text(_calculate_translation_progress(self.document))
+ if name == 'today':
+ if text := self.config.today:
+ return nodes.Text(text)
+ # special handling: can also specify a strftime format
+ return nodes.Text(format_date(
+ self.config.today_fmt or _('%b %d, %Y'),
+ language=self.config.language,
+ ))
+ # config.version and config.release
+ return nodes.Text(getattr(self.config, name))
def _calculate_translation_progress(document: nodes.document) -> str:
@@ -263,15 +277,15 @@ class ExtraTranslatableNodes(SphinxTransform):
default_priority = 10
def apply(self, **kwargs: Any) -> None:
- targets = self.config.gettext_additional_targets
- target_nodes = [v for k, v in TRANSLATABLE_NODES.items() if k in targets]
+ targets = frozenset(self.config.gettext_additional_targets)
+ target_nodes = tuple(v for k, v in TRANSLATABLE_NODES.items() if k in targets)
if not target_nodes:
return
- def is_translatable_node(node: Node) -> bool:
- return isinstance(node, tuple(target_nodes))
+ def is_translatable_node(node: Node) -> TypeIs[nodes.Element]:
+ return isinstance(node, target_nodes)
- for node in self.document.findall(is_translatable_node): # type: nodes.Element
+ for node in self.document.findall(is_translatable_node):
node['translatable'] = True