Merge branch 'master' into latex_font_for_pdflatex

This commit is contained in:
jfbu 2021-01-23 09:13:08 +01:00
commit 2e22bd0a23
34 changed files with 556 additions and 204 deletions

27
CHANGES
View File

@ -17,6 +17,8 @@ Incompatible changes
MathJax configuration may have to set the old MathJax path or update their
configuration for version 3. See :mod:`sphinx.ext.mathjax`.
* #7784: i18n: The msgid for alt text of image is changed
* #5560: napoleon: :confval:`napoleon_use_param` also affect "other parameters"
section
* #7996: manpage: Make a section directory on build manpage by default (see
:confval:`man_make_section_directory`)
* #8380: html search: search results are wrapped with ``<p>`` instead of
@ -75,9 +77,16 @@ Incompatible changes
Deprecated
----------
* pending_xref node for viewcode extension
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected``
* ``sphinx.builders.linkcheck.node_line_or_0()``
* ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()``
* ``sphinx.ext.autodoc.directive.DocumenterBridge.reporter``
* ``sphinx.ext.autodoc.importer.get_module_members()``
* ``sphinx.ext.autosummary.generate._simple_info()``
* ``sphinx.ext.autosummary.generate._simple_warn()``
Features added
--------------
@ -85,6 +94,8 @@ Features added
* #8022: autodoc: autodata and autoattribute directives does not show right-hand
value of the variable if docstring contains ``:meta hide-value:`` in
info-field-list
* #8514: autodoc: Default values of overloaded functions are taken from actual
implementation if they're ellipsis
* #8619: html: kbd role generates customizable HTML tags for compound keys
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()`
@ -93,12 +104,16 @@ Features added
:event:`html-page-context` event
* #8649: imgconverter: Skip availability check if builder supports the image
type
* #8573: napoleon: Allow to change the style of custom sections using
:confval:`napoleon_custom_styles`
* #6241: mathjax: Include mathjax.js only on the document using equations
* #8651: std domain: cross-reference for a rubric having inline item is broken
* #8681: viewcode: Support incremental build
* #8132: Add :confval:`project_copyright` as an alias of :confval:`copyright`
* #207: Now :confval:`highlight_language` supports multiple languages
* #2030: :rst:dir:`code-block` and :rst:dir:`literalinclude` supports automatic
dedent via no-argument ``:dedent:`` option
* C++, also hyperlink operator overloads in expressions and alias declarations.
Bugs fixed
----------
@ -110,19 +125,27 @@ Bugs fixed
* #8315: autodoc: Failed to resolve struct.Struct type annotation
* #8652: autodoc: All variable comments in the module are ignored if the module
contains invalid type comments
* #8693: autodoc: Default values for overloaded functions are rendered as string
* #8306: autosummary: mocked modules are documented as empty page when using
:recursive: option
* #8618: html: kbd role produces incorrect HTML when compound-key separators (-,
+ or ^) are used as keystrokes
* #8629: html: A type warning for html_use_opensearch is shown twice
* #8714: html: kbd role with "Caps Lock" rendered incorrectly
* #8665: html theme: Could not override globaltoc_maxdepth in theme.conf
* #4304: linkcheck: Fix race condition that could lead to checking the
availability of the same URL twice
* #8094: texinfo: image files on the different directory with document are not
copied
* #8720: viewcode: module pages are generated for epub on incremental build
* #8704: viewcode: anchors are generated in incremental build after singlehtml
* #8671: :confval:`highlight_options` is not working
* #8341: C, fix intersphinx lookup types for names in declarations.
* C, C++: in general fix intersphinx and role lookup types.
* #8683: :confval:`html_last_updated_fmt` does not support UTC offset (%z)
* #8683: :confval:`html_last_updated_fmt` generates wrong time zone for %Z
* #1112: ``download`` role creates duplicated copies when relative path is
specified
Testing
--------
@ -145,6 +168,10 @@ Features added
Bugs fixed
----------
* #8655: autodoc: Failed to generate document if target module contains an
object that raises an exception on ``hasattr()``
* C, ``expr`` role should start symbol lookup in the current scope.
Testing
--------

View File

@ -2,10 +2,12 @@
Extending Sphinx
================
This guide is aimed at those wishing to develop their own extensions for
Sphinx. Sphinx possesses significant extensibility capabilities including the
ability to hook into almost every point of the build process. If you simply
wish to use Sphinx with existing extensions, refer to :doc:`/usage/index`.
This guide is aimed at giving a quick introduction for those wishing to
develop their own extensions for Sphinx. Sphinx possesses significant
extensibility capabilities including the ability to hook into almost every
point of the build process. If you simply wish to use Sphinx with existing
extensions, refer to :doc:`/usage/index`. For a more detailed discussion of
the extension interface see :doc:`/extdev/index`.
.. toctree::
:maxdepth: 2

View File

@ -61,6 +61,31 @@ The following is a list of deprecated interfaces.
- 6.0
- ``docutils.utils.smartyquotes``
* - pending_xref node for viewcode extension
- 3.5
- 5.0
- ``sphinx.ext.viewcode.viewcode_anchor``
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected``
- 3.5
- 5.0
- N/A
* - ``sphinx.builders.linkcheck.node_line_or_0()``
- 3.5
- 5.0
- ``sphinx.util.nodes.get_node_line()``
* - ``sphinx.ext.autodoc.AttributeDocumenter.isinstanceattribute()``
- 3.5
- 5.0
@ -71,6 +96,16 @@ The following is a list of deprecated interfaces.
- 5.0
- ``sphinx.ext.autodoc.ModuleDocumenter.get_module_members()``
* - ``sphinx.ext.autosummary.generate._simple_info()``
- 3.5
- 5.0
- :ref:`logging-api`
* - ``sphinx.ext.autosummary.generate._simple_warn()``
- 3.5
- 5.0
- :ref:`logging-api`
* - The ``follow_wrapped`` argument of ``sphinx.util.inspect.signature()``
- 3.4
- 5.0

View File

@ -171,8 +171,8 @@ Keys that you may want to override include:
"Bjornstrup". You can also set this to ``''`` to disable fncychap.
Default: ``'\\usepackage[Bjarne]{fncychap}'`` for English documents,
``'\\usepackage[Sonny]{fncychap}'`` for internationalized documents, and
``''`` for Japanese documents.
``'\\usepackage[Sonny]{fncychap}'`` for internationalized documents, and
``''`` for Japanese documents.
``'preamble'``
Additional preamble content. One may move all needed macros into some file
@ -276,7 +276,7 @@ Keys that don't need to be overridden unless in special cases are:
"inputenc" package inclusion.
Default: ``'\\usepackage[utf8]{inputenc}'`` when using pdflatex, else
``''``
``''``
.. versionchanged:: 1.4.3
Previously ``'\\usepackage[utf8]{inputenc}'`` was used for all
@ -360,8 +360,8 @@ Keys that don't need to be overridden unless in special cases are:
``'pdflatex'`` support Greek Unicode input in :rst:dir:`math` context.
For example ``:math:`α``` (U+03B1) will render as :math:`\alpha`.
For wider Unicode support in math input, see the discussion of
:confval:`latex_engine`.
Default: ``'\\usepackage{textalpha}'`` or ``''`` if ``fontenc`` does not
include the ``LGR`` option.
.. versionadded:: 2.0
@ -379,7 +379,7 @@ Keys that don't need to be overridden unless in special cases are:
<latexsphinxsetup>`.
Default: ``'\\usepackage{geometry}'`` (or
``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents)
``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents)
.. versionadded:: 1.5
@ -762,14 +762,14 @@ macros may be significant.
|warningbdcolors|
The colour for the admonition frame.
Default: ``{rgb}{0,0,0}`` (black)
Default: ``{rgb}{0,0,0}`` (black)
.. only:: latex
|wgbdcolorslatex|
The colour for the admonition frame.
Default: ``{rgb}{0,0,0}`` (black)
Default: ``{rgb}{0,0,0}`` (black)
|warningbgcolors|
The background colours for the respective admonitions.

