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: Correct stubs for TypeVars #67

Merged
merged 20 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
47c437f
Adjusted snapshot test files for typevar stubs
Masara Feb 25, 2024
979def4
Added another test case for typevar stubs
Masara Feb 25, 2024
1b77b68
Added typevar stubs test module and separate snapshots
Masara Feb 25, 2024
79913eb
TypeVar stub creation now works with type vars if they are existing T…
Masara Feb 25, 2024
9c30988
Now "def func[T]" like TypeVars can also be analyzed. Adjusted tests.
Masara Feb 25, 2024
a90a8af
Linter fixes
Masara Feb 25, 2024
78802d0
style: apply automated linter fixes
megalinter-bot Feb 25, 2024
1f89f5a
Removed changes for 'def func[T]' TypeData from commit 9c30988b
Masara Feb 25, 2024
206338f
Merge remote-tracking branch 'origin/63-typevar-stubs' into 63-typeva…
Masara Feb 25, 2024
aade9cb
TypeVar names are now in camelCase in stubs
Masara Feb 25, 2024
12469d5
(WIP) Added a TypeVar test case for a class with type parameters.
Masara Feb 26, 2024
d3ef10f
Fixed how classes and their methods generate stubs if the classes use…
Masara Feb 28, 2024
8e01d2d
Added more snapshot tests for type var and adjusted the variance tests
Masara Feb 29, 2024
fec471a
snapshot test update
Masara Feb 29, 2024
c469f78
Fixed variance generation in stubs
Masara Feb 29, 2024
5c1e9f3
style: apply automated linter fixes
megalinter-bot Feb 29, 2024
fafa108
type vars in function stubs now can also have upper bound
Masara Feb 29, 2024
480b744
Merge remote-tracking branch 'origin/63-typevar-stubs' into 63-typeva…
Masara Feb 29, 2024
68531d4
linter fix
Masara Feb 29, 2024
b2275b6
style: apply automated linter fixes
megalinter-bot Feb 29, 2024
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
11 changes: 8 additions & 3 deletions src/safeds_stubgen/api_analyzer/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
ResultDocstring,
)

from ._types import AbstractType
from ._types import AbstractType, TypeVarType

API_SCHEMA_VERSION = 1

Expand Down Expand Up @@ -237,6 +237,7 @@ class Function:
is_static: bool
is_class_method: bool
is_property: bool
type_var_types: list[TypeVarType] = field(default_factory=list)
results: list[Result] = field(default_factory=list)
reexported_by: list[Module] = field(default_factory=list)
parameters: list[Parameter] = field(default_factory=list)
Expand Down Expand Up @@ -311,11 +312,15 @@ class ParameterAssignment(PythonEnum):
@dataclass(frozen=True)
class TypeParameter:
name: str
type: AbstractType
type: AbstractType | None
variance: VarianceKind

def to_dict(self) -> dict[str, Any]:
return {"name": self.name, "type": self.type.to_dict(), "variance_type": self.variance.name}
return {
"name": self.name,
"type": self.type.to_dict() if self.type is not None else None,
"variance_type": self.variance.name,
}


