pywire 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.
- pywire/__init__.py +2 -0
- pywire/cli/__init__.py +1 -0
- pywire/cli/generators.py +48 -0
- pywire/cli/main.py +309 -0
- pywire/cli/tui.py +563 -0
- pywire/cli/validate.py +26 -0
- pywire/client/.prettierignore +8 -0
- pywire/client/.prettierrc +7 -0
- pywire/client/build.mjs +73 -0
- pywire/client/eslint.config.js +46 -0
- pywire/client/package.json +39 -0
- pywire/client/pnpm-lock.yaml +2971 -0
- pywire/client/src/core/app.ts +263 -0
- pywire/client/src/core/dom-updater.test.ts +78 -0
- pywire/client/src/core/dom-updater.ts +321 -0
- pywire/client/src/core/index.ts +5 -0
- pywire/client/src/core/transport-manager.test.ts +179 -0
- pywire/client/src/core/transport-manager.ts +159 -0
- pywire/client/src/core/transports/base.ts +122 -0
- pywire/client/src/core/transports/http.ts +142 -0
- pywire/client/src/core/transports/index.ts +13 -0
- pywire/client/src/core/transports/websocket.ts +97 -0
- pywire/client/src/core/transports/webtransport.ts +149 -0
- pywire/client/src/dev/dev-app.ts +93 -0
- pywire/client/src/dev/error-trace.test.ts +97 -0
- pywire/client/src/dev/error-trace.ts +76 -0
- pywire/client/src/dev/index.ts +4 -0
- pywire/client/src/dev/status-overlay.ts +63 -0
- pywire/client/src/events/handler.test.ts +318 -0
- pywire/client/src/events/handler.ts +454 -0
- pywire/client/src/pywire.core.ts +22 -0
- pywire/client/src/pywire.dev.ts +27 -0
- pywire/client/tsconfig.json +17 -0
- pywire/client/vitest.config.ts +15 -0
- pywire/compiler/__init__.py +6 -0
- pywire/compiler/ast_nodes.py +304 -0
- pywire/compiler/attributes/__init__.py +6 -0
- pywire/compiler/attributes/base.py +24 -0
- pywire/compiler/attributes/conditional.py +37 -0
- pywire/compiler/attributes/events.py +55 -0
- pywire/compiler/attributes/form.py +37 -0
- pywire/compiler/attributes/loop.py +75 -0
- pywire/compiler/attributes/reactive.py +34 -0
- pywire/compiler/build.py +28 -0
- pywire/compiler/build_artifacts.py +342 -0
- pywire/compiler/codegen/__init__.py +5 -0
- pywire/compiler/codegen/attributes/__init__.py +6 -0
- pywire/compiler/codegen/attributes/base.py +19 -0
- pywire/compiler/codegen/attributes/events.py +35 -0
- pywire/compiler/codegen/directives/__init__.py +6 -0
- pywire/compiler/codegen/directives/base.py +16 -0
- pywire/compiler/codegen/directives/path.py +53 -0
- pywire/compiler/codegen/generator.py +2341 -0
- pywire/compiler/codegen/template.py +2178 -0
- pywire/compiler/directives/__init__.py +7 -0
- pywire/compiler/directives/base.py +20 -0
- pywire/compiler/directives/component.py +33 -0
- pywire/compiler/directives/context.py +93 -0
- pywire/compiler/directives/layout.py +49 -0
- pywire/compiler/directives/no_spa.py +24 -0
- pywire/compiler/directives/path.py +71 -0
- pywire/compiler/directives/props.py +88 -0
- pywire/compiler/exceptions.py +19 -0
- pywire/compiler/interpolation/__init__.py +6 -0
- pywire/compiler/interpolation/base.py +28 -0
- pywire/compiler/interpolation/jinja.py +272 -0
- pywire/compiler/parser.py +750 -0
- pywire/compiler/paths.py +29 -0
- pywire/compiler/preprocessor.py +43 -0
- pywire/core/wire.py +119 -0
- pywire/py.typed +0 -0
- pywire/runtime/__init__.py +7 -0
- pywire/runtime/aioquic_server.py +194 -0
- pywire/runtime/app.py +889 -0
- pywire/runtime/compile_error_page.py +195 -0
- pywire/runtime/debug.py +203 -0
- pywire/runtime/dev_server.py +434 -0
- pywire/runtime/dev_server.py.broken +268 -0
- pywire/runtime/error_page.py +64 -0
- pywire/runtime/error_renderer.py +23 -0
- pywire/runtime/escape.py +23 -0
- pywire/runtime/files.py +40 -0
- pywire/runtime/helpers.py +97 -0
- pywire/runtime/http_transport.py +253 -0
- pywire/runtime/loader.py +272 -0
- pywire/runtime/logging.py +72 -0
- pywire/runtime/page.py +384 -0
- pywire/runtime/pydantic_integration.py +52 -0
- pywire/runtime/router.py +229 -0
- pywire/runtime/server.py +25 -0
- pywire/runtime/style_collector.py +31 -0
- pywire/runtime/upload_manager.py +76 -0
- pywire/runtime/validation.py +449 -0
- pywire/runtime/websocket.py +665 -0
- pywire/runtime/webtransport_handler.py +195 -0
- pywire/templates/error/404.html +11 -0
- pywire/templates/error/500.html +38 -0
- pywire/templates/error/base.html +207 -0
- pywire/templates/error/compile_error.html +31 -0
- pywire-0.1.0.dist-info/METADATA +50 -0
- pywire-0.1.0.dist-info/RECORD +104 -0
- pywire-0.1.0.dist-info/WHEEL +4 -0
- pywire-0.1.0.dist-info/entry_points.txt +2 -0
- pywire-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Build system for precompiled PyWire artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
13
|
+
|
|
14
|
+
from pywire.compiler.ast_nodes import (
|
|
15
|
+
ComponentDirective,
|
|
16
|
+
LayoutDirective,
|
|
17
|
+
ParsedPyWire,
|
|
18
|
+
PathDirective,
|
|
19
|
+
)
|
|
20
|
+
from pywire.compiler.codegen.generator import CodeGenerator
|
|
21
|
+
from pywire.compiler.parser import PyWireParser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BuildSummary:
|
|
26
|
+
pages: int
|
|
27
|
+
layouts: int
|
|
28
|
+
components: int
|
|
29
|
+
out_dir: Path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ArtifactBuilder:
|
|
33
|
+
def __init__(self, pages_dir: Path, out_dir: Path) -> None:
|
|
34
|
+
self.pages_dir = pages_dir.resolve()
|
|
35
|
+
self.out_dir = out_dir.resolve()
|
|
36
|
+
self.parser = PyWireParser()
|
|
37
|
+
self.codegen = CodeGenerator()
|
|
38
|
+
self.entries: Dict[str, dict] = {}
|
|
39
|
+
self._compiled: Set[str] = set()
|
|
40
|
+
self._page_count = 0
|
|
41
|
+
self._layout_count = 0
|
|
42
|
+
self._component_count = 0
|
|
43
|
+
|
|
44
|
+
def build(self, optimize: bool = False) -> BuildSummary:
|
|
45
|
+
if self.out_dir.exists():
|
|
46
|
+
shutil.rmtree(self.out_dir)
|
|
47
|
+
|
|
48
|
+
(self.out_dir / "pages").mkdir(parents=True, exist_ok=True)
|
|
49
|
+
(self.out_dir / "components").mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
self._scan_directory(self.pages_dir, layout_path=None, url_prefix="")
|
|
52
|
+
self._build_error_page()
|
|
53
|
+
|
|
54
|
+
manifest = {
|
|
55
|
+
"version": 1,
|
|
56
|
+
"pages_dir": str(self.pages_dir),
|
|
57
|
+
"entries": self.entries,
|
|
58
|
+
}
|
|
59
|
+
manifest_path = self.out_dir / "manifest.json"
|
|
60
|
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
if optimize:
|
|
63
|
+
import compileall
|
|
64
|
+
|
|
65
|
+
compileall.compile_dir(self.out_dir, quiet=1, optimize=2)
|
|
66
|
+
|
|
67
|
+
return BuildSummary(
|
|
68
|
+
pages=self._page_count,
|
|
69
|
+
layouts=self._layout_count,
|
|
70
|
+
components=self._component_count,
|
|
71
|
+
out_dir=self.out_dir,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _build_error_page(self) -> None:
|
|
75
|
+
error_page_path = self.pages_dir / "__error__.wire"
|
|
76
|
+
if not error_page_path.exists():
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
implicit_layout = None
|
|
80
|
+
root_layout = self.pages_dir / "__layout__.wire"
|
|
81
|
+
if root_layout.exists():
|
|
82
|
+
implicit_layout = str(root_layout.resolve())
|
|
83
|
+
|
|
84
|
+
self._compile_file(
|
|
85
|
+
error_page_path, kind="page", implicit_layout=implicit_layout, is_error=True
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _scan_directory(
|
|
89
|
+
self, dir_path: Path, layout_path: Optional[str], url_prefix: str
|
|
90
|
+
) -> None:
|
|
91
|
+
current_layout = layout_path
|
|
92
|
+
potential_layout = dir_path / "__layout__.wire"
|
|
93
|
+
if potential_layout.exists():
|
|
94
|
+
self._compile_file(
|
|
95
|
+
potential_layout, kind="layout", implicit_layout=current_layout
|
|
96
|
+
)
|
|
97
|
+
current_layout = str(potential_layout.resolve())
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
entries = sorted(list(dir_path.iterdir()))
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
for entry in entries:
|
|
105
|
+
if entry.name.startswith("_") or entry.name.startswith("."):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if entry.is_dir():
|
|
109
|
+
name = entry.name
|
|
110
|
+
new_segment = name
|
|
111
|
+
param_match = re.match(r"^\[(.*?)\]$", name)
|
|
112
|
+
if param_match:
|
|
113
|
+
param_name = param_match.group(1)
|
|
114
|
+
new_segment = f"{{{param_name}}}"
|
|
115
|
+
|
|
116
|
+
new_prefix = (url_prefix + "/" + new_segment).replace("//", "/")
|
|
117
|
+
self._scan_directory(entry, current_layout, new_prefix)
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if not entry.is_file() or entry.suffix != ".wire":
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
if entry.name == "layout.wire":
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
self._compile_file(
|
|
127
|
+
entry, kind="page", implicit_layout=current_layout, is_error=False
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _compile_file(
|
|
131
|
+
self,
|
|
132
|
+
file_path: Path,
|
|
133
|
+
kind: str,
|
|
134
|
+
implicit_layout: Optional[str],
|
|
135
|
+
is_error: bool = False,
|
|
136
|
+
) -> None:
|
|
137
|
+
resolved_path = file_path.resolve()
|
|
138
|
+
key = str(resolved_path)
|
|
139
|
+
|
|
140
|
+
if key in self._compiled:
|
|
141
|
+
if kind == "page":
|
|
142
|
+
entry = self.entries.get(key)
|
|
143
|
+
if entry and entry.get("kind") != "page":
|
|
144
|
+
entry["kind"] = "page"
|
|
145
|
+
parsed = self.parser.parse_file(resolved_path)
|
|
146
|
+
entry["routes"] = self._get_routes(parsed, resolved_path, is_error)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
parsed = self.parser.parse_file(resolved_path)
|
|
150
|
+
if implicit_layout:
|
|
151
|
+
if not parsed.get_directive_by_type(LayoutDirective):
|
|
152
|
+
parsed.directives.append(
|
|
153
|
+
LayoutDirective(
|
|
154
|
+
name="layout",
|
|
155
|
+
line=0,
|
|
156
|
+
column=0,
|
|
157
|
+
layout_path=implicit_layout,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
module_ast = self.codegen.generate(parsed)
|
|
162
|
+
ast.fix_missing_locations(module_ast)
|
|
163
|
+
source = ast.unparse(module_ast)
|
|
164
|
+
|
|
165
|
+
artifact_rel = self._artifact_path_for(resolved_path)
|
|
166
|
+
artifact_path = self.out_dir / artifact_rel
|
|
167
|
+
artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
artifact_path.write_text(source, encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
deps = self._collect_deps(parsed, implicit_layout, resolved_path)
|
|
171
|
+
entry_deps = []
|
|
172
|
+
for dep_path, dep_kind in deps:
|
|
173
|
+
if not dep_path.exists():
|
|
174
|
+
continue
|
|
175
|
+
entry_deps.append(
|
|
176
|
+
{"path": str(dep_path), "hash": self._hash_file(dep_path)}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
entry = {
|
|
180
|
+
"artifact": str(artifact_rel),
|
|
181
|
+
"hash": self._hash_file(resolved_path),
|
|
182
|
+
"deps": entry_deps,
|
|
183
|
+
"kind": kind,
|
|
184
|
+
"routes": self._get_routes(parsed, resolved_path, is_error)
|
|
185
|
+
if kind == "page"
|
|
186
|
+
else [],
|
|
187
|
+
"implicit_layout": implicit_layout,
|
|
188
|
+
}
|
|
189
|
+
self.entries[key] = entry
|
|
190
|
+
self._compiled.add(key)
|
|
191
|
+
|
|
192
|
+
if kind == "page":
|
|
193
|
+
self._page_count += 1
|
|
194
|
+
elif kind == "layout":
|
|
195
|
+
self._layout_count += 1
|
|
196
|
+
elif kind == "component":
|
|
197
|
+
self._component_count += 1
|
|
198
|
+
|
|
199
|
+
for dep_path, dep_kind in deps:
|
|
200
|
+
if not dep_path.exists():
|
|
201
|
+
continue
|
|
202
|
+
dep_implicit_layout = None
|
|
203
|
+
if self._is_in_pages(dep_path):
|
|
204
|
+
dep_implicit_layout = self._resolve_implicit_layout(dep_path)
|
|
205
|
+
self._compile_file(
|
|
206
|
+
dep_path, kind=dep_kind, implicit_layout=dep_implicit_layout
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _collect_deps(
|
|
210
|
+
self, parsed: ParsedPyWire, implicit_layout: Optional[str], base_path: Path
|
|
211
|
+
) -> List[Tuple[Path, str]]:
|
|
212
|
+
deps: Dict[str, str] = {}
|
|
213
|
+
|
|
214
|
+
if implicit_layout:
|
|
215
|
+
deps[str(Path(implicit_layout).resolve())] = "layout"
|
|
216
|
+
|
|
217
|
+
for directive in parsed.directives:
|
|
218
|
+
if isinstance(directive, LayoutDirective):
|
|
219
|
+
path = self._resolve_path(directive.layout_path, base_path)
|
|
220
|
+
deps[str(path)] = "layout"
|
|
221
|
+
elif isinstance(directive, ComponentDirective):
|
|
222
|
+
path = self._resolve_path(directive.path, base_path)
|
|
223
|
+
deps[str(path)] = "component"
|
|
224
|
+
|
|
225
|
+
return [(Path(path), kind) for path, kind in deps.items()]
|
|
226
|
+
|
|
227
|
+
def _resolve_path(self, path_str: str, base_path: Path) -> Path:
|
|
228
|
+
path = Path(path_str)
|
|
229
|
+
if not path.is_absolute():
|
|
230
|
+
path = base_path.parent / path
|
|
231
|
+
return path.resolve()
|
|
232
|
+
|
|
233
|
+
def _artifact_path_for(self, file_path: Path) -> Path:
|
|
234
|
+
if self._is_in_pages(file_path):
|
|
235
|
+
rel = file_path.relative_to(self.pages_dir)
|
|
236
|
+
return Path("pages") / rel.with_suffix(".py")
|
|
237
|
+
|
|
238
|
+
file_hash = hashlib.md5(str(file_path).encode("utf-8")).hexdigest()[:10]
|
|
239
|
+
safe_name = f"{file_path.stem}_{file_hash}.py"
|
|
240
|
+
return Path("components") / safe_name
|
|
241
|
+
|
|
242
|
+
def _get_routes(
|
|
243
|
+
self, parsed: ParsedPyWire, file_path: Path, is_error: bool
|
|
244
|
+
) -> List[str]:
|
|
245
|
+
if is_error:
|
|
246
|
+
return ["/__error__"]
|
|
247
|
+
|
|
248
|
+
path_directive = parsed.get_directive_by_type(PathDirective)
|
|
249
|
+
if isinstance(path_directive, PathDirective):
|
|
250
|
+
return list(path_directive.routes.values())
|
|
251
|
+
|
|
252
|
+
implicit = self._get_implicit_route(file_path)
|
|
253
|
+
if implicit:
|
|
254
|
+
return [implicit]
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
def _get_implicit_route(self, file_path: Path) -> Optional[str]:
|
|
258
|
+
try:
|
|
259
|
+
rel_path = file_path.relative_to(self.pages_dir)
|
|
260
|
+
except ValueError:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
segments = []
|
|
264
|
+
for i, part in enumerate(rel_path.parts):
|
|
265
|
+
if part.startswith("_") or part.startswith("."):
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
name = part
|
|
269
|
+
is_file = i == len(rel_path.parts) - 1
|
|
270
|
+
if is_file:
|
|
271
|
+
if not name.endswith(".wire"):
|
|
272
|
+
return None
|
|
273
|
+
if name == "layout.wire":
|
|
274
|
+
return None
|
|
275
|
+
name = Path(name).stem
|
|
276
|
+
|
|
277
|
+
segment = name
|
|
278
|
+
if name == "index":
|
|
279
|
+
segment = ""
|
|
280
|
+
|
|
281
|
+
param_match = re.match(r"^\[(.*?)\]$", name)
|
|
282
|
+
if param_match:
|
|
283
|
+
param_name = param_match.group(1)
|
|
284
|
+
segment = f"{{{param_name}}}"
|
|
285
|
+
|
|
286
|
+
segments.append(segment)
|
|
287
|
+
|
|
288
|
+
route_path = "/" + "/".join(segments)
|
|
289
|
+
while "//" in route_path:
|
|
290
|
+
route_path = route_path.replace("//", "/")
|
|
291
|
+
|
|
292
|
+
if route_path != "/" and route_path.endswith("/"):
|
|
293
|
+
route_path = route_path.rstrip("/")
|
|
294
|
+
|
|
295
|
+
if not route_path:
|
|
296
|
+
route_path = "/"
|
|
297
|
+
|
|
298
|
+
return route_path
|
|
299
|
+
|
|
300
|
+
def _resolve_implicit_layout(self, page_path: Path) -> Optional[str]:
|
|
301
|
+
current_dir = page_path.parent
|
|
302
|
+
try:
|
|
303
|
+
current_dir.relative_to(self.pages_dir)
|
|
304
|
+
except ValueError:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
while True:
|
|
308
|
+
layout = current_dir / "__layout__.wire"
|
|
309
|
+
if layout.exists():
|
|
310
|
+
if layout.resolve() != page_path.resolve():
|
|
311
|
+
return str(layout.resolve())
|
|
312
|
+
|
|
313
|
+
if current_dir == self.pages_dir:
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
current_dir = current_dir.parent
|
|
317
|
+
if current_dir == current_dir.parent:
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def _hash_file(self, path: Path) -> str:
|
|
323
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
324
|
+
|
|
325
|
+
def _is_in_pages(self, path: Path) -> bool:
|
|
326
|
+
try:
|
|
327
|
+
path.resolve().relative_to(self.pages_dir)
|
|
328
|
+
return True
|
|
329
|
+
except ValueError:
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def build_artifacts(
|
|
334
|
+
pages_dir: Path, out_dir: Optional[Path] = None, optimize: bool = False
|
|
335
|
+
) -> BuildSummary:
|
|
336
|
+
if out_dir is None:
|
|
337
|
+
from pywire.compiler.paths import get_build_path
|
|
338
|
+
|
|
339
|
+
out_dir = get_build_path()
|
|
340
|
+
|
|
341
|
+
builder = ArtifactBuilder(pages_dir=pages_dir, out_dir=out_dir)
|
|
342
|
+
return builder.build(optimize=optimize)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Base attribute code generator."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pywire.compiler.ast_nodes import SpecialAttribute
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AttributeCodegen(ABC):
|
|
11
|
+
"""Base class for attribute code generation."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def generate_html(self, attr: SpecialAttribute) -> str:
|
|
15
|
+
"""Generate HTML attributes for client."""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def generate_handler(self, attr: SpecialAttribute) -> Optional[ast.FunctionDef]:
|
|
19
|
+
"""Generate server-side handler if needed. Returns None if user defines it."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Event attribute code generator."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pywire.compiler.ast_nodes import EventAttribute, SpecialAttribute
|
|
7
|
+
from pywire.compiler.codegen.attributes.base import AttributeCodegen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventAttributeCodegen(AttributeCodegen):
|
|
11
|
+
"""Generates event handler hookup for @click."""
|
|
12
|
+
|
|
13
|
+
def generate_html(self, attr: SpecialAttribute) -> str:
|
|
14
|
+
"""Generate HTML data attribute for event."""
|
|
15
|
+
assert isinstance(attr, EventAttribute)
|
|
16
|
+
# @click.prevent={handler} → data-on-click="handler" data-modifiers-click="prevent"
|
|
17
|
+
attrs = [f'data-on-{attr.event_type}="{attr.handler_name}"']
|
|
18
|
+
if attr.modifiers:
|
|
19
|
+
attrs.append(
|
|
20
|
+
f'data-modifiers-{attr.event_type}="{" ".join(attr.modifiers)}"'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Lifted arguments support
|
|
24
|
+
if hasattr(attr, "args") and attr.args:
|
|
25
|
+
for i, arg in enumerate(attr.args):
|
|
26
|
+
# We need to escape quotes in the argument value for HTML
|
|
27
|
+
escaped_arg = str(arg).replace('"', """)
|
|
28
|
+
attrs.append(f'data-arg-{i}="{escaped_arg}"')
|
|
29
|
+
|
|
30
|
+
return " ".join(attrs)
|
|
31
|
+
|
|
32
|
+
def generate_handler(self, attr: SpecialAttribute) -> Optional[ast.FunctionDef]:
|
|
33
|
+
"""Generate handler method AST."""
|
|
34
|
+
assert isinstance(attr, EventAttribute)
|
|
35
|
+
return None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Base directive code generator."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pywire.compiler.ast_nodes import Directive
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DirectiveCodegen(ABC):
|
|
11
|
+
"""Base class for directive code generation."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def generate(self, directive: Directive) -> List[ast.stmt]:
|
|
15
|
+
"""Generate AST statements for directive."""
|
|
16
|
+
pass
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Path directive code generator."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from pywire.compiler.ast_nodes import Directive, PathDirective
|
|
7
|
+
from pywire.compiler.codegen.directives.base import DirectiveCodegen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PathDirectiveCodegen(DirectiveCodegen):
|
|
11
|
+
"""Generates routing metadata from !path."""
|
|
12
|
+
|
|
13
|
+
def generate(self, directive: Directive) -> List[ast.stmt]:
|
|
14
|
+
"""Generate route metadata assignments."""
|
|
15
|
+
assert isinstance(directive, PathDirective)
|
|
16
|
+
statements: List[ast.stmt] = []
|
|
17
|
+
|
|
18
|
+
# Generate __routes__ dict with all route names
|
|
19
|
+
routes_dict = {}
|
|
20
|
+
for name, pattern in directive.routes.items():
|
|
21
|
+
routes_dict[name] = pattern
|
|
22
|
+
|
|
23
|
+
routes_ast = ast.Dict(
|
|
24
|
+
keys=[ast.Constant(value=k) for k in routes_dict.keys()],
|
|
25
|
+
values=[ast.Constant(value=v) for v in routes_dict.values()],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
statements.append(
|
|
29
|
+
ast.Assign(
|
|
30
|
+
targets=[ast.Name(id="__routes__", ctx=ast.Store())], value=routes_ast
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Generate __path_mode__
|
|
35
|
+
mode = "string" if directive.is_simple_string else "dict"
|
|
36
|
+
statements.append(
|
|
37
|
+
ast.Assign(
|
|
38
|
+
targets=[ast.Name(id="__path_mode__", ctx=ast.Store())],
|
|
39
|
+
value=ast.Constant(value=mode),
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Generate __route__ with first route pattern (for backward compatibility)
|
|
44
|
+
if routes_dict:
|
|
45
|
+
first_pattern = list(routes_dict.values())[0]
|
|
46
|
+
statements.append(
|
|
47
|
+
ast.Assign(
|
|
48
|
+
targets=[ast.Name(id="__route__", ctx=ast.Store())],
|
|
49
|
+
value=ast.Constant(value=first_pattern),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return statements
|