Merge branch '5.x'

# Conflicts:
#	sphinx/locale/__init__.py
This commit is contained in:
Adam Turner 2022-09-30 16:15:24 +01:00
commit 63dea6172a
27 changed files with 184 additions and 73 deletions

View File

@ -1,16 +0,0 @@
version: 2
jobs:
build:
docker:
- image: sphinxdoc/docker-ci
environment:
DO_EPUBCHECK: 1
working_directory: /sphinx
steps:
- checkout
- run: /python3.8/bin/pip install -U pip setuptools
- run: /python3.8/bin/pip install -U .[test]
- run: mkdir -p test-reports/pytest
- run: make test PYTHON=/python3.8/bin/python TEST="--junitxml=test-reports/pytest/results.xml -vv"
- store_test_results:
path: test-reports

24
.github/workflows/latex.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: CI (LaTeX)
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-18.04
name: Python 3.8
container:
image: sphinxdoc/docker-ci
env:
DO_EPUBCHECK: 1
PATH: /python3.8/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
steps:
- uses: actions/checkout@v3
- name: Check Python version
run: python --version
- name: Install dependencies
run: pip install -U pip tox
- name: Run Tox
run: tox -e py -- -vv

10
CHANGES
View File

@ -38,12 +38,22 @@ Deprecated
Features added Features added
-------------- --------------
* #10840: One can cross-reference including an option value: ``:option:`--module=foobar```.
Patch by Martin Liska.
Bugs fixed Bugs fixed
---------- ----------
Testing Testing
-------- --------
Release 5.2.3 (released Sep 30, 2022)
=====================================
* #10878: Fix base64 image embedding in ``sphinx.ext.imgmath``
* #10886: Add ``:nocontentsentry:`` flag and global domain table of contents
entry control option. Patch by Adam Turner
Release 5.2.2 (released Sep 27, 2022) Release 5.2.2 (released Sep 27, 2022)
===================================== =====================================

View File

@ -233,7 +233,7 @@ Keys that you may want to override include:
.. code-block:: python .. code-block:: python
latex_elements = { latex_elements = {
'packages': r'\usepackage{isodate}' 'extrapackages': r'\usepackage{isodate}'
} }
The specified LaTeX packages will be loaded before The specified LaTeX packages will be loaded before

View File

