Merge branch '3.x' into 7964_tuple_in_signature

This commit is contained in:
Takeshi KOMIYA 2020-10-04 11:53:41 +09:00 committed by GitHub
commit 3096b71c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 399 additions and 216 deletions

View File

@ -34,14 +34,18 @@ 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
* #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``
Testing
--------

View File

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

View File

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

View File

@ -517,7 +517,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)
@ -570,7 +570,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'))

View File

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

View File

@ -1213,7 +1213,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"),
@ -1262,7 +1263,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)
@ -1271,7 +1274,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)
@ -1392,7 +1395,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
@ -1407,7 +1411,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
@ -1417,7 +1422,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
@ -1428,7 +1434,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
@ -1475,7 +1482,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:],
@ -1820,11 +1828,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"),
@ -1884,7 +1894,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())
@ -1897,7 +1909,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)
@ -2237,6 +2249,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')

View File

@ -439,8 +439,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
@ -470,7 +470,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])

View File

@ -63,7 +63,11 @@ def is_system_TypeVar(typ: Any) -> bool:
def stringify(annotation: Any) -> str:
"""Stringify type annotation object."""
if isinstance(annotation, str):
return annotation
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__
elif not annotation:
@ -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])

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.
"""
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")

View File

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

View File

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