Merge branch '3.x' into master

This commit is contained in:
Takeshi KOMIYA 2020-10-04 22:41:44 +09:00
commit 598b85da75
113 changed files with 519 additions and 232 deletions

13
CHANGES
View File

@ -58,6 +58,8 @@ Deprecated
* ``sphinx.builders.latex.LaTeXBuilder.usepackages`` * ``sphinx.builders.latex.LaTeXBuilder.usepackages``
* ``sphinx.builders.latex.LaTeXBuilder.usepackages_afger_hyperref`` * ``sphinx.builders.latex.LaTeXBuilder.usepackages_afger_hyperref``
* ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter``
* ``sphinx.ext.autodoc.SingledispatchMethodDocumenter``
Features added Features added
-------------- --------------
@ -68,11 +70,14 @@ Features added
nested declarations. nested declarations.
* #8081: LaTeX: Allow to add LaTeX package via ``app.add_latex_package()`` until * #8081: LaTeX: Allow to add LaTeX package via ``app.add_latex_package()`` until
just before writing .tex file just before writing .tex file
* #7996: manpage: Add :confval:`man_make_section_directory` to make a section
directory on build man page
Bugs fixed Bugs fixed
---------- ----------
* #8085: i18n: Add support for having single text domain * #8085: i18n: Add support for having single text domain
* #6640: i18n: Failed to override system message translation
* #8143: autodoc: AttributeError is raised when False value is passed to * #8143: autodoc: AttributeError is raised when False value is passed to
autodoc_default_options autodoc_default_options
* #8103: autodoc: functools.cached_property is not considered as a property * #8103: autodoc: functools.cached_property is not considered as a property
@ -80,13 +85,21 @@ Bugs fixed
by string not ending with blank lines by string not ending with blank lines
* #8142: autodoc: Wrong constructor signature for the class derived from * #8142: autodoc: Wrong constructor signature for the class derived from
typing.Generic typing.Generic
* #8157: autodoc: TypeError is raised when annotation has invalid __args__
* #7964: autodoc: Tuple in default value is wrongly rendered
* #8192: napoleon: description is disappeared when it contains inline literals * #8192: napoleon: description is disappeared when it contains inline literals
* #8142: napoleon: Potential of regex denial of service in google style docs * #8142: napoleon: Potential of regex denial of service in google style docs
* #8169: LaTeX: pxjahyper loaded even when latex_engine is not platex * #8169: LaTeX: pxjahyper loaded even when latex_engine is not platex
* #8175: intersphinx: Potential of regex denial of service by broken inventory * #8175: intersphinx: Potential of regex denial of service by broken inventory
* #8277: sphinx-build: missing and redundant spacing (and etc) for console
output on building
* #7973: imgconverter: Check availability of imagemagick many times
* #8093: The highlight warning has wrong location in some builders (LaTeX, * #8093: The highlight warning has wrong location in some builders (LaTeX,
singlehtml and so on) singlehtml and so on)
* #8239: Failed to refer a token in productionlist if it is indented * #8239: Failed to refer a token in productionlist if it is indented
* #8268: linkcheck: Report HTTP errors when ``linkcheck_anchors`` is ``True``
* #8245: linkcheck: take source directory into account for local files
* #6914: figure numbers are unexpectedly assigned to uncaptioned items
Testing Testing
-------- --------

View File

@ -66,6 +66,16 @@ The following is a list of deprecated interfaces.
- 5.0 - 5.0
- N/A - N/A
* - ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter``
- 3.3
- 5.0
- ``sphinx.ext.autodoc.FunctionDocumenter``
* - ``sphinx.ext.autodoc.SingledispatchMethodDocumenter``
- 3.3
- 5.0
- ``sphinx.ext.autodoc.MethodDocumenter``
* - ``sphinx.ext.autodoc.members_set_option()`` * - ``sphinx.ext.autodoc.members_set_option()``
- 3.2 - 3.2
- 5.0 - 5.0

View File