@ -678,6 +678,11 @@ General configuration
:term:`object` names (for object types where a "module" of some kind is :term:`object` names (for object types where a "module" of some kind is
defined), e.g. for :rst:dir:`py:function` directives. Default is ``True``. defined), e.g. for :rst:dir:`py:function` directives. Default is ``True``.
.. confval:: toc_object_entries
Create table of contents entries for domain objects (e.g. functions, classes,
attributes, etc.). Default is ``True``.
.. confval:: toc_object_entries_show_parents .. confval:: toc_object_entries_show_parents
A string that determines how domain objects (e.g. functions, classes, A string that determines how domain objects (e.g. functions, classes,

View File

@ -42,11 +42,15 @@ Basic Markup
Most domains provide a number of :dfn:`object description directives`, used to Most domains provide a number of :dfn:`object description directives`, used to
describe specific objects provided by modules. Each directive requires one or describe specific objects provided by modules. Each directive requires one or
more signatures to provide basic information about what is being described, and more signatures to provide basic information about what is being described, and
the content should be the description. A domain will typically keep an the content should be the description.
internal index of all entities to aid cross-referencing. Typically it will
also add entries in the shown general index. A domain will typically keep an internal index of all entities to aid
cross-referencing.
Typically it will also add entries in the shown general index.
If you want to suppress the addition of an entry in the shown index, you can If you want to suppress the addition of an entry in the shown index, you can
give the directive option flag ``:noindexentry:``. give the directive option flag ``:noindexentry:``.
If you want to exclude the object description from the table of contents, you
can give the directive option flag ``:nocontentsentry:``.
If you want to typeset an object description, without even making it available If you want to typeset an object description, without even making it available
for cross-referencing, you can give the directive option flag ``:noindex:`` for cross-referencing, you can give the directive option flag ``:noindex:``
(which implies ``:noindexentry:``). (which implies ``:noindexentry:``).
@ -57,6 +61,10 @@ options.
The directive option ``noindexentry`` in the Python, C, C++, and Javascript The directive option ``noindexentry`` in the Python, C, C++, and Javascript
domains. domains.
.. versionadded:: 5.2.3
The directive option ``:nocontentsentry:`` in the Python, C, C++, Javascript,
and reStructuredText domains.
An example using a Python domain directive:: An example using a Python domain directive::
.. py:function:: spam(eggs) .. py:function:: spam(eggs)
@ -851,15 +859,19 @@ Example::
This will be rendered as: This will be rendered as:
.. c:struct:: Data .. c:struct:: Data
:nocontentsentry:
:noindexentry: :noindexentry:
.. c:union:: @data .. c:union:: @data
:nocontentsentry:
:noindexentry: :noindexentry:
.. c:var:: int a .. c:var:: int a
:nocontentsentry:
:noindexentry: :noindexentry:
.. c:var:: double b .. c:var:: double b
:nocontentsentry:
:noindexentry: :noindexentry:
Explicit ref: :c:var:`Data.@data.a`. Short-hand ref: :c:var:`Data.a`. Explicit ref: :c:var:`Data.@data.a`. Short-hand ref: :c:var:`Data.a`.
@ -942,9 +954,11 @@ Inline Expressions and Types
will be rendered as follows: will be rendered as follows:
.. c:var:: int a = 42 .. c:var:: int a = 42
:nocontentsentry:
:noindexentry: :noindexentry:
.. c:function:: int f(int i) .. c:function:: int f(int i)
:nocontentsentry:
:noindexentry: :noindexentry:
An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`). An expression: :c:expr:`a * f(a)` (or as text: :c:texpr:`a * f(a)`).
@ -1155,23 +1169,27 @@ visibility statement (``public``, ``private`` or ``protected``).
The example are rendered as follows. The example are rendered as follows.
.. cpp:type:: std::vector<int> MyList .. cpp:type:: std::vector<int> MyList
:noindex: :nocontentsentry:
:noindexentry:
A typedef-like declaration of a type. A typedef-like declaration of a type.
.. cpp:type:: MyContainer::const_iterator .. cpp:type:: MyContainer::const_iterator
:noindex: :nocontentsentry:
:noindexentry:
Declaration of a type alias with unspecified type. Declaration of a type alias with unspecified type.
.. cpp:type:: MyType = std::unordered_map<int, std::string> .. cpp:type:: MyType = std::unordered_map<int, std::string>
:noindex: :nocontentsentry:
:noindexentry:
Declaration of a type alias. Declaration of a type alias.
.. cpp:type:: template<typename T> \ .. cpp:type:: template<typename T> \
MyContainer = std::vector<T> MyContainer = std::vector<T>
:noindex: :nocontentsentry:
:noindexentry:
.. rst:directive:: .. cpp:enum:: unscoped enum declaration .. rst:directive:: .. cpp:enum:: unscoped enum declaration
.. cpp:enum-struct:: scoped enum declaration .. cpp:enum-struct:: scoped enum declaration
@ -1266,7 +1284,7 @@ Options
Some directives support options: Some directives support options:
- ``:noindexentry:``, see :ref:`basic-domain-markup`. - ``:noindexentry:`` and ``:nocontentsentry:``, see :ref:`basic-domain-markup`.
- ``:tparam-line-spec:``, for templated declarations. - ``:tparam-line-spec:``, for templated declarations.
If specified, each template parameter will be rendered on a separate line. If specified, each template parameter will be rendered on a separate line.
@ -1298,15 +1316,19 @@ Example::
This will be rendered as: This will be rendered as:
.. cpp:class:: Data .. cpp:class:: Data
:nocontentsentry:
:noindexentry: :noindexentry:
.. cpp:union:: @data .. cpp:union:: @data
:nocontentsentry:
:noindexentry: :noindexentry:
.. cpp:var:: int a .. cpp:var:: int a
:nocontentsentry:
:noindexentry: :noindexentry:
.. cpp:var:: double b .. cpp:var:: double b
:nocontentsentry:
:noindexentry: :noindexentry:
Explicit ref: :cpp:var:`Data::@data::a`. Short-hand ref: :cpp:var:`Data::a`. Explicit ref: :cpp:var:`Data::@data::a`. Short-hand ref: :cpp:var:`Data::a`.
@ -1413,11 +1435,13 @@ introduction` instead of a template parameter list::
They are rendered as follows. They are rendered as follows.
.. cpp:function:: std::Iterator{It} void advance(It &it) .. cpp:function:: std::Iterator{It} void advance(It &it)
:nocontentsentry:
:noindexentry: :noindexentry:
A function template with a template parameter constrained to be an Iterator. A function template with a template parameter constrained to be an Iterator.
.. cpp:class:: std::LessThanComparable{T} MySortedContainer .. cpp:class:: std::LessThanComparable{T} MySortedContainer
:nocontentsentry:
:noindexentry: :noindexentry:
A class template with a template parameter constrained to be A class template with a template parameter constrained to be
@ -1448,9 +1472,11 @@ Inline Expressions and Types
will be rendered as follows: will be rendered as follows:
.. cpp:var:: int a = 42 .. cpp:var:: int a = 42
:nocontentsentry:
:noindexentry: :noindexentry:
.. cpp:function:: int f(int i) .. cpp:function:: int f(int i)
:nocontentsentry:
:noindexentry: :noindexentry:
An expression: :cpp:expr:`a * f(a)` (or as text: :cpp:texpr:`a * f(a)`). An expression: :cpp:expr:`a * f(a)` (or as text: :cpp:texpr:`a * f(a)`).
@ -1764,6 +1790,10 @@ There is a set of directives allowing documenting command-line programs:
referenceable by :rst:role:`option` (in the example case, you'd use something referenceable by :rst:role:`option` (in the example case, you'd use something
like ``:option:`dest_dir```, ``:option:`-m```, or ``:option:`--module```). like ``:option:`dest_dir```, ``:option:`-m```, or ``:option:`--module```).
.. versionchanged:: 5.3
One can cross-reference including an option value: ``:option:`--module=foobar```.
Use :confval:`option_emphasise_placeholders` for parsing of Use :confval:`option_emphasise_placeholders` for parsing of
"variable part" of a literal text (similarly to the :rst:role:`samp` role). "variable part" of a literal text (similarly to the :rst:role:`samp` role).

View File

@ -183,18 +183,17 @@ strict_optional = false
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = [ module = [
"sphinx.application", "sphinx.application",
"sphinx.builders.*", "sphinx.builders._epub_base",
"sphinx.cmd.*", "sphinx.builders.html",
"sphinx.builders.linkcheck",
"sphinx.cmd.quickstart",
"sphinx.config", "sphinx.config",
"sphinx.deprecation",
"sphinx.domains.*", "sphinx.domains.*",
"sphinx.environment.*", "sphinx.environment.*",
"sphinx.events", "sphinx.events",
"sphinx.ext.*", "sphinx.ext.*",
"sphinx.highlighting", "sphinx.highlighting",
"sphinx.jinja2glue", "sphinx.jinja2glue",
"sphinx.locale",
"sphinx.pycode.*",
"sphinx.registry", "sphinx.registry",
"sphinx.roles", "sphinx.roles",
"sphinx.search.*", "sphinx.search.*",

View File

@ -201,7 +201,7 @@ class HyperlinkAvailabilityChecker:
self.config = config self.config = config
self.env = env self.env = env
self.rate_limits: Dict[str, RateLimit] = {} self.rate_limits: Dict[str, RateLimit] = {}
self.rqueue: Queue = Queue() self.rqueue: Queue[CheckResult] = Queue()
self.workers: List[Thread] = [] self.workers: List[Thread] = []
self.wqueue: PriorityQueue[CheckRequest] = PriorityQueue() self.wqueue: PriorityQueue[CheckRequest] = PriorityQueue()
@ -246,8 +246,8 @@ class HyperlinkAvailabilityChecker:
class HyperlinkAvailabilityCheckWorker(Thread): class HyperlinkAvailabilityCheckWorker(Thread):
"""A worker class for checking the availability of hyperlinks.""" """A worker class for checking the availability of hyperlinks."""
def __init__(self, env: BuildEnvironment, config: Config, rqueue: Queue, def __init__(self, env: BuildEnvironment, config: Config, rqueue: 'Queue[CheckResult]',
wqueue: Queue, rate_limits: Dict[str, RateLimit]) -> None: wqueue: 'Queue[CheckRequest]', rate_limits: Dict[str, RateLimit]) -> None:
self.config = config self.config = config
self.env = env self.env = env
self.rate_limits = rate_limits self.rate_limits = rate_limits
@ -428,7 +428,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
uri, docname, lineno = hyperlink uri, docname, lineno = hyperlink
except ValueError: except ValueError:
# old styled check_request (will be deprecated in Sphinx-5.0) # old styled check_request (will be deprecated in Sphinx-5.0)
next_check, uri, docname, lineno = check_request next_check, uri, docname, lineno = check_request # type: ignore[misc]
if uri is None: if uri is None:
break break

View File

@ -9,7 +9,7 @@ import pdb
import sys import sys
import traceback import traceback
from os import path from os import path
from typing import IO, Any, List, Optional, TextIO from typing import Any, List, Optional, TextIO
from docutils.utils import SystemMessage from docutils.utils import SystemMessage
@ -25,7 +25,7 @@ from sphinx.util.osutil import abspath, ensuredir
def handle_exception( def handle_exception(
app: Optional[Sphinx], args: Any, exception: BaseException, stderr: IO = sys.stderr app: Optional[Sphinx], args: Any, exception: BaseException, stderr: TextIO = sys.stderr
) -> None: ) -> None:
if isinstance(exception, bdb.BdbQuit): if isinstance(exception, bdb.BdbQuit):
return return

View File

@ -177,7 +177,7 @@ class QuickstartRenderer(SphinxRenderer):
else: else:
return False return False
def render(self, template_name: str, context: Dict) -> str: def render(self, template_name: str, context: Dict[str, Any]) -> str:
if self._has_custom_template(template_name): if self._has_custom_template(template_name):
custom_template = path.join(self.templatedir, path.basename(template_name)) custom_template = path.join(self.templatedir, path.basename(template_name))
return self.render_from_file(custom_template, context) return self.render_from_file(custom_template, context)

View File

@ -106,6 +106,7 @@ class Config:
'default_role': (None, 'env', [str]), 'default_role': (None, 'env', [str]),
'add_function_parentheses': (True, 'env', []), 'add_function_parentheses': (True, 'env', []),
'add_module_names': (True, 'env', []), 'add_module_names': (True, 'env', []),
'toc_object_entries': (True, 'env', [bool]),
'toc_object_entries_show_parents': ('domain', 'env', 'toc_object_entries_show_parents': ('domain', 'env',
ENUM('domain', 'all', 'hide')), ENUM('domain', 'all', 'hide')),
'trim_footnote_reference_space': (False, 'env', []), 'trim_footnote_reference_space': (False, 'env', []),

View File

@ -52,10 +52,10 @@ class _ModuleWrapper:
return self._objects[name] return self._objects[name]
class DeprecatedDict(dict): class DeprecatedDict(Dict[str, Any]):
"""A deprecated dict which warns on each access.""" """A deprecated dict which warns on each access."""
def __init__(self, data: Dict, message: str, warning: Type[Warning]) -> None: def __init__(self, data: Dict[str, Any], message: str, warning: Type[Warning]) -> None:
self.message = message self.message = message
self.warning = warning self.warning = warning
super().__init__(data) super().__init__(data)
@ -68,7 +68,7 @@ class DeprecatedDict(dict):
warnings.warn(self.message, self.warning, stacklevel=2) warnings.warn(self.message, self.warning, stacklevel=2)
return super().setdefault(key, default) return super().setdefault(key, default)
def __getitem__(self, key: str) -> None: def __getitem__(self, key: str) -> Any:
warnings.warn(self.message, self.warning, stacklevel=2) warnings.warn(self.message, self.warning, stacklevel=2)
return super().__getitem__(key) return super().__getitem__(key)
@ -76,6 +76,6 @@ class DeprecatedDict(dict):
warnings.warn(self.message, self.warning, stacklevel=2) warnings.warn(self.message, self.warning, stacklevel=2)
return super().get(key, default) return super().get(key, default)
def update(self, other: Dict) -> None: # type: ignore def update(self, other: Dict[str, Any]) -> None: # type: ignore
warnings.warn(self.message, self.warning, stacklevel=2) warnings.warn(self.message, self.warning, stacklevel=2)
super().update(other) super().update(other)

View File

@ -51,6 +51,8 @@ class ObjectDescription(SphinxDirective, Generic[T]):
final_argument_whitespace = True final_argument_whitespace = True
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag, 'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
} }
# types of doc fields that this directive handles, see sphinx.util.docfields # types of doc fields that this directive handles, see sphinx.util.docfields
@ -211,6 +213,7 @@ class ObjectDescription(SphinxDirective, Generic[T]):
node['objtype'] = node['desctype'] = self.objtype node['objtype'] = node['desctype'] = self.objtype
node['noindex'] = noindex = ('noindex' in self.options) node['noindex'] = noindex = ('noindex' in self.options)
node['noindexentry'] = ('noindexentry' in self.options) node['noindexentry'] = ('noindexentry' in self.options)
node['nocontentsentry'] = ('nocontentsentry' in self.options)
if self.domain: if self.domain:
node['classes'].append(self.domain) node['classes'].append(self.domain)
node['classes'].append(node['objtype']) node['classes'].append(node['objtype'])
@ -236,8 +239,12 @@ class ObjectDescription(SphinxDirective, Generic[T]):
finally: finally:
# Private attributes for ToC generation. Will be modified or removed # Private attributes for ToC generation. Will be modified or removed
# without notice. # without notice.
if self.env.app.config.toc_object_entries:
signode['_toc_parts'] = self._object_hierarchy_parts(signode) signode['_toc_parts'] = self._object_hierarchy_parts(signode)
signode['_toc_name'] = self._toc_entry_name(signode) signode['_toc_name'] = self._toc_entry_name(signode)
else:
signode['_toc_parts'] = ()
signode['_toc_name'] = ''
if name not in self.names: if name not in self.names:
self.names.append(name) self.names.append(name)
if not noindex: if not noindex:

View File

@ -3142,8 +3142,8 @@ class CObject(ObjectDescription[ASTDeclaration]):
""" """
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag,
'noindexentry': directives.flag, 'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
} }
def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None:

