Merge pull request #7941 from tk0miya/7901_resolve_types_for_overloaded_funcs

Fix #7901: autodoc: annotations for overloaded functions are not resolved
This commit is contained in:
Takeshi KOMIYA 2020-07-12 14:31:17 +09:00 committed by GitHub
commit 1960833d99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 74 additions and 7 deletions

View File

@ -31,6 +31,7 @@ Bugs fixed
* #7886: autodoc: TypeError is raised on mocking generic-typed classes
* #7935: autodoc: function signature is not shown when the function has a
parameter having ``inspect._empty`` as its default value
* #7901: autodoc: type annotations for overloaded functions are not resolved
* #7839: autosummary: cannot handle umlauts in function names
* #7865: autosummary: Failed to extract summary line when abbreviations found
* #7866: autosummary: Failed to extract correct summary line when docstring

View File

@ -33,7 +33,9 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect
from sphinx.util import logging
from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature
from sphinx.util.inspect import (
evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature
)
from sphinx.util.typing import stringify as stringify_typehint
if False:
@ -1204,7 +1206,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = evaluate_signature(overload, __globals__)
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
@ -1403,7 +1407,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
sigs = []
if overloaded:
# Use signatures for overloaded methods instead of the implementation method.
method = safe_getattr(self._signature_class, self._signature_method_name, None)
__globals__ = safe_getattr(method, '__globals__', {})
for overload in self.analyzer.overloads.get(qualname):
overload = evaluate_signature(overload, __globals__)
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:],
return_annotation=Parameter.empty)
@ -1796,7 +1804,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = evaluate_signature(overload, __globals__)
if not inspect.isstaticmethod(self.object, cls=self.parent,
name=self.object_name):
parameters = list(overload.parameters.values())

View File

@ -22,13 +22,14 @@ from inspect import ( # NOQA
Parameter, isclass, ismethod, ismethoddescriptor, ismodule
)
from io import StringIO
from typing import Any, Callable, Mapping, List, Optional, Tuple
from typing import Any, Callable, Dict, Mapping, List, Optional, Tuple
from typing import cast
from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
from sphinx.pycode.ast import ast # for py35-37
from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import logging
from sphinx.util.typing import ForwardRef
from sphinx.util.typing import stringify as stringify_annotation
if sys.version_info > (3, 7):
@ -493,6 +494,46 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
__validate_parameters__=False)
def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: Dict = None
) -> inspect.Signature:
"""Evaluate unresolved type annotations in a signature object."""
def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any:
"""Evaluate unresolved type annotation."""
try:
if isinstance(annotation, str):
ref = ForwardRef(annotation, True)
annotation = ref._evaluate(globalns, localns)
if isinstance(annotation, ForwardRef):
annotation = annotation._evaluate(globalns, localns)
elif isinstance(annotation, str):
# might be a ForwardRef'ed annotation in overloaded functions
ref = ForwardRef(annotation, True)
annotation = ref._evaluate(globalns, localns)
except (NameError, TypeError):
# failed to evaluate type. skipped.
pass
return annotation
if globalns is None:
globalns = {}
if localns is None:
localns = globalns
parameters = list(sig.parameters.values())
for i, param in enumerate(parameters):
if param.annotation:
annotation = evaluate(param.annotation, globalns, localns)
parameters[i] = param.replace(annotation=annotation)
return_annotation = sig.return_annotation
if return_annotation:
return_annotation = evaluate(return_annotation, globalns, localns)
return sig.replace(parameters=parameters, return_annotation=return_annotation)
def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
show_return_annotation: bool = True) -> str:
"""Stringify a Signature object.

View File

@ -16,6 +16,21 @@ from docutils import nodes
from docutils.parsers.rst.states import Inliner
if sys.version_info > (3, 7):
from typing import ForwardRef
else:
from typing import _ForwardRef # type: ignore
class ForwardRef:
"""A pseudo ForwardRef class for py35 and py36."""
def __init__(self, arg: Any, is_argument: bool = True) -> None:
self.arg = arg
def _evaluate(self, globalns: Dict, localns: Dict) -> Any:
ref = _ForwardRef(self.arg)
return ref._eval_type(globalns, localns)
# An entry of Directive.option_spec
DirectiveOption = Callable[[str], Any]

View File

@ -7,7 +7,7 @@ def sum(x: int, y: int) -> int:
@overload
def sum(x: float, y: float) -> float:
def sum(x: "float", y: "float") -> "float":
...
@ -29,7 +29,7 @@ class Math:
...
@overload
def sum(self, x: float, y: float) -> float:
def sum(self, x: "float", y: "float") -> "float":
...
@overload
@ -49,7 +49,7 @@ class Foo:
...
@overload
def __new__(cls, x: str, y: str) -> "Foo":
def __new__(cls, x: "str", y: "str") -> "Foo":
...
def __new__(cls, x, y):
@ -64,7 +64,7 @@ class Bar:
...
@overload
def __init__(cls, x: str, y: str) -> None:
def __init__(cls, x: "str", y: "str") -> "None":
...
def __init__(cls, x, y):
@ -77,7 +77,7 @@ class Meta(type):
...
@overload
def __call__(cls, x: str, y: str) -> Any:
def __call__(cls, x: "str", y: "str") -> "Any":
...
def __call__(cls, x, y):