@ -2246,6 +2246,12 @@ These options influence manual page output.
.. versionadded:: 1.1 .. versionadded:: 1.1
.. confval:: man_make_section_directory
If true, make a section directory on build man page. Default is False.
.. versionadded:: 3.3
.. _texinfo-options: .. _texinfo-options:

View File

@ -515,6 +515,44 @@ There are also config values that you can set:
New option ``'description'`` is added. New option ``'description'`` is added.
.. confval:: autodoc_type_aliases
A dictionary for users defined `type aliases`__ that maps a type name to the
full-qualified object name. It is used to keep type aliases not evaluated in
the document. Defaults to empty (``{}``).
The type aliases are only available if your program enables `Postponed
Evaluation of Annotations (PEP 563)`__ feature via ``from __future__ import
annotations``.
For example, there is code using a type alias::
from __future__ import annotations
AliasType = Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]
def f() -> AliasType:
...
If ``autodoc_type_aliases`` is not set, autodoc will generate internal mark-up
from this code as following::
.. py:function:: f() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]
...
If you set ``autodoc_type_aliases`` as
``{'AliasType': 'your.module.TypeAlias'}``, it generates a following document
internally::
.. py:function:: f() -> your.module.AliasType:
...
.. __: https://www.python.org/dev/peps/pep-0563/
.. __: https://mypy.readthedocs.io/en/latest/kinds_of_types.html#type-aliases
.. versionadded:: 3.3
.. confval:: autodoc_warningiserror .. confval:: autodoc_warningiserror
This value controls the behavior of :option:`sphinx-build -W` during This value controls the behavior of :option:`sphinx-build -W` during

View File

@ -17,7 +17,7 @@ import sys
from collections import deque from collections import deque
from io import StringIO from io import StringIO
from os import path from os import path
from typing import Any, Callable, Dict, IO, List, Tuple, Type, Union from typing import Any, Callable, Dict, IO, List, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from docutils import nodes from docutils import nodes
@ -289,7 +289,10 @@ class Sphinx:
if catalog.domain == 'sphinx' and catalog.is_outdated(): if catalog.domain == 'sphinx' and catalog.is_outdated():
catalog.write_mo(self.config.language) catalog.write_mo(self.config.language)
locale_dirs = [None, path.join(package_dir, 'locale')] + list(repo.locale_dirs) locale_dirs = [None] # type: List[Optional[str]]
locale_dirs += list(repo.locale_dirs)
locale_dirs += [path.join(package_dir, 'locale')]
self.translator, has_translation = locale.init(locale_dirs, self.config.language) self.translator, has_translation = locale.init(locale_dirs, self.config.language)
if has_translation or self.config.language == 'en': if has_translation or self.config.language == 'en':
# "en" never needs to be translated # "en" never needs to be translated

View File

@ -646,7 +646,7 @@ class StandaloneHTMLBuilder(Builder):
def gen_additional_pages(self) -> None: def gen_additional_pages(self) -> None:
# additional pages from conf.py # additional pages from conf.py
for pagename, template in self.config.html_additional_pages.items(): for pagename, template in self.config.html_additional_pages.items():
logger.info(' ' + pagename, nonl=True) logger.info(pagename + ' ', nonl=True)
self.handle_page(pagename, {}, template) self.handle_page(pagename, {}, template)
# the search page # the search page
@ -696,7 +696,7 @@ class StandaloneHTMLBuilder(Builder):
'content': content, 'content': content,
'collapse_index': collapse, 'collapse_index': collapse,
} }
logger.info(' ' + indexname, nonl=True) logger.info(indexname + ' ', nonl=True)
self.handle_page(indexname, indexcontext, 'domainindex.html') self.handle_page(indexname, indexcontext, 'domainindex.html')
def copy_image_files(self) -> None: def copy_image_files(self) -> None:
@ -790,7 +790,7 @@ class StandaloneHTMLBuilder(Builder):
def copy_static_files(self) -> None: def copy_static_files(self) -> None:
try: try:
with progress_message(__('copying static files... ')): with progress_message(__('copying static files')):
ensuredir(path.join(self.outdir, '_static')) ensuredir(path.join(self.outdir, '_static'))
# prepare context for templates # prepare context for templates

View File

