Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Stub generation testing and fixing of miscellaneous bugs #76

Merged
merged 84 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
3250351
Added tests for generating stubs for public methods from inherited in…
Masara Feb 28, 2024
90f2d93
Added stubs generation for public methods of inherited intern classes
Masara Feb 28, 2024
f2ddff8
style: apply automated linter fixes
megalinter-bot Feb 28, 2024
efd55c6
style: apply automated linter fixes
megalinter-bot Feb 28, 2024
0156096
Merge branch 'main' into 64-stubs-for-public-methods
Masara Mar 1, 2024
c97c653
Merge branch 'main' into 64-stubs-for-public-methods
Masara Mar 1, 2024
1520720
Refactoring
Masara Mar 2, 2024
dfab52a
Merge branch 'main' into 64-stubs-for-public-methods
Masara Mar 3, 2024
dd4bc25
Test snapshot update after merge with main
Masara Mar 3, 2024
52c6288
Added an is_internal function, refactoring how internal superclasses …
Masara Mar 3, 2024
bc9309c
Internal superclasses are now also searched transitively for methods
Masara Mar 3, 2024
a4f61e8
Changed and fixed how mypy finds the python files of the package we w…
Masara Mar 3, 2024
b4db31c
Changed how imports are found in modules, previously, imports couldn'…
Masara Mar 3, 2024
48b13da
upped the code coverage
Masara Mar 3, 2024
54b7076
Added generated stub files from the "Library" project
Masara Mar 3, 2024
3c85366
Changed the way "package" information and imports are generated for s…
Masara Mar 4, 2024
29f7c99
Removed a redundant directory creation
Masara Mar 4, 2024
9498723
Updated Library-Stubs data
Masara Mar 4, 2024
217ef56
Updated Library-Stubs data - Added too much data by mistake in the la…
Masara Mar 4, 2024
97e05b7
fixed file creation test
Masara Mar 4, 2024
9f4405c
changed the get_api test for the modules, it now checks all existing …
Masara Mar 4, 2024
9d428e0
test fix for Python3.11
Masara Mar 4, 2024
83ebdcf
small change which could fix ubuntu tests (?)
Masara Mar 4, 2024
637450b
linter fixes
Masara Mar 4, 2024
71a311a
linter fixes
Masara Mar 4, 2024
8f2c6ac
style: apply automated linter fixes
megalinter-bot Mar 4, 2024
646a110
style: apply automated linter fixes
megalinter-bot Mar 4, 2024
4f0adce
Fixing a bug where stubs with "Nothing??" as type would be generated
Masara Mar 4, 2024
b943748
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 4, 2024
5d16778
Library-Stubs update
Masara Mar 4, 2024
547952c
added test data for code coverage
Masara Mar 4, 2024
24052c3
changed test data for code coverage
Masara Mar 4, 2024
ccf5a12
Intern classes won't be displayed as superclasses in stub files
Masara Mar 4, 2024
083dce7
style: apply automated linter fixes
megalinter-bot Mar 4, 2024
2c626eb
style: apply automated linter fixes
megalinter-bot Mar 4, 2024
1fa3b5f
Merge branch '64-stubs-for-public-methods' into stubs-testing-and-fixing
Masara Mar 4, 2024
0e80f83
Merge branch 'main' into stubs-testing-and-fixing
Masara Mar 4, 2024
8e4eec6
removed Boolean, Nothing and String from the sds keyword function; No…
Masara Mar 5, 2024
6086367
Removed superclasses for stubs if the superclasses inherit directly o…
Masara Mar 5, 2024
9f29421
style: apply automated linter fixes
megalinter-bot Mar 5, 2024
35ad11c
updated Library Stub files
Masara Mar 5, 2024
ba6a7ce
style: apply automated linter fixes
megalinter-bot Mar 5, 2024
78bc870
(WIP) creating stubs for reexported modules was bugged until now, now…
Masara Mar 5, 2024
7a0aa19
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 5, 2024
d81c548
refactoring and linter fixes
Masara Mar 5, 2024
a47091c
style: apply automated linter fixes
megalinter-bot Mar 5, 2024
b4c4ff1
style: apply automated linter fixes
megalinter-bot Mar 5, 2024
a83dcff
Added more test cases for reexporting classes and functions
Masara Mar 5, 2024
9f4a9c0
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 5, 2024
73fe169
Added more test cases for reexporting classes and functions
Masara Mar 5, 2024
fa21477
Omit classes in Safe-DS that import directly or transitively from Exc…
Masara Mar 6, 2024
df007b0
Added test cases for Mapping
Masara Mar 6, 2024
6a3d430
Mapping, Sequence and Collection types for classes can now be correct…
Masara Mar 6, 2024
30d16a5
Refactoring
Masara Mar 6, 2024
28da90b
Further expanding the "reexported" checks and adding test cases
Masara Mar 7, 2024
2aec8fb
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
249d42d
Refactoring
Masara Mar 7, 2024
15a63c5
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 7, 2024
9cef7c6
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
021e84e
Refactoring
Masara Mar 7, 2024
c91bd73
Reexports from other packages are now recognized as well for the stub…
Masara Mar 7, 2024
7ef3e1e
Refactoring
Masara Mar 7, 2024
b4986e1
linter fix
Masara Mar 7, 2024
ca53f19
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
b119ef5
removed todos
Masara Mar 7, 2024
a28f1e1
Fixed package names: Now the shortest public "reexport" is being used…
Masara Mar 7, 2024
28569c2
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
e5e8355
Fixed a bug with in the ast walker and updated Library-Stub files
Masara Mar 7, 2024
d150697
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
0b66c06
code cov fix
Masara Mar 7, 2024
562a2f4
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 7, 2024
88911c0
Fixed a bug where the is_public check for classes, attr and functions…
Masara Mar 7, 2024
9ca0282
style: apply automated linter fixes
megalinter-bot Mar 7, 2024
ce2108a
Added test data for code cov
Masara Mar 8, 2024
3728c01
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 8, 2024
815b228
Fixed package names (part 2): Now the shortest public "reexport" is b…
Masara Mar 11, 2024
da9f97f
style: apply automated linter fixes
megalinter-bot Mar 11, 2024
d934d35
Refactoring and adding test data
Masara Mar 11, 2024
779a8f3
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 11, 2024
ae73a21
Inferring types (attributes): Callable classes are removed
Masara Mar 11, 2024
f123596
style: apply automated linter fixes
megalinter-bot Mar 11, 2024
5397183
Fixed package names (part 3): fixed paths, which where missing elemen…
Masara Mar 12, 2024
32a2d16
Merge remote-tracking branch 'origin/stubs-testing-and-fixing' into s…
Masara Mar 12, 2024
faadddd
chore: remove library stubs
lars-reimann Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/safeds_stubgen/api_analyzer/_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum as PythonEnum
from typing import TYPE_CHECKING, Any
Expand Down Expand Up @@ -48,6 +49,8 @@ def __init__(self, distribution: str, package: str, version: str) -> None:
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:
self.modules[module.id] = module

