mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Simplify `Tags` (#12490)
- Use a set to store the list of tags - Cache evaluated expressions - Extract locally-defined function into a private method
This commit is contained in:
@@ -51,9 +51,9 @@ Important points to note:
|
|||||||
|
|
||||||
* There is a special object named ``tags`` available in the config file.
|
* There is a special object named ``tags`` available in the config file.
|
||||||
It can be used to query and change the tags (see :ref:`tags`). Use
|
It can be used to query and change the tags (see :ref:`tags`). Use
|
||||||
``tags.has('tag')`` to query, ``tags.add('tag')`` and ``tags.remove('tag')``
|
``'tag' in tags`` to query, ``tags.add('tag')`` and ``tags.remove('tag')``
|
||||||
to change. Only tags set via the ``-t`` command-line option or via
|
to change. Only tags set via the ``-t`` command-line option or via
|
||||||
``tags.add('tag')`` can be queried using ``tags.has('tag')``.
|
``tags.add('tag')`` can be queried using ``'tag' in tags``.
|
||||||
Note that the current builder tag is not available in ``conf.py``, as it is
|
Note that the current builder tag is not available in ``conf.py``, as it is
|
||||||
created *after* the builder is initialized.
|
created *after* the builder is initialized.
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class Sphinx:
|
|||||||
buildername: str, confoverrides: dict | None = None,
|
buildername: str, confoverrides: dict | None = None,
|
||||||
status: IO | None = sys.stdout, warning: IO | None = sys.stderr,
|
status: IO | None = sys.stdout, warning: IO | None = sys.stderr,
|
||||||
freshenv: bool = False, warningiserror: bool = False,
|
freshenv: bool = False, warningiserror: bool = False,
|
||||||
tags: list[str] | None = None,
|
tags: Sequence[str] = (),
|
||||||
verbosity: int = 0, parallel: int = 0, keep_going: bool = False,
|
verbosity: int = 0, parallel: int = 0, keep_going: bool = False,
|
||||||
pdb: bool = False) -> None:
|
pdb: bool = False) -> None:
|
||||||
"""Initialize the Sphinx application.
|
"""Initialize the Sphinx application.
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ class Builder:
|
|||||||
self.tags: Tags = app.tags
|
self.tags: Tags = app.tags
|
||||||
self.tags.add(self.format)
|
self.tags.add(self.format)
|
||||||
self.tags.add(self.name)
|
self.tags.add(self.name)
|
||||||
self.tags.add("format_%s" % self.format)
|
self.tags.add(f'format_{self.format}')
|
||||||
self.tags.add("builder_%s" % self.name)
|
self.tags.add(f'builder_{self.name}')
|
||||||
|
|
||||||
# images that need to be copied over (source -> dest)
|
# images that need to be copied over (source -> dest)
|
||||||
self.images: dict[str, str] = {}
|
self.images: dict[str, str] = {}
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ class GettextRenderer(SphinxRenderer):
|
|||||||
class I18nTags(Tags):
|
class I18nTags(Tags):
|
||||||
"""Dummy tags module for I18nBuilder.
|
"""Dummy tags module for I18nBuilder.
|
||||||
|
|
||||||
To translate all text inside of only nodes, this class
|
To ensure that all text inside ``only`` nodes is translated,
|
||||||
always returns True value even if no tags are defined.
|
this class always returns ``True`` regardless the defined tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def eval_condition(self, condition: Any) -> bool:
|
def eval_condition(self, condition: Any) -> bool:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from sphinx.util.console import strip_colors
|
|||||||
from sphinx.util.docutils import additional_nodes
|
from sphinx.util.docutils import additional_nodes
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from xml.etree.ElementTree import ElementTree
|
from xml.etree.ElementTree import ElementTree
|
||||||
@@ -112,7 +112,7 @@ class SphinxTestApp(sphinx.application.Sphinx):
|
|||||||
confoverrides: dict[str, Any] | None = None,
|
confoverrides: dict[str, Any] | None = None,
|
||||||
status: StringIO | None = None,
|
status: StringIO | None = None,
|
||||||
warning: StringIO | None = None,
|
warning: StringIO | None = None,
|
||||||
tags: list[str] | None = None,
|
tags: Sequence[str] = (),
|
||||||
docutils_conf: str | None = None, # extra constructor argument
|
docutils_conf: str | None = None, # extra constructor argument
|
||||||
parallel: int = 0,
|
parallel: int = 0,
|
||||||
# additional arguments at the end to keep the signature
|
# additional arguments at the end to keep the signature
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from jinja2 import nodes
|
import jinja2.environment
|
||||||
from jinja2.environment import Environment
|
import jinja2.nodes
|
||||||
from jinja2.parser import Parser
|
import jinja2.parser
|
||||||
|
|
||||||
|
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator, Sequence
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from jinja2.nodes import Node
|
_ENV = jinja2.environment.Environment()
|
||||||
|
|
||||||
|
|
||||||
env = Environment()
|
class BooleanParser(jinja2.parser.Parser):
|
||||||
|
"""Only allow conditional expressions and binary operators."""
|
||||||
|
|
||||||
|
def parse_compare(self) -> jinja2.nodes.Expr:
|
||||||
class BooleanParser(Parser):
|
node: jinja2.nodes.Expr
|
||||||
"""
|
|
||||||
Only allow condition exprs and/or/not operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def parse_compare(self) -> nodes.Expr:
|
|
||||||
node: nodes.Expr
|
|
||||||
token = self.stream.current
|
token = self.stream.current
|
||||||
if token.type == 'name':
|
if token.type == 'name':
|
||||||
if token.value in ('true', 'false', 'True', 'False'):
|
if token.value in {'true', 'True'}:
|
||||||
node = nodes.Const(token.value in ('true', 'True'),
|
node = jinja2.nodes.Const(True, lineno=token.lineno)
|
||||||
lineno=token.lineno)
|
elif token.value in {'false', 'False'}:
|
||||||
elif token.value in ('none', 'None'):
|
node = jinja2.nodes.Const(False, lineno=token.lineno)
|
||||||
node = nodes.Const(None, lineno=token.lineno)
|
elif token.value in {'none', 'None'}:
|
||||||
|
node = jinja2.nodes.Const(None, lineno=token.lineno)
|
||||||
else:
|
else:
|
||||||
node = nodes.Name(token.value, 'load', lineno=token.lineno)
|
node = jinja2.nodes.Name(token.value, 'load', lineno=token.lineno)
|
||||||
next(self.stream)
|
next(self.stream)
|
||||||
elif token.type == 'lparen':
|
elif token.type == 'lparen':
|
||||||
next(self.stream)
|
next(self.stream)
|
||||||
@@ -42,47 +42,71 @@ class BooleanParser(Parser):
|
|||||||
|
|
||||||
|
|
||||||
class Tags:
|
class Tags:
|
||||||
def __init__(self, tags: list[str] | None = None) -> None:
|
def __init__(self, tags: Sequence[str] = ()) -> None:
|
||||||
self.tags = dict.fromkeys(tags or [], True)
|
self._tags = set(tags or ())
|
||||||
|
self._condition_cache: dict[str, bool] = {}
|
||||||
|
|
||||||
def has(self, tag: str) -> bool:
|
def __str__(self) -> str:
|
||||||
return tag in self.tags
|
return f'{self.__class__.__name__}({", ".join(sorted(self._tags))})'
|
||||||
|
|
||||||
__contains__ = has
|
def __repr__(self) -> str:
|
||||||
|
return f'{self.__class__.__name__}({tuple(sorted(self._tags))})'
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
def __iter__(self) -> Iterator[str]:
|
||||||
return iter(self.tags)
|
return iter(self._tags)
|
||||||
|
|
||||||
|
def __contains__(self, tag: str) -> bool:
|
||||||
|
return tag in self._tags
|
||||||
|
|
||||||
|
def has(self, tag: str) -> bool:
|
||||||
|
return tag in self._tags
|
||||||
|
|
||||||
def add(self, tag: str) -> None:
|
def add(self, tag: str) -> None:
|
||||||
self.tags[tag] = True
|
self._tags.add(tag)
|
||||||
|
|
||||||
def remove(self, tag: str) -> None:
|
def remove(self, tag: str) -> None:
|
||||||
self.tags.pop(tag, None)
|
self._tags.discard(tag)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self) -> dict[str, Literal[True]]:
|
||||||
|
warnings.warn('Tags.tags is deprecated, use methods on Tags.',
|
||||||
|
RemovedInSphinx90Warning, stacklevel=2)
|
||||||
|
return dict.fromkeys(self._tags, True)
|
||||||
|
|
||||||
def eval_condition(self, condition: str) -> bool:
|
def eval_condition(self, condition: str) -> bool:
|
||||||
|
"""Evaluate a boolean condition.
|
||||||
|
|
||||||
|
Only conditional expressions and binary operators (and, or, not)
|
||||||
|
are permitted, and operate on tag names, where truthy values mean
|
||||||
|
the tag is present and vice versa.
|
||||||
|
"""
|
||||||
|
if condition in self._condition_cache:
|
||||||
|
return self._condition_cache[condition]
|
||||||
|
|
||||||
# exceptions are handled by the caller
|
# exceptions are handled by the caller
|
||||||
parser = BooleanParser(env, condition, state='variable')
|
parser = BooleanParser(_ENV, condition, state='variable')
|
||||||
expr = parser.parse_expression()
|
expr = parser.parse_expression()
|
||||||
if not parser.stream.eos:
|
if not parser.stream.eos:
|
||||||
msg = 'chunk after expression'
|
msg = 'chunk after expression'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
def eval_node(node: Node | None) -> bool:
|
evaluated = self._condition_cache[condition] = self._eval_node(expr)
|
||||||
if isinstance(node, nodes.CondExpr):
|
return evaluated
|
||||||
if eval_node(node.test):
|
|
||||||
return eval_node(node.expr1)
|
|
||||||
else:
|
|
||||||
return eval_node(node.expr2)
|
|
||||||
elif isinstance(node, nodes.And):
|
|
||||||
return eval_node(node.left) and eval_node(node.right)
|
|
||||||
elif isinstance(node, nodes.Or):
|
|
||||||
return eval_node(node.left) or eval_node(node.right)
|
|
||||||
elif isinstance(node, nodes.Not):
|
|
||||||
return not eval_node(node.node)
|
|
||||||
elif isinstance(node, nodes.Name):
|
|
||||||
return self.tags.get(node.name, False)
|
|
||||||
else:
|
|
||||||
msg = 'invalid node, check parsing'
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
return eval_node(expr)
|
def _eval_node(self, node: jinja2.nodes.Node | None) -> bool:
|
||||||
|
if isinstance(node, jinja2.nodes.CondExpr):
|
||||||
|
if self._eval_node(node.test):
|
||||||
|
return self._eval_node(node.expr1)
|
||||||
|
else:
|
||||||
|
return self._eval_node(node.expr2)
|
||||||
|
elif isinstance(node, jinja2.nodes.And):
|
||||||
|
return self._eval_node(node.left) and self._eval_node(node.right)
|
||||||
|
elif isinstance(node, jinja2.nodes.Or):
|
||||||
|
return self._eval_node(node.left) or self._eval_node(node.right)
|
||||||
|
elif isinstance(node, jinja2.nodes.Not):
|
||||||
|
return not self._eval_node(node.node)
|
||||||
|
elif isinstance(node, jinja2.nodes.Name):
|
||||||
|
return node.name in self._tags
|
||||||
|
else:
|
||||||
|
msg = 'invalid node, check parsing'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user