From 216e1790cecced0d6a76313cb2ed2f725baf684f Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Sun, 25 Feb 2024 10:01:41 +0100 Subject: [PATCH] feat: Rework import generation for stubs. (#50) Closes #38 Closes #24 ### Summary of Changes - Reworked the import generation for Stubs. Imports should now be correctly generated. (#38) - Aliases are collected with mypy and are now used to resolve alias origins (#24) #### Other changes - Added a "// Todo" for the generated Stubs, if an internal class is used as a type. - Added "// Todo Safe-DS does not support set types" if a set object is used. - If booleans where used in Literal Types the first letter was capitalized in stubs, which it should not be. - Removed the "where" part and lower limits of the Stubs for constrains --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- src/safeds_stubgen/api_analyzer/_api.py | 1 + .../api_analyzer/_ast_visitor.py | 347 +++++++++++++----- src/safeds_stubgen/api_analyzer/_get_api.py | 192 +++++++--- .../api_analyzer/_mypy_helpers.py | 153 +------- src/safeds_stubgen/api_analyzer/_types.py | 8 +- .../stubs_generator/_generate_stubs.py | 292 ++++++++------- .../aliasing/aliasing_module_1.py | 24 ++ .../aliasing/aliasing_module_2.py | 24 ++ .../aliasing/aliasing_module_3.py | 7 + .../attribute_module.py | 4 + .../function_module.py | 8 + .../various_modules_package/import_module.py | 17 +- .../__snapshots__/test_main.ambr | 6 +- .../__snapshots__/test__get_api.ambr | 262 ++++++++++--- .../api_analyzer/test__get_api.py | 4 + .../api_analyzer/test_api_visitor.py | 2 +- .../safeds_stubgen/api_analyzer/test_types.py | 123 ++++--- .../docstring_parsing/test_epydoc_parser.py | 12 +- .../test_get_full_docstring.py | 12 +- .../test_googledoc_parser.py | 12 +- .../docstring_parsing/test_numpydoc_parser.py | 12 +- .../test_plaintext_docstring_parser.py | 12 +- .../docstring_parsing/test_restdoc_parser.py | 12 +- .../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 | 18 +- .../test_enum_creation.sdsstub | 2 - .../test_function_creation.sdsstub | 28 +- .../test_import_creation.sdsstub | 19 +- .../test_type_inference.sdsstub | 2 +- .../test_variance_creation.sdsstub | 9 +- .../stubs_generator/test_generate_stubs.py | 25 +- 34 files changed, 1098 insertions(+), 609 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 create mode 100644 tests/data/various_modules_package/aliasing/aliasing_module_3.py 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/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 7593ea8e..811773b8 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -1,10 +1,12 @@ from __future__ import annotations +from collections import defaultdict +from copy import deepcopy from types import NoneType from typing import TYPE_CHECKING +import mypy.nodes as mp_nodes import mypy.types as mp_types -from mypy import nodes as mp_nodes import safeds_stubgen.api_analyzer._types as sds_types @@ -32,22 +34,25 @@ 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.api_analyzer._types import AbstractType from safeds_stubgen.docstring_parsing import AbstractDocstringParser, ResultDocstring 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.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 + self.mypy_file: mp_nodes.MypyFile | None = 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] = [] @@ -151,10 +156,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( @@ -165,8 +170,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: + _, 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) @@ -386,7 +401,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) @@ -566,13 +581,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 type_: sds_types.AbstractType | None = None node = None @@ -590,6 +598,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 @@ -603,30 +615,29 @@ 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 + # 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) ): # 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,12 +664,13 @@ 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 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): @@ -672,43 +684,19 @@ 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 - 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] @@ -737,6 +725,36 @@ def _parse_parameter_data(self, node: mp_nodes.FuncDef, function_id: str) -> lis return arguments + @staticmethod + def _get_parameter_type_and_default_value( + initializer: mp_nodes.Expression, + ) -> tuple[str | None | int | float, bool]: + 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"}: + # 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, 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.") + + default_is_none = default_value is None + return default_value, default_is_none + # #### Reexport utilities def _get_reexported_by(self, name: str) -> list[Module]: @@ -765,27 +783,183 @@ 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( + self, + mypy_type: mp_types.Instance | mp_types.ProperType | mp_types.Type, + 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.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], + 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: + # 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[union-attr] + 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"}: + 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] + + 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("/", ".") + 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) + + # 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) -> tuple[str, str]: + module = self.__declaration_stack[0] - # 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: - return self.api.package in qname + # 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 = "" + 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 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) + + # 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 + else: + # In this case some types where defined in multiple modules with the same names. + for alias_qname in qnames: + # 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 + else: + 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.") + + return name, qname + + @staticmethod + def _search_alias_in_qualified_imports( + qualified_imports: list[QualifiedImport], + alias_name: 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 + return "", "" def _create_module_id(self, module_path: str) -> str: """Create an ID for the module object. @@ -831,13 +1005,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 0f578a1c..e3fa0e8d 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -1,10 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from collections import defaultdict +from pathlib import Path 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 @@ -13,11 +16,6 @@ from ._ast_walker import ASTWalker from ._package_metadata import distribution, distribution_version, package_root -if TYPE_CHECKING: - from pathlib import Path - - from mypy.nodes import MypyFile - def get_api( package_name: str, @@ -29,17 +27,8 @@ 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"): logging.info( "Working on file {posix_path}", @@ -51,48 +40,151 @@ def get_api( logging.info("Skipping test file") continue + # Check if the current file is an init file + if file_path.parts[-1] == "__init__.py": + # if a directory contains an __init__.py file it's a package + package_paths.append( + file_path.parent, + ) + continue + walkable_files.append(str(file_path)) - mypy_trees = _get_mypy_ast(walkable_files, 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], 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) - # Check mypy data key root start - graphs = result.graph - graph_keys = list(graphs.keys()) - root_path = str(root) - - # Get the needed data from mypy. The __init__ files need to be checked first, since we have to get the - # reexported data for the packages first - results = [] - init_results = [] - for graph_key in graph_keys: - graph = graphs[graph_key] - graph_path = graph.abspath - - if graph_path is None: # pragma: no cover - raise ValueError("Could not parse path of a module.") - - tree = graph.tree - if tree is None or root_path not in graph_path or not graph_path.endswith(".py"): - continue + # 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) - if graph_path.endswith("__init__.py"): - init_results.append(tree) - else: - results.append(tree) - return init_results + results +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(build_result.graph.keys()) + root_start_after = -1 + for i in range(len(parts)): + if ".".join(parts[i:]) in graph_keys: + root_start_after = i + break + + # Create the keys for getting the corresponding data + packages = [ + ".".join( + package_path.parts[root_start_after:], + ).replace(".py", "") + for package_path in package_paths + ] + + modules = [ + ".".join( + Path(file).parts[root_start_after:], + ).replace(".py", "") + for file in files + ] + + # Get the needed data from mypy. The packages need to be checked first, since we have + # to get the reexported data first + all_paths = packages + modules + + asts = [] + for path_key in all_paths: + tree = build_result.graph[path_key].tree + if tree is not None: + asts.append(tree) + + return asts + + +def _get_aliases(result_types: dict, package_name: str) -> 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): + in_package = False + name = "" + + 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 hasattr(bound_args[0], "type"): + 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 + + if not fullname: + continue + + in_package = package_name in fullname + 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) and hasattr(type_value.bound_args[0], "type"): + 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): + 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[name].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 49b6066f..c137c9dc 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,101 +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.TypeVarType): - return sds_types.TypeVarType(mypy_type.name) - 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") - 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) - 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) - 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 @@ -183,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, } @@ -200,33 +101,13 @@ 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 -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.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": @@ -235,31 +116,7 @@ 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 - # # 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/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index 9d7bbc7e..8f69db37 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -47,7 +47,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: @@ -453,14 +453,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) # @@ -518,7 +518,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] = [] # diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 6e88bdb2..e99d91b7 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -6,17 +6,16 @@ from safeds_stubgen.api_analyzer import ( API, + Attribute, Class, Enum, Function, Module, Parameter, ParameterAssignment, - QualifiedImport, Result, UnionType, VarianceKind, - WildcardImport, ) if TYPE_CHECKING: @@ -42,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) - generator = StubsStringGenerator(convert_identifiers) + stubs_generator = StubsStringGenerator(api, convert_identifiers) for module in modules: module_name = module.name @@ -50,7 +49,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: @@ -79,27 +78,27 @@ class StubsStringGenerator: method. """ - def __init__(self, convert_identifiers: bool) -> None: - self._current_todo_msgs: set[str] = set() + def __init__(self, api: API, convert_identifiers: bool) -> None: + self.module_imports: set[str] = set() + self.api = api 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: @@ -115,7 +114,28 @@ 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}") + + # We have to sort for the snapshot tests + import_strings.sort() + + 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" @@ -141,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: @@ -151,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 = "" @@ -200,16 +211,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 +280,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 +291,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 +376,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: @@ -391,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" @@ -452,40 +472,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}" @@ -534,6 +520,12 @@ def _create_type_string(self, type_data: dict | None) -> str: case "None": 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"]) @@ -561,6 +553,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) @@ -605,7 +600,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)}>" @@ -618,6 +613,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)}>" @@ -629,6 +629,41 @@ 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, 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.") + + 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: + # 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) + break + + # 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: """Generate a name for callable type parameters starting from 'a' until 'zz'.""" @@ -646,7 +681,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.", @@ -657,6 +693,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 ] @@ -669,9 +706,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) 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..8f752944 --- /dev/null +++ b/tests/data/various_modules_package/aliasing/aliasing_module_1.py @@ -0,0 +1,24 @@ +from aliasing_module_2 import AliasingModule2ClassA as AliasModule2 +from aliasing_module_3 import ImportMeAliasingModuleClass as ImportMeAlias + + +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, 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 new file mode 100644 index 00000000..6052e2e1 --- /dev/null +++ b/tests/data/various_modules_package/aliasing/aliasing_module_2.py @@ -0,0 +1,24 @@ +from aliasing_module_1 import AliasingModuleClassC + + +class AliasingModule2ClassA: + ... + + +class AliasingModuleClassB: + ... + + +class ImportMeAliasingModuleClass: + ... + + +some_alias_b = AliasingModuleClassB +ImportMeAlias = AliasingModuleClassC + + +class AliasingModuleClassC: + typed_alias_attr: some_alias_b + typed_alias_infer = ImportMeAlias + + 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/data/various_modules_package/attribute_module.py b/tests/data/various_modules_package/attribute_module.py index 8389c5b3..0b021cc3 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, TypeVar +from tests.data.main_package.another_path.another_module import AnotherClass class AttributesClassA: @@ -62,6 +63,9 @@ 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 + type_var = TypeVar("type_var") def __init__(self): diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 45b7e6ed..60c3cad6 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, TypeVar +from tests.data.main_package.another_path.another_module import AnotherClass class FunctionModuleClassA: @@ -82,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", ): ... @@ -179,6 +181,12 @@ 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): ... + + +def result_from_outside_the_package() -> AnotherClass: ... + + _type_var = TypeVar("_type_var") def type_var_func(type_var_list: list[_type_var]) -> list[_type_var]: ... 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..e7a0dfb9 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([ ]), @@ -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([ ]), @@ -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 763203bd..6165c319 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,43 @@ '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': 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({ + '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': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), dict({ 'docstring': dict({ 'default_value': '', @@ -156,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', }), }), }), @@ -249,7 +286,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), }), }), @@ -271,12 +308,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -300,12 +337,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -404,12 +441,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -432,12 +469,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -458,7 +495,7 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'UnionType', @@ -466,12 +503,12 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -776,7 +813,7 @@ dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), ]), }), @@ -828,12 +865,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), ]), }), @@ -854,7 +891,7 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'UnionType', @@ -862,12 +899,12 @@ dict({ 'kind': 'NamedType', 'name': 'AttributesClassA', - 'qname': '', + 'qname': 'various_modules_package.attribute_module.AttributesClassA', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -906,7 +943,7 @@ dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), ]), }), @@ -3226,7 +3263,7 @@ list([ dict({ 'assigned_by': 'POSITION_OR_NAME', - 'default_value': 'String', + 'default_value': '"String"', 'docstring': dict({ 'default_value': '', 'description': '', @@ -3241,6 +3278,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': 'builtins.Any', + }), + 'kind': 'DictType', + 'value_type': dict({ + 'kind': 'NamedType', + 'name': 'Any', + 'qname': 'builtins.Any', + }), + }), + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -3258,12 +3320,12 @@ dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), ]), }), @@ -3285,17 +3347,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', }), ]), }), @@ -3492,6 +3554,40 @@ }), ]) # --- +# 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': dict({ + 'kind': 'NamedType', + 'name': 'AnotherClass', + 'qname': 'tests.data.main_package.another_path.another_module.AnotherClass', + }), + }), + 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({ @@ -3526,20 +3622,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', @@ -3602,7 +3694,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), dict({ @@ -4393,12 +4485,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), ]), }), @@ -4464,7 +4556,7 @@ 'type': dict({ 'kind': 'NamedType', 'name': 'Any', - 'qname': '', + 'qname': 'builtins.Any', }), }), ]) @@ -4564,13 +4656,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', }), }), }), @@ -4643,13 +4735,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', }), }), }), @@ -4670,12 +4762,12 @@ dict({ 'kind': 'NamedType', 'name': 'int', - 'qname': '', + 'qname': 'builtins.int', }), dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), ]), }), @@ -4697,12 +4789,12 @@ dict({ 'kind': 'NamedType', 'name': 'str', - 'qname': '', + 'qname': 'builtins.str', }), dict({ 'kind': 'NamedType', 'name': 'bool', - 'qname': '', + 'qname': 'builtins.bool', }), ]), }), @@ -5023,6 +5115,23 @@ }), ]) # --- +# 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] list([ dict({ @@ -5563,6 +5672,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([ @@ -5722,6 +5832,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': '', @@ -5854,6 +5984,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': '', @@ -6080,23 +6229,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': 'ClMCC', + 'qualified_name': 'class_module.ClassModuleClassC', + }), + dict({ + 'alias': 'ClMCD', + 'qualified_name': 'class_module.ClassModuleClassD', }), dict({ - 'alias': 'static', - 'qualified_name': 'mypy', + '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/api_analyzer/test__get_api.py b/tests/safeds_stubgen/api_analyzer/test__get_api.py index 14d5fb61..66606361 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"), ("type_var_func", _function_module_name, "", "plaintext"), ("abstract_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), ("abstract_static_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), @@ -419,6 +420,7 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, "arg", "args_type", "callable_type", + "param_from_outside_the_package", "type_var_func", "abstract_method_params", "abstract_static_method_params", @@ -477,6 +479,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"), ("type_var_func", _function_module_name, "", "plaintext"), ("instance_method", _function_module_name, "FunctionModuleClassB", "plaintext"), ("static_method_params", _function_module_name, "FunctionModuleClassB", "plaintext"), @@ -516,6 +519,7 @@ def test_function_parameters( "literal_results", "any_results", "callable_type", + "result_from_outside_the_package", "type_var_func", "instance_method", "static_method_params", diff --git a/tests/safeds_stubgen/api_analyzer/test_api_visitor.py b/tests/safeds_stubgen/api_analyzer/test_api_visitor.py index 396ae3ad..afc46fac 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(module_path: str, expected_id: str, package_name: str 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 module path."): visitor._create_module_id(module_path) diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index c94df331..010b60bf 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -30,7 +30,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"}) @@ -41,10 +41,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", @@ -65,7 +65,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 @@ -108,7 +108,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": ""}], @@ -118,16 +118,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", @@ -148,48 +148,61 @@ 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"}, ], }, } @@ -198,27 +211,34 @@ 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: @@ -256,7 +276,7 @@ def test_type_var_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": ""}, @@ -266,27 +286,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 519a5709..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_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, EpydocParser, @@ -20,10 +20,12 @@ # 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 7942444e..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_ast +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 @@ -15,10 +15,12 @@ # 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 71c4997e..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_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -23,10 +23,12 @@ # 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 471f2dd8..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_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -24,10 +24,12 @@ # 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 9c283de3..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_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -22,10 +22,12 @@ # 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 7c82e831..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_ast +from safeds_stubgen.api_analyzer._get_api import _get_mypy_asts, _get_mypy_build from safeds_stubgen.docstring_parsing import ( ClassDocstring, FunctionDocstring, @@ -20,10 +20,12 @@ # 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/__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..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,11 +1,6 @@ @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 - class AttributesClassA() class AttributesClassB() { @@ -42,13 +37,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> @@ -75,9 +74,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 +89,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..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,18 +1,13 @@ @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 - // TODO Result type information missing. @Pure @PythonName("public_no_params_no_result") 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 @@ -37,6 +32,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 @@ -70,7 +66,8 @@ fun illegalParams( lst: List, @PythonName("lst_2") lst2: List, tpl: Tuple, - `_`: Int = String + dct: Map, + `_`: Int = "String" ) // TODO Result type information missing. @@ -96,7 +93,7 @@ fun paramPosition( self, a, b: Boolean, - c: FunctionModuleClassA = FunctionModuleClassA, + c, d, e: Int = 1 ) @@ -207,10 +204,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") @@ -234,6 +233,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..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,6 +1,21 @@ @PythonModule("various_modules_package.import_module") package variousModulesPackage.importModule -import mypy as `static` +from variousModulesPackage.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 29f1c1c8..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 @@ -68,48 +68,46 @@ 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) -# Todo -def test_docstring_creation() -> None: ... +@pytest.mark.parametrize("file_name", ["aliasing_module_1", "aliasing_module_2", "aliasing_module_3"]) +def test_alias_creation(file_name: str, snapshot_sds_stub: SnapshotAssertion) -> None: + file_data = "" + 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_sds_stub @pytest.mark.parametrize( @@ -136,5 +134,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( + 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