Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for overloaded functions #239

Merged
merged 5 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 12 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
repos:
- repo: https://github.com/ambv/black
 rev: 18.9b0
 hooks:
 - id: black
language_version: python3.6
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/pylint
rev: 'pylint-2.5.3'
hooks:
- id: pylint
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
rev: v3.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: trailing-whitespace
- id: end-of-file-fixer
9 changes: 6 additions & 3 deletions autoapi/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ def get_items(self, names):
for name in names:
obj = mapper.all_objects[name]
if isinstance(obj, PythonFunction):
sig = "({})".format(obj.args)
if obj.return_annotation is not None:
sig += " -> {}".format(obj.return_annotation)
if obj.overloads:
sig = "(\u2026)"
else:
sig = "({})".format(obj.args)
if obj.return_annotation is not None:
sig += " \u2192 {}".format(obj.return_annotation)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of using the unicode values here?

Copy link
Contributor Author

@ciscorn ciscorn Aug 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both and \u2192 are OK. I just copied &#x2192 from Sphinx's this line.

By the way, the original autosummary doesn’t print return type annotations (maybe for saving spaces?).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why that is. But you're right to match what Sphinx is doing so let's stick with what's here!

else:
sig = ""

Expand Down
36 changes: 36 additions & 0 deletions autoapi/mappers/python/astroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,42 @@ def is_decorated_with_property_setter(node):
return False


def is_decorated_with_overload(node):
"""Check if the function is decorated as an overload definition.

:param node: The node to check.
:type node: astroid.nodes.FunctionDef

:returns: True if the function is an overload definition, False otherwise.
:rtype: bool
"""
if not node.decorators:
return False

for decorator in node.decorators.nodes:
if not isinstance(decorator, (astroid.Name, astroid.Attribute)):
continue

try:
if _is_overload_decorator(decorator):
return True
except astroid.InferenceError:
pass

return False


def _is_overload_decorator(decorator):
for inferred in decorator.infer():
if not isinstance(inferred, astroid.nodes.FunctionDef):
continue

if inferred.name == "overload" and inferred.root().name == "typing":
return True

return False


def is_constructor(node):
"""Check if the function is a constructor.

Expand Down
5 changes: 5 additions & 0 deletions autoapi/mappers/python/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ def __init__(self, obj, **kwargs):

:type: list(str)
"""
self.overloads = obj["overloads"]
"""The list of overloaded signatures ``[(args, return_annotation), ...]`` of this function.

:type: list(tuple(str, str))
"""


class PythonMethod(PythonFunction):
Expand Down
49 changes: 40 additions & 9 deletions autoapi/mappers/python/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@ def parse_classdef(self, node, data=None):
}

self._name_stack.append(node.name)
seen = set()
overridden = set()
overloads = {}
for base in itertools.chain(iter((node,)), node.ancestors()):
seen = set()
if base.qname() in ("__builtins__.object", "builtins.object"):
continue
for child in base.get_children():
Expand All @@ -138,14 +140,17 @@ def parse_classdef(self, node, data=None):
if not assign_value:
continue
name = assign_value[0]
if not name or name in seen:

if not name or name in overridden:
continue
seen.add(name)
child_data = self.parse(child)
if child_data:
for single_data in child_data:
single_data["inherited"] = base is not node
data["children"].extend(child_data)
data["children"].extend(
_parse_child(node, child_data, overloads, base, name)
)

overridden.update(seen)

self._name_stack.pop()

return [data]
Expand All @@ -159,6 +164,7 @@ def parse_functiondef(self, node): # pylint: disable=too-many-branches

type_ = "method"
properties = []

if node.type == "function":
type_ = "function"
elif astroid_utils.is_decorated_with_property(node):
Expand Down Expand Up @@ -193,6 +199,8 @@ def parse_functiondef(self, node): # pylint: disable=too-many-branches
"to_line_no": node.tolineno,
"return_annotation": return_annotation,
"properties": properties,
"is_overload": astroid_utils.is_decorated_with_overload(node),
"overloads": [],
}

if type_ in ("method", "property"):
Expand Down Expand Up @@ -249,15 +257,15 @@ def parse_module(self, node):
"all": astroid_utils.get_module_all(node),
}

overloads = {}
top_name = node.name.split(".", 1)[0]
for child in node.get_children():
if astroid_utils.is_local_import_from(child, top_name):
child_data = self._parse_local_import_from(child)
else:
child_data = self.parse(child)

if child_data:
data["children"].extend(child_data)
data["children"].extend(_parse_child(node, child_data, overloads))

return data

Expand All @@ -273,5 +281,28 @@ def parse(self, node):
data = self.parse(child)
if data:
break

return data


def _parse_child(node, child_data, overloads, base=None, name=None):
result = []
for single_data in child_data:
if single_data["type"] in ("function", "method", "property"):
if name is None:
name = single_data["name"]
if name in overloads:
grouped = overloads[name]
grouped["doc"] = single_data["doc"]
if single_data["is_overload"]:
grouped["overloads"].append(
(single_data["args"], single_data["return_annotation"])
)
continue
if single_data["is_overload"] and name not in overloads:
overloads[name] = single_data

