diff --git a/CHANGES b/CHANGES index f9844e53b6f..35cea7ce45e 100644 --- a/CHANGES +++ b/CHANGES @@ -74,9 +74,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 -------------- @@ -84,6 +91,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()` @@ -92,12 +101,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 ---------- @@ -109,19 +122,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 -------- @@ -144,6 +165,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 -------- diff --git a/doc/development/index.rst b/doc/development/index.rst index 04918acd613..b4a7920ba03 100644 --- a/doc/development/index.rst +++ b/doc/development/index.rst @@ -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 diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index c03703c0cd3..c3993efd463 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -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 diff --git a/doc/latex.rst b/doc/latex.rst index 53fe9301a84..35e38acb478 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -195,8 +195,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 @@ -300,7 +300,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 @@ -389,7 +389,7 @@ Keys that don't need to be overridden unless in special cases are: key is ignored. Default: ``'\\usepackage{textalpha}'`` or ``''`` if ``fontenc`` does not - include the ``LGR`` option. + include the ``LGR`` option. .. versionadded:: 2.0 @@ -407,7 +407,7 @@ Keys that don't need to be overridden unless in special cases are: `. Default: ``'\\usepackage{geometry}'`` (or - ``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents) + ``'\\usepackage[dvipdfm]{geometry}'`` for Japanese documents) .. versionadded:: 1.5 @@ -784,14 +784,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. diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst index cf5b3080f6a..066c56e2d11 100644 --- a/doc/usage/extensions/napoleon.rst +++ b/doc/usage/extensions/napoleon.rst @@ -546,4 +546,28 @@ sure that "sphinx.ext.napoleon" is enabled in `conf.py`:: If an attribute is documented in the docstring without a type and has an annotation in the class body, that type is used. - .. versionadded:: 3.4 \ No newline at end of file + .. 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`` \ No newline at end of file diff --git a/setup.py b/setup.py index a0c8ac1028b..9258fded218 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ 'lint': [ 'flake8>=3.5.0', 'isort', - 'mypy>=0.790', + 'mypy>=0.800', 'docutils-stubs', ], 'test': [ diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index faee0a9769c..a5dfe4104aa 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -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 diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py index 29a9899361e..cb9af5f2895 100644 --- a/sphinx/builders/html/transforms.py +++ b/sphinx/builders/html/transforms.py @@ -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 @@ def run(self, **kwargs: Any) -> None: 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) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index cfcc182e1d0..f813922ec8c 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -14,21 +14,24 @@ 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 @@ 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 @@ def init(self) -> None: 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 @@ def check(docname: str) -> Tuple[str, str, int]: 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 @@ def check(docname: str) -> Tuple[str, str, int]: 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,65 +433,69 @@ def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: 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 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: + output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) - def prepare_writing(self, docnames: Set[str]) -> None: - return + def write_linkstat(self, data: dict) -> None: + with open(path.join(self.outdir, 'output.json'), 'a') as output: + output.write(json.dumps(data)) + output.write('\n') - def write_doc(self, docname: str, doctree: Node) -> None: + def finish(self) -> 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) + for hyperlink in self.hyperlinks.values(): + self.wqueue.put(hyperlink, 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: + 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: - output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) - - def write_linkstat(self, data: dict) -> None: - with open(path.join(self.outdir, 'output.json'), 'a') as output: - output.write(json.dumps(data)) - output.write('\n') - - def finish(self) -> None: 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) diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 064318e08ca..5336b003c89 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -137,8 +137,7 @@ def describe_signature(self, signode: TextElement, mode: str, env: "BuildEnviron 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 @@ def parse_definition(self, parser: DefinitionParser) -> 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 @@ def run(self) -> Tuple[List[Node], List[system_message]]: 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, diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 4d6e189a31e..25e6f1421b6 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -1592,6 +1592,15 @@ def describe_signature(self, signode: TextElement, mode: str, 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) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 2121b3ee465..28599c977f4 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -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 @@ def relfn2path(self, filename: str, docname: str = None) -> Tuple[str, str]: 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]: diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index f66852a34b1..ee74db37009 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1338,8 +1338,11 @@ def format_signature(self, **kwargs: Any) -> str: 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 @@ def format_signature(self, **kwargs: Any) -> str: 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 @@ def format_signature(self, **kwargs: Any) -> str: 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 @@ def format_signature(self, **kwargs: Any) -> str: 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: diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index f972124dc68..59f526744d1 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -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) diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 5b2715bac09..4a8c2135aa7 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -253,10 +253,15 @@ def __unicode__(self): * 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 diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index a5a986e4980..a7dc969d5e1 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -544,11 +544,18 @@ def _load_custom_sections(self) -> None: 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 @@ def _parse_custom_generic_section(self, section: str) -> List[str]: # 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', ''] diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 3459e45caf8..baf86dbbff3 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -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 @@ 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 has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool: # 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('') stack.append(modname + '.') html.append('
  • %s
  • \n' % ( - urito('_modules/index', '_modules/' + modname.replace('.', '/')), + urito(posixpath.join(OUTPUT_DIRNAME, 'index'), + posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))), modname)) html.append('' * (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, diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 2c563540a9c..1c424050ab8 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -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 @@ def stringify(name: str, node: Element) -> str: 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 diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 85186cf0b27..f797339cf9f 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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, diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 0a76425735a..fc597ec0881 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -1203,7 +1203,6 @@ def is_inline(self, node: Element) -> bool: 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 @@ def visit_image(self, node: Element) -> None: 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 @@ def visit_image(self, node: Element) -> None: (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: diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index a70ffd5e46e..d9187429994 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -1205,11 +1205,10 @@ def visit_image(self, node: Element) -> None: # 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:])) diff --git a/tests/roots/test-domain-c/ns_lookup.rst b/tests/roots/test-domain-c/ns_lookup.rst new file mode 100644 index 00000000000..87f9d68e731 --- /dev/null +++ b/tests/roots/test-domain-c/ns_lookup.rst @@ -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` diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py index cc4e509f259..1b395ee5b34 100644 --- a/tests/roots/test-ext-autodoc/target/overload.py +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -2,21 +2,21 @@ @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 diff --git a/tests/roots/test-linkcheck-localserver-two-links/conf.py b/tests/roots/test-linkcheck-localserver-two-links/conf.py deleted file mode 100644 index a45d22e2821..00000000000 --- a/tests/roots/test-linkcheck-localserver-two-links/conf.py +++ /dev/null @@ -1 +0,0 @@ -exclude_patterns = ['_build'] diff --git a/tests/roots/test-linkcheck-localserver-two-links/index.rst b/tests/roots/test-linkcheck-localserver-two-links/index.rst deleted file mode 100644 index 4c1bcfd6a13..00000000000 --- a/tests/roots/test-linkcheck-localserver-two-links/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. image:: http://localhost:7777/ - :target: http://localhost:7777/ - -`weblate.org`_ - -.. _weblate.org: http://localhost:7777/ diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 8427dfb599f..55a2bf233f4 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -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': '', - } - ] diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 0f17fd04119..2cfcf74faf4 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -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(): diff --git a/tests/test_environment.py b/tests/test_environment.py index 650d12bfbee..bad06baa4d7 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -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') diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index e00ab0ee25d..aed5fa5bb17 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -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', diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 9cd5f5e32ed..bae684397b5 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -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', diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 4ee66aaa5df..47561fd7116 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -1072,10 +1072,27 @@ def test_custom_generic_sections(self): 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)) diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py index 79864095b51..d75fb71966d 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_ext_viewcode.py @@ -49,6 +49,27 @@ def test_viewcode(app, status, warning): ' """\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']) diff --git a/tests/test_markup.py b/tests/test_markup.py index e762dbb3b2b..8341b882656 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -284,6 +284,13 @@ def get(name): '

    -

    ', '\\sphinxkeyboard{\\sphinxupquote{\\sphinxhyphen{}}}', ), + ( + # kbd role + 'verify', + ':kbd:`Caps Lock`', + '

    Caps Lock

    ', + '\\sphinxkeyboard{\\sphinxupquote{Caps Lock}}', + ), ( # non-interpolation of dashes in option role 'verify_re', diff --git a/utils/release-checklist b/utils/release-checklist index 671f932d816..477ddcbbef9 100644 --- a/utils/release-checklist +++ b/utils/release-checklist @@ -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)