Merge tag 'v3.1.0'

This commit is contained in:
Takeshi KOMIYA 2020-07-05 00:10:47 +09:00
commit 44f4b2ad97
31 changed files with 731 additions and 96 deletions

45
CHANGES
View File

@ -36,8 +36,8 @@ Bugs fixed
Testing
--------
Release 3.1.0 (in development)
==============================
Release 3.1.0 (released Jun 08, 2020)
=====================================
Dependencies
------------
@ -89,6 +89,8 @@ Features added
builtin base classes
* #2106: autodoc: Support multiple signatures on docstring
* #4422: autodoc: Support GenericAlias in Python 3.7 or above
* #3610: autodoc: Support overloaded functions
* #7722: autodoc: Support TypeVar
* #7466: autosummary: headings in generated documents are not translated
* #7490: autosummary: Add ``:caption:`` option to autosummary directive to set a
caption to the toctree
@ -99,7 +101,8 @@ Features added
variables for custom templates
* #7530: html: Support nested <kbd> elements
* #7481: html theme: Add right margin to footnote/citation labels
* #7482: html theme: CSS spacing for code blocks with captions and line numbers
* #7482, #7717: html theme: CSS spacing for code blocks with captions and line
numbers
* #7443: html theme: Add new options :confval:`globaltoc_collapse` and
:confval:`globaltoc_includehidden` to control the behavior of globaltoc in
sidebar
@ -111,6 +114,8 @@ Features added
* #7542: html theme: Make admonition/topic/sidebar scrollable
* #7543: html theme: Add top and bottom margins to tables
* #7695: html theme: Add viewport meta tag for basic theme
* #7721: html theme: classic: default codetextcolor/codebgcolor doesn't override
Pygments
* C and C++: allow semicolon in the end of declarations.
* C++, parse parameterized noexcept specifiers.
* #7294: C++, parse expressions with user-defined literals.
@ -118,8 +123,13 @@ Features added
* #7143: py domain: Add ``:final:`` option to :rst:dir:`py:class:`,
:rst:dir:`py:exception:` and :rst:dir:`py:method:` directives
* #7596: py domain: Change a type annotation for variables to a hyperlink
* #7770: std domain: :rst:dir:`option` directive support arguments in the form
of ``foo[=bar]``
* #7582: napoleon: a type for attribute are represented like type annotation
* #7734: napoleon: overescaped trailing underscore on attribute
* #7247: linkcheck: Add :confval:`linkcheck_request_headers` to send custom HTTP
headers for specific host
* #7792: setuptools: Support ``--verbosity`` option
* #7683: Add ``allowed_exceptions`` parameter to ``Sphinx.emit()`` to allow
handlers to raise specified exceptions
* #7295: C++, parse (trailing) requires clauses.
@ -151,6 +161,7 @@ Bugs fixed
* #7668: autodoc: wrong retann value is passed to a handler of
autodoc-proccess-signature
* #7711: autodoc: fails with ValueError when processing numpy objects
* #7791: autodoc: TypeError is raised on documenting singledispatch function
* #7551: autosummary: a nested class is indexed as non-nested class
* #7661: autosummary: autosummary directive emits warnings twices if failed to
import the target module
@ -159,8 +170,12 @@ Bugs fixed
* #7671: autosummary: The location of import failure warning is missing
* #7535: sphinx-autogen: crashes when custom template uses inheritance
* #7536: sphinx-autogen: crashes when template uses i18n feature
* #7781: sphinx-build: Wrong error message when outdir is not directory
* #7653: sphinx-quickstart: Fix multiple directory creation for nested relpath
* #2785: html: Bad alignment of equation links
* #7718: html theme: some themes does not respect background color of Pygments
style (agogo, haiku, nature, pyramid, scrolls, sphinxdoc and traditional)
* #7544: html theme: inconsistent padding in admonitions
* #7581: napoleon: bad parsing of inline code in attribute docstrings
* #7628: imgconverter: runs imagemagick once unnecessary for builders not
supporting images
@ -168,7 +183,10 @@ Bugs fixed
* #7646: handle errors on event handlers
* #4187: LaTeX: EN DASH disappears from PDF bookmarks in Japanese documents
* #7701: LaTeX: Anonymous indirect hyperlink target causes duplicated labels
* #7723: LaTeX: pdflatex crashed when URL contains a single quote
* #7756: py domain: The default value for positional only argument is not shown
* #7760: coverage: Add :confval:`coverage_show_missing_items` to show coverage
result to console
* C++, fix rendering and xrefs in nested names explicitly starting
in global scope, e.g., ``::A::B``.
* C, fix rendering and xrefs in nested names explicitly starting
@ -176,30 +194,9 @@ Bugs fixed
* #7763: C and C++, don't crash during display stringification of unary
expressions and fold expressions.
Testing
--------
Release 3.0.5 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 3.0.4 (released May 27, 2020)
=====================================

View File

