From a0c7d50085b163ca0044c63c08718ddeb334bf88 Mon Sep 17 00:00:00 2001 From: Arsam Date: Thu, 7 Dec 2023 15:18:29 +0100 Subject: [PATCH 01/49] Ignore type information if these are from another package; Added test data for codecov; Refactoring --- .../api_analyzer/_ast_visitor.py | 83 ++++++++++++------- .../api_analyzer/_mypy_helpers.py | 2 +- .../function_module.py | 4 + .../__snapshots__/test__get_api.ambr | 50 +++++++++++ .../api_analyzer/test__get_api.py | 2 + .../__snapshots__/test_generate_stubs.ambr | 10 +++ 6 files changed, 119 insertions(+), 32 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index ba308cd6..155a3515 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -636,17 +636,29 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis arguments: list[Parameter] = [] for argument in node.arguments: - arg_name = argument.variable.name mypy_type = argument.variable.type - arg_kind = get_argument_kind(argument) type_annotation = argument.type_annotation arg_type = None + default_value = None + default_is_none = False + + # Get qname + arg_type_data = getattr(argument.variable.type, "type", None) + arg_type_qname = "" + if arg_type_data is not None: + arg_type_qname = arg_type_data.fullname + # Get type information for parameter if mypy_type is None: # pragma: no cover raise ValueError("Argument has no type.") elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any): # We try to infer the type through the default value later, if possible pass + elif (isinstance(type_annotation, mp_types.UnboundType) and + arg_type_qname and not self._check_if_qname_in_package(arg_type_qname)): + # Types can only be either core classes of any type or types which are defined in the same package. + # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 + pass elif ( isinstance(type_annotation, mp_types.UnboundType) and type_annotation.name in {"list", "set"} @@ -661,37 +673,13 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis # Get default value and infer type information initializer = argument.initializer - default_value = None - default_is_none = False if initializer is not None: - infer_arg_type = arg_type is None + default_value, default_is_none = self._get_parameter_type_and_default_value(initializer) + if arg_type is None and (default_is_none or default_value is not None): + arg_type = mypy_expression_to_sds_type(initializer) - if ( - isinstance(initializer, mp_nodes.NameExpr) - and initializer.name not in {"None", "True", "False"} - and not self._check_if_qname_in_package(initializer.fullname) - ): - # Ignore this case, b/c Safe-DS does not support types that aren't core classes or classes definied - # in the package we analyze with Safe-DS. - pass - elif isinstance(initializer, mp_nodes.CallExpr): - # Safe-DS does not support call expressions as types - pass - elif isinstance( - initializer, - mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr | mp_nodes.NameExpr, - ): - # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 - inferred_default_value = mypy_expression_to_python_value(initializer) - if isinstance(inferred_default_value, str | bool | int | float | NoneType): - default_value = inferred_default_value - else: # pragma: no cover - raise TypeError("Default value got an unsupported value.") - - default_is_none = default_value is None - - if infer_arg_type: - arg_type = mypy_expression_to_sds_type(initializer) + arg_name = argument.variable.name + arg_kind = get_argument_kind(argument) # Create parameter docstring parent = self.__declaration_stack[-1] @@ -720,6 +708,37 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis return arguments + def _get_parameter_type_and_default_value( + self, initializer: mp_nodes.Expression, + ) -> tuple[str | None | int | float, bool]: + default_value = None + default_is_none = False + if initializer is not None: + if ( + isinstance(initializer, mp_nodes.NameExpr) + and initializer.name not in {"None", "True", "False"} + and not self._check_if_qname_in_package(initializer.fullname) + ): + # Ignore this case, b/c Safe-DS does not support types that aren't core classes or classes definied + # in the package we analyze with Safe-DS. + return default_value, default_is_none + elif isinstance(initializer, mp_nodes.CallExpr): + # Safe-DS does not support call expressions as types + return default_value, default_is_none + elif isinstance( + initializer, + mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr | mp_nodes.NameExpr, + ): + # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 + inferred_default_value = mypy_expression_to_python_value(initializer) + if isinstance(inferred_default_value, str | bool | int | float | NoneType): + default_value = inferred_default_value + else: # pragma: no cover + raise TypeError("Default value got an unsupported value.") + + default_is_none = default_value is None + return default_value, default_is_none + # #### Reexport utilities def _get_reexported_by(self, name: str) -> list[Module]: @@ -768,6 +787,8 @@ def _add_reexports(self, module: Module) -> None: # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed # from the package. def _check_if_qname_in_package(self, qname: str) -> bool: + if "builtins." in qname: + return True return self.api.package in qname def _create_module_id(self, qname: str) -> str: diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 99e3b4ba..134b0a26 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -224,7 +224,7 @@ def mypy_expression_to_sds_type(expr: mp_nodes.Expression) -> sds_types.Abstract raise TypeError("Unexpected expression type.") # pragma: no cover -def mypy_expression_to_python_value(expr: mp_nodes.Expression) -> str | None | int | float | list | set | dict | tuple: +def mypy_expression_to_python_value(expr: mp_nodes.Expression) -> str | None | int | float: if isinstance(expr, mp_nodes.NameExpr): match expr.name: case "None": diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 0a1df3c7..73615457 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -1,4 +1,5 @@ from typing import Callable, Optional, Literal, Any +from tests.data.main_package.another_path.another_module import AnotherClass class FunctionModuleClassA: @@ -179,6 +180,9 @@ def any_results() -> Any: ... def callable_type(param: Callable[[str], tuple[int, str]]) -> Callable[[int, int], int]: ... +def param_from_outside_the_package(param_type: AnotherClass, param_value=AnotherClass): ... + + class FunctionModulePropertiesClass: @property def property_function(self): ... 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 79aefcae..ad62b9d2 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -3416,6 +3416,36 @@ }), ]) # --- +# name: test_function_parameters[param_from_outside_the_package] + list([ + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/param_from_outside_the_package/param_type', + 'is_optional': False, + 'name': 'param_type', + 'type': None, + }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/param_from_outside_the_package/param_value', + 'is_optional': False, + 'name': 'param_value', + 'type': None, + }), + ]) +# --- # name: test_function_parameters[param_position] list([ dict({ @@ -5600,6 +5630,26 @@ 'various_modules_package/function_module/optional_results/result_1', ]), }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/function_module/param_from_outside_the_package', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'param_from_outside_the_package', + 'parameters': list([ + 'various_modules_package/function_module/param_from_outside_the_package/param_type', + 'various_modules_package/function_module/param_from_outside_the_package/param_value', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), 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 022d53bf..e2c437ca 100644 --- a/tests/safeds_stubgen/api_analyzer/test__get_api.py +++ b/tests/safeds_stubgen/api_analyzer/test__get_api.py @@ -389,6 +389,7 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, ("arg", _function_module_name, "", "plaintext"), ("args_type", _function_module_name, "", "plaintext"), ("callable_type", _function_module_name, "", "plaintext"), + ("param_from_outside_the_package", _function_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"), @@ -418,6 +419,7 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, "arg", "args_type", "callable_type", + "param_from_outside_the_package", "abstract_method_params", "abstract_static_method_params", "abstract_property_method", diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 4bd5b29e..f22e93a8 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -228,6 +228,7 @@ from typing import Optional from typing import Literal from typing import Any + from tests.data.main_package.another_path.another_module import AnotherClass // TODO Result type information missing. @Pure @@ -456,6 +457,15 @@ param: (a: String) -> (b: Int, c: String) ) -> result1: (a: Int, b: Int) -> c: Int + // TODO Result type information missing. + // TODO Some parameter have no type information. + @Pure + @PythonName("param_from_outside_the_package") + fun paramFromOutsideThePackage( + @PythonName("param_type") paramType, + @PythonName("param_value") paramValue + ) + class FunctionModuleClassA() // TODO Some parameter have no type information. From 559eefd385ef3231d87f4eb76631283ee361bbe9 Mon Sep 17 00:00:00 2001 From: Arsam Date: Thu, 7 Dec 2023 15:25:16 +0100 Subject: [PATCH 02/49] Added more test data --- .../attribute_module.py | 4 ++ .../function_module.py | 3 + .../__snapshots__/test__get_api.ambr | 56 +++++++++++++++++++ .../api_analyzer/test__get_api.py | 2 + .../__snapshots__/test_generate_stubs.ambr | 12 ++++ 5 files changed, 77 insertions(+) diff --git a/tests/data/various_modules_package/attribute_module.py b/tests/data/various_modules_package/attribute_module.py index a2bd7c34..05818940 100644 --- a/tests/data/various_modules_package/attribute_module.py +++ b/tests/data/various_modules_package/attribute_module.py @@ -1,4 +1,5 @@ from typing import Optional, Final, Literal +from tests.data.main_package.another_path.another_module import AnotherClass class AttributesClassA: @@ -62,5 +63,8 @@ def some_func() -> bool: multi_attr_5, multi_attr_6 = ("A", "B") multi_attr_7 = multi_attr_8 = "A" + attr_type_from_outside_package: AnotherClass + attr_default_value_from_outside_package = AnotherClass + 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 73615457..7fa1c037 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -183,6 +183,9 @@ def callable_type(param: Callable[[str], tuple[int, str]]) -> Callable[[int, int def param_from_outside_the_package(param_type: AnotherClass, param_value=AnotherClass): ... +def result_from_outside_the_package() -> AnotherClass: ... + + class FunctionModulePropertiesClass: @property def property_function(self): ... 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 ad62b9d2..832fee31 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -86,6 +86,30 @@ 'qname': 'builtins.int', }), }), + dict({ + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/attribute_module/AttributesClassB/attr_default_value_from_outside_package', + 'is_public': True, + 'is_static': True, + 'name': 'attr_default_value_from_outside_package', + 'type': None, + }), + dict({ + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/attribute_module/AttributesClassB/attr_type_from_outside_package', + 'is_public': True, + 'is_static': True, + 'name': 'attr_type_from_outside_package', + 'type': None, + }), dict({ 'docstring': dict({ 'default_value': '', @@ -4952,6 +4976,19 @@ }), ]) # --- +# name: test_function_results[result_from_outside_the_package] + list([ + dict({ + 'docstring': dict({ + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/result_from_outside_the_package/result_1', + 'name': 'result_1', + 'type': None, + }), + ]) +# --- # name: test_function_results[set_results] list([ dict({ @@ -5782,6 +5819,25 @@ 'results': list([ ]), }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/function_module/result_from_outside_the_package', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'result_from_outside_the_package', + 'parameters': list([ + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'various_modules_package/function_module/result_from_outside_the_package/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 e2c437ca..9c588eab 100644 --- a/tests/safeds_stubgen/api_analyzer/test__get_api.py +++ b/tests/safeds_stubgen/api_analyzer/test__get_api.py @@ -477,6 +477,7 @@ def test_function_parameters( ("literal_results", _function_module_name, "", "plaintext"), ("any_results", _function_module_name, "", "plaintext"), ("callable_type", _function_module_name, "", "plaintext"), + ("result_from_outside_the_package", _function_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"), @@ -515,6 +516,7 @@ def test_function_parameters( "literal_results", "any_results", "callable_type", + "result_from_outside_the_package", "instance_method", "static_method_params", "class_method_params", diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index f22e93a8..573c716b 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -46,6 +46,7 @@ from typing import Optional from typing import Final from typing import Literal + from tests.data.main_package.another_path.another_module import AnotherClass class AttributesClassA() @@ -131,6 +132,12 @@ static attr multiAttr7: String @PythonName("multi_attr_8") static attr multiAttr8: String + // TODO Attribute has no type information. + @PythonName("attr_type_from_outside_package") + static attr attrTypeFromOutsidePackage + // TODO Attribute has no type information. + @PythonName("attr_default_value_from_outside_package") + static attr attrDefaultValueFromOutsidePackage @PythonName("init_attr") attr initAttr: Boolean @@ -466,6 +473,11 @@ @PythonName("param_value") paramValue ) + // TODO Result type information missing. + @Pure + @PythonName("result_from_outside_the_package") + fun resultFromOutsideThePackage() + class FunctionModuleClassA() // TODO Some parameter have no type information. From be0f30bd08b03852dadc8fdb701bb422eab9f69b Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 8 Dec 2023 11:46:48 +0100 Subject: [PATCH 03/49] Ignore type information if these are from another package for return and attribute types --- .../api_analyzer/_ast_visitor.py | 30 ++++++++++++++----- .../__snapshots__/test__get_api.ambr | 10 ------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 155a3515..2c0fd8b9 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -376,7 +376,12 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul if node_type is not None and hasattr(node_type, "ret_type"): node_ret_type = node_type.ret_type - if not isinstance(node_ret_type, mp_types.NoneType): + node_ret_type_type = getattr(node_ret_type, "type", None) + is_part_of_package = True + if node_ret_type_type is not None: + is_part_of_package = self._is_part_of_package(node_ret_type_type.fullname) + + if not isinstance(node_ret_type, mp_types.NoneType) and is_part_of_package: if isinstance(node_ret_type, mp_types.AnyType) and not has_correct_type_of_any( node_ret_type.type_of_any, ): @@ -603,11 +608,20 @@ def _create_attribute( else: # pragma: no cover raise TypeError("Attribute has an unexpected type.") + # Check if the attribute type is part of the package we analyze + if isinstance(attribute_type, mp_types.CallableType): + attr_fullname = attribute_type.ret_type.type.fullname + else: + attribute_type_information = getattr(attribute_type, "type", None) + attr_fullname = getattr(attribute_type_information, "fullname", "") + is_part_of_package = attr_fullname and not self._is_part_of_package(attr_fullname) + type_ = None - # Ignore types that are special mypy any types - if attribute_type is not None and not ( - isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) + if is_part_of_package and attribute_type is not None and not ( + isinstance(attribute_type, mp_types.AnyType) and + not has_correct_type_of_any(attribute_type.type_of_any) ): + # Ignore types that are special mypy any types & ignore types from outside the package we analyze # noinspection PyTypeChecker type_ = mypy_type_to_abstract_type(attribute_type, unanalyzed_type) @@ -655,7 +669,7 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis # We try to infer the type through the default value later, if possible pass elif (isinstance(type_annotation, mp_types.UnboundType) and - arg_type_qname and not self._check_if_qname_in_package(arg_type_qname)): + arg_type_qname and not self._is_part_of_package(arg_type_qname)): # Types can only be either core classes of any type or types which are defined in the same package. # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 pass @@ -717,7 +731,7 @@ def _get_parameter_type_and_default_value( if ( isinstance(initializer, mp_nodes.NameExpr) and initializer.name not in {"None", "True", "False"} - and not self._check_if_qname_in_package(initializer.fullname) + and not self._is_part_of_package(initializer.fullname) ): # Ignore this case, b/c Safe-DS does not support types that aren't core classes or classes definied # in the package we analyze with Safe-DS. @@ -786,7 +800,9 @@ def _add_reexports(self, module: Module) -> None: # Todo This check is currently too weak, we should try to get the path to the package from the api object, not # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed # from the package. - def _check_if_qname_in_package(self, qname: str) -> bool: + def _is_part_of_package(self, qname: str) -> bool: + """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze. + """ if "builtins." in qname: return True return self.api.package in qname 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 832fee31..e04e8658 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -4978,15 +4978,6 @@ # --- # name: test_function_results[result_from_outside_the_package] list([ - dict({ - 'docstring': dict({ - 'description': '', - 'type': '', - }), - 'id': 'various_modules_package/function_module/result_from_outside_the_package/result_1', - 'name': 'result_1', - 'type': None, - }), ]) # --- # name: test_function_results[set_results] @@ -5835,7 +5826,6 @@ 'reexported_by': list([ ]), 'results': list([ - 'various_modules_package/function_module/result_from_outside_the_package/result_1', ]), }), dict({ From bfe72f2b076f20edfa7d458fc41b97f40fd5f570 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 10 Dec 2023 15:47:08 +0100 Subject: [PATCH 04/49] Moved the check if a type is from the same package to the stub generator part; Refactoring --- .../api_analyzer/_ast_visitor.py | 60 ++------- .../stubs_generator/_generate_stubs.py | 123 +++++++++++------- .../__snapshots__/test__get_api.ambr | 47 +++++-- .../__snapshots__/test_generate_stubs.ambr | 5 +- .../stubs_generator/test_generate_stubs.py | 5 +- 5 files changed, 129 insertions(+), 111 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 2c0fd8b9..abff2b0a 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -376,12 +376,7 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul if node_type is not None and hasattr(node_type, "ret_type"): node_ret_type = node_type.ret_type - node_ret_type_type = getattr(node_ret_type, "type", None) - is_part_of_package = True - if node_ret_type_type is not None: - is_part_of_package = self._is_part_of_package(node_ret_type_type.fullname) - - if not isinstance(node_ret_type, mp_types.NoneType) and is_part_of_package: + if not isinstance(node_ret_type, mp_types.NoneType): if isinstance(node_ret_type, mp_types.AnyType) and not has_correct_type_of_any( node_ret_type.type_of_any, ): @@ -558,13 +553,6 @@ def _create_attribute( unanalyzed_type: mp_types.Type | None, is_static: bool, ) -> Attribute: - # Get name and qname - if hasattr(attribute, "name"): - name = attribute.name - else: # pragma: no cover - raise AttributeError("Expected attribute to have attribute 'name'.") - qname = getattr(attribute, "fullname", "") - # Get node information if hasattr(attribute, "node"): if not isinstance(attribute.node, mp_nodes.Var): # pragma: no cover @@ -574,6 +562,10 @@ def _create_attribute( else: # pragma: no cover raise AttributeError("Expected attribute to have attribute 'node'.") + # Get name and qname + name = getattr(attribute, "name", "") + qname = getattr(attribute, "fullname", "") + # Sometimes the qname is not in the attribute.fullname field, in that case we have to get it from the node if qname in (name, "") and node is not None: qname = node.fullname @@ -608,20 +600,12 @@ def _create_attribute( else: # pragma: no cover raise TypeError("Attribute has an unexpected type.") - # Check if the attribute type is part of the package we analyze - if isinstance(attribute_type, mp_types.CallableType): - attr_fullname = attribute_type.ret_type.type.fullname - else: - attribute_type_information = getattr(attribute_type, "type", None) - attr_fullname = getattr(attribute_type_information, "fullname", "") - is_part_of_package = attr_fullname and not self._is_part_of_package(attr_fullname) - type_ = None - if is_part_of_package and attribute_type is not None and not ( + # Ignore types that are special mypy any types + if attribute_type is not None and not ( isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) ): - # Ignore types that are special mypy any types & ignore types from outside the package we analyze # noinspection PyTypeChecker type_ = mypy_type_to_abstract_type(attribute_type, unanalyzed_type) @@ -656,23 +640,12 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis default_value = None default_is_none = False - # Get qname - arg_type_data = getattr(argument.variable.type, "type", None) - arg_type_qname = "" - if arg_type_data is not None: - arg_type_qname = arg_type_data.fullname - # Get type information for parameter if mypy_type is None: # pragma: no cover raise ValueError("Argument has no type.") elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any): # We try to infer the type through the default value later, if possible pass - elif (isinstance(type_annotation, mp_types.UnboundType) and - arg_type_qname and not self._is_part_of_package(arg_type_qname)): - # Types can only be either core classes of any type or types which are defined in the same package. - # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 - pass elif ( isinstance(type_annotation, mp_types.UnboundType) and type_annotation.name in {"list", "set"} @@ -722,17 +695,14 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis return arguments + @staticmethod def _get_parameter_type_and_default_value( - self, initializer: mp_nodes.Expression, + initializer: mp_nodes.Expression, ) -> tuple[str | None | int | float, bool]: default_value = None default_is_none = False if initializer is not None: - if ( - isinstance(initializer, mp_nodes.NameExpr) - and initializer.name not in {"None", "True", "False"} - and not self._is_part_of_package(initializer.fullname) - ): + if isinstance(initializer, mp_nodes.NameExpr) and initializer.name not in {"None", "True", "False"}: # Ignore this case, b/c Safe-DS does not support types that aren't core classes or classes definied # in the package we analyze with Safe-DS. return default_value, default_is_none @@ -797,16 +767,6 @@ def _add_reexports(self, module: Module) -> None: # #### Misc. utilities - # Todo This check is currently too weak, we should try to get the path to the package from the api object, not - # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed - # from the package. - def _is_part_of_package(self, qname: str) -> bool: - """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze. - """ - if "builtins." in qname: - return True - return self.api.package in qname - def _create_module_id(self, qname: str) -> str: """Create an ID for the module object. diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index cbf9e159..cabbeaae 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -16,7 +16,7 @@ Result, UnionType, VarianceKind, - WildcardImport, + WildcardImport, Attribute, ) if TYPE_CHECKING: @@ -42,7 +42,7 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: modules = api.modules.values() Path(out_path / api.package).mkdir(parents=True, exist_ok=True) - generator = StubsStringGenerator(convert_identifiers) + generator = StubsStringGenerator(api.package, convert_identifiers) for module in modules: module_name = module.name @@ -79,7 +79,8 @@ class StubsStringGenerator: method. """ - def __init__(self, convert_identifiers: bool) -> None: + def __init__(self, package_name: str, convert_identifiers: bool) -> None: + self.package_name = package_name self._current_todo_msgs: set[str] = set() self.convert_identifiers = convert_identifiers @@ -200,16 +201,59 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st ) # Attributes + class_text += self._create_class_attribute_string(class_.attributes, inner_indentations) + + # Inner classes + for inner_class in class_.classes: + class_text += f"\n{self._create_class_string(inner_class, inner_indentations)}\n" + + # Methods + class_text += self._create_class_method_string(class_.methods, inner_indentations) + + # If the does not have a body, we just return the signature line + if not class_text: + return class_signature + + # Close class + class_text += f"{class_indentation}}}" + + return f"{class_signature} {{{class_text}" + + def _create_class_method_string(self, methods: list[Function], inner_indentations: str) -> str: + class_methods: list[str] = [] + class_property_methods: list[str] = [] + for method in methods: + if not method.is_public: + continue + elif method.is_property: + class_property_methods.append( + self._create_property_function_string(method, inner_indentations), + ) + else: + class_methods.append( + self._create_function_string(method, inner_indentations, is_method=True), + ) + + method_text = "" + if class_property_methods: + properties = "\n".join(class_property_methods) + method_text += f"\n{properties}\n" + + if class_methods: + method_infos = "\n\n".join(class_methods) + method_text += f"\n{method_infos}\n" + + return method_text + + def _create_class_attribute_string(self, attributes: list[Attribute], inner_indentations: str) -> str: class_attributes: list[str] = [] - for attribute in class_.attributes: + for attribute in attributes: if not attribute.is_public: continue attribute_type = None if attribute.type: attribute_type = attribute.type.to_dict() - else: - self._current_todo_msgs.add("attr without type") static_string = "static " if attribute.is_static else "" @@ -226,6 +270,8 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st # Create type information attr_type = self._create_type_string(attribute_type) type_string = f": {attr_type}" if attr_type else "" + if not type_string: + self._current_todo_msgs.add("attr without type") # Create attribute string class_attributes.append( @@ -235,45 +281,11 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st f"{type_string}", ) + attribute_text = "" if class_attributes: - attributes = "\n".join(class_attributes) - class_text += f"\n{attributes}\n" - - # Inner classes - for inner_class in class_.classes: - class_text += f"\n{self._create_class_string(inner_class, inner_indentations)}\n" - - # Methods - class_methods: list[str] = [] - class_property_methods: list[str] = [] - for method in class_.methods: - if not method.is_public: - continue - elif method.is_property: - class_property_methods.append( - self._create_property_function_string(method, inner_indentations), - ) - else: - class_methods.append( - self._create_function_string(method, inner_indentations, is_method=True), - ) - - if class_property_methods: - properties = "\n".join(class_property_methods) - class_text += f"\n{properties}\n" - - if class_methods: - methods = "\n\n".join(class_methods) - class_text += f"\n{methods}\n" - - # If the does not have a body, we just return the signature line - if not class_text: - return class_signature - - # Close class - class_text += f"{class_indentation}}}" - - return f"{class_signature} {{{class_text}" + attribute_infos = "\n".join(class_attributes) + attribute_text += f"\n{attribute_infos}\n" + return attribute_text def _create_function_string(self, function: Function, indentations: str = "", is_method: bool = False) -> str: """Create a function string for Safe-DS stubs.""" @@ -354,9 +366,10 @@ def _create_result_string(self, function_results: list[Result]) -> str: type_string = f": {ret_type}" if ret_type else "" result_name = self._convert_snake_to_camel_case(result.name) result_name = self._replace_if_safeds_keyword(result_name) - results.append( - f"{result_name}{type_string}", - ) + if type_string: + results.append( + f"{result_name}{type_string}", + ) if results: if len(results) == 1: @@ -534,6 +547,9 @@ def _create_type_string(self, type_data: dict | None) -> str: case "None": return none_type_name case _: + qname = type_data["qname"] + if not self._is_part_of_package(qname): + return "" return name elif kind == "FinalType": return self._create_type_string(type_data["type"]) @@ -626,6 +642,19 @@ def _create_type_string(self, type_data: dict | None) -> str: # ############################### Utilities ############################### # + # Todo This check is currently too weak, we should try to get the path to the package from the api object, not + # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed + # from the package. + def _is_part_of_package(self, qname: str) -> bool: + """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze. + """ + if qname == "": + return True + + if "builtins." in qname: + return True + return self.package_name in qname + @staticmethod def _callable_type_name_generator() -> Generator: """Generate a name for callable type parameters starting from 'a' until 'zz'.""" 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 e04e8658..8c8132b8 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -96,7 +96,16 @@ 'is_public': True, 'is_static': True, 'name': 'attr_default_value_from_outside_package', - 'type': None, + 'type': dict({ + 'kind': 'CallableType', + 'parameter_types': list([ + ]), + 'return_type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), }), dict({ 'docstring': dict({ @@ -108,7 +117,11 @@ 'is_public': True, 'is_static': True, 'name': 'attr_type_from_outside_package', - 'type': None, + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), }), dict({ 'docstring': dict({ @@ -3453,7 +3466,11 @@ 'id': 'various_modules_package/function_module/param_from_outside_the_package/param_type', 'is_optional': False, 'name': 'param_type', - 'type': None, + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), }), dict({ 'assigned_by': 'POSITION_OR_NAME', @@ -3504,20 +3521,16 @@ }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': 'FunctionModuleClassA', + 'default_value': None, 'docstring': dict({ 'default_value': '', 'description': '', 'type': '', }), 'id': 'various_modules_package/function_module/param_position/c', - 'is_optional': True, + 'is_optional': False, 'name': 'c', - 'type': dict({ - 'kind': 'NamedType', - 'name': 'FunctionModuleClassA', - 'qname': 'tests.data.various_modules_package.function_module.FunctionModuleClassA', - }), + 'type': None, }), dict({ 'assigned_by': 'NAME_ONLY', @@ -4978,6 +4991,19 @@ # --- # name: test_function_results[result_from_outside_the_package] list([ + dict({ + 'docstring': dict({ + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/result_from_outside_the_package/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), ]) # --- # name: test_function_results[set_results] @@ -5826,6 +5852,7 @@ 'reexported_by': list([ ]), 'results': list([ + 'various_modules_package/function_module/result_from_outside_the_package/result_1', ]), }), dict({ diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 573c716b..49cd598a 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -135,9 +135,8 @@ // TODO Attribute has no type information. @PythonName("attr_type_from_outside_package") static attr attrTypeFromOutsidePackage - // TODO Attribute has no type information. @PythonName("attr_default_value_from_outside_package") - static attr attrDefaultValueFromOutsidePackage + static attr attrDefaultValueFromOutsidePackage: () -> a: @PythonName("init_attr") attr initAttr: Boolean @@ -326,7 +325,7 @@ self, a, b: Boolean, - c: FunctionModuleClassA = FunctionModuleClassA, + c, d, e: Int = 1 ) diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 84a3a91d..1d1e24f3 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -136,5 +136,8 @@ def test_convert_snake_to_camel_case( is_class_name: bool, convert_identifiers: bool, ) -> None: - stubs_string_generator = StubsStringGenerator(convert_identifiers=convert_identifiers) + stubs_string_generator = StubsStringGenerator( + package_name=_test_package_name, + convert_identifiers=convert_identifiers + ) assert stubs_string_generator._convert_snake_to_camel_case(name, is_class_name) == expected_result From b0ec7fee2906ecaac6107d5c787acd8557e3044a Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 10 Dec 2023 14:52:33 +0000 Subject: [PATCH 05/49] style: apply automated linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 3 +-- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 6 +++--- tests/safeds_stubgen/stubs_generator/test_generate_stubs.py | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index abff2b0a..c71d754e 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -603,8 +603,7 @@ def _create_attribute( type_ = None # Ignore types that are special mypy any types if attribute_type is not None and not ( - isinstance(attribute_type, mp_types.AnyType) and - not has_correct_type_of_any(attribute_type.type_of_any) + isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) ): # noinspection PyTypeChecker type_ = mypy_type_to_abstract_type(attribute_type, unanalyzed_type) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index cabbeaae..2a36f43b 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -6,6 +6,7 @@ from safeds_stubgen.api_analyzer import ( API, + Attribute, Class, Enum, Function, @@ -16,7 +17,7 @@ Result, UnionType, VarianceKind, - WildcardImport, Attribute, + WildcardImport, ) if TYPE_CHECKING: @@ -646,8 +647,7 @@ def _create_type_string(self, type_data: dict | None) -> str: # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed # from the package. def _is_part_of_package(self, qname: str) -> bool: - """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze. - """ + """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze.""" if qname == "": return True diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 1d1e24f3..5b3a2160 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -137,7 +137,6 @@ def test_convert_snake_to_camel_case( convert_identifiers: bool, ) -> None: stubs_string_generator = StubsStringGenerator( - package_name=_test_package_name, - convert_identifiers=convert_identifiers + package_name=_test_package_name, convert_identifiers=convert_identifiers, ) assert stubs_string_generator._convert_snake_to_camel_case(name, is_class_name) == expected_result From 8c50f8e6278ebe69c7bde49b9688a39c8ab9b681 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 10 Dec 2023 14:54:11 +0000 Subject: [PATCH 06/49] style: apply automated linter fixes --- tests/safeds_stubgen/stubs_generator/test_generate_stubs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 5b3a2160..deb98613 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -137,6 +137,7 @@ def test_convert_snake_to_camel_case( convert_identifiers: bool, ) -> None: stubs_string_generator = StubsStringGenerator( - package_name=_test_package_name, convert_identifiers=convert_identifiers, + package_name=_test_package_name, + convert_identifiers=convert_identifiers, ) assert stubs_string_generator._convert_snake_to_camel_case(name, is_class_name) == expected_result From 9b683bd14946a2c0df2962484813328302f9a2b1 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 10 Dec 2023 18:16:02 +0100 Subject: [PATCH 07/49] Refactoring for code coverage --- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index cabbeaae..bb8b9dc8 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -650,9 +650,6 @@ def _is_part_of_package(self, qname: str) -> bool: """ if qname == "": return True - - if "builtins." in qname: - return True return self.package_name in qname @staticmethod From 36fea1f6f53ebdca45e55d07b6abf351e7cfbc50 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 10 Dec 2023 18:34:06 +0100 Subject: [PATCH 08/49] Refactoring for code coverage and adding a test case --- .../api_analyzer/_mypy_helpers.py | 63 ++----------------- .../function_module.py | 1 + .../__snapshots__/test__get_api.ambr | 26 ++++++++ .../__snapshots__/test_generate_stubs.ambr | 1 + 4 files changed, 33 insertions(+), 58 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 134b0a26..67ba7c76 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -104,21 +104,12 @@ def mypy_type_to_abstract_type( return sds_types.ListType(types=types) case "set": return sds_types.SetType(types=types) - raise ValueError("Unexpected outcome.") # pragma: no cover elif type_name == "dict": - key_type = mypy_type_to_abstract_type(mypy_type.args[0]) - value_types = [mypy_type_to_abstract_type(arg) for arg in mypy_type.args[1:]] - - value_type: AbstractType - if len(value_types) == 0: - value_type = sds_types.NamedType(name="Any") - elif len(value_types) == 1: - value_type = value_types[0] - else: - value_type = sds_types.UnionType(types=value_types) - - return sds_types.DictType(key_type=key_type, value_type=value_type) + return sds_types.DictType( + key_type=mypy_type_to_abstract_type(mypy_type.args[0]), + value_type=mypy_type_to_abstract_type(mypy_type.args[1]) + ) else: return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) raise ValueError("Unexpected type.") # pragma: no cover @@ -198,29 +189,7 @@ def mypy_expression_to_sds_type(expr: mp_nodes.Expression) -> sds_types.Abstract return sds_types.NamedType(name="str", qname="builtins.str") elif isinstance(expr, mp_nodes.TupleExpr): return sds_types.TupleType(types=[mypy_expression_to_sds_type(item) for item in expr.items]) - # # This is currently not used since Safe-DS does not support these default value types - # elif isinstance(expr, mp_nodes.ListExpr | mp_nodes.SetExpr): - # unsorted_types = {mypy_expression_to_sds_type(item) for item in expr.items} - # types = list(unsorted_types) - # types.sort() - # if isinstance(expr, mp_nodes.ListExpr): - # return sds_types.ListType(types=types) - # elif isinstance(expr, mp_nodes.SetExpr): - # return sds_types.SetType(types=types) - # elif isinstance(expr, mp_nodes.DictExpr): - # key_items = expr.items[0] - # value_items = expr.items[1] - # - # key_types = [ - # mypy_expression_to_sds_type(key_item) for key_item in key_items if key_item is not None] - # value_types = [ - # mypy_expression_to_sds_type(value_item) for value_item in value_items if value_item is not None - # ] - # - # key_type = sds_types.UnionType(types=key_types) if len(key_types) >= 2 else key_types[0] - # value_type = sds_types.UnionType(types=value_types) if len(value_types) >= 2 else value_types[0] - # - # return sds_types.DictType(key_type=key_type, value_type=value_type) + raise TypeError("Unexpected expression type.") # pragma: no cover @@ -237,27 +206,5 @@ def mypy_expression_to_python_value(expr: mp_nodes.Expression) -> str | None | i return expr.name elif isinstance(expr, mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr): return expr.value - # # This is currently not used since Safe-DS does not support these default value types - # elif isinstance(expr, mp_nodes.ListExpr): - # return [ - # mypy_expression_to_python_value(item) - # for item in expr.items - # ] - # elif isinstance(expr, mp_nodes.SetExpr): - # return { - # mypy_expression_to_python_value(item) - # for item in expr.items - # } - # elif isinstance(expr, mp_nodes.TupleExpr): - # return tuple( - # mypy_expression_to_python_value(item) - # for item in expr.items - # ) - # elif isinstance(expr, mp_nodes.DictExpr): - # return { - # mypy_expression_to_python_value(item[0]): mypy_expression_to_python_value(item[1]) - # for item in expr.items - # if item[0] is not None and item[1] is not None - # } raise TypeError("Unexpected expression type.") # pragma: no cover diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 7fa1c037..a2127782 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -83,6 +83,7 @@ def illegal_params( lst: list[int, str], lst_2: list[int, str, int], tpl: tuple[int, str, bool, int], + dct: dict[str, int, None, bool], _: int = "String", ): ... 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 8c8132b8..74fda279 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -3202,6 +3202,31 @@ 'qname': 'builtins.int', }), }), + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/illegal_params/dct', + 'is_optional': False, + 'name': 'dct', + 'type': dict({ + 'key_type': dict({ + 'kind': 'NamedType', + 'name': 'Any', + 'qname': '', + }), + 'kind': 'DictType', + 'value_type': dict({ + 'kind': 'NamedType', + 'name': 'Any', + 'qname': '', + }), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -5525,6 +5550,7 @@ 'various_modules_package/function_module/illegal_params/lst', 'various_modules_package/function_module/illegal_params/lst_2', 'various_modules_package/function_module/illegal_params/tpl', + 'various_modules_package/function_module/illegal_params/dct', 'various_modules_package/function_module/illegal_params/_', ]), 'reexported_by': list([ diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 49cd598a..89d32292 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -299,6 +299,7 @@ lst: List, @PythonName("lst_2") lst2: List, tpl: Tuple, + dct: Map, `_`: Int = String ) From 08d203b4a73022178e12e98da3b109e6f79b6c94 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 10 Dec 2023 17:35:43 +0000 Subject: [PATCH 09/49] style: apply automated linter fixes --- src/safeds_stubgen/api_analyzer/_mypy_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 67ba7c76..e8225b0f 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -108,7 +108,7 @@ def mypy_type_to_abstract_type( elif type_name == "dict": return sds_types.DictType( key_type=mypy_type_to_abstract_type(mypy_type.args[0]), - value_type=mypy_type_to_abstract_type(mypy_type.args[1]) + value_type=mypy_type_to_abstract_type(mypy_type.args[1]), ) else: return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) From 93965314deb47666899793067e78f73bdbc1ebac Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 10 Dec 2023 21:40:10 +0100 Subject: [PATCH 10/49] Changed how imports are generated for the stubs --- .../api_analyzer/_mypy_helpers.py | 2 +- .../stubs_generator/_generate_stubs.py | 103 ++++++++---------- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index e8225b0f..6c5172bf 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -72,7 +72,7 @@ def mypy_type_to_abstract_type( return_type=mypy_type_to_abstract_type(mypy_type.ret_type), ) elif isinstance(mypy_type, mp_types.AnyType): - return sds_types.NamedType(name="Any") + return sds_types.NamedType(name="Any", qname="builtins.Any") elif isinstance(mypy_type, mp_types.NoneType): return sds_types.NamedType(name="None", qname="builtins.None") elif isinstance(mypy_type, mp_types.LiteralType): diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 170f9648..027e182c 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -43,7 +43,7 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: modules = api.modules.values() Path(out_path / api.package).mkdir(parents=True, exist_ok=True) - generator = StubsStringGenerator(api.package, convert_identifiers) + stubs_generator = StubsStringGenerator(api.package, convert_identifiers) for module in modules: module_name = module.name @@ -51,7 +51,7 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: if module_name == "__init__": continue - module_text = generator.create_module_string(module) + module_text = stubs_generator(module) # Each text block we create ends with "\n", therefore, is there is only the package information # the file would look like this: "package path.to.myPackage\n" or this: @@ -81,27 +81,26 @@ class StubsStringGenerator: """ def __init__(self, package_name: str, convert_identifiers: bool) -> None: + self.module_imports: set[str] = set() self.package_name = package_name - self._current_todo_msgs: set[str] = set() self.convert_identifiers = convert_identifiers - def create_module_string(self, module: Module) -> str: + def __call__(self, module: Module) -> str: + # Reset the module_imports list + self.module_imports = set() + self._current_todo_msgs: set[str] = set() + self.module = module + return self._create_module_string(module) + + def _create_module_string(self, module: Module) -> str: # Create package info package_info = module.id.replace("/", ".") package_info_camel_case = self._convert_snake_to_camel_case(package_info) module_name_info = "" + module_text = "" if package_info != package_info_camel_case: module_name_info = f'@PythonModule("{package_info}")\n' - module_text = f"{module_name_info}package {package_info_camel_case}\n" - - # Create imports - qualified_imports = self._create_qualified_imports_string(module.qualified_imports) - if qualified_imports: - module_text += f"\n{qualified_imports}\n" - - wildcard_imports = self._create_wildcard_imports_string(module.wildcard_imports) - if wildcard_imports: - module_text += f"\n{wildcard_imports}\n" + module_header = f"{module_name_info}package {package_info_camel_case}\n" # Create global functions and properties for function in module.global_functions: @@ -117,7 +116,25 @@ def create_module_string(self, module: Module) -> str: for enum in module.enums: module_text += f"\n{self._create_enum_string(enum)}\n" - return module_text + # Create imports - We have to create them last, since we have to check all used types in this module first + module_header += self._create_imports_string() + + return module_header + module_text + + def _create_imports_string(self) -> str: + if not self.module_imports: + return "" + + import_strings = [] + + for import_ in self.module_imports: + import_parts = import_.split(".") + from_ = ".".join(import_parts[0:-1]) + name = import_parts[-1] + import_strings.append(f"from {from_} import {name}") + + import_string = "\n".join(import_strings) + return f"\n{import_string}\n" def _create_class_string(self, class_: Class, class_indentation: str = "") -> str: inner_indentations = class_indentation + "\t" @@ -466,40 +483,6 @@ def _create_parameter_string( return f"\n{inner_indentations}{inner_param_data}\n{indentations}" return "" - def _create_qualified_imports_string(self, qualified_imports: list[QualifiedImport]) -> str: - if not qualified_imports: - return "" - - imports: list[str] = [] - for qualified_import in qualified_imports: - qualified_name = qualified_import.qualified_name - import_path, name = self._split_import_id(qualified_name) - - # Ignore enum imports, since those are build in types in Safe-DS stubs - if import_path == "enum" and name in {"Enum", "IntEnum"}: - continue - - # Create string and check if Safe-DS keywords are used and escape them if necessary - from_path = f"from {self._replace_if_safeds_keyword(import_path)} " if import_path else "" - alias = f" as {self._replace_if_safeds_keyword(qualified_import.alias)}" if qualified_import.alias else "" - - imports.append( - f"{from_path}import {self._replace_if_safeds_keyword(name)}{alias}", - ) - - return "\n".join(imports) - - def _create_wildcard_imports_string(self, wildcard_imports: list[WildcardImport]) -> str: - if not wildcard_imports: - return "" - - imports = [ - f"from {self._replace_if_safeds_keyword(wildcard_import.module_name)} import *" - for wildcard_import in wildcard_imports - ] - - return "\n".join(imports) - def _create_enum_string(self, enum_data: Enum) -> str: # Signature enum_signature = f"enum {enum_data.name}" @@ -548,9 +531,7 @@ def _create_type_string(self, type_data: dict | None) -> str: case "None": return none_type_name case _: - qname = type_data["qname"] - if not self._is_part_of_package(qname): - return "" + self._add_to_imports(type_data["qname"]) return name elif kind == "FinalType": return self._create_type_string(type_data["type"]) @@ -643,14 +624,18 @@ def _create_type_string(self, type_data: dict | None) -> str: # ############################### Utilities ############################### # - # Todo This check is currently too weak, we should try to get the path to the package from the api object, not - # just the package name. We will resolve this with or after issue #24 and #38, since more information are needed - # from the package. - def _is_part_of_package(self, qname: str) -> bool: - """Check if the qname of an attribute, parameter or a smiliar object is part of the current package we analyze.""" + def _add_to_imports(self, qname: str) -> None: + """Check if the qname of a type is defined in the current module. If not, we create an import for it.""" if qname == "": - return True - return self.package_name in qname + raise ValueError("Type has no import source.") + + qname_parts = qname.split(".") + if qname_parts[0] == "builtins" and len(qname_parts) == 2: + return + + module_id = self.module.id.replace("/", ".") + if module_id not in qname: + self.module_imports.add(qname) @staticmethod def _callable_type_name_generator() -> Generator: From e9e91d55d437c431c7051a86094f415a3294f23e Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 10 Dec 2023 20:41:45 +0000 Subject: [PATCH 11/49] style: apply automated linter fixes --- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 027e182c..c1d0db4b 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -13,11 +13,9 @@ Module, Parameter, ParameterAssignment, - QualifiedImport, Result, UnionType, VarianceKind, - WildcardImport, ) if TYPE_CHECKING: From 925c8d35eaedba51d864f08ccc9eec2e6c9b2de4 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 19:09:52 +0100 Subject: [PATCH 12/49] The api-analyzer now can handle aliasing for types; Adjusted tests --- .../api_analyzer/_ast_visitor.py | 177 ++++++++++++++++-- src/safeds_stubgen/api_analyzer/_get_api.py | 134 ++++++++++--- .../api_analyzer/_mypy_helpers.py | 89 --------- src/safeds_stubgen/api_analyzer/_types.py | 2 +- .../aliasing/aliasing_module_1.py | 23 +++ .../aliasing/aliasing_module_2.py | 15 ++ .../api_analyzer/test_api_visitor.py | 2 +- .../safeds_stubgen/api_analyzer/test_types.py | 113 ++++++----- .../docstring_parsing/test_epydoc_parser.py | 11 +- .../test_get_full_docstring.py | 11 +- .../test_googledoc_parser.py | 11 +- .../docstring_parsing/test_numpydoc_parser.py | 11 +- .../test_plaintext_docstring_parser.py | 11 +- .../docstring_parsing/test_restdoc_parser.py | 11 +- .../stubs_generator/test_generate_stubs.py | 22 +-- 15 files changed, 417 insertions(+), 226 deletions(-) create mode 100644 tests/data/various_modules_package/aliasing/aliasing_module_1.py create mode 100644 tests/data/various_modules_package/aliasing/aliasing_module_2.py diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index c71d754e..497e2aee 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,10 +1,11 @@ from __future__ import annotations +from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING import mypy.types as mp_types -from mypy import nodes as mp_nodes +import mypy.nodes as mp_nodes import safeds_stubgen.api_analyzer._types as sds_types @@ -32,22 +33,26 @@ has_correct_type_of_any, mypy_expression_to_python_value, mypy_expression_to_sds_type, - mypy_type_to_abstract_type, mypy_variance_parser, ) if TYPE_CHECKING: from safeds_stubgen.docstring_parsing import AbstractDocstringParser, ResultDocstring + from safeds_stubgen.api_analyzer._types import AbstractType + class MyPyAstVisitor: - def __init__(self, docstring_parser: AbstractDocstringParser, api: API) -> None: + def __init__(self, docstring_parser: AbstractDocstringParser, api: API, aliases: dict[str, set[str]]) -> None: self.docstring_parser: AbstractDocstringParser = docstring_parser self.reexported: dict[str, list[Module]] = {} self.api: API = api self.__declaration_stack: list[Module | Class | Function | Enum | list[Attribute | EnumInstance]] = [] + self.aliases = aliases + self.mypy_file = None def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: + self.mypy_file = node is_package = node.path.endswith("__init__.py") qualified_imports: list[QualifiedImport] = [] @@ -152,10 +157,10 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: variance_values: sds_types.AbstractType if variance_type == VarianceKind.INVARIANT: variance_values = sds_types.UnionType([ - mypy_type_to_abstract_type(value) for value in generic_type.values + self.mypy_type_to_abstract_type(value) for value in generic_type.values ]) else: - variance_values = mypy_type_to_abstract_type(generic_type.upper_bound) + variance_values = self.mypy_type_to_abstract_type(generic_type.upper_bound) type_parameters.append( TypeParameter( @@ -387,7 +392,7 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul else: # Otherwise, we can parse the type normally unanalyzed_ret_type = getattr(node.unanalyzed_type, "ret_type", None) - ret_type = mypy_type_to_abstract_type(node_ret_type, unanalyzed_ret_type) + ret_type = self.mypy_type_to_abstract_type(node_ret_type, unanalyzed_ret_type) else: # Infer type ret_type = self._infer_type_from_return_stmts(node) @@ -601,12 +606,14 @@ def _create_attribute( raise TypeError("Attribute has an unexpected type.") type_ = None - # Ignore types that are special mypy any types + # Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase if attribute_type is not None and not ( - isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) + isinstance(attribute_type, mp_types.AnyType) and + not has_correct_type_of_any(attribute_type.type_of_any) and + attribute_type.type_of_any != mp_types.TypeOfAny.from_unimported_type ): # noinspection PyTypeChecker - type_ = mypy_type_to_abstract_type(attribute_type, unanalyzed_type) + type_ = self.mypy_type_to_abstract_type(attribute_type, unanalyzed_type) # Get docstring parent = self.__declaration_stack[-1] @@ -653,9 +660,9 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis # A special case where the argument is a list with multiple types. We have to handle this case like this # b/c something like list[int, str] is not allowed according to PEP and therefore not handled the normal # way in Mypy. - arg_type = mypy_type_to_abstract_type(type_annotation) + arg_type = self.mypy_type_to_abstract_type(type_annotation) elif type_annotation is not None: - arg_type = mypy_type_to_abstract_type(mypy_type) + arg_type = self.mypy_type_to_abstract_type(mypy_type) # Get default value and infer type information initializer = argument.initializer @@ -765,6 +772,154 @@ def _add_reexports(self, module: Module) -> None: self.reexported[name] = [module] # #### Misc. utilities + def mypy_type_to_abstract_type( + self, + mypy_type: mp_types.Instance | mp_types.ProperType | mp_types.MypyType, + unanalyzed_type: mp_types.Type | None = None, + ) -> AbstractType: + + # Special cases where we need the unanalyzed_type to get the type information we need + if unanalyzed_type is not None and hasattr(unanalyzed_type, "name"): + unanalyzed_type_name = unanalyzed_type.name + if unanalyzed_type_name == "Final": + # Final type + types = [self.mypy_type_to_abstract_type(arg) for arg in getattr(unanalyzed_type, "args", [])] + if len(types) == 1: + return sds_types.FinalType(type_=types[0]) + elif len(types) == 0: # pragma: no cover + raise ValueError("Final type has no type arguments.") + return sds_types.FinalType(type_=sds_types.UnionType(types=types)) + elif unanalyzed_type_name in {"list", "set"}: + type_args = getattr(mypy_type, "args", []) + if ( + len(type_args) == 1 + and isinstance(type_args[0], mp_types.AnyType) + and not has_correct_type_of_any(type_args[0].type_of_any) + ): + # This case happens if we have a list or set with multiple arguments like "list[str, int]" which is + # not allowed. In this case mypy interprets the type as "list[Any]", but we want the real types + # of the list arguments, which we cant get through the "unanalyzed_type" attribute + return self.mypy_type_to_abstract_type(unanalyzed_type) + + # Iterable mypy types + if isinstance(mypy_type, mp_types.TupleType): + return sds_types.TupleType(types=[self.mypy_type_to_abstract_type(item) for item in mypy_type.items]) + elif isinstance(mypy_type, mp_types.UnionType): + return sds_types.UnionType(types=[self.mypy_type_to_abstract_type(item) for item in mypy_type.items]) + + # Special Cases + 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], + return_type=self.mypy_type_to_abstract_type(mypy_type.ret_type), + ) + elif isinstance(mypy_type, mp_types.AnyType): + if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type: + missing_import_name = mypy_type.missing_import_name.split(".")[-1] + name, qname = self._find_alias(missing_import_name) + return sds_types.NamedType(name=name, qname=qname) + else: + return sds_types.NamedType(name="Any", qname="builtins.Any") + elif isinstance(mypy_type, mp_types.NoneType): + return sds_types.NamedType(name="None", qname="builtins.None") + elif isinstance(mypy_type, mp_types.LiteralType): + return sds_types.LiteralType(literals=[mypy_type.value]) + elif isinstance(mypy_type, mp_types.UnboundType): + if mypy_type.name in {"list", "set"}: + return { + "list": sds_types.ListType, + "set": sds_types.SetType, + }[ + mypy_type.name + ](types=[self.mypy_type_to_abstract_type(arg) for arg in mypy_type.args]) + + # Get qname + if mypy_type.name in {"Any", "str", "int", "bool", "float", "None"}: + name = mypy_type.name + qname = f"builtins.{mypy_type.name}" + else: + name, qname = self._find_alias(mypy_type.name) + + return sds_types.NamedType(name=name, qname=qname) + + # Builtins + elif isinstance(mypy_type, mp_types.Instance): + type_name = mypy_type.type.name + if type_name in {"int", "str", "bool", "float"}: + return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) + + # Iterable builtins + elif type_name in {"tuple", "list", "set"}: + types = [self.mypy_type_to_abstract_type(arg) for arg in mypy_type.args] + match type_name: + case "tuple": + return sds_types.TupleType(types=types) + case "list": + return sds_types.ListType(types=types) + case "set": + return sds_types.SetType(types=types) + + elif type_name == "dict": + return sds_types.DictType( + key_type=self.mypy_type_to_abstract_type(mypy_type.args[0]), + value_type=self.mypy_type_to_abstract_type(mypy_type.args[1]), + ) + else: + return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) + raise ValueError("Unexpected type.") # pragma: no cover + + def _find_alias(self, type_name: str): + module = self.__declaration_stack[0] + + name = "" + qname = "" + if type_name in self.aliases.keys(): + qnames: set = self.aliases[type_name] + if len(qnames) == 1: + qname = deepcopy(qnames).pop() + name = qname.split(".")[-1] + + # We have to check if this is an alias from an import + found_alias = False + for qualified_import in module.qualified_imports: + if qualified_import.alias == name: + qname = qualified_import.qualified_name + found_alias = True + break + + if found_alias: + # Overwrite the name, since it was only an alias + name = qname.split(".")[-1] + + else: + # In this case some type was defined in multiple modules with the same name. + for alias_qname in qnames: + # First we check if the type was defined in the same module + type_path = ".".join(alias_qname.split(".")[0:-1]) + name = alias_qname.split(".")[-1] + if type_path == self.mypy_file.fullname: + qname = alias_qname + break + + # Then we check if the type was perhapse imported + for qualified_import in module.qualified_imports: + if qualified_import.alias == name: + qname = qualified_import.qualified_name + name = qname.split(".")[-1] + break + elif qualified_import.qualified_name in alias_qname: + qname = alias_qname + break + + for wildcard_import in module.wildcard_imports: + if wildcard_import.module_name in alias_qname: + qname = alias_qname + break + + if not qname: # pragma: no cover + raise ValueError(f"It was not possible to find out where the alias {type_name} was defined.") + + return name, qname def _create_module_id(self, qname: str) -> str: """Create an ID for the module object. diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index e20b1c6f..651c60e9 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -2,7 +2,6 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING import mypy.build as mypy_build import mypy.main as mypy_main @@ -13,9 +12,8 @@ from ._ast_visitor import MyPyAstVisitor from ._ast_walker import ASTWalker from ._package_metadata import distribution, distribution_version, package_root - -if TYPE_CHECKING: - from mypy.nodes import MypyFile +from mypy import nodes as mypy_nodes +from mypy import types as mypy_types def get_api( @@ -28,16 +26,6 @@ def get_api( if root is None: root = package_root(package_name) - # Get distribution data - dist = distribution(package_name) or "" - dist_version = distribution_version(dist) or "" - - # Setup api walker - api = API(dist, package_name, dist_version) - docstring_parser = create_docstring_parser(docstring_style) - callable_visitor = MyPyAstVisitor(docstring_parser, api) - walker = ASTWalker(callable_visitor) - walkable_files = [] package_paths = [] for file_path in root.glob(pattern="./**/*.py"): @@ -61,26 +49,53 @@ def get_api( walkable_files.append(str(file_path)) - mypy_trees = _get_mypy_ast(walkable_files, package_paths, root) - for tree in mypy_trees: + if not walkable_files: + raise ValueError("No files found to analyse.") + + # Get distribution data + dist = distribution(package_name) or "" + dist_version = distribution_version(dist) or "" + + # Get mypy ast and aliases + build_result = _get_mypy_build(walkable_files) + mypy_asts = _get_mypy_asts(build_result, walkable_files, package_paths, root) + aliases = _get_aliases(build_result.types, package_name) + + # Setup api walker + api = API(dist, package_name, dist_version) + docstring_parser = create_docstring_parser(docstring_style) + callable_visitor = MyPyAstVisitor(docstring_parser, api, aliases) + walker = ASTWalker(callable_visitor) + + for tree in mypy_asts: walker.walk(tree) return callable_visitor.api -def _get_mypy_ast(files: list[str], package_paths: list[Path], root: Path) -> list[MypyFile]: - if not files: - raise ValueError("No files found to analyse.") - - # Build mypy checker +def _get_mypy_build(files: list[str]) -> mypy_build.BuildResult: + """Build a mypy checker and return the build result.""" mypyfiles, opt = mypy_main.process_options(files) - opt.preserve_asts = True # Disable the memory optimization of freeing ASTs when possible - opt.fine_grained_incremental = True # Only check parts of the code that have changed since the last check - result = mypy_build.build(mypyfiles, options=opt) + # Disable the memory optimization of freeing ASTs when possible + opt.preserve_asts = True + # Only check parts of the code that have changed since the last check + opt.fine_grained_incremental = True + # Export inferred types for all expressions + opt.export_types = True + + return mypy_build.build(mypyfiles, options=opt) + + +def _get_mypy_asts( + build_result: mypy_build.BuildResult, + files: list[str], + package_paths: list[Path], + root: Path, +) -> list[mypy_nodes.MypyFile]: # Check mypy data key root start parts = root.parts - graph_keys = list(result.graph.keys()) + graph_keys = list(build_result.graph.keys()) root_start_after = -1 for i in range(len(parts)): if ".".join(parts[i:]) in graph_keys: @@ -106,10 +121,69 @@ def _get_mypy_ast(files: list[str], package_paths: list[Path], root: Path) -> li # to get the reexported data first all_paths = packages + modules - results = [] + asts = [] for path_key in all_paths: - tree = result.graph[path_key].tree + tree = build_result.graph[path_key].tree if tree is not None: - results.append(tree) - - return results + asts.append(tree) + + return asts + + +def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: + if not result_types: + return {} + + aliases = {} + for key in result_types.keys(): + if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr): + if isinstance(key, mypy_nodes.NameExpr): + type_value = result_types[key] + + if hasattr(type_value, "type") and getattr(type_value, "type", None) is not None: + name = type_value.type.name + in_package = package_name in type_value.type.fullname + elif hasattr(key, "name"): + name = key.name + fullname = "" + + if (hasattr(key, "node") and + isinstance(key.node, mypy_nodes.TypeAlias) and + isinstance(key.node.target, mypy_types.Instance)): + fullname = key.node.target.type.fullname + elif isinstance(type_value, mypy_types.CallableType): + bound_args = type_value.bound_args + if bound_args and not isinstance(bound_args[0], mypy_types.TupleType): + fullname = bound_args[0].type.fullname + elif hasattr(key, "node") and isinstance(key.node, mypy_nodes.Var): + fullname = key.node.fullname + + if not fullname: + continue + + in_package = package_name in fullname + else: + continue + else: + in_package = package_name in key.fullname + if in_package: + type_value = result_types[key] + name = key.name + else: + continue + + if in_package: + if isinstance(type_value, mypy_types.CallableType): + fullname = type_value.bound_args[0].type.fullname + elif isinstance(type_value, mypy_types.Instance): + fullname = type_value.type.fullname + elif isinstance(key, mypy_nodes.TypeVarExpr): + fullname = key.fullname + elif isinstance(key, mypy_nodes.NameExpr) and isinstance(key.node, mypy_nodes.Var): + fullname = key.node.fullname + else: # pragma: no cover + raise TypeError("Received unexpected type while searching for aliases.") + + aliases.setdefault(name, set()).add(fullname) + + return aliases diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 6c5172bf..4b104e15 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -5,7 +5,6 @@ import mypy.types as mp_types from mypy import nodes as mp_nodes from mypy.nodes import ArgKind -from mypy.types import Instance import safeds_stubgen.api_analyzer._types as sds_types @@ -13,10 +12,6 @@ if TYPE_CHECKING: from mypy.nodes import ClassDef, FuncDef, MypyFile - from mypy.types import ProperType - from mypy.types import Type as MypyType - - from safeds_stubgen.api_analyzer._types import AbstractType def get_classdef_definitions(node: ClassDef) -> list: @@ -31,90 +26,6 @@ def get_mypyfile_definitions(node: MypyFile) -> list: return node.defs -def mypy_type_to_abstract_type( - mypy_type: Instance | ProperType | MypyType, - unanalyzed_type: mp_types.Type | None = None, -) -> AbstractType: - - # Special cases where we need the unanalyzed_type to get the type information we need - if unanalyzed_type is not None and hasattr(unanalyzed_type, "name"): - unanalyzed_type_name = unanalyzed_type.name - if unanalyzed_type_name == "Final": - # Final type - types = [mypy_type_to_abstract_type(arg) for arg in getattr(unanalyzed_type, "args", [])] - if len(types) == 1: - return sds_types.FinalType(type_=types[0]) - elif len(types) == 0: # pragma: no cover - raise ValueError("Final type has no type arguments.") - return sds_types.FinalType(type_=sds_types.UnionType(types=types)) - elif unanalyzed_type_name in {"list", "set"}: - type_args = getattr(mypy_type, "args", []) - if ( - len(type_args) == 1 - and isinstance(type_args[0], mp_types.AnyType) - and not has_correct_type_of_any(type_args[0].type_of_any) - ): - # This case happens if we have a list or set with multiple arguments like "list[str, int]" which is - # not allowed. In this case mypy interprets the type as "list[Any]", but we want the real types - # of the list arguments, which we cant get through the "unanalyzed_type" attribute - return mypy_type_to_abstract_type(unanalyzed_type) - - # Iterable mypy types - if isinstance(mypy_type, mp_types.TupleType): - return sds_types.TupleType(types=[mypy_type_to_abstract_type(item) for item in mypy_type.items]) - elif isinstance(mypy_type, mp_types.UnionType): - return sds_types.UnionType(types=[mypy_type_to_abstract_type(item) for item in mypy_type.items]) - - # Special Cases - elif isinstance(mypy_type, mp_types.CallableType): - return sds_types.CallableType( - parameter_types=[mypy_type_to_abstract_type(arg_type) for arg_type in mypy_type.arg_types], - return_type=mypy_type_to_abstract_type(mypy_type.ret_type), - ) - elif isinstance(mypy_type, mp_types.AnyType): - return sds_types.NamedType(name="Any", qname="builtins.Any") - elif isinstance(mypy_type, mp_types.NoneType): - return sds_types.NamedType(name="None", qname="builtins.None") - elif isinstance(mypy_type, mp_types.LiteralType): - return sds_types.LiteralType(literals=[mypy_type.value]) - elif isinstance(mypy_type, mp_types.UnboundType): - if mypy_type.name in {"list", "set"}: - return { - "list": sds_types.ListType, - "set": sds_types.SetType, - }[ - mypy_type.name - ](types=[mypy_type_to_abstract_type(arg) for arg in mypy_type.args]) - # Todo Aliasing: Import auflösen, wir können hier keinen fullname (qname) bekommen - return sds_types.NamedType(name=mypy_type.name) - - # Builtins - elif isinstance(mypy_type, Instance): - type_name = mypy_type.type.name - if type_name in {"int", "str", "bool", "float"}: - return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) - - # Iterable builtins - elif type_name in {"tuple", "list", "set"}: - types = [mypy_type_to_abstract_type(arg) for arg in mypy_type.args] - match type_name: - case "tuple": - return sds_types.TupleType(types=types) - case "list": - return sds_types.ListType(types=types) - case "set": - return sds_types.SetType(types=types) - - elif type_name == "dict": - return sds_types.DictType( - key_type=mypy_type_to_abstract_type(mypy_type.args[0]), - value_type=mypy_type_to_abstract_type(mypy_type.args[1]), - ) - else: - return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) - raise ValueError("Unexpected type.") # pragma: no cover - - def get_argument_kind(arg: mp_nodes.Argument) -> ParameterAssignment: if arg.variable.is_self or arg.variable.is_cls: return ParameterAssignment.IMPLICIT diff --git a/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index cc908413..1dd3b6e7 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -46,7 +46,7 @@ def to_dict(self) -> dict[str, Any]: @dataclass(frozen=True) class NamedType(AbstractType): name: str - qname: str = "" + qname: str @classmethod def from_dict(cls, d: dict[str, Any]) -> NamedType: diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py new file mode 100644 index 00000000..ec9a05e6 --- /dev/null +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -0,0 +1,23 @@ +from aliasing_module_2 import AliasingModule2ClassA as AliasModule2 + + +class _AliasingModuleClassA: + ... + + +class AliasingModuleClassB: + ... + + +_some_alias_a = _AliasingModuleClassA +some_alias_b = AliasingModuleClassB + + +class AliasingModuleClassC(_some_alias_a): + typed_alias_attr: some_alias_b + infer_alias_attr = _some_alias_a + + typed_alias_attr2: AliasModule2 + infer_alias_attr2 = AliasModule2 + + alias_list: list[_some_alias_a | some_alias_b, AliasModule2] diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_2.py b/tests/data/various_modules_package/aliasing/aliasing_module_2.py new file mode 100644 index 00000000..e93d3918 --- /dev/null +++ b/tests/data/various_modules_package/aliasing/aliasing_module_2.py @@ -0,0 +1,15 @@ +class AliasingModule2ClassA: + ... + + +class AliasingModuleClassB: + ... + + +some_alias_b = AliasingModuleClassB + + +class AliasingModuleClassC: + typed_alias_attr: some_alias_b + + alias_list: list[str | some_alias_b] diff --git a/tests/safeds_stubgen/api_analyzer/test_api_visitor.py b/tests/safeds_stubgen/api_analyzer/test_api_visitor.py index d4bcc389..17857cc2 100644 --- a/tests/safeds_stubgen/api_analyzer/test_api_visitor.py +++ b/tests/safeds_stubgen/api_analyzer/test_api_visitor.py @@ -66,7 +66,7 @@ def test__create_module_id(qname: str, expected_id: str, package_name: str) -> N version="1.3", ) - visitor = MyPyAstVisitor(PlaintextDocstringParser(), api) + visitor = MyPyAstVisitor(PlaintextDocstringParser(), api, {}) if not expected_id: with pytest.raises(ValueError, match="Package name could not be found in the qualified name of the module."): visitor._create_module_id(qname) diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index f0ec2524..cea899d2 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -29,7 +29,7 @@ def test_correct_hash() -> None: default_value="test_str", assigned_by=ParameterAssignment.POSITION_OR_NAME, docstring=ParameterDocstring("'hashvalue'", "r", "r"), - type=NamedType(name="str"), + type=NamedType(name="str", qname=""), ) assert hash(parameter) == hash(deepcopy(parameter)) enum_values = frozenset({"a", "b", "c"}) @@ -40,10 +40,10 @@ def test_correct_hash() -> None: assert hash(enum_type) == hash(EnumType(deepcopy(enum_values), "full_match")) assert enum_type != EnumType(frozenset({"a", "b"}), "full_match") assert hash(enum_type) != hash(EnumType(frozenset({"a", "b"}), "full_match")) - assert NamedType("a") == NamedType("a") - assert hash(NamedType("a")) == hash(NamedType("a")) - assert NamedType("a") != NamedType("b") - assert hash(NamedType("a")) != hash(NamedType("b")) + assert NamedType("a", "") == NamedType("a", "") + assert hash(NamedType("a", "")) == hash(NamedType("a", "")) + assert NamedType("a", "") != NamedType("b", "") + assert hash(NamedType("a", "")) != hash(NamedType("b", "")) attribute = Attribute( id="boundary", name="boundary", @@ -64,7 +64,7 @@ def test_correct_hash() -> None: def test_named_type() -> None: name = "str" - named_type = NamedType(name) + named_type = NamedType(name, "") named_type_dict = {"kind": "NamedType", "name": name, "qname": ""} assert AbstractType.from_dict(named_type_dict) == named_type @@ -107,7 +107,7 @@ def test_boundary_type() -> None: def test_union_type() -> None: - union_type = UnionType([NamedType("str"), NamedType("int")]) + union_type = UnionType([NamedType("str", ""), NamedType("int", "")]) union_type_dict = { "kind": "UnionType", "types": [{"kind": "NamedType", "name": "str", "qname": ""}, {"kind": "NamedType", "name": "int", "qname": ""}], @@ -117,16 +117,16 @@ def test_union_type() -> None: assert UnionType.from_dict(union_type_dict) == union_type assert union_type.to_dict() == union_type_dict - assert UnionType([NamedType("a")]) == UnionType([NamedType("a")]) - assert hash(UnionType([NamedType("a")])) == hash(UnionType([NamedType("a")])) - assert UnionType([NamedType("a")]) != UnionType([NamedType("b")]) - assert hash(UnionType([NamedType("a")])) != hash(UnionType([NamedType("b")])) + assert UnionType([NamedType("a", "")]) == UnionType([NamedType("a", "")]) + assert hash(UnionType([NamedType("a", "")])) == hash(UnionType([NamedType("a", "")])) + assert UnionType([NamedType("a", "")]) != UnionType([NamedType("b", "")]) + assert hash(UnionType([NamedType("a", "")])) != hash(UnionType([NamedType("b", "")])) def test_callable_type() -> None: callable_type = CallableType( - parameter_types=[NamedType("str"), NamedType("int")], - return_type=TupleType(types=[NamedType("bool"), NamedType("None")]), + parameter_types=[NamedType("str", ""), NamedType("int", "")], + return_type=TupleType(types=[NamedType("bool", ""), NamedType("None", "")]), ) callable_type_dict = { "kind": "CallableType", @@ -147,48 +147,55 @@ def test_callable_type() -> None: assert CallableType.from_dict(callable_type_dict) == callable_type assert callable_type.to_dict() == callable_type_dict - assert CallableType([NamedType("a")], NamedType("a")) == CallableType([NamedType("a")], NamedType("a")) - assert hash(CallableType([NamedType("a")], NamedType("a"))) == hash(CallableType([NamedType("a")], NamedType("a"))) - assert CallableType([NamedType("a")], NamedType("a")) != CallableType([NamedType("b")], NamedType("a")) - assert hash(CallableType([NamedType("a")], NamedType("a"))) != hash(CallableType([NamedType("b")], NamedType("a"))) + assert (CallableType([NamedType("a", "")], NamedType("a", "")) == + CallableType([NamedType("a", "")], NamedType("a", ""))) + assert (hash(CallableType([NamedType("a", "")], NamedType("a", ""))) == + hash(CallableType([NamedType("a", "")], NamedType("a", "")))) + assert (CallableType([NamedType("a", "")], NamedType("a", "")) != + CallableType([NamedType("b", "")], NamedType("a", ""))) + assert (hash(CallableType([NamedType("a", "")], NamedType("a", ""))) != + hash(CallableType([NamedType("b", "")], NamedType("a", "")))) def test_list_type() -> None: - list_type = ListType([NamedType("str"), NamedType("int")]) + list_type = ListType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]) list_type_dict = { "kind": "ListType", - "types": [{"kind": "NamedType", "name": "str", "qname": ""}, {"kind": "NamedType", "name": "int", "qname": ""}], + "types": [ + {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, + {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + ], } assert AbstractType.from_dict(list_type_dict) == list_type assert ListType.from_dict(list_type_dict) == list_type assert list_type.to_dict() == list_type_dict - assert ListType([NamedType("a")]) == ListType([NamedType("a")]) - assert hash(ListType([NamedType("a")])) == hash(ListType([NamedType("a")])) - assert ListType([NamedType("a")]) != ListType([NamedType("b")]) - assert hash(ListType([NamedType("a")])) != hash(ListType([NamedType("b")])) + assert ListType([NamedType("a", "")]) == ListType([NamedType("a", "")]) + assert hash(ListType([NamedType("a", "")])) == hash(ListType([NamedType("a", "")])) + assert ListType([NamedType("a", "")]) != ListType([NamedType("b", "")]) + assert hash(ListType([NamedType("a", "")])) != hash(ListType([NamedType("b", "")])) def test_dict_type() -> None: dict_type = DictType( - key_type=UnionType([NamedType("str"), NamedType("int")]), - value_type=UnionType([NamedType("str"), NamedType("int")]), + key_type=UnionType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]), + value_type=UnionType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]), ) dict_type_dict = { "kind": "DictType", "key_type": { "kind": "UnionType", "types": [ - {"kind": "NamedType", "name": "str", "qname": ""}, - {"kind": "NamedType", "name": "int", "qname": ""}, + {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, + {"kind": "NamedType", "name": "int", "qname": "builtins.int"}, ], }, "value_type": { "kind": "UnionType", "types": [ - {"kind": "NamedType", "name": "str", "qname": ""}, - {"kind": "NamedType", "name": "int", "qname": ""}, + {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, + {"kind": "NamedType", "name": "int", "qname": "builtins.int"}, ], }, } @@ -197,27 +204,30 @@ def test_dict_type() -> None: assert DictType.from_dict(dict_type_dict) == dict_type assert dict_type.to_dict() == dict_type_dict - assert DictType(NamedType("a"), NamedType("a")) == DictType(NamedType("a"), NamedType("a")) - assert hash(DictType(NamedType("a"), NamedType("a"))) == hash(DictType(NamedType("a"), NamedType("a"))) - assert DictType(NamedType("a"), NamedType("a")) != DictType(NamedType("b"), NamedType("a")) - assert hash(DictType(NamedType("a"), NamedType("a"))) != hash(DictType(NamedType("b"), NamedType("a"))) + assert DictType(NamedType("a", ""), NamedType("a", "")) == DictType(NamedType("a", ""), NamedType("a", "")) + assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) == hash(DictType(NamedType("a", ""), NamedType("a", ""))) + assert DictType(NamedType("a", ""), NamedType("a", "")) != DictType(NamedType("b", ""), NamedType("a", "")) + assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) != hash(DictType(NamedType("b", ""), NamedType("a", ""))) def test_set_type() -> None: - set_type = SetType([NamedType("str"), NamedType("int")]) + set_type = SetType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]) set_type_dict = { "kind": "SetType", - "types": [{"kind": "NamedType", "name": "str", "qname": ""}, {"kind": "NamedType", "name": "int", "qname": ""}], + "types": [ + {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, + {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + ], } assert AbstractType.from_dict(set_type_dict) == set_type assert SetType.from_dict(set_type_dict) == set_type assert set_type.to_dict() == set_type_dict - assert SetType([NamedType("a")]) == SetType([NamedType("a")]) - assert hash(SetType([NamedType("a")])) == hash(SetType([NamedType("a")])) - assert SetType([NamedType("a")]) != SetType([NamedType("b")]) - assert hash(SetType([NamedType("a")])) != hash(SetType([NamedType("b")])) + assert SetType([NamedType("a", "")]) == SetType([NamedType("a", "")]) + assert hash(SetType([NamedType("a", "")])) == hash(SetType([NamedType("a", "")])) + assert SetType([NamedType("a", "")]) != SetType([NamedType("b", "")]) + assert hash(SetType([NamedType("a", "")])) != hash(SetType([NamedType("b", "")])) def test_literal_type() -> None: @@ -238,7 +248,7 @@ def test_literal_type() -> None: def test_final_type() -> None: - type_ = FinalType(NamedType("some_type")) + type_ = FinalType(NamedType("some_type", "")) type_dict = { "kind": "FinalType", "type": {"kind": "NamedType", "name": "some_type", "qname": ""}, @@ -248,27 +258,30 @@ def test_final_type() -> None: assert FinalType.from_dict(type_dict) == type_ assert type_.to_dict() == type_dict - assert FinalType(NamedType("a")) == FinalType(NamedType("a")) - assert hash(FinalType(NamedType("a"))) == hash(FinalType(NamedType("a"))) - assert FinalType(NamedType("a")) != FinalType(NamedType("b")) - assert hash(FinalType(NamedType("a"))) != hash(FinalType(NamedType("b"))) + assert FinalType(NamedType("a", "")) == FinalType(NamedType("a", "")) + assert hash(FinalType(NamedType("a", ""))) == hash(FinalType(NamedType("a", ""))) + assert FinalType(NamedType("a", "")) != FinalType(NamedType("b", "")) + assert hash(FinalType(NamedType("a", ""))) != hash(FinalType(NamedType("b", ""))) def test_tuple_type() -> None: - set_type = TupleType([NamedType("str"), NamedType("int")]) + set_type = TupleType([NamedType("str", "builtins.str"), NamedType("int", "builtins.int")]) set_type_dict = { "kind": "TupleType", - "types": [{"kind": "NamedType", "name": "str", "qname": ""}, {"kind": "NamedType", "name": "int", "qname": ""}], + "types": [ + {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, + {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + ], } assert AbstractType.from_dict(set_type_dict) == set_type assert TupleType.from_dict(set_type_dict) == set_type assert set_type.to_dict() == set_type_dict - assert TupleType([NamedType("a")]) == TupleType([NamedType("a")]) - assert hash(TupleType([NamedType("a")])) == hash(TupleType([NamedType("a")])) - assert TupleType([NamedType("a")]) != TupleType([NamedType("b")]) - assert hash(TupleType([NamedType("a")])) != hash(TupleType([NamedType("b")])) + assert TupleType([NamedType("a", "")]) == TupleType([NamedType("a", "")]) + assert hash(TupleType([NamedType("a", "")])) == hash(TupleType([NamedType("a", "")])) + assert TupleType([NamedType("a", "")]) != TupleType([NamedType("b", "")]) + assert hash(TupleType([NamedType("a", "")])) != hash(TupleType([NamedType("b", "")])) def test_abstract_type_from_dict_exception() -> None: diff --git a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py index 0a001ff0..182d304b 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts from safeds_stubgen.docstring_parsing import ( ClassDocstring, EpydocParser, @@ -20,10 +20,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / "docstring_parser_package" / "epydoc.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "epydoc.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py index 9db26201..c146fbc4 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py +++ b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py @@ -6,7 +6,7 @@ from mypy import nodes # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts # noinspection PyProtectedMember from safeds_stubgen.docstring_parsing._helpers import get_full_docstring @@ -15,10 +15,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / "docstring_parser_package" / "full_docstring.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "full_docstring.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py index e8d95a2a..8d29ec07 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -23,10 +23,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / "docstring_parser_package" / "googledoc.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "googledoc.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py index f517c668..a21c85d0 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -24,10 +24,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent _test_package_name = "docstring_parser_package" -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / _test_package_name / "numpydoc.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "numpydoc.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / _test_package_name), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py index 87a93c31..ec3e7aa9 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -22,10 +22,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / "docstring_parser_package" / "plaintext.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "plaintext.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py index 8f8fff51..43ccbefb 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -20,10 +20,11 @@ # Setup _test_dir = Path(__file__).parent.parent.parent -mypy_file = _get_mypy_ast( - files=[ - str(Path(_test_dir / "data" / "docstring_parser_package" / "restdoc.py")), - ], +files = [str(Path(_test_dir / "data" / "docstring_parser_package" / "restdoc.py"))] +mypy_build = _get_mypy_build(files) +mypy_file = _get_mypy_asts( + build_result=mypy_build, + files=files, package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index deb98613..4590e828 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -68,48 +68,42 @@ def test_file_creation() -> None: ) -# Todo Check snapshot def test_class_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("class_module", snapshot) -# Todo Check snapshot def test_class_attribute_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("attribute_module", snapshot) -# Todo Check snapshot def test_function_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("function_module", snapshot) -# Todo Check snapshot def test_enum_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("enum_module", snapshot) -# Todo Check snapshot -def test_import_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("import_module", snapshot) - - -# Todo Check snapshot def test_type_inference(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("infer_types_module", snapshot) -# Todo Check snapshot def test_variance_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("variance_module", snapshot) -# Todo Check snapshot def test_abstract_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("abstract_module", snapshot) -# Todo -def test_docstring_creation() -> None: ... +def test_alias_creation(snapshot: SnapshotAssertion) -> None: + file_data = "" + for file_name in {"aliasing_module_1", "aliasing_module_2"}: + stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") + with stubs_file.open("r") as f: + file_data += f.read() + + assert file_data == snapshot @pytest.mark.parametrize( From b2a0a7748620b40f45070ed4af9f9a75b591429c Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 19:21:52 +0100 Subject: [PATCH 13/49] Adjusted test snapshots --- .../aliasing/aliasing_module_1.py | 1 + .../__snapshots__/test__get_api.ambr | 80 +++++++++---------- .../__snapshots__/test_generate_stubs.ambr | 80 +++++++++++-------- .../stubs_generator/test_generate_stubs.py | 13 +-- 4 files changed, 96 insertions(+), 78 deletions(-) diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py index ec9a05e6..f142d0d4 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_1.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -1,6 +1,7 @@ from aliasing_module_2 import AliasingModule2ClassA as AliasModule2 +# Todo Frage Die Klasse _AliasingModuleClassA wird als Typ genutzt aber nicht generiert class _AliasingModuleClassA: ... 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 74fda279..b0863197 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -193,13 +193,13 @@ 'key_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), 'kind': 'DictType', 'value_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), }), @@ -286,7 +286,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), }), }), @@ -308,12 +308,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -337,12 +337,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -441,12 +441,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -469,12 +469,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -495,7 +495,7 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'UnionType', @@ -503,12 +503,12 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -813,7 +813,7 @@ dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), ]), }), @@ -865,12 +865,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -891,7 +891,7 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'UnionType', @@ -899,12 +899,12 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -943,7 +943,7 @@ dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), ]), }), @@ -3217,13 +3217,13 @@ 'key_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), 'kind': 'DictType', 'value_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), }), @@ -3244,12 +3244,12 @@ dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), ]), }), @@ -3271,17 +3271,17 @@ dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -3618,7 +3618,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), dict({ @@ -4384,12 +4384,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -4455,7 +4455,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), ]) @@ -4555,13 +4555,13 @@ 'key_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), 'kind': 'DictType', 'value_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), }), @@ -4634,13 +4634,13 @@ 'key_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), 'kind': 'DictType', 'value_type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), }), @@ -4661,12 +4661,12 @@ dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), ]), }), @@ -4688,12 +4688,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'bool', - 'qname': '', + 'qname': 'builtins.bool', }), ]), }), diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 89d32292..0e4556aa 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -4,9 +4,6 @@ @PythonModule("various_modules_package.abstract_module") package variousModulesPackage.abstractModule - from abc import ABC - from abc import abstractmethod - class AbstractModuleClass { @PythonName("abstract_property_method") attr abstractPropertyMethod: union @@ -38,14 +35,54 @@ ''' # --- +# name: test_alias_creation[aliasing_module_1] + ''' + @PythonModule("various_modules_package.aliasing.aliasing_module_1") + package variousModulesPackage.aliasing.aliasingModule1 + + from aliasing_module_2 import AliasingModule2ClassA + + class AliasingModuleClassB() + + class AliasingModuleClassC() sub _AliasingModuleClassA { + @PythonName("typed_alias_attr") + static attr typedAliasAttr: AliasingModuleClassB + @PythonName("infer_alias_attr") + static attr inferAliasAttr: () -> a: _AliasingModuleClassA + @PythonName("typed_alias_attr2") + static attr typedAliasAttr2: AliasingModule2ClassA + @PythonName("infer_alias_attr2") + static attr inferAliasAttr2: AliasingModule2ClassA + // TODO List type has to many type arguments. + @PythonName("alias_list") + static attr aliasList: List, AliasingModule2ClassA> + } + + ''' +# --- +# name: test_alias_creation[aliasing_module_2] + ''' + @PythonModule("various_modules_package.aliasing.aliasing_module_2") + package variousModulesPackage.aliasing.aliasingModule2 + + class AliasingModule2ClassA() + + class AliasingModuleClassB() + + class AliasingModuleClassC() { + @PythonName("typed_alias_attr") + static attr typedAliasAttr: AliasingModuleClassB + @PythonName("alias_list") + static attr aliasList: List> + } + + ''' +# --- # name: test_class_attribute_creation ''' @PythonModule("various_modules_package.attribute_module") package variousModulesPackage.attributeModule - from typing import Optional - from typing import Final - from typing import Literal from tests.data.main_package.another_path.another_module import AnotherClass class AttributesClassA() @@ -132,11 +169,10 @@ static attr multiAttr7: String @PythonName("multi_attr_8") static attr multiAttr8: String - // TODO Attribute has no type information. @PythonName("attr_type_from_outside_package") - static attr attrTypeFromOutsidePackage + static attr attrTypeFromOutsidePackage: AnotherClass @PythonName("attr_default_value_from_outside_package") - static attr attrDefaultValueFromOutsidePackage: () -> a: + static attr attrDefaultValueFromOutsidePackage: () -> a: AnotherClass @PythonName("init_attr") attr initAttr: Boolean @@ -197,8 +233,6 @@ @PythonModule("various_modules_package.enum_module") package variousModulesPackage.enumModule - from another_path.another_module import AnotherClass as _AcImportAlias - enum _ReexportedEmptyEnum enum EnumTest { @@ -230,10 +264,6 @@ @PythonModule("various_modules_package.function_module") package variousModulesPackage.functionModule - from typing import Callable - from typing import Optional - from typing import Literal - from typing import Any from tests.data.main_package.another_path.another_module import AnotherClass // TODO Result type information missing. @@ -469,14 +499,13 @@ @Pure @PythonName("param_from_outside_the_package") fun paramFromOutsideThePackage( - @PythonName("param_type") paramType, + @PythonName("param_type") paramType: AnotherClass, @PythonName("param_value") paramValue ) - // TODO Result type information missing. @Pure @PythonName("result_from_outside_the_package") - fun resultFromOutsideThePackage() + fun resultFromOutsideThePackage() -> result1: AnotherClass class FunctionModuleClassA() @@ -534,17 +563,6 @@ ''' # --- -# name: test_import_creation - ''' - @PythonModule("various_modules_package.import_module") - package variousModulesPackage.importModule - - import mypy as `static` - - from math import * - - ''' -# --- # name: test_type_inference ''' @PythonModule("various_modules_package.infer_types_module") @@ -613,10 +631,6 @@ @PythonModule("various_modules_package.variance_module") package variousModulesPackage.varianceModule - from typing import Generic - from typing import TypeVar - from typing import Literal - class A() class VarianceClassAll() where { diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 4590e828..4f89a48b 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -96,12 +96,15 @@ def test_abstract_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("abstract_module", snapshot) -def test_alias_creation(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "file_name", + ["aliasing_module_1", "aliasing_module_2"] +) +def test_alias_creation(file_name: str, snapshot: SnapshotAssertion) -> None: file_data = "" - for file_name in {"aliasing_module_1", "aliasing_module_2"}: - stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") - with stubs_file.open("r") as f: - file_data += f.read() + stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") + with stubs_file.open("r") as f: + file_data += f.read() assert file_data == snapshot From ffe353e643a248b407ac1efc3a9824af60f18dae Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 20:42:18 +0100 Subject: [PATCH 14/49] It is now possible to resolve aliases for superclasses; adjusted tests --- .../api_analyzer/_ast_visitor.py | 57 ++++++++++++------- .../various_modules_package/import_module.py | 17 +++--- .../__snapshots__/test_main.ambr | 2 +- .../__snapshots__/test__get_api.ambr | 21 ++++--- .../__snapshots__/test_generate_stubs.ambr | 27 +++++++++ .../stubs_generator/test_generate_stubs.py | 4 ++ 6 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 497e2aee..deb60a0d 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -171,8 +171,18 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: ) # superclasses - # Todo Aliasing: Werden noch nicht aufgelöst - superclasses = [superclass.fullname for superclass in node.base_type_exprs if hasattr(superclass, "fullname")] + superclasses = [] + for superclass in node.base_type_exprs: + if hasattr(superclass, "fullname"): + superclass_qname = superclass.fullname + superclass_name = superclass_qname.split(".")[-1] + + # Check if the superclass name is an alias and find the real name + if superclass_name in self.aliases.keys(): + _, superclass_alias_qname = self._find_alias(superclass_name) + superclass_qname = superclass_alias_qname if superclass_alias_qname else superclass_qname + + superclasses.append(superclass_qname) # Get reexported data reexported_by = self._get_reexported_by(name) @@ -880,16 +890,10 @@ def _find_alias(self, type_name: str): name = qname.split(".")[-1] # We have to check if this is an alias from an import - found_alias = False - for qualified_import in module.qualified_imports: - if qualified_import.alias == name: - qname = qualified_import.qualified_name - found_alias = True - break + import_name, import_qname = self._search_alias_in_qualified_imports(type_name) - if found_alias: - # Overwrite the name, since it was only an alias - name = qname.split(".")[-1] + name = import_name if import_name else name + qname = import_qname if import_qname else qname else: # In this case some type was defined in multiple modules with the same name. @@ -902,25 +906,40 @@ def _find_alias(self, type_name: str): break # Then we check if the type was perhapse imported - for qualified_import in module.qualified_imports: - if qualified_import.alias == name: - qname = qualified_import.qualified_name - name = qname.split(".")[-1] - break - elif qualified_import.qualified_name in alias_qname: - qname = alias_qname - break + qimport_name, qimport_qname = self._search_alias_in_qualified_imports(name, alias_qname) + if qimport_qname: + qname = qimport_qname + name = qimport_name if qimport_name else name + break + found_qname = False for wildcard_import in module.wildcard_imports: if wildcard_import.module_name in alias_qname: qname = alias_qname + found_qname = True break + if found_qname: + break + + else: + name, qname = self._search_alias_in_qualified_imports(type_name) if not qname: # pragma: no cover raise ValueError(f"It was not possible to find out where the alias {type_name} was defined.") return name, qname + def _search_alias_in_qualified_imports(self, alias_name: str, alias_qname: str = "") -> tuple[str, str]: + module = self.__declaration_stack[0] + for qualified_import in module.qualified_imports: + if qualified_import.alias == alias_name: + qname = qualified_import.qualified_name + name = qname.split(".")[-1] + return name, qname + elif alias_qname and qualified_import.qualified_name in alias_qname: + return "", alias_qname + return "", "" + def _create_module_id(self, qname: str) -> str: """Create an ID for the module object. diff --git a/tests/data/various_modules_package/import_module.py b/tests/data/various_modules_package/import_module.py index 5a34cd5d..0cf1da12 100644 --- a/tests/data/various_modules_package/import_module.py +++ b/tests/data/various_modules_package/import_module.py @@ -1,11 +1,12 @@ -# Keyword -from enum import IntEnum +from another_path.another_module import AnotherClass +from class_module import ClassModuleClassB +from class_module import ClassModuleClassC as ClMCC +from class_module import ClassModuleClassD as ClMCD +from class_module import ClassModuleEmptyClassA as ClMECA -# Alias -from enum import Enum as _Enum -# Keyword as alias -import mypy as static +class ImportClass(AnotherClass): + typed_import_attr: ClMCD + default_import_attr = ClMECA -# Wildcard -from math import * + def import_function(self, import_param: ClassModuleClassB) -> ClMCC: ... diff --git a/tests/safeds_stubgen/__snapshots__/test_main.ambr b/tests/safeds_stubgen/__snapshots__/test_main.ambr index 57dfbf97..048f8e28 100644 --- a/tests/safeds_stubgen/__snapshots__/test_main.ambr +++ b/tests/safeds_stubgen/__snapshots__/test_main.ambr @@ -180,7 +180,7 @@ 'reexported_by': list([ ]), 'superclasses': list([ - 'tests.data.main_package.main_module.AcDoubleAlias', + 'tests.data.main_package.another_path.another_module.AnotherClass', ]), 'type_parameters': list([ ]), 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 b0863197..c805a681 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -6068,23 +6068,28 @@ list([ dict({ 'alias': None, - 'qualified_name': 'enum.IntEnum', + 'qualified_name': 'another_path.another_module.AnotherClass', }), dict({ - 'alias': '_Enum', - 'qualified_name': 'enum.Enum', + 'alias': None, + 'qualified_name': 'class_module.ClassModuleClassB', }), dict({ - 'alias': 'static', - 'qualified_name': 'mypy', + 'alias': 'ClMCC', + 'qualified_name': 'class_module.ClassModuleClassC', + }), + dict({ + 'alias': 'ClMCD', + 'qualified_name': 'class_module.ClassModuleClassD', + }), + dict({ + 'alias': 'ClMECA', + 'qualified_name': 'class_module.ClassModuleEmptyClassA', }), ]) # --- # name: test_imports[import_module (wildcard_imports)] list([ - dict({ - 'module_name': 'math', - }), ]) # --- # name: test_modules[__init__] diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 0e4556aa..ff46ffab 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -563,6 +563,33 @@ ''' # --- +# name: test_import_creation + ''' + @PythonModule("various_modules_package.import_module") + package variousModulesPackage.importModule + + from class_module import ClassModuleClassB + from class_module import ClassModuleClassD + from class_module import ClassModuleEmptyClassA + from class_module import ClassModuleClassC + + class ImportClass() sub AnotherClass { + @PythonName("typed_import_attr") + static attr typedImportAttr: ClassModuleClassD + @PythonName("default_import_attr") + static attr defaultImportAttr: ClassModuleEmptyClassA + + // TODO Result type information missing. + // TODO Some parameter have no type information. + @Pure + @PythonName("import_function") + fun importFunction( + @PythonName("import_param") importParam: ClassModuleClassB + ) -> ClassModuleClassC + } + + ''' +# --- # name: test_type_inference ''' @PythonModule("various_modules_package.infer_types_module") diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 4f89a48b..c8d689a5 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -96,6 +96,10 @@ def test_abstract_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("abstract_module", snapshot) +def test_import_creation(snapshot: SnapshotAssertion) -> None: + assert_stubs_snapshot("import_module", snapshot) + + @pytest.mark.parametrize( "file_name", ["aliasing_module_1", "aliasing_module_2"] From 8d638128d4251fead1986be5aed691c3ccb7394b Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 20:58:54 +0100 Subject: [PATCH 15/49] Fixed a bug where types and imports would not be generated for param types and result types --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 12 +++++++----- .../__snapshots__/test_generate_stubs.ambr | 6 ++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index deb60a0d..f07b6ecc 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -392,9 +392,9 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul node_ret_type = node_type.ret_type if not isinstance(node_ret_type, mp_types.NoneType): - if isinstance(node_ret_type, mp_types.AnyType) and not has_correct_type_of_any( - node_ret_type.type_of_any, - ): + if (isinstance(node_ret_type, mp_types.AnyType) and + not has_correct_type_of_any(node_ret_type.type_of_any) and + not node_ret_type.type_of_any == mp_types.TypeOfAny.from_unimported_type): # In this case, the "Any" type was given because it was not explicitly annotated. # Therefor we have to try to infer the type. ret_type = self._infer_type_from_return_stmts(node) @@ -659,7 +659,9 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis # Get type information for parameter if mypy_type is None: # pragma: no cover raise ValueError("Argument has no type.") - elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any): + elif (isinstance(mypy_type, mp_types.AnyType) and + not has_correct_type_of_any(mypy_type.type_of_any) and + not mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type): # We try to infer the type through the default value later, if possible pass elif ( @@ -932,7 +934,7 @@ def _find_alias(self, type_name: str): def _search_alias_in_qualified_imports(self, alias_name: str, alias_qname: str = "") -> tuple[str, str]: module = self.__declaration_stack[0] for qualified_import in module.qualified_imports: - if qualified_import.alias == alias_name: + if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: qname = qualified_import.qualified_name name = qname.split(".")[-1] return name, qname diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index ff46ffab..6d23c499 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -568,9 +568,9 @@ @PythonModule("various_modules_package.import_module") package variousModulesPackage.importModule - from class_module import ClassModuleClassB from class_module import ClassModuleClassD from class_module import ClassModuleEmptyClassA + from class_module import ClassModuleClassB from class_module import ClassModuleClassC class ImportClass() sub AnotherClass { @@ -579,13 +579,11 @@ @PythonName("default_import_attr") static attr defaultImportAttr: ClassModuleEmptyClassA - // TODO Result type information missing. - // TODO Some parameter have no type information. @Pure @PythonName("import_function") fun importFunction( @PythonName("import_param") importParam: ClassModuleClassB - ) -> ClassModuleClassC + ) -> result1: ClassModuleClassC } ''' From f12624e7d9fdd891c826f3737d23e170628592fd Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 21:20:34 +0100 Subject: [PATCH 16/49] Added imports for subclasses; Refactoring --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 11 ++++------- src/safeds_stubgen/api_analyzer/_mypy_helpers.py | 1 + src/safeds_stubgen/stubs_generator/_generate_stubs.py | 9 ++++++++- tests/safeds_stubgen/__snapshots__/test_main.ambr | 2 +- .../__snapshots__/test_generate_stubs.ambr | 5 +++-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index f07b6ecc..07e78448 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -55,6 +55,7 @@ def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: self.mypy_file = node is_package = node.path.endswith("__init__.py") + # Todo Frage: Alte Importfunktionalität behalten? Wird nicht benutzt qualified_imports: list[QualifiedImport] = [] wildcard_imports: list[WildcardImport] = [] docstring = "" @@ -393,8 +394,7 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul if not isinstance(node_ret_type, mp_types.NoneType): if (isinstance(node_ret_type, mp_types.AnyType) and - not has_correct_type_of_any(node_ret_type.type_of_any) and - not node_ret_type.type_of_any == mp_types.TypeOfAny.from_unimported_type): + not has_correct_type_of_any(node_ret_type.type_of_any)): # In this case, the "Any" type was given because it was not explicitly annotated. # Therefor we have to try to infer the type. ret_type = self._infer_type_from_return_stmts(node) @@ -619,8 +619,7 @@ def _create_attribute( # Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase if attribute_type is not None and not ( isinstance(attribute_type, mp_types.AnyType) and - not has_correct_type_of_any(attribute_type.type_of_any) and - attribute_type.type_of_any != mp_types.TypeOfAny.from_unimported_type + not has_correct_type_of_any(attribute_type.type_of_any) ): # noinspection PyTypeChecker type_ = self.mypy_type_to_abstract_type(attribute_type, unanalyzed_type) @@ -659,9 +658,7 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis # Get type information for parameter if mypy_type is None: # pragma: no cover raise ValueError("Argument has no type.") - elif (isinstance(mypy_type, mp_types.AnyType) and - not has_correct_type_of_any(mypy_type.type_of_any) and - not mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type): + elif isinstance(mypy_type, mp_types.AnyType) and not has_correct_type_of_any(mypy_type.type_of_any): # We try to infer the type through the default value later, if possible pass elif ( diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 4b104e15..d07056aa 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -83,6 +83,7 @@ def has_correct_type_of_any(type_of_any: int) -> bool: mp_types.TypeOfAny.explicit, mp_types.TypeOfAny.from_omitted_generics, mp_types.TypeOfAny.from_another_any, + mp_types.TypeOfAny.from_unimported_type, } diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index c1d0db4b..e10973de 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -131,6 +131,9 @@ def _create_imports_string(self) -> str: name = import_parts[-1] import_strings.append(f"from {from_} import {name}") + # We have to sort for the snapshot tests + import_strings.sort() + import_string = "\n".join(import_strings) return f"\n{import_string}\n" @@ -158,7 +161,11 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st superclasses = class_.superclasses superclass_info = "" if superclasses and not class_.is_abstract: - superclass_names = [self._split_import_id(superclass)[1] for superclass in superclasses] + superclass_names = [] + for superclass in superclasses: + superclass_names.append(self._split_import_id(superclass)[1]) + self._add_to_imports(superclass) + superclass_info = f" sub {', '.join(superclass_names)}" if len(superclasses) > 1: diff --git a/tests/safeds_stubgen/__snapshots__/test_main.ambr b/tests/safeds_stubgen/__snapshots__/test_main.ambr index 048f8e28..babbc78e 100644 --- a/tests/safeds_stubgen/__snapshots__/test_main.ambr +++ b/tests/safeds_stubgen/__snapshots__/test_main.ambr @@ -204,7 +204,7 @@ 'reexported_by': list([ ]), 'superclasses': list([ - 'tests.data.main_package.another_path.another_module.AnotherClass', + 'another_path.another_module.AnotherClass', ]), 'type_parameters': list([ ]), diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 6d23c499..82c133e2 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -568,10 +568,11 @@ @PythonModule("various_modules_package.import_module") package variousModulesPackage.importModule - from class_module import ClassModuleClassD - from class_module import ClassModuleEmptyClassA + from another_path.another_module import AnotherClass from class_module import ClassModuleClassB from class_module import ClassModuleClassC + from class_module import ClassModuleClassD + from class_module import ClassModuleEmptyClassA class ImportClass() sub AnotherClass { @PythonName("typed_import_attr") From 1b02852862140eef81f183fe858baf7efecd0bee Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 21:27:49 +0100 Subject: [PATCH 17/49] Removed unimportant todo markings for now --- src/safeds_stubgen/api_analyzer/_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index 1dd3b6e7..7fc36529 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -437,14 +437,14 @@ def __hash__(self) -> int: # raise TypeParsingError("") # # -# # Todo Return mypy\types -> Type class +# # T0do Return mypy\types -> Type class # def create_type(type_string: str, description: str) -> AbstractType: # if not type_string: # return NamedType("None", "builtins.None") # # type_string = type_string.replace(" ", "") # -# # todo Replace pipes with Union +# # t0do Replace pipes with Union # # if "|" in type_string: # # type_string = _replace_pipes_with_union(type_string) # @@ -502,7 +502,7 @@ def __hash__(self) -> int: # return NamedType(name=type_string, qname=) # # -# # todo übernehmen in create_type -> Tests schlagen nun fehl +# # t0do übernehmen in create_type -> Tests schlagen nun fehl # def _create_enum_boundry_type(type_string: str, description: str) -> AbstractType | None: # types: list[AbstractType] = [] # From 37a92f978ebe3d3988dcad1f93e9b4de232d2637 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 16 Dec 2023 21:48:57 +0100 Subject: [PATCH 18/49] linter fixes --- .../api_analyzer/_ast_visitor.py | 26 +++++++++++++------ src/safeds_stubgen/api_analyzer/_get_api.py | 10 +++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 07e78448..f5404a7f 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -49,7 +49,7 @@ def __init__(self, docstring_parser: AbstractDocstringParser, api: API, aliases: self.api: API = api self.__declaration_stack: list[Module | Class | Function | Enum | list[Attribute | EnumInstance]] = [] self.aliases = aliases - self.mypy_file = None + self.mypy_file: mp_nodes.MypyFile | None = None def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: self.mypy_file = node @@ -783,7 +783,7 @@ def _add_reexports(self, module: Module) -> None: # #### Misc. utilities def mypy_type_to_abstract_type( self, - mypy_type: mp_types.Instance | mp_types.ProperType | mp_types.MypyType, + mypy_type: mp_types.Instance | mp_types.ProperType | mp_types.Type, unanalyzed_type: mp_types.Type | None = None, ) -> AbstractType: @@ -824,7 +824,9 @@ def mypy_type_to_abstract_type( ) elif isinstance(mypy_type, mp_types.AnyType): if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type: - missing_import_name = mypy_type.missing_import_name.split(".")[-1] + # If the Any type is generated b/c of from_unimported_type, then we can parse the type + # from the import information + missing_import_name = mypy_type.missing_import_name.split(".")[-1] # type: ignore name, qname = self._find_alias(missing_import_name) return sds_types.NamedType(name=name, qname=qname) else: @@ -880,6 +882,10 @@ def mypy_type_to_abstract_type( def _find_alias(self, type_name: str): module = self.__declaration_stack[0] + # At this point, the first item of the stack can only ever be a module + if not isinstance(module, Module): # pragma: no cover + raise TypeError(f"Expected module, got {type(module)}.") + name = "" qname = "" if type_name in self.aliases.keys(): @@ -889,7 +895,7 @@ def _find_alias(self, type_name: str): name = qname.split(".")[-1] # We have to check if this is an alias from an import - import_name, import_qname = self._search_alias_in_qualified_imports(type_name) + import_name, import_qname = self._search_alias_in_qualified_imports(module, type_name) name = import_name if import_name else name qname = import_qname if import_qname else qname @@ -900,12 +906,16 @@ def _find_alias(self, type_name: str): # First we check if the type was defined in the same module type_path = ".".join(alias_qname.split(".")[0:-1]) name = alias_qname.split(".")[-1] + + if self.mypy_file is None: # pragma: no cover + raise TypeError("Expected mypy_file (module information), got None.") + if type_path == self.mypy_file.fullname: qname = alias_qname break # Then we check if the type was perhapse imported - qimport_name, qimport_qname = self._search_alias_in_qualified_imports(name, alias_qname) + qimport_name, qimport_qname = self._search_alias_in_qualified_imports(module, name, alias_qname) if qimport_qname: qname = qimport_qname name = qimport_name if qimport_name else name @@ -921,15 +931,15 @@ def _find_alias(self, type_name: str): break else: - name, qname = self._search_alias_in_qualified_imports(type_name) + name, qname = self._search_alias_in_qualified_imports(module, type_name) if not qname: # pragma: no cover raise ValueError(f"It was not possible to find out where the alias {type_name} was defined.") return name, qname - def _search_alias_in_qualified_imports(self, alias_name: str, alias_qname: str = "") -> tuple[str, str]: - module = self.__declaration_stack[0] + @staticmethod + def _search_alias_in_qualified_imports(module: Module, alias_name: str, alias_qname: str = "") -> tuple[str, str]: for qualified_import in module.qualified_imports: if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: qname = qualified_import.qualified_name diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index 651c60e9..e251d216 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -134,7 +134,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: if not result_types: return {} - aliases = {} + aliases: dict[str, set[str]] = {} for key in result_types.keys(): if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr): if isinstance(key, mypy_nodes.NameExpr): @@ -153,8 +153,8 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: fullname = key.node.target.type.fullname elif isinstance(type_value, mypy_types.CallableType): bound_args = type_value.bound_args - if bound_args and not isinstance(bound_args[0], mypy_types.TupleType): - fullname = bound_args[0].type.fullname + if bound_args and hasattr(bound_args[0], "type"): + fullname = bound_args[0].type.fullname # type: ignore elif hasattr(key, "node") and isinstance(key.node, mypy_nodes.Var): fullname = key.node.fullname @@ -173,8 +173,8 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: continue if in_package: - if isinstance(type_value, mypy_types.CallableType): - fullname = type_value.bound_args[0].type.fullname + if isinstance(type_value, mypy_types.CallableType) and hasattr(type_value.bound_args[0], "type"): + fullname = type_value.bound_args[0].type.fullname # type: ignore elif isinstance(type_value, mypy_types.Instance): fullname = type_value.type.fullname elif isinstance(key, mypy_nodes.TypeVarExpr): From 9c20382b3dfa10178ca452bc36e34fdf51c1f29f Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 17 Dec 2023 14:29:12 +0100 Subject: [PATCH 19/49] linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 6 +++--- src/safeds_stubgen/api_analyzer/_get_api.py | 10 +++++----- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index f5404a7f..0f12233f 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -179,7 +179,7 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: superclass_name = superclass_qname.split(".")[-1] # Check if the superclass name is an alias and find the real name - if superclass_name in self.aliases.keys(): + if superclass_name in self.aliases: _, superclass_alias_qname = self._find_alias(superclass_name) superclass_qname = superclass_alias_qname if superclass_alias_qname else superclass_qname @@ -826,7 +826,7 @@ def mypy_type_to_abstract_type( if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type: # If the Any type is generated b/c of from_unimported_type, then we can parse the type # from the import information - missing_import_name = mypy_type.missing_import_name.split(".")[-1] # type: ignore + missing_import_name = mypy_type.missing_import_name.split(".")[-1] name, qname = self._find_alias(missing_import_name) return sds_types.NamedType(name=name, qname=qname) else: @@ -888,7 +888,7 @@ def _find_alias(self, type_name: str): name = "" qname = "" - if type_name in self.aliases.keys(): + if type_name in self.aliases: qnames: set = self.aliases[type_name] if len(qnames) == 1: qname = deepcopy(qnames).pop() diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index e251d216..a3519b7b 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -5,6 +5,8 @@ import mypy.build as mypy_build import mypy.main as mypy_main +from mypy import nodes as mypy_nodes +from mypy import types as mypy_types from safeds_stubgen.docstring_parsing import DocstringStyle, create_docstring_parser @@ -12,8 +14,6 @@ from ._ast_visitor import MyPyAstVisitor from ._ast_walker import ASTWalker from ._package_metadata import distribution, distribution_version, package_root -from mypy import nodes as mypy_nodes -from mypy import types as mypy_types def get_api( @@ -135,7 +135,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: return {} aliases: dict[str, set[str]] = {} - for key in result_types.keys(): + for key in result_types: if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr): if isinstance(key, mypy_nodes.NameExpr): type_value = result_types[key] @@ -154,7 +154,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: elif isinstance(type_value, mypy_types.CallableType): bound_args = type_value.bound_args if bound_args and hasattr(bound_args[0], "type"): - fullname = bound_args[0].type.fullname # type: ignore + fullname = bound_args[0].type.fullname elif hasattr(key, "node") and isinstance(key.node, mypy_nodes.Var): fullname = key.node.fullname @@ -174,7 +174,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: if in_package: if isinstance(type_value, mypy_types.CallableType) and hasattr(type_value.bound_args[0], "type"): - fullname = type_value.bound_args[0].type.fullname # type: ignore + fullname = type_value.bound_args[0].type.fullname elif isinstance(type_value, mypy_types.Instance): fullname = type_value.type.fullname elif isinstance(key, mypy_nodes.TypeVarExpr): diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index e10973de..b80b0bb0 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -631,7 +631,7 @@ def _create_type_string(self, type_data: dict | None) -> str: def _add_to_imports(self, qname: str) -> None: """Check if the qname of a type is defined in the current module. If not, we create an import for it.""" - if qname == "": + if qname == "": # pragma: no cover raise ValueError("Type has no import source.") qname_parts = qname.split(".") From b92232b4281268ce1fa408a287c4a46488f29998 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 17 Dec 2023 14:34:55 +0100 Subject: [PATCH 20/49] linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 +- src/safeds_stubgen/api_analyzer/_get_api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 0f12233f..dbc5d514 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -826,7 +826,7 @@ def mypy_type_to_abstract_type( if mypy_type.type_of_any == mp_types.TypeOfAny.from_unimported_type: # If the Any type is generated b/c of from_unimported_type, then we can parse the type # from the import information - missing_import_name = mypy_type.missing_import_name.split(".")[-1] + missing_import_name = mypy_type.missing_import_name.split(".")[-1] # type: ignore[union-attr] name, qname = self._find_alias(missing_import_name) return sds_types.NamedType(name=name, qname=qname) else: diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index a3519b7b..a0a8b99e 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -154,7 +154,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: elif isinstance(type_value, mypy_types.CallableType): bound_args = type_value.bound_args if bound_args and hasattr(bound_args[0], "type"): - fullname = bound_args[0].type.fullname + fullname = bound_args[0].type.fullname # type: ignore[union-attr] elif hasattr(key, "node") and isinstance(key.node, mypy_nodes.Var): fullname = key.node.fullname @@ -174,7 +174,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: if in_package: if isinstance(type_value, mypy_types.CallableType) and hasattr(type_value.bound_args[0], "type"): - fullname = type_value.bound_args[0].type.fullname + fullname = type_value.bound_args[0].type.fullname # type: ignore[union-attr] elif isinstance(type_value, mypy_types.Instance): fullname = type_value.type.fullname elif isinstance(key, mypy_nodes.TypeVarExpr): From ee2079e6fe4ce1ea8ee1246097b47d27ebe40630 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 17 Dec 2023 14:39:30 +0100 Subject: [PATCH 21/49] linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index dbc5d514..e35264b8 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -879,7 +879,7 @@ def mypy_type_to_abstract_type( return sds_types.NamedType(name=type_name, qname=mypy_type.type.fullname) raise ValueError("Unexpected type.") # pragma: no cover - def _find_alias(self, type_name: str): + def _find_alias(self, type_name: str) -> tuple[str, str]: module = self.__declaration_stack[0] # At this point, the first item of the stack can only ever be a module From 78c4973cff30b2d31802ac806d45a4b9ebad992f Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 17 Dec 2023 13:41:09 +0000 Subject: [PATCH 22/49] style: apply automated linter fixes --- .../api_analyzer/_ast_visitor.py | 15 ++++---- src/safeds_stubgen/api_analyzer/_get_api.py | 8 +++-- .../safeds_stubgen/api_analyzer/test_types.py | 34 ++++++++++++------- .../docstring_parsing/test_epydoc_parser.py | 2 +- .../test_get_full_docstring.py | 2 +- .../test_googledoc_parser.py | 2 +- .../docstring_parsing/test_numpydoc_parser.py | 2 +- .../test_plaintext_docstring_parser.py | 2 +- .../docstring_parsing/test_restdoc_parser.py | 2 +- .../stubs_generator/test_generate_stubs.py | 5 +-- 10 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index e35264b8..e37a1ece 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,11 +1,11 @@ from __future__ import annotations -from copy import deepcopy +from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING -import mypy.types as mp_types import mypy.nodes as mp_nodes +import mypy.types as mp_types import safeds_stubgen.api_analyzer._types as sds_types @@ -37,9 +37,8 @@ ) if TYPE_CHECKING: - from safeds_stubgen.docstring_parsing import AbstractDocstringParser, ResultDocstring - from safeds_stubgen.api_analyzer._types import AbstractType + from safeds_stubgen.docstring_parsing import AbstractDocstringParser, ResultDocstring class MyPyAstVisitor: @@ -393,8 +392,9 @@ def _parse_results(self, node: mp_nodes.FuncDef, function_id: str) -> list[Resul node_ret_type = node_type.ret_type if not isinstance(node_ret_type, mp_types.NoneType): - if (isinstance(node_ret_type, mp_types.AnyType) and - not has_correct_type_of_any(node_ret_type.type_of_any)): + if isinstance(node_ret_type, mp_types.AnyType) and not has_correct_type_of_any( + node_ret_type.type_of_any, + ): # In this case, the "Any" type was given because it was not explicitly annotated. # Therefor we have to try to infer the type. ret_type = self._infer_type_from_return_stmts(node) @@ -618,8 +618,7 @@ def _create_attribute( type_ = None # Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase if attribute_type is not None and not ( - isinstance(attribute_type, mp_types.AnyType) and - not has_correct_type_of_any(attribute_type.type_of_any) + isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) ): # noinspection PyTypeChecker type_ = self.mypy_type_to_abstract_type(attribute_type, unanalyzed_type) diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index a0a8b99e..1ac9739a 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -147,9 +147,11 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: name = key.name fullname = "" - if (hasattr(key, "node") and - isinstance(key.node, mypy_nodes.TypeAlias) and - isinstance(key.node.target, mypy_types.Instance)): + if ( + hasattr(key, "node") + and isinstance(key.node, mypy_nodes.TypeAlias) + and isinstance(key.node.target, mypy_types.Instance) + ): fullname = key.node.target.type.fullname elif isinstance(type_value, mypy_types.CallableType): bound_args = type_value.bound_args diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index cea899d2..da790457 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -147,14 +147,18 @@ def test_callable_type() -> None: assert CallableType.from_dict(callable_type_dict) == callable_type assert callable_type.to_dict() == callable_type_dict - assert (CallableType([NamedType("a", "")], NamedType("a", "")) == - CallableType([NamedType("a", "")], NamedType("a", ""))) - assert (hash(CallableType([NamedType("a", "")], NamedType("a", ""))) == - hash(CallableType([NamedType("a", "")], NamedType("a", "")))) - assert (CallableType([NamedType("a", "")], NamedType("a", "")) != - CallableType([NamedType("b", "")], NamedType("a", ""))) - assert (hash(CallableType([NamedType("a", "")], NamedType("a", ""))) != - hash(CallableType([NamedType("b", "")], NamedType("a", "")))) + assert CallableType([NamedType("a", "")], NamedType("a", "")) == CallableType( + [NamedType("a", "")], NamedType("a", ""), + ) + assert hash(CallableType([NamedType("a", "")], NamedType("a", ""))) == hash( + CallableType([NamedType("a", "")], NamedType("a", "")), + ) + assert CallableType([NamedType("a", "")], NamedType("a", "")) != CallableType( + [NamedType("b", "")], NamedType("a", ""), + ) + assert hash(CallableType([NamedType("a", "")], NamedType("a", ""))) != hash( + CallableType([NamedType("b", "")], NamedType("a", "")), + ) def test_list_type() -> None: @@ -163,7 +167,7 @@ def test_list_type() -> None: "kind": "ListType", "types": [ {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, - {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + {"kind": "NamedType", "name": "int", "qname": "builtins.int"}, ], } @@ -205,9 +209,13 @@ def test_dict_type() -> None: assert dict_type.to_dict() == dict_type_dict assert DictType(NamedType("a", ""), NamedType("a", "")) == DictType(NamedType("a", ""), NamedType("a", "")) - assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) == hash(DictType(NamedType("a", ""), NamedType("a", ""))) + assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) == hash( + DictType(NamedType("a", ""), NamedType("a", "")), + ) assert DictType(NamedType("a", ""), NamedType("a", "")) != DictType(NamedType("b", ""), NamedType("a", "")) - assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) != hash(DictType(NamedType("b", ""), NamedType("a", ""))) + assert hash(DictType(NamedType("a", ""), NamedType("a", ""))) != hash( + DictType(NamedType("b", ""), NamedType("a", "")), + ) def test_set_type() -> None: @@ -216,7 +224,7 @@ def test_set_type() -> None: "kind": "SetType", "types": [ {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, - {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + {"kind": "NamedType", "name": "int", "qname": "builtins.int"}, ], } @@ -270,7 +278,7 @@ def test_tuple_type() -> None: "kind": "TupleType", "types": [ {"kind": "NamedType", "name": "str", "qname": "builtins.str"}, - {"kind": "NamedType", "name": "int", "qname": "builtins.int"} + {"kind": "NamedType", "name": "int", "qname": "builtins.int"}, ], } diff --git a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py index 182d304b..89801409 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, EpydocParser, diff --git a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py index c146fbc4..3e2425bc 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py +++ b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py @@ -6,7 +6,7 @@ from mypy import nodes # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build # noinspection PyProtectedMember from safeds_stubgen.docstring_parsing._helpers import get_full_docstring diff --git a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py index 8d29ec07..f91ae12d 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, diff --git a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py index a21c85d0..d01ade45 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, diff --git a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py index ec3e7aa9..3902cace 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, diff --git a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py index 43ccbefb..6839001d 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py @@ -7,7 +7,7 @@ from safeds_stubgen.api_analyzer import Class, ParameterAssignment, get_classdef_definitions # noinspection PyProtectedMember -from safeds_stubgen.api_analyzer._get_api import _get_mypy_build, _get_mypy_asts +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index c8d689a5..455a969b 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -100,10 +100,7 @@ def test_import_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("import_module", snapshot) -@pytest.mark.parametrize( - "file_name", - ["aliasing_module_1", "aliasing_module_2"] -) +@pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2"]) def test_alias_creation(file_name: str, snapshot: SnapshotAssertion) -> None: file_data = "" stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") From 45d8666f9eb4d105c548506b2ab8116fe51d6ba4 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Sun, 17 Dec 2023 13:42:41 +0000 Subject: [PATCH 23/49] style: apply automated linter fixes --- tests/safeds_stubgen/api_analyzer/test_types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index da790457..d02b4fab 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -148,13 +148,15 @@ def test_callable_type() -> None: assert callable_type.to_dict() == callable_type_dict assert CallableType([NamedType("a", "")], NamedType("a", "")) == CallableType( - [NamedType("a", "")], NamedType("a", ""), + [NamedType("a", "")], + NamedType("a", ""), ) assert hash(CallableType([NamedType("a", "")], NamedType("a", ""))) == hash( CallableType([NamedType("a", "")], NamedType("a", "")), ) assert CallableType([NamedType("a", "")], NamedType("a", "")) != CallableType( - [NamedType("b", "")], NamedType("a", ""), + [NamedType("b", "")], + NamedType("a", ""), ) assert hash(CallableType([NamedType("a", "")], NamedType("a", ""))) != hash( CallableType([NamedType("b", "")], NamedType("a", "")), From fae277c79f21361b9cc1435d4cec1ca34619706f Mon Sep 17 00:00:00 2001 From: Arsam Date: Sun, 17 Dec 2023 14:55:31 +0100 Subject: [PATCH 24/49] Refactoring for Codecov --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 15 ++++----------- src/safeds_stubgen/api_analyzer/_get_api.py | 5 +++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index e37a1ece..5982fa27 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import defaultdict from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING @@ -44,7 +45,7 @@ class MyPyAstVisitor: def __init__(self, docstring_parser: AbstractDocstringParser, api: API, aliases: dict[str, set[str]]) -> None: self.docstring_parser: AbstractDocstringParser = docstring_parser - self.reexported: dict[str, list[Module]] = {} + self.reexported: dict[str, set[Module]] = defaultdict(set) self.api: API = api self.__declaration_stack: list[Module | Class | Function | Enum | list[Attribute | EnumInstance]] = [] self.aliases = aliases @@ -765,19 +766,11 @@ def _get_reexported_by(self, name: str) -> list[Module]: def _add_reexports(self, module: Module) -> None: for qualified_import in module.qualified_imports: name = qualified_import.qualified_name - if name in self.reexported: - if module not in self.reexported[name]: - self.reexported[name].append(module) - else: - self.reexported[name] = [module] + self.reexported[name].add(module) for wildcard_import in module.wildcard_imports: name = wildcard_import.module_name - if name in self.reexported: - if module not in self.reexported[name]: - self.reexported[name].append(module) - else: - self.reexported[name] = [module] + self.reexported[name].add(module) # #### Misc. utilities def mypy_type_to_abstract_type( diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index 1ac9739a..2099d6b8 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from collections import defaultdict from pathlib import Path import mypy.build as mypy_build @@ -134,7 +135,7 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: if not result_types: return {} - aliases: dict[str, set[str]] = {} + aliases: dict[str, set[str]] = defaultdict(set) for key in result_types: if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr): if isinstance(key, mypy_nodes.NameExpr): @@ -186,6 +187,6 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: else: # pragma: no cover raise TypeError("Received unexpected type while searching for aliases.") - aliases.setdefault(name, set()).add(fullname) + aliases[name].add(fullname) return aliases From aefc3d482ccb83c80d1cd228eab2c807513cbd50 Mon Sep 17 00:00:00 2001 From: Arsam Date: Mon, 18 Dec 2023 22:04:29 +0100 Subject: [PATCH 25/49] Adding more test data for Codecov (not working yet) --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 +- .../various_modules_package/aliasing/aliasing_module_1.py | 3 ++- .../various_modules_package/aliasing/aliasing_module_2.py | 6 +++++- .../various_modules_package/aliasing/aliasing_module_3.py | 7 +++++++ .../safeds_stubgen/stubs_generator/test_generate_stubs.py | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 tests/data/various_modules_package/aliasing/aliasing_module_3.py diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 5982fa27..41d91c68 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -893,7 +893,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: qname = import_qname if import_qname else qname else: - # In this case some type was defined in multiple modules with the same name. + # In this case some types where defined in multiple modules with the same names. for alias_qname in qnames: # First we check if the type was defined in the same module type_path = ".".join(alias_qname.split(".")[0:-1]) diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py index f142d0d4..47a36c7e 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_1.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -1,4 +1,5 @@ from aliasing_module_2 import AliasingModule2ClassA as AliasModule2 +from aliasing_module_3 import ImportMeAliasingModuleClass as ImportMeAlias # Todo Frage Die Klasse _AliasingModuleClassA wird als Typ genutzt aber nicht generiert @@ -21,4 +22,4 @@ class AliasingModuleClassC(_some_alias_a): typed_alias_attr2: AliasModule2 infer_alias_attr2 = AliasModule2 - alias_list: list[_some_alias_a | some_alias_b, AliasModule2] + alias_list: list[_some_alias_a | some_alias_b, AliasModule2, ImportMeAlias] diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_2.py b/tests/data/various_modules_package/aliasing/aliasing_module_2.py index e93d3918..e873fe43 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_2.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_2.py @@ -6,10 +6,14 @@ class AliasingModuleClassB: ... +class ImportMeAliasingModuleClass: + ... + + some_alias_b = AliasingModuleClassB class AliasingModuleClassC: typed_alias_attr: some_alias_b - alias_list: list[str | some_alias_b] + alias_list: list[str | some_alias_b, ImportMeAliasingModuleClass] diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_3.py b/tests/data/various_modules_package/aliasing/aliasing_module_3.py new file mode 100644 index 00000000..ffd92607 --- /dev/null +++ b/tests/data/various_modules_package/aliasing/aliasing_module_3.py @@ -0,0 +1,7 @@ +from aliasing_module_2 import ImportMeAliasingModuleClass as ImportMeAlias + + +class ImportMeAliasingModuleClass: + import_alias_attr: ImportMeAlias = ImportMeAlias() + + alias_list: list[ImportMeAlias, str] diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 455a969b..cb6805f9 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -100,7 +100,7 @@ def test_import_creation(snapshot: SnapshotAssertion) -> None: assert_stubs_snapshot("import_module", snapshot) -@pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2"]) +@pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2", "aliasing_module_3"]) def test_alias_creation(file_name: str, snapshot: SnapshotAssertion) -> None: file_data = "" stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") From 905ab1e4d7cc4bfa80bc68a15ab156f130941891 Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Thu, 28 Dec 2023 10:35:39 +0100 Subject: [PATCH 26/49] Adjusted todos --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 1 - .../aliasing/aliasing_module_1.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 41d91c68..e5732483 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -55,7 +55,6 @@ def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: self.mypy_file = node is_package = node.path.endswith("__init__.py") - # Todo Frage: Alte Importfunktionalität behalten? Wird nicht benutzt qualified_imports: list[QualifiedImport] = [] wildcard_imports: list[WildcardImport] = [] docstring = "" diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py index 47a36c7e..c7805822 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_1.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -2,7 +2,9 @@ from aliasing_module_3 import ImportMeAliasingModuleClass as ImportMeAlias -# Todo Frage Die Klasse _AliasingModuleClassA wird als Typ genutzt aber nicht generiert +# Todo Erstelle ein Todo für Klassen die privat sind jedoch intern als Typen benutzt werden. Das Todo kommt über die +# Stellen an denen diese Klasse als Typen genutzt wird. +# // TODO An internal class must not be used as a type in a public class. class _AliasingModuleClassA: ... @@ -15,9 +17,14 @@ class AliasingModuleClassB: some_alias_b = AliasingModuleClassB +# Todo Create an issue: +# Wir erben von einer privaten internen Klasse, welche normalerweise nicht generiert werden würde, aber in diesem Fall +# müssten wir Stubs generieren. Um das zu bewerkstelligen müssten wir die Informationen in den API Daten in so fern +# erweitern, dass Superklassen Informationen auch in die andere Richtung zeigen, d.h. wir müssen die Unterklassen für +# alle Klassen erhausfinden. class AliasingModuleClassC(_some_alias_a): typed_alias_attr: some_alias_b - infer_alias_attr = _some_alias_a + infer_alias_attr = _some_alias_a() typed_alias_attr2: AliasModule2 infer_alias_attr2 = AliasModule2 From 90c9efdaf95f47a1f7ce8f060ec0e641e4c3f934 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:39:16 +0000 Subject: [PATCH 27/49] style: apply automated linter fixes --- package-lock.json | 20 +++++++++---------- package.json | 2 +- .../api_analyzer/_ast_visitor.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fbb12ea..c4ccb3c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "safe-ds-stub-generator", "version": "0.0.1", "devDependencies": { - "@lars-reimann/prettier-config": "^5.0.0", + "@lars-reimann/prettier-config": "^5.2.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -63,12 +63,12 @@ } }, "node_modules/@lars-reimann/prettier-config": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@lars-reimann/prettier-config/-/prettier-config-5.0.0.tgz", - "integrity": "sha512-52Ha8xMKpQESiaEzceWgyQb+fuPVD3wl2p6Op1mpLyLj6natjq7Vy8lAmbWS3AbPRjPlJZZHnp/b+sOAOdNqbA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@lars-reimann/prettier-config/-/prettier-config-5.2.1.tgz", + "integrity": "sha512-mZv2ZmWHDoibDb07k+DOFHlcAKNygaWJsvlTPaWW4jPuo/vjULnlk6kNxGnEoFXz9Lk7rIjOUHd/Nk5csWKPEQ==", "dev": true, "peerDependencies": { - "prettier": ">= 2" + "prettier": "^3.2.5" } }, "node_modules/@nodelib/fs.scandir": { @@ -6053,16 +6053,16 @@ } }, "node_modules/prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "peer": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" diff --git a/package.json b/package.json index 4a791496..1614ad03 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "prettier": "@lars-reimann/prettier-config", "devDependencies": { - "@lars-reimann/prettier-config": "^5.0.0", + "@lars-reimann/prettier-config": "^5.2.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index e5732483..675acba8 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -156,9 +156,9 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: variance_type = mypy_variance_parser(generic_type.variance) variance_values: sds_types.AbstractType if variance_type == VarianceKind.INVARIANT: - variance_values = sds_types.UnionType([ - self.mypy_type_to_abstract_type(value) for value in generic_type.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) From cd5b7b2f54697b7b01cbfa313e0cdb3f26ff4855 Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 16:22:51 +0100 Subject: [PATCH 28/49] An error has been corrected which occurred when a class from the same module was used as a type during stub generation and was defined by Mypy as "UnboundType" and searched for as an alias. --- .../api_analyzer/_ast_visitor.py | 17 +++++++++--- .../aliasing/aliasing_module_1.py | 7 +---- .../__snapshots__/test_generate_stubs.ambr | 27 ++++++++++++++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index e5732483..41dbb729 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -837,12 +837,21 @@ def mypy_type_to_abstract_type( # Get qname if mypy_type.name in {"Any", "str", "int", "bool", "float", "None"}: - name = mypy_type.name - qname = f"builtins.{mypy_type.name}" + return sds_types.NamedType( + name=mypy_type.name, + qname=f"builtins.{mypy_type.name}" + ) else: + # first we check if it's a class from the same module + module = self.__declaration_stack[0] + for module_class in module.classes: + if module_class.name == mypy_type.name: + qname = module_class.id.replace("/", ".") + return sds_types.NamedType(name=module_class.name, qname=qname) + + # if not, we check if it's an alias name, qname = self._find_alias(mypy_type.name) - - return sds_types.NamedType(name=name, qname=qname) + return sds_types.NamedType(name=name, qname=qname) # Builtins elif isinstance(mypy_type, mp_types.Instance): diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py index c7805822..f3fec557 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_1.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -4,7 +4,7 @@ # Todo Erstelle ein Todo für Klassen die privat sind jedoch intern als Typen benutzt werden. Das Todo kommt über die # Stellen an denen diese Klasse als Typen genutzt wird. -# // TODO An internal class must not be used as a type in a public class. +# "// TODO An internal class must not be used as a type in a public class." class _AliasingModuleClassA: ... @@ -17,11 +17,6 @@ class AliasingModuleClassB: some_alias_b = AliasingModuleClassB -# Todo Create an issue: -# Wir erben von einer privaten internen Klasse, welche normalerweise nicht generiert werden würde, aber in diesem Fall -# müssten wir Stubs generieren. Um das zu bewerkstelligen müssten wir die Informationen in den API Daten in so fern -# erweitern, dass Superklassen Informationen auch in die andere Richtung zeigen, d.h. wir müssen die Unterklassen für -# alle Klassen erhausfinden. class AliasingModuleClassC(_some_alias_a): typed_alias_attr: some_alias_b infer_alias_attr = _some_alias_a() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 82c133e2..e331e8f9 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -41,6 +41,7 @@ package variousModulesPackage.aliasing.aliasingModule1 from aliasing_module_2 import AliasingModule2ClassA + from aliasing_module_3 import ImportMeAliasingModuleClass class AliasingModuleClassB() @@ -48,14 +49,14 @@ @PythonName("typed_alias_attr") static attr typedAliasAttr: AliasingModuleClassB @PythonName("infer_alias_attr") - static attr inferAliasAttr: () -> a: _AliasingModuleClassA + static attr inferAliasAttr: _AliasingModuleClassA @PythonName("typed_alias_attr2") static attr typedAliasAttr2: AliasingModule2ClassA @PythonName("infer_alias_attr2") static attr inferAliasAttr2: AliasingModule2ClassA // TODO List type has to many type arguments. @PythonName("alias_list") - static attr aliasList: List, AliasingModule2ClassA> + static attr aliasList: List, AliasingModule2ClassA, ImportMeAliasingModuleClass> } ''' @@ -69,11 +70,31 @@ class AliasingModuleClassB() + class ImportMeAliasingModuleClass() + class AliasingModuleClassC() { @PythonName("typed_alias_attr") static attr typedAliasAttr: AliasingModuleClassB + // TODO List type has to many type arguments. + @PythonName("alias_list") + static attr aliasList: List, ImportMeAliasingModuleClass> + } + + ''' +# --- +# name: test_alias_creation[aliasing_module_3] + ''' + @PythonModule("various_modules_package.aliasing.aliasing_module_3") + package variousModulesPackage.aliasing.aliasingModule3 + + from aliasing_module_2 import ImportMeAliasingModuleClass + + class ImportMeAliasingModuleClass() { + @PythonName("import_alias_attr") + static attr importAliasAttr: ImportMeAliasingModuleClass + // TODO List type has to many type arguments. @PythonName("alias_list") - static attr aliasList: List> + static attr aliasList: List } ''' From d7a48f348a89ebf562cb300a16cc30907421881f Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 16:55:26 +0100 Subject: [PATCH 29/49] snapshot update --- .../api_analyzer/__snapshots__/test__get_api.ambr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 c805a681..9ee10c6f 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -446,7 +446,7 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -474,7 +474,7 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -503,7 +503,7 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', @@ -870,7 +870,7 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -899,7 +899,7 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': 'tests.data.various_modules_package.attribute_module.AttributesClassA', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', From 9e7c35d7791888f15bc4d430e8b7df34d47484e3 Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 18:28:08 +0100 Subject: [PATCH 30/49] Added a "// Todo" if an internal class is used as type --- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 6 ++++++ .../various_modules_package/aliasing/aliasing_module_1.py | 3 --- .../stubs_generator/__snapshots__/test_generate_stubs.ambr | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index b80b0bb0..d556ca01 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -537,6 +537,11 @@ def _create_type_string(self, type_data: dict | None) -> str: return none_type_name case _: self._add_to_imports(type_data["qname"]) + + # inner classes that are private should not be used as types, therefore we add a todo + if name[0] == "_" and type_data["qname"] not in self.module_imports: + self._current_todo_msgs.add("internal class as type") + return name elif kind == "FinalType": return self._create_type_string(type_data["type"]) @@ -670,6 +675,7 @@ def _create_todo_msg(self, indentations: str) -> str: "param without type": "Some parameter have no type information.", "attr without type": "Attribute has no type information.", "result without type": "Result type information missing.", + "internal class as type": "An internal class must not be used as a type in a public class.", }[msg] for msg in self._current_todo_msgs ] diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_1.py b/tests/data/various_modules_package/aliasing/aliasing_module_1.py index f3fec557..8f752944 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_1.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -2,9 +2,6 @@ from aliasing_module_3 import ImportMeAliasingModuleClass as ImportMeAlias -# Todo Erstelle ein Todo für Klassen die privat sind jedoch intern als Typen benutzt werden. Das Todo kommt über die -# Stellen an denen diese Klasse als Typen genutzt wird. -# "// TODO An internal class must not be used as a type in a public class." class _AliasingModuleClassA: ... diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index e331e8f9..5f06e33b 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -48,12 +48,14 @@ class AliasingModuleClassC() sub _AliasingModuleClassA { @PythonName("typed_alias_attr") static attr typedAliasAttr: AliasingModuleClassB + // TODO An internal class must not be used as a type in a public class. @PythonName("infer_alias_attr") static attr inferAliasAttr: _AliasingModuleClassA @PythonName("typed_alias_attr2") static attr typedAliasAttr2: AliasingModule2ClassA @PythonName("infer_alias_attr2") static attr inferAliasAttr2: AliasingModule2ClassA + // TODO An internal class must not be used as a type in a public class. // TODO List type has to many type arguments. @PythonName("alias_list") static attr aliasList: List, AliasingModule2ClassA, ImportMeAliasingModuleClass> From 5c98df7856f378f524974fcfb18c80b5f704a07d Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 18:51:36 +0100 Subject: [PATCH 31/49] Codecov fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 16 +++++++--------- src/safeds_stubgen/api_analyzer/_get_api.py | 8 +++----- src/safeds_stubgen/api_analyzer/_mypy_helpers.py | 6 +++--- .../stubs_generator/_generate_stubs.py | 3 --- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index db2ac13a..fced6898 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections import defaultdict -from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING @@ -844,6 +843,10 @@ def mypy_type_to_abstract_type( else: # first we check if it's a class from the same module module = self.__declaration_stack[0] + + if not isinstance(module, Module): # pragma: no cover + raise TypeError(f"Expected module, got {type(module)}.") + for module_class in module.classes: if module_class.name == mypy_type.name: qname = module_class.id.replace("/", ".") @@ -891,7 +894,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: if type_name in self.aliases: qnames: set = self.aliases[type_name] if len(qnames) == 1: - qname = deepcopy(qnames).pop() + qname = qnames.pop() name = qname.split(".")[-1] # We have to check if this is an alias from an import @@ -989,13 +992,8 @@ def _is_public(self, name: str, qualified_name: str) -> bool: return True parent = self.__declaration_stack[-1] - if isinstance(parent, Class): - # Containing class is re-exported (always false if the current API element is not a method) - if parent.reexported_by: - return True - - if name == "__init__": - return parent.is_public + if isinstance(parent, Class) and name == "__init__": + return parent.is_public # The slicing is necessary so __init__ functions are not excluded (already handled in the first condition). return all(not it.startswith("_") for it in qualified_name.split(".")[:-1]) diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index 2099d6b8..e3fa0e8d 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -132,12 +132,12 @@ def _get_mypy_asts( def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: - if not result_types: - return {} - aliases: dict[str, set[str]] = defaultdict(set) for key in result_types: if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr): + in_package = False + name = "" + if isinstance(key, mypy_nodes.NameExpr): type_value = result_types[key] @@ -165,8 +165,6 @@ def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]: continue in_package = package_name in fullname - else: - continue else: in_package = package_name in key.fullname if in_package: diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index d07056aa..d36004ff 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -105,7 +105,9 @@ def mypy_expression_to_sds_type(expr: mp_nodes.Expression) -> sds_types.Abstract raise TypeError("Unexpected expression type.") # pragma: no cover -def mypy_expression_to_python_value(expr: mp_nodes.Expression) -> str | None | int | float: +def mypy_expression_to_python_value( + expr: mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr | mp_nodes.NameExpr +) -> str | None | int | float: if isinstance(expr, mp_nodes.NameExpr): match expr.name: case "None": @@ -114,8 +116,6 @@ def mypy_expression_to_python_value(expr: mp_nodes.Expression) -> str | None | i return True case "False": return False - case _: - return expr.name elif isinstance(expr, mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr): return expr.value diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index d556ca01..05d2fbee 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -688,9 +688,6 @@ def _create_todo_msg(self, indentations: str) -> str: @staticmethod def _split_import_id(id_: str) -> tuple[str, str]: - if "." not in id_: - return "", id_ - split_qname = id_.split(".") name = split_qname.pop(-1) import_path = ".".join(split_qname) From 01355f0dd2032d80895e6cff61bf9f3eebda9ef2 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:53:15 +0000 Subject: [PATCH 32/49] style: apply automated linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 5 +---- src/safeds_stubgen/api_analyzer/_mypy_helpers.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index fced6898..72d5a11b 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -836,10 +836,7 @@ def mypy_type_to_abstract_type( # Get qname if mypy_type.name in {"Any", "str", "int", "bool", "float", "None"}: - return sds_types.NamedType( - name=mypy_type.name, - qname=f"builtins.{mypy_type.name}" - ) + return sds_types.NamedType(name=mypy_type.name, qname=f"builtins.{mypy_type.name}") else: # first we check if it's a class from the same module module = self.__declaration_stack[0] diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index d36004ff..c137c9dc 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -106,7 +106,7 @@ def mypy_expression_to_sds_type(expr: mp_nodes.Expression) -> sds_types.Abstract def mypy_expression_to_python_value( - expr: mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr | mp_nodes.NameExpr + expr: mp_nodes.IntExpr | mp_nodes.FloatExpr | mp_nodes.StrExpr | mp_nodes.NameExpr, ) -> str | None | int | float: if isinstance(expr, mp_nodes.NameExpr): match expr.name: From fc931157c5d51dac09afe7346e1c37997ec1f7ab Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 19:05:58 +0100 Subject: [PATCH 33/49] Fixed tests which I broke with the last commit. --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 72d5a11b..4d8756b7 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import defaultdict +from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING @@ -891,7 +892,8 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: if type_name in self.aliases: qnames: set = self.aliases[type_name] if len(qnames) == 1: - qname = qnames.pop() + # We need a deepcopy since qnames is a pointer to the set in the alias dict + qname = deepcopy(qnames).pop() name = qname.split(".")[-1] # We have to check if this is an alias from an import From a7b5f88fb633c516a29a0a25a607c4cd15086a46 Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 19:35:02 +0100 Subject: [PATCH 34/49] codecov fix --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 15 ++++++++++----- .../aliasing/aliasing_module_2.py | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 4d8756b7..aefab7dd 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -889,6 +889,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: name = "" qname = "" + qualified_imports = module.qualified_imports if type_name in self.aliases: qnames: set = self.aliases[type_name] if len(qnames) == 1: @@ -897,7 +898,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: name = qname.split(".")[-1] # We have to check if this is an alias from an import - import_name, import_qname = self._search_alias_in_qualified_imports(module, type_name) + import_name, import_qname = self._search_alias_in_qualified_imports(qualified_imports, type_name) name = import_name if import_name else name qname = import_qname if import_qname else qname @@ -917,7 +918,9 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: break # Then we check if the type was perhapse imported - qimport_name, qimport_qname = self._search_alias_in_qualified_imports(module, name, alias_qname) + qimport_name, qimport_qname = self._search_alias_in_qualified_imports( + qualified_imports, name, alias_qname + ) if qimport_qname: qname = qimport_qname name = qimport_name if qimport_name else name @@ -933,7 +936,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: break else: - name, qname = self._search_alias_in_qualified_imports(module, type_name) + name, qname = self._search_alias_in_qualified_imports(qualified_imports, type_name) if not qname: # pragma: no cover raise ValueError(f"It was not possible to find out where the alias {type_name} was defined.") @@ -941,8 +944,10 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: return name, qname @staticmethod - def _search_alias_in_qualified_imports(module: Module, alias_name: str, alias_qname: str = "") -> tuple[str, str]: - for qualified_import in module.qualified_imports: + def _search_alias_in_qualified_imports( + qualified_imports: list[QualifiedImport], alias_name: str, alias_qname: str = "" + ) -> tuple[str, str]: + for qualified_import in qualified_imports: if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: qname = qualified_import.qualified_name name = qname.split(".")[-1] diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_2.py b/tests/data/various_modules_package/aliasing/aliasing_module_2.py index e873fe43..a1f99f8a 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_2.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_2.py @@ -1,3 +1,6 @@ +from aliasing_module_1 import AliasingModuleClassC + + class AliasingModule2ClassA: ... @@ -11,6 +14,7 @@ class ImportMeAliasingModuleClass: some_alias_b = AliasingModuleClassB +ImportMeAlias = AliasingModuleClassC class AliasingModuleClassC: From 57056168e60043472c56a5d6293f9e479503ddb9 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:39:04 +0000 Subject: [PATCH 35/49] style: apply automated linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index aefab7dd..1f71916a 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -919,7 +919,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: # Then we check if the type was perhapse imported qimport_name, qimport_qname = self._search_alias_in_qualified_imports( - qualified_imports, name, alias_qname + qualified_imports, name, alias_qname, ) if qimport_qname: qname = qimport_qname @@ -945,7 +945,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: @staticmethod def _search_alias_in_qualified_imports( - qualified_imports: list[QualifiedImport], alias_name: str, alias_qname: str = "" + qualified_imports: list[QualifiedImport], alias_name: str, alias_qname: str = "", ) -> tuple[str, str]: for qualified_import in qualified_imports: if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: From b172648664b0efa7c917c9ead0c5bc52d9fdbc6f Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:40:35 +0000 Subject: [PATCH 36/49] style: apply automated linter fixes --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 1f71916a..dc4df018 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -919,7 +919,9 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: # Then we check if the type was perhapse imported qimport_name, qimport_qname = self._search_alias_in_qualified_imports( - qualified_imports, name, alias_qname, + qualified_imports, + name, + alias_qname, ) if qimport_qname: qname = qimport_qname @@ -945,7 +947,9 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: @staticmethod def _search_alias_in_qualified_imports( - qualified_imports: list[QualifiedImport], alias_name: str, alias_qname: str = "", + qualified_imports: list[QualifiedImport], + alias_name: str, + alias_qname: str = "", ) -> tuple[str, str]: for qualified_import in qualified_imports: if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: From cd615280732358994beff3aff1719d75c839c462 Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 20:29:38 +0100 Subject: [PATCH 37/49] codecov and test fixes --- .../api_analyzer/_ast_visitor.py | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index dc4df018..ad46ab7a 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -890,23 +890,28 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: name = "" qname = "" qualified_imports = module.qualified_imports + import_aliases = [qimport.alias for qimport in qualified_imports] + if type_name in self.aliases: qnames: set = self.aliases[type_name] if len(qnames) == 1: - # We need a deepcopy since qnames is a pointer to the set in the alias dict - qname = deepcopy(qnames).pop() - name = qname.split(".")[-1] - # We have to check if this is an alias from an import import_name, import_qname = self._search_alias_in_qualified_imports(qualified_imports, type_name) - name = import_name if import_name else name - qname = import_qname if import_qname else qname - + # We need a deepcopy since qnames is a pointer to the set in the alias dict + qname = import_qname if import_qname else deepcopy(qnames).pop() + name = import_name if import_name else qname.split(".")[-1] + elif type_name in import_aliases: + # We check if the type was imported + qimport_name, qimport_qname = self._search_alias_in_qualified_imports(qualified_imports, type_name) + + if qimport_qname: + qname = qimport_qname + name = qimport_name if qimport_name else name else: # In this case some types where defined in multiple modules with the same names. for alias_qname in qnames: - # First we check if the type was defined in the same module + # We check if the type was defined in the same module type_path = ".".join(alias_qname.split(".")[0:-1]) name = alias_qname.split(".")[-1] @@ -916,27 +921,6 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: if type_path == self.mypy_file.fullname: qname = alias_qname break - - # Then we check if the type was perhapse imported - qimport_name, qimport_qname = self._search_alias_in_qualified_imports( - qualified_imports, - name, - alias_qname, - ) - if qimport_qname: - qname = qimport_qname - name = qimport_name if qimport_name else name - break - - found_qname = False - for wildcard_import in module.wildcard_imports: - if wildcard_import.module_name in alias_qname: - qname = alias_qname - found_qname = True - break - if found_qname: - break - else: name, qname = self._search_alias_in_qualified_imports(qualified_imports, type_name) @@ -949,15 +933,12 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: def _search_alias_in_qualified_imports( qualified_imports: list[QualifiedImport], alias_name: str, - alias_qname: str = "", ) -> tuple[str, str]: for qualified_import in qualified_imports: if alias_name in {qualified_import.alias, qualified_import.qualified_name.split(".")[-1]}: qname = qualified_import.qualified_name name = qname.split(".")[-1] return name, qname - elif alias_qname and qualified_import.qualified_name in alias_qname: - return "", alias_qname return "", "" def _create_module_id(self, qname: str) -> str: From e8f739d14b403bd861db802bb0bda838b4e30c22 Mon Sep 17 00:00:00 2001 From: Arsam Date: Tue, 20 Feb 2024 20:33:05 +0100 Subject: [PATCH 38/49] Refactoring --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index ad46ab7a..55a097c5 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -907,7 +907,7 @@ def _find_alias(self, type_name: str) -> tuple[str, str]: if qimport_qname: qname = qimport_qname - name = qimport_name if qimport_name else name + name = qimport_name else: # In this case some types where defined in multiple modules with the same names. for alias_qname in qnames: From f209301912b24f78859b10d59e98cb9968e67376 Mon Sep 17 00:00:00 2001 From: Arsam Date: Wed, 21 Feb 2024 10:50:54 +0100 Subject: [PATCH 39/49] Added a test case for aliasing --- .../various_modules_package/aliasing/aliasing_module_2.py | 1 + .../stubs_generator/__snapshots__/test_generate_stubs.ambr | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/tests/data/various_modules_package/aliasing/aliasing_module_2.py b/tests/data/various_modules_package/aliasing/aliasing_module_2.py index a1f99f8a..6052e2e1 100644 --- a/tests/data/various_modules_package/aliasing/aliasing_module_2.py +++ b/tests/data/various_modules_package/aliasing/aliasing_module_2.py @@ -19,5 +19,6 @@ class ImportMeAliasingModuleClass: class AliasingModuleClassC: typed_alias_attr: some_alias_b + typed_alias_infer = ImportMeAlias alias_list: list[str | some_alias_b, ImportMeAliasingModuleClass] diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 5f06e33b..7d353f26 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -68,6 +68,8 @@ @PythonModule("various_modules_package.aliasing.aliasing_module_2") package variousModulesPackage.aliasing.aliasingModule2 + from aliasing_module_1 import AliasingModuleClassC + class AliasingModule2ClassA() class AliasingModuleClassB() @@ -77,6 +79,8 @@ class AliasingModuleClassC() { @PythonName("typed_alias_attr") static attr typedAliasAttr: AliasingModuleClassB + @PythonName("typed_alias_infer") + static attr typedAliasInfer: AliasingModuleClassC // TODO List type has to many type arguments. @PythonName("alias_list") static attr aliasList: List, ImportMeAliasingModuleClass> From 4e59ebe4cae6b8e4e79e02675adde588f66812b2 Mon Sep 17 00:00:00 2001 From: Arsam Date: Wed, 21 Feb 2024 11:26:56 +0100 Subject: [PATCH 40/49] Adding a missing line that was lost with the last merge with the main branch --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 ++ .../stubs_generator/__snapshots__/test_generate_stubs.ambr | 1 + 2 files changed, 3 insertions(+) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 35b16732..7a4c92ab 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -825,6 +825,8 @@ def mypy_type_to_abstract_type( return sds_types.UnionType(types=[self.mypy_type_to_abstract_type(item) for item in mypy_type.items]) # Special Cases + elif isinstance(mypy_type, mp_types.TypeVarType): + return sds_types.TypeVarType(mypy_type.name) 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/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 28c7e7a1..72d041f9 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -200,6 +200,7 @@ 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") From bb2a761492875ebab66f6672dd598178794a85ef Mon Sep 17 00:00:00 2001 From: Arsam Date: Thu, 22 Feb 2024 14:08:41 +0100 Subject: [PATCH 41/49] WIP test config --- tests/conftest.py | 18 +++++++++ .../stubs_generator/test_generate_stubs.py | 40 +++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b38ce1fe --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +from typing import Any + +import pytest +from syrupy import SnapshotAssertion +from syrupy.extensions.single_file import SingleFileSnapshotExtension +from syrupy.types import SerializableData, SerializedData + + +class SDSStubExtension(SingleFileSnapshotExtension): + _file_extension = "sdsstub" + + def serialize(self, data: str, **_kwargs: Any) -> SerializedData: + return bytes(data, encoding="utf8") + + +@pytest.fixture() +def snapshot_sdsstub(snapshot: SnapshotAssertion) -> SnapshotAssertion: + return snapshot.use_extension(SDSStubExtension) diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index cb6805f9..b4a64f83 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -54,10 +54,10 @@ def _assert_file_creation_recursive(python_path: Path, stub_path: Path) -> None: _assert_file_creation_recursive(py_item, stub_item) -def assert_stubs_snapshot(filename: str, snapshot: SnapshotAssertion) -> None: +def assert_stubs_snapshot(filename: str, snapshot_sdsstub: SnapshotAssertion) -> None: stubs_file = Path(_out_dir_stubs / filename / f"{filename}.sdsstub") with stubs_file.open("r") as f: - assert f.read() == snapshot + assert f.read() == snapshot_sdsstub # ############################## Tests ############################## # @@ -68,46 +68,46 @@ def test_file_creation() -> None: ) -def test_class_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("class_module", snapshot) +def test_class_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("class_module", snapshot_sdsstub) -def test_class_attribute_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("attribute_module", snapshot) +def test_class_attribute_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("attribute_module", snapshot_sdsstub) -def test_function_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("function_module", snapshot) +def test_function_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("function_module", snapshot_sdsstub) -def test_enum_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("enum_module", snapshot) +def test_enum_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("enum_module", snapshot_sdsstub) -def test_type_inference(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("infer_types_module", snapshot) +def test_type_inference(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("infer_types_module", snapshot_sdsstub) -def test_variance_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("variance_module", snapshot) +def test_variance_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("variance_module", snapshot_sdsstub) -def test_abstract_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("abstract_module", snapshot) +def test_abstract_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("abstract_module", snapshot_sdsstub) -def test_import_creation(snapshot: SnapshotAssertion) -> None: - assert_stubs_snapshot("import_module", snapshot) +def test_import_creation(snapshot_sdsstub: SnapshotAssertion) -> None: + assert_stubs_snapshot("import_module", snapshot_sdsstub) @pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2", "aliasing_module_3"]) -def test_alias_creation(file_name: str, snapshot: SnapshotAssertion) -> None: +def test_alias_creation(file_name: str, snapshot_sdsstub: SnapshotAssertion) -> None: file_data = "" stubs_file = Path(_out_dir_stubs / "aliasing" / f"{file_name}" / f"{file_name}.sdsstub") with stubs_file.open("r") as f: file_data += f.read() - assert file_data == snapshot + assert file_data == snapshot_sdsstub @pytest.mark.parametrize( From f67983534d124484b21d494f622fed96619a3d8c Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:10:18 +0000 Subject: [PATCH 42/49] style: apply automated linter fixes --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index b38ce1fe..2c66daec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest from syrupy import SnapshotAssertion from syrupy.extensions.single_file import SingleFileSnapshotExtension -from syrupy.types import SerializableData, SerializedData +from syrupy.types import SerializedData class SDSStubExtension(SingleFileSnapshotExtension): From 7eaf17bf506fdc8de3d176ba2a76c83baafd7bd9 Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 23 Feb 2024 14:26:36 +0100 Subject: [PATCH 43/49] WIP adjusted snapshot tests files --- .../test_abstract_creation.sdsstub | 3 --- ..._alias_creation[aliasing_module_1].sdsstub | 23 +++++++++++++++++++ ..._alias_creation[aliasing_module_2].sdsstub | 20 ++++++++++++++++ ..._alias_creation[aliasing_module_3].sdsstub | 12 ++++++++++ .../test_class_attribute_creation.sdsstub | 14 ++++++----- .../test_enum_creation.sdsstub | 2 -- .../test_function_creation.sdsstub | 22 +++++++++++++----- .../test_import_creation.sdsstub | 19 +++++++++++++-- .../test_type_inference.sdsstub | 2 +- .../test_variance_creation.sdsstub | 9 +------- .../stubs_generator/test_generate_stubs.py | 8 ------- 11 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_1].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_2].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_3].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_abstract_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_abstract_creation.sdsstub index ba935696..3f62de73 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_abstract_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_abstract_creation.sdsstub @@ -1,9 +1,6 @@ @PythonModule("various_modules_package.abstract_module") package variousModulesPackage.abstractModule -from abc import ABC -from abc import abstractmethod - class AbstractModuleClass { @PythonName("abstract_property_method") attr abstractPropertyMethod: union diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_1].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_1].sdsstub new file mode 100644 index 00000000..4c4542d2 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_1].sdsstub @@ -0,0 +1,23 @@ +@PythonModule("various_modules_package.aliasing.aliasing_module_1") +package variousModulesPackage.aliasing.aliasingModule1 + +from variousModulesPackage.aliasing.aliasingModule2 import AliasingModule2ClassA +from variousModulesPackage.aliasing.aliasingModule3 import ImportMeAliasingModuleClass + +class AliasingModuleClassB() + +class AliasingModuleClassC() sub _AliasingModuleClassA { + @PythonName("typed_alias_attr") + static attr typedAliasAttr: AliasingModuleClassB + // TODO An internal class must not be used as a type in a public class. + @PythonName("infer_alias_attr") + static attr inferAliasAttr: _AliasingModuleClassA + @PythonName("typed_alias_attr2") + static attr typedAliasAttr2: AliasingModule2ClassA + @PythonName("infer_alias_attr2") + static attr inferAliasAttr2: AliasingModule2ClassA + // TODO An internal class must not be used as a type in a public class. + // TODO List type has to many type arguments. + @PythonName("alias_list") + static attr aliasList: List, AliasingModule2ClassA, ImportMeAliasingModuleClass> +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_2].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_2].sdsstub new file mode 100644 index 00000000..2955104b --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_2].sdsstub @@ -0,0 +1,20 @@ +@PythonModule("various_modules_package.aliasing.aliasing_module_2") +package variousModulesPackage.aliasing.aliasingModule2 + +from variousModulesPackage.aliasing.aliasingModule1 import AliasingModuleClassC + +class AliasingModule2ClassA() + +class AliasingModuleClassB() + +class ImportMeAliasingModuleClass() + +class AliasingModuleClassC() { + @PythonName("typed_alias_attr") + static attr typedAliasAttr: AliasingModuleClassB + @PythonName("typed_alias_infer") + static attr typedAliasInfer: AliasingModuleClassC + // TODO List type has to many type arguments. + @PythonName("alias_list") + static attr aliasList: List, ImportMeAliasingModuleClass> +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_3].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_3].sdsstub new file mode 100644 index 00000000..ec910335 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_3].sdsstub @@ -0,0 +1,12 @@ +@PythonModule("various_modules_package.aliasing.aliasing_module_3") +package variousModulesPackage.aliasing.aliasingModule3 + +from variousModulesPackage.aliasing.aliasingModule2 import ImportMeAliasingModuleClass + +class ImportMeAliasingModuleClass() { + @PythonName("import_alias_attr") + static attr importAliasAttr: ImportMeAliasingModuleClass + // TODO List type has to many type arguments. + @PythonName("alias_list") + static attr aliasList: List +} 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 84cb8262..f60b00e0 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 @@ -1,10 +1,7 @@ @PythonModule("various_modules_package.attribute_module") package variousModulesPackage.attributeModule -from typing import Optional -from typing import Final -from typing import Literal -from typing import TypeVar +from mainPackage.anotherPath.anotherModule import AnotherClass class AttributesClassA() @@ -75,9 +72,9 @@ class AttributesClassB() { static attr finalUnion: union static attr `literal`: literal<"Some String"> @PythonName("multiple_literals") - static attr multipleLiterals: literal<"Literal_1", "Literal_2", 3, True> + static attr multipleLiterals: literal<"Literal_1", "Literal_2", 3, true> @PythonName("mixed_literal_union") - static attr mixedLiteralUnion: union> + static attr mixedLiteralUnion: union> @PythonName("multi_attr_1") static attr multiAttr1: Int @PythonName("multi_attr_3") @@ -90,6 +87,11 @@ class AttributesClassB() { static attr multiAttr7: String @PythonName("multi_attr_8") static attr multiAttr8: String + @PythonName("attr_type_from_outside_package") + 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") diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_enum_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_enum_creation.sdsstub index cf7c4f78..83c0e80b 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_enum_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_enum_creation.sdsstub @@ -1,8 +1,6 @@ @PythonModule("various_modules_package.enum_module") package variousModulesPackage.enumModule -from another_path.another_module import AnotherClass as _AcImportAlias - enum _ReexportedEmptyEnum enum EnumTest { 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 dc5bd5df..d0a7cc9b 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 @@ -1,11 +1,7 @@ @PythonModule("various_modules_package.function_module") package variousModulesPackage.functionModule -from typing import Callable -from typing import Optional -from typing import Literal -from typing import Any -from typing import TypeVar +from mainPackage.anotherPath.anotherModule import AnotherClass // TODO Result type information missing. @Pure @@ -70,6 +66,7 @@ fun illegalParams( lst: List, @PythonName("lst_2") lst2: List, tpl: Tuple, + dct: Map, `_`: Int = String ) @@ -96,7 +93,7 @@ fun paramPosition( self, a, b: Boolean, - c: FunctionModuleClassA = FunctionModuleClassA, + c, d, e: Int = 1 ) @@ -234,6 +231,19 @@ fun callableType( param: (a: String) -> (b: Int, c: String) ) -> result1: (a: Int, b: Int) -> c: Int +// TODO Result type information missing. +// TODO Some parameter have no type information. +@Pure +@PythonName("param_from_outside_the_package") +fun paramFromOutsideThePackage( + @PythonName("param_type") paramType: AnotherClass, + @PythonName("param_value") paramValue +) + +@Pure +@PythonName("result_from_outside_the_package") +fun resultFromOutsideThePackage() -> result1: AnotherClass + @Pure @PythonName("type_var_func") fun typeVarFunc( diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub index 8cba1d1f..a2842c14 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub @@ -1,6 +1,21 @@ @PythonModule("various_modules_package.import_module") package variousModulesPackage.importModule -import mypy as `static` +from mainPackage.anotherPath.anotherModule import AnotherClass +from variousModulesPackage.classModule import ClassModuleClassB +from variousModulesPackage.classModule import ClassModuleClassC +from variousModulesPackage.classModule import ClassModuleClassD +from variousModulesPackage.classModule import ClassModuleEmptyClassA -from math import * +class ImportClass() sub AnotherClass { + @PythonName("typed_import_attr") + static attr typedImportAttr: ClassModuleClassD + @PythonName("default_import_attr") + static attr defaultImportAttr: ClassModuleEmptyClassA + + @Pure + @PythonName("import_function") + fun importFunction( + @PythonName("import_param") importParam: ClassModuleClassB + ) -> result1: ClassModuleClassC +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_inference.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_inference.sdsstub index 3611fb3c..22573858 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_inference.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_inference.sdsstub @@ -47,7 +47,7 @@ class InferMyTypes( @PythonName("infer_function") static fun inferFunction( @PythonName("infer_param") inferParam: Int = 1, - @PythonName("infer_param_2") inferParam2: Int = Something + @PythonName("infer_param_2") inferParam2: Int = "Something" ) -> (result1: union, result2: union, result3: Float) @Pure 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 0b7b3488..46fdfc17 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 @@ -1,15 +1,8 @@ @PythonModule("various_modules_package.variance_module") package variousModulesPackage.varianceModule -from typing import Generic -from typing import TypeVar -from typing import Literal - class A() -class VarianceClassAll() where { - TCo sub String, - TCon super A -} +class VarianceClassAll() class VarianceClassOnlyInvariance() diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 823696a2..02baa471 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -68,42 +68,34 @@ def test_file_creation() -> None: ) -# Todo Check snapshot def test_class_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("class_module", snapshot_sds_stub) -# Todo Check snapshot def test_class_attribute_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("attribute_module", snapshot_sds_stub) -# Todo Check snapshot def test_function_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("function_module", snapshot_sds_stub) -# Todo Check snapshot def test_enum_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("enum_module", snapshot_sds_stub) -# Todo Check snapshot def test_import_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("import_module", snapshot_sds_stub) -# Todo Check snapshot def test_type_inference(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("infer_types_module", snapshot_sds_stub) -# Todo Check snapshot def test_variance_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("variance_module", snapshot_sds_stub) -# Todo Check snapshot def test_abstract_creation(snapshot_sds_stub: SnapshotAssertion) -> None: assert_stubs_snapshot("abstract_module", snapshot_sds_stub) From 83c062e541f8a698ef90df1bff0f0ba1c0018383 Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 23 Feb 2024 18:09:20 +0100 Subject: [PATCH 44/49] Fixed paths for imports, added a "// Todo" for the usage sets in stubs, which are not supported and adjusted snapshot test files --- .../api_analyzer/_ast_visitor.py | 31 +++++++-------- .../stubs_generator/_generate_stubs.py | 39 ++++++++++++++++--- .../test_class_attribute_creation.sdsstub | 6 ++- .../test_function_creation.sdsstub | 6 ++- .../test_import_creation.sdsstub | 2 +- .../stubs_generator/test_generate_stubs.py | 4 +- 6 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 7a4c92ab..930a7473 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -615,23 +615,22 @@ def _create_attribute( attribute_type = None # NameExpr are class attributes - elif node is not None and isinstance(attribute, mp_nodes.NameExpr): - if not node.explicit_self_type: - attribute_type = node.type + elif node is not None and isinstance(attribute, mp_nodes.NameExpr) and not node.explicit_self_type: + attribute_type = node.type - # We need to get the unanalyzed_type for lists, since mypy is not able to check type hint information - # regarding list item types - if ( - attribute_type is not None - and hasattr(attribute_type, "type") - and hasattr(attribute_type, "args") - and attribute_type.type.fullname == "builtins.list" - and not node.is_inferred - ): - if unanalyzed_type is not None and hasattr(unanalyzed_type, "args"): - attribute_type.args = unanalyzed_type.args - else: # pragma: no cover - raise AttributeError("Could not get argument information for attribute.") + # We need to get the unanalyzed_type for lists, since mypy is not able to check type hint information + # regarding list item types + if ( + attribute_type is not None + and hasattr(attribute_type, "type") + and hasattr(attribute_type, "args") + and attribute_type.type.fullname == "builtins.list" + and not node.is_inferred + ): + if unanalyzed_type is not None and hasattr(unanalyzed_type, "args"): + attribute_type.args = unanalyzed_type.args + else: # pragma: no cover + raise AttributeError("Could not get argument information for attribute.") # Ignore types that are special mypy any types. The Any type "from_unimported_type" could appear for aliase if attribute_type is not None and not ( diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index fe6c74f6..84845eb9 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -41,7 +41,7 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: modules = api.modules.values() Path(out_path / api.package).mkdir(parents=True, exist_ok=True) - stubs_generator = StubsStringGenerator(api.package, convert_identifiers) + stubs_generator = StubsStringGenerator(api, convert_identifiers) for module in modules: module_name = module.name @@ -78,9 +78,9 @@ class StubsStringGenerator: method. """ - def __init__(self, package_name: str, convert_identifiers: bool) -> None: + def __init__(self, api: API, convert_identifiers: bool) -> None: self.module_imports: set[str] = set() - self.package_name = package_name + self.api = api self.convert_identifiers = convert_identifiers def __call__(self, module: Module) -> str: @@ -569,6 +569,9 @@ def _create_type_string(self, type_data: dict | None) -> str: # Cut out the "Type" in the kind name name = kind[0:-4] + if name == "Set": + self._current_todo_msgs.add("no set support") + if types: if len(types) >= 2: self._current_todo_msgs.add(name) @@ -613,7 +616,7 @@ def _create_type_string(self, type_data: dict | None) -> str: return f"union<{', '.join(types)}>" return "" elif kind == "TupleType": - self._current_todo_msgs.add("Tuple") + self._current_todo_msgs.add("no tuple support") types = [self._create_type_string(type_) for type_ in type_data["types"]] return f"Tuple<{', '.join(types)}>" @@ -626,6 +629,11 @@ def _create_type_string(self, type_data: dict | None) -> str: for literal_type in type_data["literals"]: if isinstance(literal_type, str): types.append(f'"{literal_type}"') + elif isinstance(literal_type, bool): + if literal_type: + types.append("true") + else: + types.append("false") else: types.append(f"{literal_type}") return f"literal<{', '.join(types)}>" @@ -638,7 +646,17 @@ def _create_type_string(self, type_data: dict | None) -> str: # ############################### Utilities ############################### # def _add_to_imports(self, qname: str) -> None: - """Check if the qname of a type is defined in the current module. If not, we create an import for it.""" + """Check if the qname of a type is defined in the current module, if not, create an import for it. + + Paramters + --------- + qname : str + The qualified name of a module/class/etc. + + Returns + ------- + None + """ if qname == "": # pragma: no cover raise ValueError("Type has no import source.") @@ -648,6 +666,14 @@ def _add_to_imports(self, qname: str) -> None: module_id = self.module.id.replace("/", ".") if module_id not in qname: + # We need the full path for an import from the same package, but we sometimes don't get enough information, + # therefore we have to search for the class and get its id + qname_path = qname.replace(".", "/") + for class_ in self.api.classes: + if class_.endswith(qname_path): + qname = class_.replace("/", ".") + qname = self._convert_snake_to_camel_case(qname) + self.module_imports.add(qname) @staticmethod @@ -667,7 +693,8 @@ def _create_todo_msg(self, indentations: str) -> str: todo_msgs = [ "// TODO " + { - "Tuple": "Safe-DS does not support tuple types.", + "no tuple support": "Safe-DS does not support tuple types.", + "no set support": "Safe-DS does not support set types.", "List": "List type has to many type arguments.", "Set": "Set type has to many type arguments.", "OPT_POS_ONLY": "Safe-DS does not support optional but position only parameter assignments.", 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 f60b00e0..4548f3e4 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 @@ -1,7 +1,7 @@ @PythonModule("various_modules_package.attribute_module") package variousModulesPackage.attributeModule -from mainPackage.anotherPath.anotherModule import AnotherClass +from tests.data.main_package.another_path.another_module import AnotherClass class AttributesClassA() @@ -39,13 +39,17 @@ class AttributesClassB() { // TODO List type has to many type arguments. @PythonName("list_attr_4") static attr listAttr4: List> + // TODO Safe-DS does not support set types. @PythonName("set_attr_1") static attr setAttr1: Set + // TODO Safe-DS does not support set types. @PythonName("set_attr_2") static attr setAttr2: Set> + // TODO Safe-DS does not support set types. // TODO Set type has to many type arguments. @PythonName("set_attr_3") static attr setAttr3: Set + // TODO Safe-DS does not support set types. // TODO Set type has to many type arguments. @PythonName("set_attr_4") static attr setAttr4: Set> 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 d0a7cc9b..e444f5f8 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 @@ -1,7 +1,7 @@ @PythonModule("various_modules_package.function_module") package variousModulesPackage.functionModule -from mainPackage.anotherPath.anotherModule import AnotherClass +from tests.data.main_package.another_path.another_module import AnotherClass // TODO Result type information missing. @Pure @@ -9,6 +9,7 @@ from mainPackage.anotherPath.anotherModule import AnotherClass fun publicNoParamsNoResult() // TODO Result type information missing. +// TODO Safe-DS does not support set types. // TODO Safe-DS does not support tuple types. // TODO Some parameter have no type information. @Pure @@ -33,6 +34,7 @@ fun params( ) // TODO Result type information missing. +// TODO Safe-DS does not support set types. // TODO Safe-DS does not support tuple types. // TODO Some parameter have no type information. @Pure @@ -204,10 +206,12 @@ fun illegalDictionaryResults() -> result1: Map @PythonName("union_dictionary_results") fun unionDictionaryResults() -> result1: Map, union> +// TODO Safe-DS does not support set types. @Pure @PythonName("set_results") fun setResults() -> result1: Set +// TODO Safe-DS does not support set types. // TODO Set type has to many type arguments. @Pure @PythonName("illegal_set_results") diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub index a2842c14..5ae002a1 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub @@ -1,7 +1,7 @@ @PythonModule("various_modules_package.import_module") package variousModulesPackage.importModule -from mainPackage.anotherPath.anotherModule import AnotherClass +from variousModulesPackage.anotherPath.anotherModule import AnotherClass from variousModulesPackage.classModule import ClassModuleClassB from variousModulesPackage.classModule import ClassModuleClassC from variousModulesPackage.classModule import ClassModuleClassD diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index 02baa471..0a3fa63b 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING import pytest -from safeds_stubgen.api_analyzer import get_api +from safeds_stubgen.api_analyzer import API, get_api from safeds_stubgen.stubs_generator import generate_stubs # noinspection PyProtectedMember @@ -135,7 +135,7 @@ def test_convert_snake_to_camel_case( convert_identifiers: bool, ) -> None: stubs_string_generator = StubsStringGenerator( - package_name=_test_package_name, + api=API(distribution="", package=_test_package_name, version=""), convert_identifiers=convert_identifiers, ) assert stubs_string_generator._convert_snake_to_camel_case(name, is_class_name) == expected_result From 9dcb01daac8755637007eeaf6c2ad97154c94d7a Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 23 Feb 2024 18:59:22 +0100 Subject: [PATCH 45/49] Fixed a bug in the stub generator where string default values would not be seen as strings if the parameter has a type hint of another type than string --- src/safeds_stubgen/api_analyzer/_api.py | 1 + .../api_analyzer/_ast_visitor.py | 4 +- .../stubs_generator/_generate_stubs.py | 38 ++++++------------- .../test_function_creation.sdsstub | 2 +- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/safeds_stubgen/api_analyzer/_api.py b/src/safeds_stubgen/api_analyzer/_api.py index 9b7cac80..522c2120 100644 --- a/src/safeds_stubgen/api_analyzer/_api.py +++ b/src/safeds_stubgen/api_analyzer/_api.py @@ -261,6 +261,7 @@ class Parameter: id: str name: str is_optional: bool + # We do not support default values that aren't core classes or classes definied in the package we analyze. default_value: str | bool | int | float | None assigned_by: ParameterAssignment docstring: ParameterDocstring diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 930a7473..0cb3088d 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -745,8 +745,10 @@ def _get_parameter_type_and_default_value( ): # See https://github.com/Safe-DS/Stub-Generator/issues/34#issuecomment-1819643719 inferred_default_value = mypy_expression_to_python_value(initializer) - if isinstance(inferred_default_value, str | bool | int | float | NoneType): + if isinstance(inferred_default_value, bool | int | float | NoneType): default_value = inferred_default_value + elif isinstance(inferred_default_value, str): + default_value = f'"{inferred_default_value}"' else: # pragma: no cover raise TypeError("Default value got an unsupported value.") diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 84845eb9..8e191050 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -175,40 +175,27 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st constraints_info = "" variance_info = "" if class_.type_parameters: - constraints = [] variances = [] + out = "out " for variance in class_.type_parameters: - match variance.variance.name: - case VarianceKind.INVARIANT.name: - variance_inheritance = "" - variance_direction = "" - case VarianceKind.COVARIANT.name: - variance_inheritance = "sub" - variance_direction = "out " - case VarianceKind.CONTRAVARIANT.name: - variance_inheritance = "super" - variance_direction = "in " - case _: # pragma: no cover - raise ValueError(f"Expected variance kind, got {variance.variance.name}.") + variance_direction = { + VarianceKind.INVARIANT.name: "", + VarianceKind.COVARIANT.name: out, + VarianceKind.CONTRAVARIANT.name: "in " + }[variance.variance.name] # Convert name to camelCase and check for keywords variance_name_camel_case = self._convert_snake_to_camel_case(variance.name) variance_name_camel_case = self._replace_if_safeds_keyword(variance_name_camel_case) - variances.append(f"{variance_direction}{variance_name_camel_case}") - if variance_inheritance: - constraints.append( - f"{variance_name_camel_case} {variance_inheritance} " - f"{self._create_type_string(variance.type.to_dict())}", - ) + variance_item = f"{variance_direction}{variance_name_camel_case}" + if variance_direction == out: + variance_item = f"{variance_item} sub {self._create_type_string(variance.type.to_dict())}" + variances.append(variance_item) if variances: variance_info = f"<{', '.join(variances)}>" - if constraints: - constraints_info_inner = f",\n{inner_indentations}".join(constraints) - constraints_info = f" where {{\n{inner_indentations}{constraints_info_inner}\n}}" - # Class name - Convert to camelCase and check for keywords class_name = class_.name python_name_info = "" @@ -427,10 +414,7 @@ def _create_parameter_string( # Default value if parameter.is_optional: if isinstance(param_default_value, str): - if parameter_type_data["kind"] == "NamedType" and parameter_type_data["name"] != "str": - default_value = f"{param_default_value}" - else: - default_value = f'"{param_default_value}"' + default_value = f"{param_default_value}" elif isinstance(param_default_value, bool): # Bool values have to be written in lower case default_value = "true" if param_default_value else "false" 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 e444f5f8..47274739 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 @@ -69,7 +69,7 @@ fun illegalParams( @PythonName("lst_2") lst2: List, tpl: Tuple, dct: Map, - `_`: Int = String + `_`: Int = "String" ) // TODO Result type information missing. From 4c72850f0436c96f3af5de13b472e9a9b6bcc657 Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 23 Feb 2024 20:08:40 +0100 Subject: [PATCH 46/49] adjusted test snapshots --- tests/safeds_stubgen/__snapshots__/test_main.ambr | 2 +- .../api_analyzer/__snapshots__/test__get_api.ambr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/safeds_stubgen/__snapshots__/test_main.ambr b/tests/safeds_stubgen/__snapshots__/test_main.ambr index babbc78e..e7a0dfb9 100644 --- a/tests/safeds_stubgen/__snapshots__/test_main.ambr +++ b/tests/safeds_stubgen/__snapshots__/test_main.ambr @@ -685,7 +685,7 @@ }), dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': 'first param', + 'default_value': '"first param"', 'docstring': dict({ 'default_value': '', 'description': '', 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 452501e8..6165c319 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -3263,7 +3263,7 @@ list([ dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': 'String', + 'default_value': '"String"', 'docstring': dict({ 'default_value': '', 'description': '', From e9d87c18b1d117973bceb96fb291f54e5e6aece4 Mon Sep 17 00:00:00 2001 From: Arsam Date: Fri, 23 Feb 2024 20:14:04 +0100 Subject: [PATCH 47/49] linter fix --- src/safeds_stubgen/api_analyzer/_ast_visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 0cb3088d..811773b8 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -729,7 +729,7 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis def _get_parameter_type_and_default_value( initializer: mp_nodes.Expression, ) -> tuple[str | None | int | float, bool]: - default_value = None + default_value: str | None | int | float = None default_is_none = False if initializer is not None: if isinstance(initializer, mp_nodes.NameExpr) and initializer.name not in {"None", "True", "False"}: From 5c2876199dd0e12310d1acd3754f2bcccd50a5bb Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:15:42 +0000 Subject: [PATCH 48/49] style: apply automated linter fixes --- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 8e191050..9f63494f 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -181,7 +181,7 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st variance_direction = { VarianceKind.INVARIANT.name: "", VarianceKind.COVARIANT.name: out, - VarianceKind.CONTRAVARIANT.name: "in " + VarianceKind.CONTRAVARIANT.name: "in ", }[variance.variance.name] # Convert name to camelCase and check for keywords From efde8b4ea1e757c90c8b2edda13588efdc921c47 Mon Sep 17 00:00:00 2001 From: Arsam Date: Sat, 24 Feb 2024 15:53:44 +0100 Subject: [PATCH 49/49] Stubs generator: Removed imports from outside the currently analyzed package --- src/safeds_stubgen/stubs_generator/_generate_stubs.py | 6 +++++- .../test_class_attribute_creation.sdsstub | 2 -- .../test_generate_stubs/test_function_creation.sdsstub | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 8e191050..e8dd07a0 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -657,8 +657,12 @@ def _add_to_imports(self, qname: str) -> None: if class_.endswith(qname_path): qname = class_.replace("/", ".") qname = self._convert_snake_to_camel_case(qname) + self.module_imports.add(qname) + break - self.module_imports.add(qname) + # Todo Currently deactivated, since imports from other packages don't have stubs - see issue #66 + # If the issue is resolved, remove the "self.module_imports.add(qname)" above + # self.module_imports.add(qname) @staticmethod def _callable_type_name_generator() -> Generator: 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 4548f3e4..858101a2 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 @@ -1,8 +1,6 @@ @PythonModule("various_modules_package.attribute_module") package variousModulesPackage.attributeModule -from tests.data.main_package.another_path.another_module import AnotherClass - class AttributesClassA() class AttributesClassB() { 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 47274739..d63a22d9 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 @@ -1,8 +1,6 @@ @PythonModule("various_modules_package.function_module") package variousModulesPackage.functionModule -from tests.data.main_package.another_path.another_module import AnotherClass - // TODO Result type information missing. @Pure @PythonName("public_no_params_no_result")