diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4ec40f3..d78e340 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,20 @@ Changelog
Versions follow `Semantic Versioning `_ (``..``).
+v1.2.0 (TBC)
+------------
+
+Features
+^^^^^^^^
+
+* (Python) Can read per argument type comments with astroid > 2.2.5.
+
+Bug Fixes
+^^^^^^^^^
+
+* (Python) Forward reference annotations are no longer rendered as strings.
+
+
v1.1.0 (2019-06-23)
-------------------
diff --git a/autoapi/mappers/python/astroid_utils.py b/autoapi/mappers/python/astroid_utils.py
index 50fcbc3..b21d21d 100644
--- a/autoapi/mappers/python/astroid_utils.py
+++ b/autoapi/mappers/python/astroid_utils.py
@@ -2,6 +2,7 @@ try:
import builtins
except ImportError:
import __builtin__ as builtins
+import itertools
import re
import sys
@@ -13,9 +14,11 @@ if sys.version_info < (3,):
_EXCEPTIONS_MODULE = "exceptions"
# getattr to keep linter happy
_STRING_TYPES = getattr(builtins, "basestring")
+ _zip_longest = itertools.izip_longest
else:
_EXCEPTIONS_MODULE = "builtins"
_STRING_TYPES = str
+ _zip_longest = itertools.zip_longest
def resolve_import_alias(name, import_names):
@@ -359,3 +362,106 @@ def get_module_all(node):
all_.append(elt_name.value)
return all_
+
+
+def merge_annotations(annotations, comment_annotations):
+ for ann, comment_ann in _zip_longest(annotations, comment_annotations):
+ if not ann or isinstance(ann, astroid.Ellipsis):
+ yield comment_ann
+ else:
+ yield ann
+
+
+def _format_args(args, defaults=None, annotations=None):
+ values = []
+
+ if args is None:
+ return ""
+
+ if annotations is None:
+ annotations = []
+
+ if defaults is not None:
+ default_offset = len(args) - len(defaults)
+
+ packed = _zip_longest(args, annotations)
+ for i, (arg, annotation) in enumerate(packed):
+ if isinstance(arg, astroid.Tuple):
+ values.append("({})".format(_format_args(arg.elts)))
+ else:
+ argname = arg.name
+ default_sep = "="
+ if annotation is not None:
+ ann_str = annotation.as_string()
+ if isinstance(annotation, astroid.Const):
+ ann_str = annotation.value
+ argname = "{}: {}".format(argname, ann_str)
+ default_sep = " = "
+ values.append(argname)
+
+ if defaults is not None and i >= default_offset:
+ if defaults[i - default_offset] is not None:
+ values[-1] += default_sep + defaults[i - default_offset].as_string()
+
+ return ", ".join(values)
+
+
+def format_args(args_node):
+ result = []
+ positional_only_defaults = []
+ positional_or_keyword_defaults = args_node.defaults
+ if args_node.defaults:
+ args = args_node.args or []
+ positional_or_keyword_defaults = args_node.defaults[-len(args) :]
+ positional_only_defaults = args_node.defaults[
+ : len(args_node.defaults) - len(args)
+ ]
+
+ plain_annotations = getattr(args_node, "annotations", ()) or ()
+ func_comment_annotations = getattr(args_node.parent, "type_comment_args", ()) or ()
+ comment_annotations = getattr(args_node, "type_comment_args", ()) or ()
+ annotations = list(
+ merge_annotations(
+ plain_annotations,
+ merge_annotations(func_comment_annotations, comment_annotations),
+ )
+ )
+
+ if getattr(args_node, "posonlyargs", None):
+ result.append(_format_args(args_node.posonlyargs, positional_only_defaults))
+ result.append("/")
+
+ if args_node.args:
+ result.append(
+ _format_args(args_node.args, positional_or_keyword_defaults, annotations)
+ )
+
+ if args_node.vararg:
+ vararg_result = "*{}".format(args_node.vararg)
+ if getattr(args_node, "varargannotation", None):
+ vararg_result = "{}: {}".format(
+ vararg_result, args_node.varargannotation.as_string()
+ )
+ result.append(vararg_result)
+
+ if getattr(args_node, "kwonlyargs", None):
+ if not args_node.vararg:
+ result.append("*")
+
+ result.append(
+ _format_args(
+ args_node.kwonlyargs,
+ args_node.kw_defaults,
+ args_node.kwonlyargs_annotations,
+ )
+ )
+
+ if args_node.kwarg:
+ kwarg_result = "**{}".format(args_node.kwarg)
+ if getattr(args_node, "kwargannotation", None):
+ kwarg_result = "{}: {}".format(
+ kwarg_result, args_node.kwargannotation.as_string()
+ )
+ result.append(kwarg_result)
+
+ return ", ".join(result)
diff --git a/autoapi/mappers/python/parser.py b/autoapi/mappers/python/parser.py
index 57d4aa5..01e2296 100644
--- a/autoapi/mappers/python/parser.py
+++ b/autoapi/mappers/python/parser.py
@@ -157,11 +157,13 @@ class Parser(object):
elif getattr(node, "type_comment_returns", None):
return_annotation = node.type_comment_returns.as_string()
+ arg_string = astroid_utils.format_args(node.args)
+
data = {
"type": type_,
"name": node.name,
"full_name": self._get_full_name(node.name),
- "args": node.args.as_string(),
+ "args": arg_string,
"doc": self._encode(node.doc or ""),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
diff --git a/tests/python/pyannotationcommentsexample/example/example.py b/tests/python/pyannotationcommentsexample/example/example.py
index 1c6eedf..648684c 100644
--- a/tests/python/pyannotationcommentsexample/example/example.py
+++ b/tests/python/pyannotationcommentsexample/example/example.py
@@ -11,7 +11,7 @@ ratings = [0, 1, 2, 3, 4, 5] # type: List[int]
rating_names = {0: "zero", 1: "one"} # type: Dict[int, str]
-# TODO: Currently unsupported by astroid (#665)
+
def f(
start, # type: int
end, # type: int
diff --git a/tests/python/test_pyintegration.py b/tests/python/test_pyintegration.py
index 591e9d6..47bc699 100644
--- a/tests/python/test_pyintegration.py
+++ b/tests/python/test_pyintegration.py
@@ -132,14 +132,12 @@ class TestPy3Module(object):
assert "Dict[int, str]" in example_file
- assert "start:int" in example_file
+ assert "start: int" in example_file
assert "Iterable[int]" in example_file
assert "List[Union[str, int]]" in example_file
- # TODO: This should not display as a string
- # after we do proper formatting
- assert "not_yet_a:'A'" in example_file
+ assert "not_yet_a: A" in example_file
assert "is_an_a" in example_file
assert "ClassVar" in example_file
@@ -180,14 +178,14 @@ class TestAnnotationCommentsModule(object):
assert "Dict[int, str]" in example_file
- # TODO: Type is currently unsupported by astroid (#665)
- assert "start" in example_file
+ # When astroid>2.2.5
+ # assert "start: int" in example_file
+ # assert "end: int" in example_file
assert "Iterable[int]" in example_file
assert "List[Union[str, int]]" in example_file
- # TODO: This should not display the type after we do proper formatting
- assert "not_yet_a" in example_file
+ assert "not_yet_a: A" in example_file
assert "is_an_a" in example_file
assert "ClassVar" in example_file