Fix #7901: autodoc: annotations for overloaded functions are not resolved

So far, type annotations for overloaded functions are not resolved
because they are obtained from AST directly.  This tries to evaluate
them using a context of its function or method.
This commit is contained in:
Takeshi KOMIYA 2020-07-11 12:10:07 +09:00
parent 98a854deca
commit 916cd4c844
5 changed files with 74 additions and 7 deletions

View File

@ -25,6 +25,7 @@ Bugs fixed
----------
* #7886: autodoc: TypeError is raised on mocking generic-typed classes
* #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):
@ -487,6 +488,46 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
return inspect.Signature(parameters, return_annotation=return_annotation)
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):