From 522f38d64ea812b8eca52ae4f8ac2426e74fbf01 Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Sun, 3 Mar 2024 10:29:50 +0100 Subject: [PATCH] feat: Stubs are created for referenced declarations in other packages (#70) Closes #66 ### Summary of Changes Now limited stubs are created for imported classes from other packages. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- .../stubs_generator/_generate_stubs.py | 367 +++++++++++------- .../another_path/another_module.py | 4 + .../various_modules_package/class_module.py | 5 +- .../__snapshots__/test_main.ambr | 70 ++++ .../__snapshots__/test__get_api.ambr | 5 + ...tion.test_stub_creation[_module_3].sdsstub | 4 + ...tion.test_stub_creation[_module_6].sdsstub | 7 + ..._stub_creation[_reexport_module_1].sdsstub | 4 + ..._stub_creation[_reexport_module_2].sdsstub | 9 + ..._stub_creation[_reexport_module_4].sdsstub | 4 + ...st_stub_creation[abstract_module].sdsstub} | 0 ..._stub_creation[aliasing_module_1].sdsstub} | 0 ..._stub_creation[aliasing_module_2].sdsstub} | 0 ..._stub_creation[aliasing_module_3].sdsstub} | 0 ...test_stub_creation[another_module].sdsstub | 4 + ...t_stub_creation[attribute_module].sdsstub} | 4 +- ....test_stub_creation[class_module].sdsstub} | 4 +- ...st_stub_creation[docstring_module].sdsstub | 58 +++ ...n.test_stub_creation[enum_module].sdsstub} | 0 ...st_stub_creation[function_module].sdsstub} | 6 +- ...test_stub_creation[import_module].sdsstub} | 0 ...stub_creation[infer_types_module].sdsstub} | 2 +- ...ation.test_stub_creation[module_1].sdsstub | 4 + ...ation.test_stub_creation[module_5].sdsstub | 4 + ...st_stub_creation[type_var_module].sdsstub} | 0 ...st_stub_creation[variance_module].sdsstub} | 0 ...tion_limited_stubs_outside_package.sdsstub | 7 + .../stubs_generator/test_generate_stubs.py | 136 +++---- 28 files changed, 488 insertions(+), 220 deletions(-) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_3].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_6].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_1].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub create 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/{test_abstract_creation.sdsstub => TestStubFileGeneration.test_stub_creation[abstract_module].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_alias_creation[aliasing_module_1].sdsstub => TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_alias_creation[aliasing_module_2].sdsstub => TestStubFileGeneration.test_stub_creation[aliasing_module_2].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_alias_creation[aliasing_module_3].sdsstub => TestStubFileGeneration.test_stub_creation[aliasing_module_3].sdsstub} (100%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[another_module].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_class_attribute_creation.sdsstub => TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub} (96%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_class_creation.sdsstub => TestStubFileGeneration.test_stub_creation[class_module].sdsstub} (89%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[docstring_module].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_enum_creation.sdsstub => TestStubFileGeneration.test_stub_creation[enum_module].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_function_creation.sdsstub => TestStubFileGeneration.test_stub_creation[function_module].sdsstub} (97%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_import_creation.sdsstub => TestStubFileGeneration.test_stub_creation[import_module].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_type_inference.sdsstub => TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub} (97%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_1].sdsstub create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_type_var_creation.sdsstub => TestStubFileGeneration.test_stub_creation[type_var_module].sdsstub} (100%) rename tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/{test_variance_creation.sdsstub => TestStubFileGeneration.test_stub_creation[variance_module].sdsstub} (100%) create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_file_creation_limited_stubs_outside_package.sdsstub diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index 9dd2c7c0..b3b9a768 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -1,6 +1,6 @@ from __future__ import annotations -import string +from enum import IntEnum from pathlib import Path from typing import TYPE_CHECKING @@ -22,6 +22,11 @@ from collections.abc import Generator +class NamingConvention(IntEnum): + PYTHON = 1 + SAFE_DS = 2 + + def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: """Generate Safe-DS stubs. @@ -29,29 +34,34 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: Parameters ---------- - api : API + api The API object from which the stubs - out_path : Path + out_path The path in which the stub files should be created. If no such path exists this function creates the directory files. - convert_identifiers : bool + convert_identifiers Set this True if the identifiers should be converted to Safe-DS standard (UpperCamelCase for classes and camelCase for everything else). """ - modules = api.modules.values() - - Path(out_path / api.package).mkdir(parents=True, exist_ok=True) - stubs_generator = StubsStringGenerator(api, convert_identifiers) - - for module in modules: - module_name = module.name - - if module_name == "__init__": + naming_convention = NamingConvention.SAFE_DS if convert_identifiers else NamingConvention.PYTHON + stubs_generator = StubsStringGenerator(api, naming_convention) + stubs_data = _generate_stubs_data(api, out_path, stubs_generator) + _generate_stubs_files(stubs_data, api, out_path, stubs_generator, naming_convention) + + +def _generate_stubs_data( + api: API, + out_path: Path, + stubs_generator: StubsStringGenerator, +) -> list[tuple[Path, str, str]]: + stubs_data: list[tuple[Path, str, str]] = [] + for module in api.modules.values(): + if module.name == "__init__": continue module_text = stubs_generator(module) - # Each text block we create ends with "\n", therefore, is there is only the package information + # Each text block we create ends with "\n", therefore, if there is only the package information # the file would look like this: "package path.to.myPackage\n" or this: # '@PythonModule("path.to.my_package")\npackage path.to.myPackage\n'. With the split we check if the module # has enough information, if not, we won't create it. @@ -59,8 +69,22 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: if len(splitted_text) <= 2 or (len(splitted_text) == 3 and splitted_text[1].startswith("package ")): continue - # Create module dir module_dir = Path(out_path / module.id) + stubs_data.append((module_dir, module.name, module_text)) + return stubs_data + + +def _generate_stubs_files( + stubs_data: list[tuple[Path, str, str]], + api: API, + out_path: Path, + stubs_generator: StubsStringGenerator, + naming_convention: NamingConvention, +) -> None: + Path(out_path / api.package).mkdir(parents=True, exist_ok=True) + + for module_dir, module_name, module_text in stubs_data: + # Create module dir module_dir.mkdir(parents=True, exist_ok=True) # Create and open module file @@ -70,6 +94,69 @@ def generate_stubs(api: API, out_path: Path, convert_identifiers: bool) -> None: with file_path.open("w") as f: f.write(module_text) + created_module_paths: set[str] = set() + classes_outside_package = list(stubs_generator.classes_outside_package) + classes_outside_package.sort() + for class_ in classes_outside_package: + created_module_paths = _create_outside_package_class(class_, out_path, naming_convention, created_module_paths) + + +def _create_outside_package_class( + class_path: str, + out_path: Path, + naming_convention: NamingConvention, + created_module_paths: set[str], +) -> set[str]: + path_parts = class_path.split(".") + class_name = path_parts.pop(-1) + module_name = path_parts[-1] + module_path = "/".join(path_parts) + + first_creation = False + if module_path not in created_module_paths: + created_module_paths.add(module_path) + first_creation = True + + module_dir = Path(out_path / module_path) + module_dir.mkdir(parents=True, exist_ok=True) + + file_path = Path(module_dir / f"{module_name}.sdsstub") + if Path.exists(file_path) and not first_creation: + with file_path.open("a") as f: + f.write(_create_outside_package_class_text(class_name, naming_convention)) + else: + with file_path.open("w") as f: + module_text = "" + + # package name & annotation + python_module_path = ".".join(path_parts) + module_path_camel_case = _convert_name_to_convention(python_module_path, naming_convention) + module_name_info = "" + if python_module_path != module_path_camel_case: + module_text += f'@PythonModule("{python_module_path}")\n' + module_text += f"{module_name_info}package {module_path_camel_case}\n" + + module_text += _create_outside_package_class_text(class_name, naming_convention) + + f.write(module_text) + + return created_module_paths + + +def _create_outside_package_class_text(class_name: str, naming_convention: NamingConvention) -> str: + # to camel case + camel_case_name = _convert_name_to_convention(class_name, naming_convention, is_class_name=True) + + # add name annotation + class_annotation = "" + if class_name != camel_case_name: + class_annotation = f"\n{_create_name_annotation(class_name)}" + + # check for identifiers + safe_class_name = _replace_if_safeds_keyword(camel_case_name) + + return f"{class_annotation}\nclass {safe_class_name}\n" + class StubsStringGenerator: """Generate Safe-DS stub strings. @@ -78,14 +165,13 @@ class StubsStringGenerator: method. """ - def __init__(self, api: API, convert_identifiers: bool) -> None: - self.module_imports: set[str] = set() + def __init__(self, api: API, naming_convention: NamingConvention) -> None: self.api = api - self.convert_identifiers = convert_identifiers + self.naming_convention = naming_convention + self.classes_outside_package: set[str] = set() def __call__(self, module: Module) -> str: - # Reset the module_imports list - self.module_imports = set() + self.module_imports: set[str] = set() self._current_todo_msgs: set[str] = set() self.module = module self.class_generics: list = [] @@ -94,7 +180,7 @@ def __call__(self, module: Module) -> str: 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) + 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: @@ -128,8 +214,15 @@ def _create_imports_string(self) -> str: for import_ in self.module_imports: import_parts = import_.split(".") + from_ = ".".join(import_parts[0:-1]) + from_ = _convert_name_to_convention(from_, self.naming_convention) + from_ = _replace_if_safeds_keyword(from_) + name = import_parts[-1] + name = _convert_name_to_convention(name, self.naming_convention) + name = _replace_if_safeds_keyword(name) + import_strings.append(f"from {from_} import {name}") # We have to sort for the snapshot tests @@ -164,7 +257,7 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st if superclasses and not class_.is_abstract: superclass_names = [] for superclass in superclasses: - superclass_names.append(self._split_import_id(superclass)[1]) + superclass_names.append(superclass.split(".")[-1]) self._add_to_imports(superclass) superclass_info = f" sub {', '.join(superclass_names)}" @@ -187,8 +280,8 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st }[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) + variance_name_camel_case = _convert_name_to_convention(variance.name, self.naming_convention) + variance_name_camel_case = _replace_if_safeds_keyword(variance_name_camel_case) variance_item = f"{variance_direction}{variance_name_camel_case}" if variance.type is not None: @@ -201,10 +294,10 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st # Class name - Convert to camelCase and check for keywords class_name = class_.name python_name_info = "" - class_name_camel_case = self._convert_snake_to_camel_case(class_name, is_class_name=True) + class_name_camel_case = _convert_name_to_convention(class_name, self.naming_convention, is_class_name=True) if class_name_camel_case != class_name: - python_name_info = f"{class_indentation}{self._create_name_annotation(class_name)}\n" - class_name_camel_case = self._replace_if_safeds_keyword(class_name_camel_case) + python_name_info = f"{class_indentation}{_create_name_annotation(class_name)}\n" + class_name_camel_case = _replace_if_safeds_keyword(class_name_camel_case) # Class signature line class_signature = ( @@ -275,13 +368,13 @@ def _create_class_attribute_string(self, attributes: list[Attribute], inner_inde # Convert name to camelCase and add PythonName annotation attr_name = attribute.name - attr_name_camel_case = self._convert_snake_to_camel_case(attr_name) + attr_name_camel_case = _convert_name_to_convention(attr_name, self.naming_convention) attr_name_annotation = "" if attr_name_camel_case != attr_name: - attr_name_annotation = f"{self._create_name_annotation(attr_name)}\n{inner_indentations}" + attr_name_annotation = f"{_create_name_annotation(attr_name)}\n{inner_indentations}" # Check if name is a Safe-DS keyword and escape it if necessary - attr_name_camel_case = self._replace_if_safeds_keyword(attr_name_camel_case) + attr_name_camel_case = _replace_if_safeds_keyword(attr_name_camel_case) # Create type information attr_type = self._create_type_string(attribute_type) @@ -328,8 +421,8 @@ def _create_function_string(self, function: Function, indentations: str = "", is if function.type_var_types: type_var_names = [] for type_var in function.type_var_types: - type_var_name = self._convert_snake_to_camel_case(type_var.name) - type_var_name = self._replace_if_safeds_keyword(type_var_name) + type_var_name = _convert_name_to_convention(type_var.name, self.naming_convention) + type_var_name = _replace_if_safeds_keyword(type_var_name) # We don't have to display generic types in methods if they were already displayed in the class if not is_method or (is_method and type_var_name not in self.class_generics): @@ -343,13 +436,13 @@ def _create_function_string(self, function: Function, indentations: str = "", is # Convert function name to camelCase name = function.name - camel_case_name = self._convert_snake_to_camel_case(name) + camel_case_name = _convert_name_to_convention(name, self.naming_convention) function_name_annotation = "" if camel_case_name != name: - function_name_annotation = f"{indentations}{self._create_name_annotation(name)}\n" + function_name_annotation = f"{indentations}{_create_name_annotation(name)}\n" # Escape keywords - camel_case_name = self._replace_if_safeds_keyword(camel_case_name) + camel_case_name = _replace_if_safeds_keyword(camel_case_name) result_string = self._create_result_string(function.results) @@ -368,13 +461,13 @@ def _create_property_function_string(self, function: Function, indentations: str Functions or methods with the @property decorator are handled the same way as class attributes. """ name = function.name - camel_case_name = self._convert_snake_to_camel_case(name) + camel_case_name = _convert_name_to_convention(name, self.naming_convention) function_name_annotation = "" if camel_case_name != name: - function_name_annotation = f"{self._create_name_annotation(name)} " + function_name_annotation = f"{_create_name_annotation(name)} " # Escape keywords - camel_case_name = self._replace_if_safeds_keyword(camel_case_name) + camel_case_name = _replace_if_safeds_keyword(camel_case_name) # Create type information result_types = [result.type for result in function.results if result.type is not None] @@ -398,8 +491,8 @@ def _create_result_string(self, function_results: list[Result]) -> str: result_type = result.type.to_dict() ret_type = self._create_type_string(result_type) 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) + result_name = _convert_name_to_convention(result.name, self.naming_convention) + result_name = _replace_if_safeds_keyword(result_name) if type_string: results.append( f"{result_name}{type_string}", @@ -476,14 +569,14 @@ def _create_parameter_string( # Convert to camelCase if necessary name = parameter.name - camel_case_name = self._convert_snake_to_camel_case(name) + camel_case_name = _convert_name_to_convention(name, self.naming_convention) name_annotation = "" if camel_case_name != name: # Memorize the changed name for the @PythonName() annotation - name_annotation = f"{self._create_name_annotation(name)} " + name_annotation = f"{_create_name_annotation(name)} " # Check if it's a Safe-DS keyword and escape it - camel_case_name = self._replace_if_safeds_keyword(camel_case_name) + camel_case_name = _replace_if_safeds_keyword(camel_case_name) # Create string and append to the list parameters_data.append( @@ -510,13 +603,13 @@ def _create_enum_string(self, enum_data: Enum) -> str: name = enum_instance.name # Convert snake_case names to camelCase - camel_case_name = self._convert_snake_to_camel_case(name) + camel_case_name = _convert_name_to_convention(name, self.naming_convention) annotation = "" if camel_case_name != name: - annotation = f"{self._create_name_annotation(name)} " + annotation = f"{_create_name_annotation(name)} " # Check if the name is a Safe-DS keyword and escape it - camel_case_name = self._replace_if_safeds_keyword(camel_case_name) + camel_case_name = _replace_if_safeds_keyword(camel_case_name) enum_text += f"\t{annotation}{camel_case_name}\n" return f"{enum_signature} {{{enum_text}}}" @@ -554,7 +647,7 @@ def _create_type_string(self, type_data: dict | None) -> str: elif kind == "FinalType": return self._create_type_string(type_data["type"]) elif kind == "CallableType": - name_generator = self._callable_type_name_generator() + name_generator = _callable_type_name_generator() params = [ f"{next(name_generator)}: {self._create_type_string(parameter_type)}" @@ -646,8 +739,8 @@ def _create_type_string(self, type_data: dict | None) -> str: types.append(f"{literal_type}") return f"literal<{', '.join(types)}>" elif kind == "TypeVarType": - name = self._convert_snake_to_camel_case(type_data["name"]) - return self._replace_if_safeds_keyword(name) + name = _convert_name_to_convention(type_data["name"], self.naming_convention) + return _replace_if_safeds_keyword(name) raise ValueError(f"Unexpected type: {kind}") # pragma: no cover @@ -658,12 +751,8 @@ def _add_to_imports(self, qname: str) -> None: Paramters --------- - qname : str + qname The qualified name of a module/class/etc. - - Returns - ------- - None """ if qname == "": # pragma: no cover raise ValueError("Type has no import source.") @@ -677,26 +766,18 @@ def _add_to_imports(self, qname: str) -> None: # 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(".", "/") + in_package = False 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) + qname = _convert_name_to_convention(qname, self.naming_convention) + in_package = True 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) + if not in_package: + self.classes_outside_package.add(qname) - @staticmethod - def _callable_type_name_generator() -> Generator: - """Generate a name for callable type parameters starting from 'a' until 'zz'.""" - while True: - for x in range(1, 27): - yield string.ascii_lowercase[x - 1] - for x in range(1, 27): # pragma: no cover - for y in range(1, 27): - yield string.ascii_lowercase[x - 1] + string.ascii_lowercase[y - 1] + self.module_imports.add(qname) def _create_todo_msg(self, indentations: str) -> str: if not self._current_todo_msgs: @@ -728,78 +809,80 @@ def _create_todo_msg(self, indentations: str) -> str: return indentations + f"\n{indentations}".join(todo_msgs) + "\n" - @staticmethod - def _split_import_id(id_: str) -> tuple[str, str]: - split_qname = id_.split(".") - name = split_qname.pop(-1) - import_path = ".".join(split_qname) - return import_path, name - - @staticmethod - def _create_name_annotation(name: str) -> str: - return f'@PythonName("{name}")' - - @staticmethod - def _replace_if_safeds_keyword(keyword: str) -> str: - if keyword in { - "as", - "from", - "import", - "literal", - "union", - "where", - "yield", - "false", - "null", - "true", - "annotation", - "attr", - "class", - "enum", - "fun", - "package", - "pipeline", - "schema", - "segment", - "val", - "const", - "in", - "internal", - "out", - "private", - "static", - "and", - "not", - "or", - "sub", - "super", - "_", - }: - return f"`{keyword}`" - return keyword - - def _convert_snake_to_camel_case(self, name: str, is_class_name: bool = False) -> str: - if not self.convert_identifiers: - return name - - if name == "_": - return name - - # Count underscores in front and behind the name - underscore_count_start = len(name) - len(name.lstrip("_")) - underscore_count_end = len(name) - len(name.rstrip("_")) - - if underscore_count_end == 0: - cleaned_name = name[underscore_count_start:] - else: - cleaned_name = name[underscore_count_start:-underscore_count_end] - - # Remove underscores and join in camelCase - name_parts = cleaned_name.split("_") - - # UpperCamelCase for class names - if is_class_name: - return "".join(part[0].upper() + part[1:] for part in name_parts if part) - # Normal camelCase for everything else - return name_parts[0] + "".join(part[0].upper() + part[1:] for part in name_parts[1:] if part) +def _callable_type_name_generator() -> Generator: + """Generate a name for callable type parameters starting from 'a' until 'zz'.""" + while True: + for x in range(1, 1000): + yield f"param{x}" + + +def _create_name_annotation(name: str) -> str: + return f'@PythonName("{name}")' + + +def _replace_if_safeds_keyword(keyword: str) -> str: + if keyword in { + "as", + "from", + "import", + "literal", + "union", + "where", + "yield", + "false", + "null", + "true", + "annotation", + "attr", + "class", + "enum", + "fun", + "package", + "pipeline", + "schema", + "segment", + "val", + "const", + "in", + "internal", + "out", + "private", + "static", + "and", + "not", + "or", + "sub", + "super", + "_", + }: + return f"`{keyword}`" + return keyword + + +def _convert_name_to_convention( + name: str, + naming_convention: NamingConvention, + is_class_name: bool = False, +) -> str: + if name == "_" or naming_convention == NamingConvention.PYTHON: + return name + + # Count underscores in front and behind the name + underscore_count_start = len(name) - len(name.lstrip("_")) + underscore_count_end = len(name) - len(name.rstrip("_")) + + if underscore_count_end == 0: + cleaned_name = name[underscore_count_start:] + else: + cleaned_name = name[underscore_count_start:-underscore_count_end] + + # Remove underscores and join in camelCase + name_parts = cleaned_name.split("_") + + # UpperCamelCase for class names + if is_class_name: + return "".join(part[0].upper() + part[1:] for part in name_parts if part) + + # Normal camelCase for everything else + return name_parts[0] + "".join(part[0].upper() + part[1:] for part in name_parts[1:] if part) diff --git a/tests/data/main_package/another_path/another_module.py b/tests/data/main_package/another_path/another_module.py index 0d53fe84..2769375c 100644 --- a/tests/data/main_package/another_path/another_module.py +++ b/tests/data/main_package/another_path/another_module.py @@ -6,3 +6,7 @@ class AnotherClass: pass + + +class yetAnotherClass: + def another_function(self) -> str: ... diff --git a/tests/data/various_modules_package/class_module.py b/tests/data/various_modules_package/class_module.py index fdc1de62..f4fe3650 100644 --- a/tests/data/various_modules_package/class_module.py +++ b/tests/data/various_modules_package/class_module.py @@ -1,3 +1,6 @@ +from tests.data.main_package.another_path.another_module import yetAnotherClass + + class ClassModuleEmptyClassA: ... @@ -8,7 +11,7 @@ def __init__(self, a: int, b: ClassModuleEmptyClassA | None): ... def f(self): ... -class ClassModuleClassC(ClassModuleEmptyClassA, ClassModuleClassB): +class ClassModuleClassC(ClassModuleEmptyClassA, ClassModuleClassB, yetAnotherClass): attr_1: int attr_2: int diff --git a/tests/safeds_stubgen/__snapshots__/test_main.ambr b/tests/safeds_stubgen/__snapshots__/test_main.ambr index e7a0dfb9..644f4266 100644 --- a/tests/safeds_stubgen/__snapshots__/test_main.ambr +++ b/tests/safeds_stubgen/__snapshots__/test_main.ambr @@ -122,6 +122,29 @@ 'type_parameters': list([ ]), }), + dict({ + 'attributes': list([ + ]), + 'classes': list([ + ]), + 'constructor': None, + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'main_package/another_path/another_module/yetAnotherClass', + 'is_public': True, + 'methods': list([ + 'main_package/another_path/another_module/yetAnotherClass/another_function', + ]), + 'name': 'yetAnotherClass', + 'reexported_by': list([ + ]), + 'superclasses': list([ + ]), + 'type_parameters': list([ + ]), + }), dict({ 'attributes': list([ 'main_package/main_module/ModuleClass/attr_1', @@ -307,6 +330,26 @@ 'enums': list([ ]), 'functions': list([ + dict({ + 'docstring': dict({ + 'description': '', + 'full_docstring': '', + }), + 'id': 'main_package/another_path/another_module/yetAnotherClass/another_function', + 'is_class_method': False, + 'is_property': False, + 'is_public': True, + 'is_static': False, + 'name': 'another_function', + 'parameters': list([ + 'main_package/another_path/another_module/yetAnotherClass/another_function/self', + ]), + 'reexported_by': list([ + ]), + 'results': list([ + 'main_package/another_path/another_module/yetAnotherClass/another_function/result_1', + ]), + }), dict({ 'docstring': dict({ 'description': '', @@ -495,6 +538,7 @@ dict({ 'classes': list([ 'main_package/another_path/another_module/AnotherClass', + 'main_package/another_path/another_module/yetAnotherClass', ]), 'docstring': ''' Another Module Docstring. @@ -554,6 +598,19 @@ ]), 'package': 'main_package', 'parameters': list([ + dict({ + 'assigned_by': 'IMPLICIT', + 'default_value': None, + 'docstring': dict({ + 'default_value': '', + 'description': '', + 'type': '', + }), + 'id': 'main_package/another_path/another_module/yetAnotherClass/another_function/self', + 'is_optional': False, + 'name': 'self', + 'type': None, + }), dict({ 'assigned_by': 'POSITION_OR_NAME', 'default_value': None, @@ -729,6 +786,19 @@ }), ]), 'results': list([ + dict({ + 'docstring': dict({ + 'description': '', + 'type': '', + }), + 'id': 'main_package/another_path/another_module/yetAnotherClass/another_function/result_1', + 'name': 'result_1', + 'type': dict({ + 'kind': 'NamedType', + 'name': 'str', + 'qname': 'builtins.str', + }), + }), dict({ 'docstring': dict({ '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 e5a230db..af97144d 100644 --- a/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr +++ b/tests/safeds_stubgen/api_analyzer/__snapshots__/test__get_api.ambr @@ -2035,6 +2035,7 @@ 'superclasses': list([ 'tests.data.various_modules_package.class_module.ClassModuleEmptyClassA', 'tests.data.various_modules_package.class_module.ClassModuleClassB', + 'tests.data.main_package.another_path.another_module.yetAnotherClass', ]), 'type_parameters': list([ ]), @@ -6431,6 +6432,10 @@ 'id': 'various_modules_package/class_module', 'name': 'class_module', 'qualified_imports': list([ + dict({ + 'alias': None, + 'qualified_name': 'tests.data.main_package.another_path.another_module.yetAnotherClass', + }), ]), 'wildcard_imports': list([ ]), 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[_module_3].sdsstub new file mode 100644 index 00000000..13a5c6ae --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_3].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package.file_creation._module_3") +package variousModulesPackage.fileCreation.Module3 + +class Reexported() 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[_module_6].sdsstub new file mode 100644 index 00000000..1a35cac7 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_module_6].sdsstub @@ -0,0 +1,7 @@ +@PythonModule("various_modules_package.file_creation._module_6") +package variousModulesPackage.fileCreation.Module6 + +// TODO Result type information missing. +@Pure +@PythonName("public_reexported") +fun publicReexported() 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[_reexport_module_1].sdsstub new file mode 100644 index 00000000..ae12122b --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_1].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package._reexport_module_1") +package variousModulesPackage.ReexportModule1 + +class ReexportClass() 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[_reexport_module_2].sdsstub new file mode 100644 index 00000000..51c2cfb0 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_2].sdsstub @@ -0,0 +1,9 @@ +@PythonModule("various_modules_package._reexport_module_2") +package variousModulesPackage.ReexportModule2 + +// TODO Result type information missing. +@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_4].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub new file mode 100644 index 00000000..825344dd --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[_reexport_module_4].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package._reexport_module_4") +package variousModulesPackage.ReexportModule4 + +class FourthReexportClass() 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/TestStubFileGeneration.test_stub_creation[abstract_module].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_abstract_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[abstract_module].sdsstub 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/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_1].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub 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/TestStubFileGeneration.test_stub_creation[aliasing_module_2].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_2].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_2].sdsstub 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/TestStubFileGeneration.test_stub_creation[aliasing_module_3].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_alias_creation[aliasing_module_3].sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_3].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[another_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[another_module].sdsstub new file mode 100644 index 00000000..9427bd52 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[another_module].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package.another_path.another_module") +package variousModulesPackage.anotherPath.anotherModule + +class AnotherClass() 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/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub similarity index 96% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_attribute_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub index f9ef79bc..f761891e 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/TestStubFileGeneration.test_stub_creation[attribute_module].sdsstub @@ -1,6 +1,8 @@ @PythonModule("various_modules_package.attribute_module") package variousModulesPackage.attributeModule +from tests.data.mainPackage.anotherPath.anotherModule import AnotherClass + class AttributesClassA() class AttributesClassB() { @@ -92,7 +94,7 @@ class AttributesClassB() { @PythonName("attr_type_from_outside_package") static attr attrTypeFromOutsidePackage: AnotherClass @PythonName("attr_default_value_from_outside_package") - static attr attrDefaultValueFromOutsidePackage: () -> a: AnotherClass + static attr attrDefaultValueFromOutsidePackage: () -> param1: AnotherClass @PythonName("init_attr") attr initAttr: Boolean diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub similarity index 89% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub index d601410b..c4059ac1 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_class_creation.sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[class_module].sdsstub @@ -1,6 +1,8 @@ @PythonModule("various_modules_package.class_module") package variousModulesPackage.classModule +from tests.data.mainPackage.anotherPath.anotherModule import yetAnotherClass + class ClassModuleEmptyClassA() class ClassModuleClassB( @@ -13,7 +15,7 @@ class ClassModuleClassB( } // TODO Safe-DS does not support multiple inheritance. -class ClassModuleClassC() sub ClassModuleEmptyClassA, ClassModuleClassB { +class ClassModuleClassC() sub ClassModuleEmptyClassA, ClassModuleClassB, yetAnotherClass { @PythonName("attr_1") static attr attr1: Int @PythonName("attr_2") diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[docstring_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[docstring_module].sdsstub new file mode 100644 index 00000000..85efda92 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[docstring_module].sdsstub @@ -0,0 +1,58 @@ +@PythonModule("various_modules_package.docstring_module") +package variousModulesPackage.docstringModule + +class EpydocDocstringClass( + @PythonName("param_1") param1: String +) { + @PythonName("attr_1") + static attr attr1: String + + @Pure + @PythonName("epydoc_docstring_func") + fun epydocDocstringFunc( + x: Int, + y: Int + ) -> result1: Boolean +} + +class RestDocstringClass( + @PythonName("param_1") param1: String +) { + @PythonName("attr_1") + static attr attr1: String + + @Pure + @PythonName("rest_docstring_func") + fun restDocstringFunc( + x: Int, + y: Int + ) -> result1: Boolean +} + +class NumpyDocstringClass( + @PythonName("param_1") param1: String +) { + @PythonName("attr_1") + static attr attr1: String + + @Pure + @PythonName("numpy_docstring_func") + fun numpyDocstringFunc( + x: Int, + y: Int + ) -> result1: Boolean +} + +class GoogleDocstringClass( + @PythonName("param_1") param1: String +) { + @PythonName("attr_1") + static attr attr1: String + + @Pure + @PythonName("google_docstring_func") + fun googleDocstringFunc( + x: Int, + y: Int + ) -> result1: Boolean +} 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/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_enum_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[enum_module].sdsstub 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/TestStubFileGeneration.test_stub_creation[function_module].sdsstub similarity index 97% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_function_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[function_module].sdsstub index cd5f8643..8a41218c 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/TestStubFileGeneration.test_stub_creation[function_module].sdsstub @@ -1,6 +1,8 @@ @PythonModule("various_modules_package.function_module") package variousModulesPackage.functionModule +from tests.data.mainPackage.anotherPath.anotherModule import AnotherClass + // TODO Result type information missing. @Pure @PythonName("public_no_params_no_result") @@ -230,8 +232,8 @@ fun anyResults() -> result1: Any @Pure @PythonName("callable_type") fun callableType( - param: (a: String) -> (b: Int, c: String) -) -> result1: (a: Int, b: Int) -> c: Int + param: (param1: String) -> (param2: Int, param3: String) +) -> result1: (param1: Int, param2: Int) -> param3: Int // TODO Result type information missing. // TODO Some parameter have no type information. 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/TestStubFileGeneration.test_stub_creation[import_module].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_import_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[import_module].sdsstub 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/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub similarity index 97% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_inference.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub index 22573858..03b492be 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/TestStubFileGeneration.test_stub_creation[infer_types_module].sdsstub @@ -21,7 +21,7 @@ class InferMyTypes( @PythonName("infer_none") static attr inferNone: Nothing? @PythonName("infer_obj") - static attr inferObj: () -> a: InferMe + static attr inferObj: () -> param1: InferMe // TODO Attribute has no type information. @PythonName("init_infer") attr initInfer diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_1].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_1].sdsstub new file mode 100644 index 00000000..fae86c54 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_1].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package.file_creation.module_1") +package variousModulesPackage.fileCreation.module1 + +class C() 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 new file mode 100644 index 00000000..7e20559d --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[module_5].sdsstub @@ -0,0 +1,4 @@ +@PythonModule("various_modules_package.file_creation.package_1.module_5") +package variousModulesPackage.fileCreation.package1.module5 + +class C() diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_var_creation.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[type_var_module].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_type_var_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[type_var_module].sdsstub 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/TestStubFileGeneration.test_stub_creation[variance_module].sdsstub similarity index 100% rename from tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_variance_creation.sdsstub rename to tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[variance_module].sdsstub diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_file_creation_limited_stubs_outside_package.sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_file_creation_limited_stubs_outside_package.sdsstub new file mode 100644 index 00000000..e736c1fd --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/test_file_creation_limited_stubs_outside_package.sdsstub @@ -0,0 +1,7 @@ +@PythonModule("tests.data.main_package.another_path.another_module") +package tests.data.mainPackage.anotherPath.anotherModule + +class AnotherClass + +@PythonName("yetAnotherClass") +class YetAnotherClass diff --git a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py index dbb71aee..cb9a25ab 100644 --- a/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py +++ b/tests/safeds_stubgen/stubs_generator/test_generate_stubs.py @@ -4,13 +4,21 @@ from typing import TYPE_CHECKING import pytest -from safeds_stubgen.api_analyzer import API, get_api +from safeds_stubgen.api_analyzer import get_api from safeds_stubgen.stubs_generator import generate_stubs # noinspection PyProtectedMember -from safeds_stubgen.stubs_generator._generate_stubs import StubsStringGenerator +from safeds_stubgen.stubs_generator._generate_stubs import ( + NamingConvention, + StubsStringGenerator, + _convert_name_to_convention, + _generate_stubs_data, + _generate_stubs_files, +) if TYPE_CHECKING: + from collections.abc import Generator + from syrupy import SnapshotAssertion # Setup - Run API to create stub files @@ -21,10 +29,18 @@ _out_dir_stubs = Path(_out_dir / _test_package_name) api = get_api(_test_package_name, _test_package_dir, is_test_run=True) -generate_stubs(api, _out_dir, convert_identifiers=True) +stubs_generator = StubsStringGenerator(api, naming_convention=NamingConvention.SAFE_DS) +stubs_data = _generate_stubs_data(api, _out_dir, stubs_generator) + + +def test_file_creation() -> None: + _generate_stubs_files(stubs_data, api, _out_dir, stubs_generator, naming_convention=NamingConvention.SAFE_DS) + _assert_file_creation_recursive( + python_path=Path(_test_package_dir / "file_creation"), + stub_path=Path(_out_dir_stubs / "file_creation"), + ) -# Utilites def _assert_file_creation_recursive(python_path: Path, stub_path: Path) -> None: assert python_path.is_dir() assert stub_path.is_dir() @@ -54,92 +70,68 @@ def _assert_file_creation_recursive(python_path: Path, stub_path: Path) -> None: _assert_file_creation_recursive(py_item, stub_item) -def assert_stubs_snapshot(filename: str, snapshot_sds_stub: SnapshotAssertion) -> None: - stubs_file = Path(_out_dir_stubs / filename / f"{filename}.sdsstub") - with stubs_file.open("r") as f: - assert f.read() == snapshot_sds_stub - +def test_file_creation_limited_stubs_outside_package(snapshot_sds_stub: SnapshotAssertion) -> None: + # Somehow the stubs get overwritten by other tests, therefore we have to call the function before asserting + generate_stubs(api, _out_dir, convert_identifiers=True) + path = Path(_out_dir / "tests/data/main_package/another_path/another_module/another_module.sdsstub") + assert path.is_file() -# ############################## Tests ############################## # -def test_file_creation() -> None: - _assert_file_creation_recursive( - python_path=Path(_test_package_dir / "file_creation"), - stub_path=Path(_out_dir_stubs / "file_creation"), - ) - - -def test_class_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("class_module", snapshot_sds_stub) - - -def test_class_attribute_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("attribute_module", snapshot_sds_stub) - - -def test_function_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("function_module", snapshot_sds_stub) - - -def test_enum_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("enum_module", snapshot_sds_stub) - - -def test_import_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("import_module", snapshot_sds_stub) - - -def test_type_inference(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("infer_types_module", snapshot_sds_stub) + with path.open("r") as f: + assert f.read() == snapshot_sds_stub -def test_variance_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("variance_module", snapshot_sds_stub) +def _python_files() -> Generator: + return Path(_test_package_dir).rglob(pattern="*.py") -def test_abstract_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("abstract_module", snapshot_sds_stub) +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 test_type_var_creation(snapshot_sds_stub: SnapshotAssertion) -> None: - assert_stubs_snapshot("type_var_module", snapshot_sds_stub) +@pytest.mark.parametrize("python_file", _python_files(), ids=_python_file_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 -@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() + # For these files stubs won't get created, because they are either empty or private. + if file_name in {"__init__", "_reexport_module_3", "_module_2", "_module_4"}: + return - assert file_data == snapshot_sds_stub + raise AssertionError(f"Stub file not found for '{file_name}'.") @pytest.mark.parametrize( - ("name", "expected_result", "is_class_name", "convert_identifiers"), + ("name", "expected_result", "naming_convention", "is_class_name"), [ - ("", "", False, True), - ("_", "_", False, True), - ("__get_function_name__", "getFunctionName", False, True), - ("__get_function_name", "getFunctionName", False, True), - ("get_function_name__", "getFunctionName", False, True), - ("__getFunction_name__", "getFunctionName", False, True), - ("__get__function___name__", "getFunctionName", False, True), - ("__get_funCtion_NamE__", "getFunCtionNamE", False, True), - ("getFunctionName", "getFunctionName", False, True), - ("a_a_A_aAAaA_1_1_2_aAa", "aAAAAAaA112AAa", False, True), - ("some_class_name", "SomeClassName", True, True), - ("some_function_name", "some_function_name", False, False), - ("some_class_name", "some_class_name", True, False), + ("", "", "Safe-DS", False), + ("_", "_", "Safe-DS", False), + ("__get_function_name__", "getFunctionName", NamingConvention.SAFE_DS, False), + ("__get_function_name", "getFunctionName", NamingConvention.SAFE_DS, False), + ("get_function_name__", "getFunctionName", NamingConvention.SAFE_DS, False), + ("__getFunction_name__", "getFunctionName", NamingConvention.SAFE_DS, False), + ("__get__function___name__", "getFunctionName", NamingConvention.SAFE_DS, False), + ("__get_funCtion_NamE__", "getFunCtionNamE", NamingConvention.SAFE_DS, False), + ("getFunctionName", "getFunctionName", NamingConvention.SAFE_DS, False), + ("a_a_A_aAAaA_1_1_2_aAa", "aAAAAAaA112AAa", NamingConvention.SAFE_DS, False), + ("some_class_name", "SomeClassName", NamingConvention.SAFE_DS, True), + ("some_class_name", "some_class_name", NamingConvention.PYTHON, True), + ("__get_function_name__", "__get_function_name__", NamingConvention.PYTHON, False), ], ) -def test_convert_snake_to_camel_case( +def test_convert_name_to_convention( name: str, expected_result: str, + naming_convention: NamingConvention, is_class_name: bool, - convert_identifiers: bool, ) -> None: - stubs_string_generator = StubsStringGenerator( - api=API(distribution="", package=_test_package_name, version=""), - convert_identifiers=convert_identifiers, + assert ( + _convert_name_to_convention(name=name, naming_convention=naming_convention, is_class_name=is_class_name) + == expected_result ) - assert stubs_string_generator._convert_snake_to_camel_case(name, is_class_name) == expected_result