@ -474,7 +474,7 @@ def validate_latex_theme_options(app: Sphinx, config: Config) -> None:
config.latex_theme_options.pop(key) config.latex_theme_options.pop(key)
def install_pakcages_for_ja(app: Sphinx) -> None: def install_packages_for_ja(app: Sphinx) -> None:
"""Install packages for Japanese.""" """Install packages for Japanese."""
if app.config.language == 'ja' and app.config.latex_engine in ('platex', 'uplatex'): if app.config.language == 'ja' and app.config.latex_engine in ('platex', 'uplatex'):
app.add_latex_package('pxjahyper', after_hyperref=True) app.add_latex_package('pxjahyper', after_hyperref=True)
@ -527,7 +527,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_builder(LaTeXBuilder) app.add_builder(LaTeXBuilder)
app.connect('config-inited', validate_config_values, priority=800) app.connect('config-inited', validate_config_values, priority=800)
app.connect('config-inited', validate_latex_theme_options, priority=800) app.connect('config-inited', validate_latex_theme_options, priority=800)
app.connect('builder-inited', install_pakcages_for_ja) app.connect('builder-inited', install_packages_for_ja)
app.add_config_value('latex_engine', default_latex_engine, None, app.add_config_value('latex_engine', default_latex_engine, None,
ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex')) ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex'))

View File

@ -166,6 +166,7 @@ class CheckExternalLinksBuilder(Builder):
# Read the whole document and see if #anchor exists # Read the whole document and see if #anchor exists
response = requests.get(req_url, stream=True, config=self.app.config, response = requests.get(req_url, stream=True, config=self.app.config,
auth=auth_info, **kwargs) auth=auth_info, **kwargs)
response.raise_for_status()
found = check_anchor(response, unquote(anchor)) found = check_anchor(response, unquote(anchor))
if not found: if not found:
@ -210,7 +211,7 @@ class CheckExternalLinksBuilder(Builder):
else: else:
return 'redirected', new_url, 0 return 'redirected', new_url, 0
def check() -> Tuple[str, str, int]: def check(docname: str) -> Tuple[str, str, int]:
# check for various conditions without bothering the network # check for various conditions without bothering the network
if len(uri) == 0 or uri.startswith(('#', 'mailto:')): if len(uri) == 0 or uri.startswith(('#', 'mailto:')):
return 'unchecked', '', 0 return 'unchecked', '', 0
@ -219,7 +220,8 @@ class CheckExternalLinksBuilder(Builder):
# non supported URI schemes (ex. ftp) # non supported URI schemes (ex. ftp)
return 'unchecked', '', 0 return 'unchecked', '', 0
else: else:
if path.exists(path.join(self.srcdir, uri)): srcdir = path.dirname(self.env.doc2path(docname))
if path.exists(path.join(srcdir, uri)):
return 'working', '', 0 return 'working', '', 0
else: else:
for rex in self.to_ignore: for rex in self.to_ignore:
@ -256,7 +258,7 @@ class CheckExternalLinksBuilder(Builder):
uri, docname, lineno = self.wqueue.get() uri, docname, lineno = self.wqueue.get()
if uri is None: if uri is None:
break break
status, info, code = check() status, info, code = check(docname)
self.rqueue.put((uri, docname, lineno, status, info, code)) self.rqueue.put((uri, docname, lineno, status, info, code))
def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None:

View File

