Skip to content

Commit

Permalink
fix: Fix transforming elements of signatures to annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Jan 3, 2022
1 parent 8aa5ed0 commit e278c11
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 6 deletions.
1 change: 1 addition & 0 deletions config/coverage.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ omit =
src/*/__init__.py
src/*/__main__.py
tests/__init__.py
tests/tmp/*

[coverage:json]
output = htmlcov/coverage.json
33 changes: 28 additions & 5 deletions src/griffe/agents/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from __future__ import annotations

import ast
from inspect import Parameter as SignatureParameter
from inspect import Signature, getdoc, getmodule
from inspect import signature as getsignature
Expand Down Expand Up @@ -317,12 +318,14 @@ def handle_function(self, node: ObjectNode, labels: set | None = None): # noqa:
parameters = None
returns = None
else:
parameters = Parameters(*[_convert_parameter(parameter) for parameter in signature.parameters.values()])
parameters = Parameters(
*[_convert_parameter(parameter, parent=self.current) for parameter in signature.parameters.values()]
)
return_annotation = signature.return_annotation
if return_annotation is empty:
returns = None
else:
returns = return_annotation and get_annotation(return_annotation, parent=self.current)
returns = _convert_object_to_annotation(return_annotation, parent=self.current)

function = Function(
name=node.name,
Expand Down Expand Up @@ -393,15 +396,35 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Name | Expression
}


def _convert_parameter(parameter):
def _convert_parameter(parameter, parent):
name = parameter.name
if parameter.annotation is empty:
annotation = None
else:
annotation = parameter.annotation
annotation = _convert_object_to_annotation(parameter.annotation, parent=parent)
kind = _kind_map[parameter.kind]
if parameter.default is empty:
default = None
else:
default = parameter.default
default = repr(parameter.default)
return Parameter(name, annotation=annotation, kind=kind, default=default)


def _convert_object_to_annotation(obj, parent):
# even when *we* import future annotations,
# the object from which we get a signature
# can come from modules which did *not* import them,
# so inspect.signature returns actual Python objects
# that we must deal with
if not isinstance(obj, str):
if hasattr(obj, "__name__"):
# simple types like int, str, custom classes, etc.
obj = obj.__name__
else:
# other, more complex types: hope for the best
obj = repr(obj)
try:
annotation_node = compile(obj, mode="eval", filename="<>", flags=ast.PyCF_ONLY_AST, optimize=2)
except SyntaxError:
return obj
return get_annotation(annotation_node.body, parent=parent)
3 changes: 3 additions & 0 deletions src/griffe/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def __init__(self, source: str, full: str | Callable) -> None:
self._full = ""
self._resolver = full

def __eq__(self, other: Name) -> bool: # type: ignore[override]
return self.source == other.source and self.full == other.full # noqa: WPS437

def __repr__(self) -> str:
return f"Name(source={self.source!r}, full={self.full!r})"

Expand Down
44 changes: 44 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""General helpers for tests."""

from __future__ import annotations

import tempfile
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator

from griffe.agents.inspector import inspect
from griffe.dataclasses import Module
from tests import TESTS_DIR, TMP_DIR


@contextmanager
def temporary_pyfile(code: str) -> Iterator[tuple[str, Path]]:
"""Create a module.py file containing the given code in a temporary directory.
Parameters:
code: The code to write to the temporary file.
Yields:
module_name: The module name, as to dynamically import it.
module_path: The module path.
"""
with tempfile.TemporaryDirectory(dir=TMP_DIR) as tmpdir:
tmpdirpath = Path(tmpdir).relative_to(TESTS_DIR.parent)
tmpfile = tmpdirpath / "module.py"
tmpfile.write_text(code)
yield ".".join(tmpdirpath.parts) + ".module", tmpfile


@contextmanager
def temporary_inspected_module(code: str) -> Iterator[Module]:
"""Create and inspect a temporary module with the given code.
Parameters:
code: The code of the module.
Yields:
The inspected module.
"""
with temporary_pyfile(code) as (name, path):
yield inspect(name, filepath=path)
2 changes: 1 addition & 1 deletion tests/test_docstrings/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,5 @@ def assert_element_equal(actual: DocstringElement, expected: DocstringElement) -
actual: The actual element.
expected: The expected element.
"""
assert actual.annotation == expected.annotation
assert actual.annotation == expected.annotation # type: ignore[operator]
assert actual.description == expected.description
23 changes: 23 additions & 0 deletions tests/test_inspector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Test inspection mechanisms."""


from griffe.expressions import Name
from tests.helpers import temporary_inspected_module


def test_annotations_from_builtin_types():
"""Assert builtin types are correctly transformed to annotations."""
with temporary_inspected_module("def func(a: int) -> str: pass") as module:
func = module["func"]
assert func.parameters[0].name == "a"
assert func.parameters[0].annotation == Name("int", full="int")
assert func.returns == Name("str", full="str")


def test_annotations_from_classes():
"""Assert custom classes are correctly transformed to annotations."""
with temporary_inspected_module("class A: pass\ndef func(a: A) -> A: pass") as module:
func = module["func"]
assert func.parameters[0].name == "a"
assert func.parameters[0].annotation == Name("A", full=f"{module.name}.A")
assert func.returns == Name("A", full=f"{module.name}.A")

0 comments on commit e278c11

Please sign in to comment.