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

feat: Added handling for sequence classes #127

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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