From 48c5367d68a34c6693dee9660a4d696520e6aec0 Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Sat, 4 May 2024 14:18:03 +0200 Subject: [PATCH] fix: Fixed a bug where imports would not check reexports for shortest path (#112) Closes #82 ### Summary of Changes Fixed the bug where imports would not use the shortest path from existing reexports. --------- 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 | 56 +-- .../stubs_generator/_generate_stubs.py | 353 +++++++++++++----- .../data/various_modules_package/__init__.py | 3 + .../_reexport_module_2.py | 2 +- .../_reexport_module_3.py | 4 + .../file_creation/__init__.py | 1 + .../file_creation/module_1.py | 12 + .../file_creation/package_1/__init__.py | 1 + .../file_creation/package_1/module_5.py | 7 + .../__snapshots__/test__get_api.ambr | 50 ++- .../api_analyzer/test__get_api.py | 4 +- ...neration.test_stub_creation[ClMCD].sdsstub | 23 ++ ...stub_creation[FourthReexportClass].sdsstub | 4 + ...Generation.test_stub_creation[Lv1].sdsstub | 8 + ...Generation.test_stub_creation[Lv2].sdsstub | 7 + ...[ReexportedInAnotherPackageClass2].sdsstub | 4 + ...n[ReexportedInAnotherPackageClass].sdsstub | 4 + ...on.test_stub_creation[Reexported].sdsstub} | 0 ...eation[ThirdReexportedClassAsType].sdsstub | 4 + ..._stub_creation[_reexport_module_4].sdsstub | 12 - ...n.test_stub_creation[class_module].sdsstub | 21 -- ...on.test_stub_creation[enum_module].sdsstub | 4 +- ....test_stub_creation[import_module].sdsstub | 2 +- ...ation.test_stub_creation[module_5].sdsstub | 5 + ..._stub_creation[public_reexported].sdsstub} | 0 ...ration.test_stub_creation[reex_1].sdsstub} | 0 ...eexported_from_another_package_3].sdsstub} | 0 ...b_creation[reexported_function_2].sdsstub} | 2 - ...b_creation[reexported_function_3].sdsstub} | 0 ...ation[reexported_function_4_alias].sdsstub | 6 + ...ted_in_another_package_function2].sdsstub} | 2 - ...rted_in_another_package_function].sdsstub} | 2 - ...tub_creation[two_times_reexported].sdsstub | 6 + .../stubs_generator/test_generate_stubs.py | 60 ++- 35 files changed, 473 insertions(+), 197 deletions(-) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ClMCD].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[FourthReexportClass].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv1].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv2].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass2].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_module_3].sdsstub => TestStubFileGeneration.test_stub_creation[Reexported].sdsstub} (100%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ThirdReexportedClassAsType].sdsstub delete mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_module_6].sdsstub => TestStubFileGeneration.test_stub_creation[public_reexported].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexport_module_1].sdsstub => TestStubFileGeneration.test_stub_creation[reex_1].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_3].sdsstub => TestStubFileGeneration.test_stub_creation[reexported_from_another_package_3].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub => TestStubFileGeneration.test_stub_creation[reexported_function_2].sdsstub} (84%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexport_module_3].sdsstub => TestStubFileGeneration.test_stub_creation[reexported_function_3].sdsstub} (100%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_4_alias].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_2].sdsstub => TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function2].sdsstub} (85%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{TestStubFileGeneration.test_stub_creation[_reexported_from_another_package].sdsstub => TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function].sdsstub} (85%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[two_times_reexported].sdsstub diff --git a/src/safeds_stubgen/api_analyzer/_api.py b/src/safeds_stubgen/api_analyzer/_api.py index f485e131..26f07327 100644 --- a/src/safeds_stubgen/api_analyzer/_api.py +++ b/src/safeds_stubgen/api_analyzer/_api.py @@ -48,7 +48,6 @@ def __init__(self, distribution: str, package: str, version: str) -> None: self.enum_instances: dict[str, EnumInstance] = {} self.attributes_: dict[str, Attribute] = {} self.parameters_: dict[str, Parameter] = {} - self.reexport_map: dict[str, set[Module]] = defaultdict(set) def add_module(self, module: Module) -> None: diff --git a/src/safeds_stubgen/api_analyzer/_ast_visitor.py b/src/safeds_stubgen/api_analyzer/_ast_visitor.py index 3fcdfccb..17492c30 100644 --- a/src/safeds_stubgen/api_analyzer/_ast_visitor.py +++ b/src/safeds_stubgen/api_analyzer/_ast_visitor.py @@ -129,8 +129,7 @@ def leave_moduledef(self, _: mp_nodes.MypyFile) -> None: self.api.add_module(module) def enter_classdef(self, node: mp_nodes.ClassDef) -> None: - name = node.name - id_ = self._create_id_from_stack(name) + id_ = self._create_id_from_stack(node.name) # Get docstring docstring = self.docstring_parser.get_class_documentation(node) @@ -202,7 +201,9 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: superclasses.append(superclass_qname) # Get reexported data - reexported_by = self._get_reexported_by(name) + reexported_by = self._get_reexported_by(node.fullname) + # Sort for snapshot tests + reexported_by.sort(key=lambda x: x.id) # Get constructor docstring definitions = get_classdef_definitions(node) @@ -215,9 +216,9 @@ def enter_classdef(self, node: mp_nodes.ClassDef) -> None: # Remember class, so we can later add methods class_ = Class( id=id_, - name=name, + name=node.name, superclasses=superclasses, - is_public=self._is_public(node.name, name), + is_public=self._is_public(node.name, node.fullname), docstring=docstring, reexported_by=reexported_by, constructor_fulldocstring=constructor_fulldocstring, @@ -266,7 +267,9 @@ def enter_funcdef(self, node: mp_nodes.FuncDef) -> None: results = self._parse_results(node, function_id, result_docstrings) # Get reexported data - reexported_by = self._get_reexported_by(name) + reexported_by = self._get_reexported_by(node.fullname) + # Sort for snapshot tests + reexported_by.sort(key=lambda x: x.id) # Create and add Function to stack function = Function( @@ -845,25 +848,25 @@ def _get_parameter_type_and_default_value( # #### Reexport utilities - def _get_reexported_by(self, name: str) -> list[Module]: - # Get the uppermost module and the path to the current node - parents = [] - parent = None - i = 1 - while not isinstance(parent, Module): - parent = self.__declaration_stack[-i] - if isinstance(parent, list): # pragma: no cover - continue - parents.append(parent.name) - i += 1 - path = [*list(reversed(parents)), name] + def _get_reexported_by(self, qname: str) -> list[Module]: + path = qname.split(".") # Check if there is a reexport entry for each item in the path to the current module reexported_by = set() for i in range(len(path)): - reexport_name = ".".join(path[: i + 1]) - if reexport_name in self.api.reexport_map: - for mod in self.api.reexport_map[reexport_name]: + reexport_name_forward = ".".join(path[: i + 1]) + if reexport_name_forward in self.api.reexport_map: + for mod in self.api.reexport_map[reexport_name_forward]: + reexported_by.add(mod) + + reexport_name_backward = ".".join(path[-i - 1 :]) + if reexport_name_backward in self.api.reexport_map: + for mod in self.api.reexport_map[reexport_name_backward]: + reexported_by.add(mod) + + reexport_name_backward_whitelist = f"{'.'.join(path[-2 - i:-1])}.*" + if reexport_name_backward_whitelist in self.api.reexport_map: + for mod in self.api.reexport_map[reexport_name_backward_whitelist]: reexported_by.add(mod) return list(reexported_by) @@ -875,7 +878,7 @@ def _add_reexports(self, module: Module) -> None: for wildcard_import in module.wildcard_imports: name = wildcard_import.module_name - self.api.reexport_map[name].add(module) + self.api.reexport_map[f"{name}.*"].add(module) # #### Misc. utilities def mypy_type_to_abstract_type( @@ -1130,7 +1133,12 @@ def _check_publicity_in_reexports(self, name: str, qname: str, parent: Module | package_id = "/".join(module_qname.split(".")[:-1]) for reexported_key in self.api.reexport_map: - module_is_reexported = reexported_key in {module_name, module_qname} + module_is_reexported = reexported_key in { + module_name, + module_qname, + f"{module_name}.*", + f"{module_qname}.*", + } # Check if the function/class/module is reexported if reexported_key.endswith(name) or module_is_reexported: @@ -1140,7 +1148,7 @@ def _check_publicity_in_reexports(self, name: str, qname: str, parent: Module | # We have to check if it's the correct reexport with the ID is_from_same_package = reexport_source.id == package_id - is_from_another_package = reexported_key in {qname, module_qname} + is_from_another_package = reexported_key.rstrip(".*") in {qname, module_qname} if not is_from_same_package and not is_from_another_package: continue diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 8f06bf89..63832a4e 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from collections import defaultdict from enum import IntEnum from pathlib import Path from types import NoneType @@ -38,7 +39,7 @@ class NamingConvention(IntEnum): def generate_stub_data( stubs_generator: StubsStringGenerator, out_path: Path, -) -> list[tuple[Path, str, str]]: +) -> list[tuple[Path, str, str, bool]]: """Generate Safe-DS stubs. Generates stub data from an API object. @@ -53,10 +54,11 @@ def generate_stub_data( Returns ------- - A list of tuples, which are 1. the path of the stub file, 2. the name of the stub file and 3. its content. + A list of tuples, which are 1. the path of the stub file, 2. the name of the stub file, 3. its content and 4. if + it's a package file (created through init reexports). """ api = stubs_generator.api - stubs_data: list[tuple[Path, str, str]] = [] + stubs_data: list[tuple[Path, str, str, bool]] = [] for module in api.modules.values(): if module.name == "__init__": continue @@ -78,27 +80,50 @@ def generate_stub_data( if len(splitted_text) <= 2 or (len(splitted_text) == 3 and splitted_text[1].startswith("package ")): continue - module_dir = Path(out_path / package_info.replace(".", "/")) - stubs_data.append((module_dir, module.name, module_text)) - return stubs_data + shortest_path, alias = _get_shortest_public_reexport( + reexport_map=api.reexport_map, + name=module.name, + qname="", + is_module=True, + ) + if shortest_path: + shortest_path = shortest_path.replace(".", "/") + + module_id = shortest_path if shortest_path else package_info.replace(".", "/") + module_name = alias if alias else module.name + + module_dir = Path(out_path / module_id) + stubs_data.append((module_dir, module_name, module_text, False)) + + reexport_module_data = stubs_generator.create_reexport_module_strings(out_path=out_path) + + return stubs_data + reexport_module_data def create_stub_files( stubs_generator: StubsStringGenerator, - stubs_data: list[tuple[Path, str, str]], + stubs_data: list[tuple[Path, str, str, bool]], out_path: Path, ) -> None: naming_convention = stubs_generator.naming_convention - for module_dir, module_name, module_text in stubs_data: - log_msg = f"Creating stub file for {module_dir}" + # A "package module" is a module which is created though the reexported classes and functions in the __init__.py + for module_dir, module_name, module_text, is_package_module in stubs_data: + if is_package_module: + # Cut out the last part of the path, since we don't want "path/to/package/package.sdsstubs" but + # "path/to/package.sdsstubs" so that "package" is not doubled + corrected_module_dir = Path("/".join(module_dir.parts[:-1])) + else: + corrected_module_dir = module_dir + + log_msg = f"Creating stub file for {corrected_module_dir}" logging.info(log_msg) # Create module dir - module_dir.mkdir(parents=True, exist_ok=True) + corrected_module_dir.mkdir(parents=True, exist_ok=True) # Create and open module file public_module_name = module_name.lstrip("_") - file_path = Path(module_dir / f"{public_module_name}.sdsstub") + file_path = Path(corrected_module_dir / f"{public_module_name}.sdsstub") Path(file_path).touch() with file_path.open("w") as f: @@ -176,44 +201,112 @@ class StubsStringGenerator: """ def __init__(self, api: API, convert_identifiers: bool) -> None: + self.module_id: str = "" + self.class_generics: list = [] + self.module_imports: set[str] = set() + self.api = api self.naming_convention = NamingConvention.SAFE_DS if convert_identifiers else NamingConvention.PYTHON self.classes_outside_package: set[str] = set() + self.reexport_modules: dict[str, list[Class | Function]] = defaultdict(list) def __call__(self, module: Module) -> tuple[str, str]: - self.module_imports: set[str] = set() + self.module_id = module.id + self.class_generics = [] + self.module_imports = set() + self._current_todo_msgs: set[str] = set() - self.module = module - self.class_generics: list = [] - return self._create_module_string() + return self._create_module_string(module) + + def create_reexport_module_strings(self, out_path: Path) -> list[tuple[Path, str, str, bool]]: + module_data = [] + for module_id in self.reexport_modules: + elements: list[Class | Function] = self.reexport_modules[module_id] + + # We sort for the snapshot tests + elements.sort(key=lambda x: x.name) + + for element in elements: + # Reset the objects that we normally would reset in the __call__ + self.module_imports = set() + self.class_generics = [] + + module_name = element.name + self.module_id = f"{module_id}/{module_name}" + + # Create module header + package_info = ".".join(self.module_id.split("/")[:-1]) + package_info_camel_case = _convert_name_to_convention(package_info, self.naming_convention) + module_name_info = "" + if package_info != package_info_camel_case: + module_name_info = f'@PythonModule("{package_info}")\n' + module_header = f"{module_name_info}package {package_info_camel_case}\n" + + # Create body text + if isinstance(element, Class): + module_text = f"\n{self._create_class_string(class_=element, in_reexport_module=True)}\n" + elif isinstance(element, Function): + module_text = f"\n{self._create_function_string(function=element, in_reexport_module=True)}\n" + else: # pragma: no cover + msg = f"Could not create a module for {element.id}. Unsupported type {type(element)}." + logging.warning(msg) + continue + + # 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() + module_text = module_header + module_text + module_data.append((Path(out_path / self.module_id), module_name, module_text, True)) + + return module_data + + def _create_module_string(self, module: Module) -> tuple[str, str]: + module_text = "" - def _create_module_string(self) -> tuple[str, str]: # Create package info - package_info = self._get_shortest_public_reexport() + package_info, _ = _get_shortest_public_reexport( + reexport_map=self.api.reexport_map, + name=module.name, + qname="", + is_module=True, + ) + + in_reexport_module = bool(package_info) + + if not package_info: + package_info = ".".join(module.id.split("/")) + package_info_camel_case = _convert_name_to_convention(package_info, self.naming_convention) module_name_info = "" - module_text = "" if package_info != package_info_camel_case: module_name_info = f'@PythonModule("{package_info}")\n' module_header = f"{module_name_info}package {package_info_camel_case}\n" # Create docstring - docstring = self._create_sds_docstring_description(self.module.docstring, "") + docstring = self._create_sds_docstring_description(module.docstring, "") if docstring: docstring += "\n" # Create global functions and properties - for function in self.module.global_functions: + for function in module.global_functions: if function.is_public: - module_text += f"\n{self._create_function_string(function, is_method=False)}\n" + function_string = self._create_function_string( + function=function, + is_method=False, + in_reexport_module=in_reexport_module, + ) + if function_string: + module_text += f"\n{function_string}\n" # Create classes, class attr. & class methods - for class_ in self.module.classes: + for class_ in module.classes: if class_.is_public and not class_.inherits_from_exception: - module_text += f"\n{self._create_class_string(class_)}\n" + class_string = self._create_class_string(class_=class_, in_reexport_module=in_reexport_module) + if class_string: + module_text += f"\n{class_string}\n" # Create enums & enum instances - for enum in self.module.enums: + for enum in module.enums: module_text += f"\n{self._create_enum_string(enum)}\n" # Create imports - We have to create them last, since we have to check all used types in this module first @@ -246,7 +339,11 @@ def _create_imports_string(self) -> str: import_string = "\n".join(import_strings) return f"\n{import_string}\n" - def _create_class_string(self, class_: Class, class_indentation: str = "") -> str: + def _create_class_string(self, class_: Class, class_indentation: str = "", in_reexport_module: bool = False) -> str: + if not in_reexport_module and self._has_node_shorter_reexport(node=class_): + return "" + + # Set indentation inner_indentations = class_indentation + INDENTATION # Constructor parameter @@ -345,7 +442,12 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st # Inner classes for inner_class in class_.classes: - class_text += f"\n{self._create_class_string(inner_class, inner_indentations)}\n" + class_string = self._create_class_string( + class_=inner_class, + class_indentation=inner_indentations, + in_reexport_module=in_reexport_module, + ) + class_text += f"\n{class_string}\n" # Superclass methods, if the superclass is an internal class class_text += superclass_methods_text @@ -383,7 +485,7 @@ def _create_class_method_string( ) else: class_methods.append( - self._create_function_string(method, inner_indentations, is_method=True), + self._create_function_string(function=method, indentations=inner_indentations, is_method=True), ) method_text = "" @@ -446,8 +548,17 @@ def _create_class_attribute_string(self, attributes: list[Attribute], inner_inde attribute_text += f"\n{attribute_infos}\n" return attribute_text - def _create_function_string(self, function: Function, indentations: str = "", is_method: bool = False) -> str: + def _create_function_string( + self, + function: Function, + indentations: str = "", + is_method: bool = False, + in_reexport_module: bool = False, + ) -> str: """Create a function string for Safe-DS stubs.""" + if not is_method and not in_reexport_module and self._has_node_shorter_reexport(node=function): + return "" + # Check if static or class method is_static = function.is_static is_class_method = function.is_class_method @@ -961,7 +1072,49 @@ def _create_sds_docstring( # ############################### Utilities ############################### # - def _add_to_imports(self, qname: str) -> None: + def _has_node_shorter_reexport(self, node: Class | Function) -> bool: + # Check if this node is beeing reexported from a shorter path. If it is, we create it there, not in this module + shortest_reexport_module_id = self.module_id + shortest_reexport_module: Module | None = None + for reexport_module in node.reexported_by: + if len(reexport_module.id.split("/")) < len(shortest_reexport_module_id.split("/")): + shortest_reexport_module_id = reexport_module.id + shortest_reexport_module = reexport_module + + if shortest_reexport_module_id != self.module_id and shortest_reexport_module is not None: + # Get alias + alias = None + for qualified_import in shortest_reexport_module.qualified_imports: + if qualified_import.qualified_name.endswith(node.name): + alias = qualified_import.alias + + if alias: + node.name = alias + + self.reexport_modules[shortest_reexport_module_id].append(node) + return True + return False + + def _is_path_connected_to_class(self, path: str, class_path: str) -> bool: + if class_path.endswith(path): + return True + + name = path.split("/")[-1] + class_name = class_path.split("/")[-1] + for reexport in self.api.reexport_map: + if reexport.endswith(name): + for module in self.api.reexport_map[reexport]: + # Added "no cover" since I can't recreate this in the tests + if ( + path.startswith(module.id) + and class_path.startswith(module.id) + and path.lstrip(module.id).lstrip("/") == name == class_name + ): # pragma: no cover + return True + + return False + + def _add_to_imports(self, import_qname: str) -> None: """Check if the qname of a type is defined in the current module, if not, create an import for it. Paramters @@ -969,29 +1122,45 @@ def _add_to_imports(self, qname: str) -> None: qname The qualified name of a module/class/etc. """ - if qname == "": # pragma: no cover + if import_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) or qname == "typing.Any": + qname_parts = import_qname.split(".") + if (qname_parts[0] == "builtins" and len(qname_parts) == 2) or import_qname == "typing.Any": return - module_id = self.module.id.replace("/", ".") - if module_id not in qname: + module_id = self.module_id.replace("/", ".") + if module_id not in import_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(".", "/") + import_qname_path = import_qname.replace(".", "/") in_package = False - for class_ in self.api.classes: - if class_.endswith(qname_path): - qname = class_.replace("/", ".") + qname = "" + for class_id in self.api.classes: + if self._is_path_connected_to_class(import_qname_path, class_id): + qname = class_id.replace("/", ".") + + name = qname.split(".")[-1] + shortest_qname, _ = _get_shortest_public_reexport( + reexport_map=self.api.reexport_map, + name=name, + qname=qname, + is_module=False, + ) + + if shortest_qname: + qname = f"{shortest_qname}.{name}" + in_package = True break + qname = qname or import_qname + if not in_package: self.classes_outside_package.add(qname) - self.module_imports.add(qname) + if qname.replace(".", "/") != self.module_id: + self.module_imports.add(qname) def _create_todo_msg(self, indentations: str) -> str: if not self._current_todo_msgs: @@ -1025,60 +1194,15 @@ def _create_todo_msg(self, indentations: str) -> str: return indentations + f"\n{indentations}".join(todo_msgs) + "\n" def _get_class_in_module(self, class_name: str) -> Class: - if f"{self.module.id}/{class_name}" in self.api.classes: - return self.api.classes[f"{self.module.id}/{class_name}"] + if f"{self.module_id}/{class_name}" in self.api.classes: + return self.api.classes[f"{self.module_id}/{class_name}"] # If the class is a nested class for class_ in self.api.classes: - if class_.startswith(self.module.id) and class_.endswith(class_name): + if class_.startswith(self.module_id) and class_.endswith(class_name): return self.api.classes[class_] - raise LookupError(f"Expected finding class '{class_name}' in module '{self.module.id}'.") # pragma: no cover - - def _get_shortest_public_reexport(self) -> str: - module_qname = self.module.id.replace("/", ".") - module_name = self.module.name - reexports = self.api.reexport_map - - def _module_name_check(name: str, string: str) -> bool: - return ( - string == name - or (f".{name}" in string and (string.endswith(f".{name}") or f"{name}." in string)) - or (f"{name}." in string and (string.startswith(f"{name}.") or f".{name}" in string)) - ) - - keys = [reexport_key for reexport_key in reexports if _module_name_check(module_name, reexport_key)] - - module_ids = set() - for key in keys: - for module in reexports[key]: - added_module_id = False - - for qualified_import in module.qualified_imports: - if _module_name_check(module_name, qualified_import.qualified_name): - module_ids.add(module.id) - added_module_id = True - break - - if added_module_id: - continue - - for wildcard_import in module.wildcard_imports: - if _module_name_check(module_name, wildcard_import.module_name): - module_ids.add(module.id) - break - - # Adjust all ids - fixed_module_ids_parts = [module_id.split("/") for module_id in module_ids] - - shortest_id = None - for fixed_module_id_parts in fixed_module_ids_parts: - if shortest_id is None or len(fixed_module_id_parts) < len(shortest_id): - shortest_id = fixed_module_id_parts - - if shortest_id is None: - return module_qname - return ".".join(shortest_id) + raise LookupError(f"Expected finding class '{class_name}' in module '{self.module_id}'.") # pragma: no cover @staticmethod def _create_docstring_description_part(description: str, indentations: str) -> str: @@ -1098,6 +1222,59 @@ def _create_docstring_description_part(description: str, indentations: str) -> s return full_docstring + "\n" +def _get_shortest_public_reexport( + reexport_map: dict[str, set[Module]], + name: str, + qname: str, + is_module: bool, +) -> tuple[str, str]: + parent_name = "" + if not is_module and qname: + qname_parts = qname.split(".") + if len(qname_parts) > 2: + parent_name = qname_parts[-2] + + def _module_name_check(text: str, is_wildcard: bool = False) -> bool: + if is_module: + return text.endswith(f".{name}") or text == name + elif is_wildcard: + return text.endswith(f".{parent_name}") or text == parent_name + return ( + text == name + or (f".{name}" in text and (text.endswith(f".{name}") or f"{name}." in text)) + or (f"{name}." in text and (text.startswith(f"{name}.") or f".{name}" in text)) + or (parent_name != "" and text.endswith(f"{parent_name}.*")) + ) + + keys = [reexport_key for reexport_key in reexport_map if _module_name_check(reexport_key)] + + module_ids = set() + for key in keys: + for module in reexport_map[key]: + + for qualified_import in module.qualified_imports: + if _module_name_check(qualified_import.qualified_name): + module_ids.add((module.id, qualified_import.alias)) + break + + for wildcard_import in module.wildcard_imports: + if _module_name_check(wildcard_import.module_name, is_wildcard=True): + module_ids.add((module.id, None)) + break + + shortest_id = None + alias = None + for module_id_tuple in module_ids: + module_id_parts = module_id_tuple[0].split("/") + if shortest_id is None or len(module_id_parts) < len(shortest_id): + shortest_id = module_id_parts + alias = module_id_tuple[1] + + if shortest_id is None: + return "", "" + return ".".join(shortest_id), alias or "" + + def _create_name_annotation(name: str) -> str: return f'@PythonName("{name}")' diff --git a/tests/data/various_modules_package/__init__.py b/tests/data/various_modules_package/__init__.py index f97049c7..25bd9a4e 100644 --- a/tests/data/various_modules_package/__init__.py +++ b/tests/data/various_modules_package/__init__.py @@ -9,10 +9,13 @@ from ._reexport_module_4 import _two_times_reexported as two_times_reexported from .enum_module import _ReexportedEmptyEnum from file_creation._module_3 import Reexported +from .class_module import ClassModuleClassD as ClMCD +from file_creation.module_1 import Lv2 __all__ = [ "reex_1", "ReexportClass", + "ClMCD", "reexported_function_2", "reexported_function_3", "_reexported_function_4", diff --git a/tests/data/various_modules_package/_reexport_module_2.py b/tests/data/various_modules_package/_reexport_module_2.py index fc2dadad..53da882f 100644 --- a/tests/data/various_modules_package/_reexport_module_2.py +++ b/tests/data/various_modules_package/_reexport_module_2.py @@ -1,4 +1,4 @@ -class AnotherReexportClass: +class AnotherUnreexportedClass: pass diff --git a/tests/data/various_modules_package/_reexport_module_3.py b/tests/data/various_modules_package/_reexport_module_3.py index deb00c69..ce3777f5 100644 --- a/tests/data/various_modules_package/_reexport_module_3.py +++ b/tests/data/various_modules_package/_reexport_module_3.py @@ -2,5 +2,9 @@ class _ThirdReexportClass: pass +class ThirdReexportedClassAsType: + pass + + def reexported_function_3() -> None: pass diff --git a/tests/data/various_modules_package/file_creation/__init__.py b/tests/data/various_modules_package/file_creation/__init__.py index 8e6b9658..31ed60d2 100644 --- a/tests/data/various_modules_package/file_creation/__init__.py +++ b/tests/data/various_modules_package/file_creation/__init__.py @@ -1,3 +1,4 @@ +from .module_1 import Lv1 from ._module_2 import _private_reexported from ._module_3 import Reexported from ._module_6 import public_reexported diff --git a/tests/data/various_modules_package/file_creation/module_1.py b/tests/data/various_modules_package/file_creation/module_1.py index aa27c8af..b4611f12 100644 --- a/tests/data/various_modules_package/file_creation/module_1.py +++ b/tests/data/various_modules_package/file_creation/module_1.py @@ -1,2 +1,14 @@ +from typing import Self + + class C: ... + + +class Lv1: + def level1_function(self) -> Self: + ... + + +class Lv2: + lv2_attr: Self diff --git a/tests/data/various_modules_package/file_creation/package_1/__init__.py b/tests/data/various_modules_package/file_creation/package_1/__init__.py index e69de29b..dace8e4d 100644 --- a/tests/data/various_modules_package/file_creation/package_1/__init__.py +++ b/tests/data/various_modules_package/file_creation/package_1/__init__.py @@ -0,0 +1 @@ +from tests.data.various_modules_package._reexport_module_3 import * diff --git a/tests/data/various_modules_package/file_creation/package_1/module_5.py b/tests/data/various_modules_package/file_creation/package_1/module_5.py index aa27c8af..da43f2d2 100644 --- a/tests/data/various_modules_package/file_creation/package_1/module_5.py +++ b/tests/data/various_modules_package/file_creation/package_1/module_5.py @@ -1,2 +1,9 @@ +from . import ThirdReexportedClassAsType + + class C: ... + + +def f() -> ThirdReexportedClassAsType: + ... 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 8206ab97..159e7e92 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -1836,7 +1836,6 @@ 'parameters': list([ ]), 'reexported_by': list([ - 'tests/data/various_modules_package', ]), 'results': list([ 'tests/data/various_modules_package/_reexport_module_1/ReexportClass/_private_class_method_of_reexported_class/result_1', @@ -2012,7 +2011,7 @@ ]), }) # --- -# name: test_classes[AnotherReexportClass] +# name: test_classes[AnotherUnreexportedClass] dict({ 'attributes': list([ ]), @@ -2024,12 +2023,12 @@ 'example': '', 'full_docstring': '', }), - 'id': 'tests/data/various_modules_package/_reexport_module_2/AnotherReexportClass', + 'id': 'tests/data/various_modules_package/_reexport_module_2/AnotherUnreexportedClass', 'inherits_from_exception': False, - 'is_public': True, + 'is_public': False, 'methods': list([ ]), - 'name': 'AnotherReexportClass', + 'name': 'AnotherUnreexportedClass', 'reexported_by': list([ ]), 'superclasses': list([ @@ -2139,6 +2138,7 @@ ]), 'name': 'ClassModuleClassD', 'reexported_by': list([ + 'tests/data/various_modules_package', ]), 'superclasses': list([ ]), @@ -2743,6 +2743,7 @@ 'name': '_ThirdReexportClass', 'reexported_by': list([ 'tests/data/various_modules_package', + 'tests/data/various_modules_package/file_creation/package_1', ]), 'superclasses': list([ ]), @@ -5599,7 +5600,6 @@ 'parameters': list([ ]), 'reexported_by': list([ - 'tests/data/various_modules_package', ]), 'results': list([ 'tests/data/various_modules_package/_reexport_module_1/reexported_function/result_1', @@ -5650,6 +5650,7 @@ ]), 'reexported_by': list([ 'tests/data/various_modules_package', + 'tests/data/various_modules_package/file_creation/package_1', ]), 'results': list([ 'tests/data/various_modules_package/_reexport_module_3/reexported_function_3/result_1', @@ -6571,6 +6572,14 @@ 'alias': None, 'qualified_name': 'file_creation._module_3.Reexported', }), + dict({ + 'alias': 'ClMCD', + 'qualified_name': 'class_module.ClassModuleClassD', + }), + dict({ + 'alias': None, + 'qualified_name': 'file_creation.module_1.Lv2', + }), ]) # --- # name: test_imports[__init__ (wildcard_imports)] @@ -6684,6 +6693,14 @@ 'alias': None, 'qualified_name': 'file_creation._module_3.Reexported', }), + dict({ + 'alias': 'ClMCD', + 'qualified_name': 'class_module.ClassModuleClassD', + }), + dict({ + 'alias': None, + 'qualified_name': 'file_creation.module_1.Lv2', + }), ]), 'wildcard_imports': list([ dict({ @@ -6714,7 +6731,7 @@ # name: test_modules[_reexport_module_2] dict({ 'classes': list([ - 'tests/data/various_modules_package/_reexport_module_2/AnotherReexportClass', + 'tests/data/various_modules_package/_reexport_module_2/AnotherUnreexportedClass', ]), 'docstring': '', 'enums': list([ @@ -6734,6 +6751,7 @@ dict({ 'classes': list([ 'tests/data/various_modules_package/_reexport_module_3/_ThirdReexportClass', + 'tests/data/various_modules_package/_reexport_module_3/ThirdReexportedClassAsType', ]), 'docstring': '', 'enums': list([ @@ -7151,6 +7169,10 @@ 'id': 'tests/data/various_modules_package/file_creation', 'name': '__init__', 'qualified_imports': list([ + dict({ + 'alias': None, + 'qualified_name': 'module_1.Lv1', + }), dict({ 'alias': None, 'qualified_name': '_module_2._private_reexported', @@ -7259,6 +7281,8 @@ dict({ 'classes': list([ 'tests/data/various_modules_package/file_creation/module_1/C', + 'tests/data/various_modules_package/file_creation/module_1/Lv1', + 'tests/data/various_modules_package/file_creation/module_1/Lv2', ]), 'docstring': '', 'enums': list([ @@ -7268,6 +7292,10 @@ 'id': 'tests/data/various_modules_package/file_creation/module_1', 'name': 'module_1', 'qualified_imports': list([ + dict({ + 'alias': None, + 'qualified_name': 'typing.Self', + }), ]), 'wildcard_imports': list([ ]), @@ -7287,6 +7315,9 @@ 'qualified_imports': list([ ]), 'wildcard_imports': list([ + dict({ + 'module_name': 'tests.data.various_modules_package._reexport_module_3', + }), ]), }) # --- @@ -7299,10 +7330,15 @@ 'enums': list([ ]), 'functions': list([ + 'tests/data/various_modules_package/file_creation/package_1/module_5/f', ]), 'id': 'tests/data/various_modules_package/file_creation/package_1/module_5', 'name': 'module_5', 'qualified_imports': list([ + dict({ + 'alias': None, + 'qualified_name': 'ThirdReexportedClassAsType', + }), ]), 'wildcard_imports': list([ ]), diff --git a/tests/safeds_stubgen/api_analyzer/test__get_api.py b/tests/safeds_stubgen/api_analyzer/test__get_api.py index b6ea5ab1..25f4edfd 100644 --- a/tests/safeds_stubgen/api_analyzer/test__get_api.py +++ b/tests/safeds_stubgen/api_analyzer/test__get_api.py @@ -179,7 +179,7 @@ def test_imports(module_name: str, import_type: str, snapshot: SnapshotAssertion (_docstring_module_name, "NumpyDocstringClass", "numpydoc"), (_docstring_module_name, "GoogleDocstringClass", "google"), ("_reexport_module_1", "ReexportClass", "plaintext"), - ("_reexport_module_2", "AnotherReexportClass", "plaintext"), + ("_reexport_module_2", "AnotherUnreexportedClass", "plaintext"), ("_reexport_module_3", "_ThirdReexportClass", "plaintext"), ("_reexport_module_4", "FourthReexportClass", "plaintext"), (_abstract_module_name, "AbstractModuleClass", "plaintext"), @@ -201,7 +201,7 @@ def test_imports(module_name: str, import_type: str, snapshot: SnapshotAssertion "NumpyDocstringClass", "GoogleDocstringClass", "ReexportClass", - "AnotherReexportClass", + "AnotherUnreexportedClass", "_ThirdReexportClass", "FourthReexportClass", "AbstractModuleClass", diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ClMCD].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ClMCD].sdsstub new file mode 100644 index 00000000..8f815f9a --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ClMCD].sdsstub @@ -0,0 +1,23 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +class ClMCD() { + /** + * Docstring of the nested class E. + */ + class ClassModuleNestedClassE() { + @PythonName("nested_attr_1") + static attr nestedAttr1: Nothing? + + @PythonName("_ClassModulePrivateDoubleNestedClassF") + class ClassModulePrivateDoubleNestedClassF() + + // TODO Result type information missing. + /** + * Docstring of func of nested class E + */ + @Pure + @PythonName("class_e_func") + fun classEFunc() + } +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[FourthReexportClass].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[FourthReexportClass].sdsstub new file mode 100644 index 00000000..04428778 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[FourthReexportClass].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +class FourthReexportClass() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv1].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv1].sdsstub new file mode 100644 index 00000000..6e3cf69f --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv1].sdsstub @@ -0,0 +1,8 @@ +@PythonModule("tests.data.various_modules_package.file_creation") +package tests.data.variousModulesPackage.fileCreation + +class Lv1() { + @Pure + @PythonName("level1_function") + fun level1Function() -> result1: Lv1 +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv2].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv2].sdsstub new file mode 100644 index 00000000..b6a99aec --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Lv2].sdsstub @@ -0,0 +1,7 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +class Lv2() { + @PythonName("lv2_attr") + static attr lv2Attr: Lv2 +} diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass2].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass2].sdsstub new file mode 100644 index 00000000..000e2b55 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass2].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("tests.data.various_modules_package.file_creation") +package tests.data.variousModulesPackage.fileCreation + +class ReexportedInAnotherPackageClass2() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass].sdsstub new file mode 100644 index 00000000..ca9efa57 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ReexportedInAnotherPackageClass].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("tests.data.various_modules_package.file_creation") +package tests.data.variousModulesPackage.fileCreation + +class ReexportedInAnotherPackageClass() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_3].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Reexported].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_3].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[Reexported].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ThirdReexportedClassAsType].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ThirdReexportedClassAsType].sdsstub new file mode 100644 index 00000000..b95aa5ee --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[ThirdReexportedClassAsType].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +class ThirdReexportedClassAsType() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub deleted file mode 100644 index 3b2cc00f..00000000 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub +++ /dev/null @@ -1,12 +0,0 @@ -@PythonModule("tests.data.various_modules_package") -package tests.data.variousModulesPackage - -@Pure -@PythonName("_reexported_function_4_alias") -fun reexportedFunction4Alias() - -@Pure -@PythonName("_two_times_reexported") -fun twoTimesReexported() - -class FourthReexportClass() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub index 65739867..694f33bf 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub @@ -26,27 +26,6 @@ class ClassModuleClassC() sub ClassModuleEmptyClassA, ClassModuleClassB, yetAnot fun f1() } -class ClassModuleClassD() { - /** - * Docstring of the nested class E. - */ - class ClassModuleNestedClassE() { - @PythonName("nested_attr_1") - static attr nestedAttr1: Nothing? - - @PythonName("_ClassModulePrivateDoubleNestedClassF") - class ClassModulePrivateDoubleNestedClassF() - - // TODO Result type information missing. - /** - * Docstring of func of nested class E - */ - @Pure - @PythonName("class_e_func") - fun classEFunc() - } -} - class SelfTypes1() { @Pure @PythonName("self_result1") diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub index c02f9c36..6a9a9afd 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub @@ -1,5 +1,5 @@ -@PythonModule("tests.data.various_modules_package") -package tests.data.variousModulesPackage +@PythonModule("tests.data.various_modules_package.enum_module") +package tests.data.variousModulesPackage.enumModule /** * Nothing's here. diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[import_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[import_module].sdsstub index 60fb7937..2ffb4151 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[import_module].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[import_module].sdsstub @@ -1,10 +1,10 @@ @PythonModule("tests.data.various_modules_package.import_module") package tests.data.variousModulesPackage.importModule +from tests.data.variousModulesPackage import ClassModuleClassD from tests.data.variousModulesPackage.anotherPath.anotherModule import AnotherClass from tests.data.variousModulesPackage.classModule import ClassModuleClassB from tests.data.variousModulesPackage.classModule import ClassModuleClassC -from tests.data.variousModulesPackage.classModule import ClassModuleClassD from tests.data.variousModulesPackage.classModule import ClassModuleEmptyClassA class ImportClass() sub AnotherClass { diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub index eee2ef65..78599650 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub @@ -1,4 +1,9 @@ @PythonModule("tests.data.various_modules_package.file_creation.package_1.module_5") package tests.data.variousModulesPackage.fileCreation.package1.module5 +from tests.data.variousModulesPackage import ThirdReexportedClassAsType + +@Pure +fun f() -> result1: ThirdReexportedClassAsType + class C() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_6].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[public_reexported].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_6].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[public_reexported].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_1].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reex_1].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_1].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reex_1].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_3].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_from_another_package_3].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_3].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_from_another_package_3].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_2].sdsstub similarity index 84% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_2].sdsstub index c9e709aa..86db0c76 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_2].sdsstub @@ -4,5 +4,3 @@ package tests.data.variousModulesPackage @Pure @PythonName("reexported_function_2") fun reexportedFunction2() - -class AnotherReexportClass() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_3].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_3].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_3].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_3].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_4_alias].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_4_alias].sdsstub new file mode 100644 index 00000000..7e69f3be --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_function_4_alias].sdsstub @@ -0,0 +1,6 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +@Pure +@PythonName("reexported_function_4_alias") +fun reexportedFunction4Alias() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_2].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function2].sdsstub similarity index 85% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_2].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function2].sdsstub index 2fda21c7..09f83b80 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package_2].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function2].sdsstub @@ -4,5 +4,3 @@ package tests.data.variousModulesPackage.fileCreation @Pure @PythonName("reexported_in_another_package_function2") fun reexportedInAnotherPackageFunction2() -> result1: String - -class ReexportedInAnotherPackageClass2() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function].sdsstub similarity index 85% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function].sdsstub index 6e6b144e..be918b72 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexported_from_another_package].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[reexported_in_another_package_function].sdsstub @@ -4,5 +4,3 @@ package tests.data.variousModulesPackage.fileCreation @Pure @PythonName("reexported_in_another_package_function") fun reexportedInAnotherPackageFunction() -> result1: String - -class ReexportedInAnotherPackageClass() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[two_times_reexported].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[two_times_reexported].sdsstub new file mode 100644 index 00000000..fde6c646 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[two_times_reexported].sdsstub @@ -0,0 +1,6 @@ +@PythonModule("tests.data.various_modules_package") +package tests.data.variousModulesPackage + +@Pure +@PythonName("two_times_reexported") +fun twoTimesReexported() diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index aeb8fd63..e1631721 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -39,20 +39,27 @@ def test_file_creation() -> None: data_to_test.sort(key=lambda x: x[1]) expected_files: list[tuple[str, str]] = [ - # We reexport these three modules from another package into the file_creation package. - ("tests/data/various_modules_package/file_creation", "_reexported_from_another_package"), - ("tests/data/various_modules_package/file_creation", "_reexported_from_another_package_2"), - ("tests/data/various_modules_package/file_creation", "_reexported_from_another_package_3"), - # module_1 is public + ("tests/data/various_modules_package/file_creation/Lv1", "Lv1"), + ( + "tests/data/various_modules_package/file_creation/ReexportedInAnotherPackageClass", + "ReexportedInAnotherPackageClass", + ), + ( + "tests/data/various_modules_package/file_creation/ReexportedInAnotherPackageClass2", + "ReexportedInAnotherPackageClass2", + ), ("tests/data/various_modules_package/file_creation/module_1", "module_1"), - # _module_6 has a public reexport in the file_creation package - ("tests/data/various_modules_package/file_creation", "_module_6"), - # module_5 is publich ("tests/data/various_modules_package/file_creation/package_1/module_5", "module_5"), - # _module_3 is not created, even though it is reexported, since it's also reexported in the parent - # package. - # _module_2 is not created, since the reexport is still private - # _module_4 is not created, since it has no (public) reexport + ("tests/data/various_modules_package/file_creation/public_reexported", "public_reexported"), + ("tests/data/various_modules_package/file_creation", "reexported_from_another_package_3"), + ( + "tests/data/various_modules_package/file_creation/reexported_in_another_package_function", + "reexported_in_another_package_function", + ), + ( + "tests/data/various_modules_package/file_creation/reexported_in_another_package_function2", + "reexported_in_another_package_function2", + ), ] expected_files.sort(key=lambda x: x[1]) @@ -72,31 +79,20 @@ def test_file_creation_limited_stubs_outside_package(snapshot_sds_stub: Snapshot assert f.read() == snapshot_sds_stub -def _python_files() -> Generator: - return Path(_test_package_dir).rglob(pattern="*.py") +def stub_texts() -> Generator[str, None, None]: + for stub_data in stubs_data: + yield stub_data[2] -def _python_file_ids() -> Generator: - files = Path(_test_package_dir).rglob(pattern="*.py") - for file in files: - yield file.parts[-1].split(".py")[0] +def _stub_ids() -> Generator[str, None, None]: + for stub_data in stubs_data: + yield stub_data[1] -@pytest.mark.parametrize("python_file", _python_files(), ids=_python_file_ids()) +@pytest.mark.parametrize("stub_text", stub_texts(), ids=_stub_ids()) class TestStubFileGeneration: - def test_stub_creation(self, python_file: Path, snapshot_sds_stub: SnapshotAssertion) -> None: - file_name = python_file.parts[-1].split(".py")[0] - - for stub_data in stubs_data: - if stub_data[1] == file_name: - assert stub_data[2] == snapshot_sds_stub - return - - # For these files stubs won't get created, because they are either empty or private. - if file_name in {"__init__", "_module_2", "_module_4", "_reexport_module_5"}: - return - - raise pytest.fail(f"Stub file not found for '{file_name}'.") + def test_stub_creation(self, stub_text: str, snapshot_sds_stub: SnapshotAssertion) -> None: + assert stub_text == snapshot_sds_stub @pytest.mark.parametrize(