View File

@ -547,3 +547,27 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`::
has an annotation in the class body, that type is used.
.. versionadded:: 3.4
.. confval:: napoleon_custom_sections
Add a list of custom sections to include, expanding the list of parsed sections.
*Defaults to None.*
The entries can either be strings or tuples, depending on the intention:
* To create a custom "generic" section, just pass a string.
* To create an alias for an existing section, pass a tuple containing the
alias name and the original, in that order.
* To create a custom section that displays like the parameters or returns
section, pass a tuple containing the custom section name and a string
value, "params_style" or "returns_style".
If an entry is just a string, it is interpreted as a header for a generic
section. If the entry is a tuple/list/indexed container, the first entry
is the name of the section, the second is the section key to emulate. If the
second entry value is "params_style" or "returns_style", the custom section
will be displayed like the parameters section or returns section.
.. versionadded:: 1.8
.. versionchanged:: 3.5
Support ``params_style`` and ``returns_style``

View File

@ -44,7 +44,7 @@ extras_require = {
'lint': [
'flake8>=3.5.0',
'isort',
'mypy>=0.790',
'mypy>=0.800',
'docutils-stubs',
],
'test': [

View File

@ -88,7 +88,7 @@ class Stylesheet(str):
def __new__(cls, filename: str, *args: str, priority: int = 500, **attributes: Any
) -> "Stylesheet":
self = str.__new__(cls, filename) # type: ignore
self = str.__new__(cls, filename)
self.filename = filename
self.priority = priority
self.attributes = attributes
@ -113,7 +113,7 @@ class JavaScript(str):
priority = None # type: int
def __new__(cls, filename: str, priority: int = 500, **attributes: str) -> "JavaScript":
self = str.__new__(cls, filename) # type: ignore
self = str.__new__(cls, filename)
self.filename = filename
self.priority = priority
self.attributes = attributes

View File

@ -9,7 +9,7 @@
"""
import re
from typing import Any, Dict
from typing import Any, Dict, List
from docutils import nodes
@ -38,18 +38,29 @@ class KeyboardTransform(SphinxPostTransform):
default_priority = 400
builders = ('html',)
pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)')
multiwords_keys = (('caps', 'lock'),
('page' 'down'),
('page', 'up'),
('scroll' 'lock'),
('num', 'lock'),
('sys' 'rq'),
('back' 'space'))
def run(self, **kwargs: Any) -> None:
matcher = NodeMatcher(nodes.literal, classes=["kbd"])
for node in self.document.traverse(matcher): # type: nodes.literal
parts = self.pattern.split(node[-1].astext())
if len(parts) == 1:
if len(parts) == 1 or self.is_multiwords_key(parts):
continue
node['classes'].append('compound')
node.pop()
while parts:
key = parts.pop(0)
if self.is_multiwords_key(parts):
key = ''.join(parts[:3])
parts[:3] = []
else:
key = parts.pop(0)
node += nodes.literal('', key, classes=["kbd"])
try:
@ -59,6 +70,16 @@ class KeyboardTransform(SphinxPostTransform):
except IndexError:
pass
def is_multiwords_key(self, parts: List[str]) -> bool:
if len(parts) >= 3 and parts[1].strip() == '':
name = parts[0].lower(), parts[2].lower()
if name in self.multiwords_keys:
return True
else:
return False
else:
return False
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_post_transform(KeyboardTransform)

View File

@ -14,21 +14,24 @@ import re
import socket
import threading
import time
import warnings
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from html.parser import HTMLParser
from os import path
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple, cast
from urllib.parse import unquote, urlparse
from docutils import nodes
from docutils.nodes import Element, Node
from docutils.nodes import Element
from requests import Response
from requests.exceptions import HTTPError, TooManyRedirects
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.builders.dummy import DummyBuilder
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.locale import __
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import encode_uri, logging, requests
from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise # type: ignore
from sphinx.util.nodes import get_node_line
@ -37,6 +40,10 @@ logger = logging.getLogger(__name__)
uri_re = re.compile('([a-z]+:)?//') # matches to foo:// and // (a protocol relative URL)
Hyperlink = NamedTuple('Hyperlink', (('next_check', float),
('uri', Optional[str]),
('docname', Optional[str]),
('lineno', Optional[int])))
RateLimit = NamedTuple('RateLimit', (('delay', float), ('next_check', float)))
DEFAULT_REQUEST_HEADERS = {
@ -52,6 +59,8 @@ def node_line_or_0(node: Element) -> int:
PriorityQueue items must be comparable. The line number is part of the
tuple used by the PriorityQueue, keep an homogeneous type for comparison.
"""
warnings.warn('node_line_or_0() is deprecated.',
RemovedInSphinx50Warning, stacklevel=2)
return get_node_line(node) or 0
@ -89,7 +98,7 @@ def check_anchor(response: requests.requests.Response, anchor: str) -> bool:
return parser.found
class CheckExternalLinksBuilder(Builder):
class CheckExternalLinksBuilder(DummyBuilder):
"""
Checks for broken external links.
"""
@ -98,14 +107,15 @@ class CheckExternalLinksBuilder(Builder):
'%(outdir)s/output.txt')
def init(self) -> None:
self.hyperlinks = {} # type: Dict[str, Hyperlink]
self.to_ignore = [re.compile(x) for x in self.app.config.linkcheck_ignore]
self.anchors_ignore = [re.compile(x)
for x in self.app.config.linkcheck_anchors_ignore]
self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info
in self.app.config.linkcheck_auth]
self.good = set() # type: Set[str]
self.broken = {} # type: Dict[str, str]
self.redirected = {} # type: Dict[str, Tuple[str, int]]
self._good = set() # type: Set[str]
self._broken = {} # type: Dict[str, str]
self._redirected = {} # type: Dict[str, Tuple[str, int]]
# set a timeout for non-responding servers
socket.setdefaulttimeout(5.0)
# create output file
@ -123,6 +133,33 @@ class CheckExternalLinksBuilder(Builder):
thread.start()
self.workers.append(thread)
@property
def good(self):
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "good"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return self._good
@property
def broken(self):
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "broken"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return self._broken
@property
def redirected(self):
warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "redirected"),
RemovedInSphinx50Warning,
stacklevel=2,
)
return self._redirected
def check_thread(self) -> None:
kwargs = {}
if self.app.config.linkcheck_timeout:
@ -251,14 +288,14 @@ class CheckExternalLinksBuilder(Builder):
if rex.match(uri):
return 'ignored', '', 0
else:
self.broken[uri] = ''
self._broken[uri] = ''
return 'broken', '', 0
elif uri in self.good:
elif uri in self._good:
return 'working', 'old', 0
elif uri in self.broken:
return 'broken', self.broken[uri], 0
elif uri in self.redirected:
return 'redirected', self.redirected[uri][0], self.redirected[uri][1]
elif uri in self._broken:
return 'broken', self._broken[uri], 0
elif uri in self._redirected:
return 'redirected', self._redirected[uri][0], self._redirected[uri][1]
for rex in self.to_ignore:
if rex.match(uri):
return 'ignored', '', 0
@ -270,11 +307,11 @@ class CheckExternalLinksBuilder(Builder):
break
if status == "working":
self.good.add(uri)
self._good.add(uri)
elif status == "broken":
self.broken[uri] = info
self._broken[uri] = info
elif status == "redirected":
self.redirected[uri] = (info, code)
self._redirected[uri] = (info, code)
return (status, info, code)
@ -396,46 +433,6 @@ class CheckExternalLinksBuilder(Builder):
lineno, uri + ' to ' + info)
self.write_linkstat(linkstat)
def get_target_uri(self, docname: str, typ: str = None) -> str:
return ''
def get_outdated_docs(self) -> Set[str]:
return self.env.found_docs
def prepare_writing(self, docnames: Set[str]) -> None:
return
def write_doc(self, docname: str, doctree: Node) -> None:
logger.info('')
n = 0
# reference nodes
for refnode in doctree.traverse(nodes.reference):
if 'refuri' not in refnode:
continue
uri = refnode['refuri']
lineno = node_line_or_0(refnode)
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
self.wqueue.put(uri_info, False)
n += 1
# image nodes
for imgnode in doctree.traverse(nodes.image):
uri = imgnode['candidates'].get('?')
if uri and '://' in uri:
lineno = node_line_or_0(imgnode)
uri_info = (CHECK_IMMEDIATELY, uri, docname, lineno)
self.wqueue.put(uri_info, False)
n += 1
done = 0
while done < n:
self.process_result(self.rqueue.get())
done += 1
if self.broken:
self.app.statuscode = 1
def write_entry(self, what: str, docname: str, filename: str, line: int,
uri: str) -> None:
with open(path.join(self.outdir, 'output.txt'), 'a') as output:
@ -447,14 +444,58 @@ class CheckExternalLinksBuilder(Builder):
output.write('\n')
def finish(self) -> None:
logger.info('')
n = 0
for hyperlink in self.hyperlinks.values():
self.wqueue.put(hyperlink, False)
n += 1
done = 0
while done < n:
self.process_result(self.rqueue.get())
done += 1
if self._broken:
self.app.statuscode = 1
self.wqueue.join()
# Shutdown threads.
for worker in self.workers:
self.wqueue.put((CHECK_IMMEDIATELY, None, None, None), False)
class HyperlinkCollector(SphinxPostTransform):
builders = ('linkcheck',)
default_priority = 800
def run(self, **kwargs: Any) -> None:
builder = cast(CheckExternalLinksBuilder, self.app.builder)
hyperlinks = builder.hyperlinks
# reference nodes
for refnode in self.document.traverse(nodes.reference):
if 'refuri' not in refnode:
continue
uri = refnode['refuri']
lineno = get_node_line(refnode)
uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno)
if uri not in hyperlinks:
hyperlinks[uri] = uri_info
# image nodes
for imgnode in self.document.traverse(nodes.image):
uri = imgnode['candidates'].get('?')
if uri and '://' in uri:
lineno = get_node_line(imgnode)
uri_info = Hyperlink(CHECK_IMMEDIATELY, uri, self.env.docname, lineno)
if uri not in hyperlinks:
hyperlinks[uri] = uri_info
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_builder(CheckExternalLinksBuilder)
app.add_post_transform(HyperlinkCollector)
app.add_config_value('linkcheck_ignore', [], None)
app.add_config_value('linkcheck_auth', [], None)

View File

@ -137,8 +137,7 @@ class ASTIdentifier(ASTBaseBase):
reftype='identifier',
reftarget=targetText, modname=None,
classname=None)
key = symbol.get_lookup_key()
pnode['c:parent_key'] = key
pnode['c:parent_key'] = symbol.get_lookup_key()
if self.is_anon():
pnode += nodes.strong(text="[anonymous]")
else:
@ -3204,7 +3203,8 @@ class CObject(ObjectDescription[ASTDeclaration]):
def parse_pre_v3_type_definition(self, parser: DefinitionParser) -> ASTDeclaration:
return parser.parse_pre_v3_type_definition()
def describe_signature(self, signode: TextElement, ast: Any, options: Dict) -> None:
def describe_signature(self, signode: TextElement, ast: ASTDeclaration,
options: Dict) -> None:
ast.describe_signature(signode, 'lastIsName', self.env, options)
def run(self) -> List[Node]:
@ -3642,7 +3642,7 @@ class CExprRole(SphinxRole):
location=self.get_source_info())
# see below
return [self.node_type(text, text, classes=classes)], []
parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None)
parentSymbol = self.env.temp_data.get('c:parent_symbol', None)
if parentSymbol is None:
parentSymbol = self.env.domaindata['c']['root_symbol']
# ...most if not all of these classes should really apply to the individual references,

View File

@ -1592,6 +1592,15 @@ class ASTOperator(ASTBase):
identifier = str(self)
if mode == 'lastIsName':
signode += addnodes.desc_name(identifier, identifier)
elif mode == 'markType':
targetText = prefix + identifier + templateArgs
pnode = addnodes.pending_xref('', refdomain='cpp',
reftype='identifier',
reftarget=targetText, modname=None,
classname=None)
pnode['cpp:parent_key'] = symbol.get_lookup_key()
pnode += nodes.Text(identifier)
signode += pnode
else:
signode += addnodes.desc_addname(identifier, identifier)

View File

@ -10,6 +10,7 @@
import os
import pickle
import posixpath
from collections import defaultdict
from copy import copy
from datetime import datetime
@ -340,9 +341,9 @@ class BuildEnvironment:
docdir = path.dirname(self.doc2path(docname or self.docname,
base=None))
rel_fn = path.join(docdir, filename)
# the path.abspath() might seem redundant, but otherwise artifacts
# such as ".." will remain in the path
return rel_fn, path.abspath(path.join(self.srcdir, rel_fn))
return (posixpath.normpath(rel_fn),
path.normpath(path.join(self.srcdir, rel_fn)))
@property
def found_docs(self) -> Set[str]:

View File

@ -1338,8 +1338,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
@ -1348,6 +1351,16 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
return "\n".join(sigs)
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
"""Merge default values of actual implementation to the overload variants."""
parameters = list(overload.parameters.values())
for i, param in enumerate(parameters):
actual_param = actual.parameters.get(param.name)
if actual_param and param.default == '...':
parameters[i] = param.replace(default=actual_param.default)
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
try:
@ -2096,8 +2109,16 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
type_aliases=self.config.autodoc_type_aliases)
else:
actual = inspect.signature(self.object, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
@ -2110,6 +2131,16 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return "\n".join(sigs)
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
"""Merge default values of actual implementation to the overload variants."""
parameters = list(overload.parameters.values())
for i, param in enumerate(parameters):
actual_param = actual.parameters.get(param.name)
if actual_param and param.default == '...':
parameters[i] = param.replace(default=actual_param.default)
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
try:

View File

@ -97,10 +97,14 @@ def setup_documenters(app: Any) -> None:
def _simple_info(msg: str) -> None:
warnings.warn('_simple_info() is deprecated.',
RemovedInSphinx50Warning, stacklevel=2)
print(msg)
def _simple_warn(msg: str) -> None:
warnings.warn('_simple_warn() is deprecated.',
RemovedInSphinx50Warning, stacklevel=2)
print('WARNING: ' + msg, file=sys.stderr)

View File

@ -253,10 +253,15 @@ class Config:
* To create a custom "generic" section, just pass a string.
* To create an alias for an existing section, pass a tuple containing the
alias name and the original, in that order.
* To create a custom section that displays like the parameters or returns
section, pass a tuple containing the custom section name and a string
value, "params_style" or "returns_style".
If an entry is just a string, it is interpreted as a header for a generic
section. If the entry is a tuple/list/indexed container, the first entry
is the name of the section, the second is the section key to emulate.
is the name of the section, the second is the section key to emulate. If the
second entry value is "params_style" or "returns_style", the custom section
will be displayed like the parameters section or returns section.
napoleon_attr_annotations : :obj:`bool` (Defaults to True)
Use the type annotations of class attributes that are documented in the docstring

View File

@ -544,11 +544,18 @@ class GoogleDocstring:
self._sections[entry.lower()] = self._parse_custom_generic_section
else:
# otherwise, assume entry is container;
# [0] is new section, [1] is the section to alias.
# in the case of key mismatch, just handle as generic section.
self._sections[entry[0].lower()] = \
self._sections.get(entry[1].lower(),
self._parse_custom_generic_section)
if entry[1] == "params_style":
self._sections[entry[0].lower()] = \
self._parse_custom_params_style_section
elif entry[1] == "returns_style":
self._sections[entry[0].lower()] = \
self._parse_custom_returns_style_section
else:
# [0] is new section, [1] is the section to alias.
# in the case of key mismatch, just handle as generic section.
self._sections[entry[0].lower()] = \
self._sections.get(entry[1].lower(),
self._parse_custom_generic_section)
def _parse(self) -> None:
self._parsed_lines = self._consume_empty()
@ -636,6 +643,13 @@ class GoogleDocstring:
# for now, no admonition for simple custom sections
return self._parse_generic_section(section, False)
def _parse_custom_params_style_section(self, section: str) -> List[str]:
return self._format_fields(section, self._consume_fields())
def _parse_custom_returns_style_section(self, section: str) -> List[str]:
fields = self._consume_returns_section()
return self._format_fields(section, fields)
def _parse_usage_section(self, section: str) -> List[str]:
header = ['.. rubric:: Usage:', '']
block = ['.. code-block:: python', '']
@ -682,7 +696,13 @@ class GoogleDocstring:
return self._parse_generic_section(_('Notes'), use_admonition)
def _parse_other_parameters_section(self, section: str) -> List[str]:
return self._format_fields(_('Other Parameters'), self._consume_fields())
if self._config.napoleon_use_param:
# Allow to declare multiple parameters at once (ex: x, y: int)
fields = self._consume_fields(multiple=True)
return self._format_docutils_params(fields)
else:
fields = self._consume_fields()
return self._format_fields(_('Other Parameters'), fields)
def _parse_parameters_section(self, section: str) -> List[str]:
if self._config.napoleon_use_param:

View File

@ -8,8 +8,11 @@
:license: BSD, see LICENSE for details.
"""
import posixpath
import traceback
from typing import Any, Dict, Iterable, Iterator, Set, Tuple
import warnings
from os import path
from typing import Any, Dict, Generator, Iterable, Optional, Set, Tuple, cast
from docutils import nodes
from docutils.nodes import Element, Node
@ -17,16 +20,32 @@ from docutils.nodes import Element, Node
import sphinx
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import get_full_modname, logging, status_iterator
from sphinx.util.nodes import make_refnode
logger = logging.getLogger(__name__)
def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str:
OUTPUT_DIRNAME = '_modules'
class viewcode_anchor(Element):
"""Node for viewcode anchors.
This node will be processed in the resolving phase.
For viewcode supported builders, they will be all converted to the anchors.
For not supported builders, they will be removed.
"""
def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> Optional[str]:
try:
return get_full_modname(modname, attribute)
except AttributeError:
@ -44,14 +63,21 @@ def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str:
return None
def is_supported_builder(builder: Builder) -> bool:
if builder.format != 'html':
return False
elif builder.name == 'singlehtml':
return False
elif builder.name.startswith('epub') and not builder.config.viewcode_enable_epub:
return False
else:
return True
def doctree_read(app: Sphinx, doctree: Node) -> None:
env = app.builder.env
if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {} # type: ignore
if app.builder.name == "singlehtml":
return
if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub:
return
def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool:
entry = env._viewcode_modules.get(modname, None) # type: ignore
@ -108,13 +134,8 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
# only one link per name, please
continue
names.add(fullname)
pagename = '_modules/' + modname.replace('.', '/')
inline = nodes.inline('', _('[source]'), classes=['viewcode-link'])
onlynode = addnodes.only(expr='html')
onlynode += addnodes.pending_xref('', inline, reftype='viewcode', refdomain='std',
refexplicit=False, reftarget=pagename,
refid=fullname, refdoc=env.docname)
signode += onlynode
pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))
signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname)
def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
@ -128,20 +149,80 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
env._viewcode_modules.update(other._viewcode_modules) # type: ignore
class ViewcodeAnchorTransform(SphinxPostTransform):
"""Convert or remove viewcode_anchor nodes depends on builder."""
default_priority = 100
def run(self, **kwargs: Any) -> None:
if is_supported_builder(self.app.builder):
self.convert_viewcode_anchors()
else:
self.remove_viewcode_anchors()
def convert_viewcode_anchors(self) -> None:
for node in self.document.traverse(viewcode_anchor):
anchor = nodes.inline('', _('[source]'), classes=['viewcode-link'])
refnode = make_refnode(self.app.builder, node['refdoc'], node['reftarget'],
node['refid'], anchor)
node.replace_self(refnode)
def remove_viewcode_anchors(self) -> None:
for node in self.document.traverse(viewcode_anchor):
node.parent.remove(node)
def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node
) -> Node:
) -> Optional[Node]:
# resolve our "viewcode" reference nodes -- they need special treatment
if node['reftype'] == 'viewcode':
warnings.warn('viewcode extension is no longer use pending_xref node. '
'Please update your extension.', RemovedInSphinx50Warning)
return make_refnode(app.builder, node['refdoc'], node['reftarget'],
node['refid'], contnode)
return None
def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
def get_module_filename(app: Sphinx, modname: str) -> Optional[str]:
"""Get module filename for *modname*."""
source_info = app.emit_firstresult('viewcode-find-source', modname)
if source_info:
return None
else:
try:
filename, source = ModuleAnalyzer.get_module_source(modname)
return filename
except Exception:
return None
def should_generate_module_page(app: Sphinx, modname: str) -> bool:
"""Check generation of module page is needed."""
module_filename = get_module_filename(app, modname)
if module_filename is None:
# Always (re-)generate module page when module filename is not found.
return True
builder = cast(StandaloneHTMLBuilder, app.builder)
basename = modname.replace('.', '/') + builder.out_suffix
page_filename = path.join(app.outdir, '_modules/', basename)
try:
if path.getmtime(module_filename) <= path.getmtime(page_filename):
# generation is not needed if the HTML page is newer than module file.
return False
except IOError:
pass
return True
def collect_pages(app: Sphinx) -> Generator[Tuple[str, Dict[str, Any], str], None, None]:
env = app.builder.env
if not hasattr(env, '_viewcode_modules'):
return
if not is_supported_builder(app.builder):
return
highlighter = app.builder.highlighter # type: ignore
urito = app.builder.get_relative_uri
@ -154,9 +235,12 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
app.verbosity, lambda x: x[0]):
if not entry:
continue
if not should_generate_module_page(app, modname):
continue
code, tags, used, refname = entry
# construct a page name for the highlighted source
pagename = '_modules/' + modname.replace('.', '/')
pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))
# highlight the source using the builder's highlighter
if env.config.highlight_language in ('python3', 'default', 'none'):
lexer = env.config.highlight_language
@ -188,10 +272,10 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
parent = parent.rsplit('.', 1)[0]
if parent in modnames:
parents.append({
'link': urito(pagename, '_modules/' +
parent.replace('.', '/')),
'link': urito(pagename,
posixpath.join(OUTPUT_DIRNAME, parent.replace('.', '/'))),
'title': parent})
parents.append({'link': urito(pagename, '_modules/index'),
parents.append({'link': urito(pagename, posixpath.join(OUTPUT_DIRNAME, 'index')),
'title': _('Module code')})
parents.reverse()
# putting it all together
@ -220,7 +304,8 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
html.append('</ul>')
stack.append(modname + '.')
html.append('<li><a href="%s">%s</a></li>\n' % (
urito('_modules/index', '_modules/' + modname.replace('.', '/')),
urito(posixpath.join(OUTPUT_DIRNAME, 'index'),
posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))),
modname))
html.append('</ul>' * (len(stack) - 1))
context = {
@ -229,7 +314,7 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
''.join(html)),
}
yield ('_modules/index', context, 'page.html')
yield (posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html')
def setup(app: Sphinx) -> Dict[str, Any]:
@ -244,6 +329,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
# app.add_config_value('viewcode_exclude_modules', [], 'env')
app.add_event('viewcode-find-source')
app.add_event('viewcode-follow-imported')
app.add_post_transform(ViewcodeAnchorTransform)
return {
'version': sphinx.__display_version__,
'env_version': 1,

View File

@ -8,7 +8,7 @@
:license: BSD, see LICENSE for details.
"""
from typing import Any, Dict, List, Tuple, Type, cast
from typing import Any, Dict, List, Optional, Tuple, Type, cast
from docutils import nodes
from docutils.nodes import Element
@ -150,7 +150,7 @@ class ReferencesResolver(SphinxPostTransform):
return newnode
def warn_missing_reference(self, refdoc: str, typ: str, target: str,
node: pending_xref, domain: Domain) -> None:
node: pending_xref, domain: Optional[Domain]) -> None:
warn = node.get('refwarn')
if self.config.nitpicky:
warn = True

View File

@ -482,6 +482,19 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
return getattr(builtins, name, None) is cls
class DefaultValue:
"""A simple wrapper for default value of the parameters of overload functions."""
def __init__(self, value: str) -> None:
self.value = value
def __eq__(self, other: object) -> bool:
return self.value == other
def __repr__(self) -> str:
return self.value
def _should_unwrap(subject: Callable) -> bool:
"""Check the function should be unwrapped on getting signature."""
if (safe_getattr(subject, '__globals__', None) and
@ -687,7 +700,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu
if defaults[i] is Parameter.empty:
default = Parameter.empty
else:
default = ast_unparse(defaults[i], code)
default = DefaultValue(ast_unparse(defaults[i], code))
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY,
@ -697,7 +710,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu
if defaults[i + posonlyargs] is Parameter.empty:
default = Parameter.empty
else:
default = ast_unparse(defaults[i + posonlyargs], code)
default = DefaultValue(ast_unparse(defaults[i + posonlyargs], code))
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,

View File

@ -1203,7 +1203,6 @@ class LaTeXTranslator(SphinxTranslator):
return isinstance(node.parent, nodes.TextElement)
def visit_image(self, node: Element) -> None:
attrs = node.attributes
pre = [] # type: List[str]
# in reverse order
post = [] # type: List[str]
@ -1213,27 +1212,27 @@ class LaTeXTranslator(SphinxTranslator):
is_inline = self.is_inline(node.parent)
else:
is_inline = self.is_inline(node)
if 'width' in attrs:
if 'scale' in attrs:
w = self.latex_image_length(attrs['width'], attrs['scale'])
if 'width' in node:
if 'scale' in node:
w = self.latex_image_length(node['width'], node['scale'])
else:
w = self.latex_image_length(attrs['width'])
w = self.latex_image_length(node['width'])
if w:
include_graphics_options.append('width=%s' % w)
if 'height' in attrs:
if 'scale' in attrs:
h = self.latex_image_length(attrs['height'], attrs['scale'])
if 'height' in node:
if 'scale' in node:
h = self.latex_image_length(node['height'], node['scale'])
else:
h = self.latex_image_length(attrs['height'])
h = self.latex_image_length(node['height'])
if h:
include_graphics_options.append('height=%s' % h)
if 'scale' in attrs:
if 'scale' in node:
if not include_graphics_options:
# if no "width" nor "height", \sphinxincludegraphics will fit
# to the available text width if oversized after rescaling.
include_graphics_options.append('scale=%s'
% (float(attrs['scale']) / 100.0))
if 'align' in attrs:
% (float(node['scale']) / 100.0))
if 'align' in node:
align_prepost = {
# By default latex aligns the top of an image.
(1, 'top'): ('', ''),
@ -1247,8 +1246,8 @@ class LaTeXTranslator(SphinxTranslator):
(0, 'right'): ('{\\hspace*{\\fill}', '}'),
}
try:
pre.append(align_prepost[is_inline, attrs['align']][0])
post.append(align_prepost[is_inline, attrs['align']][1])
pre.append(align_prepost[is_inline, node['align']][0])
post.append(align_prepost[is_inline, node['align']][1])
except KeyError:
pass
if self.in_parsed_literal:

View File

@ -1205,11 +1205,10 @@ class TexinfoTranslator(SphinxTranslator):
# ignore remote images
return
name, ext = path.splitext(uri)
attrs = node.attributes
# width and height ignored in non-tex output
width = self.tex_image_length(attrs.get('width', ''))
height = self.tex_image_length(attrs.get('height', ''))
alt = self.escape_arg(attrs.get('alt', ''))
width = self.tex_image_length(node.get('width', ''))
height = self.tex_image_length(node.get('height', ''))
alt = self.escape_arg(node.get('alt', ''))
filename = "%s-figures/%s" % (self.elements['filename'][:-5], name) # type: ignore
self.body.append('\n@image{%s,%s,%s,%s,%s}\n' %
(filename, width, height, alt, ext[1:]))

View File

@ -0,0 +1,13 @@
.. c:namespace:: ns_lookup
.. c:var:: int i
.. c:function:: void f(int j)
- :c:var:`i`
- :c:var:`j`
- :c:expr:`i`
- :c:expr:`j`
- :c:var:`i`
- :c:expr:`i`

View File

@ -2,21 +2,21 @@ from typing import Any, overload
@overload
def sum(x: int, y: int) -> int:
def sum(x: int, y: int = 0) -> int:
...
@overload
def sum(x: "float", y: "float") -> "float":
def sum(x: "float", y: "float" = 0.0) -> "float":
...
@overload
def sum(x: str, y: str) -> str:
def sum(x: str, y: str = ...) -> str:
...
def sum(x, y):
def sum(x, y=None):
"""docstring"""
return x + y
@ -25,18 +25,18 @@ class Math:
"""docstring"""
@overload
def sum(self, x: int, y: int) -> int:
def sum(self, x: int, y: int = 0) -> int:
...
@overload
def sum(self, x: "float", y: "float") -> "float":
def sum(self, x: "float", y: "float" = 0.0) -> "float":
...
@overload
def sum(self, x: str, y: str) -> str:
def sum(self, x: str, y: str = ...) -> str:
...
def sum(self, x, y):
def sum(self, x, y=None):
"""docstring"""
return x + y

View File

@ -1 +0,0 @@
exclude_patterns = ['_build']

View File

@ -1,6 +0,0 @@
.. image:: http://localhost:7777/
:target: http://localhost:7777/
`weblate.org`_
.. _weblate.org: http://localhost:7777/

View File

@ -573,40 +573,3 @@ def test_limit_rate_bails_out_after_waiting_max_time(app):
checker.rate_limits = {"localhost": RateLimit(90.0, 0.0)}
next_check = checker.limit_rate(FakeResponse())
assert next_check is None
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck-localserver-two-links', freshenv=True,
)
def test_priorityqueue_items_are_comparable(app):
with http_server(OKHandler):
app.builder.build_all()
content = (app.outdir / 'output.json').read_text()
rows = [json.loads(x) for x in sorted(content.splitlines())]
assert rows == [
{
'filename': 'index.rst',
# Should not be None.
'lineno': 0,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
},
{
'filename': 'index.rst',
'lineno': 0,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
},
{
'filename': 'index.rst',
'lineno': 4,
'status': 'working',
'code': 0,
'uri': 'http://localhost:7777/',
'info': '',
}
]

View File

@ -598,6 +598,13 @@ def test_build_function_param_target(app, warning):
]
@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True})
def test_build_ns_lookup(app, warning):
app.builder.build_all()
ws = filter_warnings(warning, "ns_lookup")
assert len(ws) == 0
def _get_obj(app, queryName):
domain = app.env.get_domain('c')
for name, dispname, objectType, docname, anchor, prio in domain.get_objects():

View File

@ -138,6 +138,11 @@ def test_env_relfn2path(app):
assert relfn == '../logo.jpg'
assert absfn == app.srcdir.parent / 'logo.jpg'
# relative path traversal
relfn, absfn = app.env.relfn2path('subdir/../logo.jpg', 'index')
assert relfn == 'logo.jpg'
assert absfn == app.srcdir / 'logo.jpg'
# omit docname (w/ current docname)
app.env.temp_data['docname'] = 'subdir/document'
relfn, absfn = app.env.relfn2path('images/logo.jpg')

View File

@ -2067,17 +2067,17 @@ def test_overload(app):
' docstring',
'',
'',
' .. py:method:: Math.sum(x: int, y: int) -> int',
' Math.sum(x: float, y: float) -> float',
' Math.sum(x: str, y: str) -> str',
' .. py:method:: Math.sum(x: int, y: int = 0) -> int',
' Math.sum(x: float, y: float = 0.0) -> float',
' Math.sum(x: str, y: str = None) -> str',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int, y: int) -> int',
' sum(x: float, y: float) -> float',
' sum(x: str, y: str) -> str',
'.. py:function:: sum(x: int, y: int = 0) -> int',
' sum(x: float, y: float = 0.0) -> float',
' sum(x: str, y: str = None) -> str',
' :module: target.overload',
'',
' docstring',

View File

@ -647,13 +647,13 @@ def test_autodoc_typehints_none_for_overload(app):
' docstring',
'',
'',
' .. py:method:: Math.sum(x, y)',
' .. py:method:: Math.sum(x, y=None)',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:function:: sum(x, y)',
'.. py:function:: sum(x, y=None)',
' :module: target.overload',
'',
' docstring',

View File

@ -1072,10 +1072,27 @@ You should listen to me!
Sooper Warning:
Stop hitting yourself!
""", """:Warns: **Stop hitting yourself!**
"""),
("""\
Params Style:
arg1 (int): Description of arg1
arg2 (str): Description of arg2
""", """\
:Params Style: * **arg1** (*int*) -- Description of arg1
* **arg2** (*str*) -- Description of arg2
"""),
("""\
Returns Style:
description of custom section
""", """:Returns Style: description of custom section
"""))
testConfig = Config(napoleon_custom_sections=['Really Important Details',
('Sooper Warning', 'warns')])
('Sooper Warning', 'warns'),
('Params Style', 'params_style'),
('Returns Style', 'returns_style')])
for docstring, expected in docstrings:
actual = str(GoogleDocstring(docstring, testConfig))
@ -1441,12 +1458,18 @@ Parameters
----------
param1 : :class:`MyClass <name.space.MyClass>` instance
Other Parameters
----------------
param2 : :class:`MyClass <name.space.MyClass>` instance
"""
config = Config(napoleon_use_param=False)
actual = str(NumpyDocstring(docstring, config))
expected = """\
:Parameters: **param1** (:class:`MyClass <name.space.MyClass>` instance)
:Other Parameters: **param2** (:class:`MyClass <name.space.MyClass>` instance)
"""
self.assertEqual(expected, actual)
@ -1455,6 +1478,9 @@ param1 : :class:`MyClass <name.space.MyClass>` instance
expected = """\
:param param1:
:type param1: :class:`MyClass <name.space.MyClass>` instance
:param param2:
:type param2: :class:`MyClass <name.space.MyClass>` instance
"""
self.assertEqual(expected, actual)

View File

@ -49,6 +49,27 @@ def test_viewcode(app, status, warning):
'<span> &quot;&quot;&quot;</span></div>\n') in result
@pytest.mark.sphinx('epub', testroot='ext-viewcode')
def test_viewcode_epub_default(app, status, warning):
app.builder.build_all()
assert not (app.outdir / '_modules/spam/mod1.xhtml').exists()
result = (app.outdir / 'index.xhtml').read_text()
assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 0
@pytest.mark.sphinx('epub', testroot='ext-viewcode',
confoverrides={'viewcode_enable_epub': True})
def test_viewcode_epub_enabled(app, status, warning):
app.builder.build_all()
assert (app.outdir / '_modules/spam/mod1.xhtml').exists()
result = (app.outdir / 'index.xhtml').read_text()
assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 2
@pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode'])
def test_linkcode(app, status, warning):
app.builder.build(['objects'])

View File

@ -284,6 +284,13 @@ def get_verifier(verify, verify_re):
'<p><kbd class="kbd docutils literal notranslate">-</kbd></p>',
'\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}',
),
(
# kbd role
'verify',
':kbd:`Caps Lock`',
'<p><kbd class="kbd docutils literal notranslate">Caps Lock</kbd></p>',
'\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}',
),
(
# non-interpolation of dashes in option role
'verify_re',

View File

@ -18,10 +18,10 @@ for stable releases
* ``python utils/bump_version.py --in-develop X.Y.Zb0`` (ex. 1.5.3b0)
* Check diff by ``git diff``
* ``git commit -am 'Bump version'``
* ``git push origin X.Y --tags``
* ``git checkout master``
* ``git merge X.Y``
* ``git push origin master``
* ``git push origin X.Y.x --tags``
* ``git checkout X.x``
* ``git merge X.Y.x``
* ``git push origin X.x``
* Add new version/milestone to tracker categories
* Write announcement and send to sphinx-dev, sphinx-users and python-announce
@ -43,10 +43,10 @@ for first beta releases
* ``python utils/bump_version.py --in-develop X.Y.0b2`` (ex. 1.6.0b2)
* Check diff by ``git diff``
* ``git commit -am 'Bump version'``
* ``git checkout -b X.Y``
* ``git push origin X.Y --tags``
* ``git checkout -b X.x``
* ``git push origin X.x --tags``
* ``git checkout master``
* ``git merge X.Y``
* ``git merge X.x``
* ``python utils/bump_version.py --in-develop A.B.0b0`` (ex. 1.7.0b0)
* Check diff by ``git diff``
* ``git commit -am 'Bump version'``
@ -71,9 +71,9 @@ for other beta releases
* ``python utils/bump_version.py --in-develop X.Y.0bM`` (ex. 1.6.0b3)
* Check diff by `git diff``
* ``git commit -am 'Bump version'``
* ``git push origin X.Y --tags``
* ``git push origin X.x --tags``
* ``git checkout master``
* ``git merge X.Y``
* ``git merge X.x``
* ``git push origin master``
* Add new version/milestone to tracker categories
* Write announcement and send to sphinx-dev, sphinx-users and python-announce
@ -99,9 +99,9 @@ for major releases
* ``python utils/bump_version.py --in-develop X.Y.1b0`` (ex. 1.6.1b0)
* Check diff by ``git diff``
* ``git commit -am 'Bump version'``
* ``git push origin X.Y --tags``
* ``git push origin X.x --tags``
* ``git checkout master``
* ``git merge X.Y``
* ``git merge X.x``
* ``git push origin master``
* open https://github.com/sphinx-doc/sphinx/settings/branches and make ``A.B`` branch *not* protected
* ``git checkout A.B`` (checkout old stable)