@ -2388,6 +2388,32 @@ Options for the linkcheck builder
.. versionadded:: 1.1
.. confval:: linkcheck_request_headers
A dictionary that maps baseurls to HTTP request headers.
The key is a URL base string like ``"https://sphinx-doc.org/"``. To specify
headers for other hosts, ``"*"`` can be used. It matches all hosts only when
the URL does not match other settings.
The value is a dictionary that maps header name to its value.
Example:
.. code-block:: python
linkcheck_request_headers = {
"https://sphinx-doc.org/": {
"Accept": "text/html",
"Accept-Encoding": "utf-8",
},
"*": {
"Accept": "text/html,application/xhtml+xml",
}
}
.. versionadded:: 3.1
.. confval:: linkcheck_retries
The number of times the linkcheck builder will attempt to check a URL before

View File

@ -51,4 +51,11 @@ should check:
.. versionadded:: 1.1
.. _Python regular expressions: https://docs.python.org/library/re
.. confval:: coverage_show_missing_items
Print objects that are missing to standard output also.
``False`` by default.
.. versionadded:: 3.1
.. _Python regular expressions: https://docs.python.org/library/re

View File

@ -161,7 +161,7 @@ class Sphinx:
if path.exists(self.outdir) and not path.isdir(self.outdir):
raise ApplicationError(__('Output directory (%s) is not a directory') %
self.srcdir)
self.outdir)
if self.srcdir == self.outdir:
raise ApplicationError(__('Source directory and destination '

View File

@ -16,7 +16,7 @@ import threading
from html.parser import HTMLParser
from os import path
from typing import Any, Dict, List, Set, Tuple
from urllib.parse import unquote
from urllib.parse import unquote, urlparse
from docutils import nodes
from docutils.nodes import Node
@ -36,6 +36,11 @@ from sphinx.util.requests import is_ssl_error
logger = logging.getLogger(__name__)
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
}
class AnchorCheckParser(HTMLParser):
"""Specialized HTML parser that looks for a specific anchor."""
@ -107,13 +112,25 @@ class CheckExternalLinksBuilder(Builder):
def check_thread(self) -> None:
kwargs = {
'allow_redirects': True,
'headers': {
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
},
}
} # type: Dict
if self.app.config.linkcheck_timeout:
kwargs['timeout'] = self.app.config.linkcheck_timeout
def get_request_headers() -> Dict:
url = urlparse(uri)
candidates = ["%s://%s" % (url.scheme, url.netloc),
"%s://%s/" % (url.scheme, url.netloc),
uri,
"*"]
for u in candidates:
if u in self.config.linkcheck_request_headers:
headers = dict(DEFAULT_REQUEST_HEADERS)
headers.update(self.config.linkcheck_request_headers[u])
return headers
return {}
def check_uri() -> Tuple[str, str, int]:
# split off anchor
if '#' in uri:
@ -139,6 +156,9 @@ class CheckExternalLinksBuilder(Builder):
else:
auth_info = None
# update request headers for the URL
kwargs['headers'] = get_request_headers()
try:
if anchor and self.app.config.linkcheck_anchors:
# Read the whole document and see if #anchor exists
@ -337,6 +357,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('linkcheck_ignore', [], None)
app.add_config_value('linkcheck_auth', [], None)
app.add_config_value('linkcheck_request_headers', {}, None)
app.add_config_value('linkcheck_retries', 1, None)
app.add_config_value('linkcheck_timeout', None, None, [int])
app.add_config_value('linkcheck_workers', 5, None)

View File

@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
# RE for option descriptions
option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)')
option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=[]+)(=?\s*.*)')
# RE for grammar tokens
token_re = re.compile(r'`(\w+)`', re.U)

View File