@ -24,7 +24,7 @@ from sphinx.util import logging
from sphinx.util import progress_message from sphinx.util import progress_message
from sphinx.util.console import darkgreen # type: ignore from sphinx.util.console import darkgreen # type: ignore
from sphinx.util.nodes import inline_all_toctrees from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import make_filename_from_project from sphinx.util.osutil import ensuredir, make_filename_from_project
from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator
@ -80,7 +80,12 @@ class ManualPageBuilder(Builder):
docsettings.authors = authors docsettings.authors = authors
docsettings.section = section docsettings.section = section
if self.config.man_make_section_directory:
ensuredir(path.join(self.outdir, str(section)))
targetname = '%s/%s.%s' % (section, name, section)
else:
targetname = '%s.%s' % (name, section) targetname = '%s.%s' % (name, section)
logger.info(darkgreen(targetname) + ' { ', nonl=True) logger.info(darkgreen(targetname) + ' { ', nonl=True)
destination = FileOutput( destination = FileOutput(
destination_path=path.join(self.outdir, targetname), destination_path=path.join(self.outdir, targetname),
@ -115,6 +120,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('man_pages', default_man_pages, None) app.add_config_value('man_pages', default_man_pages, None)
app.add_config_value('man_show_urls', False, None) app.add_config_value('man_show_urls', False, None)
app.add_config_value('man_make_section_directory', False, None)
return { return {
'version': 'builtin', 'version': 'builtin',

View File

@ -220,6 +220,10 @@ class TocTreeCollector(EnvironmentCollector):
def get_figtype(node: Node) -> str: def get_figtype(node: Node) -> str:
for domain in env.domains.values(): for domain in env.domains.values():
figtype = domain.get_enumerable_node_type(node) figtype = domain.get_enumerable_node_type(node)
if domain.name == 'std' and not domain.get_numfig_title(node): # type: ignore
# Skip if uncaptioned node
continue
if figtype: if figtype:
return figtype return figtype

View File

@ -1198,7 +1198,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
try: try:
self.env.app.emit('autodoc-before-process-signature', self.object, False) self.env.app.emit('autodoc-before-process-signature', self.object, False)
sig = inspect.signature(self.object, follow_wrapped=True) sig = inspect.signature(self.object, follow_wrapped=True,
type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs) args = stringify_signature(sig, **kwargs)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"), logger.warning(__("Failed to get a function signature for %s: %s"),
@ -1247,7 +1248,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if overloaded: if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {}) __globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)): for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = evaluate_signature(overload, __globals__) overload = evaluate_signature(overload, __globals__,
self.env.config.autodoc_type_aliases)
sig = stringify_signature(overload, **kwargs) sig = stringify_signature(overload, **kwargs)
sigs.append(sig) sigs.append(sig)
@ -1256,7 +1259,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed.""" """Annotate type hint to the first argument of function if needed."""
try: try:
sig = inspect.signature(func) sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"), logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc) self.fullname, exc)
@ -1284,6 +1287,11 @@ class SingledispatchFunctionDocumenter(FunctionDocumenter):
Retained for backwards compatibility, now does the same as the FunctionDocumenter Retained for backwards compatibility, now does the same as the FunctionDocumenter
""" """
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn("%s is deprecated." % self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
super().__init__(*args, **kwargs)
class DecoratorDocumenter(FunctionDocumenter): class DecoratorDocumenter(FunctionDocumenter):
""" """
@ -1377,7 +1385,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if call is not None: if call is not None:
self.env.app.emit('autodoc-before-process-signature', call, True) self.env.app.emit('autodoc-before-process-signature', call, True)
try: try:
sig = inspect.signature(call, bound_method=True) sig = inspect.signature(call, bound_method=True,
type_aliases=self.env.config.autodoc_type_aliases)
return type(self.object), '__call__', sig return type(self.object), '__call__', sig
except ValueError: except ValueError:
pass pass
@ -1392,7 +1401,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if new is not None: if new is not None:
self.env.app.emit('autodoc-before-process-signature', new, True) self.env.app.emit('autodoc-before-process-signature', new, True)
try: try:
sig = inspect.signature(new, bound_method=True) sig = inspect.signature(new, bound_method=True,
type_aliases=self.env.config.autodoc_type_aliases)
return self.object, '__new__', sig return self.object, '__new__', sig
except ValueError: except ValueError:
pass pass
@ -1402,7 +1412,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if init is not None: if init is not None:
self.env.app.emit('autodoc-before-process-signature', init, True) self.env.app.emit('autodoc-before-process-signature', init, True)
try: try:
sig = inspect.signature(init, bound_method=True) sig = inspect.signature(init, bound_method=True,
type_aliases=self.env.config.autodoc_type_aliases)
return self.object, '__init__', sig return self.object, '__init__', sig
except ValueError: except ValueError:
pass pass
@ -1413,7 +1424,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# the signature from, so just pass the object itself to our hook. # the signature from, so just pass the object itself to our hook.
self.env.app.emit('autodoc-before-process-signature', self.object, False) self.env.app.emit('autodoc-before-process-signature', self.object, False)
try: try:
sig = inspect.signature(self.object, bound_method=False) sig = inspect.signature(self.object, bound_method=False,
type_aliases=self.env.config.autodoc_type_aliases)
return None, None, sig return None, None, sig
except ValueError: except ValueError:
pass pass
@ -1460,7 +1472,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
method = safe_getattr(self._signature_class, self._signature_method_name, None) method = safe_getattr(self._signature_class, self._signature_method_name, None)
__globals__ = safe_getattr(method, '__globals__', {}) __globals__ = safe_getattr(method, '__globals__', {})
for overload in self.analyzer.overloads.get(qualname): for overload in self.analyzer.overloads.get(qualname):
overload = evaluate_signature(overload, __globals__) overload = evaluate_signature(overload, __globals__,
self.env.config.autodoc_type_aliases)
parameters = list(overload.parameters.values()) parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:], overload = overload.replace(parameters=parameters[1:],
@ -1798,11 +1811,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
else: else:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
self.env.app.emit('autodoc-before-process-signature', self.object, False) self.env.app.emit('autodoc-before-process-signature', self.object, False)
sig = inspect.signature(self.object, bound_method=False) sig = inspect.signature(self.object, bound_method=False,
type_aliases=self.env.config.autodoc_type_aliases)
else: else:
self.env.app.emit('autodoc-before-process-signature', self.object, True) self.env.app.emit('autodoc-before-process-signature', self.object, True)
sig = inspect.signature(self.object, bound_method=True, sig = inspect.signature(self.object, bound_method=True,
follow_wrapped=True) follow_wrapped=True,
type_aliases=self.env.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs) args = stringify_signature(sig, **kwargs)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"), logger.warning(__("Failed to get a method signature for %s: %s"),
@ -1862,7 +1877,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if overloaded: if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {}) __globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)): for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = evaluate_signature(overload, __globals__) overload = evaluate_signature(overload, __globals__,
self.env.config.autodoc_type_aliases)
if not inspect.isstaticmethod(self.object, cls=self.parent, if not inspect.isstaticmethod(self.object, cls=self.parent,
name=self.object_name): name=self.object_name):
parameters = list(overload.parameters.values()) parameters = list(overload.parameters.values())
@ -1875,7 +1892,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed.""" """Annotate type hint to the first argument of function if needed."""
try: try:
sig = inspect.signature(func) sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
except TypeError as exc: except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"), logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc) self.fullname, exc)
@ -1902,6 +1919,11 @@ class SingledispatchMethodDocumenter(MethodDocumenter):
Retained for backwards compatibility, now does the same as the MethodDocumenter Retained for backwards compatibility, now does the same as the MethodDocumenter
""" """
def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn("%s is deprecated." % self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
super().__init__(*args, **kwargs)
class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore
""" """
@ -2212,6 +2234,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_mock_imports', [], True)
app.add_config_value('autodoc_typehints', "signature", True, app.add_config_value('autodoc_typehints', "signature", True,
ENUM("signature", "description", "none")) ENUM("signature", "description", "none"))
app.add_config_value('autodoc_type_aliases', {}, True)
app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_warningiserror', True, True)
app.add_config_value('autodoc_inherit_docstrings', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True)
app.add_event('autodoc-before-process-signature') app.add_event('autodoc-before-process-signature')

View File

@ -106,7 +106,7 @@ class _TranslationProxy(UserString):
translators = defaultdict(NullTranslations) # type: Dict[Tuple[str, str], NullTranslations] translators = defaultdict(NullTranslations) # type: Dict[Tuple[str, str], NullTranslations]
def init(locale_dirs: List[str], language: str, def init(locale_dirs: List[Optional[str]], language: str,
catalog: str = 'sphinx', namespace: str = 'general') -> Tuple[NullTranslations, bool]: catalog: str = 'sphinx', namespace: str = 'general') -> Tuple[NullTranslations, bool]:
"""Look for message catalogs in `locale_dirs` and *ensure* that there is at """Look for message catalogs in `locale_dirs` and *ensure* that there is at
least a NullTranslations catalog set in `translators`. If called multiple least a NullTranslations catalog set in `translators`. If called multiple

View File

@ -183,6 +183,20 @@ class _UnparseVisitor(ast.NodeVisitor):
return "{" + ", ".join(self.visit(e) for e in node.elts) + "}" return "{" + ", ".join(self.visit(e) for e in node.elts) + "}"
def visit_Subscript(self, node: ast.Subscript) -> str: def visit_Subscript(self, node: ast.Subscript) -> str:
def is_simple_tuple(value: ast.AST) -> bool:
return (
isinstance(value, ast.Tuple) and
bool(value.elts) and
not any(isinstance(elt, ast.Starred) for elt in value.elts)
)
if is_simple_tuple(node.slice):
elts = ", ".join(self.visit(e) for e in node.slice.elts) # type: ignore
return "%s[%s]" % (self.visit(node.value), elts)
elif isinstance(node.slice, ast.Index) and is_simple_tuple(node.slice.value):
elts = ", ".join(self.visit(e) for e in node.slice.value.elts) # type: ignore
return "%s[%s]" % (self.visit(node.value), elts)
else:
return "%s[%s]" % (self.visit(node.value), self.visit(node.slice)) return "%s[%s]" % (self.visit(node.value), self.visit(node.slice))
def visit_UnaryOp(self, node: ast.UnaryOp) -> str: def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
@ -190,7 +204,7 @@ class _UnparseVisitor(ast.NodeVisitor):
def visit_Tuple(self, node: ast.Tuple) -> str: def visit_Tuple(self, node: ast.Tuple) -> str:
if node.elts: if node.elts:
return ", ".join(self.visit(e) for e in node.elts) return "(" + ", ".join(self.visit(e) for e in node.elts) + ")"
else: else:
return "()" return "()"

View File

@ -11,7 +11,7 @@
import os import os
import re import re
from math import ceil from math import ceil
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Optional, Tuple
from docutils import nodes from docutils import nodes
@ -175,6 +175,13 @@ class ImageConverter(BaseImageConverter):
""" """
default_priority = 200 default_priority = 200
#: The converter is available or not. Will be filled at the first call of
#: the build. The result is shared in the same process.
#:
#: .. todo:: This should be refactored not to store the state without class
#: variable.
available = None # type: Optional[bool]
#: A conversion rules the image converter supports. #: A conversion rules the image converter supports.
#: It is represented as a list of pair of source image format (mimetype) and #: It is represented as a list of pair of source image format (mimetype) and
#: destination one:: #: destination one::
@ -187,16 +194,14 @@ class ImageConverter(BaseImageConverter):
conversion_rules = [] # type: List[Tuple[str, str]] conversion_rules = [] # type: List[Tuple[str, str]]
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self.available = None # type: bool
# the converter is available or not.
# Will be checked at first conversion
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def match(self, node: nodes.image) -> bool: def match(self, node: nodes.image) -> bool:
if not self.app.builder.supported_image_types: if not self.app.builder.supported_image_types:
return False return False
elif self.available is None: elif self.available is None:
self.available = self.is_available() # store the value to the class variable to share it during the build
self.__class__.available = self.is_available()
if not self.available: if not self.available:
return False return False

View File

@ -422,8 +422,8 @@ def _should_unwrap(subject: Callable) -> bool:
return False return False
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False,
) -> inspect.Signature: type_aliases: Dict = {}) -> inspect.Signature:
"""Return a Signature object for the given *subject*. """Return a Signature object for the given *subject*.
:param bound_method: Specify *subject* is a bound method or not :param bound_method: Specify *subject* is a bound method or not
@ -453,7 +453,7 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
try: try:
# Update unresolved annotations using ``get_type_hints()``. # Update unresolved annotations using ``get_type_hints()``.
annotations = typing.get_type_hints(subject) annotations = typing.get_type_hints(subject, None, type_aliases)
for i, param in enumerate(parameters): for i, param in enumerate(parameters):
if isinstance(param.annotation, str) and param.name in annotations: if isinstance(param.annotation, str) and param.name in annotations:
parameters[i] = param.replace(annotation=annotations[param.name]) parameters[i] = param.replace(annotation=annotations[param.name])