if base:
single_data["inherited"] = base is not node
result.append(single_data)

return result
4 changes: 4 additions & 0 deletions autoapi/templates/python/function.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{% if obj.display %}
.. function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}

{% for (args, return_annotation) in obj.overloads %}
{{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}

{% endfor %}
{% if sphinx_version >= (2, 1) %}
{% for property in obj.properties %}
:{{ property }}:
Expand Down
11 changes: 9 additions & 2 deletions autoapi/templates/python/method.rst
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
{%- if obj.display %}
{% if sphinx_version >= (2, 1) %}
.. method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %}
{% if obj.properties %}

{% for (args, return_annotation) in obj.overloads %}
{{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %}

{% endfor %}
{% if obj.properties %}
{% for property in obj.properties %}
:{{ property }}:
{% endfor %}

{% else %}

{% endif %}
{% else %}
.. {{ obj.method_type }}:: {{ obj.short_name }}({{ obj.args }})
{% for (args, return_annotation) in obj.overloads %}
{{ " " * (obj.method_type | length) }} {{ obj.short_name }}({{ args }})
{% endfor %}

{% endif %}

{% if obj.docstring %}
{{ obj.docstring|prepare_docstring|indent(3) }}
{% endif %}
Expand Down
63 changes: 62 additions & 1 deletion tests/python/py3example/example/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
This is a description
"""
import asyncio
from typing import ClassVar, Dict, Iterable, List, Union
import typing
from typing import ClassVar, Dict, Iterable, List, Union, overload

max_rating: int = 10

Expand Down Expand Up @@ -35,7 +36,33 @@ def f2(not_yet_a: "A") -> int:
...


@overload
def overloaded_func(a: float) -> float:
...


@typing.overload
def overloaded_func(a: str) -> str:
...


def overloaded_func(a: Union[float, str]) -> Union[float, str]:
"""Overloaded function"""
return a * 2


@overload
def undoc_overloaded_func(a: str) -> str:
...


def undoc_overloaded_func(a: str) -> str:
return a * 2


class A:
"""class A"""

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

Expand All @@ -57,6 +84,40 @@ def my_method(self) -> str:
"""My method."""
return "method"

@overload
def overloaded_method(self, a: float) -> float:
...

@typing.overload
def overloaded_method(self, a: str) -> str:
...

def overloaded_method(self, a: Union[float, str]) -> Union[float, str]:
"""Overloaded method"""
return a * 2

@overload
def undoc_overloaded_method(self, a: float) -> float:
...

def undoc_overloaded_method(self, a: float) -> float:
return a * 2

@typing.overload
@classmethod
def overloaded_class_method(cls, a: float) -> float:
...

@overload
@classmethod
def overloaded_class_method(cls, a: str) -> str:
...

@classmethod
def overloaded_class_method(cls, a: Union[float, str]) -> Union[float, str]:
"""Overloaded class method"""
return a * 2


async def async_function(self, wait: bool) -> int:
if wait:
Expand Down
40 changes: 40 additions & 0 deletions tests/python/test_pyintegration.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@ def test_annotations(self):
if sphinx.version_info >= (2, 1):
assert "my_method(self) -> str" in example_file

def test_overload(self):
example_path = "_build/text/autoapi/example/index.txt"
with io.open(example_path, encoding="utf8") as example_handle:
example_file = example_handle.read()

assert "overloaded_func(a: float" in example_file
assert "overloaded_func(a: str" in example_file
assert "overloaded_func(a: Union" not in example_file
assert "Overloaded function" in example_file

assert "overloaded_method(self, a: float" in example_file
assert "overloaded_method(self, a: str" in example_file
assert "overloaded_method(self, a: Union" not in example_file
assert "Overloaded method" in example_file

assert "overloaded_class_method(cls, a: float" in example_file
assert "overloaded_class_method(cls, a: str" in example_file
assert "overloaded_class_method(cls, a: Union" not in example_file
assert "Overloaded method" in example_file

assert "undoc_overloaded_func" in example_file
assert "undoc_overloaded_method" in example_file

def test_async(self):
example_path = "_build/text/autoapi/example/index.txt"
with io.open(example_path, encoding="utf8") as example_handle:
Expand All @@ -190,6 +213,23 @@ def test_async(self):
assert "async_function" in example_file


@pytest.mark.skipif(
sys.version_info < (3, 6), reason="Annotations are invalid in Python <3.5"
)
def test_py3_hiding_undoc_overloaded_members(builder):
confoverrides = {"autoapi_options": ["members", "special-members"]}
builder("py3example", confoverrides=confoverrides)

example_path = "_build/text/autoapi/example/index.txt"
with io.open(example_path, encoding="utf8") as example_handle:
example_file = example_handle.read()

assert "overloaded_func" in example_file
assert "overloaded_method" in example_file
assert "undoc_overloaded_func" not in example_file
assert "undoc_overloaded_method" not in example_file


@pytest.mark.skipif(
sys.version_info < (3,), reason="Annotations are not supported in astroid<2"
)
Expand Down