From 71b38d7635b59ef6892c17624472a32db464a940 Mon Sep 17 00:00:00 2001 From: Arsam Islami Date: Mon, 4 Mar 2024 21:12:29 +0100 Subject: [PATCH] feat: Create stubs for public methods of inherited internal classes (#69) Closes #64 ### Summary of Changes Now stubs for public methods of internal classes that are being inherited will be created for the classes that inherit them. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- .../stubs_generator/_generate_stubs.py | 64 +++++++++++++++++-- .../inheritance_module.py | 35 ++++++++++ ...t_stub_creation[aliasing_module_1].sdsstub | 2 +- ..._stub_creation[inheritance_module].sdsstub | 38 +++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 tests/data/various_modules_package/inheritance_module.py create mode 100644 tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[inheritance_module].sdsstub diff --git a/src/safeds_stubgen/stubs_generator/_generate_stubs.py b/src/safeds_stubgen/stubs_generator/_generate_stubs.py index b3b9a768..87935228 100644 --- a/src/safeds_stubgen/stubs_generator/_generate_stubs.py +++ b/src/safeds_stubgen/stubs_generator/_generate_stubs.py @@ -254,13 +254,25 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st # Superclasses superclasses = class_.superclasses superclass_info = "" + superclass_methods_text = "" if superclasses and not class_.is_abstract: superclass_names = [] for superclass in superclasses: - superclass_names.append(superclass.split(".")[-1]) + superclass_name = superclass.split(".")[-1] self._add_to_imports(superclass) - superclass_info = f" sub {', '.join(superclass_names)}" + if superclass not in self.module_imports and is_internal(superclass_name): + # If the superclass was not added to the module_imports through the _add_to_imports method, it means + # that the superclass is a class from the same module. + # For internal superclasses, we have to add their public members to subclasses. + superclass_methods_text += self._create_internal_class_methods_string( + superclass=superclass, + inner_indentations=inner_indentations, + ) + else: + superclass_names.append(superclass_name) + + superclass_info = f" sub {', '.join(superclass_names)}" if superclass_names else "" if len(superclasses) > 1: self._current_todo_msgs.add("multiple_inheritance") @@ -312,6 +324,9 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st for inner_class in class_.classes: class_text += f"\n{self._create_class_string(inner_class, inner_indentations)}\n" + # Superclass methods, if the superclass is an internal class + class_text += superclass_methods_text + # Methods class_text += self._create_class_method_string(class_.methods, inner_indentations) @@ -324,11 +339,17 @@ def _create_class_string(self, class_: Class, class_indentation: str = "") -> st return f"{class_signature} {{{class_text}" - def _create_class_method_string(self, methods: list[Function], inner_indentations: str) -> str: + def _create_class_method_string( + self, + methods: list[Function], + inner_indentations: str, + is_internal_class: bool = False, + ) -> str: class_methods: list[str] = [] class_property_methods: list[str] = [] for method in methods: - if not method.is_public: + # Add methods of internal classes that are inherited if the methods themselfe are public + if not method.is_public and (not is_internal_class or (is_internal_class and is_internal(method.name))): continue elif method.is_property: class_property_methods.append( @@ -744,6 +765,26 @@ def _create_type_string(self, type_data: dict | None) -> str: raise ValueError(f"Unexpected type: {kind}") # pragma: no cover + def _create_internal_class_methods_string(self, superclass: str, inner_indentations: str) -> str: + superclass_name = superclass.split(".")[-1] + + superclass_class = self._get_class_in_module(superclass_name) + superclass_methods_text = self._create_class_method_string( + superclass_class.methods, + inner_indentations, + is_internal_class=True, + ) + + for superclass_superclass in superclass_class.superclasses: + name = superclass_superclass.split(".")[-1] + if is_internal(name): + superclass_methods_text += self._create_internal_class_methods_string( + superclass_superclass, + inner_indentations, + ) + + return superclass_methods_text + # ############################### Utilities ############################### # def _add_to_imports(self, qname: str) -> None: @@ -809,6 +850,17 @@ 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 the class is a nested class + for class_ in self.api.classes: + 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 _callable_type_name_generator() -> Generator: """Generate a name for callable type parameters starting from 'a' until 'zz'.""" @@ -860,6 +912,10 @@ def _replace_if_safeds_keyword(keyword: str) -> str: return keyword +def is_internal(name: str) -> bool: + return name.startswith("_") + + def _convert_name_to_convention( name: str, naming_convention: NamingConvention, diff --git a/tests/data/various_modules_package/inheritance_module.py b/tests/data/various_modules_package/inheritance_module.py new file mode 100644 index 00000000..e6910b67 --- /dev/null +++ b/tests/data/various_modules_package/inheritance_module.py @@ -0,0 +1,35 @@ +class PublicSuperClass: + def public_superclass_method(self) -> str: ... + + +class PublicSubClass(PublicSuperClass): + ... + + +class _PrivateInternalClass: + class _PrivateInternalNestedClass: + def public_internal_nested_class_method(self, a: None) -> bool: ... + + def public_internal_class_method(self, a: int) -> str: ... + + def _private_internal_class_method(self, b: list) -> None: ... + + +class PublicSubClass2(_PrivateInternalClass): + def public_subclass_method(self) -> str: ... + + +class PublicSubClassFromNested(_PrivateInternalClass._PrivateInternalNestedClass): + ... + + +class _TransitiveInternalClassA: + def transitive_class_fun(self, c: list) -> list: ... + + +class _TransitiveInternalClassB(_TransitiveInternalClassA): + pass + + +class InheritTransitively(_TransitiveInternalClassB): + pass diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub index 4c4542d2..3bc4f5d6 100644 --- a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[aliasing_module_1].sdsstub @@ -6,7 +6,7 @@ from variousModulesPackage.aliasing.aliasingModule3 import ImportMeAliasingModul class AliasingModuleClassB() -class AliasingModuleClassC() sub _AliasingModuleClassA { +class AliasingModuleClassC() { @PythonName("typed_alias_attr") static attr typedAliasAttr: AliasingModuleClassB // TODO An internal class must not be used as a type in a public class. diff --git a/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[inheritance_module].sdsstub b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[inheritance_module].sdsstub new file mode 100644 index 00000000..f424bcf9 --- /dev/null +++ b/tests/safeds_stubgen/stubs_generator/__snapshots__/test_generate_stubs/TestStubFileGeneration.test_stub_creation[inheritance_module].sdsstub @@ -0,0 +1,38 @@ +@PythonModule("various_modules_package.inheritance_module") +package variousModulesPackage.inheritanceModule + +class PublicSuperClass() { + @Pure + @PythonName("public_superclass_method") + fun publicSuperclassMethod() -> result1: String +} + +class PublicSubClass() sub PublicSuperClass + +class PublicSubClass2() { + @Pure + @PythonName("public_internal_class_method") + fun publicInternalClassMethod( + a: Int + ) -> result1: String + + @Pure + @PythonName("public_subclass_method") + fun publicSubclassMethod() -> result1: String +} + +class PublicSubClassFromNested() { + @Pure + @PythonName("public_internal_nested_class_method") + fun publicInternalNestedClassMethod( + a: Nothing? + ) -> result1: Boolean +} + +class InheritTransitively() { + @Pure + @PythonName("transitive_class_fun") + fun transitiveClassFun( + c: List + ) -> result1: List +}