View File

@ -63,6 +63,10 @@ def is_system_TypeVar(typ: Any) -> bool:
def stringify(annotation: Any) -> str: def stringify(annotation: Any) -> str:
"""Stringify type annotation object.""" """Stringify type annotation object."""
if isinstance(annotation, str): if isinstance(annotation, str):
if annotation.startswith("'") and annotation.endswith("'"):
# might be a double Forward-ref'ed type. Go unquoting.
return annotation[1:-2]
else:
return annotation return annotation
elif isinstance(annotation, TypeVar): # type: ignore elif isinstance(annotation, TypeVar): # type: ignore
return annotation.__name__ return annotation.__name__
@ -105,7 +109,10 @@ def _stringify_py37(annotation: Any) -> str:
return repr(annotation) return repr(annotation)
if getattr(annotation, '__args__', None): if getattr(annotation, '__args__', None):
if qualname == 'Union': if not isinstance(annotation.__args__, (list, tuple)):
# broken __args__ found
pass
elif qualname == 'Union':
if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType: if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType:
if len(annotation.__args__) > 2: if len(annotation.__args__) > 2:
args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) args = ', '.join(stringify(a) for a in annotation.__args__[:-1])

View File

@ -0,0 +1,25 @@
from __future__ import annotations
from typing import overload
myint = int
def sum(x: myint, y: myint) -> myint:
"""docstring"""
return x + y
@overload
def mult(x: myint, y: myint) -> myint:
...
@overload
def mult(x: float, y: float) -> float:
...
def mult(x, y):
"""docstring"""
return x, y