Expand Down Expand Up @@ -170,6 +173,7 @@ class Class:
docstring: ClassDocstring
constructor: Function | None = None
constructor_fulldocstring: str = ""
inherits_from_exception: bool = False
reexported_by: list[Module] = field(default_factory=list)
attributes: list[Attribute] = field(default_factory=list)
methods: list[Function] = field(default_factory=list)
Expand All @@ -184,6 +188,7 @@ def to_dict(self) -> dict[str, Any]:
"is_public": self.is_public,
"superclasses": self.superclasses,
"constructor": self.constructor.to_dict() if self.constructor is not None else None,
"inherits_from_exception": self.inherits_from_exception,
"reexported_by": [module.id for module in self.reexported_by],
"attributes": [attribute.id for attribute in self.attributes],
"methods": [method.id for method in self.methods],
Expand Down
212 changes: 142 additions & 70 deletions src/safeds_stubgen/api_analyzer/_ast_visitor.py

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/safeds_stubgen/api_analyzer/_ast_walker.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __walk(self, node: MypyFile | ClassDef | Decorator | FuncDef | AssignmentStm
if isinstance(node, Decorator):
node = node.func

if node in visited_nodes:
if node in visited_nodes: # pragma: no cover
raise AssertionError("Node visited twice")
visited_nodes.add(node)

Expand Down Expand Up @@ -71,6 +71,9 @@ def __walk(self, node: MypyFile | ClassDef | Decorator | FuncDef | AssignmentStm
if isinstance(node, FuncDef) and node.name != "__init__":
continue

if isinstance(child_node, FuncDef) and isinstance(node, FuncDef):
continue

self.__walk(child_node, visited_nodes)
self.__leave(node)

Expand Down
64 changes: 24 additions & 40 deletions src/safeds_stubgen/api_analyzer/_get_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING

import mypy.build as mypy_build
import mypy.main as mypy_main
Expand All @@ -16,6 +16,9 @@
from ._ast_walker import ASTWalker
from ._package_metadata import distribution, distribution_version, package_root

if TYPE_CHECKING:
from pathlib import Path


def get_api(
package_name: str,
Expand Down Expand Up @@ -44,7 +47,7 @@ def get_api(
if file_path.parts[-1] == "__init__.py":
# if a directory contains an __init__.py file it's a package
package_paths.append(
file_path.parent,
str(file_path.parent),
)
continue

Expand All @@ -59,7 +62,7 @@ def get_api(

# Get mypy ast and aliases
build_result = _get_mypy_build(walkable_files)
mypy_asts = _get_mypy_asts(build_result, walkable_files, package_paths, root)
mypy_asts = _get_mypy_asts(build_result, walkable_files, package_paths)
aliases = _get_aliases(build_result.types, package_name)

# Setup api walker
Expand Down Expand Up @@ -91,44 +94,25 @@ def _get_mypy_build(files: list[str]) -> mypy_build.BuildResult:
def _get_mypy_asts(
build_result: mypy_build.BuildResult,
files: list[str],
package_paths: list[Path],
root: Path,
package_paths: list[str],
) -> list[mypy_nodes.MypyFile]:
# Check mypy data key root start
parts = root.parts
graph_keys = list(build_result.graph.keys())
root_start_after = -1
for i in range(len(parts)):
if ".".join(parts[i:]) in graph_keys:
root_start_after = i
break

# Create the keys for getting the corresponding data
packages = [
".".join(
package_path.parts[root_start_after:],
).replace(".py", "")
for package_path in package_paths
]

modules = [
".".join(
Path(file).parts[root_start_after:],
).replace(".py", "")
for file in files
]

# Get the needed data from mypy. The packages need to be checked first, since we have
# to get the reexported data first
all_paths = packages + modules

asts = []
for path_key in all_paths:
tree = build_result.graph[path_key].tree
if tree is not None:
asts.append(tree)

return asts
package_ast = []
module_ast = []
for graph_key in build_result.graph:
ast = build_result.graph[graph_key].tree

if ast is None: # pragma: no cover
raise ValueError

if ast.path.endswith("__init__.py"):
ast_package_path = ast.path.split("__init__.py")[0][:-1]
if ast_package_path in package_paths:
package_ast.append(ast)
elif ast.path in files:
module_ast.append(ast)

# The packages need to be checked first, since we have to get the reexported data first
return package_ast + module_ast


def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
class AbstractDocstringParser(ABC):
@abstractmethod
def get_class_documentation(self, class_node: nodes.ClassDef) -> ClassDocstring:
pass
pass # pragma: no cover

@abstractmethod
def get_function_documentation(self, function_node: nodes.FuncDef) -> FunctionDocstring:
pass
pass # pragma: no cover

@abstractmethod
def get_parameter_documentation(
Expand All @@ -34,16 +34,16 @@ def get_parameter_documentation(
parameter_assigned_by: ParameterAssignment,
parent_class: Class | None,
) -> ParameterDocstring:
pass
pass # pragma: no cover

@abstractmethod
def get_attribute_documentation(
self,
parent_class: Class,
attribute_name: str,
) -> AttributeDocstring:
pass
pass # pragma: no cover

@abstractmethod
def get_result_documentation(self, function_node: nodes.FuncDef) -> ResultDocstring:
pass
pass # pragma: no cover
3 changes: 2 additions & 1 deletion src/safeds_stubgen/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The entrypoint to the program."""

from __future__ import annotations

import time
Expand All @@ -16,5 +17,5 @@ def main() -> None:
print(f"Program ran in {time.time() - start_time}s") # noqa: T201


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
main()
74 changes: 65 additions & 9 deletions src/safeds_stubgen/stubs_generator/_generate_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
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)
_generate_stubs_files(stubs_data, out_path, stubs_generator, naming_convention)


def _generate_stubs_data(
Expand Down Expand Up @@ -76,13 +76,10 @@

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)
Expand Down Expand Up @@ -179,7 +176,7 @@

def _create_module_string(self, module: Module) -> str:
# Create package info
package_info = module.id.replace("/", ".")
package_info = self._get_shortest_public_reexport()
package_info_camel_case = _convert_name_to_convention(package_info, self.naming_convention)
module_name_info = ""
module_text = ""
Expand All @@ -194,7 +191,7 @@

# Create classes, class attr. & class methods
for class_ in module.classes:
if class_.is_public:
if class_.is_public and not class_.inherits_from_exception:
module_text += f"\n{self._create_class_string(class_)}\n"

# Create enums & enum instances
Expand Down Expand Up @@ -280,14 +277,18 @@
# Type parameters
constraints_info = ""
variance_info = ""
if class_.type_parameters:

constructor_type_vars = None
if class_.constructor:
constructor_type_vars = class_.constructor.type_var_types

if class_.type_parameters or constructor_type_vars:
# We collect the class generics for the methods later
self.class_generics = []
out = "out "
for variance in class_.type_parameters:
variance_direction = {
VarianceKind.INVARIANT.name: "",
VarianceKind.COVARIANT.name: out,
VarianceKind.COVARIANT.name: "out ",
VarianceKind.CONTRAVARIANT.name: "in ",
}[variance.variance.name]

Expand All @@ -300,6 +301,11 @@
variance_item = f"{variance_item} sub {self._create_type_string(variance.type.to_dict())}"
self.class_generics.append(variance_item)

if constructor_type_vars:
for constructor_type_var in constructor_type_vars:
if constructor_type_var.name not in self.class_generics:
self.class_generics.append(constructor_type_var.name)

if self.class_generics:
variance_info = f"<{', '.join(self.class_generics)}>"

Expand Down Expand Up @@ -729,6 +735,8 @@
if len(types) == 2 and none_type_name in types:
# if None is at least one of the two possible types, we can remove the None and just return the
# other type with a question mark
if types[0] == none_type_name:
return f"{types[1]}?"
return f"{types[0]}?"

# If the union contains only one type, return the type instead of creating a union
Expand Down Expand Up @@ -861,6 +869,54 @@

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 len(shortest_id) == 1:
break

Check warning on line 914 in src/safeds_stubgen/stubs_generator/_generate_stubs.py

View check run for this annotation

Codecov / codecov/patch

src/safeds_stubgen/stubs_generator/_generate_stubs.py#L914

Added line #L914 was not covered by tests

if shortest_id is None:
return module_qname
return ".".join(shortest_id)


def _callable_type_name_generator() -> Generator:
"""Generate a name for callable type parameters starting from 'a' until 'zz'."""
Expand Down
8 changes: 8 additions & 0 deletions tests/data/various_modules_package/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
from ._reexport_module_3 import *
from ._reexport_module_4 import FourthReexportClass
from ._reexport_module_4 import _reexported_function_4
from ._reexport_module_4 import _reexported_function_4_alias as reexported_function_4_alias
from ._reexport_module_4 import _two_times_reexported
from ._reexport_module_4 import _two_times_reexported as two_times_reexported
from .enum_module import _ReexportedEmptyEnum
from file_creation._module_3 import Reexported

__all__ = [
"reex_1",
"ReexportClass",
"reexported_function_2",
"reexported_function_3",
"_reexported_function_4",
"reexported_function_4_alias",
"_two_times_reexported",
"two_times_reexported",
"FourthReexportClass",
"_ReexportedEmptyEnum",
"Reexported",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from tests.data.various_modules_package._reexport_module_5 import reexported_in_an_internal_package
8 changes: 8 additions & 0 deletions tests/data/various_modules_package/_reexport_module_4.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ def _unreexported_function() -> None:

def _reexported_function_4() -> None:
pass


def _reexported_function_4_alias() -> None:
pass


def _two_times_reexported() -> None:
pass
1 change: 1 addition & 0 deletions tests/data/various_modules_package/_reexport_module_5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def reexported_in_an_internal_package(): ...
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ class AliasingModuleClassC(_some_alias_a):
typed_alias_attr2: AliasModule2
infer_alias_attr2 = AliasModule2

infer_alias_attr3 = _some_alias_a

alias_list: list[_some_alias_a | some_alias_b, AliasModule2, ImportMeAlias]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def reexported_in_another_package_function() -> str: ...


class ReexportedInAnotherPackageClass:
pass


def _still_internal_function(): ...
Loading