diff --git a/CHANGES b/CHANGES index 7c2748b98..f67879019 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 9300a2cce..31f390a99 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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()) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 441f850d1..d34c9cb5d 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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. diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 18b363eca..86f9c6e5c 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -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] diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py index da43d32eb..cc4e509f2 100644 --- a/tests/roots/test-ext-autodoc/target/overload.py +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -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):