View File

@ -7186,8 +7186,8 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
] ]
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag,
'noindexentry': directives.flag, 'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'tparam-line-spec': directives.flag, 'tparam-line-spec': directives.flag,
} }

View File

@ -40,6 +40,7 @@ class JSObject(ObjectDescription[Tuple[str, str]]):
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag, 'noindex': directives.flag,
'noindexentry': directives.flag, 'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
} }
def get_display_prefix(self) -> List[Node]: def get_display_prefix(self) -> List[Node]:
@ -284,7 +285,8 @@ class JSModule(SphinxDirective):
optional_arguments = 0 optional_arguments = 0
final_argument_whitespace = False final_argument_whitespace = False
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag 'noindex': directives.flag,
'nocontentsentry': directives.flag,
} }
def run(self) -> List[Node]: def run(self) -> List[Node]:

View File

@ -412,6 +412,7 @@ class PyObject(ObjectDescription[Tuple[str, str]]):
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag, 'noindex': directives.flag,
'noindexentry': directives.flag, 'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'module': directives.unchanged, 'module': directives.unchanged,
'canonical': directives.unchanged, 'canonical': directives.unchanged,
'annotation': directives.unchanged, 'annotation': directives.unchanged,
@ -978,6 +979,7 @@ class PyModule(SphinxDirective):
'platform': lambda x: x, 'platform': lambda x: x,
'synopsis': lambda x: x, 'synopsis': lambda x: x,
'noindex': directives.flag, 'noindex': directives.flag,
'nocontentsentry': directives.flag,
'deprecated': directives.flag, 'deprecated': directives.flag,
} }

