Skip to content

Commit

Permalink
feat: Added handling for sequence classes (#127)
Browse files Browse the repository at this point in the history
Closes #126

### Summary of Changes

Added handling for sequence classes and also removed a few "raises" and
replaced them with UnknownType's.
  • Loading branch information
Masara committed May 4, 2024
1 parent 3477b4a commit cb061ab
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 9 deletions.
2 changes: 2 additions & 0 deletions src/safeds_stubgen/api_analyzer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
FinalType,
ListType,
LiteralType,
NamedSequenceType,
NamedType,
SetType,
TupleType,
Expand Down Expand Up @@ -58,6 +59,7 @@
"ListType",
"LiteralType",
"Module",
"NamedSequenceType",
"NamedType",
"Parameter",
"ParameterAssignment",
Expand Down
7 changes: 7 additions & 0 deletions src/safeds_stubgen/api_analyzer/_ast_visitor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import logging
from copy import deepcopy
from types import NoneType
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -941,6 +942,7 @@ def mypy_type_to_abstract_type(
name, qname = self._find_alias(missing_import_name)

if not qname: # pragma: no cover
logging.warning("Could not parse a type, added unknown type instead.")
return sds_types.UnknownType()

return sds_types.NamedType(name=name, qname=qname)
Expand Down Expand Up @@ -978,6 +980,7 @@ def mypy_type_to_abstract_type(
name, qname = self._find_alias(mypy_type.name)

if not qname: # pragma: no cover
logging.warning("Could not parse a type, added unknown type instead.")
return sds_types.UnknownType()

return sds_types.NamedType(name=name, qname=qname)
Expand Down Expand Up @@ -1009,8 +1012,12 @@ def mypy_type_to_abstract_type(
value_type=self.mypy_type_to_abstract_type(mypy_type.args[1]),
)
else:
if mypy_type.args:
types = [self.mypy_type_to_abstract_type(arg) for arg in mypy_type.args]
return sds_types.NamedSequenceType(name=type_name, qname=mypy_type.type.fullname, types=types)
return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname)

logging.warning("Could not parse a type, added unknown type instead.") # pragma: no cover
return sds_types.UnknownType() # pragma: no cover

def _find_alias(self, type_name: str) -> tuple[str, str]:
Expand Down
34 changes: 34 additions & 0 deletions src/safeds_stubgen/api_analyzer/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def from_dict(cls, d: dict[str, Any]) -> AbstractType:
return UnknownType.from_dict(d)
case NamedType.__name__:
return NamedType.from_dict(d)
case NamedSequenceType.__name__:
return NamedSequenceType.from_dict(d)
case EnumType.__name__:
return EnumType.from_dict(d)
case BoundaryType.__name__:
Expand Down Expand Up @@ -83,6 +85,38 @@ def __hash__(self) -> int:
return hash((self.name, self.qname))


@dataclass(frozen=True)
class NamedSequenceType(AbstractType):
name: str
qname: str
types: Sequence[AbstractType]

@classmethod
def from_dict(cls, d: dict[str, Any]) -> NamedSequenceType:
types = []
for element in d["types"]:
type_ = AbstractType.from_dict(element)
if type_ is not None:
types.append(type_)
return NamedSequenceType(name=d["name"], qname=d["qname"], types=types)

def to_dict(self) -> dict[str, Any]:
return {
"kind": self.__class__.__name__,
"name": self.name,
"qname": self.qname,
"types": [t.to_dict() for t in self.types],
}

def __eq__(self, other: object) -> bool:
if not isinstance(other, NamedSequenceType): # pragma: no cover
return NotImplemented
return Counter(self.types) == Counter(other.types) and self.name == other.name and self.qname == other.qname

def __hash__(self) -> int:
return hash(frozenset([self.name, self.qname, *self.types]))


@dataclass(frozen=True)
class EnumType(AbstractType):
values: frozenset[str] = field(default_factory=frozenset)
Expand Down
20 changes: 13 additions & 7 deletions src/safeds_stubgen/docstring_parsing/_docstring_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Literal

from griffe import load
Expand Down Expand Up @@ -296,7 +297,9 @@ def _griffe_annotation_to_api_type(
elif annotation.canonical_path in {"collections.abc.Callable", "typing.Callable"}:
param_type = types[0] if len(types) >= 1 else [any_type]
if not isinstance(param_type, sds_types.AbstractType): # pragma: no cover
raise TypeError(f"Expected AbstractType object, received {type(param_type)}")
msg = f"Expected AbstractType object, received {type(param_type)}. Added unknown type instead."
logging.warning(msg)
return sds_types.UnknownType()
parameter_types = param_type.types if isinstance(param_type, sds_types.ListType) else [param_type]
return_type = types[1] if len(types) >= 2 else any_type
return sds_types.CallableType(parameter_types=parameter_types, return_type=return_type)
Expand All @@ -307,8 +310,12 @@ def _griffe_annotation_to_api_type(
elif annotation.canonical_path == "typing.Optional":
types.append(sds_types.NamedType(name="None", qname="builtins.None"))
return sds_types.UnionType(types=types)
else: # pragma: no cover
raise TypeError(f"Can't parse unexpected type from docstring {annotation.canonical_path}.")
else:
return sds_types.NamedSequenceType(
name=annotation.canonical_name,
qname=annotation.canonical_path,
types=types,
)
elif isinstance(annotation, ExprList):
elements = []
for element in annotation.elements:
Expand Down Expand Up @@ -363,10 +370,9 @@ def _griffe_annotation_to_api_type(
types.append(left_type)
return sds_types.UnionType(types=types)
else: # pragma: no cover
raise TypeError(
f"Can't parse unexpected type from docstring: {annotation}. This case is not handled by us "
f"(yet), please report this.",
)
msg = f"Can't parse unexpected type from docstring: {annotation}. Added unknown type instead."
logging.warning(msg)
return sds_types.UnknownType()

def _remove_default_from_griffe_annotation(self, annotation: str) -> str:
if self.parser == Parser.numpy:
Expand Down
6 changes: 4 additions & 2 deletions src/safeds_stubgen/stubs_generator/_generate_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,17 +737,19 @@ def _create_type_string(self, type_data: dict | None) -> str:
return_type_string = f"{result_name}: {self._create_type_string(return_type)}"

return f"({', '.join(params)}) -> {return_type_string}"
elif kind in {"SetType", "ListType"}:
elif kind in {"SetType", "ListType", "NamedSequenceType"}:
types = [self._create_type_string(type_) for type_ in type_data["types"]]

# Cut out the "Type" in the kind name
name = kind[0:-4]

if name == "Set":
self._current_todo_msgs.add("no set support")
elif name == "NamedSequence":
name = type_data["name"]

if types:
if len(types) >= 2:
if len(types) >= 2 and name in {"Set", "List"}:
self._current_todo_msgs.add(name)
return f"{name}<{', '.join(types)}>"
return f"{name}<Any>"
Expand Down
19 changes: 19 additions & 0 deletions tests/data/docstring_parser_package/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any, Optional, Callable, Mapping
from enum import Enum
from tests.data.various_modules_package.another_path.another_module import AnotherClass
from tests.data.various_modules_package.type_var_module import SequenceTypeVar, SequenceTypeVar2


class ClassWithDocumentation:
Expand Down Expand Up @@ -505,3 +506,21 @@ def numpy_func_with_examples(self):
>>> func()
This text should be ignored.
"""


def numpy_sequence_types(a: SequenceTypeVar[list]) -> SequenceTypeVar2[int]:
"""
numpy_sequence_types.
Dolor sit amet.
Parameters
----------
a: SequenceTypeVar[list]
Returns
-------
named_result : SequenceTypeVar2[int]
this will be the return value
"""
pass
43 changes: 43 additions & 0 deletions tests/safeds_stubgen/api_analyzer/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FinalType,
ListType,
LiteralType,
NamedSequenceType,
NamedType,
Parameter,
ParameterAssignment,
Expand Down Expand Up @@ -206,6 +207,48 @@ def test_list_type() -> None:
assert ListType([NamedType("a", ""), NamedType("b", "")]) != ListType([NamedType("a", ""), NamedType("c", "")])


def test_named_sequence_type() -> None:
list_type = NamedSequenceType("a", "b.a", [NamedType("str", "builtins.str"), NamedType("int", "builtins.int")])
named_sequence_type_dict = {
"kind": "NamedSequenceType",
"name": "a",
"qname": "b.a",
"types": [
{"kind": "NamedType", "name": "str", "qname": "builtins.str"},
{"kind": "NamedType", "name": "int", "qname": "builtins.int"},
],
}

assert AbstractType.from_dict(named_sequence_type_dict) == list_type
assert NamedSequenceType.from_dict(named_sequence_type_dict) == list_type
assert list_type.to_dict() == named_sequence_type_dict

assert NamedSequenceType("a", "b.a", [NamedType("a", "")]) == NamedSequenceType("a", "b.a", [NamedType("a", "")])
assert hash(NamedSequenceType("a", "b.a", [NamedType("a", "")])) == hash(
NamedSequenceType("a", "b.a", [NamedType("a", "")]),
)
assert NamedSequenceType("a", "b.a", [NamedType("a", "")]) != NamedSequenceType("a", "b.a", [NamedType("b", "")])
assert hash(NamedSequenceType("a", "b.a", [NamedType("a", "")])) != hash(
NamedSequenceType("a", "b.a", [NamedType("b", "")]),
)

assert NamedSequenceType("a", "b.a", [NamedType("a", ""), LiteralType(["b"])]) == NamedSequenceType(
"a",
"b.a",
[LiteralType(["b"]), NamedType("a", "")],
)
assert NamedSequenceType("a", "b.a", [NamedType("a", ""), LiteralType(["b"])]) != NamedSequenceType(
"a",
"b.a",
[LiteralType(["a"]), NamedType("b", "")],
)
assert NamedSequenceType("a", "b.a", [NamedType("a", ""), NamedType("b", "")]) != NamedSequenceType(
"a",
"b.a",
[NamedType("a", ""), NamedType("c", "")],
)


def test_dict_type() -> None:
dict_type = DictType(
key_type=UnionType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ fun inferTypes3(
b
) -> result1: union<Boolean, Int, String>

/**
* numpy_sequence_types.
*
* Dolor sit amet.
*
* @result namedResult this will be the return value
*/
@Pure
@PythonName("numpy_sequence_types")
fun numpySequenceTypes(
a: SequenceTypeVar<List<Any>>
) -> namedResult: SequenceTypeVar2<Int>

/**
* ClassWithDocumentation. Code::
*
Expand Down

0 comments on commit cb061ab

Please sign in to comment.