@ -16,7 +16,7 @@ import warnings
from inspect import Parameter, Signature
from types import ModuleType
from typing import (
Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, Union
Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union
)
from typing import TYPE_CHECKING
@ -1178,8 +1178,14 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
self.add_line(' :async:', sourcename)
def format_signature(self, **kwargs: Any) -> str:
sig = super().format_signature(**kwargs)
sigs = [sig]
sigs = []
if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads:
# Use signatures for overloaded functions instead of the implementation function.
overloaded = True
else:
overloaded = False
sig = super().format_signature(**kwargs)
sigs.append(sig)
if inspect.is_singledispatch_function(self.object):
# append signature of singledispatch'ed functions
@ -1193,12 +1199,24 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
return "\n".join(sigs)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
sig = inspect.signature(func)
try:
sig = inspect.signature(func)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return
except ValueError:
return
if len(sig.parameters) == 0:
return
@ -1255,6 +1273,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
'private-members': bool_option, 'special-members': members_option,
} # type: Dict[str, Callable]
_signature_class = None # type: Any
_signature_method_name = None # type: str
def __init__(self, *args: Any) -> None:
super().__init__(*args)
merge_special_members_option(self.options)
@ -1275,7 +1296,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.doc_as_attr = True
return ret
def _get_signature(self) -> Optional[Signature]:
def _get_signature(self) -> Tuple[Optional[Any], Optional[str], Optional[Signature]]:
def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
""" Get the `attr` function or method from `obj`, if it is user-defined. """
if inspect.is_builtin_class_method(obj, attr):
@ -1299,7 +1320,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if call is not None:
self.env.app.emit('autodoc-before-process-signature', call, True)
try:
return inspect.signature(call, bound_method=True)
sig = inspect.signature(call, bound_method=True)
return type(self.object), '__call__', sig
except ValueError:
pass
@ -1308,7 +1330,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if new is not None:
self.env.app.emit('autodoc-before-process-signature', new, True)
try:
return inspect.signature(new, bound_method=True)
sig = inspect.signature(new, bound_method=True)
return self.object, '__new__', sig
except ValueError:
pass
@ -1317,7 +1340,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if init is not None:
self.env.app.emit('autodoc-before-process-signature', init, True)
try:
return inspect.signature(init, bound_method=True)
sig = inspect.signature(init, bound_method=True)
return self.object, '__init__', sig
except ValueError:
pass
@ -1327,20 +1351,21 @@ 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:
return inspect.signature(self.object, bound_method=False)
sig = inspect.signature(self.object, bound_method=False)
return None, None, sig
except ValueError:
pass
# Still no signature: happens e.g. for old-style classes
# with __init__ in C and no `__text_signature__`.
return None
return None, None, None
def format_args(self, **kwargs: Any) -> str:
if self.env.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
try:
sig = self._get_signature()
self._signature_class, self._signature_method_name, sig = self._get_signature()
except TypeError as exc:
# __signature__ attribute contained junk
logger.warning(__("Failed to get a constructor signature for %s: %s"),
@ -1356,7 +1381,30 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if self.doc_as_attr:
return ''
return super().format_signature(**kwargs)
sig = super().format_signature()
overloaded = False
qualname = None
# TODO: recreate analyzer for the module of class (To be clear, owner of the method)
if self._signature_class and self._signature_method_name and self.analyzer:
qualname = '.'.join([self._signature_class.__qualname__,
self._signature_method_name])
if qualname in self.analyzer.overloads:
overloaded = True
sigs = []
if overloaded:
# Use signatures for overloaded methods instead of the implementation method.
for overload in self.analyzer.overloads.get(qualname):
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:],
return_annotation=Parameter.empty)
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
else:
sigs.append(sig)
return "\n".join(sigs)
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
@ -1586,6 +1634,48 @@ class GenericAliasDocumenter(DataDocumenter):
super().add_content(content)
class TypeVarDocumenter(DataDocumenter):
"""
Specialized Documenter subclass for TypeVars.
"""
objtype = 'typevar'
directivetype = 'data'
priority = DataDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return isinstance(member, TypeVar) and isattr # type: ignore
def add_directive_header(self, sig: str) -> None:
self.options.annotation = SUPPRESS # type: ignore
super().add_directive_header(sig)
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
if ignore is not None:
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
if self.object.__doc__ != TypeVar.__doc__:
return super().get_doc()
else:
return []
def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
attrs = [repr(self.object.__name__)]
for constraint in self.object.__constraints__:
attrs.append(stringify_typehint(constraint))
if self.object.__covariant__:
attrs.append("covariant=True")
if self.object.__contravariant__:
attrs.append("contravariant=True")
content = StringList([_('alias of TypeVar(%s)') % ", ".join(attrs)], source='')
super().add_content(content)
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for methods (normal, static and class).
@ -1675,8 +1765,14 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
pass
def format_signature(self, **kwargs: Any) -> str:
sig = super().format_signature(**kwargs)
sigs = [sig]
sigs = []
if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads:
# Use signatures for overloaded methods instead of the implementation method.
overloaded = True
else:
overloaded = False
sig = super().format_signature(**kwargs)
sigs.append(sig)
meth = self.parent.__dict__.get(self.objpath[-1])
if inspect.is_singledispatch_method(meth):
@ -1692,12 +1788,27 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
if not inspect.isstaticmethod(self.object, cls=self.parent,
name=self.object_name):
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:])
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
return "\n".join(sigs)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
"""Annotate type hint to the first argument of function if needed."""
sig = inspect.signature(func)
try:
sig = inspect.signature(func)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return
except ValueError:
return
if len(sig.parameters) == 1:
return
@ -1945,6 +2056,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(GenericAliasDocumenter)
app.add_autodocumenter(TypeVarDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)

View File