View File

@ -31,6 +31,7 @@ class ReSTMarkup(ObjectDescription[str]):
option_spec: OptionSpec = { option_spec: OptionSpec = {
'noindex': directives.flag, 'noindex': directives.flag,
'noindexentry': directives.flag, 'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
} }
def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None: def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:

View File

@ -780,6 +780,8 @@ class StandardDomain(Domain):
self.labels[name] = docname, labelid, sectname self.labels[name] = docname, labelid, sectname
def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None: def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
# prefer first command option entry
if (program, name) not in self.progoptions:
self.progoptions[program, name] = (docname, labelid) self.progoptions[program, name] = (docname, labelid)
def build_reference_node(self, fromdocname: str, builder: "Builder", docname: str, def build_reference_node(self, fromdocname: str, builder: "Builder", docname: str,
@ -941,6 +943,10 @@ class StandardDomain(Domain):
progname = node.get('std:program') progname = node.get('std:program')
target = target.strip() target = target.strip()
docname, labelid = self.progoptions.get((progname, target), ('', '')) docname, labelid = self.progoptions.get((progname, target), ('', ''))
# for :option:`-foo=bar` search for -foo option directive
if not docname and '=' in target:
target2 = target[:target.find('=')]
docname, labelid = self.progoptions.get((progname, target2), ('', ''))
if not docname: if not docname:
commands = [] commands = []
while ws_re.search(target): while ws_re.search(target):

View File

@ -112,9 +112,12 @@ class TocTreeCollector(EnvironmentCollector):
# Skip if no name set # Skip if no name set
if not sig_node.get('_toc_name', ''): if not sig_node.get('_toc_name', ''):
continue continue
# Skip if explicitly disabled
if sig_node.parent.get('nocontentsentry'):
continue
# Skip entries with no ID (e.g. with :noindex: set) # Skip entries with no ID (e.g. with :noindex: set)
ids = sig_node['ids'] ids = sig_node['ids']
if not ids or sig_node.parent.get('noindexentry'): if not ids:
continue continue
anchorname = _make_anchor_name(ids, numentries) anchorname = _make_anchor_name(ids, numentries)

View File

@ -308,7 +308,7 @@ def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
raise nodes.SkipNode from exc raise nodes.SkipNode from exc
if self.builder.config.imgmath_embed: if self.builder.config.imgmath_embed:
image_format = self.builder.config.imgmath_image_format.lower() image_format = self.builder.config.imgmath_image_format.lower()
img_src = render_maths_to_base64(image_format, outfn) img_src = render_maths_to_base64(image_format, imgpath)
else: else:
# Move generated image on tempdir to build dir # Move generated image on tempdir to build dir
if imgpath is not None: if imgpath is not None:
@ -350,7 +350,7 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
self.body.append('</span>') self.body.append('</span>')
if self.builder.config.imgmath_embed: if self.builder.config.imgmath_embed:
image_format = self.builder.config.imgmath_image_format.lower() image_format = self.builder.config.imgmath_image_format.lower()
img_src = render_maths_to_base64(image_format, outfn) img_src = render_maths_to_base64(image_format, imgpath)
else: else:
# Move generated image on tempdir to build dir # Move generated image on tempdir to build dir
if imgpath is not None: if imgpath is not None:

View File

@ -1,9 +1,8 @@
"""Locale utilities.""" """Locale utilities."""
import gettext
import locale import locale
from collections import defaultdict from gettext import NullTranslations, translation
from gettext import NullTranslations from os import path
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
@ -17,7 +16,7 @@ class _TranslationProxy:
""" """
__slots__ = ('_func', '_args') __slots__ = ('_func', '_args')
def __new__(cls, func: Callable, *args: str) -> "_TranslationProxy": def __new__(cls, func: Callable[..., str], *args: str) -> '_TranslationProxy':
if not args: if not args:
# not called with "function" and "arguments", but a plain string # not called with "function" and "arguments", but a plain string
return str(func) # type: ignore[return-value] return str(func) # type: ignore[return-value]
@ -26,7 +25,7 @@ class _TranslationProxy:
def __getnewargs__(self) -> Tuple[str]: def __getnewargs__(self) -> Tuple[str]:
return (self._func,) + self._args # type: ignore return (self._func,) + self._args # type: ignore
def __init__(self, func: Callable, *args: str) -> None: def __init__(self, func: Callable[..., str], *args: str) -> None:
self._func = func self._func = func
self._args = args self._args = args
@ -39,13 +38,13 @@ class _TranslationProxy:
def __getattr__(self, name: str) -> Any: def __getattr__(self, name: str) -> Any:
return getattr(self.__str__(), name) return getattr(self.__str__(), name)
def __getstate__(self) -> Tuple[Callable, Tuple[str, ...]]: def __getstate__(self) -> Tuple[Callable[..., str], Tuple[str, ...]]:
return self._func, self._args return self._func, self._args
def __setstate__(self, tup: Tuple[Callable, Tuple[str]]) -> None: def __setstate__(self, tup: Tuple[Callable[..., str], Tuple[str]]) -> None:
self._func, self._args = tup self._func, self._args = tup
def __copy__(self) -> "_TranslationProxy": def __copy__(self) -> '_TranslationProxy':
return _TranslationProxy(self._func, *self._args) return _TranslationProxy(self._func, *self._args)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -91,11 +90,15 @@ class _TranslationProxy:
return self.__str__()[index] return self.__str__()[index]
translators: Dict[Tuple[str, str], NullTranslations] = defaultdict(NullTranslations) translators: Dict[Tuple[str, str], NullTranslations] = {}
def init(locale_dirs: List[Optional[str]], language: Optional[str], def init(
catalog: str = 'sphinx', namespace: str = 'general') -> Tuple[NullTranslations, bool]: locale_dirs: List[Optional[str]],
language: Optional[str],
catalog: str = 'sphinx',
namespace: str = 'general',
) -> Tuple[NullTranslations, bool]:
"""Look for message catalogs in `locale_dirs` and *ensure* that there is at """Look for message catalogs in `locale_dirs` and *ensure* that there is at
least a NullTranslations catalog set in `translators`. If called multiple least a NullTranslations catalog set in `translators`. If called multiple
times or if several ``.mo`` files are found, their contents are merged times or if several ``.mo`` files are found, their contents are merged
@ -120,7 +123,7 @@ def init(locale_dirs: List[Optional[str]], language: Optional[str],
# loading # loading
for dir_ in locale_dirs: for dir_ in locale_dirs:
try: try:
trans = gettext.translation(catalog, localedir=dir_, languages=languages) trans = translation(catalog, localedir=dir_, languages=languages)
if translator is None: if translator is None:
translator = trans translator = trans
else: else:
@ -148,7 +151,7 @@ def setlocale(category: int, value: Union[str, Iterable[str], None] = None) -> N
* https://bugs.python.org/issue18378#msg215215 * https://bugs.python.org/issue18378#msg215215
.. note:: Only for internal use. Please don't call this method from extensions. .. note:: Only for internal use. Please don't call this method from extensions.
This will be removed in future. This will be removed in Sphinx 6.0.
""" """
try: try:
locale.setlocale(category, value) locale.setlocale(category, value)
@ -156,7 +159,13 @@ def setlocale(category: int, value: Union[str, Iterable[str], None] = None) -> N
pass pass
def init_console(locale_dir: str, catalog: str) -> Tuple[NullTranslations, bool]: _LOCALE_DIR = path.abspath(path.dirname(__file__))
def init_console(
locale_dir: str = _LOCALE_DIR,
catalog: str = 'sphinx',
) -> Tuple[NullTranslations, bool]:
"""Initialize locale for console. """Initialize locale for console.
.. versionadded:: 1.8 .. versionadded:: 1.8
@ -172,7 +181,7 @@ def init_console(locale_dir: str, catalog: str) -> Tuple[NullTranslations, bool]
def get_translator(catalog: str = 'sphinx', namespace: str = 'general') -> NullTranslations: def get_translator(catalog: str = 'sphinx', namespace: str = 'general') -> NullTranslations:
return translators[(namespace, catalog)] return translators.get((namespace, catalog), NullTranslations())
def is_translator_registered(catalog: str = 'sphinx', namespace: str = 'general') -> bool: def is_translator_registered(catalog: str = 'sphinx', namespace: str = 'general') -> bool:

View File

@ -5,9 +5,8 @@ import tokenize
from collections import OrderedDict from collections import OrderedDict
from importlib import import_module from importlib import import_module
from inspect import Signature from inspect import Signature
from io import StringIO
from os import path from os import path
from typing import IO, Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from zipfile import ZipFile from zipfile import ZipFile
from sphinx.errors import PycodeError from sphinx.errors import PycodeError
@ -76,7 +75,7 @@ class ModuleAnalyzer:
@classmethod @classmethod
def for_string(cls, string: str, modname: str, srcname: str = '<string>' def for_string(cls, string: str, modname: str, srcname: str = '<string>'
) -> "ModuleAnalyzer": ) -> "ModuleAnalyzer":
return cls(StringIO(string), modname, srcname) return cls(string, modname, srcname)
@classmethod @classmethod
def for_file(cls, filename: str, modname: str) -> "ModuleAnalyzer": def for_file(cls, filename: str, modname: str) -> "ModuleAnalyzer":
@ -84,7 +83,8 @@ class ModuleAnalyzer:
return cls.cache['file', filename] return cls.cache['file', filename]
try: try:
with tokenize.open(filename) as f: with tokenize.open(filename) as f:
obj = cls(f, modname, filename) string = f.read()
obj = cls(string, modname, filename)
cls.cache['file', filename] = obj cls.cache['file', filename] = obj
except Exception as err: except Exception as err:
if '.egg' + path.sep in filename: if '.egg' + path.sep in filename:
@ -124,12 +124,12 @@ class ModuleAnalyzer:
cls.cache['module', modname] = obj cls.cache['module', modname] = obj
return obj return obj
def __init__(self, source: IO, modname: str, srcname: str) -> None: def __init__(self, source: str, modname: str, srcname: str) -> None:
self.modname = modname # name of the module self.modname = modname # name of the module
self.srcname = srcname # name of the source file self.srcname = srcname # name of the source file
# cache the source code as well # cache the source code as well
self.code = source.read() self.code = source
self._analyzed = False self._analyzed = False

View File

@ -463,7 +463,7 @@ class DefinitionFinder(TokenProcessor):
super().__init__(lines) super().__init__(lines)
self.decorator: Optional[Token] = None self.decorator: Optional[Token] = None
self.context: List[str] = [] self.context: List[str] = []
self.indents: List = [] self.indents: List[Tuple[str, Optional[str], Optional[int]]] = []
self.definitions: Dict[str, Tuple[str, int, int]] = {} self.definitions: Dict[str, Tuple[str, int, int]] = {}
def add_definition(self, name: str, entry: Tuple[str, int, int]) -> None: def add_definition(self, name: str, entry: Tuple[str, int, int]) -> None:

View File

@ -204,6 +204,19 @@ Link to :option:`hg commit` and :option:`git commit -p`.
Foo bar. Foo bar.
Test repeated option directive.
.. option:: -mapi
My API.
.. option:: -mapi=secret
My secret API.
Reference the first option :option:`-mapi=secret`.
User markup User markup
=========== ===========

View File

@ -1709,6 +1709,15 @@ def test_option_emphasise_placeholders_default(app, status, warning):
'<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Permalink to this definition">¶</a></dt>') in content '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Permalink to this definition">¶</a></dt>') in content
@pytest.mark.sphinx('html', testroot='root')
def test_option_reference_with_value(app, status, warning):
app.build()
content = (app.outdir / 'objects.html').read_text()
assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">'
'</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content
assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content
@pytest.mark.sphinx('html', testroot='theming') @pytest.mark.sphinx('html', testroot='theming')
def test_theme_options(app, status, warning): def test_theme_options(app, status, warning):
app.build() app.build()

View File

@ -1,6 +1,7 @@
"""Test math extensions.""" """Test math extensions."""
import re import re
import shutil
import subprocess import subprocess
import warnings import warnings
@ -33,6 +34,7 @@ def test_imgmath_png(app, status, warning):
raise pytest.skip.Exception('dvipng command "dvipng" is not available') raise pytest.skip.Exception('dvipng command "dvipng" is not available')
content = (app.outdir / 'index.html').read_text(encoding='utf8') content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.png"' html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.png"'
r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>')
assert re.search(html, content, re.S) assert re.search(html, content, re.S)
@ -51,6 +53,7 @@ def test_imgmath_svg(app, status, warning):
raise pytest.skip.Exception('dvisvgm command "dvisvgm" is not available') raise pytest.skip.Exception('dvisvgm command "dvisvgm" is not available')
content = (app.outdir / 'index.html').read_text(encoding='utf8') content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.svg"' html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.svg"'
r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>')
assert re.search(html, content, re.S) assert re.search(html, content, re.S)
@ -70,6 +73,7 @@ def test_imgmath_svg_embed(app, status, warning):
pytest.skip('dvisvgm command "dvisvgm" is not available') pytest.skip('dvisvgm command "dvisvgm" is not available')
content = (app.outdir / 'index.html').read_text(encoding='utf8') content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
html = r'<img src="data:image/svg\+xml;base64,[\w\+/=]+"' html = r'<img src="data:image/svg\+xml;base64,[\w\+/=]+"'
assert re.search(html, content, re.DOTALL) assert re.search(html, content, re.DOTALL)
@ -81,6 +85,7 @@ def test_mathjax_options(app, status, warning):
app.builder.build_all() app.builder.build_all()
content = (app.outdir / 'index.html').read_text(encoding='utf8') content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
assert ('<script async="async" integrity="sha384-0123456789" ' assert ('<script async="async" integrity="sha384-0123456789" '
'src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">' 'src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">'
'</script>' in content) '</script>' in content)
@ -92,6 +97,7 @@ def test_mathjax_align(app, status, warning):
app.builder.build_all() app.builder.build_all()
content = (app.outdir / 'index.html').read_text(encoding='utf8') content = (app.outdir / 'index.html').read_text(encoding='utf8')
shutil.rmtree(app.outdir)
html = (r'<div class="math notranslate nohighlight">\s*' html = (r'<div class="math notranslate nohighlight">\s*'
r'\\\[ \\begin\{align\}\\begin\{aligned\}S \&amp;= \\pi r\^2\\\\' r'\\\[ \\begin\{align\}\\begin\{aligned\}S \&amp;= \\pi r\^2\\\\'
r'V \&amp;= \\frac\{4\}\{3\} \\pi r\^3\\end\{aligned\}\\end\{align\} \\\]</div>') r'V \&amp;= \\frac\{4\}\{3\} \\pi r\^3\\end\{aligned\}\\end\{align\} \\\]</div>')