taut.tk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- taut/__init__.py +35 -0
- taut/cli/__init__.py +36 -0
- taut/cli/stub_model.py +161 -0
- taut/cli/stubs.py +393 -0
- taut/cli/watch.py +218 -0
- taut/component.py +114 -0
- taut/context.py +108 -0
- taut/control.py +493 -0
- taut/layout.py +205 -0
- taut/nodes.py +308 -0
- taut/props.py +119 -0
- taut/reactive.py +135 -0
- taut/resource.py +235 -0
- taut/runtime.py +466 -0
- taut/scheduler.py +52 -0
- taut/stores.py +490 -0
- taut/style.py +247 -0
- taut/tk.py +304 -0
- taut/tk_props.py +735 -0
- taut/ttk.py +272 -0
- taut_tk-0.1.0.dist-info/METADATA +215 -0
- taut_tk-0.1.0.dist-info/RECORD +24 -0
- taut_tk-0.1.0.dist-info/WHEEL +4 -0
- taut_tk-0.1.0.dist-info/entry_points.txt +3 -0
taut/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""A Solid-style layer for Tkinter powered by reaktiv."""
|
|
2
|
+
|
|
3
|
+
from . import context
|
|
4
|
+
from . import control
|
|
5
|
+
from . import layout
|
|
6
|
+
from . import props
|
|
7
|
+
from . import reactive
|
|
8
|
+
from . import resource
|
|
9
|
+
from . import runtime
|
|
10
|
+
from . import stores
|
|
11
|
+
from . import style
|
|
12
|
+
from . import tk
|
|
13
|
+
from . import tk_props
|
|
14
|
+
from . import ttk
|
|
15
|
+
from .component import Component
|
|
16
|
+
from .component import component
|
|
17
|
+
from .runtime import Fragment
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Component",
|
|
21
|
+
"Fragment",
|
|
22
|
+
"component",
|
|
23
|
+
"context",
|
|
24
|
+
"control",
|
|
25
|
+
"layout",
|
|
26
|
+
"props",
|
|
27
|
+
"reactive",
|
|
28
|
+
"resource",
|
|
29
|
+
"runtime",
|
|
30
|
+
"style",
|
|
31
|
+
"stores",
|
|
32
|
+
"tk",
|
|
33
|
+
"tk_props",
|
|
34
|
+
"ttk",
|
|
35
|
+
]
|
taut/cli/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from taut.cli.stubs import main_cli as stubby
|
|
5
|
+
from taut.cli.watch import main as watcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(description="taut.tk development tools.")
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
11
|
+
|
|
12
|
+
parser_stubs = subparsers.add_parser("stubs", help="Generate taut.tk .pyi stubs.")
|
|
13
|
+
parser_stubs.add_argument("paths", nargs="+", type=Path)
|
|
14
|
+
parser_stubs.add_argument(
|
|
15
|
+
"-o",
|
|
16
|
+
"--out-dir",
|
|
17
|
+
default="typings",
|
|
18
|
+
type=Path,
|
|
19
|
+
help="Directory for generated stubs. Defaults to typings.",
|
|
20
|
+
)
|
|
21
|
+
parser_stubs.add_argument(
|
|
22
|
+
"--in-place",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Write sibling .pyi files next to source files.",
|
|
25
|
+
)
|
|
26
|
+
parser_stubs.set_defaults(func=stubby)
|
|
27
|
+
|
|
28
|
+
parser_watch = subparsers.add_parser("watch", help="Regenerate .pyi stubs on edits.")
|
|
29
|
+
parser_watch.set_defaults(func=watcher)
|
|
30
|
+
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
args.func(args)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
taut/cli/stub_model.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PropField:
|
|
9
|
+
name: str
|
|
10
|
+
internal_type: str
|
|
11
|
+
external_type: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ComponentStub:
|
|
16
|
+
name: str
|
|
17
|
+
fields: list[PropField]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ImportStub:
|
|
22
|
+
module: str
|
|
23
|
+
name: str
|
|
24
|
+
export_name: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def collect_protocols(module: ast.Module) -> dict[str, list[PropField]]:
|
|
28
|
+
protocols: dict[str, list[PropField]] = {}
|
|
29
|
+
for node in module.body:
|
|
30
|
+
if not isinstance(node, ast.ClassDef):
|
|
31
|
+
continue
|
|
32
|
+
if not any(base_name(base) == "Protocol" for base in node.bases):
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
protocols[node.name] = [
|
|
36
|
+
PropField(
|
|
37
|
+
statement.target.id,
|
|
38
|
+
ast.unparse(statement.annotation),
|
|
39
|
+
external_prop_type(ast.unparse(statement.annotation)),
|
|
40
|
+
)
|
|
41
|
+
for statement in node.body
|
|
42
|
+
if isinstance(statement, ast.AnnAssign)
|
|
43
|
+
and isinstance(statement.target, ast.Name)
|
|
44
|
+
]
|
|
45
|
+
return protocols
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def collect_components(
|
|
49
|
+
module: ast.Module,
|
|
50
|
+
protocols: dict[str, list[PropField]],
|
|
51
|
+
) -> list[ComponentStub]:
|
|
52
|
+
components: list[ComponentStub] = []
|
|
53
|
+
for node in module.body:
|
|
54
|
+
if isinstance(node, ast.FunctionDef) and is_component_function(node):
|
|
55
|
+
protocol_name = first_arg_annotation(node)
|
|
56
|
+
elif isinstance(node, ast.ClassDef) and is_component_class(node):
|
|
57
|
+
protocol_name = init_props_annotation(node)
|
|
58
|
+
else:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if protocol_name is None:
|
|
62
|
+
components.append(ComponentStub(node.name, []))
|
|
63
|
+
elif protocol_name in protocols:
|
|
64
|
+
components.append(ComponentStub(node.name, protocols[protocol_name]))
|
|
65
|
+
return components
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def collect_public_imports(module: ast.Module) -> list[ImportStub]:
|
|
69
|
+
imports: list[ImportStub] = []
|
|
70
|
+
for node in module.body:
|
|
71
|
+
if not isinstance(node, ast.ImportFrom) or node.module is None:
|
|
72
|
+
continue
|
|
73
|
+
if node.module in {"__future__", "typing"} or node.module.startswith("solid_tk"):
|
|
74
|
+
continue
|
|
75
|
+
module_name = "." * node.level + node.module
|
|
76
|
+
for alias in node.names:
|
|
77
|
+
export_name = alias.asname or alias.name
|
|
78
|
+
if export_name.startswith("_"):
|
|
79
|
+
continue
|
|
80
|
+
imports.append(ImportStub(module_name, alias.name, export_name))
|
|
81
|
+
return imports
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def render_component(component: ComponentStub) -> list[str]:
|
|
85
|
+
fields = [field for field in component.fields if field.name != "children"]
|
|
86
|
+
children_field = next(
|
|
87
|
+
(field for field in component.fields if field.name == "children"),
|
|
88
|
+
None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not component.fields:
|
|
92
|
+
return [
|
|
93
|
+
f"def {component.name}("
|
|
94
|
+
"*child_nodes: Any, children: Any = ..."
|
|
95
|
+
") -> runtime.Node: ..."
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
lines = [f"def {component.name}(", " *child_nodes: Any,"]
|
|
99
|
+
lines.extend(f" {field.name}: {field.external_type}," for field in fields)
|
|
100
|
+
if children_field is not None:
|
|
101
|
+
lines.append(f" children: {children_field.external_type},")
|
|
102
|
+
else:
|
|
103
|
+
lines.append(" children: Any = ...,")
|
|
104
|
+
lines.append(") -> runtime.Node: ...")
|
|
105
|
+
return lines
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_component_function(node: ast.FunctionDef) -> bool:
|
|
109
|
+
return any(base_name(decorator) == "component" for decorator in node.decorator_list)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_component_class(node: ast.ClassDef) -> bool:
|
|
113
|
+
return any(base_name(base) == "Component" for base in node.bases)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def first_arg_annotation(node: ast.FunctionDef) -> str | None:
|
|
117
|
+
if not node.args.args:
|
|
118
|
+
return None
|
|
119
|
+
annotation = node.args.args[0].annotation
|
|
120
|
+
return ast.unparse(annotation) if annotation is not None else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def init_props_annotation(node: ast.ClassDef) -> str | None:
|
|
124
|
+
for statement in node.body:
|
|
125
|
+
if not isinstance(statement, ast.FunctionDef) or statement.name != "__init__":
|
|
126
|
+
continue
|
|
127
|
+
if len(statement.args.args) < 2:
|
|
128
|
+
return None
|
|
129
|
+
annotation = statement.args.args[1].annotation
|
|
130
|
+
return ast.unparse(annotation) if annotation is not None else None
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def external_prop_type(internal_type: str) -> str:
|
|
135
|
+
if internal_type.startswith("Accessor[") and internal_type.endswith("]"):
|
|
136
|
+
inner = internal_type.removeprefix("Accessor[").removesuffix("]")
|
|
137
|
+
return f"{inner} | reactive.Accessor[{inner}]"
|
|
138
|
+
if internal_type.startswith("reactive.Accessor[") and internal_type.endswith("]"):
|
|
139
|
+
inner = internal_type.removeprefix("reactive.Accessor[").removesuffix("]")
|
|
140
|
+
return f"{inner} | reactive.Accessor[{inner}]"
|
|
141
|
+
if internal_type.startswith("Mutator[") and internal_type.endswith("]"):
|
|
142
|
+
inner = internal_type.removeprefix("Mutator[").removesuffix("]")
|
|
143
|
+
return f"reactive.Mutator[{inner}]"
|
|
144
|
+
if internal_type.startswith("reactive.Mutator[") and internal_type.endswith("]"):
|
|
145
|
+
return internal_type
|
|
146
|
+
return "Any"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def base_name(node: ast.AST) -> str:
|
|
150
|
+
match node:
|
|
151
|
+
case ast.Name(id=name):
|
|
152
|
+
return name
|
|
153
|
+
case ast.Attribute(attr=name):
|
|
154
|
+
return name
|
|
155
|
+
case ast.Call(func=func):
|
|
156
|
+
return base_name(func)
|
|
157
|
+
case ast.Subscript(value=value):
|
|
158
|
+
return base_name(value)
|
|
159
|
+
case _:
|
|
160
|
+
return ""
|
|
161
|
+
|
taut/cli/stubs.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import shutil
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .stub_model import ComponentStub
|
|
10
|
+
from .stub_model import ImportStub
|
|
11
|
+
from .stub_model import collect_components
|
|
12
|
+
from .stub_model import collect_protocols
|
|
13
|
+
from .stub_model import collect_public_imports
|
|
14
|
+
from .stub_model import render_component
|
|
15
|
+
|
|
16
|
+
DEFAULT_OUT_DIR = Path("typings")
|
|
17
|
+
SKIPPED_DIRS = {
|
|
18
|
+
".git",
|
|
19
|
+
".mypy_cache",
|
|
20
|
+
".pytest_cache",
|
|
21
|
+
".ruff_cache",
|
|
22
|
+
".venv",
|
|
23
|
+
"__pycache__",
|
|
24
|
+
"build",
|
|
25
|
+
"dist",
|
|
26
|
+
"typings",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class GeneratedModule:
|
|
32
|
+
output_path: Path
|
|
33
|
+
import_path: str
|
|
34
|
+
components: list[ComponentStub]
|
|
35
|
+
public_imports: list[ImportStub]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main_cli(args):
|
|
39
|
+
main(args.paths, args.out_dir, args.in_place)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(paths: list[Path], out_dir: Path = DEFAULT_OUT_DIR, in_place=False) -> None:
|
|
43
|
+
all_source_paths = list(expand_paths(paths))
|
|
44
|
+
modules = [
|
|
45
|
+
module
|
|
46
|
+
for path in all_source_paths
|
|
47
|
+
if (module := generate_module(path, out_dir, in_place=in_place)) is not None
|
|
48
|
+
]
|
|
49
|
+
generated_import_paths = {module.import_path for module in modules}
|
|
50
|
+
|
|
51
|
+
for module in modules:
|
|
52
|
+
module.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
module.output_path.write_text(
|
|
54
|
+
render_stub(
|
|
55
|
+
module.components,
|
|
56
|
+
module.public_imports,
|
|
57
|
+
generated_import_paths=generated_import_paths,
|
|
58
|
+
),
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not in_place:
|
|
63
|
+
write_package_exports(modules, all_source_paths, out_dir)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def expand_paths(paths: Iterable[Path]) -> Iterable[Path]:
|
|
67
|
+
for path in paths:
|
|
68
|
+
if path.is_dir():
|
|
69
|
+
yield from sorted(
|
|
70
|
+
child for child in path.rglob("*.py") if not is_skipped_path(child)
|
|
71
|
+
)
|
|
72
|
+
elif path.suffix == ".py":
|
|
73
|
+
yield path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_skipped_path(path: Path) -> bool:
|
|
77
|
+
return any(part in SKIPPED_DIRS for part in path.parts)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def output_path_for(path: Path, out_dir: Path, in_place: bool) -> Path:
|
|
81
|
+
if in_place:
|
|
82
|
+
return path.with_suffix(".pyi")
|
|
83
|
+
|
|
84
|
+
rel = path.resolve().relative_to(Path.cwd())
|
|
85
|
+
return out_dir / rel.with_suffix(".pyi")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_module(path: Path) -> ast.Module | None:
|
|
89
|
+
try:
|
|
90
|
+
return ast.parse(path.read_text(encoding="utf-8"))
|
|
91
|
+
except (OSError, SyntaxError):
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def remove_stub_for_source(
|
|
96
|
+
path: Path,
|
|
97
|
+
out_dir: Path = DEFAULT_OUT_DIR,
|
|
98
|
+
in_place: bool = False,
|
|
99
|
+
) -> bool:
|
|
100
|
+
try:
|
|
101
|
+
output_path = output_path_for(path, out_dir, in_place)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return False
|
|
104
|
+
try:
|
|
105
|
+
output_path.unlink()
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
return False
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def remove_stubs_for_source_dir(
|
|
112
|
+
path: Path,
|
|
113
|
+
out_dir: Path = DEFAULT_OUT_DIR,
|
|
114
|
+
) -> bool:
|
|
115
|
+
try:
|
|
116
|
+
rel = path.resolve().relative_to(Path.cwd())
|
|
117
|
+
except ValueError:
|
|
118
|
+
return False
|
|
119
|
+
output_dir = out_dir / rel
|
|
120
|
+
if not output_dir.is_dir():
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
shutil.rmtree(output_dir)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def generate_module(
|
|
128
|
+
path: Path,
|
|
129
|
+
out_dir: Path,
|
|
130
|
+
*,
|
|
131
|
+
in_place: bool,
|
|
132
|
+
) -> GeneratedModule | None:
|
|
133
|
+
module = read_module(path)
|
|
134
|
+
if module is None:
|
|
135
|
+
return None
|
|
136
|
+
components = collect_components(module, collect_protocols(module))
|
|
137
|
+
if not components:
|
|
138
|
+
return None
|
|
139
|
+
output_path = output_path_for(path, out_dir, in_place)
|
|
140
|
+
return GeneratedModule(
|
|
141
|
+
output_path=output_path,
|
|
142
|
+
import_path=module_import_path(path),
|
|
143
|
+
components=components,
|
|
144
|
+
public_imports=collect_public_imports(module),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def contains_component_def(path: Path) -> bool:
|
|
149
|
+
return bool(component_stubs_for(path))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def component_stubs_for(path: Path) -> list[ComponentStub]:
|
|
153
|
+
module = read_module(path)
|
|
154
|
+
if module is None:
|
|
155
|
+
return []
|
|
156
|
+
return collect_components(module, collect_protocols(module))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def module_import_path(path: Path) -> str:
|
|
160
|
+
module_path = path.with_suffix("") if path.suffix == ".py" else path
|
|
161
|
+
try:
|
|
162
|
+
module_path = module_path.resolve().relative_to(Path.cwd().resolve())
|
|
163
|
+
except ValueError:
|
|
164
|
+
pass
|
|
165
|
+
return ".".join(module_path.parts)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_stub(
|
|
169
|
+
components: list[ComponentStub],
|
|
170
|
+
public_imports: list[ImportStub] | None = None,
|
|
171
|
+
generated_import_paths: set[str] | None = None,
|
|
172
|
+
) -> str:
|
|
173
|
+
imports = collect_stub_imports(components)
|
|
174
|
+
public_imports = public_imports or []
|
|
175
|
+
generated_import_paths = generated_import_paths or set()
|
|
176
|
+
public_import_lines = render_public_imports(public_imports, generated_import_paths)
|
|
177
|
+
lines = ["from __future__ import annotations", ""]
|
|
178
|
+
import_lines = format_imports([*imports, *public_import_lines])
|
|
179
|
+
if import_lines:
|
|
180
|
+
lines.extend([*import_lines, ""])
|
|
181
|
+
for component in components:
|
|
182
|
+
lines.extend(render_component(component))
|
|
183
|
+
lines.append("")
|
|
184
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def render_public_imports(
|
|
188
|
+
public_imports: list[ImportStub],
|
|
189
|
+
generated_import_paths: set[str],
|
|
190
|
+
) -> list[str]:
|
|
191
|
+
lines: list[str] = []
|
|
192
|
+
for import_stub in public_imports:
|
|
193
|
+
import_path = f"{import_stub.module}.{import_stub.name}"
|
|
194
|
+
if import_path in generated_import_paths:
|
|
195
|
+
lines.append(
|
|
196
|
+
f"from {import_stub.module} import {import_stub.name} "
|
|
197
|
+
f"as {import_stub.export_name}"
|
|
198
|
+
)
|
|
199
|
+
return lines
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def format_imports(imports: list[str]) -> list[str]:
|
|
203
|
+
stdlib = sorted(line for line in imports if is_stdlib_import(line))
|
|
204
|
+
relative = sorted(line for line in imports if line.startswith("from ."))
|
|
205
|
+
absolute = sorted(
|
|
206
|
+
line
|
|
207
|
+
for line in imports
|
|
208
|
+
if line not in stdlib and line not in relative
|
|
209
|
+
)
|
|
210
|
+
sections = [stdlib, absolute, relative]
|
|
211
|
+
lines: list[str] = []
|
|
212
|
+
for section in sections:
|
|
213
|
+
if not section:
|
|
214
|
+
continue
|
|
215
|
+
if lines:
|
|
216
|
+
lines.append("")
|
|
217
|
+
lines.extend(section)
|
|
218
|
+
return lines
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def is_stdlib_import(line: str) -> bool:
|
|
222
|
+
return line.startswith("from collections.") or line.startswith("from typing ")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def collect_stub_imports(components: list[ComponentStub]) -> list[str]:
|
|
226
|
+
external_types = {
|
|
227
|
+
field.external_type for component in components for field in component.fields
|
|
228
|
+
}
|
|
229
|
+
imports = []
|
|
230
|
+
if any("Callable" in external_type for external_type in external_types):
|
|
231
|
+
imports.append("from collections.abc import Callable")
|
|
232
|
+
if components or "Any" in external_types:
|
|
233
|
+
imports.append("from typing import Any")
|
|
234
|
+
if components:
|
|
235
|
+
imports.append("from taut import runtime")
|
|
236
|
+
if any("reactive." in external_type for external_type in external_types):
|
|
237
|
+
imports.append("from taut import reactive")
|
|
238
|
+
return imports
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def write_package_exports(
|
|
242
|
+
modules: list[GeneratedModule],
|
|
243
|
+
source_paths: list[Path],
|
|
244
|
+
out_dir: Path,
|
|
245
|
+
) -> None:
|
|
246
|
+
packages = package_marker_dirs(modules, source_paths, out_dir)
|
|
247
|
+
components_by_module = {
|
|
248
|
+
module.import_path: {component.name for component in module.components}
|
|
249
|
+
for module in modules
|
|
250
|
+
}
|
|
251
|
+
generated_modules = {module.import_path for module in modules}
|
|
252
|
+
attrs_by_package = public_attrs_by_package(modules)
|
|
253
|
+
|
|
254
|
+
for package_dir in sorted(packages):
|
|
255
|
+
marker = package_dir / "__init__.pyi"
|
|
256
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
source_init_path = source_init_for_marker(package_dir, out_dir)
|
|
258
|
+
marker.write_text(
|
|
259
|
+
render_init_stub(
|
|
260
|
+
source_init_path,
|
|
261
|
+
components_by_module,
|
|
262
|
+
generated_modules,
|
|
263
|
+
attrs_by_package.get(module_import_path(source_init_path.parent), set()),
|
|
264
|
+
),
|
|
265
|
+
encoding="utf-8",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def public_attrs_by_package(modules: list[GeneratedModule]) -> dict[str, set[str]]:
|
|
270
|
+
attrs_by_package: dict[str, set[str]] = {}
|
|
271
|
+
for module in modules:
|
|
272
|
+
package = module_import_path(Path(*module.import_path.split(".")[:-1]))
|
|
273
|
+
if not package:
|
|
274
|
+
continue
|
|
275
|
+
for import_stub in module.public_imports:
|
|
276
|
+
if import_stub.module == package:
|
|
277
|
+
attrs_by_package.setdefault(package, set()).add(import_stub.export_name)
|
|
278
|
+
return attrs_by_package
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def package_marker_dirs(
|
|
282
|
+
modules: list[GeneratedModule],
|
|
283
|
+
source_paths: list[Path],
|
|
284
|
+
out_dir: Path,
|
|
285
|
+
) -> set[Path]:
|
|
286
|
+
package_dirs: set[Path] = set()
|
|
287
|
+
out_dir = out_dir.resolve()
|
|
288
|
+
for module in modules:
|
|
289
|
+
directory = module.output_path.parent.resolve()
|
|
290
|
+
while directory != out_dir and out_dir in directory.parents:
|
|
291
|
+
package_dirs.add(directory)
|
|
292
|
+
directory = directory.parent
|
|
293
|
+
|
|
294
|
+
for init_path in source_paths:
|
|
295
|
+
if init_path.name != "__init__.py":
|
|
296
|
+
continue
|
|
297
|
+
try:
|
|
298
|
+
package_dirs.add(output_path_for(init_path, out_dir, in_place=False).parent)
|
|
299
|
+
except ValueError:
|
|
300
|
+
pass
|
|
301
|
+
return package_dirs
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def source_init_for_marker(package_dir: Path, out_dir: Path) -> Path:
|
|
305
|
+
rel = package_dir.resolve().relative_to(out_dir.resolve())
|
|
306
|
+
return Path.cwd() / rel / "__init__.py"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def render_init_stub(
|
|
310
|
+
source_init_path: Path,
|
|
311
|
+
components_by_module: dict[str, set[str]],
|
|
312
|
+
generated_modules: set[str],
|
|
313
|
+
package_attrs: set[str] | None = None,
|
|
314
|
+
) -> str:
|
|
315
|
+
reexports: list[str] = []
|
|
316
|
+
functions: list[str] = []
|
|
317
|
+
attrs = sorted(package_attrs or set())
|
|
318
|
+
module = read_module(source_init_path)
|
|
319
|
+
|
|
320
|
+
package_import_path = module_import_path(source_init_path.parent)
|
|
321
|
+
if module is not None:
|
|
322
|
+
for node in module.body:
|
|
323
|
+
if isinstance(node, ast.ImportFrom) and node.module is not None:
|
|
324
|
+
import_path = resolve_import_from(source_init_path, node)
|
|
325
|
+
component_names = components_by_module.get(import_path)
|
|
326
|
+
module_ref = marker_import_ref(package_import_path, import_path)
|
|
327
|
+
for alias in node.names:
|
|
328
|
+
export_name = alias.asname or alias.name
|
|
329
|
+
if component_names and alias.name in component_names:
|
|
330
|
+
reexports.append(
|
|
331
|
+
f"from {module_ref} import {alias.name} as {export_name}"
|
|
332
|
+
)
|
|
333
|
+
elif f"{import_path}.{alias.name}" in generated_modules:
|
|
334
|
+
reexports.append(
|
|
335
|
+
f"from {module_ref} import {alias.name} as {export_name}"
|
|
336
|
+
)
|
|
337
|
+
elif isinstance(node, ast.FunctionDef) and is_public_name(node.name):
|
|
338
|
+
functions.append(render_function_stub(node))
|
|
339
|
+
|
|
340
|
+
reexports.extend(
|
|
341
|
+
f"from . import {name} as {name}"
|
|
342
|
+
for name in attrs
|
|
343
|
+
if f"{package_import_path}.{name}" in generated_modules
|
|
344
|
+
)
|
|
345
|
+
imports = (
|
|
346
|
+
["from typing import Any"]
|
|
347
|
+
if any_function_uses_any(functions)
|
|
348
|
+
else []
|
|
349
|
+
)
|
|
350
|
+
import_lines = format_imports([*imports, *reexports])
|
|
351
|
+
sections = [["from __future__ import annotations"], import_lines, functions]
|
|
352
|
+
lines = [line for section in sections if section for line in (*section, "")]
|
|
353
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def is_public_name(name: str) -> bool:
|
|
357
|
+
return not name.startswith("_")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def render_function_stub(node: ast.FunctionDef) -> str:
|
|
361
|
+
returns = ast.unparse(node.returns) if node.returns is not None else "Any"
|
|
362
|
+
return f"def {node.name}({ast.unparse(node.args)}) -> {returns}: ..."
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def any_function_uses_any(functions: list[str]) -> bool:
|
|
366
|
+
return any(" -> Any:" in function for function in functions)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def resolve_import_from(source_path: Path, node: ast.ImportFrom) -> str:
|
|
370
|
+
if node.level == 0:
|
|
371
|
+
return node.module or ""
|
|
372
|
+
|
|
373
|
+
package_path = source_path.parent
|
|
374
|
+
for _ in range(node.level - 1):
|
|
375
|
+
package_path = package_path.parent
|
|
376
|
+
package_import_path = module_import_path(package_path)
|
|
377
|
+
if not node.module:
|
|
378
|
+
return package_import_path
|
|
379
|
+
return f"{package_import_path}.{node.module}"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def marker_import_ref(package_import_path: str, import_path: str) -> str:
|
|
383
|
+
package_parts = package_import_path.split(".")
|
|
384
|
+
import_parts = import_path.split(".")
|
|
385
|
+
common = 0
|
|
386
|
+
for package_part, import_part in zip(package_parts, import_parts):
|
|
387
|
+
if package_part != import_part:
|
|
388
|
+
break
|
|
389
|
+
common += 1
|
|
390
|
+
|
|
391
|
+
up_levels = len(package_parts) - common
|
|
392
|
+
remainder = ".".join(import_parts[common:])
|
|
393
|
+
return "." * (up_levels + 1) + remainder
|