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

feat: Rework import generation for stubs. #50

Merged
merged 58 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a0c7d50
Ignore type information if these are from another package; Added test…
Masara Dec 7, 2023
559eefd
Added more test data
Masara Dec 7, 2023
be0f30b
Ignore type information if these are from another package for return …
Masara Dec 8, 2023
bfe72f2
Moved the check if a type is from the same package to the stub genera…
Masara Dec 10, 2023
b0ec7fe
style: apply automated linter fixes
megalinter-bot Dec 10, 2023
8c50f8e
style: apply automated linter fixes
megalinter-bot Dec 10, 2023
9b683bd
Refactoring for code coverage
Masara Dec 10, 2023
336e551
Merge remote-tracking branch 'origin/ignore_types_from_outside_packag…
Masara Dec 10, 2023
36fea1f
Refactoring for code coverage and adding a test case
Masara Dec 10, 2023
08d203b
style: apply automated linter fixes
megalinter-bot Dec 10, 2023
953e4fd
Merge branch 'main' into ignore_types_from_outside_packages
Masara Dec 10, 2023
f800cad
Merge remote-tracking branch 'origin/ignore_types_from_outside_packag…
Masara Dec 10, 2023
9396531
Changed how imports are generated for the stubs
Masara Dec 10, 2023
e9e91d5
style: apply automated linter fixes
megalinter-bot Dec 10, 2023
925c8d3
The api-analyzer now can handle aliasing for types; Adjusted tests
Masara Dec 16, 2023
b81a24d
Merge remote-tracking branch 'origin/ignore_types_from_outside_packag…
Masara Dec 16, 2023
b2a0a77
Adjusted test snapshots
Masara Dec 16, 2023
ffe353e
It is now possible to resolve aliases for superclasses; adjusted tests
Masara Dec 16, 2023
8d63812
Fixed a bug where types and imports would not be generated for param …
Masara Dec 16, 2023
f12624e
Added imports for subclasses; Refactoring
Masara Dec 16, 2023
1b02852
Removed unimportant todo markings for now
Masara Dec 16, 2023
37a92f9
linter fixes
Masara Dec 16, 2023
9c20382
linter fixes
Masara Dec 17, 2023
b92232b
linter fixes
Masara Dec 17, 2023
ee2079e
linter fixes
Masara Dec 17, 2023
78c4973
style: apply automated linter fixes
megalinter-bot Dec 17, 2023
45d8666
style: apply automated linter fixes
megalinter-bot Dec 17, 2023
fae277c
Refactoring for Codecov
Masara Dec 17, 2023
aefc3d4
Adding more test data for Codecov (not working yet)
Masara Dec 18, 2023
905ab1e
Adjusted todos
Masara Dec 28, 2023
85046f2
Merge branch 'main' into rework_import_generation
Masara Feb 20, 2024
90c9efd
style: apply automated linter fixes
megalinter-bot Feb 20, 2024
cd5b7b2
An error has been corrected which occurred when a class from the same…
Masara Feb 20, 2024
5c08515
Merge remote-tracking branch 'origin/ignore_types_from_outside_packag…
Masara Feb 20, 2024
d7a48f3
snapshot update
Masara Feb 20, 2024
9e7c35d
Added a "// Todo" if an internal class is used as type
Masara Feb 20, 2024
5c98df7
Codecov fixes
Masara Feb 20, 2024
01355f0
style: apply automated linter fixes
megalinter-bot Feb 20, 2024
fc93115
Fixed tests which I broke with the last commit.
Masara Feb 20, 2024
a7b5f88
codecov fix
Masara Feb 20, 2024
5705616
style: apply automated linter fixes
megalinter-bot Feb 20, 2024
b172648
style: apply automated linter fixes
megalinter-bot Feb 20, 2024
cd61528
codecov and test fixes
Masara Feb 20, 2024
e8f739d
Refactoring
Masara Feb 20, 2024
f209301
Added a test case for aliasing
Masara Feb 21, 2024
50b149e
Merge branch 'main' into rework_import_generation
Masara Feb 21, 2024
4e59ebe
Adding a missing line that was lost with the last merge with the main…
Masara Feb 21, 2024
bb2a761
WIP test config
Masara Feb 22, 2024
f679835
style: apply automated linter fixes
megalinter-bot Feb 22, 2024
8de08be
Merge branch 'main' into rework_import_generation
Masara Feb 23, 2024
7eaf17b
WIP adjusted snapshot tests files
Masara Feb 23, 2024
83c062e
Fixed paths for imports, added a "// Todo" for the usage sets in stub…
Masara Feb 23, 2024
9dcb01d
Fixed a bug in the stub generator where string default values would n…
Masara Feb 23, 2024
4c72850
adjusted test snapshots
Masara Feb 23, 2024
e9d87c1
linter fix
Masara Feb 23, 2024
5c28761
style: apply automated linter fixes
megalinter-bot Feb 23, 2024
efde8b4
Stubs generator: Removed imports from outside the currently analyzed …
Masara Feb 24, 2024
47077b6
Merge remote-tracking branch 'origin/ignore_types_from_outside_packag…
Masara Feb 24, 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
1 change: 1 addition & 0 deletions src/safeds_stubgen/api_analyzer/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class Parameter:
id: str
name: str
is_optional: bool
# We do not support default values that aren't core classes or classes definied in the package we analyze.
default_value: str | bool | int | float | None
assigned_by: ParameterAssignment
docstring: ParameterDocstring
Expand Down
347 changes: 258 additions & 89 deletions src/safeds_stubgen/api_analyzer/_ast_visitor.py

Large diffs are not rendered by default.

192 changes: 142 additions & 50 deletions src/safeds_stubgen/api_analyzer/_get_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

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