View File

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

View File

@ -0,0 +1 @@
`local server <http://localhost:7777/#anchor>`_

View File

@ -8,8 +8,10 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import http.server
import json import json
import re import re
import threading
from unittest import mock from unittest import mock
import pytest import pytest
@ -106,6 +108,21 @@ def test_anchors_ignored(app, status, warning):
# expect all ok when excluding #top # expect all ok when excluding #top
assert not content assert not content
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_raises_for_invalid_status(app, status, warning):
server_thread = HttpServerThread(InternalServerErrorHandler, daemon=True)
server_thread.start()
try:
app.builder.build_all()
finally:
server_thread.terminate()
content = (app.outdir / 'output.txt').read_text()
assert content == (
"index.rst:1: [broken] http://localhost:7777/#anchor: "
"500 Server Error: Internal Server Error "
"for url: http://localhost:7777/\n"
)
@pytest.mark.sphinx( @pytest.mark.sphinx(
'linkcheck', testroot='linkcheck', freshenv=True, 'linkcheck', testroot='linkcheck', freshenv=True,
@ -160,3 +177,22 @@ def test_linkcheck_request_headers(app, status, warning):
assert headers["X-Secret"] == "open sesami" assert headers["X-Secret"] == "open sesami"
else: else:
assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8" assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
class HttpServerThread(threading.Thread):
def __init__(self, handler, *args, **kwargs):
super().__init__(*args, **kwargs)
self.server = http.server.HTTPServer(("localhost", 7777), handler)
def run(self):
self.server.serve_forever(poll_interval=0.01)
def terminate(self):
self.server.shutdown()
self.server.server_close()
self.join()
class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_error(500, "Internal Server Error")

View File

@ -30,6 +30,13 @@ def test_all(app, status, warning):
assert 'Footnotes' not in content assert 'Footnotes' not in content
@pytest.mark.sphinx('man', testroot='basic',
confoverrides={'man_make_section_directory': True})
def test_man_make_section_directory(app, status, warning):
app.build()
assert (app.outdir / '1' / 'python.1').exists()
@pytest.mark.sphinx('man', testroot='directive-code') @pytest.mark.sphinx('man', testroot='directive-code')
def test_captioned_code_block(app, status, warning): def test_captioned_code_block(app, status, warning):
app.builder.build_all() app.builder.build_all()

View File

@ -642,6 +642,54 @@ def test_autodoc_typehints_description_for_invalid_node(app):
restructuredtext.parse(app, text) # raises no error restructuredtext.parse(app, text) # raises no error
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@pytest.mark.sphinx('text', testroot='ext-autodoc')
def test_autodoc_type_aliases(app):
# default
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.annotations', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'',
'',
'.. py:function:: mult(x: int, y: int) -> int',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int, y: int) -> int',
' :module: target.annotations',
'',
' docstring',
'',
]
# define aliases
app.config.autodoc_type_aliases = {'myint': 'myint'}
actual = do_autodoc(app, 'module', 'target.annotations', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'',
'',
'.. py:function:: mult(x: myint, y: myint) -> myint',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: myint, y: myint) -> myint',
' :module: target.annotations',
'',
' docstring',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_default_options(app): def test_autodoc_default_options(app):
# no settings # no settings

