Skip to content

Commit

Permalink
Support basic type annotations
Browse files Browse the repository at this point in the history
Closes #139
  • Loading branch information
AWhetter committed Apr 22, 2019
1 parent 43250fb commit b90284f
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 7 deletions.
38 changes: 35 additions & 3 deletions autoapi/mappers/python/astroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,19 @@ def get_assign_value(node):
Assignments to multiple names are ignored, as per PEP 257.
:param node: The node to get the assignment value from.
:type node: astroid.nodes.Assign
:type node: astroid.nodes.Assign or astroid.nodes.AnnAssign
:returns: The name that is assigned to,
and the value assigned to the name (if it can be converted).
:rtype: tuple(str, object or None) or None
"""
if len(node.targets) == 1:
target = node.targets[0]
try:
targets = node.targets
except AttributeError:
targets = [node.target]

if len(targets) == 1:
target = targets[0]
if isinstance(target, astroid.nodes.AssignName):
name = target.name
elif isinstance(target, astroid.nodes.AssignAttr):
Expand All @@ -170,6 +175,33 @@ def get_assign_value(node):
return None


def get_assign_annotation(node):
"""Get the type annotation of the assignment of the given node.
:param node: The node to get the annotation for.
:type node: astroid.nodes.Assign or astroid.nodes.AnnAssign
:returns: The type annotation as a string, or None if one does not exist.
:type: str or None
"""
annotation = None

annotation_node = None
try:
annotation_node = node.annotation
except AttributeError:
# Python 2 has no support for type annotations, so use getattr
annotation_node = getattr(node, "type_annotation", None)

if annotation_node:
if isinstance(annotation_node, astroid.nodes.Const):
annotation = node.value
else:
annotation = annotation_node.as_string()

return annotation


def is_decorated_with_property(node):
"""Check if the function is decorated as a property.
Expand Down
22 changes: 21 additions & 1 deletion autoapi/mappers/python/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,20 @@ class PythonFunction(PythonPythonMapper):
is_callable = True
ref_directive = "func"

def __init__(self, obj, **kwargs):
super(PythonFunction, self).__init__(obj, **kwargs)

self.return_annotation = obj["return_annotation"]
"""The type annotation for the return type of this function.
This will be ``None`` if an annotation
or annotation comment was not given.
:type: str or None
"""

class PythonMethod(PythonPythonMapper):

class PythonMethod(PythonFunction):
type = "method"
is_callable = True
ref_directive = "meth"
Expand Down Expand Up @@ -177,6 +189,14 @@ def __init__(self, obj, **kwargs):
:type: str or None
"""
self.annotation = obj.get("annotation")
"""The type annotation of this attribute.
This will be ``None`` if an annotation
or annotation comment was not given.
:type: str or None
"""


class PythonAttribute(PythonData):
Expand Down
16 changes: 15 additions & 1 deletion autoapi/mappers/python/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def parse_file(self, file_path):
node = astroid.MANAGER.ast_from_file(file_path, module_name, source=True)
return self.parse(node)

def parse_annassign(self, node):
return self.parse_assign(node)

def parse_assign(self, node):
doc = ""
doc_node = node.next_sibling()
Expand Down Expand Up @@ -72,6 +75,8 @@ def parse_assign(self, node):
if sys.version_info[0] >= 3:
raise

annotation = astroid_utils.get_assign_annotation(node)

data = {
"type": type_,
"name": target,
Expand All @@ -80,6 +85,7 @@ def parse_assign(self, node):
"value": value,
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"annotation": annotation,
}

return [data]
Expand Down Expand Up @@ -141,6 +147,13 @@ def parse_functiondef(self, node):

type_ = "function" if node.type == "function" else "method"

return_annotation = None
if node.returns:
return_annotation = node.returns.as_string()
# Python 2 has no support for type annotations, so use getattr
elif getattr(node, "type_comment_returns", None):
return_annotation = node.type_comment_returns.as_string()

data = {
"type": type_,
"name": node.name,
Expand All @@ -149,6 +162,7 @@ def parse_functiondef(self, node):
"doc": self._encode(node.doc or ""),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"return_annotation": return_annotation,
}

if type_ == "method":
Expand All @@ -158,7 +172,7 @@ def parse_functiondef(self, node):

if node.name == "__init__":
for child in node.get_children():
if isinstance(child, astroid.Assign):
if isinstance(child, (astroid.nodes.Assign, astroid.nodes.AnnAssign)):
child_data = self.parse_assign(child)
result.extend(data for data in child_data if data["doc"])

Expand Down
2 changes: 1 addition & 1 deletion autoapi/templates/python/data.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% if obj.display %}
.. {{ obj.type }}:: {{ obj.name }}
{%+ if obj.value is not none %}:annotation: = {{ obj.value }}{% endif %}
{%+ if obj.value is not none or obj.annotation is not none %}:annotation:{% if obj.annotation %} :{{ obj.annotation }}{% endif %}{% if obj.value is not none %} = {{ obj.value }}{% endif %}{% endif %}
{{ obj.docstring|prepare_docstring|indent(3) }}
Expand Down
2 changes: 1 addition & 1 deletion autoapi/templates/python/function.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% if obj.display %}
.. function:: {{ obj.short_name }}({{ obj.args }})
.. function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}

{% if obj.docstring %}
{{ obj.docstring|prepare_docstring|indent(3) }}
Expand Down
21 changes: 21 additions & 0 deletions tests/python/pyannotationcommentsexample/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-

templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = u"pyexample"
copyright = u"2015, rtfd"
author = u"rtfd"
version = "0.1"
release = "0.1"
language = None
exclude_patterns = ["_build"]
pygments_style = "sphinx"
todo_include_todos = False
html_theme = "alabaster"
html_static_path = ["_static"]
htmlhelp_basename = "pyexampledoc"
extensions = ["sphinx.ext.autodoc", "autoapi.extension"]
autoapi_type = "python"
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"
41 changes: 41 additions & 0 deletions tests/python/pyannotationcommentsexample/example/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""Example module
This is a description
"""
from typing import ClassVar, Dict, Iterable, List, Union