import mypy.build as mypy_build
import mypy.main as mypy_main
from mypy import nodes as mypy_nodes
from mypy import types as mypy_types

from safeds_stubgen.docstring_parsing import DocstringStyle, create_docstring_parser

Expand All @@ -13,11 +16,6 @@
from ._ast_walker import ASTWalker
from ._package_metadata import distribution, distribution_version, package_root

if TYPE_CHECKING:
from pathlib import Path

from mypy.nodes import MypyFile


def get_api(
package_name: str,
Expand All @@ -29,17 +27,8 @@ def get_api(
if root is None:
root = package_root(package_name)

# Get distribution data
dist = distribution(package_name) or ""
dist_version = distribution_version(dist) or ""

# Setup api walker
api = API(dist, package_name, dist_version)
docstring_parser = create_docstring_parser(docstring_style)
callable_visitor = MyPyAstVisitor(docstring_parser, api)
walker = ASTWalker(callable_visitor)

walkable_files = []
package_paths = []
for file_path in root.glob(pattern="./**/*.py"):
logging.info(
"Working on file {posix_path}",
Expand All @@ -51,48 +40,151 @@ def get_api(
logging.info("Skipping test file")
continue

# Check if the current file is an init file
if file_path.parts[-1] == "__init__.py":
# if a directory contains an __init__.py file it's a package
package_paths.append(
file_path.parent,
)
continue

walkable_files.append(str(file_path))

mypy_trees = _get_mypy_ast(walkable_files, root)
for tree in mypy_trees:
if not walkable_files:
raise ValueError("No files found to analyse.")

# Get distribution data
dist = distribution(package_name) or ""
dist_version = distribution_version(dist) or ""

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

# Setup api walker
api = API(dist, package_name, dist_version)
docstring_parser = create_docstring_parser(docstring_style)
callable_visitor = MyPyAstVisitor(docstring_parser, api, aliases)
walker = ASTWalker(callable_visitor)

for tree in mypy_asts:
walker.walk(tree)

return callable_visitor.api


def _get_mypy_ast(files: list[str], root: Path) -> list[MypyFile]:
if not files:
raise ValueError("No files found to analyse.")

# Build mypy checker
def _get_mypy_build(files: list[str]) -> mypy_build.BuildResult:
"""Build a mypy checker and return the build result."""
mypyfiles, opt = mypy_main.process_options(files)
opt.preserve_asts = True # Disable the memory optimization of freeing ASTs when possible
opt.fine_grained_incremental = True # Only check parts of the code that have changed since the last check
result = mypy_build.build(mypyfiles, options=opt)

# Check mypy data key root start
graphs = result.graph
graph_keys = list(graphs.keys())
root_path = str(root)

# Get the needed data from mypy. The __init__ files need to be checked first, since we have to get the
# reexported data for the packages first
results = []
init_results = []
for graph_key in graph_keys:
graph = graphs[graph_key]
graph_path = graph.abspath

if graph_path is None: # pragma: no cover
raise ValueError("Could not parse path of a module.")

tree = graph.tree
if tree is None or root_path not in graph_path or not graph_path.endswith(".py"):
continue
# Disable the memory optimization of freeing ASTs when possible
opt.preserve_asts = True
# Only check parts of the code that have changed since the last check
opt.fine_grained_incremental = True
# Export inferred types for all expressions
opt.export_types = True

return mypy_build.build(mypyfiles, options=opt)

if graph_path.endswith("__init__.py"):
init_results.append(tree)
else:
results.append(tree)

return init_results + results
def _get_mypy_asts(
build_result: mypy_build.BuildResult,
files: list[str],
package_paths: list[Path],
root: Path,
) -> 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


def _get_aliases(result_types: dict, package_name: str) -> dict[str, set[str]]:
aliases: dict[str, set[str]] = defaultdict(set)
for key in result_types:
if isinstance(key, mypy_nodes.NameExpr | mypy_nodes.MemberExpr | mypy_nodes.TypeVarExpr):
in_package = False
name = ""

if isinstance(key, mypy_nodes.NameExpr):
type_value = result_types[key]

if hasattr(type_value, "type") and getattr(type_value, "type", None) is not None:
name = type_value.type.name
in_package = package_name in type_value.type.fullname
elif hasattr(key, "name"):
name = key.name
fullname = ""

if (
hasattr(key, "node")
and isinstance(key.node, mypy_nodes.TypeAlias)
and isinstance(key.node.target, mypy_types.Instance)
):
fullname = key.node.target.type.fullname
elif isinstance(type_value, mypy_types.CallableType):
bound_args = type_value.bound_args
if bound_args and hasattr(bound_args[0], "type"):
fullname = bound_args[0].type.fullname # type: ignore[union-attr]
elif hasattr(key, "node") and isinstance(key.node, mypy_nodes.Var):
fullname = key.node.fullname

if not fullname:
continue

in_package = package_name in fullname
else:
in_package = package_name in key.fullname
if in_package:
type_value = result_types[key]
name = key.name
else:
continue

if in_package:
if isinstance(type_value, mypy_types.CallableType) and hasattr(type_value.bound_args[0], "type"):
fullname = type_value.bound_args[0].type.fullname # type: ignore[union-attr]
elif isinstance(type_value, mypy_types.Instance):
fullname = type_value.type.fullname
elif isinstance(key, mypy_nodes.TypeVarExpr):
fullname = key.fullname
elif isinstance(key, mypy_nodes.NameExpr) and isinstance(key.node, mypy_nodes.Var):
fullname = key.node.fullname
else: # pragma: no cover
raise TypeError("Received unexpected type while searching for aliases.")

aliases[name].add(fullname)

return aliases
Loading