From 34d02faa2fe52727f74f23b677e84d2e0875e13b Mon Sep 17 00:00:00 2001 From: Ashley Whetter Date: Wed, 7 Aug 2019 23:23:21 -0700 Subject: [PATCH] Custom argument formatting Closes #162 --- CHANGELOG.rst | 14 +++ autoapi/mappers/python/astroid_utils.py | 106 ++++++++++++++++++ autoapi/mappers/python/parser.py | 4 +- .../example/example.py | 2 +- tests/python/test_pyintegration.py | 14 +-- 5 files changed, 130 insertions(+), 10 deletions(-) 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