max_rating = 10 # type: int

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
): # type: (...) -> Iterable[int]
i = start
while i < end:
yield i
i += 1


mixed_list = [1, "two", 3] # type: List[Union[str, int]]


def f2(not_yet_a):
# type: (A) -> int
pass


class A:
is_an_a = True # type: ClassVar[bool]

def __init__(self):
self.instance_var = True # type: bool
"""This is an instance_var."""


global_a = A() # type: A
26 changes: 26 additions & 0 deletions tests/python/pyannotationcommentsexample/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.. pyexample documentation master file, created by
sphinx-quickstart on Fri May 29 13:34:37 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyexample's documentation!
=====================================

.. toctree::

autoapi/index

Contents:

.. toctree::
:maxdepth: 2



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

21 changes: 21 additions & 0 deletions tests/python/pyannotationsexample/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-

templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = u"pyexample"
copyright = u"2015, rtfd"
author = u"rtfd"
version = "0.1"
release = "0.1"
language = None
exclude_patterns = ["_build"]
pygments_style = "sphinx"
todo_include_todos = False
html_theme = "alabaster"
html_static_path = ["_static"]
htmlhelp_basename = "pyexampledoc"
extensions = ["sphinx.ext.autodoc", "autoapi.extension"]
autoapi_type = "python"
autoapi_dirs = ["example"]
autoapi_file_pattern = "*.py"
44 changes: 44 additions & 0 deletions tests/python/pyannotationsexample/example/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Example module
This is a description
"""
from typing import ClassVar, Dict, Iterable, List, Union

max_rating: int = 10

is_valid: bool
if max_rating > 100:
is_valid = False
else:
is_valid = True

ratings: List[int] = [0, 1, 2, 3, 4, 5]

rating_names: Dict[int, str] = {0: "zero", 1: "one"}


def f(start: int, end: int) -> Iterable[int]:
i = start
while i < end:
yield i
i += 1


mixed_list: List[Union[str, int]] = [1, "two", 3]


def f2(not_yet_a: "A") -> int:
...


class A:
is_an_a: ClassVar[bool] = True
not_assigned_to: ClassVar[str]

def __init__(self):
self.instance_var: bool = True
"""This is an instance_var."""


global_a: A = A()
26 changes: 26 additions & 0 deletions tests/python/pyannotationsexample/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.. pyexample documentation master file, created by
sphinx-quickstart on Fri May 29 13:34:37 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to pyexample's documentation!
=====================================

.. toctree::

autoapi/index

Contents:

.. toctree::
:maxdepth: 2



Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Loading

0 comments on commit b90284f

Please sign in to comment.