View File

@ -14,8 +14,10 @@ import re
import pytest import pytest
from babel.messages import pofile, mofile from babel.messages import pofile, mofile
from babel.messages.catalog import Catalog
from docutils import nodes from docutils import nodes
from sphinx import locale
from sphinx.testing.util import ( from sphinx.testing.util import (
path, etree_parse, strip_escseq, path, etree_parse, strip_escseq,
assert_re_search, assert_not_re_search, assert_startswith, assert_node assert_re_search, assert_not_re_search, assert_startswith, assert_node
@ -1289,3 +1291,30 @@ def test_image_glob_intl_using_figure_language_filename(app):
def getwarning(warnings): def getwarning(warnings):
return strip_escseq(warnings.getvalue().replace(os.sep, '/')) return strip_escseq(warnings.getvalue().replace(os.sep, '/'))
@pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'})
def test_customize_system_message(make_app, app_params, sphinx_test_tempdir):
try:
# clear translators cache
locale.translators.clear()
# prepare message catalog (.po)
locale_dir = sphinx_test_tempdir / 'basic' / 'locales' / 'de' / 'LC_MESSAGES'
locale_dir.makedirs()
with (locale_dir / 'sphinx.po').open('wb') as f:
catalog = Catalog()
catalog.add('Quick search', 'QUICK SEARCH')
pofile.write_po(f, catalog)
# construct application and convert po file to .mo
args, kwargs = app_params
app = make_app(*args, **kwargs)
assert (locale_dir / 'sphinx.mo').exists()
assert app.translator.gettext('Quick search') == 'QUICK SEARCH'
app.build()
content = (app.outdir / 'index.html').read_text()
assert 'QUICK SEARCH' in content
finally:
locale.translators.clear()

View File

@ -53,7 +53,7 @@ from sphinx.pycode import ast
("+ a", "+ a"), # UAdd ("+ a", "+ a"), # UAdd
("- 1", "- 1"), # UnaryOp ("- 1", "- 1"), # UnaryOp
("- a", "- a"), # USub ("- a", "- a"), # USub
("(1, 2, 3)", "1, 2, 3"), # Tuple ("(1, 2, 3)", "(1, 2, 3)"), # Tuple
("()", "()"), # Tuple (empty) ("()", "()"), # Tuple (empty)
]) ])
def test_unparse(source, expected): def test_unparse(source, expected):

View File

@ -32,6 +32,10 @@ class MyList(List[T]):
pass pass
class BrokenType:
__args__ = int
def test_stringify(): def test_stringify():
assert stringify(int) == "int" assert stringify(int) == "int"
assert stringify(str) == "str" assert stringify(str) == "str"
@ -113,3 +117,7 @@ def test_stringify_type_hints_alias():
MyTuple = Tuple[str, str] MyTuple = Tuple[str, str]
assert stringify(MyStr) == "str" assert stringify(MyStr) == "str"
assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore
def test_stringify_broken_type_hints():
assert stringify(BrokenType) == 'test_util_typing.BrokenType'