mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x' into master
This commit is contained in:
commit
598b85da75
13
CHANGES
13
CHANGES
@ -58,6 +58,8 @@ Deprecated
|
||||
|
||||
* ``sphinx.builders.latex.LaTeXBuilder.usepackages``
|
||||
* ``sphinx.builders.latex.LaTeXBuilder.usepackages_afger_hyperref``
|
||||
* ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter``
|
||||
* ``sphinx.ext.autodoc.SingledispatchMethodDocumenter``
|
||||
|
||||
Features added
|
||||
--------------
|
||||
@ -68,11 +70,14 @@ Features added
|
||||
nested declarations.
|
||||
* #8081: LaTeX: Allow to add LaTeX package via ``app.add_latex_package()`` until
|
||||
just before writing .tex file
|
||||
* #7996: manpage: Add :confval:`man_make_section_directory` to make a section
|
||||
directory on build man page
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #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
|
||||
autodoc_default_options
|
||||
* #8103: autodoc: functools.cached_property is not considered as a property
|
||||
@ -80,13 +85,21 @@ Bugs fixed
|
||||
by string not ending with blank lines
|
||||
* #8142: autodoc: Wrong constructor signature for the class derived from
|
||||
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
|
||||
* #8142: napoleon: Potential of regex denial of service in google style docs
|
||||
* #8169: LaTeX: pxjahyper loaded even when latex_engine is not platex
|
||||
* #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,
|
||||
singlehtml and so on)
|
||||
* #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
|
||||
--------
|
||||
|
@ -66,6 +66,16 @@ The following is a list of deprecated interfaces.
|
||||
- 5.0
|
||||
- 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()``
|
||||
- 3.2
|
||||
- 5.0
|
||||
|
@ -2246,6 +2246,12 @@ These options influence manual page output.
|
||||
|
||||
.. 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:
|
||||
|
||||
|
@ -515,6 +515,44 @@ There are also config values that you can set:
|
||||
|
||||
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
|
||||
|
||||
This value controls the behavior of :option:`sphinx-build -W` during
|
||||
|
@ -17,7 +17,7 @@ import sys
|
||||
from collections import deque
|
||||
from io import StringIO
|
||||
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 docutils import nodes
|
||||
@ -289,7 +289,10 @@ class Sphinx:
|
||||
if catalog.domain == 'sphinx' and catalog.is_outdated():
|
||||
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)
|
||||
if has_translation or self.config.language == 'en':
|
||||
# "en" never needs to be translated
|
||||
|
@ -646,7 +646,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
def gen_additional_pages(self) -> None:
|
||||
# additional pages from conf.py
|
||||
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)
|
||||
|
||||
# the search page
|
||||
@ -696,7 +696,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
'content': content,
|
||||
'collapse_index': collapse,
|
||||
}
|
||||
logger.info(' ' + indexname, nonl=True)
|
||||
logger.info(indexname + ' ', nonl=True)
|
||||
self.handle_page(indexname, indexcontext, 'domainindex.html')
|
||||
|
||||
def copy_image_files(self) -> None:
|
||||
@ -790,7 +790,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
|
||||
def copy_static_files(self) -> None:
|
||||
try:
|
||||
with progress_message(__('copying static files... ')):
|
||||
with progress_message(__('copying static files')):
|
||||
ensuredir(path.join(self.outdir, '_static'))
|
||||
|
||||
# prepare context for templates
|
||||
|
@ -474,7 +474,7 @@ def validate_latex_theme_options(app: Sphinx, config: Config) -> None:
|
||||
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."""
|
||||
if app.config.language == 'ja' and app.config.latex_engine in ('platex', 'uplatex'):
|
||||
app.add_latex_package('pxjahyper', after_hyperref=True)
|
||||
@ -527,7 +527,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
app.add_builder(LaTeXBuilder)
|
||||
app.connect('config-inited', validate_config_values, 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,
|
||||
ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex'))
|
||||
|
@ -166,6 +166,7 @@ class CheckExternalLinksBuilder(Builder):
|
||||
# Read the whole document and see if #anchor exists
|
||||
response = requests.get(req_url, stream=True, config=self.app.config,
|
||||
auth=auth_info, **kwargs)
|
||||
response.raise_for_status()
|
||||
found = check_anchor(response, unquote(anchor))
|
||||
|
||||
if not found:
|
||||
@ -210,7 +211,7 @@ class CheckExternalLinksBuilder(Builder):
|
||||
else:
|
||||
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
|
||||
if len(uri) == 0 or uri.startswith(('#', 'mailto:')):
|
||||
return 'unchecked', '', 0
|
||||
@ -219,7 +220,8 @@ class CheckExternalLinksBuilder(Builder):
|
||||
# non supported URI schemes (ex. ftp)
|
||||
return 'unchecked', '', 0
|
||||
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
|
||||
else:
|
||||
for rex in self.to_ignore:
|
||||
@ -256,7 +258,7 @@ class CheckExternalLinksBuilder(Builder):
|
||||
uri, docname, lineno = self.wqueue.get()
|
||||
if uri is None:
|
||||
break
|
||||
status, info, code = check()
|
||||
status, info, code = check(docname)
|
||||
self.rqueue.put((uri, docname, lineno, status, info, code))
|
||||
|
||||
def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None:
|
||||
|
@ -24,7 +24,7 @@ from sphinx.util import logging
|
||||
from sphinx.util import progress_message
|
||||
from sphinx.util.console import darkgreen # type: ignore
|
||||
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
|
||||
|
||||
|
||||
@ -80,7 +80,12 @@ class ManualPageBuilder(Builder):
|
||||
docsettings.authors = authors
|
||||
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)
|
||||
|
||||
logger.info(darkgreen(targetname) + ' { ', nonl=True)
|
||||
destination = FileOutput(
|
||||
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_show_urls', False, None)
|
||||
app.add_config_value('man_make_section_directory', False, None)
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
|
@ -220,6 +220,10 @@ class TocTreeCollector(EnvironmentCollector):
|
||||
def get_figtype(node: Node) -> str:
|
||||
for domain in env.domains.values():
|
||||
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:
|
||||
return figtype
|
||||
|
||||
|
@ -1198,7 +1198,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
|
||||
try:
|
||||
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)
|
||||
except TypeError as exc:
|
||||
logger.warning(__("Failed to get a function signature for %s: %s"),
|
||||
@ -1247,7 +1248,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
if overloaded:
|
||||
__globals__ = safe_getattr(self.object, '__globals__', {})
|
||||
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)
|
||||
sigs.append(sig)
|
||||
|
||||
@ -1256,7 +1259,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
|
||||
"""Annotate type hint to the first argument of function if needed."""
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
|
||||
except TypeError as exc:
|
||||
logger.warning(__("Failed to get a function signature for %s: %s"),
|
||||
self.fullname, exc)
|
||||
@ -1284,6 +1287,11 @@ class SingledispatchFunctionDocumenter(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):
|
||||
"""
|
||||
@ -1377,7 +1385,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
if call is not None:
|
||||
self.env.app.emit('autodoc-before-process-signature', call, True)
|
||||
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
|
||||
except ValueError:
|
||||
pass
|
||||
@ -1392,7 +1401,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
if new is not None:
|
||||
self.env.app.emit('autodoc-before-process-signature', new, True)
|
||||
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
|
||||
except ValueError:
|
||||
pass
|
||||
@ -1402,7 +1412,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
if init is not None:
|
||||
self.env.app.emit('autodoc-before-process-signature', init, True)
|
||||
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
|
||||
except ValueError:
|
||||
pass
|
||||
@ -1413,7 +1424,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
# the signature from, so just pass the object itself to our hook.
|
||||
self.env.app.emit('autodoc-before-process-signature', self.object, False)
|
||||
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
|
||||
except ValueError:
|
||||
pass
|
||||
@ -1460,7 +1472,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
method = safe_getattr(self._signature_class, self._signature_method_name, None)
|
||||
__globals__ = safe_getattr(method, '__globals__', {})
|
||||
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())
|
||||
overload = overload.replace(parameters=parameters[1:],
|
||||
@ -1798,11 +1811,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
|
||||
else:
|
||||
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
|
||||
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:
|
||||
self.env.app.emit('autodoc-before-process-signature', self.object, 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)
|
||||
except TypeError as exc:
|
||||
logger.warning(__("Failed to get a method signature for %s: %s"),
|
||||
@ -1862,7 +1877,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
|
||||
if overloaded:
|
||||
__globals__ = safe_getattr(self.object, '__globals__', {})
|
||||
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,
|
||||
name=self.object_name):
|
||||
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:
|
||||
"""Annotate type hint to the first argument of function if needed."""
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
sig = inspect.signature(func, type_aliases=self.env.config.autodoc_type_aliases)
|
||||
except TypeError as exc:
|
||||
logger.warning(__("Failed to get a method signature for %s: %s"),
|
||||
self.fullname, exc)
|
||||
@ -1902,6 +1919,11 @@ class SingledispatchMethodDocumenter(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
|
||||
"""
|
||||
@ -2212,6 +2234,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
app.add_config_value('autodoc_mock_imports', [], True)
|
||||
app.add_config_value('autodoc_typehints', "signature", True,
|
||||
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_inherit_docstrings', True, True)
|
||||
app.add_event('autodoc-before-process-signature')
|
||||
|
@ -106,7 +106,7 @@ class _TranslationProxy(UserString):
|
||||
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]:
|
||||
"""Look for message catalogs in `locale_dirs` and *ensure* that there is at
|
||||
least a NullTranslations catalog set in `translators`. If called multiple
|
||||
|
@ -183,6 +183,20 @@ class _UnparseVisitor(ast.NodeVisitor):
|
||||
return "{" + ", ".join(self.visit(e) for e in node.elts) + "}"
|
||||
|
||||
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))
|
||||
|
||||
def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
|
||||
@ -190,7 +204,7 @@ class _UnparseVisitor(ast.NodeVisitor):
|
||||
|
||||
def visit_Tuple(self, node: ast.Tuple) -> str:
|
||||
if node.elts:
|
||||
return ", ".join(self.visit(e) for e in node.elts)
|
||||
return "(" + ", ".join(self.visit(e) for e in node.elts) + ")"
|
||||
else:
|
||||
return "()"
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
import os
|
||||
import re
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
@ -175,6 +175,13 @@ class ImageConverter(BaseImageConverter):
|
||||
"""
|
||||
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.
|
||||
#: It is represented as a list of pair of source image format (mimetype) and
|
||||
#: destination one::
|
||||
@ -187,16 +194,14 @@ class ImageConverter(BaseImageConverter):
|
||||
conversion_rules = [] # type: List[Tuple[str, str]]
|
||||
|
||||
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)
|
||||
|
||||
def match(self, node: nodes.image) -> bool:
|
||||
if not self.app.builder.supported_image_types:
|
||||
return False
|
||||
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:
|
||||
return False
|
||||
|
@ -422,8 +422,8 @@ def _should_unwrap(subject: Callable) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False
|
||||
) -> inspect.Signature:
|
||||
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False,
|
||||
type_aliases: Dict = {}) -> inspect.Signature:
|
||||
"""Return a Signature object for the given *subject*.
|
||||
|
||||
: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:
|
||||
# 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):
|
||||
if isinstance(param.annotation, str) and param.name in annotations:
|
||||
parameters[i] = param.replace(annotation=annotations[param.name])
|
||||
|
@ -63,6 +63,10 @@ def is_system_TypeVar(typ: Any) -> bool:
|
||||
def stringify(annotation: Any) -> str:
|
||||
"""Stringify type annotation object."""
|
||||
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
|
||||
elif isinstance(annotation, TypeVar): # type: ignore
|
||||
return annotation.__name__
|
||||
@ -105,7 +109,10 @@ def _stringify_py37(annotation: Any) -> str:
|
||||
return repr(annotation)
|
||||
|
||||
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__) > 2:
|
||||
args = ', '.join(stringify(a) for a in annotation.__args__[:-1])
|
||||
|
25
tests/roots/test-ext-autodoc/target/annotations.py
Normal file
25
tests/roots/test-ext-autodoc/target/annotations.py
Normal 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
|
2
tests/roots/test-linkcheck-localserver/conf.py
Normal file
2
tests/roots/test-linkcheck-localserver/conf.py
Normal file
@ -0,0 +1,2 @@
|
||||
exclude_patterns = ['_build']
|
||||
linkcheck_anchors = True
|
1
tests/roots/test-linkcheck-localserver/index.rst
Normal file
1
tests/roots/test-linkcheck-localserver/index.rst
Normal file
@ -0,0 +1 @@
|
||||
`local server <http://localhost:7777/#anchor>`_
|
@ -8,8 +8,10 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
@ -106,6 +108,21 @@ def test_anchors_ignored(app, status, warning):
|
||||
# expect all ok when excluding #top
|
||||
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(
|
||||
'linkcheck', testroot='linkcheck', freshenv=True,
|
||||
@ -160,3 +177,22 @@ def test_linkcheck_request_headers(app, status, warning):
|
||||
assert headers["X-Secret"] == "open sesami"
|
||||
else:
|
||||
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")
|
||||
|
@ -30,6 +30,13 @@ def test_all(app, status, warning):
|
||||
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')
|
||||
def test_captioned_code_block(app, status, warning):
|
||||
app.builder.build_all()
|
||||
|
@ -642,6 +642,54 @@ def test_autodoc_typehints_description_for_invalid_node(app):
|
||||
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')
|
||||
def test_autodoc_default_options(app):
|
||||
# no settings
|
||||
|
@ -14,8 +14,10 @@ import re
|
||||
|
||||
import pytest
|
||||
from babel.messages import pofile, mofile
|
||||
from babel.messages.catalog import Catalog
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx import locale
|
||||
from sphinx.testing.util import (
|
||||
path, etree_parse, strip_escseq,
|
||||
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):
|
||||
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()
|
||||
|
@ -53,7 +53,7 @@ from sphinx.pycode import ast
|
||||
("+ a", "+ a"), # UAdd
|
||||
("- 1", "- 1"), # UnaryOp
|
||||
("- a", "- a"), # USub
|
||||
("(1, 2, 3)", "1, 2, 3"), # Tuple
|
||||
("(1, 2, 3)", "(1, 2, 3)"), # Tuple
|
||||
("()", "()"), # Tuple (empty)
|
||||
])
|
||||
def test_unparse(source, expected):
|
||||
|
@ -32,6 +32,10 @@ class MyList(List[T]):
|
||||
pass
|
||||
|
||||
|
||||
class BrokenType:
|
||||
__args__ = int
|
||||
|
||||
|
||||
def test_stringify():
|
||||
assert stringify(int) == "int"
|
||||
assert stringify(str) == "str"
|
||||
@ -113,3 +117,7 @@ def test_stringify_type_hints_alias():
|
||||
MyTuple = Tuple[str, str]
|
||||
assert stringify(MyStr) == "str"
|
||||
assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore
|
||||
|
||||
|
||||
def test_stringify_broken_type_hints():
|
||||
assert stringify(BrokenType) == 'test_util_typing.BrokenType'
|
||||
|
Loading…
Reference in New Issue
Block a user