@ -22,6 +22,7 @@ from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.console import red # type: ignore
from sphinx.util.inspect import safe_getattr
logger = logging.getLogger(__name__)
@ -121,6 +122,14 @@ class CoverageBuilder(Builder):
write_header(op, filename)
for typ, name in sorted(undoc):
op.write(' * %-50s [%9s]\n' % (name, typ))
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
logger.warning(__('undocumented c api: %s [%s] in file %s'),
name, typ, filename)
else:
logger.info(red('undocumented ') + 'c ' + 'api ' +
'%-30s' % (name + " [%9s]" % typ) +
red(' - in file ') + filename)
op.write('\n')
def ignore_pyobj(self, full_name: str) -> bool:
@ -239,16 +248,48 @@ class CoverageBuilder(Builder):
if undoc['funcs']:
op.write('Functions:\n')
op.writelines(' * %s\n' % x for x in undoc['funcs'])
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
for func in undoc['funcs']:
logger.warning(
__('undocumented python function: %s :: %s'),
name, func)
else:
for func in undoc['funcs']:
logger.info(red('undocumented ') + 'py ' + 'function ' +
'%-30s' % func + red(' - in module ') + name)
op.write('\n')
if undoc['classes']:
op.write('Classes:\n')
for name, methods in sorted(
for class_name, methods in sorted(
undoc['classes'].items()):
if not methods:
op.write(' * %s\n' % name)
op.write(' * %s\n' % class_name)
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
logger.warning(
__('undocumented python class: %s :: %s'),
name, class_name)
else:
logger.info(red('undocumented ') + 'py ' +
'class ' + '%-30s' % class_name +
red(' - in module ') + name)
else:
op.write(' * %s -- missing methods:\n\n' % name)
op.write(' * %s -- missing methods:\n\n' % class_name)
op.writelines(' - %s\n' % x for x in methods)
if self.config.coverage_show_missing_items:
if self.app.quiet or self.app.warningiserror:
for meth in methods:
logger.warning(
__('undocumented python method:' +
' %s :: %s :: %s'),
name, class_name, meth)
else:
for meth in methods:
logger.info(red('undocumented ') + 'py ' +
'method ' + '%-30s' %
(class_name + '.' + meth) +
red(' - in module ') + name)
op.write('\n')
if failed:
@ -273,4 +314,5 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('coverage_ignore_c_items', {}, False)
app.add_config_value('coverage_write_headline', True, False)
app.add_config_value('coverage_skip_undoc_in_source', False, False)
app.add_config_value('coverage_show_missing_items', False, False)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -11,6 +11,7 @@
import re
import tokenize
from importlib import import_module
from inspect import Signature
from io import StringIO
from os import path
from typing import Any, Dict, IO, List, Tuple, Optional
@ -134,6 +135,7 @@ class ModuleAnalyzer:
self.annotations = None # type: Dict[Tuple[str, str], str]
self.attr_docs = None # type: Dict[Tuple[str, str], List[str]]
self.finals = None # type: List[str]
self.overloads = None # type: Dict[str, List[Signature]]
self.tagorder = None # type: Dict[str, int]
self.tags = None # type: Dict[str, Tuple[str, int, int]]
@ -152,6 +154,7 @@ class ModuleAnalyzer:
self.annotations = parser.annotations
self.finals = parser.finals
self.overloads = parser.overloads
self.tags = parser.definitions
self.tagorder = parser.deforders
except Exception as exc:

View File

@ -12,12 +12,14 @@ import itertools
import re
import sys
import tokenize
from inspect import Signature
from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING
from tokenize import COMMENT, NL
from typing import Any, Dict, List, Optional, Tuple
from sphinx.pycode.ast import ast # for py37 or older
from sphinx.pycode.ast import parse, unparse
from sphinx.util.inspect import signature_from_ast
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
@ -235,8 +237,10 @@ class VariableCommentPicker(ast.NodeVisitor):
self.previous = None # type: ast.AST
self.deforders = {} # type: Dict[str, int]
self.finals = [] # type: List[str]
self.overloads = {} # type: Dict[str, List[Signature]]
self.typing = None # type: str
self.typing_final = None # type: str
self.typing_overload = None # type: str
super().__init__()
def get_qualname_for(self, name: str) -> Optional[List[str]]:
@ -260,6 +264,12 @@ class VariableCommentPicker(ast.NodeVisitor):
if qualname:
self.finals.append(".".join(qualname))
def add_overload_entry(self, func: ast.FunctionDef) -> None:
qualname = self.get_qualname_for(func.name)
if qualname:
overloads = self.overloads.setdefault(".".join(qualname), [])
overloads.append(signature_from_ast(func))
def add_variable_comment(self, name: str, comment: str) -> None:
qualname = self.get_qualname_for(name)
if qualname:
@ -288,6 +298,22 @@ class VariableCommentPicker(ast.NodeVisitor):
return False
def is_overload(self, decorators: List[ast.expr]) -> bool:
overload = []
if self.typing:
overload.append('%s.overload' % self.typing)
if self.typing_overload:
overload.append(self.typing_overload)
for decorator in decorators:
try:
if unparse(decorator) in overload:
return True
except NotImplementedError:
pass
return False
def get_self(self) -> ast.arg:
"""Returns the name of first argument if in function."""
if self.current_function and self.current_function.args.args:
@ -313,6 +339,8 @@ class VariableCommentPicker(ast.NodeVisitor):
self.typing = name.asname or name.name
elif name.name == 'typing.final':
self.typing_final = name.asname or name.name
elif name.name == 'typing.overload':
self.typing_overload = name.asname or name.name
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""Handles Import node and record it to definition orders."""
@ -321,6 +349,8 @@ class VariableCommentPicker(ast.NodeVisitor):
if node.module == 'typing' and name.name == 'final':
self.typing_final = name.asname or name.name
elif node.module == 'typing' and name.name == 'overload':
self.typing_overload = name.asname or name.name
def visit_Assign(self, node: ast.Assign) -> None:
"""Handles Assign node and pick up a variable comment."""
@ -420,6 +450,8 @@ class VariableCommentPicker(ast.NodeVisitor):
self.add_entry(node.name) # should be called before setting self.current_function
if self.is_final(node.decorator_list):
self.add_final_entry(node.name)
if self.is_overload(node.decorator_list):
self.add_overload_entry(node)
self.context.append(node.name)
self.current_function = node
for child in node.body:
@ -521,6 +553,7 @@ class Parser:
self.deforders = {} # type: Dict[str, int]
self.definitions = {} # type: Dict[str, Tuple[str, int, int]]
self.finals = [] # type: List[str]
self.overloads = {} # type: Dict[str, List[Signature]]
def parse(self) -> None:
"""Parse the source code."""
@ -536,6 +569,7 @@ class Parser:
self.comments = picker.comments
self.deforders = picker.deforders
self.finals = picker.finals
self.overloads = picker.overloads
def parse_definition(self) -> None:
"""Parse the location of definitions from the code."""

View File

@ -84,6 +84,7 @@ class BuildDoc(Command):
('link-index', 'i', 'Link index.html to the master doc'),
('copyright', None, 'The copyright string'),
('pdb', None, 'Start pdb on exception'),
('verbosity', 'v', 'increase verbosity (can be repeated)'),
('nitpicky', 'n', 'nit-picky mode, warn about all missing references'),
('keep-going', None, 'With -W, keep going when getting warnings'),
]
@ -189,7 +190,7 @@ class BuildDoc(Command):
builder, confoverrides, status_stream,
freshenv=self.fresh_env,
warningiserror=self.warning_is_error,
keep_going=self.keep_going)
verbosity=self.verbosity, keep_going=self.keep_going)
app.build(force_all=self.all_files)
if app.statuscode:
raise DistutilsExecError(

View File

@ -207,7 +207,6 @@ div.document .section:first-child {
div.document div.highlight {
padding: 3px;
background-color: #eeeeec;
border-top: 2px solid #dddddd;
border-bottom: 2px solid #dddddd;
margin-top: .8em;

View File

@ -316,7 +316,7 @@ img.align-default, .figure.align-default {
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px 7px 0 7px;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
@ -336,7 +336,7 @@ div.admonition, div.topic, pre, div[class|="highlight"] {
div.topic {
border: 1px solid #ccc;
padding: 7px 7px 0 7px;
padding: 7px;
margin: 10px 0 10px 0;
overflow-x: auto;
}
@ -360,10 +360,6 @@ div.admonition dt {
font-weight: bold;
}
div.admonition dl {
margin-bottom: 0;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
@ -374,6 +370,14 @@ div.body p.centered {
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
@ -426,13 +430,13 @@ table.citation td {
border-bottom: none;
}
th > p:first-child,
td > p:first-child {
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > p:last-child,
td > p:last-child {
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
@ -478,6 +482,10 @@ table.field-list td, table.field-list th {
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
@ -505,14 +513,30 @@ ol.upperroman {
list-style: upper-roman;
}
li > p:first-child {
ol > li:first-child > :first-child,
ul > li:first-child > :first-child {
margin-top: 0px;
}
li > p:last-child {
ol ol > li:first-child > :first-child,
ol ul > li:first-child > :first-child,
ul ol > li:first-child > :first-child,
ul ul > li:first-child > :first-child {
margin-top: revert;
}
ol > li:last-child > :last-child,
ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol ol > li:last-child > :last-child,
ol ul > li:last-child > :last-child,
ul ol > li:last-child > :last-child,
ul ul > li:last-child > :last-child {
margin-bottom: revert;
}
dl.footnote > dt,
dl.citation > dt {
float: left;
@ -557,7 +581,7 @@ dl {
margin-bottom: 15px;
}
dd > p:first-child {
dd > :first-child {
margin-top: 0px;
}
@ -571,6 +595,11 @@ dd {
margin-left: 30px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
@ -655,6 +684,10 @@ span.pre {
hyphens: none;
}
div[class^="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
@ -663,7 +696,6 @@ td.linenos pre {
table.highlighttable {
display: block;
margin: 1em 0;
}
table.highlighttable tbody {
@ -680,7 +712,7 @@ table.highlighttable td {
}
table.highlighttable td.linenos {
padding: 0 0.5em;
padding-right: 0.5em;
}
table.highlighttable td.code {
@ -692,11 +724,12 @@ table.highlighttable td.code {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div > table.highlighttable {
div.code-block-caption + div {
margin-top: 0;
}
@ -710,10 +743,6 @@ div.code-block-caption code {
background-color: transparent;
}
div.code-block-caption + div > div.highlight > pre {
margin-top: 0;
}
table.highlighttable td.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;

View File

@ -25,8 +25,8 @@ headtextcolor = #20435c
headlinkcolor = #c60f0f
linkcolor = #355f7c
visitedlinkcolor = #355f7c
codebgcolor = #eeffcc
codetextcolor = #333333
codebgcolor = unset
codetextcolor = unset
bodyfont = sans-serif
headfont = 'Trebuchet MS', sans-serif

View File

@ -319,7 +319,6 @@ pre {
border-width: thin;
margin: 0 0 12px 0;
padding: 0.8em;
background-color: #f0f0f0;
}
hr {

View File

@ -184,10 +184,6 @@ div.admonition p.admonition-title + p {
display: inline;
}
div.highlight{
background-color: white;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
@ -217,8 +213,6 @@ p.admonition-title:after {
pre {
padding: 10px;
background-color: White;
color: #222;
line-height: 1.2em;
border: 1px solid #C6C9CB;
font-size: 1.1em;

View File

@ -229,10 +229,6 @@ div.admonition {
padding: 10px 20px 10px 60px;
}
div.highlight{
background-color: white;
}
div.note {
border: 2px solid #7a9eec;
border-right-style: none;
@ -286,8 +282,6 @@ p.admonition-title:after {
pre {
padding: 10px;
background-color: #fafafa;
color: #222;
line-height: 1.2em;
border: 2px solid #C6C9CB;
font-size: 1.1em;

View File

@ -188,7 +188,7 @@ a:hover {
}
pre {
background: #ededed url(metal.png);
background-image: url(metal.png);
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding: 5px;

View File

@ -247,7 +247,6 @@ pre {
line-height: 120%;
padding: 0.5em;
border: 1px solid #ccc;
background-color: #f8f8f8;
}
pre a {

View File

@ -632,7 +632,6 @@ th {
pre {
font-family: monospace;
padding: 5px;
color: #00008b;
border-left: none;
border-right: none;
}

View File

@ -510,10 +510,14 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
def signature_from_str(signature: str) -> inspect.Signature:
"""Create a Signature object from string."""
module = ast.parse('def func' + signature + ': pass')
definition = cast(ast.FunctionDef, module.body[0]) # type: ignore
function = cast(ast.FunctionDef, module.body[0]) # type: ignore
# parameters
args = definition.args
return signature_from_ast(function)
def signature_from_ast(node: ast.FunctionDef) -> inspect.Signature:
"""Create a Signature object from AST *node*."""
args = node.args
defaults = list(args.defaults)
params = []
if hasattr(args, "posonlyargs"):
@ -563,7 +567,7 @@ def signature_from_str(signature: str) -> inspect.Signature:
params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD,
annotation=annotation))
return_annotation = ast_unparse(definition.returns) or Parameter.empty
return_annotation = ast_unparse(node.returns) or Parameter.empty
return inspect.Signature(params, return_annotation=return_annotation)

View File

@ -1974,7 +1974,8 @@ class LaTeXTranslator(SphinxTranslator):
# mainly, %, #, {, } and \ need escaping via a \ escape
# in \href, the tilde is allowed and must be represented literally
return self.encode(text).replace('\\textasciitilde{}', '~').\
replace('\\sphinxhyphen{}', '-')
replace('\\sphinxhyphen{}', '-').\
replace('\\textquotesingle{}', "'")
def visit_Text(self, node: Text) -> None:
text = self.encode(node.astext())

View File

@ -0,0 +1,88 @@
from typing import Any, overload
@overload
def sum(x: int, y: int) -> int:
...
@overload
def sum(x: float, y: float) -> float:
...
@overload
def sum(x: str, y: str) -> str:
...
def sum(x, y):
"""docstring"""
return x + y
class Math:
"""docstring"""
@overload
def sum(self, x: int, y: int) -> int:
...
@overload
def sum(self, x: float, y: float) -> float:
...
@overload
def sum(self, x: str, y: str) -> str:
...
def sum(self, x, y):
"""docstring"""
return x + y
class Foo:
"""docstring"""
@overload
def __new__(cls, x: int, y: int) -> "Foo":
...
@overload
def __new__(cls, x: str, y: str) -> "Foo":
...
def __new__(cls, x, y):
pass
class Bar:
"""docstring"""
@overload
def __init__(cls, x: int, y: int) -> None:
...
@overload
def __init__(cls, x: str, y: str) -> None:
...
def __init__(cls, x, y):
pass
class Meta(type):
@overload
def __call__(cls, x: int, y: int) -> Any:
...
@overload
def __call__(cls, x: str, y: str) -> Any:
...
def __call__(cls, x, y):
pass
class Baz(metaclass=Meta):
"""docstring"""

View File

@ -0,0 +1,15 @@
from typing import TypeVar
#: T1
T1 = TypeVar("T1")
T2 = TypeVar("T2") # A TypeVar not having doc comment
#: T3
T3 = TypeVar("T3", int, str)
#: T4
T4 = TypeVar("T4", covariant=True)
#: T5
T5 = TypeVar("T5", contravariant=True)

View File

@ -180,7 +180,9 @@ Others
.. option:: arg
Link to :option:`perl +p`, :option:`--ObjC++`, :option:`--plugin.option`, :option:`create-auth-token` and :option:`arg`
.. option:: -j[=N]
Link to :option:`perl +p`, :option:`--ObjC++`, :option:`--plugin.option`, :option:`create-auth-token`, :option:`arg` and :option:`-j`
.. program:: hg

View File

@ -331,6 +331,8 @@ def test_html4_output(app, status, warning):
'create-auth-token'),
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span",
'arg'),
(".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span",
'-j'),
(".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",
'hg'),
(".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span",

View File

@ -124,3 +124,36 @@ def test_auth(app, status, warning):
assert c_kwargs['auth'] == 'authinfo2'
else:
assert not c_kwargs['auth']
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck', freshenv=True,
confoverrides={'linkcheck_request_headers': {
"https://localhost:7777/": {
"Accept": "text/html",
},
"http://www.sphinx-doc.org": { # no slash at the end
"Accept": "application/json",
},
"*": {
"X-Secret": "open sesami",
}
}})
def test_linkcheck_request_headers(app, status, warning):
mock_req = mock.MagicMock()
mock_req.return_value = 'fake-response'
with mock.patch.multiple('requests', get=mock_req, head=mock_req):
app.builder.build_all()
for args, kwargs in mock_req.call_args_list:
url = args[0]
headers = kwargs.get('headers', {})
if "https://localhost:7777" in url:
assert headers["Accept"] == "text/html"
elif 'http://www.sphinx-doc.org' in url:
assert headers["Accept"] == "application/json"
elif 'https://www.google.com' in url:
assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"
assert headers["X-Secret"] == "open sesami"
else:
assert headers["Accept"] == "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8"

View File

@ -1618,6 +1618,46 @@ def test_autodoc_GenericAlias(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_TypeVar(app):
options = {"members": None,
"undoc-members": None}
actual = do_autodoc(app, 'module', 'target.typevar', options)
assert list(actual) == [
'',
'.. py:module:: target.typevar',
'',
'',
'.. py:data:: T1',
' :module: target.typevar',
'',
' T1',
'',
" alias of TypeVar('T1')",
'',
'.. py:data:: T3',
' :module: target.typevar',
'',
' T3',
'',
" alias of TypeVar('T3', int, str)",
'',
'.. py:data:: T4',
' :module: target.typevar',
'',
' T4',
'',
" alias of TypeVar('T4', covariant=True)",
'',
'.. py:data:: T5',
' :module: target.typevar',
'',
' T5',
'',
" alias of TypeVar('T5', contravariant=True)",
]
@pytest.mark.skipif(sys.version_info < (3, 9), reason='py39+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_Annotated(app):
@ -1787,6 +1827,60 @@ def test_final(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_overload(app):
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.overload', options)
assert list(actual) == [
'',
'.. py:module:: target.overload',
'',
'',
'.. py:class:: Bar(x: int, y: int)',
' Bar(x: str, y: str)',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:class:: Baz(x: int, y: int)',
' Baz(x: str, y: str)',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:class:: Foo(x: int, y: int)',
' Foo(x: str, y: str)',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:class:: Math()',
' :module: target.overload',
'',
' docstring',
'',
'',
' .. py:method:: Math.sum(x: int, y: int) -> int',
' Math.sum(x: float, y: float) -> float',
' Math.sum(x: str, y: str) -> str',
' :module: target.overload',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int, y: int) -> int',
' sum(x: float, y: float) -> float',
' sum(x: str, y: str) -> str',
' :module: target.overload',
'',
' docstring',
'',
]
@pytest.mark.sphinx('dummy', testroot='ext-autodoc')
def test_autodoc(app, status, warning):
app.builder.build_all()

View File

@ -28,6 +28,8 @@ def test_build(app, status, warning):
assert ' * mod -- No module named mod' # in the "failed import" section
assert "undocumented py" not in status.getvalue()
c_undoc = (app.outdir / 'c.txt').read_text()
assert c_undoc.startswith('Undocumented C API elements\n'
'===========================\n')
@ -46,6 +48,8 @@ def test_build(app, status, warning):
assert 'Class' in undoc_py['autodoc_target']['classes']
assert 'undocmeth' in undoc_py['autodoc_target']['classes']['Class']
assert "undocumented c" not in status.getvalue()
@pytest.mark.sphinx('coverage', testroot='ext-coverage')
def test_coverage_ignore_pyobjects(app, status, warning):
@ -64,3 +68,28 @@ Classes:
'''
assert actual == expected
@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True})
def test_show_missing_items(app, status, warning):
app.builder.build_all()
assert "undocumented" in status.getvalue()
assert "py function raises" in status.getvalue()
assert "py class Base" in status.getvalue()
assert "py method Class.roger" in status.getvalue()
assert "c api Py_SphinxTest [ function]" in status.getvalue()
@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True})
def test_show_missing_items_quiet(app, status, warning):
app.quiet = True
app.builder.build_all()
assert "undocumented python function: autodoc_target :: raises" in warning.getvalue()
assert "undocumented python class: autodoc_target :: Base" in warning.getvalue()
assert "undocumented python method: autodoc_target :: Class :: roger" in warning.getvalue()
assert "undocumented c api: Py_SphinxTest [function]" in warning.getvalue()

View File

@ -13,6 +13,7 @@ import sys
import pytest
from sphinx.pycode.parser import Parser
from sphinx.util.inspect import signature_from_str
def test_comment_picker_basic():
@ -452,3 +453,80 @@ def test_typing_final_not_imported():
parser = Parser(source)
parser.parse()
assert parser.finals == []
def test_typing_overload():
source = ('import typing\n'
'\n'
'@typing.overload\n'
'def func(x: int, y: int) -> int: pass\n'
'\n'
'@typing.overload\n'
'def func(x: str, y: str) -> str: pass\n'
'\n'
'def func(x, y): pass\n')
parser = Parser(source)
parser.parse()
assert parser.overloads == {'func': [signature_from_str('(x: int, y: int) -> int'),
signature_from_str('(x: str, y: str) -> str')]}
def test_typing_overload_from_import():
source = ('from typing import overload\n'
'\n'
'@overload\n'
'def func(x: int, y: int) -> int: pass\n'
'\n'
'@overload\n'
'def func(x: str, y: str) -> str: pass\n'
'\n'
'def func(x, y): pass\n')
parser = Parser(source)
parser.parse()
assert parser.overloads == {'func': [signature_from_str('(x: int, y: int) -> int'),
signature_from_str('(x: str, y: str) -> str')]}
def test_typing_overload_import_as():
source = ('import typing as foo\n'
'\n'
'@foo.overload\n'
'def func(x: int, y: int) -> int: pass\n'
'\n'
'@foo.overload\n'
'def func(x: str, y: str) -> str: pass\n'
'\n'
'def func(x, y): pass\n')
parser = Parser(source)
parser.parse()
assert parser.overloads == {'func': [signature_from_str('(x: int, y: int) -> int'),
signature_from_str('(x: str, y: str) -> str')]}
def test_typing_overload_from_import_as():
source = ('from typing import overload as bar\n'
'\n'
'@bar\n'
'def func(x: int, y: int) -> int: pass\n'
'\n'
'@bar\n'
'def func(x: str, y: str) -> str: pass\n'
'\n'
'def func(x, y): pass\n')
parser = Parser(source)
parser.parse()
assert parser.overloads == {'func': [signature_from_str('(x: int, y: int) -> int'),
signature_from_str('(x: str, y: str) -> str')]}
def test_typing_overload_not_imported():
source = ('@typing.final\n'
'def func(x: int, y: int) -> int: pass\n'
'\n'
'@typing.final\n'
'def func(x: str, y: str) -> str: pass\n'
'\n'
'def func(x, y): pass\n')
parser = Parser(source)
parser.parse()
assert parser.overloads == {}

View File

@ -9,6 +9,7 @@
"""
import _testcapi
import ast
import datetime
import functools
import sys
@ -350,6 +351,38 @@ def test_signature_from_str_invalid():
inspect.signature_from_str('')
def test_signature_from_ast():
signature = 'def func(a, b, *args, c=0, d="blah", **kwargs): pass'
tree = ast.parse(signature)
sig = inspect.signature_from_ast(tree.body[0])
assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs']
assert sig.parameters['a'].name == 'a'
assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters['a'].default == Parameter.empty
assert sig.parameters['a'].annotation == Parameter.empty
assert sig.parameters['b'].name == 'b'
assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD
assert sig.parameters['b'].default == Parameter.empty
assert sig.parameters['b'].annotation == Parameter.empty
assert sig.parameters['args'].name == 'args'
assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL
assert sig.parameters['args'].default == Parameter.empty
assert sig.parameters['args'].annotation == Parameter.empty
assert sig.parameters['c'].name == 'c'
assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY
assert sig.parameters['c'].default == '0'
assert sig.parameters['c'].annotation == Parameter.empty
assert sig.parameters['d'].name == 'd'
assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY
assert sig.parameters['d'].default == "'blah'"
assert sig.parameters['d'].annotation == Parameter.empty
assert sig.parameters['kwargs'].name == 'kwargs'
assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD
assert sig.parameters['kwargs'].default == Parameter.empty
assert sig.parameters['kwargs'].annotation == Parameter.empty
assert sig.return_annotation == Parameter.empty
def test_safe_getattr_with_default():
class Foo:
def __getattr__(self, item):