Merge branch '3.x' into 8200_typealias_break_type_annotation

This commit is contained in:
Takeshi KOMIYA 2020-10-04 22:55:42 +09:00 committed by GitHub
commit d8cdad919b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 141 additions and 23 deletions

12
CHANGES
View File

@ -12,6 +12,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
-------------- --------------
@ -22,11 +24,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
@ -34,15 +39,22 @@ 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
* #8200: autodoc: type aliases break type formatting of autoattribute * #8200: autodoc: type aliases break type formatting of autoattribute
* #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`` * #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

@ -36,6 +36,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

@ -2245,6 +2245,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

@ -18,7 +18,7 @@ import warnings
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, Union from typing import Any, Callable, Dict, IO, List, Optional, Tuple, Union
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, TextElement from docutils.nodes import Element, TextElement
@ -293,7 +293,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

@ -641,17 +641,17 @@ 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
if self.search: if self.search:
logger.info(' search', nonl=True) logger.info('search ', nonl=True)
self.handle_page('search', {}, 'search.html') self.handle_page('search', {}, 'search.html')
# the opensearch xml file # the opensearch xml file
if self.config.html_use_opensearch and self.search: if self.config.html_use_opensearch and self.search:
logger.info(' opensearch', nonl=True) logger.info('opensearch ', nonl=True)
fn = path.join(self.outdir, '_static', 'opensearch.xml') fn = path.join(self.outdir, '_static', 'opensearch.xml')
self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
@ -669,7 +669,7 @@ class StandaloneHTMLBuilder(Builder):
'genindexcounts': indexcounts, 'genindexcounts': indexcounts,
'split_index': self.config.html_split_index, 'split_index': self.config.html_split_index,
} }
logger.info(' genindex', nonl=True) logger.info('genindex ', nonl=True)
if self.config.html_split_index: if self.config.html_split_index:
self.handle_page('genindex', genindexcontext, self.handle_page('genindex', genindexcontext,
@ -691,7 +691,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:
@ -785,7 +785,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

@ -211,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
@ -220,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:
@ -257,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
targetname = '%s.%s' % (name, 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) 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

@ -224,6 +224,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

@ -1302,6 +1302,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):
""" """
@ -1938,6 +1943,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
""" """

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

@ -166,14 +166,28 @@ 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:
return "%s[%s]" % (self.visit(node.value), self.visit(node.slice)) 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: def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
return "%s %s" % (self.visit(node.op), self.visit(node.operand)) return "%s %s" % (self.visit(node.op), self.visit(node.operand))
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

@ -109,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

@ -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

@ -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'