From 73bb2d61053e451596d3f664e2418262dc5e38d8 Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Sun, 31 Jan 2021 10:14:52 -0800 Subject: [PATCH] The fully qualified path of objects are included type annotations --- CHANGELOG.rst | 2 + autoapi/mappers/python/astroid_utils.py | 64 ++++++++++++++++----- autoapi/mappers/python/parser.py | 6 +- tests/python/py3example/example/example.py | 7 +++ tests/python/py3example/example/example2.py | 2 + tests/python/test_pyintegration.py | 3 + 6 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 tests/python/py3example/example/example2.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8083f10..172fa2f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ V1.6.1 (TBC) Features ^^^^^^^^ +* The fully qualified path of objects are included type annotations + so that Sphinx can link to them. * Added support for Sphinx 3.3. and 3.4. diff --git a/autoapi/mappers/python/astroid_utils.py b/autoapi/mappers/python/astroid_utils.py index e4bd94d..34591fc 100644 --- a/autoapi/mappers/python/astroid_utils.py +++ b/autoapi/mappers/python/astroid_utils.py @@ -73,10 +73,11 @@ def get_full_basename(node, basename): full_basename = basename top_level_name = re.sub(r"\(.*\)", "", basename).split(".", 1)[0] - lookup_node = node - while not hasattr(lookup_node, "lookup"): - lookup_node = lookup_node.parent + lookup_node = ( + node if isinstance(node, astroid.node_classes.LookupMixIn) else node.scope() + ) assigns = lookup_node.lookup(top_level_name)[1] + for assignment in assigns: if isinstance(assignment, astroid.nodes.ImportFrom): import_name = get_full_import_name(assignment, top_level_name) @@ -403,12 +404,45 @@ def merge_annotations(annotations, comment_annotations): yield None -def _format_annotation(annotation): +def _resolve_annotation(annotation): + resolved = None + + if isinstance(annotation, astroid.Const): + resolved = get_full_basename(annotation, str(annotation.value)) + elif isinstance(annotation, astroid.Name): + resolved = get_full_basename(annotation, annotation.name) + elif isinstance(annotation, astroid.Attribute): + resolved = get_full_basename(annotation, annotation.as_string()) + elif isinstance(annotation, astroid.Subscript): + value = _resolve_annotation(annotation.value) + slice_ = _resolve_annotation(annotation.slice) + resolved = f"{value}[{slice_}]" + elif isinstance(annotation, astroid.Tuple): + resolved = ( + "(" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + ")" + ) + elif isinstance(annotation, astroid.List): + resolved = ( + "[" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + "]" + ) + else: + resolved = annotation.as_string() + + if resolved.startswith("typing."): + return resolved[len("typing.") :] + + return resolved + + +def format_annotation(annotation, parent): if annotation: - if isinstance(annotation, astroid.Const): - annotation = annotation.value - else: - annotation = annotation.as_string() + # Workaround https://github.com/PyCQA/astroid/issues/851 + if annotation.parent and not isinstance( + annotation.parent, astroid.node_classes.NodeNG + ): + annotation.parent = parent + + return _resolve_annotation(annotation) return annotation @@ -426,7 +460,7 @@ def _iter_args(args, annotations, defaults): if isinstance(arg, astroid.Tuple): name = "({})".format(", ".join(x.name for x in arg.elts)) - yield (name, _format_annotation(annotation), default) + yield (name, format_annotation(annotation, arg.parent), default) def _get_args_info(args_node): # pylint: disable=too-many-branches,too-many-statements @@ -485,9 +519,11 @@ def _get_args_info(args_node): # pylint: disable=too-many-branches,too-many-sta if args_node.vararg: annotation = None if args_node.varargannotation: - annotation = _format_annotation(args_node.varargannotation) + annotation = format_annotation(args_node.varargannotation, args_node.parent) elif len(annotations) > annotation_offset and annotations[annotation_offset]: - annotation = _format_annotation(annotations[annotation_offset]) + annotation = format_annotation( + annotations[annotation_offset], args_node.parent + ) annotation_offset += 1 result.append(("*", args_node.vararg, annotation, None)) @@ -515,9 +551,11 @@ def _get_args_info(args_node): # pylint: disable=too-many-branches,too-many-sta if args_node.kwarg: annotation = None if args_node.kwargannotation: - annotation = _format_annotation(args_node.kwargannotation) + annotation = format_annotation(args_node.kwargannotation, args_node.parent) elif len(annotations) > annotation_offset and annotations[annotation_offset]: - annotation = _format_annotation(annotations[annotation_offset]) + annotation = format_annotation( + annotations[annotation_offset], args_node.parent + ) annotation_offset += 1 result.append(("**", args_node.kwarg, annotation, None)) diff --git a/autoapi/mappers/python/parser.py b/autoapi/mappers/python/parser.py index e92f048..e14f606 100644 --- a/autoapi/mappers/python/parser.py +++ b/autoapi/mappers/python/parser.py @@ -173,10 +173,12 @@ class Parser(object): annotations = astroid_utils.get_annotations_dict(node.args) return_annotation = None if node.returns: - return_annotation = node.returns.as_string() + return_annotation = astroid_utils.format_annotation(node.returns, node) annotations["return"] = return_annotation elif node.type_comment_returns: - return_annotation = node.type_comment_returns.as_string() + return_annotation = astroid_utils.format_annotation( + node.type_comment_returns, node + ) annotations["return"] = return_annotation arg_string = astroid_utils.format_args(node.args) diff --git a/tests/python/py3example/example/example.py b/tests/python/py3example/example/example.py index dfb54d9..a433fe3 100644 --- a/tests/python/py3example/example/example.py +++ b/tests/python/py3example/example/example.py @@ -7,6 +7,9 @@ import asyncio import typing from typing import ClassVar, Dict, Iterable, List, Union, overload +from example2 import B + + max_rating: int = 10 is_valid: bool @@ -36,6 +39,10 @@ def f2(not_yet_a: "A") -> int: ... +def f3(imported: B) -> B: + ... + + @overload def overloaded_func(a: float) -> float: ... diff --git a/tests/python/py3example/example/example2.py b/tests/python/py3example/example/example2.py new file mode 100644 index 0000000..51a2e57 --- /dev/null +++ b/tests/python/py3example/example/example2.py @@ -0,0 +1,2 @@ +class B: + pass diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py index 82c7f97..ad50619 100644 --- a/tests/python/test_pyintegration.py +++ b/tests/python/test_pyintegration.py @@ -182,6 +182,9 @@ class TestPy3Module(object): assert "List[Union[str, int]]" in example_file assert "not_yet_a: A" in example_file + assert "imported: example2.B" in example_file + assert "-> example2.B" in example_file + assert "is_an_a" in example_file assert "ClassVar" in example_file