diff --git a/CHANGES b/CHANGES index 6b7f0e477..283983ced 100644 --- a/CHANGES +++ b/CHANGES @@ -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 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) ===================================== diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 48444c277..51ae2bd1a 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -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 diff --git a/doc/usage/extensions/coverage.rst b/doc/usage/extensions/coverage.rst index 46d31053c..db989f38d 100644 --- a/doc/usage/extensions/coverage.rst +++ b/doc/usage/extensions/coverage.rst @@ -51,4 +51,11 @@ should check: .. versionadded:: 1.1 -.. _Python regular expressions: https://docs.python.org/library/re \ No newline at end of file +.. 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 diff --git a/sphinx/application.py b/sphinx/application.py index 55a2889a7..4a82efcfa 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -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 ' diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 9fe689ec9..dd5317087 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -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) diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 430af3043..637dbd305 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -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) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 721211317..ae3bf1166 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index e8157848f..536b3b9d2 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -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} diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 6505f8dbb..87ac5d9ec 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -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: diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 7eb2419c0..7463249b5 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -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.""" diff --git a/sphinx/setup_command.py b/sphinx/setup_command.py index fa28c230d..36e15cd19 100644 --- a/sphinx/setup_command.py +++ b/sphinx/setup_command.py @@ -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( diff --git a/sphinx/themes/agogo/static/agogo.css_t b/sphinx/themes/agogo/static/agogo.css_t index d74604ac1..ff43186da 100644 --- a/sphinx/themes/agogo/static/agogo.css_t +++ b/sphinx/themes/agogo/static/agogo.css_t @@ -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; diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index 45908ece1..cf169f5e3 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -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; diff --git a/sphinx/themes/classic/theme.conf b/sphinx/themes/classic/theme.conf index d7389eaa0..cd16f4b47 100644 --- a/sphinx/themes/classic/theme.conf +++ b/sphinx/themes/classic/theme.conf @@ -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 diff --git a/sphinx/themes/haiku/static/haiku.css_t b/sphinx/themes/haiku/static/haiku.css_t index cac283400..21caac0fd 100644 --- a/sphinx/themes/haiku/static/haiku.css_t +++ b/sphinx/themes/haiku/static/haiku.css_t @@ -319,7 +319,6 @@ pre { border-width: thin; margin: 0 0 12px 0; padding: 0.8em; - background-color: #f0f0f0; } hr { diff --git a/sphinx/themes/nature/static/nature.css_t b/sphinx/themes/nature/static/nature.css_t index 13c64467b..34893b86a 100644 --- a/sphinx/themes/nature/static/nature.css_t +++ b/sphinx/themes/nature/static/nature.css_t @@ -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; diff --git a/sphinx/themes/pyramid/static/pyramid.css_t b/sphinx/themes/pyramid/static/pyramid.css_t index 48cb2ab6f..dafd898d5 100644 --- a/sphinx/themes/pyramid/static/pyramid.css_t +++ b/sphinx/themes/pyramid/static/pyramid.css_t @@ -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; diff --git a/sphinx/themes/scrolls/static/scrolls.css_t b/sphinx/themes/scrolls/static/scrolls.css_t index b01d4ad9f..e484f8c4f 100644 --- a/sphinx/themes/scrolls/static/scrolls.css_t +++ b/sphinx/themes/scrolls/static/scrolls.css_t @@ -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; diff --git a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t index 626d8c3f2..f9961ae36 100644 --- a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t +++ b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t @@ -247,7 +247,6 @@ pre { line-height: 120%; padding: 0.5em; border: 1px solid #ccc; - background-color: #f8f8f8; } pre a { diff --git a/sphinx/themes/traditional/static/traditional.css_t b/sphinx/themes/traditional/static/traditional.css_t index 68719dcd1..0120f83a5 100644 --- a/sphinx/themes/traditional/static/traditional.css_t +++ b/sphinx/themes/traditional/static/traditional.css_t @@ -632,7 +632,6 @@ th { pre { font-family: monospace; padding: 5px; - color: #00008b; border-left: none; border-right: none; } diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index f1efeee2b..ddf72f1a4 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index b49ad6991..600c8ba13 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -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()) diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py new file mode 100644 index 000000000..da43d32eb --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -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""" diff --git a/tests/roots/test-ext-autodoc/target/typevar.py b/tests/roots/test-ext-autodoc/target/typevar.py new file mode 100644 index 000000000..9c6b0eab0 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/typevar.py @@ -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) diff --git a/tests/roots/test-root/objects.txt b/tests/roots/test-root/objects.txt index f713e076c..adb090a44 100644 --- a/tests/roots/test-root/objects.txt +++ b/tests/roots/test-root/objects.txt @@ -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 diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 6888fc52a..66b2a27ac 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -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", diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 54bde6b68..d1fec550f 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -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" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 5cc1f3c22..e4ec4a815 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -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() diff --git a/tests/test_ext_coverage.py b/tests/test_ext_coverage.py index 16f53112b..033a1c1b1 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_ext_coverage.py @@ -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() diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index 398c9f8a4..71847f04f 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -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 == {} diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index fa0ff84e1..6a14dc1ac 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -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):