diff --git a/src/safeds_stubgen/api_analyzer/_api.py b/src/safeds_stubgen/api_analyzer/_api.py index 522c2120..7971e426 100644 --- a/src/safeds_stubgen/api_analyzer/_api.py +++ b/src/safeds_stubgen/api_analyzer/_api.py @@ -16,7 +16,7 @@ ResultDocstring, ) - from ._types import AbstractType + from ._types import AbstractType, TypeVarType API_SCHEMA_VERSION = 1 @@ -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) @@ -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): diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 811773b8..da156e82 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -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 @@ -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( @@ -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) @@ -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) @@ -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 @@ -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], diff --git a/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index 8f69db37..4c5d7527 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -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 ############################## # diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index e99d91b7..9dd2c7c0 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -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: @@ -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 = { @@ -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 @@ -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 @@ -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) @@ -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: @@ -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 diff --git a/tests/data/various_modules_package/attribute_module.py b/tests/data/various_modules_package/attribute_module.py index 0b021cc3..05818940 100644 --- a/tests/data/various_modules_package/attribute_module.py +++ b/tests/data/various_modules_package/attribute_module.py @@ -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 @@ -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 diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 60c3cad6..0a7a1a38 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -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 @@ -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): ... diff --git a/tests/data/various_modules_package/type_var_module.py b/tests/data/various_modules_package/type_var_module.py new file mode 100644 index 00000000..d286c4d2 --- /dev/null +++ b/tests/data/various_modules_package/type_var_module.py @@ -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: ... diff --git a/tests/data/various_modules_package/variance_module.py b/tests/data/various_modules_package/variance_module.py index acbac3ef..de3a8ded 100644 --- a/tests/data/various_modules_package/variance_module.py +++ b/tests/data/various_modules_package/variance_module.py @@ -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]): ... diff --git a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr index 6165c319..e5a230db 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -1021,21 +1021,6 @@ 'qname': 'builtins.int', }), }), - dict({ - 'docstring': dict({ - 'default_value': '', - 'description': '', - 'type': '', - }), - 'id': 'various_modules_package/attribute_module/AttributesClassB/type_var', - 'is_public': True, - 'is_static': True, - 'name': 'type_var', - 'type': dict({ - 'kind': 'TypeVarType', - 'name': 'type_var', - }), - }), ]) # --- # name: test_class_attributes[ClassModuleNestedClassE] @@ -1859,6 +1844,31 @@ }), ]) # --- +# name: test_class_methods[TypeVarClass] + list([ + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/type_var_module/TypeVarClass/type_var_class_method', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'type_var_class_method', + 'parameters': list([ + 'various_modules_package/type_var_module/TypeVarClass/type_var_class_method/self', + 'various_modules_package/type_var_module/TypeVarClass/type_var_class_method/a', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'various_modules_package/type_var_module/TypeVarClass/type_var_class_method/result_1', + ]), + }), + ]) +# --- # name: test_class_methods[_ClassModulePrivateDoubleNestedClassF] list([ dict({ @@ -2462,7 +2472,7 @@ ]), 'type_parameters': list([ dict({ - 'name': '_T_co', + 'name': '_T_co2', 'type': dict({ 'kind': 'NamedType', 'name': 'str', @@ -2471,7 +2481,7 @@ 'variance_type': 'COVARIANT', }), dict({ - 'name': '_T_con', + 'name': '_T_con2', 'type': dict({ 'kind': 'NamedType', 'name': 'A', @@ -2480,7 +2490,7 @@ 'variance_type': 'CONTRAVARIANT', }), dict({ - 'name': '_T_in', + 'name': '_T_in2', 'type': dict({ 'kind': 'UnionType', 'types': list([ @@ -2513,7 +2523,65 @@ ]), }) # --- -# name: test_classes[VarianceClassOnlyInvariance] +# name: test_classes[VarianceClassOnlyContravarianceNoBound] + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/variance_module/VarianceClassOnlyContravarianceNoBound', + 'is_public': True, + 'methods': list([ + ]), + 'name': 'VarianceClassOnlyContravarianceNoBound', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + dict({ + 'name': '_T_con', + 'type': None, + 'variance_type': 'CONTRAVARIANT', + }), + ]), + }) +# --- +# name: test_classes[VarianceClassOnlyCovarianceNoBound] + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/variance_module/VarianceClassOnlyCovarianceNoBound', + 'is_public': True, + 'methods': list([ + ]), + 'name': 'VarianceClassOnlyCovarianceNoBound', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + dict({ + 'name': '_T_co', + 'type': None, + 'variance_type': 'COVARIANT', + }), + ]), + }) +# --- +# name: test_classes[VarianceClassOnlyVarianceNoBound] dict({ 'attributes': list([ ]), @@ -2524,11 +2592,11 @@ 'description': '', 'full_docstring': '', }), - 'id': 'various_modules_package/variance_module/VarianceClassOnlyInvariance', + 'id': 'various_modules_package/variance_module/VarianceClassOnlyVarianceNoBound', 'is_public': True, 'methods': list([ ]), - 'name': 'VarianceClassOnlyInvariance', + 'name': 'VarianceClassOnlyVarianceNoBound', 'reexported_by': list([ ]), 'superclasses': list([ @@ -2536,33 +2604,7 @@ 'type_parameters': list([ dict({ 'name': '_T_in', - 'type': dict({ - 'kind': 'UnionType', - 'types': list([ - dict({ - 'kind': 'NamedType', - 'name': 'int', - 'qname': 'builtins.int', - }), - dict({ - 'kind': 'UnionType', - 'types': list([ - dict({ - 'kind': 'LiteralType', - 'literals': list([ - 1, - ]), - }), - dict({ - 'kind': 'LiteralType', - 'literals': list([ - 2, - ]), - }), - ]), - }), - ]), - }), + 'type': None, 'variance_type': 'INVARIANT', }), ]), @@ -3401,6 +3443,44 @@ }), ]) # --- +# name: test_function_parameters[multiple_type_var] + list([ + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/type_var_module/multiple_type_var/a', + 'is_optional': False, + 'name': 'a', + 'type': dict({ + 'kind': 'TypeVarType', + 'name': '_type_var1', + 'upper_bound': None, + }), + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/type_var_module/multiple_type_var/b', + 'is_optional': False, + 'name': 'b', + 'type': dict({ + 'kind': 'TypeVarType', + 'name': '_type_var2', + 'upper_bound': None, + }), + }), + ]) +# --- # name: test_function_parameters[nested_class_function] list([ dict({ @@ -4455,15 +4535,16 @@ 'description': '', 'type': '', }), - 'id': 'various_modules_package/function_module/type_var_func/type_var_list', + 'id': 'various_modules_package/type_var_module/type_var_func/a', 'is_optional': False, - 'name': 'type_var_list', + 'name': 'a', 'type': dict({ 'kind': 'ListType', 'types': list([ dict({ 'kind': 'TypeVarType', 'name': '_type_var', + 'upper_bound': None, }), ]), }), @@ -4978,6 +5059,38 @@ }), ]) # --- +# name: test_function_results[multiple_type_var] + list([ + dict({ + 'docstring': dict({ + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/type_var_module/multiple_type_var/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'ListType', + 'types': list([ + dict({ + 'kind': 'UnionType', + 'types': list([ + dict({ + 'kind': 'TypeVarType', + 'name': '_type_var1', + 'upper_bound': None, + }), + dict({ + 'kind': 'TypeVarType', + 'name': '_type_var2', + 'upper_bound': None, + }), + ]), + }), + ]), + }), + }), + ]) +# --- # name: test_function_results[nested_class_function] list([ dict({ @@ -5212,7 +5325,7 @@ 'description': '', 'type': '', }), - 'id': 'various_modules_package/function_module/type_var_func/result_1', + 'id': 'various_modules_package/type_var_module/type_var_func/result_1', 'name': 'result_1', 'type': dict({ 'kind': 'ListType', @@ -5220,6 +5333,7 @@ dict({ 'kind': 'TypeVarType', 'name': '_type_var', + 'upper_bound': None, }), ]), }), @@ -6106,26 +6220,6 @@ 'various_modules_package/function_module/tuple_results/result_2', ]), }), - dict({ - 'docstring': dict({ - 'description': '', - 'full_docstring': '', - }), - 'id': 'various_modules_package/function_module/type_var_func', - 'is_class_method': False, - 'is_property': False, - 'is_public': True, - 'is_static': False, - 'name': 'type_var_func', - 'parameters': list([ - 'various_modules_package/function_module/type_var_func/type_var_list', - ]), - 'reexported_by': list([ - ]), - 'results': list([ - 'various_modules_package/function_module/type_var_func/result_1', - ]), - }), dict({ 'docstring': dict({ 'description': '', diff --git a/tests/safeds_stubgen/api_analyzer/test__get_api.py b/tests/safeds_stubgen/api_analyzer/test__get_api.py index 66606361..8ef3c73c 100644 --- a/tests/safeds_stubgen/api_analyzer/test__get_api.py +++ b/tests/safeds_stubgen/api_analyzer/test__get_api.py @@ -107,6 +107,7 @@ def _get_specific_function_data( _import_module_name = "import_module" _abstract_module_name = "abstract_module" _infer_types_module_name = "infer_types_module" +_type_var_module_name = "type_var_module" # ############################## Tests ############################## # @@ -168,8 +169,10 @@ def test_imports(module_name: str, import_type: str, snapshot: SnapshotAssertion (_class_module_name, "ClassModuleNestedClassE", "plaintext"), (_class_module_name, "_ClassModulePrivateDoubleNestedClassF", "plaintext"), (_class_module_name, "_ClassModulePrivateClassG", "plaintext"), + ("variance_module", "VarianceClassOnlyCovarianceNoBound", "plaintext"), + ("variance_module", "VarianceClassOnlyVarianceNoBound", "plaintext"), + ("variance_module", "VarianceClassOnlyContravarianceNoBound", "plaintext"), ("variance_module", "VarianceClassAll", "plaintext"), - ("variance_module", "VarianceClassOnlyInvariance", "plaintext"), (_infer_types_module_name, "InferMyTypes", "plaintext"), (_docstring_module_name, "EpydocDocstringClass", "epydoc"), (_docstring_module_name, "RestDocstringClass", "rest"), @@ -189,8 +192,10 @@ def test_imports(module_name: str, import_type: str, snapshot: SnapshotAssertion "ClassModuleNestedClassE", "_ClassModulePrivateDoubleNestedClassF", "_ClassModulePrivateClassG", + "VarianceClassOnlyCovarianceNoBound", + "VarianceClassOnlyVarianceNoBound", + "VarianceClassOnlyContravarianceNoBound", "VarianceClassAll", - "VarianceClassOnlyInvariance", "InferMyTypes", "EpydocDocstringClass", "RestDocstringClass", @@ -332,6 +337,7 @@ def test_global_functions(module_name: str, snapshot: SnapshotAssertion) -> None (_function_module_name, "FunctionModuleClassC", "plaintext"), (_function_module_name, "FunctionModulePropertiesClass", "plaintext"), (_infer_types_module_name, "InferMyTypes", "plaintext"), + (_type_var_module_name, "TypeVarClass", "plaintext"), ("_reexport_module_1", "ReexportClass", "plaintext"), (_abstract_module_name, "AbstractModuleClass", "plaintext"), (_docstring_module_name, "EpydocDocstringClass", "epydoc"), @@ -349,6 +355,7 @@ def test_global_functions(module_name: str, snapshot: SnapshotAssertion) -> None "FunctionModuleClassC", "FunctionModulePropertiesClass", "InferMyTypes", + "TypeVarClass", "ReexportClass", "AbstractModuleClass", "EpydocDocstringClass", @@ -390,7 +397,8 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, ("args_type", _function_module_name, "", "plaintext"), ("callable_type", _function_module_name, "", "plaintext"), ("param_from_outside_the_package", _function_module_name, "", "plaintext"), - ("type_var_func", _function_module_name, "", "plaintext"), + ("type_var_func", _type_var_module_name, "", "plaintext"), + ("multiple_type_var", _type_var_module_name, "", "plaintext"), ("abstract_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), ("abstract_static_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), ("abstract_property_method", _abstract_module_name, "AbstractModuleClass", "plaintext"), @@ -422,6 +430,7 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, "callable_type", "param_from_outside_the_package", "type_var_func", + "multiple_type_var", "abstract_method_params", "abstract_static_method_params", "abstract_property_method", @@ -480,7 +489,8 @@ def test_function_parameters( ("any_results", _function_module_name, "", "plaintext"), ("callable_type", _function_module_name, "", "plaintext"), ("result_from_outside_the_package", _function_module_name, "", "plaintext"), - ("type_var_func", _function_module_name, "", "plaintext"), + ("type_var_func", _type_var_module_name, "", "plaintext"), + ("multiple_type_var", _type_var_module_name, "", "plaintext"), ("instance_method", _function_module_name, "FunctionModuleClassB", "plaintext"), ("static_method_params", _function_module_name, "FunctionModuleClassB", "plaintext"), ("class_method_params", _function_module_name, "FunctionModuleClassB", "plaintext"), @@ -521,6 +531,7 @@ def test_function_parameters( "callable_type", "result_from_outside_the_package", "type_var_func", + "multiple_type_var", "instance_method", "static_method_params", "class_method_params", diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index 010b60bf..17e99220 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -260,19 +260,16 @@ def test_literal_type() -> None: def test_type_var_type() -> None: type_ = TypeVarType("_T") - type_dict = { - "kind": "TypeVarType", - "name": "_T", - } + type_dict = {"kind": "TypeVarType", "name": "_T", "upper_bound": None} assert AbstractType.from_dict(type_dict) == type_ assert TypeVarType.from_dict(type_dict) == type_ assert type_.to_dict() == type_dict - assert TypeVarType("a") == TypeVarType("a") - assert hash(TypeVarType("a")) == hash(TypeVarType("a")) - assert TypeVarType("a") != TypeVarType("b") - assert hash(TypeVarType("a")) != hash(TypeVarType("b")) + assert TypeVarType("a", None) == TypeVarType("a", None) + assert hash(TypeVarType("a", None)) == hash(TypeVarType("a", None)) + assert TypeVarType("a", None) != TypeVarType("b", None) + assert hash(TypeVarType("a", None)) != hash(TypeVarType("b", None)) def test_final_type() -> None: diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_attribute_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_attribute_creation.sdsstub index 858101a2..f9ef79bc 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_attribute_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_attribute_creation.sdsstub @@ -93,9 +93,6 @@ class AttributesClassB() { static attr attrTypeFromOutsidePackage: AnotherClass @PythonName("attr_default_value_from_outside_package") static attr attrDefaultValueFromOutsidePackage: () -> a: AnotherClass - // TODO Attribute has no type information. - @PythonName("type_var") - static attr typeVar @PythonName("init_attr") attr initAttr: Boolean diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_function_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_function_creation.sdsstub index d63a22d9..cd5f8643 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_function_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_function_creation.sdsstub @@ -246,12 +246,6 @@ fun paramFromOutsideThePackage( @PythonName("result_from_outside_the_package") fun resultFromOutsideThePackage() -> result1: AnotherClass -@Pure -@PythonName("type_var_func") -fun typeVarFunc( - @PythonName("type_var_list") typeVarList: List<> -) -> result1: List<> - @Pure @PythonName("ret_conditional_statement") fun retConditionalStatement() -> result1: union diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_var_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_var_creation.sdsstub new file mode 100644 index 00000000..e55164ca --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_var_creation.sdsstub @@ -0,0 +1,39 @@ +@PythonModule("various_modules_package.type_var_module") +package variousModulesPackage.typeVarModule + +@Pure +@PythonName("type_var_func") +fun typeVarFunc( + a: List +) -> result1: List + +@Pure +@PythonName("multiple_type_var") +fun multipleTypeVar( + a: typeVar1, + b: typeVar2 +) -> result1: List> + +@Pure +@PythonName("type_var_fun_invariance_with_bound") +fun typeVarFunInvarianceWithBound( + a: List +) -> result1: TIn + +class TypeVarClass( + items: List +) { + @Pure + @PythonName("type_var_class_method") + fun typeVarClassMethod( + a: T + ) -> result1: T +} + +class TypeVarClass2() { + @Pure + @PythonName("type_var_class_method2") + fun typeVarClassMethod2( + a: T + ) -> result1: T +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_variance_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_variance_creation.sdsstub index 46fdfc17..1775853f 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_variance_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_variance_creation.sdsstub @@ -3,6 +3,10 @@ package variousModulesPackage.varianceModule class A() -class VarianceClassAll() +class VarianceClassOnlyCovarianceNoBound() -class VarianceClassOnlyInvariance() +class VarianceClassOnlyVarianceNoBound() + +class VarianceClassOnlyContravarianceNoBound() + +class VarianceClassAll>>() diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 0a3fa63b..dbb71aee 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -100,6 +100,10 @@ def test_abstract_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("abstract_module", snapshot_sds_stub) +def test_type_var_creation(snapshot_sds_stub: SnapshotAssertion) -> None: + assert_stubs_snapshot("type_var_module", snapshot_sds_stub) + + @pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2", "aliasing_module_3"]) def test_alias_creation(file_name: str, snapshot_sds_stub: SnapshotAssertion) -> None: file_data = ""