diff --git a/package-lock.json b/package-lock.json index 2fbb12ea..c4ccb3c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "safe-ds-stub-generator", "version": "0.0.1", "devDependencies": { - "@lars-reimann/prettier-config": "^5.0.0", + "@lars-reimann/prettier-config": "^5.2.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", @@ -63,12 +63,12 @@ } }, "node_modules/@lars-reimann/prettier-config": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@lars-reimann/prettier-config/-/prettier-config-5.0.0.tgz", - "integrity": "sha512-52Ha8xMKpQESiaEzceWgyQb+fuPVD3wl2p6Op1mpLyLj6natjq7Vy8lAmbWS3AbPRjPlJZZHnp/b+sOAOdNqbA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@lars-reimann/prettier-config/-/prettier-config-5.2.1.tgz", + "integrity": "sha512-mZv2ZmWHDoibDb07k+DOFHlcAKNygaWJsvlTPaWW4jPuo/vjULnlk6kNxGnEoFXz9Lk7rIjOUHd/Nk5csWKPEQ==", "dev": true, "peerDependencies": { - "prettier": ">= 2" + "prettier": "^3.2.5" } }, "node_modules/@nodelib/fs.scandir": { @@ -6053,16 +6053,16 @@ } }, "node_modules/prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "peer": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" diff --git a/package.json b/package.json index 4a791496..1614ad03 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "prettier": "@lars-reimann/prettier-config", "devDependencies": { - "@lars-reimann/prettier-config": "^5.0.0", + "@lars-reimann/prettier-config": "^5.2.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", diff --git a/src/safeds_stubgen/api_analyzer/__init__.py b/src/safeds_stubgen/api_analyzer/__init__.py index 575a2556..5a35b0d6 100644 --- a/src/safeds_stubgen/api_analyzer/__init__.py +++ b/src/safeds_stubgen/api_analyzer/__init__.py @@ -31,6 +31,7 @@ NamedType, SetType, TupleType, + TypeVarType, UnionType, ) @@ -63,6 +64,7 @@ "Result", "SetType", "TupleType", + "TypeVarType", "UnionType", "VarianceKind", "WildcardImport", diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index ba308cd6..7593ea8e 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -88,13 +88,12 @@ def enter_moduledef(self, node: mp_nodes.MypyFile) -> None: docstring = definition.expr.value # Create module id to get the full path - id_ = self._create_module_id(node.fullname) + id_ = self._create_module_id(node.path) # If we are checking a package node.name will be the package name, but since we get import information from # the __init__.py file we set the name to __init__ if is_package: name = "__init__" - id_ += f"/{name}" else: name = node.name @@ -151,9 +150,9 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: variance_type = mypy_variance_parser(generic_type.variance) variance_values: sds_types.AbstractType if variance_type == VarianceKind.INVARIANT: - variance_values = sds_types.UnionType([ - mypy_type_to_abstract_type(value) for value in generic_type.values - ]) + variance_values = sds_types.UnionType( + [mypy_type_to_abstract_type(value) for value in generic_type.values], + ) else: variance_values = mypy_type_to_abstract_type(generic_type.upper_bound) @@ -419,14 +418,27 @@ def _infer_type_from_return_stmts(func_node: mp_nodes.FuncDef) -> sds_types.Name func_defn = get_funcdef_definitions(func_node) return_stmts = find_return_stmts_recursive(func_defn) if return_stmts: - # In this case the items of the types set can only be of the class "NamedType" or "TupleType" but we have to - # make a typecheck anyway for the mypy linter. types = set() for return_stmt in return_stmts: - if return_stmt.expr is not None: - type_ = mypy_expression_to_sds_type(return_stmt.expr) - if isinstance(type_, sds_types.NamedType | sds_types.TupleType): - types.add(type_) + if return_stmt.expr is None: # pragma: no cover + continue + + if not isinstance(return_stmt.expr, mp_nodes.CallExpr | mp_nodes.MemberExpr): + # Todo Frage: Parse conditional branches recursively? + # If the return statement is a conditional expression we parse the "if" and "else" branches + if isinstance(return_stmt.expr, mp_nodes.ConditionalExpr): + for conditional_branch in [return_stmt.expr.if_expr, return_stmt.expr.else_expr]: + if conditional_branch is None: # pragma: no cover + continue + + if not isinstance(conditional_branch, mp_nodes.CallExpr | mp_nodes.MemberExpr): + type_ = mypy_expression_to_sds_type(conditional_branch) + if isinstance(type_, sds_types.NamedType | sds_types.TupleType): + types.add(type_) + else: + type_ = mypy_expression_to_sds_type(return_stmt.expr) + if isinstance(type_, sds_types.NamedType | sds_types.TupleType): + types.add(type_) # We have to sort the list for the snapshot tests return_stmt_types = list(types) @@ -434,9 +446,10 @@ def _infer_type_from_return_stmts(func_node: mp_nodes.FuncDef) -> sds_types.Name key=lambda x: (x.name if isinstance(x, sds_types.NamedType) else str(len(x.types))), ) - if len(return_stmt_types) >= 2: + if len(return_stmt_types) == 1: + return return_stmt_types[0] + elif len(return_stmt_types) >= 2: return sds_types.TupleType(types=return_stmt_types) - return return_stmt_types[0] return None @staticmethod @@ -561,11 +574,19 @@ def _create_attribute( qname = getattr(attribute, "fullname", "") # Get node information + type_: sds_types.AbstractType | None = None + node = None if hasattr(attribute, "node"): - if not isinstance(attribute.node, mp_nodes.Var): # pragma: no cover - raise TypeError("node has wrong type") + if not isinstance(attribute.node, mp_nodes.Var): + # In this case we have a TypeVar attribute + attr_name = getattr(attribute, "name", "") + + if not attr_name: # pragma: no cover + raise AttributeError("Expected TypeVar to have attribute 'name'.") - node: mp_nodes.Var = attribute.node + type_ = sds_types.TypeVarType(attr_name) + else: + node = attribute.node else: # pragma: no cover raise AttributeError("Expected attribute to have attribute 'node'.") @@ -576,13 +597,13 @@ def _create_attribute( attribute_type = None # MemberExpr are constructor (__init__) attributes - if isinstance(attribute, mp_nodes.MemberExpr): + if node is not None and isinstance(attribute, mp_nodes.MemberExpr): attribute_type = node.type if isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any): attribute_type = None # NameExpr are class attributes - elif isinstance(attribute, mp_nodes.NameExpr): + elif node is not None and isinstance(attribute, mp_nodes.NameExpr): if not node.explicit_self_type: attribute_type = node.type @@ -600,10 +621,6 @@ def _create_attribute( else: # pragma: no cover raise AttributeError("Could not get argument information for attribute.") - else: # pragma: no cover - raise TypeError("Attribute has an unexpected type.") - - type_ = None # Ignore types that are special mypy any types if attribute_type is not None and not ( isinstance(attribute_type, mp_types.AnyType) and not has_correct_type_of_any(attribute_type.type_of_any) @@ -770,7 +787,7 @@ def _add_reexports(self, module: Module) -> None: def _check_if_qname_in_package(self, qname: str) -> bool: return self.api.package in qname - def _create_module_id(self, qname: str) -> str: + def _create_module_id(self, module_path: str) -> str: """Create an ID for the module object. Creates the module ID while discarding possible unnecessary information from the module qname. @@ -787,19 +804,23 @@ def _create_module_id(self, qname: str) -> str: """ package_name = self.api.package - if package_name not in qname: - raise ValueError("Package name could not be found in the qualified name of the module.") + if package_name not in module_path: + raise ValueError("Package name could not be found in the module path.") # We have to split the qname of the module at the first occurence of the package name and reconnect it while # discarding everything in front of it. This is necessary since the qname could contain unwanted information. - module_id = qname.split(f"{package_name}", 1)[-1] + module_id = module_path.split(package_name, 1)[-1] + module_id = module_id.replace("\\", "/") - if module_id.startswith("."): + if module_id.startswith("/"): module_id = module_id[1:] - # Replaces dots with slashes and add the package name at the start of the id, since we removed it - module_id = f"/{module_id.replace('.', '/')}" if module_id else "" - return f"{package_name}{module_id}" + if module_id.endswith(".py"): + module_id = module_id[:-3] + + if module_id: + return f"{package_name}/{module_id}" + return package_name def _is_public(self, name: str, qualified_name: str) -> bool: if name.startswith("_") and not name.endswith("__"): diff --git a/src/safeds_stubgen/api_analyzer/_get_api.py b/src/safeds_stubgen/api_analyzer/_get_api.py index e20b1c6f..0f578a1c 100644 --- a/src/safeds_stubgen/api_analyzer/_get_api.py +++ b/src/safeds_stubgen/api_analyzer/_get_api.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from pathlib import Path from typing import TYPE_CHECKING import mypy.build as mypy_build @@ -15,6 +14,8 @@ from ._package_metadata import distribution, distribution_version, package_root if TYPE_CHECKING: + from pathlib import Path + from mypy.nodes import MypyFile @@ -39,7 +40,6 @@ def get_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,24 +51,16 @@ 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, package_paths, root) + mypy_trees = _get_mypy_ast(walkable_files, root) for tree in mypy_trees: walker.walk(tree) return callable_visitor.api -def _get_mypy_ast(files: list[str], package_paths: list[Path], root: Path) -> list[MypyFile]: +def _get_mypy_ast(files: list[str], root: Path) -> list[MypyFile]: if not files: raise ValueError("No files found to analyse.") @@ -79,37 +71,28 @@ def _get_mypy_ast(files: list[str], package_paths: list[Path], root: Path) -> li result = mypy_build.build(mypyfiles, options=opt) # Check mypy data key root start - parts = root.parts - graph_keys = list(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 + 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 = [] - for path_key in all_paths: - tree = result.graph[path_key].tree - if tree is not None: + 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 + + if graph_path.endswith("__init__.py"): + init_results.append(tree) + else: results.append(tree) - return results + return init_results + results diff --git a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py index 99e3b4ba..49b6066f 100644 --- a/src/safeds_stubgen/api_analyzer/_mypy_helpers.py +++ b/src/safeds_stubgen/api_analyzer/_mypy_helpers.py @@ -66,6 +66,8 @@ def mypy_type_to_abstract_type( 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], diff --git a/src/safeds_stubgen/api_analyzer/_types.py b/src/safeds_stubgen/api_analyzer/_types.py index cc908413..9d7bbc7e 100644 --- a/src/safeds_stubgen/api_analyzer/_types.py +++ b/src/safeds_stubgen/api_analyzer/_types.py @@ -35,12 +35,13 @@ def from_dict(cls, d: dict[str, Any]) -> AbstractType: return UnionType.from_dict(d) case CallableType.__name__: return CallableType.from_dict(d) + case TypeVarType.__name__: + return TypeVarType.from_dict(d) case _: raise ValueError(f"Cannot parse {d['kind']} value.") @abstractmethod - def to_dict(self) -> dict[str, Any]: - pass + def to_dict(self) -> dict[str, Any]: ... @dataclass(frozen=True) @@ -387,6 +388,21 @@ def __hash__(self) -> int: return hash(frozenset(self.types)) +@dataclass(frozen=True) +class TypeVarType(AbstractType): + name: str + + @classmethod + def from_dict(cls, d: dict[str, str]) -> TypeVarType: + return TypeVarType(d["name"]) + + def to_dict(self) -> dict[str, str]: + return {"kind": self.__class__.__name__, "name": self.name} + + def __hash__(self) -> int: + return hash(frozenset([self.name])) + + # ############################## Utilities ############################## # # def _dismantel_type_string_structure(type_structure: str) -> list: # current_type = "" diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index cbf9e159..6e88bdb2 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -621,6 +621,9 @@ def _create_type_string(self, type_data: dict | None) -> str: else: types.append(f"{literal_type}") return f"literal<{', '.join(types)}>" + # Todo See issue #63 + elif kind == "TypeVarType": + return "" raise ValueError(f"Unexpected type: {kind}") # pragma: no cover diff --git a/tests/data/various_modules_package/attribute_module.py b/tests/data/various_modules_package/attribute_module.py index a2bd7c34..8389c5b3 100644 --- a/tests/data/various_modules_package/attribute_module.py +++ b/tests/data/various_modules_package/attribute_module.py @@ -1,4 +1,4 @@ -from typing import Optional, Final, Literal +from typing import Optional, Final, Literal, TypeVar class AttributesClassA: @@ -62,5 +62,7 @@ def some_func() -> bool: multi_attr_5, multi_attr_6 = ("A", "B") multi_attr_7 = multi_attr_8 = "A" + type_var = TypeVar("type_var") + def __init__(self): self.init_attr: bool = False diff --git a/tests/data/various_modules_package/function_module.py b/tests/data/various_modules_package/function_module.py index 0a1df3c7..45b7e6ed 100644 --- a/tests/data/various_modules_package/function_module.py +++ b/tests/data/various_modules_package/function_module.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional, Literal, Any +from typing import Callable, Optional, Literal, Any, TypeVar class FunctionModuleClassA: @@ -179,6 +179,10 @@ def any_results() -> Any: ... def callable_type(param: Callable[[str], tuple[int, str]]) -> Callable[[int, int], int]: ... +_type_var = TypeVar("_type_var") +def type_var_func(type_var_list: list[_type_var]) -> list[_type_var]: ... + + class FunctionModulePropertiesClass: @property def property_function(self): ... @@ -189,3 +193,7 @@ def property_function_params(self) -> str: ... @property def property_function_infer(self): return "some string" + + +def ret_conditional_statement(): + return 1 if True else False diff --git a/tests/data/various_modules_package/infer_types_module.py b/tests/data/various_modules_package/infer_types_module.py index c6a34d96..3256b6e2 100644 --- a/tests/data/various_modules_package/infer_types_module.py +++ b/tests/data/various_modules_package/infer_types_module.py @@ -82,3 +82,11 @@ def infer_function_2(i=2): return "a" else: return False + + def infer_call_result_1(self): + return self.infer_function_2() + + def _(self) -> str: ... + + def infer_call_result_2(self): + return self._() diff --git a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr index 79aefcae..763203bd 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -984,6 +984,21 @@ 'qname': 'builtins.int', }), }), + dict({ + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/attribute_module/AttributesClassB/type_var', + 'is_public': True, + 'is_static': True, + 'name': 'type_var', + 'type': dict({ + 'kind': 'TypeVarType', + 'name': 'type_var', + }), + }), ]) # --- # name: test_class_attributes[ClassModuleNestedClassE] @@ -1567,6 +1582,64 @@ # --- # name: test_class_methods[InferMyTypes] list([ + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/infer_types_module/InferMyTypes/_', + 'is_class_method': False, + 'is_property': False, + 'is_public': False, + 'is_static': False, + 'name': '_', + 'parameters': list([ + 'various_modules_package/infer_types_module/InferMyTypes/_/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'various_modules_package/infer_types_module/InferMyTypes/_/result_1', + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_1', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'infer_call_result_1', + 'parameters': list([ + 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_1/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_2', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'infer_call_result_2', + 'parameters': list([ + 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_2/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + ]), + }), dict({ 'docstring': dict({ 'description': '', @@ -2176,6 +2249,9 @@ 'various_modules_package/infer_types_module/InferMyTypes/infer_param_types', 'various_modules_package/infer_types_module/InferMyTypes/infer_function', 'various_modules_package/infer_types_module/InferMyTypes/infer_function_2', + 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_1', + 'various_modules_package/infer_types_module/InferMyTypes/_', + 'various_modules_package/infer_types_module/InferMyTypes/infer_call_result_2', ]), 'name': 'InferMyTypes', 'reexported_by': list([ @@ -4277,6 +4353,31 @@ }), ]) # --- +# name: test_function_parameters[type_var_func] + list([ + dict({ + 'assigned_by': 'POSITION_OR_NAME', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/type_var_func/type_var_list', + 'is_optional': False, + 'name': 'type_var_list', + 'type': dict({ + 'kind': 'ListType', + 'types': list([ + dict({ + 'kind': 'TypeVarType', + 'name': '_type_var', + }), + ]), + }), + }), + ]) +# --- # name: test_function_results[abstract_method_params] list([ dict({ @@ -4995,6 +5096,27 @@ }), ]) # --- +# name: test_function_results[type_var_func] + list([ + dict({ + 'docstring': dict({ + 'description': '', + 'type': '', + }), + 'id': 'various_modules_package/function_module/type_var_func/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'ListType', + 'types': list([ + dict({ + 'kind': 'TypeVarType', + 'name': '_type_var', + }), + ]), + }), + }), + ]) +# --- # name: test_function_results[union_dictionary_results] list([ dict({ @@ -5732,6 +5854,25 @@ 'results': list([ ]), }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/function_module/ret_conditional_statement', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'ret_conditional_statement', + 'parameters': list([ + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'various_modules_package/function_module/ret_conditional_statement/result_1', + ]), + }), dict({ 'docstring': dict({ 'description': '', @@ -5816,6 +5957,26 @@ 'various_modules_package/function_module/tuple_results/result_2', ]), }), + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'various_modules_package/function_module/type_var_func', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'type_var_func', + 'parameters': list([ + 'various_modules_package/function_module/type_var_func/type_var_list', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'various_modules_package/function_module/type_var_func/result_1', + ]), + }), dict({ 'docstring': dict({ 'description': '', diff --git a/tests/safeds_stubgen/api_analyzer/test__get_api.py b/tests/safeds_stubgen/api_analyzer/test__get_api.py index 022d53bf..14d5fb61 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"), + ("type_var_func", _function_module_name, "", "plaintext"), ("abstract_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), ("abstract_static_method_params", _abstract_module_name, "AbstractModuleClass", "plaintext"), ("abstract_property_method", _abstract_module_name, "AbstractModuleClass", "plaintext"), @@ -418,6 +419,7 @@ def test_class_methods(module_name: str, class_name: str, docstring_style: str, "arg", "args_type", "callable_type", + "type_var_func", "abstract_method_params", "abstract_static_method_params", "abstract_property_method", @@ -475,6 +477,7 @@ def test_function_parameters( ("literal_results", _function_module_name, "", "plaintext"), ("any_results", _function_module_name, "", "plaintext"), ("callable_type", _function_module_name, "", "plaintext"), + ("type_var_func", _function_module_name, "", "plaintext"), ("instance_method", _function_module_name, "FunctionModuleClassB", "plaintext"), ("static_method_params", _function_module_name, "FunctionModuleClassB", "plaintext"), ("class_method_params", _function_module_name, "FunctionModuleClassB", "plaintext"), @@ -513,6 +516,7 @@ def test_function_parameters( "literal_results", "any_results", "callable_type", + "type_var_func", "instance_method", "static_method_params", "class_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 d4bcc389..396ae3ad 100644 --- a/tests/safeds_stubgen/api_analyzer/test_api_visitor.py +++ b/tests/safeds_stubgen/api_analyzer/test_api_visitor.py @@ -11,20 +11,20 @@ @pytest.mark.parametrize( - ("qname", "expected_id", "package_name"), + ("module_path", "expected_id", "package_name"), [ ( - "some.path.package_name.src.data", + "path\\to\\some\\path\\package_name\\src\\data", "package_name/src/data", "package_name", ), ( - "some.path.package_name", + "path\\to\\some\\path\\package_name", "package_name", "package_name", ), ( - "some.path.no_package", + "path\\to\\some\\path\\no_package", "", "package_name", ), @@ -34,17 +34,17 @@ "package_name", ), ( - "some.package_name.package_name.src.data", + "path\\to\\some\\package_name\\package_name\\src\\data", "package_name/package_name/src/data", "package_name", ), ( - "some.path.package_name.src.package_name", + "path\\to\\some\\path\\package_name\\src\\package_name", "package_name/src/package_name", "package_name", ), ( - "some.package_name.package_name.src.package_name", + "path\\to\\some\\package_name\\package_name\\src\\package_name", "package_name/package_name/src/package_name", "package_name", ), @@ -59,7 +59,7 @@ "Package name twice in qname 3", ], ) -def test__create_module_id(qname: str, expected_id: str, package_name: str) -> None: +def test__create_module_id(module_path: str, expected_id: str, package_name: str) -> None: api = API( distribution="dist_name", package=package_name, @@ -68,8 +68,8 @@ def test__create_module_id(qname: str, expected_id: str, package_name: str) -> N visitor = MyPyAstVisitor(PlaintextDocstringParser(), api) if not expected_id: - with pytest.raises(ValueError, match="Package name could not be found in the qualified name of the module."): - visitor._create_module_id(qname) + with pytest.raises(ValueError, match="Package name could not be found in the module path."): + visitor._create_module_id(module_path) else: - module_id = visitor._create_module_id(qname) + module_id = visitor._create_module_id(module_path) assert module_id == expected_id diff --git a/tests/safeds_stubgen/api_analyzer/test_types.py b/tests/safeds_stubgen/api_analyzer/test_types.py index f0ec2524..c94df331 100644 --- a/tests/safeds_stubgen/api_analyzer/test_types.py +++ b/tests/safeds_stubgen/api_analyzer/test_types.py @@ -16,6 +16,7 @@ ParameterAssignment, SetType, TupleType, + TypeVarType, UnionType, ) from safeds_stubgen.docstring_parsing import AttributeDocstring, ParameterDocstring @@ -237,6 +238,23 @@ def test_literal_type() -> None: assert hash(LiteralType(["a"])) != hash(LiteralType(["b"])) +def test_type_var_type() -> None: + type_ = TypeVarType("_T") + type_dict = { + "kind": "TypeVarType", + "name": "_T", + } + + assert AbstractType.from_dict(type_dict) == type_ + assert TypeVarType.from_dict(type_dict) == type_ + assert type_.to_dict() == type_dict + + assert TypeVarType("a") == TypeVarType("a") + assert hash(TypeVarType("a")) == hash(TypeVarType("a")) + assert TypeVarType("a") != TypeVarType("b") + assert hash(TypeVarType("a")) != hash(TypeVarType("b")) + + def test_final_type() -> None: type_ = FinalType(NamedType("some_type")) type_dict = { diff --git a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py index 0a001ff0..519a5709 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_epydoc_parser.py @@ -24,7 +24,6 @@ files=[ str(Path(_test_dir / "data" / "docstring_parser_package" / "epydoc.py")), ], - package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py index 9db26201..7942444e 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py +++ b/tests/safeds_stubgen/docstring_parsing/test_get_full_docstring.py @@ -19,7 +19,6 @@ files=[ str(Path(_test_dir / "data" / "docstring_parser_package" / "full_docstring.py")), ], - package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py index e8d95a2a..71c4997e 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_googledoc_parser.py @@ -27,7 +27,6 @@ files=[ str(Path(_test_dir / "data" / "docstring_parser_package" / "googledoc.py")), ], - package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py index f517c668..471f2dd8 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_numpydoc_parser.py @@ -28,7 +28,6 @@ files=[ str(Path(_test_dir / "data" / _test_package_name / "numpydoc.py")), ], - package_paths=[], root=Path(_test_dir / "data" / _test_package_name), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py index 87a93c31..9c283de3 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_plaintext_docstring_parser.py @@ -26,7 +26,6 @@ files=[ str(Path(_test_dir / "data" / "docstring_parser_package" / "plaintext.py")), ], - package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py index 8f8fff51..7c82e831 100644 --- a/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py +++ b/tests/safeds_stubgen/docstring_parsing/test_restdoc_parser.py @@ -24,7 +24,6 @@ files=[ str(Path(_test_dir / "data" / "docstring_parser_package" / "restdoc.py")), ], - package_paths=[], root=Path(_test_dir / "data" / "docstring_parser_package"), )[0] diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr index 4bd5b29e..a10871d0 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs.ambr @@ -46,6 +46,7 @@ from typing import Optional from typing import Final from typing import Literal + from typing import TypeVar class AttributesClassA() @@ -131,6 +132,8 @@ static attr multiAttr7: String @PythonName("multi_attr_8") static attr multiAttr8: String + @PythonName("type_var") + static attr typeVar @PythonName("init_attr") attr initAttr: Boolean @@ -228,6 +231,7 @@ from typing import Optional from typing import Literal from typing import Any + from typing import TypeVar // TODO Result type information missing. @Pure @@ -456,6 +460,16 @@ param: (a: String) -> (b: Int, c: String) ) -> result1: (a: Int, b: Int) -> c: Int + @Pure + @PythonName("type_var_func") + fun typeVarFunc( + @PythonName("type_var_list") typeVarList: List<> + ) -> result1: List<> + + @Pure + @PythonName("ret_conditional_statement") + fun retConditionalStatement() -> result1: union + class FunctionModuleClassA() // TODO Some parameter have no type information. @@ -582,6 +596,16 @@ static fun inferFunction2( i: Int = 2 ) -> result1: union + + // TODO Result type information missing. + @Pure + @PythonName("infer_call_result_1") + fun inferCallResult1() + + // TODO Result type information missing. + @Pure + @PythonName("infer_call_result_2") + fun inferCallResult2() } '''