class VarianceKind(PythonEnum):
Expand Down
38 changes: 30 additions & 8 deletions src/safeds_stubgen/api_analyzer/_ast_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(self, docstring_parser: AbstractDocstringParser, api: API, aliases:
self.__declaration_stack: list[Module | Class | Function | Enum | list[Attribute | EnumInstance]] = []
self.aliases = aliases
self.mypy_file: mp_nodes.MypyFile | None = None
# We gather type var types used as a parameter type in a function
self.type_var_types: set[sds_types.TypeVarType] = set()

def enter_moduledef(self, node: mp_nodes.MypyFile) -> None:
self.mypy_file = node
Expand Down Expand Up @@ -153,13 +155,17 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None:

for generic_type in generic_types:
variance_type = mypy_variance_parser(generic_type.variance)
variance_values: sds_types.AbstractType
variance_values: sds_types.AbstractType | None = None
if variance_type == VarianceKind.INVARIANT:
variance_values = sds_types.UnionType(
[self.mypy_type_to_abstract_type(value) for value in generic_type.values],
)
values = [self.mypy_type_to_abstract_type(value) for value in generic_type.values]
if values:
variance_values = sds_types.UnionType(
[self.mypy_type_to_abstract_type(value) for value in generic_type.values],
)
else:
variance_values = self.mypy_type_to_abstract_type(generic_type.upper_bound)
upper_bound = generic_type.upper_bound
if upper_bound.__str__() != "builtins.object":
variance_values = self.mypy_type_to_abstract_type(upper_bound)

type_parameters.append(
TypeParameter(
Expand Down Expand Up @@ -229,11 +235,19 @@ def enter_funcdef(self, node: mp_nodes.FuncDef) -> None:
# Get docstring
docstring = self.docstring_parser.get_function_documentation(node)

# Function args
# Function args & TypeVar
arguments: list[Parameter] = []
type_var_types: list[sds_types.TypeVarType] = []
# Reset the type_var_types list
self.type_var_types = set()
if getattr(node, "arguments", None) is not None:
arguments = self._parse_parameter_data(node, function_id)

if self.type_var_types:
type_var_types = list(self.type_var_types)
# Sort for the snapshot tests
type_var_types.sort(key=lambda x: x.name)

# Create results
results = self._parse_results(node, function_id)

Expand All @@ -252,6 +266,7 @@ def enter_funcdef(self, node: mp_nodes.FuncDef) -> None:
results=results,
reexported_by=reexported_by,
parameters=arguments,
type_var_types=type_var_types,
)
self.__declaration_stack.append(function)

Expand Down Expand Up @@ -666,7 +681,7 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis
for argument in node.arguments:
mypy_type = argument.variable.type
type_annotation = argument.type_annotation
arg_type = None
arg_type: AbstractType | None = None
default_value = None
default_is_none = False

Expand Down Expand Up @@ -827,7 +842,14 @@ def mypy_type_to_abstract_type(

# Special Cases
elif isinstance(mypy_type, mp_types.TypeVarType):
return sds_types.TypeVarType(mypy_type.name)
upper_bound = mypy_type.upper_bound
type_ = None
if upper_bound.__str__() != "builtins.object":
type_ = self.mypy_type_to_abstract_type(upper_bound)

type_var = sds_types.TypeVarType(name=mypy_type.name, upper_bound=type_)
self.type_var_types.add(type_var)
return type_var
elif isinstance(mypy_type, mp_types.CallableType):
return sds_types.CallableType(
parameter_types=[self.mypy_type_to_abstract_type(arg_type) for arg_type in mypy_type.arg_types],
Expand Down
15 changes: 10 additions & 5 deletions src/safeds_stubgen/api_analyzer/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,16 +391,21 @@ def __hash__(self) -> int:
@dataclass(frozen=True)
class TypeVarType(AbstractType):
name: str
upper_bound: AbstractType | None = None

@classmethod
def from_dict(cls, d: dict[str, str]) -> TypeVarType:
return TypeVarType(d["name"])
def from_dict(cls, d: dict[str, Any]) -> TypeVarType:
return TypeVarType(d["name"], d["upper_bound"])

def to_dict(self) -> dict[str, str]:
return {"kind": self.__class__.__name__, "name": self.name}
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.__class__.__name__,
"name": self.name,
"upper_bound": self.upper_bound.to_dict() if self.upper_bound is not None else None,
}

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


# ############################## Utilities ############################## #
Expand Down
42 changes: 33 additions & 9 deletions src/safeds_stubgen/stubs_generator/_generate_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __call__(self, module: Module) -> str:
self.module_imports = set()
self._current_todo_msgs: set[str] = set()
self.module = module
self.class_generics: list = []
return self._create_module_string(module)

def _create_module_string(self, module: Module) -> str:
Expand Down Expand Up @@ -175,7 +176,8 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st
constraints_info = ""
variance_info = ""
if class_.type_parameters:
variances = []
# We collect the class generics for the methods later
self.class_generics = []
out = "out "
for variance in class_.type_parameters:
variance_direction = {
Expand All @@ -189,12 +191,12 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st
variance_name_camel_case = self._replace_if_safeds_keyword(variance_name_camel_case)

variance_item = f"{variance_direction}{variance_name_camel_case}"
if variance_direction == out:
if variance.type is not None:
variance_item = f"{variance_item} sub {self._create_type_string(variance.type.to_dict())}"
variances.append(variance_item)
self.class_generics.append(variance_item)

if variances:
variance_info = f"<{', '.join(variances)}>"
if self.class_generics:
variance_info = f"<{', '.join(self.class_generics)}>"

# Class name - Convert to camelCase and check for keywords
class_name = class_.name
Expand Down Expand Up @@ -265,6 +267,10 @@ def _create_class_attribute_string(self, attributes: list[Attribute], inner_inde
if attribute.type:
attribute_type = attribute.type.to_dict()

# Don't create TypeVar attributes
if attribute_type["kind"] == "TypeVarType":
continue

static_string = "static " if attribute.is_static else ""

# Convert name to camelCase and add PythonName annotation
Expand Down Expand Up @@ -317,6 +323,24 @@ def _create_function_string(self, function: Function, indentations: str = "", is
is_instance_method=not is_static and is_method,
)

# TypeVar
type_var_info = ""
if function.type_var_types:
type_var_names = []
for type_var in function.type_var_types:
type_var_name = self._convert_snake_to_camel_case(type_var.name)
type_var_name = self._replace_if_safeds_keyword(type_var_name)

# We don't have to display generic types in methods if they were already displayed in the class
if not is_method or (is_method and type_var_name not in self.class_generics):
if type_var.upper_bound is not None:
type_var_name += f" sub {self._create_type_string(type_var.upper_bound.to_dict())}"
type_var_names.append(type_var_name)

if type_var_names:
type_var_string = ", ".join(type_var_names)
type_var_info = f"<{type_var_string}>"

# Convert function name to camelCase
name = function.name
camel_case_name = self._convert_snake_to_camel_case(name)
Expand All @@ -334,8 +358,8 @@ def _create_function_string(self, function: Function, indentations: str = "", is
f"{self._create_todo_msg(indentations)}"
f"{indentations}@Pure\n"
f"{function_name_annotation}"
f"{indentations}{static}fun {camel_case_name}({func_params})"
f"{result_string}"
f"{indentations}{static}fun {camel_case_name}{type_var_info}"
f"({func_params}){result_string}"
)

def _create_property_function_string(self, function: Function, indentations: str = "") -> str:
Expand Down Expand Up @@ -621,9 +645,9 @@ def _create_type_string(self, type_data: dict | None) -> str:
else:
types.append(f"{literal_type}")
return f"literal<{', '.join(types)}>"
# Todo See issue #63
elif kind == "TypeVarType":
return ""
name = self._convert_snake_to_camel_case(type_data["name"])
return self._replace_if_safeds_keyword(name)

raise ValueError(f"Unexpected type: {kind}") # pragma: no cover

Expand Down
4 changes: 1 addition & 3 deletions tests/data/various_modules_package/attribute_module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Final, Literal, TypeVar
from typing import Optional, Final, Literal
from tests.data.main_package.another_path.another_module import AnotherClass


Expand Down Expand Up @@ -66,7 +66,5 @@ def some_func() -> bool:
attr_type_from_outside_package: AnotherClass
attr_default_value_from_outside_package = AnotherClass

type_var = TypeVar("type_var")

def __init__(self):
self.init_attr: bool = False
6 changes: 1 addition & 5 deletions tests/data/various_modules_package/function_module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Optional, Literal, Any, TypeVar
from typing import Callable, Optional, Literal, Any
from tests.data.main_package.another_path.another_module import AnotherClass


Expand Down Expand Up @@ -187,10 +187,6 @@ def param_from_outside_the_package(param_type: AnotherClass, param_value=Another
def result_from_outside_the_package() -> AnotherClass: ...


_type_var = TypeVar("_type_var")
def type_var_func(type_var_list: list[_type_var]) -> list[_type_var]: ...


class FunctionModulePropertiesClass:
@property
def property_function(self): ...
Expand Down
30 changes: 30 additions & 0 deletions tests/data/various_modules_package/type_var_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import TypeVar, Generic

T = TypeVar('T')


class TypeVarClass(Generic[T]):
type_var = TypeVar("type_var")

def __init__(self, items: list[T]): ...

def type_var_class_method(self, a: T) -> T: ...


class TypeVarClass2(Generic[T]):
type_var = TypeVar("type_var")

def type_var_class_method2(self, a: T) -> T: ...


_type_var = TypeVar("_type_var")
def type_var_func(a: list[_type_var]) -> list[_type_var]: ...


_type_var1 = TypeVar("_type_var1")
_type_var2 = TypeVar("_type_var2")
def multiple_type_var(a: _type_var1, b: _type_var2) -> list[_type_var1 | _type_var2]: ...


T_in = TypeVar("T_in", bound=int)
def type_var_fun_invariance_with_bound(a: list[T_in]) -> T_in: ...
23 changes: 18 additions & 5 deletions tests/data/various_modules_package/variance_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ class A:
...


_T_co = TypeVar("_T_co", covariant=True, bound=str)
_T_con = TypeVar("_T_con", contravariant=True, bound=A)
_T_in = TypeVar("_T_in", int, Literal[1, 2])
_T_in = TypeVar("_T_in")
_T_co = TypeVar("_T_co", covariant=True)
_T_con = TypeVar("_T_con", contravariant=True)


class VarianceClassAll(Generic[_T_co, _T_con, _T_in]):
class VarianceClassOnlyCovarianceNoBound(Generic[_T_co]):
...


class VarianceClassOnlyInvariance(Generic[_T_in]):
class VarianceClassOnlyVarianceNoBound(Generic[_T_in]):
...


class VarianceClassOnlyContravarianceNoBound(Generic[_T_con]):
...


_T_co2 = TypeVar("_T_co2", covariant=True, bound=str)
_T_con2 = TypeVar("_T_con2", contravariant=True, bound=A)
_T_in2 = TypeVar("_T_in2", int, Literal[1, 2])


class VarianceClassAll(Generic[_T_co2, _T_con2, _T_in2]):
...
Loading