pywire 0.1.0__py3-none-any.whl → 0.1.1__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-0.1.0.dist-info → pywire-0.1.1.dist-info}/METADATA +23 -1
- pywire-0.1.1.dist-info/RECORD +9 -0
- pywire/__init__.py +0 -2
- pywire/cli/__init__.py +0 -1
- pywire/cli/generators.py +0 -48
- pywire/cli/main.py +0 -309
- pywire/cli/tui.py +0 -563
- pywire/cli/validate.py +0 -26
- pywire/client/.prettierignore +0 -8
- pywire/client/.prettierrc +0 -7
- pywire/client/build.mjs +0 -73
- pywire/client/eslint.config.js +0 -46
- pywire/client/package.json +0 -39
- pywire/client/pnpm-lock.yaml +0 -2971
- pywire/client/src/core/app.ts +0 -263
- pywire/client/src/core/dom-updater.test.ts +0 -78
- pywire/client/src/core/dom-updater.ts +0 -321
- pywire/client/src/core/index.ts +0 -5
- pywire/client/src/core/transport-manager.test.ts +0 -179
- pywire/client/src/core/transport-manager.ts +0 -159
- pywire/client/src/core/transports/base.ts +0 -122
- pywire/client/src/core/transports/http.ts +0 -142
- pywire/client/src/core/transports/index.ts +0 -13
- pywire/client/src/core/transports/websocket.ts +0 -97
- pywire/client/src/core/transports/webtransport.ts +0 -149
- pywire/client/src/dev/dev-app.ts +0 -93
- pywire/client/src/dev/error-trace.test.ts +0 -97
- pywire/client/src/dev/error-trace.ts +0 -76
- pywire/client/src/dev/index.ts +0 -4
- pywire/client/src/dev/status-overlay.ts +0 -63
- pywire/client/src/events/handler.test.ts +0 -318
- pywire/client/src/events/handler.ts +0 -454
- pywire/client/src/pywire.core.ts +0 -22
- pywire/client/src/pywire.dev.ts +0 -27
- pywire/client/tsconfig.json +0 -17
- pywire/client/vitest.config.ts +0 -15
- pywire/compiler/__init__.py +0 -6
- pywire/compiler/ast_nodes.py +0 -304
- pywire/compiler/attributes/__init__.py +0 -6
- pywire/compiler/attributes/base.py +0 -24
- pywire/compiler/attributes/conditional.py +0 -37
- pywire/compiler/attributes/events.py +0 -55
- pywire/compiler/attributes/form.py +0 -37
- pywire/compiler/attributes/loop.py +0 -75
- pywire/compiler/attributes/reactive.py +0 -34
- pywire/compiler/build.py +0 -28
- pywire/compiler/build_artifacts.py +0 -342
- pywire/compiler/codegen/__init__.py +0 -5
- pywire/compiler/codegen/attributes/__init__.py +0 -6
- pywire/compiler/codegen/attributes/base.py +0 -19
- pywire/compiler/codegen/attributes/events.py +0 -35
- pywire/compiler/codegen/directives/__init__.py +0 -6
- pywire/compiler/codegen/directives/base.py +0 -16
- pywire/compiler/codegen/directives/path.py +0 -53
- pywire/compiler/codegen/generator.py +0 -2341
- pywire/compiler/codegen/template.py +0 -2178
- pywire/compiler/directives/__init__.py +0 -7
- pywire/compiler/directives/base.py +0 -20
- pywire/compiler/directives/component.py +0 -33
- pywire/compiler/directives/context.py +0 -93
- pywire/compiler/directives/layout.py +0 -49
- pywire/compiler/directives/no_spa.py +0 -24
- pywire/compiler/directives/path.py +0 -71
- pywire/compiler/directives/props.py +0 -88
- pywire/compiler/exceptions.py +0 -19
- pywire/compiler/interpolation/__init__.py +0 -6
- pywire/compiler/interpolation/base.py +0 -28
- pywire/compiler/interpolation/jinja.py +0 -272
- pywire/compiler/parser.py +0 -750
- pywire/compiler/paths.py +0 -29
- pywire/compiler/preprocessor.py +0 -43
- pywire/core/wire.py +0 -119
- pywire/py.typed +0 -0
- pywire/runtime/__init__.py +0 -7
- pywire/runtime/aioquic_server.py +0 -194
- pywire/runtime/app.py +0 -889
- pywire/runtime/compile_error_page.py +0 -195
- pywire/runtime/debug.py +0 -203
- pywire/runtime/dev_server.py +0 -434
- pywire/runtime/dev_server.py.broken +0 -268
- pywire/runtime/error_page.py +0 -64
- pywire/runtime/error_renderer.py +0 -23
- pywire/runtime/escape.py +0 -23
- pywire/runtime/files.py +0 -40
- pywire/runtime/helpers.py +0 -97
- pywire/runtime/http_transport.py +0 -253
- pywire/runtime/loader.py +0 -272
- pywire/runtime/logging.py +0 -72
- pywire/runtime/page.py +0 -384
- pywire/runtime/pydantic_integration.py +0 -52
- pywire/runtime/router.py +0 -229
- pywire/runtime/server.py +0 -25
- pywire/runtime/style_collector.py +0 -31
- pywire/runtime/upload_manager.py +0 -76
- pywire/runtime/validation.py +0 -449
- pywire/runtime/websocket.py +0 -665
- pywire/runtime/webtransport_handler.py +0 -195
- pywire-0.1.0.dist-info/RECORD +0 -104
- {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/WHEEL +0 -0
- {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/entry_points.txt +0 -0
- {pywire-0.1.0.dist-info → pywire-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,2341 +0,0 @@
|
|
|
1
|
-
"""Main code generator orchestrator."""
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import re
|
|
5
|
-
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union, cast
|
|
6
|
-
|
|
7
|
-
from pywire.compiler.ast_nodes import (
|
|
8
|
-
ComponentDirective,
|
|
9
|
-
Directive,
|
|
10
|
-
EventAttribute,
|
|
11
|
-
FormValidationSchema,
|
|
12
|
-
InjectDirective,
|
|
13
|
-
LayoutDirective,
|
|
14
|
-
NoSpaDirective,
|
|
15
|
-
ParsedPyWire,
|
|
16
|
-
PathDirective,
|
|
17
|
-
PropsDirective,
|
|
18
|
-
ProvideDirective,
|
|
19
|
-
SpecialAttribute,
|
|
20
|
-
TemplateNode,
|
|
21
|
-
)
|
|
22
|
-
from pywire.compiler.codegen.attributes.base import AttributeCodegen
|
|
23
|
-
from pywire.compiler.codegen.attributes.events import EventAttributeCodegen
|
|
24
|
-
from pywire.compiler.codegen.directives.base import DirectiveCodegen
|
|
25
|
-
from pywire.compiler.codegen.directives.path import PathDirectiveCodegen
|
|
26
|
-
from pywire.compiler.codegen.template import TemplateCodegen
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CodeGenerator:
|
|
30
|
-
"""Generates Python module from ParsedPyWire AST."""
|
|
31
|
-
|
|
32
|
-
def __init__(self) -> None:
|
|
33
|
-
self.directive_handlers: Dict[Type[Directive], DirectiveCodegen] = {
|
|
34
|
-
PathDirective: PathDirectiveCodegen(),
|
|
35
|
-
# Future: LayoutDirective: LayoutDirectiveCodegen(), etc.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
self.attribute_handlers: Dict[Type[SpecialAttribute], AttributeCodegen] = {
|
|
39
|
-
EventAttribute: EventAttributeCodegen(),
|
|
40
|
-
# Future: BindAttribute: BindAttributeCodegen(), etc.
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
self.template_codegen = TemplateCodegen()
|
|
44
|
-
|
|
45
|
-
def _generate_component_loading(
|
|
46
|
-
self, parsed: ParsedPyWire
|
|
47
|
-
) -> Tuple[List[ast.stmt], Dict[str, str]]:
|
|
48
|
-
"""
|
|
49
|
-
Generate code to load components and return Tag -> ClassName map.
|
|
50
|
-
Returns: (stmts, component_map)
|
|
51
|
-
"""
|
|
52
|
-
stmts: List[ast.stmt] = []
|
|
53
|
-
component_map = {}
|
|
54
|
-
|
|
55
|
-
for directive in parsed.directives:
|
|
56
|
-
if isinstance(directive, ComponentDirective):
|
|
57
|
-
# Name = load_component("path", __file_path__)
|
|
58
|
-
|
|
59
|
-
# Check for "as Name" collision with imports or other components?
|
|
60
|
-
# Python handles it (overwrite), but maybe warn?
|
|
61
|
-
|
|
62
|
-
target_name = directive.component_name
|
|
63
|
-
path = directive.path
|
|
64
|
-
|
|
65
|
-
# component_map[target_name] = target_name (class is assigned to this var)
|
|
66
|
-
# Parse lowercases HTML tags, so we map lowercase name to actual class name
|
|
67
|
-
component_map[target_name.lower()] = target_name
|
|
68
|
-
|
|
69
|
-
stmts.append(
|
|
70
|
-
ast.Assign(
|
|
71
|
-
targets=[ast.Name(id=target_name, ctx=ast.Store())],
|
|
72
|
-
value=ast.Call(
|
|
73
|
-
func=ast.Name(id="load_component", ctx=ast.Load()),
|
|
74
|
-
args=[
|
|
75
|
-
ast.Constant(value=path),
|
|
76
|
-
ast.Constant(value=parsed.file_path),
|
|
77
|
-
],
|
|
78
|
-
keywords=[],
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
return stmts, component_map
|
|
84
|
-
|
|
85
|
-
def generate(self, parsed: ParsedPyWire) -> ast.Module:
|
|
86
|
-
"""Generate complete module AST."""
|
|
87
|
-
self.file_path = parsed.file_path
|
|
88
|
-
self._has_top_level_init = False
|
|
89
|
-
self._collected_mount_hooks: List[str] = []
|
|
90
|
-
module_body = []
|
|
91
|
-
|
|
92
|
-
# Imports
|
|
93
|
-
module_body.extend(self._generate_imports())
|
|
94
|
-
|
|
95
|
-
# Add asyncio import for handle_event
|
|
96
|
-
module_body.append(ast.Import(names=[ast.alias(name="asyncio", asname=None)]))
|
|
97
|
-
|
|
98
|
-
# Component loading (early, so they are available)
|
|
99
|
-
comp_stmts, component_map = self._generate_component_loading(parsed)
|
|
100
|
-
module_body.extend(comp_stmts)
|
|
101
|
-
|
|
102
|
-
# Layout logic
|
|
103
|
-
layout_directive = parsed.get_directive_by_type(LayoutDirective)
|
|
104
|
-
|
|
105
|
-
if layout_directive:
|
|
106
|
-
layout_directive = cast(LayoutDirective, layout_directive)
|
|
107
|
-
# Import load_layout
|
|
108
|
-
module_body.append(
|
|
109
|
-
ast.ImportFrom(
|
|
110
|
-
module="pywire.runtime.loader",
|
|
111
|
-
names=[ast.alias(name="load_layout", asname=None)],
|
|
112
|
-
level=0,
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
# Load layout class
|
|
116
|
-
# _LayoutBase = load_layout("path", __file_path__)
|
|
117
|
-
module_body.append(
|
|
118
|
-
ast.Assign(
|
|
119
|
-
targets=[ast.Name(id="_LayoutBase", ctx=ast.Store())],
|
|
120
|
-
value=ast.Call(
|
|
121
|
-
func=ast.Name(id="load_layout", ctx=ast.Load()),
|
|
122
|
-
args=[
|
|
123
|
-
ast.Constant(value=layout_directive.layout_path),
|
|
124
|
-
ast.Constant(
|
|
125
|
-
value=parsed.file_path
|
|
126
|
-
), # Pass page file path for relative resolution
|
|
127
|
-
],
|
|
128
|
-
keywords=[],
|
|
129
|
-
),
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
# Extract user imports from Python section
|
|
133
|
-
if parsed.python_ast:
|
|
134
|
-
module_body.extend(self._extract_user_imports(parsed.python_ast))
|
|
135
|
-
# Extract user classes to module level (Pydantic models, etc.)
|
|
136
|
-
module_body.extend(self._extract_user_classes(parsed.python_ast))
|
|
137
|
-
|
|
138
|
-
# Extract method names early for binding logic
|
|
139
|
-
known_methods, known_vars, async_methods = self._collect_global_names(
|
|
140
|
-
parsed.python_ast
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
# Include explicit variable assignments
|
|
144
|
-
known_vars.update(self._extract_user_variables(parsed.python_ast))
|
|
145
|
-
|
|
146
|
-
known_imports = self._extract_import_names(parsed.python_ast)
|
|
147
|
-
all_globals = known_methods.union(known_vars).union(known_imports)
|
|
148
|
-
|
|
149
|
-
# Inline handlers (with method names)
|
|
150
|
-
# Note: Handlers only need to know about globals to avoid "self." prefixing if needed,
|
|
151
|
-
# but _process_handlers mostly cares about wrapping logic.
|
|
152
|
-
# Actually _process_handlers calls _transform_inline_code which uses known_methods.
|
|
153
|
-
# Ideally it should know about all globals too.
|
|
154
|
-
handlers, allowed_handlers = self._process_handlers(
|
|
155
|
-
parsed, all_globals, async_methods
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
# Page class
|
|
159
|
-
page_class = self._generate_page_class(
|
|
160
|
-
parsed,
|
|
161
|
-
handlers,
|
|
162
|
-
known_methods,
|
|
163
|
-
known_vars,
|
|
164
|
-
known_imports,
|
|
165
|
-
async_methods,
|
|
166
|
-
component_map,
|
|
167
|
-
allowed_handlers,
|
|
168
|
-
)
|
|
169
|
-
module_body.append(page_class)
|
|
170
|
-
|
|
171
|
-
# Export reference to main class
|
|
172
|
-
module_body.append(
|
|
173
|
-
ast.Assign(
|
|
174
|
-
targets=[ast.Name(id="__page_class__", ctx=ast.Store())],
|
|
175
|
-
value=ast.Name(id=page_class.name, ctx=ast.Load()),
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
module = ast.Module(body=module_body, type_ignores=[])
|
|
180
|
-
ast.fix_missing_locations(module)
|
|
181
|
-
|
|
182
|
-
return module
|
|
183
|
-
|
|
184
|
-
def _generate_imports(self) -> List[ast.stmt]:
|
|
185
|
-
"""Generate framework imports."""
|
|
186
|
-
imports: List[ast.stmt] = [
|
|
187
|
-
ast.ImportFrom(
|
|
188
|
-
module="pywire.runtime.page",
|
|
189
|
-
names=[ast.alias(name="BasePage", asname=None)],
|
|
190
|
-
level=0,
|
|
191
|
-
),
|
|
192
|
-
ast.ImportFrom(
|
|
193
|
-
module="pywire.core.wire",
|
|
194
|
-
names=[ast.alias(name="wire", asname=None)],
|
|
195
|
-
level=0,
|
|
196
|
-
),
|
|
197
|
-
ast.ImportFrom(
|
|
198
|
-
module="starlette.responses",
|
|
199
|
-
names=[ast.alias(name="Response", asname=None)],
|
|
200
|
-
level=0,
|
|
201
|
-
),
|
|
202
|
-
ast.Import(names=[ast.alias(name="json", asname=None)]),
|
|
203
|
-
# Form validation imports
|
|
204
|
-
ast.ImportFrom(
|
|
205
|
-
module="pywire.runtime.validation",
|
|
206
|
-
names=[
|
|
207
|
-
ast.alias(name="form_validator", asname=None),
|
|
208
|
-
ast.alias(name="FieldRules", asname=None),
|
|
209
|
-
ast.alias(name="FormValidationSchema", asname=None),
|
|
210
|
-
],
|
|
211
|
-
level=0,
|
|
212
|
-
),
|
|
213
|
-
ast.ImportFrom(
|
|
214
|
-
module="pywire.runtime.pydantic_integration",
|
|
215
|
-
names=[ast.alias(name="validate_with_model", asname=None)],
|
|
216
|
-
level=0,
|
|
217
|
-
),
|
|
218
|
-
ast.ImportFrom(
|
|
219
|
-
module="pywire.runtime.loader",
|
|
220
|
-
names=[ast.alias(name="load_component", asname=None)],
|
|
221
|
-
level=0,
|
|
222
|
-
),
|
|
223
|
-
ast.ImportFrom(
|
|
224
|
-
module="pywire.runtime.helpers",
|
|
225
|
-
names=[
|
|
226
|
-
ast.alias(name="unwrap_wire", asname=None),
|
|
227
|
-
ast.alias(name="set_render_context", asname=None),
|
|
228
|
-
ast.alias(name="reset_render_context", asname=None),
|
|
229
|
-
],
|
|
230
|
-
level=0,
|
|
231
|
-
),
|
|
232
|
-
]
|
|
233
|
-
return imports
|
|
234
|
-
|
|
235
|
-
def _generate_component_imports(
|
|
236
|
-
self, parsed: ParsedPyWire
|
|
237
|
-
) -> Tuple[List[ast.stmt], Dict[str, str]]:
|
|
238
|
-
"""
|
|
239
|
-
Generate imports for components and return a map of TagName -> ClassName.
|
|
240
|
-
Returns: (import_stmts, component_map)
|
|
241
|
-
"""
|
|
242
|
-
imports: List[ast.stmt] = []
|
|
243
|
-
component_map: Dict[str, str] = {}
|
|
244
|
-
|
|
245
|
-
from pywire.compiler.ast_nodes import ComponentDirective
|
|
246
|
-
|
|
247
|
-
for directive in parsed.directives:
|
|
248
|
-
if isinstance(directive, ComponentDirective):
|
|
249
|
-
# !component 'path' as Name
|
|
250
|
-
# We need to resolve 'path' to a python module path.
|
|
251
|
-
# Assuming 'path' is relative to project root or use loader helper?
|
|
252
|
-
# Actually, generated code will run in server context.
|
|
253
|
-
# Better to use our dynamic loader:
|
|
254
|
-
# Name = load_component('path', __file_path__)
|
|
255
|
-
|
|
256
|
-
# Import load_component if not already
|
|
257
|
-
# (handled in _generate_imports? No, let's assume we import a loader helper)
|
|
258
|
-
|
|
259
|
-
# We'll generate:
|
|
260
|
-
# Name = load_component("path", __file_path__)
|
|
261
|
-
# But imports are usually at module level.
|
|
262
|
-
# If we use load_component, it's an assignment, not an import.
|
|
263
|
-
# That's fine, we can add it to module body.
|
|
264
|
-
|
|
265
|
-
# However, cleaner if we can generate `from x import Y`.
|
|
266
|
-
# But we don't know the exact class name inside the file (it's generated).
|
|
267
|
-
# The dynamic loader `load_component` is robust.
|
|
268
|
-
|
|
269
|
-
# Let's add `load_component` to imports
|
|
270
|
-
pass
|
|
271
|
-
|
|
272
|
-
return imports, component_map
|
|
273
|
-
|
|
274
|
-
def _extract_user_imports(self, python_ast: ast.Module) -> List[ast.stmt]:
|
|
275
|
-
"""Extract import statements from user Python code."""
|
|
276
|
-
imports: List[ast.stmt] = []
|
|
277
|
-
for node in python_ast.body:
|
|
278
|
-
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
279
|
-
imports.append(node)
|
|
280
|
-
return imports
|
|
281
|
-
|
|
282
|
-
def _extract_user_classes(self, python_ast: ast.Module) -> List[ast.stmt]:
|
|
283
|
-
"""Extract class definitions from user Python code."""
|
|
284
|
-
classes: List[ast.stmt] = []
|
|
285
|
-
for node in python_ast.body:
|
|
286
|
-
if isinstance(node, ast.ClassDef):
|
|
287
|
-
classes.append(node)
|
|
288
|
-
return classes
|
|
289
|
-
|
|
290
|
-
def _extract_import_names(self, python_ast: Optional[ast.Module]) -> Set[str]:
|
|
291
|
-
"""Extract names defined by imports."""
|
|
292
|
-
names = set()
|
|
293
|
-
# Add default imports
|
|
294
|
-
names.add("json")
|
|
295
|
-
names.add("form_validator")
|
|
296
|
-
names.add("FieldRules")
|
|
297
|
-
|
|
298
|
-
if python_ast:
|
|
299
|
-
for node in python_ast.body:
|
|
300
|
-
if isinstance(node, ast.Import):
|
|
301
|
-
for alias in node.names:
|
|
302
|
-
names.add(alias.asname or alias.name)
|
|
303
|
-
elif isinstance(node, ast.ImportFrom):
|
|
304
|
-
for alias in node.names:
|
|
305
|
-
names.add(alias.asname or alias.name)
|
|
306
|
-
return names
|
|
307
|
-
|
|
308
|
-
def _extract_user_variables(self, python_ast: Optional[ast.Module]) -> Set[str]:
|
|
309
|
-
"""Extract variable names assigned at the top level of user code."""
|
|
310
|
-
vars: Set[str] = set()
|
|
311
|
-
if not python_ast:
|
|
312
|
-
return vars
|
|
313
|
-
|
|
314
|
-
for node in python_ast.body:
|
|
315
|
-
if isinstance(node, ast.Assign):
|
|
316
|
-
for target in node.targets:
|
|
317
|
-
if isinstance(target, ast.Name):
|
|
318
|
-
vars.add(target.id)
|
|
319
|
-
elif isinstance(node, ast.AnnAssign):
|
|
320
|
-
if isinstance(node.target, ast.Name):
|
|
321
|
-
vars.add(node.target.id)
|
|
322
|
-
return vars
|
|
323
|
-
|
|
324
|
-
def _extract_route_params_from_pattern(self, pattern: str) -> Set[str]:
|
|
325
|
-
params: Set[str] = set()
|
|
326
|
-
if not pattern:
|
|
327
|
-
return params
|
|
328
|
-
|
|
329
|
-
for name in re.findall(r"\{([a-zA-Z_]\w*)(?::[^}]+)?\}", pattern):
|
|
330
|
-
if not name.isidentifier():
|
|
331
|
-
continue
|
|
332
|
-
params.add(name)
|
|
333
|
-
|
|
334
|
-
for name in re.findall(r":([a-zA-Z_]\w*)(?::[^/]+)?", pattern):
|
|
335
|
-
if not name.isidentifier():
|
|
336
|
-
continue
|
|
337
|
-
params.add(name)
|
|
338
|
-
|
|
339
|
-
return params
|
|
340
|
-
|
|
341
|
-
def _extract_route_params_from_file_path(
|
|
342
|
-
self, file_path: Optional[str]
|
|
343
|
-
) -> Set[str]:
|
|
344
|
-
params: Set[str] = set()
|
|
345
|
-
if not file_path:
|
|
346
|
-
return params
|
|
347
|
-
|
|
348
|
-
from pathlib import Path
|
|
349
|
-
|
|
350
|
-
path = Path(file_path)
|
|
351
|
-
|
|
352
|
-
for part in path.parts:
|
|
353
|
-
if not part:
|
|
354
|
-
continue
|
|
355
|
-
if not (part.startswith("[") and part.endswith("]")):
|
|
356
|
-
continue
|
|
357
|
-
name = part[1:-1]
|
|
358
|
-
if not name:
|
|
359
|
-
continue
|
|
360
|
-
if not name.isidentifier():
|
|
361
|
-
continue
|
|
362
|
-
params.add(name)
|
|
363
|
-
|
|
364
|
-
stem = path.stem
|
|
365
|
-
if stem.startswith("[") and stem.endswith("]"):
|
|
366
|
-
name = stem[1:-1]
|
|
367
|
-
if name and name.isidentifier():
|
|
368
|
-
params.add(name)
|
|
369
|
-
|
|
370
|
-
return params
|
|
371
|
-
|
|
372
|
-
def _extract_route_params(self, parsed: ParsedPyWire) -> Set[str]:
|
|
373
|
-
params: Set[str] = set()
|
|
374
|
-
|
|
375
|
-
path_directive = parsed.get_directive_by_type(PathDirective)
|
|
376
|
-
if path_directive:
|
|
377
|
-
assert isinstance(path_directive, PathDirective)
|
|
378
|
-
for pattern in path_directive.routes.values():
|
|
379
|
-
params.update(self._extract_route_params_from_pattern(pattern))
|
|
380
|
-
|
|
381
|
-
params.update(self._extract_route_params_from_file_path(parsed.file_path))
|
|
382
|
-
|
|
383
|
-
return params
|
|
384
|
-
|
|
385
|
-
def _generate_page_class(
|
|
386
|
-
self,
|
|
387
|
-
parsed: ParsedPyWire,
|
|
388
|
-
handlers: List[ast.AsyncFunctionDef],
|
|
389
|
-
known_methods: Set[str],
|
|
390
|
-
known_vars: Set[str],
|
|
391
|
-
known_imports: Set[str],
|
|
392
|
-
async_methods: Set[str],
|
|
393
|
-
component_map: Dict[str, str],
|
|
394
|
-
allowed_handlers: Optional[Set[str]] = None,
|
|
395
|
-
) -> ast.ClassDef:
|
|
396
|
-
"""Generate page class definition."""
|
|
397
|
-
class_body: List[ast.stmt] = []
|
|
398
|
-
|
|
399
|
-
# Add generated handlers
|
|
400
|
-
class_body.extend(handlers)
|
|
401
|
-
|
|
402
|
-
# Generate __allowed_handlers__ for security (prevents arbitrary method invocation)
|
|
403
|
-
if allowed_handlers is None:
|
|
404
|
-
allowed_handlers = set()
|
|
405
|
-
# Include _handle_bind_* handlers that may be generated later
|
|
406
|
-
# These will be added dynamically, but we pre-allow the pattern
|
|
407
|
-
class_body.append(
|
|
408
|
-
ast.Assign(
|
|
409
|
-
targets=[ast.Name(id="__allowed_handlers__", ctx=ast.Store())],
|
|
410
|
-
value=ast.Set(
|
|
411
|
-
elts=[ast.Constant(value=h) for h in sorted(allowed_handlers)]
|
|
412
|
-
)
|
|
413
|
-
if allowed_handlers
|
|
414
|
-
else ast.Call(
|
|
415
|
-
func=ast.Name(id="set", ctx=ast.Load()), args=[], keywords=[]
|
|
416
|
-
),
|
|
417
|
-
)
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
# Generate directive assignments (e.g., __routes__)
|
|
421
|
-
for directive in parsed.directives:
|
|
422
|
-
handler = self.directive_handlers.get(type(directive))
|
|
423
|
-
if handler:
|
|
424
|
-
class_body.extend(handler.generate(directive))
|
|
425
|
-
|
|
426
|
-
# Generate SPA navigation metadata
|
|
427
|
-
class_body.extend(self._generate_spa_metadata(parsed))
|
|
428
|
-
|
|
429
|
-
# Inject __no_spa__ flag if !no_spa was detected
|
|
430
|
-
if parsed.get_directive_by_type(NoSpaDirective):
|
|
431
|
-
class_body.append(
|
|
432
|
-
ast.Assign(
|
|
433
|
-
targets=[ast.Name(id="__no_spa__", ctx=ast.Store())],
|
|
434
|
-
value=ast.Constant(value=True),
|
|
435
|
-
)
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
# Transform user Python code to class methods (Must run before __init__ to set flags)
|
|
439
|
-
route_params = self._extract_route_params(parsed)
|
|
440
|
-
all_globals = known_methods.union(known_vars).union(route_params)
|
|
441
|
-
user_code_stmts: List[ast.stmt] = []
|
|
442
|
-
if parsed.python_ast:
|
|
443
|
-
user_code_stmts = self._transform_user_code(parsed.python_ast, all_globals)
|
|
444
|
-
|
|
445
|
-
# Generate __init__ method
|
|
446
|
-
class_body.append(self._generate_init_method(parsed))
|
|
447
|
-
|
|
448
|
-
# Add user code
|
|
449
|
-
class_body.extend(user_code_stmts)
|
|
450
|
-
|
|
451
|
-
# Generate form validation schemas and wrappers
|
|
452
|
-
# MUST happen before render generation as it updates EventAttributes to point to wrappers
|
|
453
|
-
form_validation_methods = self._generate_form_validation_methods(
|
|
454
|
-
parsed, all_globals
|
|
455
|
-
)
|
|
456
|
-
class_body.extend(form_validation_methods)
|
|
457
|
-
# Generate _render_template method AND binding methods
|
|
458
|
-
# Pass ALL globals to avoid auto-calling variables and prefixing imports
|
|
459
|
-
all_globals = (
|
|
460
|
-
known_methods.union(known_vars).union(route_params).union(known_imports)
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
render_func, binding_funcs = self._generate_render_template_method(
|
|
464
|
-
parsed, known_methods, all_globals, async_methods, component_map
|
|
465
|
-
)
|
|
466
|
-
if render_func:
|
|
467
|
-
class_body.append(render_func)
|
|
468
|
-
class_body.extend(binding_funcs)
|
|
469
|
-
|
|
470
|
-
# Inject __has_uploads__ flag if file inputs were detected
|
|
471
|
-
if self.template_codegen.has_file_inputs:
|
|
472
|
-
class_body.append(
|
|
473
|
-
ast.Assign(
|
|
474
|
-
targets=[ast.Name(id="__has_uploads__", ctx=ast.Store())],
|
|
475
|
-
value=ast.Constant(value=True),
|
|
476
|
-
)
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
# Determine base class
|
|
480
|
-
base_id = "BasePage"
|
|
481
|
-
if parsed.get_directive_by_type(LayoutDirective):
|
|
482
|
-
base_id = "_LayoutBase"
|
|
483
|
-
|
|
484
|
-
# Inject LAYOUT_ID if we determined one is needed
|
|
485
|
-
# We need to calculate it here too or pass it back from _generate_render_template_method
|
|
486
|
-
# Since we need it for class attribute, let's calculate it early.
|
|
487
|
-
layout_id_to_inject = None
|
|
488
|
-
if parsed.file_path:
|
|
489
|
-
import hashlib
|
|
490
|
-
|
|
491
|
-
layout_id_hash = hashlib.md5(str(parsed.file_path).encode()).hexdigest()
|
|
492
|
-
# Recursive check for slots
|
|
493
|
-
has_slots = self._has_slots_recursive(parsed.template)
|
|
494
|
-
if has_slots:
|
|
495
|
-
layout_id_to_inject = layout_id_hash
|
|
496
|
-
|
|
497
|
-
if layout_id_to_inject:
|
|
498
|
-
class_body.append(
|
|
499
|
-
ast.Assign(
|
|
500
|
-
targets=[ast.Name(id="LAYOUT_ID", ctx=ast.Store())],
|
|
501
|
-
value=ast.Constant(value=layout_id_to_inject),
|
|
502
|
-
)
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
# Lifecycle hooks calculation
|
|
506
|
-
init_hooks = []
|
|
507
|
-
# If we found @mount decorated methods
|
|
508
|
-
if hasattr(self, "_collected_mount_hooks") and self._collected_mount_hooks:
|
|
509
|
-
init_hooks.extend(self._collected_mount_hooks)
|
|
510
|
-
|
|
511
|
-
# Ensure 'on_before_load' and 'on_load' are present
|
|
512
|
-
final_init_hooks = []
|
|
513
|
-
|
|
514
|
-
# Standard hooks - REMOVED per user request
|
|
515
|
-
# final_init_hooks.append('on_before_load')
|
|
516
|
-
# final_init_hooks.append('on_load')
|
|
517
|
-
|
|
518
|
-
# Add mount hooks
|
|
519
|
-
if hasattr(self, "_collected_mount_hooks") and self._collected_mount_hooks:
|
|
520
|
-
final_init_hooks.extend(self._collected_mount_hooks)
|
|
521
|
-
|
|
522
|
-
class_body.append(
|
|
523
|
-
ast.Assign(
|
|
524
|
-
targets=[ast.Name(id="INIT_HOOKS", ctx=ast.Store())],
|
|
525
|
-
value=ast.List(
|
|
526
|
-
elts=[ast.Constant(value=h) for h in final_init_hooks],
|
|
527
|
-
ctx=ast.Load(),
|
|
528
|
-
),
|
|
529
|
-
)
|
|
530
|
-
)
|
|
531
|
-
cls_def = ast.ClassDef(
|
|
532
|
-
name=self._get_class_name(parsed),
|
|
533
|
-
bases=[ast.Name(id=base_id, ctx=ast.Load())],
|
|
534
|
-
keywords=[],
|
|
535
|
-
body=class_body,
|
|
536
|
-
decorator_list=[],
|
|
537
|
-
)
|
|
538
|
-
cls_def.lineno = 1
|
|
539
|
-
cls_def.col_offset = 0
|
|
540
|
-
return cls_def
|
|
541
|
-
|
|
542
|
-
def _collect_global_names(
|
|
543
|
-
self, python_ast: Optional[ast.Module]
|
|
544
|
-
) -> Tuple[Set[str], Set[str], Set[str]]:
|
|
545
|
-
"""Collect defined function names and variables, and identify async functions.
|
|
546
|
-
Returns: (method_names, variable_names, async_method_names)
|
|
547
|
-
"""
|
|
548
|
-
methods = set()
|
|
549
|
-
variables = {
|
|
550
|
-
"path",
|
|
551
|
-
"params",
|
|
552
|
-
"query",
|
|
553
|
-
"url",
|
|
554
|
-
"request",
|
|
555
|
-
"error_code",
|
|
556
|
-
"error_detail",
|
|
557
|
-
"error_trace",
|
|
558
|
-
}
|
|
559
|
-
async_methods = set()
|
|
560
|
-
|
|
561
|
-
if python_ast:
|
|
562
|
-
for node in python_ast.body:
|
|
563
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
564
|
-
methods.add(node.name)
|
|
565
|
-
if isinstance(node, ast.AsyncFunctionDef):
|
|
566
|
-
async_methods.add(node.name)
|
|
567
|
-
elif isinstance(node, ast.Assign):
|
|
568
|
-
for target in node.targets:
|
|
569
|
-
for child in ast.walk(target):
|
|
570
|
-
if isinstance(child, ast.Name) and isinstance(
|
|
571
|
-
child.ctx, ast.Store
|
|
572
|
-
):
|
|
573
|
-
variables.add(child.id)
|
|
574
|
-
elif isinstance(node, ast.AnnAssign):
|
|
575
|
-
if isinstance(node.target, ast.Name):
|
|
576
|
-
variables.add(node.target.id)
|
|
577
|
-
|
|
578
|
-
# Add implicit params from filename if available
|
|
579
|
-
if hasattr(self, "file_path") and self.file_path:
|
|
580
|
-
import re
|
|
581
|
-
from pathlib import Path
|
|
582
|
-
|
|
583
|
-
path_obj = Path(self.file_path)
|
|
584
|
-
# Check current file name and parent directories for [param] syntax
|
|
585
|
-
for part in path_obj.parts:
|
|
586
|
-
match = re.match(r"^\[(.*?)\]$", part.replace(".pywire", ""))
|
|
587
|
-
if match:
|
|
588
|
-
variables.add(match.group(1))
|
|
589
|
-
|
|
590
|
-
return methods, variables, async_methods
|
|
591
|
-
|
|
592
|
-
def _process_handlers(
|
|
593
|
-
self, parsed: ParsedPyWire, known_methods: Set[str], async_methods: Set[str]
|
|
594
|
-
) -> Tuple[List[ast.AsyncFunctionDef], Set[str]]:
|
|
595
|
-
"""Extract inline handlers and wrap handlers for bindings.
|
|
596
|
-
|
|
597
|
-
Returns:
|
|
598
|
-
Tuple of (handler_methods, allowed_handler_names)
|
|
599
|
-
"""
|
|
600
|
-
handlers = []
|
|
601
|
-
allowed_handlers: Set[str] = set()
|
|
602
|
-
handler_count = 0
|
|
603
|
-
from pywire.compiler.ast_nodes import EventAttribute
|
|
604
|
-
|
|
605
|
-
def visit_nodes(nodes: List[TemplateNode]) -> None:
|
|
606
|
-
nonlocal handler_count
|
|
607
|
-
for node in nodes:
|
|
608
|
-
# Check for events
|
|
609
|
-
for attr in node.special_attributes:
|
|
610
|
-
if isinstance(attr, EventAttribute):
|
|
611
|
-
# Pre-processing: Strip wrapping braces if present (e.g. from {code} syntax)
|
|
612
|
-
# This ensures code inside is processed correctly whether quoted or not in
|
|
613
|
-
# source
|
|
614
|
-
raw = attr.handler_name.strip()
|
|
615
|
-
if raw.startswith("{") and raw.endswith("}"):
|
|
616
|
-
attr.handler_name = raw[1:-1].strip()
|
|
617
|
-
|
|
618
|
-
is_identifier = attr.handler_name.isidentifier()
|
|
619
|
-
needs_wrapper = not is_identifier
|
|
620
|
-
|
|
621
|
-
if needs_wrapper:
|
|
622
|
-
# Create distinct handler methods
|
|
623
|
-
method_name = f"_handler_{handler_count}"
|
|
624
|
-
handler_count += 1
|
|
625
|
-
|
|
626
|
-
try:
|
|
627
|
-
# Transform body logic
|
|
628
|
-
code_to_transform = attr.handler_name
|
|
629
|
-
|
|
630
|
-
body, args = self._transform_inline_code(
|
|
631
|
-
code_to_transform, known_methods, async_methods
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
# Store extracted args
|
|
635
|
-
attr.args = args
|
|
636
|
-
|
|
637
|
-
# Create handler method
|
|
638
|
-
# async def _handler_X(self, arg0, arg1...):
|
|
639
|
-
arg_definitions = [ast.arg(arg="self")]
|
|
640
|
-
for i in range(len(args)):
|
|
641
|
-
arg_definitions.append(ast.arg(arg=f"arg{i}"))
|
|
642
|
-
|
|
643
|
-
handlers.append(
|
|
644
|
-
ast.AsyncFunctionDef(
|
|
645
|
-
name=method_name,
|
|
646
|
-
args=ast.arguments(
|
|
647
|
-
posonlyargs=[],
|
|
648
|
-
args=arg_definitions,
|
|
649
|
-
vararg=None,
|
|
650
|
-
kwonlyargs=[],
|
|
651
|
-
kw_defaults=[],
|
|
652
|
-
defaults=[],
|
|
653
|
-
),
|
|
654
|
-
body=body,
|
|
655
|
-
decorator_list=[],
|
|
656
|
-
returns=None,
|
|
657
|
-
)
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
attr.handler_name = method_name
|
|
661
|
-
# Add generated handler to allowlist
|
|
662
|
-
allowed_handlers.add(method_name)
|
|
663
|
-
|
|
664
|
-
except Exception as e:
|
|
665
|
-
print(
|
|
666
|
-
f"Error compiling handler '{attr.handler_name}': {e}"
|
|
667
|
-
)
|
|
668
|
-
else:
|
|
669
|
-
# Simple identifier handler - add to allowlist
|
|
670
|
-
allowed_handlers.add(attr.handler_name)
|
|
671
|
-
|
|
672
|
-
visit_nodes(node.children)
|
|
673
|
-
|
|
674
|
-
visit_nodes(parsed.template)
|
|
675
|
-
return handlers, allowed_handlers
|
|
676
|
-
|
|
677
|
-
def _transform_inline_code(
|
|
678
|
-
self,
|
|
679
|
-
code: str,
|
|
680
|
-
known_methods: Set[str] = set(),
|
|
681
|
-
async_methods: Set[str] = set(),
|
|
682
|
-
) -> Tuple[List[ast.stmt], List[str]]:
|
|
683
|
-
"""Transform inline code: lift arguments and prefix globals with self."""
|
|
684
|
-
import builtins
|
|
685
|
-
|
|
686
|
-
# Map $event to event for Alpine compatibility
|
|
687
|
-
code = code.replace("$event", "event")
|
|
688
|
-
|
|
689
|
-
from pywire.compiler.preprocessor import preprocess_python_code
|
|
690
|
-
|
|
691
|
-
code = preprocess_python_code(code)
|
|
692
|
-
|
|
693
|
-
tree = ast.parse(code)
|
|
694
|
-
extracted_args: List[str] = []
|
|
695
|
-
|
|
696
|
-
class ArgumentLifter(ast.NodeTransformer):
|
|
697
|
-
def visit_Call(self, node: ast.Call) -> Any:
|
|
698
|
-
# Check arguments for unbound variables
|
|
699
|
-
new_args: List[ast.expr] = []
|
|
700
|
-
for arg in node.args:
|
|
701
|
-
# Quick check: does this arg contain unbound names?
|
|
702
|
-
unbound = False
|
|
703
|
-
for child in ast.walk(arg):
|
|
704
|
-
if isinstance(child, ast.Name):
|
|
705
|
-
if child.id not in known_methods and child.id not in dir(
|
|
706
|
-
builtins
|
|
707
|
-
):
|
|
708
|
-
unbound = True
|
|
709
|
-
break
|
|
710
|
-
|
|
711
|
-
if unbound:
|
|
712
|
-
# Lift it!
|
|
713
|
-
arg_index = len(extracted_args)
|
|
714
|
-
extracted_args.append(ast.unparse(arg))
|
|
715
|
-
new_args.append(ast.Name(id=f"arg{arg_index}", ctx=ast.Load()))
|
|
716
|
-
else:
|
|
717
|
-
new_args.append(self.visit(arg))
|
|
718
|
-
|
|
719
|
-
node.args = new_args
|
|
720
|
-
return self.generic_visit(node)
|
|
721
|
-
|
|
722
|
-
def visit_Name(self, node: ast.Name) -> Any:
|
|
723
|
-
# Transform known methods and globals to self.X
|
|
724
|
-
if node.id in known_methods:
|
|
725
|
-
return ast.Attribute(
|
|
726
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
727
|
-
attr=node.id,
|
|
728
|
-
ctx=node.ctx,
|
|
729
|
-
)
|
|
730
|
-
return node
|
|
731
|
-
|
|
732
|
-
# Run transformer
|
|
733
|
-
transformer = ArgumentLifter()
|
|
734
|
-
new_tree = transformer.visit(tree)
|
|
735
|
-
|
|
736
|
-
if async_methods:
|
|
737
|
-
|
|
738
|
-
class AsyncCallTransformer(ast.NodeTransformer):
|
|
739
|
-
def visit_Call(self, node: ast.Call) -> Any:
|
|
740
|
-
# Check if call to self.async_method
|
|
741
|
-
# The func is now (after ArgumentLifter/Name visit) self.method_name
|
|
742
|
-
if (
|
|
743
|
-
isinstance(node.func, ast.Attribute)
|
|
744
|
-
and isinstance(node.func.value, ast.Name)
|
|
745
|
-
and node.func.value.id == "self"
|
|
746
|
-
and node.func.attr in async_methods
|
|
747
|
-
):
|
|
748
|
-
return ast.Await(value=node)
|
|
749
|
-
return self.generic_visit(node)
|
|
750
|
-
|
|
751
|
-
AsyncCallTransformer().visit(new_tree)
|
|
752
|
-
|
|
753
|
-
ast.fix_missing_locations(new_tree)
|
|
754
|
-
|
|
755
|
-
return new_tree.body, extracted_args
|
|
756
|
-
|
|
757
|
-
def _generate_form_validation_methods(
|
|
758
|
-
self, parsed: ParsedPyWire, known_globals: Set[str]
|
|
759
|
-
) -> List[ast.stmt]:
|
|
760
|
-
"""Generate validation schema and wrapper methods for forms with @submit."""
|
|
761
|
-
methods: List[ast.stmt] = []
|
|
762
|
-
form_count = 0
|
|
763
|
-
|
|
764
|
-
def visit_nodes(nodes: List[TemplateNode]) -> None:
|
|
765
|
-
nonlocal form_count
|
|
766
|
-
for node in nodes:
|
|
767
|
-
# Check for form with @submit that has validation schema
|
|
768
|
-
if node.tag and node.tag.lower() == "form":
|
|
769
|
-
for attr in node.special_attributes:
|
|
770
|
-
if (
|
|
771
|
-
isinstance(attr, EventAttribute)
|
|
772
|
-
and attr.event_type == "submit"
|
|
773
|
-
):
|
|
774
|
-
if attr.validation_schema and attr.validation_schema.fields:
|
|
775
|
-
form_id = form_count
|
|
776
|
-
form_count += 1
|
|
777
|
-
|
|
778
|
-
# Generate validation schema as class attribute
|
|
779
|
-
schema_name = f"_form_schema_{form_id}"
|
|
780
|
-
original_handler = attr.handler_name
|
|
781
|
-
|
|
782
|
-
# Build dict literal for schema fields
|
|
783
|
-
schema_methods = self._generate_form_schema_literal(
|
|
784
|
-
attr.validation_schema, schema_name, known_globals
|
|
785
|
-
)
|
|
786
|
-
methods.extend(schema_methods)
|
|
787
|
-
|
|
788
|
-
# Generate wrapper handler
|
|
789
|
-
wrapper = self._generate_form_wrapper(
|
|
790
|
-
form_id,
|
|
791
|
-
original_handler,
|
|
792
|
-
schema_name,
|
|
793
|
-
attr.validation_schema,
|
|
794
|
-
known_globals,
|
|
795
|
-
)
|
|
796
|
-
methods.append(wrapper)
|
|
797
|
-
|
|
798
|
-
# Update handler name to point to wrapper
|
|
799
|
-
attr.handler_name = f"_form_submit_{form_id}"
|
|
800
|
-
|
|
801
|
-
# Recurse
|
|
802
|
-
visit_nodes(node.children)
|
|
803
|
-
|
|
804
|
-
visit_nodes(parsed.template)
|
|
805
|
-
return methods
|
|
806
|
-
|
|
807
|
-
def _generate_form_schema_literal(
|
|
808
|
-
self, schema: FormValidationSchema, schema_name: str, known_globals: Set[str]
|
|
809
|
-
) -> List[ast.stmt]:
|
|
810
|
-
"""Generate validation schema as a class attribute."""
|
|
811
|
-
field_items = []
|
|
812
|
-
for field_name, rules in schema.fields.items():
|
|
813
|
-
keywords = []
|
|
814
|
-
|
|
815
|
-
if rules.required:
|
|
816
|
-
keywords.append(
|
|
817
|
-
ast.keyword(arg="required", value=ast.Constant(value=True))
|
|
818
|
-
)
|
|
819
|
-
if rules.required_expr:
|
|
820
|
-
expr_ast = self.template_codegen._transform_expr(
|
|
821
|
-
rules.required_expr, set(), known_globals
|
|
822
|
-
)
|
|
823
|
-
expr_str = ast.unparse(expr_ast)
|
|
824
|
-
keywords.append(
|
|
825
|
-
ast.keyword(arg="required_expr", value=ast.Constant(value=expr_str))
|
|
826
|
-
)
|
|
827
|
-
if rules.pattern:
|
|
828
|
-
keywords.append(
|
|
829
|
-
ast.keyword(arg="pattern", value=ast.Constant(value=rules.pattern))
|
|
830
|
-
)
|
|
831
|
-
if rules.minlength is not None:
|
|
832
|
-
keywords.append(
|
|
833
|
-
ast.keyword(
|
|
834
|
-
arg="minlength", value=ast.Constant(value=rules.minlength)
|
|
835
|
-
)
|
|
836
|
-
)
|
|
837
|
-
if rules.maxlength is not None:
|
|
838
|
-
keywords.append(
|
|
839
|
-
ast.keyword(
|
|
840
|
-
arg="maxlength", value=ast.Constant(value=rules.maxlength)
|
|
841
|
-
)
|
|
842
|
-
)
|
|
843
|
-
if rules.min_value:
|
|
844
|
-
keywords.append(
|
|
845
|
-
ast.keyword(
|
|
846
|
-
arg="min_value", value=ast.Constant(value=rules.min_value)
|
|
847
|
-
)
|
|
848
|
-
)
|
|
849
|
-
if rules.min_expr:
|
|
850
|
-
expr_ast = self.template_codegen._transform_expr(
|
|
851
|
-
rules.min_expr, set(), known_globals
|
|
852
|
-
)
|
|
853
|
-
expr_str = ast.unparse(expr_ast)
|
|
854
|
-
keywords.append(
|
|
855
|
-
ast.keyword(arg="min_expr", value=ast.Constant(value=expr_str))
|
|
856
|
-
)
|
|
857
|
-
if rules.max_value:
|
|
858
|
-
keywords.append(
|
|
859
|
-
ast.keyword(
|
|
860
|
-
arg="max_value", value=ast.Constant(value=rules.max_value)
|
|
861
|
-
)
|
|
862
|
-
)
|
|
863
|
-
if rules.max_expr:
|
|
864
|
-
expr_ast = self.template_codegen._transform_expr(
|
|
865
|
-
rules.max_expr, set(), known_globals
|
|
866
|
-
)
|
|
867
|
-
expr_str = ast.unparse(expr_ast)
|
|
868
|
-
keywords.append(
|
|
869
|
-
ast.keyword(arg="max_expr", value=ast.Constant(value=expr_str))
|
|
870
|
-
)
|
|
871
|
-
if rules.step:
|
|
872
|
-
keywords.append(
|
|
873
|
-
ast.keyword(arg="step", value=ast.Constant(value=rules.step))
|
|
874
|
-
)
|
|
875
|
-
if rules.input_type != "text":
|
|
876
|
-
keywords.append(
|
|
877
|
-
ast.keyword(
|
|
878
|
-
arg="input_type", value=ast.Constant(value=rules.input_type)
|
|
879
|
-
)
|
|
880
|
-
)
|
|
881
|
-
if rules.title:
|
|
882
|
-
keywords.append(
|
|
883
|
-
ast.keyword(arg="title", value=ast.Constant(value=rules.title))
|
|
884
|
-
)
|
|
885
|
-
if rules.max_size is not None:
|
|
886
|
-
keywords.append(
|
|
887
|
-
ast.keyword(
|
|
888
|
-
arg="max_size", value=ast.Constant(value=rules.max_size)
|
|
889
|
-
)
|
|
890
|
-
)
|
|
891
|
-
if rules.allowed_types:
|
|
892
|
-
keywords.append(
|
|
893
|
-
ast.keyword(
|
|
894
|
-
arg="allowed_types",
|
|
895
|
-
value=ast.List(
|
|
896
|
-
elts=[ast.Constant(value=t) for t in rules.allowed_types],
|
|
897
|
-
ctx=ast.Load(),
|
|
898
|
-
),
|
|
899
|
-
)
|
|
900
|
-
)
|
|
901
|
-
|
|
902
|
-
field_rules_call = ast.Call(
|
|
903
|
-
func=ast.Name(id="FieldRules", ctx=ast.Load()),
|
|
904
|
-
args=[],
|
|
905
|
-
keywords=keywords,
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
field_items.append((ast.Constant(value=field_name), field_rules_call))
|
|
909
|
-
|
|
910
|
-
schema_dict = ast.Dict(
|
|
911
|
-
keys=[k for k, v in field_items], values=[v for k, v in field_items]
|
|
912
|
-
)
|
|
913
|
-
|
|
914
|
-
schema_call = ast.Call(
|
|
915
|
-
func=ast.Name(id="FormValidationSchema", ctx=ast.Load()),
|
|
916
|
-
args=[],
|
|
917
|
-
keywords=[ast.keyword(arg="fields", value=schema_dict)],
|
|
918
|
-
)
|
|
919
|
-
|
|
920
|
-
if schema.model_name:
|
|
921
|
-
schema_call.keywords.append(
|
|
922
|
-
ast.keyword(
|
|
923
|
-
arg="model_name", value=ast.Constant(value=schema.model_name)
|
|
924
|
-
)
|
|
925
|
-
)
|
|
926
|
-
|
|
927
|
-
return [
|
|
928
|
-
ast.Assign(
|
|
929
|
-
targets=[ast.Name(id=schema_name, ctx=ast.Store())], value=schema_call
|
|
930
|
-
)
|
|
931
|
-
]
|
|
932
|
-
|
|
933
|
-
def _generate_form_wrapper(
|
|
934
|
-
self,
|
|
935
|
-
form_id: int,
|
|
936
|
-
original_handler: str,
|
|
937
|
-
schema_name: str,
|
|
938
|
-
schema: FormValidationSchema,
|
|
939
|
-
known_globals: Set[str],
|
|
940
|
-
) -> ast.AsyncFunctionDef:
|
|
941
|
-
"""Generate wrapper handler that validates then calls original handler."""
|
|
942
|
-
wrapper_name = f"_form_submit_{form_id}"
|
|
943
|
-
|
|
944
|
-
# Generate:
|
|
945
|
-
# async def _form_submit_0(self, **kwargs):
|
|
946
|
-
# form_data = kwargs.get('formData', {})
|
|
947
|
-
#
|
|
948
|
-
# # Build state getter for conditional validation
|
|
949
|
-
# def get_state(expr):
|
|
950
|
-
# return eval(expr, {'self': self})
|
|
951
|
-
#
|
|
952
|
-
# # Validate
|
|
953
|
-
# self.errors = form_validator.validate_form(form_data, self._form_schema_0, get_state)
|
|
954
|
-
# if self.errors:
|
|
955
|
-
# return
|
|
956
|
-
#
|
|
957
|
-
# # Call original handler
|
|
958
|
-
# await self.original_handler(form_data)
|
|
959
|
-
|
|
960
|
-
body: List[ast.stmt] = []
|
|
961
|
-
|
|
962
|
-
# form_data = kwargs.get('formData', {})
|
|
963
|
-
body.append(
|
|
964
|
-
ast.Assign(
|
|
965
|
-
targets=[ast.Name(id="form_data", ctx=ast.Store())],
|
|
966
|
-
value=ast.Call(
|
|
967
|
-
func=ast.Attribute(
|
|
968
|
-
value=ast.Name(id="kwargs", ctx=ast.Load()),
|
|
969
|
-
attr="get",
|
|
970
|
-
ctx=ast.Load(),
|
|
971
|
-
),
|
|
972
|
-
args=[ast.Constant(value="formData"), ast.Dict(keys=[], values=[])],
|
|
973
|
-
keywords=[],
|
|
974
|
-
),
|
|
975
|
-
)
|
|
976
|
-
)
|
|
977
|
-
|
|
978
|
-
# Define state getter for conditional validation
|
|
979
|
-
# def get_state(expr):
|
|
980
|
-
# return eval(expr, {'self': self})
|
|
981
|
-
state_getter = ast.FunctionDef(
|
|
982
|
-
name="get_state",
|
|
983
|
-
args=ast.arguments(
|
|
984
|
-
posonlyargs=[],
|
|
985
|
-
args=[ast.arg(arg="expr")],
|
|
986
|
-
vararg=None,
|
|
987
|
-
kwonlyargs=[],
|
|
988
|
-
kw_defaults=[],
|
|
989
|
-
defaults=[],
|
|
990
|
-
),
|
|
991
|
-
body=[
|
|
992
|
-
ast.Return(
|
|
993
|
-
value=ast.Call(
|
|
994
|
-
func=ast.Name(id="eval", ctx=ast.Load()),
|
|
995
|
-
args=[
|
|
996
|
-
ast.Name(id="expr", ctx=ast.Load()),
|
|
997
|
-
# Use module globals (imports, classes)
|
|
998
|
-
ast.Call(
|
|
999
|
-
func=ast.Name(id="globals", ctx=ast.Load()),
|
|
1000
|
-
args=[],
|
|
1001
|
-
keywords=[],
|
|
1002
|
-
),
|
|
1003
|
-
# Locals: just self, because _transform_expr converts other names
|
|
1004
|
-
# to self.x
|
|
1005
|
-
ast.Dict(
|
|
1006
|
-
keys=[ast.Constant(value="self")],
|
|
1007
|
-
values=[ast.Name(id="self", ctx=ast.Load())],
|
|
1008
|
-
),
|
|
1009
|
-
],
|
|
1010
|
-
keywords=[],
|
|
1011
|
-
)
|
|
1012
|
-
)
|
|
1013
|
-
],
|
|
1014
|
-
decorator_list=[],
|
|
1015
|
-
returns=None,
|
|
1016
|
-
)
|
|
1017
|
-
body.append(state_getter)
|
|
1018
|
-
|
|
1019
|
-
# cleaned_data, self.errors = form_validator.validate_form(
|
|
1020
|
-
# form_data, self._form_schema_X.fields, get_state
|
|
1021
|
-
# )
|
|
1022
|
-
# Note: pass .fields from the schema object
|
|
1023
|
-
body.append(
|
|
1024
|
-
ast.Assign(
|
|
1025
|
-
targets=[
|
|
1026
|
-
ast.Tuple(
|
|
1027
|
-
elts=[
|
|
1028
|
-
ast.Name(id="cleaned_data", ctx=ast.Store()),
|
|
1029
|
-
ast.Attribute(
|
|
1030
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1031
|
-
attr="errors",
|
|
1032
|
-
ctx=ast.Store(),
|
|
1033
|
-
),
|
|
1034
|
-
],
|
|
1035
|
-
ctx=ast.Store(),
|
|
1036
|
-
)
|
|
1037
|
-
],
|
|
1038
|
-
value=ast.Call(
|
|
1039
|
-
func=ast.Attribute(
|
|
1040
|
-
value=ast.Name(id="form_validator", ctx=ast.Load()),
|
|
1041
|
-
attr="validate_form",
|
|
1042
|
-
ctx=ast.Load(),
|
|
1043
|
-
),
|
|
1044
|
-
args=[
|
|
1045
|
-
ast.Name(id="form_data", ctx=ast.Load()),
|
|
1046
|
-
ast.Attribute(
|
|
1047
|
-
value=ast.Attribute(
|
|
1048
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1049
|
-
attr=schema_name,
|
|
1050
|
-
ctx=ast.Load(),
|
|
1051
|
-
),
|
|
1052
|
-
attr="fields",
|
|
1053
|
-
ctx=ast.Load(),
|
|
1054
|
-
),
|
|
1055
|
-
ast.Name(id="get_state", ctx=ast.Load()),
|
|
1056
|
-
],
|
|
1057
|
-
keywords=[],
|
|
1058
|
-
),
|
|
1059
|
-
)
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
# If Pydantic model is used:
|
|
1063
|
-
# if not self.errors and self._form_schema_X.model_name:
|
|
1064
|
-
# model_instance, pydantic_errors = validate_with_model(
|
|
1065
|
-
# cleaned_data, globals()[self._form_schema_X.model_name]
|
|
1066
|
-
# )
|
|
1067
|
-
# if pydantic_errors:
|
|
1068
|
-
# self.errors.update(pydantic_errors)
|
|
1069
|
-
# else:
|
|
1070
|
-
# cleaned_data = model_instance # Replace dict with model instance
|
|
1071
|
-
|
|
1072
|
-
if schema.model_name:
|
|
1073
|
-
pydantic_block: List[ast.stmt] = []
|
|
1074
|
-
|
|
1075
|
-
# model_instance, pydantic_errors = validate_with_model(cleaned_data, ModelClass)
|
|
1076
|
-
|
|
1077
|
-
# PARSE NESTED DATA FIRST
|
|
1078
|
-
# nested_data = form_validator.parse_nested_data(cleaned_data)
|
|
1079
|
-
pydantic_block.append(
|
|
1080
|
-
ast.Assign(
|
|
1081
|
-
targets=[ast.Name(id="nested_data", ctx=ast.Store())],
|
|
1082
|
-
value=ast.Call(
|
|
1083
|
-
func=ast.Attribute(
|
|
1084
|
-
value=ast.Name(id="form_validator", ctx=ast.Load()),
|
|
1085
|
-
attr="parse_nested_data",
|
|
1086
|
-
ctx=ast.Load(),
|
|
1087
|
-
),
|
|
1088
|
-
args=[ast.Name(id="cleaned_data", ctx=ast.Load())],
|
|
1089
|
-
keywords=[],
|
|
1090
|
-
),
|
|
1091
|
-
)
|
|
1092
|
-
)
|
|
1093
|
-
|
|
1094
|
-
validate_call = ast.Call(
|
|
1095
|
-
func=ast.Name(id="validate_with_model", ctx=ast.Load()),
|
|
1096
|
-
args=[
|
|
1097
|
-
ast.Name(id="nested_data", ctx=ast.Load()),
|
|
1098
|
-
ast.Name(id=schema.model_name, ctx=ast.Load()),
|
|
1099
|
-
],
|
|
1100
|
-
keywords=[],
|
|
1101
|
-
)
|
|
1102
|
-
|
|
1103
|
-
pydantic_block.append(
|
|
1104
|
-
ast.Assign(
|
|
1105
|
-
targets=[
|
|
1106
|
-
ast.Tuple(
|
|
1107
|
-
elts=[
|
|
1108
|
-
ast.Name(id="model_instance", ctx=ast.Store()),
|
|
1109
|
-
ast.Name(id="pydantic_errors", ctx=ast.Store()),
|
|
1110
|
-
],
|
|
1111
|
-
ctx=ast.Store(),
|
|
1112
|
-
)
|
|
1113
|
-
],
|
|
1114
|
-
value=validate_call,
|
|
1115
|
-
)
|
|
1116
|
-
)
|
|
1117
|
-
|
|
1118
|
-
# if pydantic_errors: self.errors.update(pydantic_errors)
|
|
1119
|
-
# else: cleaned_data = model_instance
|
|
1120
|
-
pydantic_block.append(
|
|
1121
|
-
ast.If(
|
|
1122
|
-
test=ast.Name(id="pydantic_errors", ctx=ast.Load()),
|
|
1123
|
-
body=[
|
|
1124
|
-
ast.Expr(
|
|
1125
|
-
value=ast.Call(
|
|
1126
|
-
func=ast.Attribute(
|
|
1127
|
-
value=ast.Attribute(
|
|
1128
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1129
|
-
attr="errors",
|
|
1130
|
-
ctx=ast.Load(),
|
|
1131
|
-
),
|
|
1132
|
-
attr="update",
|
|
1133
|
-
ctx=ast.Load(),
|
|
1134
|
-
),
|
|
1135
|
-
args=[ast.Name(id="pydantic_errors", ctx=ast.Load())],
|
|
1136
|
-
keywords=[],
|
|
1137
|
-
)
|
|
1138
|
-
)
|
|
1139
|
-
],
|
|
1140
|
-
orelse=[
|
|
1141
|
-
ast.Assign(
|
|
1142
|
-
targets=[ast.Name(id="cleaned_data", ctx=ast.Store())],
|
|
1143
|
-
value=ast.Name(id="model_instance", ctx=ast.Load()),
|
|
1144
|
-
)
|
|
1145
|
-
],
|
|
1146
|
-
)
|
|
1147
|
-
)
|
|
1148
|
-
|
|
1149
|
-
# Wrap in check: if not self.errors:
|
|
1150
|
-
body.append(
|
|
1151
|
-
ast.If(
|
|
1152
|
-
test=ast.UnaryOp(
|
|
1153
|
-
op=ast.Not(),
|
|
1154
|
-
operand=ast.Attribute(
|
|
1155
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1156
|
-
attr="errors",
|
|
1157
|
-
ctx=ast.Load(),
|
|
1158
|
-
),
|
|
1159
|
-
),
|
|
1160
|
-
body=pydantic_block,
|
|
1161
|
-
orelse=[],
|
|
1162
|
-
)
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1165
|
-
# if self.errors: return
|
|
1166
|
-
body.append(
|
|
1167
|
-
ast.If(
|
|
1168
|
-
test=ast.Attribute(
|
|
1169
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1170
|
-
attr="errors",
|
|
1171
|
-
ctx=ast.Load(),
|
|
1172
|
-
),
|
|
1173
|
-
body=[ast.Return(value=None)],
|
|
1174
|
-
orelse=[],
|
|
1175
|
-
)
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
# Call original handler - need to check if it's async
|
|
1179
|
-
handler_call = ast.Call(
|
|
1180
|
-
func=ast.Attribute(
|
|
1181
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1182
|
-
attr=original_handler,
|
|
1183
|
-
ctx=ast.Load(),
|
|
1184
|
-
),
|
|
1185
|
-
args=[ast.Name(id="cleaned_data", ctx=ast.Load())],
|
|
1186
|
-
keywords=[],
|
|
1187
|
-
)
|
|
1188
|
-
|
|
1189
|
-
# Assume async for safety - await it
|
|
1190
|
-
body.append(ast.Expr(value=ast.Await(value=handler_call)))
|
|
1191
|
-
|
|
1192
|
-
return ast.AsyncFunctionDef(
|
|
1193
|
-
name=wrapper_name,
|
|
1194
|
-
args=ast.arguments(
|
|
1195
|
-
posonlyargs=[],
|
|
1196
|
-
args=[ast.arg(arg="self")],
|
|
1197
|
-
vararg=None,
|
|
1198
|
-
kwonlyargs=[],
|
|
1199
|
-
kw_defaults=[],
|
|
1200
|
-
kwarg=ast.arg(arg="kwargs"),
|
|
1201
|
-
defaults=[],
|
|
1202
|
-
),
|
|
1203
|
-
body=body,
|
|
1204
|
-
decorator_list=[],
|
|
1205
|
-
returns=None,
|
|
1206
|
-
)
|
|
1207
|
-
|
|
1208
|
-
def _generate_spa_metadata(self, parsed: ParsedPyWire) -> List[ast.stmt]:
|
|
1209
|
-
"""Generate __spa_enabled__ and __sibling_paths__ class attributes."""
|
|
1210
|
-
stmts: List[ast.stmt] = []
|
|
1211
|
-
|
|
1212
|
-
# Get path directive
|
|
1213
|
-
path_directive = cast(
|
|
1214
|
-
Optional[PathDirective], parsed.get_directive_by_type(PathDirective)
|
|
1215
|
-
)
|
|
1216
|
-
if path_directive:
|
|
1217
|
-
# assert isinstance(path_directive, PathDirective)
|
|
1218
|
-
pass
|
|
1219
|
-
is_multi_path = path_directive and not path_directive.is_simple_string
|
|
1220
|
-
|
|
1221
|
-
# Check for !no_spa directive
|
|
1222
|
-
no_spa = parsed.get_directive_by_type(NoSpaDirective) is not None
|
|
1223
|
-
|
|
1224
|
-
# SPA is enabled for multi-path pages unless !no_spa is present
|
|
1225
|
-
spa_enabled = is_multi_path and not no_spa
|
|
1226
|
-
|
|
1227
|
-
# __spa_enabled__ = True/False
|
|
1228
|
-
stmts.append(
|
|
1229
|
-
ast.Assign(
|
|
1230
|
-
targets=[ast.Name(id="__spa_enabled__", ctx=ast.Store())],
|
|
1231
|
-
value=ast.Constant(value=bool(spa_enabled)),
|
|
1232
|
-
)
|
|
1233
|
-
)
|
|
1234
|
-
|
|
1235
|
-
# __sibling_paths__ = ['/path1', '/path2', ...]
|
|
1236
|
-
if path_directive and not path_directive.is_simple_string:
|
|
1237
|
-
paths = list(path_directive.routes.values())
|
|
1238
|
-
else:
|
|
1239
|
-
paths = []
|
|
1240
|
-
|
|
1241
|
-
stmts.append(
|
|
1242
|
-
ast.Assign(
|
|
1243
|
-
targets=[ast.Name(id="__sibling_paths__", ctx=ast.Store())],
|
|
1244
|
-
value=ast.List(
|
|
1245
|
-
elts=[ast.Constant(value=p) for p in paths], ctx=ast.Load()
|
|
1246
|
-
),
|
|
1247
|
-
)
|
|
1248
|
-
)
|
|
1249
|
-
|
|
1250
|
-
# Inject __file_path__ for hot reload route cleanup
|
|
1251
|
-
if parsed.file_path:
|
|
1252
|
-
stmts.append(
|
|
1253
|
-
ast.Assign(
|
|
1254
|
-
targets=[ast.Name(id="__file_path__", ctx=ast.Store())],
|
|
1255
|
-
value=ast.Constant(value=str(parsed.file_path)),
|
|
1256
|
-
)
|
|
1257
|
-
)
|
|
1258
|
-
|
|
1259
|
-
return stmts
|
|
1260
|
-
|
|
1261
|
-
def _get_class_name(self, parsed: ParsedPyWire) -> str:
|
|
1262
|
-
"""Generate class name from file path."""
|
|
1263
|
-
if not parsed.file_path:
|
|
1264
|
-
return "Page"
|
|
1265
|
-
|
|
1266
|
-
from pathlib import Path
|
|
1267
|
-
|
|
1268
|
-
path = Path(parsed.file_path)
|
|
1269
|
-
# Convert pages/index.pywire -> IndexPage
|
|
1270
|
-
name = path.stem
|
|
1271
|
-
return "".join(word.capitalize() for word in name.split("_")) + "Page"
|
|
1272
|
-
|
|
1273
|
-
def _generate_init_method(self, parsed: ParsedPyWire) -> ast.FunctionDef:
|
|
1274
|
-
"""Generate __init__ method."""
|
|
1275
|
-
# Base init args
|
|
1276
|
-
init_args = [
|
|
1277
|
-
ast.arg(arg="self"),
|
|
1278
|
-
ast.arg(arg="request"),
|
|
1279
|
-
ast.arg(arg="params"),
|
|
1280
|
-
ast.arg(arg="query"),
|
|
1281
|
-
ast.arg(arg="path"),
|
|
1282
|
-
ast.arg(arg="url"),
|
|
1283
|
-
]
|
|
1284
|
-
defaults: List[ast.expr] = [ast.Constant(value=None), ast.Constant(value=None)]
|
|
1285
|
-
props_assigns: List[ast.stmt] = []
|
|
1286
|
-
|
|
1287
|
-
# Handle Props directive
|
|
1288
|
-
props_directive = parsed.get_directive_by_type(PropsDirective)
|
|
1289
|
-
if props_directive:
|
|
1290
|
-
assert isinstance(props_directive, PropsDirective)
|
|
1291
|
-
# !props(name: type, arg=default)
|
|
1292
|
-
# Add to init_args
|
|
1293
|
-
for name, type_hint, default_val in props_directive.args:
|
|
1294
|
-
# Annotation
|
|
1295
|
-
annotation = (
|
|
1296
|
-
ast.parse(type_hint, mode="eval").body if type_hint else None
|
|
1297
|
-
)
|
|
1298
|
-
|
|
1299
|
-
# Default
|
|
1300
|
-
if default_val is not None:
|
|
1301
|
-
# Parse default value expr
|
|
1302
|
-
defaults.append(ast.parse(default_val, mode="eval").body)
|
|
1303
|
-
else:
|
|
1304
|
-
# No default: must come before args with defaults?
|
|
1305
|
-
# Standard python rules: non-default args first.
|
|
1306
|
-
# But we are appending AFTER request/params etc which DON'T have defaults
|
|
1307
|
-
# (except path/url which do)
|
|
1308
|
-
# Wait, request, params, query don't have defaults in base method signature
|
|
1309
|
-
# we generated before:
|
|
1310
|
-
# args=[arg('self'), arg('request')...]
|
|
1311
|
-
# defaults=[None, None] (for path/url?)
|
|
1312
|
-
|
|
1313
|
-
# Actually standard signature above was:
|
|
1314
|
-
# args: self, request, params, query, path, url
|
|
1315
|
-
# defaults: path=None, url=None
|
|
1316
|
-
|
|
1317
|
-
# So if we add a non-default prop after 'url=None', it's invalid syntax.
|
|
1318
|
-
# "non-default argument follows default argument"
|
|
1319
|
-
|
|
1320
|
-
# Strategy: Make ALL props keyword-only or ensure order?
|
|
1321
|
-
# Components are instantiated with kwargs mostly?
|
|
1322
|
-
# Or we just add them to the end and expect users to provide defaults if we
|
|
1323
|
-
# have defaults before?
|
|
1324
|
-
# Simpler: Make them keyword arguments (kwonlyargs).
|
|
1325
|
-
pass
|
|
1326
|
-
|
|
1327
|
-
# Implementation: Use kwonlyargs for props to avoid mess with positional defaults
|
|
1328
|
-
kwonlyargs: List[ast.arg] = []
|
|
1329
|
-
kw_defaults: List[Optional[ast.expr]] = []
|
|
1330
|
-
|
|
1331
|
-
for name, type_hint, default_val in props_directive.args:
|
|
1332
|
-
annotation = (
|
|
1333
|
-
ast.parse(type_hint, mode="eval").body if type_hint else None
|
|
1334
|
-
)
|
|
1335
|
-
kwonlyargs.append(ast.arg(arg=name, annotation=annotation))
|
|
1336
|
-
|
|
1337
|
-
if default_val is not None:
|
|
1338
|
-
kw_defaults.append(ast.parse(default_val, mode="eval").body)
|
|
1339
|
-
else:
|
|
1340
|
-
kw_defaults.append(None) # Required kwarg
|
|
1341
|
-
|
|
1342
|
-
# Assign to self
|
|
1343
|
-
# self.name = name
|
|
1344
|
-
props_assigns.append(
|
|
1345
|
-
ast.Assign(
|
|
1346
|
-
targets=[
|
|
1347
|
-
ast.Attribute(
|
|
1348
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1349
|
-
attr=name,
|
|
1350
|
-
ctx=ast.Store(),
|
|
1351
|
-
)
|
|
1352
|
-
],
|
|
1353
|
-
value=ast.Name(id=name, ctx=ast.Load()),
|
|
1354
|
-
)
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
else:
|
|
1358
|
-
kwonlyargs = []
|
|
1359
|
-
kw_defaults = []
|
|
1360
|
-
|
|
1361
|
-
body: List[ast.stmt] = [
|
|
1362
|
-
ast.Expr(
|
|
1363
|
-
value=ast.Call(
|
|
1364
|
-
func=ast.Attribute(
|
|
1365
|
-
value=ast.Call(
|
|
1366
|
-
func=ast.Name(id="super", ctx=ast.Load()),
|
|
1367
|
-
args=[],
|
|
1368
|
-
keywords=[],
|
|
1369
|
-
),
|
|
1370
|
-
attr="__init__",
|
|
1371
|
-
ctx=ast.Load(),
|
|
1372
|
-
),
|
|
1373
|
-
args=[
|
|
1374
|
-
ast.Name(id="request", ctx=ast.Load()),
|
|
1375
|
-
ast.Name(id="params", ctx=ast.Load()),
|
|
1376
|
-
ast.Name(id="query", ctx=ast.Load()),
|
|
1377
|
-
ast.Name(id="path", ctx=ast.Load()),
|
|
1378
|
-
ast.Name(id="url", ctx=ast.Load()),
|
|
1379
|
-
],
|
|
1380
|
-
keywords=[
|
|
1381
|
-
ast.keyword(
|
|
1382
|
-
arg=None, value=ast.Name(id="kwargs", ctx=ast.Load())
|
|
1383
|
-
)
|
|
1384
|
-
],
|
|
1385
|
-
)
|
|
1386
|
-
)
|
|
1387
|
-
]
|
|
1388
|
-
|
|
1389
|
-
# Add prop assignments
|
|
1390
|
-
body.extend(props_assigns)
|
|
1391
|
-
|
|
1392
|
-
# NOTE: !provide is now handled in _generate_render_template_method to ensure reactivity
|
|
1393
|
-
|
|
1394
|
-
# Handle !inject - retrieve values from context
|
|
1395
|
-
inject_directive = parsed.get_directive_by_type(InjectDirective)
|
|
1396
|
-
if inject_directive:
|
|
1397
|
-
assert isinstance(inject_directive, InjectDirective)
|
|
1398
|
-
# self.local_var = self.context.get('GLOBAL_KEY')
|
|
1399
|
-
for local_var, global_key in inject_directive.mapping.items():
|
|
1400
|
-
body.append(
|
|
1401
|
-
ast.Assign(
|
|
1402
|
-
targets=[
|
|
1403
|
-
ast.Attribute(
|
|
1404
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1405
|
-
attr=local_var,
|
|
1406
|
-
ctx=ast.Store(),
|
|
1407
|
-
)
|
|
1408
|
-
],
|
|
1409
|
-
value=ast.Call(
|
|
1410
|
-
func=ast.Attribute(
|
|
1411
|
-
value=ast.Attribute(
|
|
1412
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1413
|
-
attr="context",
|
|
1414
|
-
ctx=ast.Load(),
|
|
1415
|
-
),
|
|
1416
|
-
attr="get",
|
|
1417
|
-
ctx=ast.Load(),
|
|
1418
|
-
),
|
|
1419
|
-
args=[ast.Constant(value=global_key)],
|
|
1420
|
-
keywords=[],
|
|
1421
|
-
),
|
|
1422
|
-
)
|
|
1423
|
-
)
|
|
1424
|
-
|
|
1425
|
-
# Call _init_slots
|
|
1426
|
-
body.append(
|
|
1427
|
-
ast.Expr(
|
|
1428
|
-
value=ast.Call(
|
|
1429
|
-
func=ast.Attribute(
|
|
1430
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1431
|
-
attr="_init_slots",
|
|
1432
|
-
ctx=ast.Load(),
|
|
1433
|
-
),
|
|
1434
|
-
args=[],
|
|
1435
|
-
keywords=[],
|
|
1436
|
-
)
|
|
1437
|
-
)
|
|
1438
|
-
)
|
|
1439
|
-
|
|
1440
|
-
# Call __top_level_init__ if it exists (for wire() and mutable init)
|
|
1441
|
-
if hasattr(self, "_has_top_level_init") and self._has_top_level_init:
|
|
1442
|
-
body.append(
|
|
1443
|
-
ast.Expr(
|
|
1444
|
-
value=ast.Call(
|
|
1445
|
-
func=ast.Attribute(
|
|
1446
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1447
|
-
attr="__top_level_init__",
|
|
1448
|
-
ctx=ast.Load(),
|
|
1449
|
-
),
|
|
1450
|
-
args=[],
|
|
1451
|
-
keywords=[],
|
|
1452
|
-
)
|
|
1453
|
-
)
|
|
1454
|
-
)
|
|
1455
|
-
|
|
1456
|
-
return ast.FunctionDef(
|
|
1457
|
-
name="__init__",
|
|
1458
|
-
args=ast.arguments(
|
|
1459
|
-
posonlyargs=[],
|
|
1460
|
-
args=init_args,
|
|
1461
|
-
vararg=None,
|
|
1462
|
-
kwonlyargs=kwonlyargs,
|
|
1463
|
-
kw_defaults=kw_defaults,
|
|
1464
|
-
kwarg=ast.arg(arg="kwargs", annotation=None),
|
|
1465
|
-
defaults=defaults,
|
|
1466
|
-
),
|
|
1467
|
-
body=body,
|
|
1468
|
-
decorator_list=[],
|
|
1469
|
-
returns=None,
|
|
1470
|
-
)
|
|
1471
|
-
|
|
1472
|
-
def _transform_user_code(
|
|
1473
|
-
self, python_ast: ast.Module, known_globals: Optional[Set[str]] = None
|
|
1474
|
-
) -> List[ast.stmt]:
|
|
1475
|
-
"""Transform user Python code to class methods/attributes."""
|
|
1476
|
-
transformed: List[ast.stmt] = []
|
|
1477
|
-
if known_globals is None:
|
|
1478
|
-
known_globals = set()
|
|
1479
|
-
|
|
1480
|
-
# Collect hooks
|
|
1481
|
-
self._collected_mount_hooks = []
|
|
1482
|
-
self._has_top_level_init = False
|
|
1483
|
-
|
|
1484
|
-
top_level_statements: List[ast.stmt] = []
|
|
1485
|
-
|
|
1486
|
-
for node in python_ast.body:
|
|
1487
|
-
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
1488
|
-
# Skip imports - already handled
|
|
1489
|
-
continue
|
|
1490
|
-
elif isinstance(node, ast.Assign):
|
|
1491
|
-
# Module-level assignments become class attributes
|
|
1492
|
-
# UNLESS they target 'self' (e.g. self.x = 1), which makes no sense at class level
|
|
1493
|
-
# and implies instance initialization.
|
|
1494
|
-
|
|
1495
|
-
is_instance_assign = False
|
|
1496
|
-
for target in node.targets:
|
|
1497
|
-
# Check if target is Attribute(value=Name(id='self'))
|
|
1498
|
-
if (
|
|
1499
|
-
isinstance(target, ast.Attribute)
|
|
1500
|
-
and isinstance(target.value, ast.Name)
|
|
1501
|
-
and target.value.id == "self"
|
|
1502
|
-
):
|
|
1503
|
-
is_instance_assign = True
|
|
1504
|
-
break
|
|
1505
|
-
|
|
1506
|
-
# Also check if value is a Call (like wire()) or mutable structure.
|
|
1507
|
-
# These should be instance-level to avoid shared state.
|
|
1508
|
-
is_mutable_init = isinstance(
|
|
1509
|
-
node.value,
|
|
1510
|
-
(
|
|
1511
|
-
ast.Call,
|
|
1512
|
-
ast.List,
|
|
1513
|
-
ast.Dict,
|
|
1514
|
-
ast.Set,
|
|
1515
|
-
ast.ListComp,
|
|
1516
|
-
ast.DictComp,
|
|
1517
|
-
ast.SetComp,
|
|
1518
|
-
),
|
|
1519
|
-
)
|
|
1520
|
-
|
|
1521
|
-
if is_instance_assign or is_mutable_init:
|
|
1522
|
-
top_level_statements.append(node)
|
|
1523
|
-
else:
|
|
1524
|
-
# Simple literals (int, str) stay class attributes
|
|
1525
|
-
transformed.append(node)
|
|
1526
|
-
|
|
1527
|
-
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1528
|
-
# Check for decorators
|
|
1529
|
-
new_decorators = []
|
|
1530
|
-
for dec in node.decorator_list:
|
|
1531
|
-
if isinstance(dec, ast.Name) and dec.id == "mount":
|
|
1532
|
-
self._collected_mount_hooks.append(node.name)
|
|
1533
|
-
elif isinstance(dec, ast.Name) and dec.id == "unmount":
|
|
1534
|
-
# Placeholder for future unmount
|
|
1535
|
-
pass
|
|
1536
|
-
else:
|
|
1537
|
-
new_decorators.append(dec)
|
|
1538
|
-
|
|
1539
|
-
node.decorator_list = new_decorators
|
|
1540
|
-
|
|
1541
|
-
# Functions become methods - transform them
|
|
1542
|
-
transformed.append(self._transform_to_method(node, known_globals))
|
|
1543
|
-
elif isinstance(node, ast.ClassDef):
|
|
1544
|
-
# Classes are moved to module level, skip here
|
|
1545
|
-
continue
|
|
1546
|
-
else:
|
|
1547
|
-
# Other statements (Expr, If, For, While, Try)
|
|
1548
|
-
# Move to top-level init
|
|
1549
|
-
top_level_statements.append(node)
|
|
1550
|
-
|
|
1551
|
-
if top_level_statements:
|
|
1552
|
-
self._has_top_level_init = True
|
|
1553
|
-
transformed.append(
|
|
1554
|
-
self._generate_top_level_init(top_level_statements, known_globals)
|
|
1555
|
-
)
|
|
1556
|
-
|
|
1557
|
-
return transformed
|
|
1558
|
-
|
|
1559
|
-
def _generate_top_level_init(
|
|
1560
|
-
self, statements: List[ast.stmt], known_globals: Set[str]
|
|
1561
|
-
) -> ast.FunctionDef:
|
|
1562
|
-
"""Generate __top_level_init__ method from top-level statements."""
|
|
1563
|
-
|
|
1564
|
-
# 1. Collect all variables assigned in this scope to promote them to instance attributes.
|
|
1565
|
-
# This ensures 'x = 1' inside match/if/for becomes 'self.x = 1'.
|
|
1566
|
-
local_assignments = set()
|
|
1567
|
-
|
|
1568
|
-
class AssignmentCollector(ast.NodeVisitor):
|
|
1569
|
-
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
1570
|
-
# Do not recurse into nested functions
|
|
1571
|
-
pass
|
|
1572
|
-
|
|
1573
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
1574
|
-
pass
|
|
1575
|
-
|
|
1576
|
-
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
1577
|
-
pass
|
|
1578
|
-
|
|
1579
|
-
def visit_Name(self, node: ast.Name) -> None:
|
|
1580
|
-
# If name is being stored (assigned to), collect it
|
|
1581
|
-
if isinstance(node.ctx, ast.Store):
|
|
1582
|
-
local_assignments.add(node.id)
|
|
1583
|
-
|
|
1584
|
-
collector = AssignmentCollector()
|
|
1585
|
-
for stmt in statements:
|
|
1586
|
-
collector.visit(stmt)
|
|
1587
|
-
|
|
1588
|
-
# Combine with explicit known globals
|
|
1589
|
-
# We start with a copy to avoid mutating the passed set if it's used elsewhere
|
|
1590
|
-
# (though it seems local usually)
|
|
1591
|
-
combined_globals = set(known_globals)
|
|
1592
|
-
combined_globals.update(local_assignments)
|
|
1593
|
-
|
|
1594
|
-
# Wrap statements in sync method (must be sync for __init__)
|
|
1595
|
-
# Transform variables to self.X
|
|
1596
|
-
|
|
1597
|
-
wrapper = ast.FunctionDef(
|
|
1598
|
-
name="__top_level_init__",
|
|
1599
|
-
args=ast.arguments(
|
|
1600
|
-
posonlyargs=[],
|
|
1601
|
-
args=[ast.arg(arg="self")],
|
|
1602
|
-
vararg=None,
|
|
1603
|
-
kwonlyargs=[],
|
|
1604
|
-
kw_defaults=[],
|
|
1605
|
-
defaults=[],
|
|
1606
|
-
),
|
|
1607
|
-
body=statements,
|
|
1608
|
-
decorator_list=[],
|
|
1609
|
-
returns=None,
|
|
1610
|
-
)
|
|
1611
|
-
|
|
1612
|
-
return cast(
|
|
1613
|
-
ast.FunctionDef, self._transform_to_method(wrapper, combined_globals)
|
|
1614
|
-
)
|
|
1615
|
-
|
|
1616
|
-
def _transform_to_method(
|
|
1617
|
-
self, node: Any, known_methods: Optional[Set[str]] = None
|
|
1618
|
-
) -> Any:
|
|
1619
|
-
"""Transform a function into a method (add self, handle globals)."""
|
|
1620
|
-
# 1. Add self argument if not present
|
|
1621
|
-
if not (node.args.args and node.args.args[0].arg == "self"):
|
|
1622
|
-
node.args.args.insert(0, ast.arg(arg="self"))
|
|
1623
|
-
|
|
1624
|
-
# 2. Find global declarations and include known methods
|
|
1625
|
-
global_vars = set()
|
|
1626
|
-
if known_methods:
|
|
1627
|
-
global_vars.update(known_methods)
|
|
1628
|
-
|
|
1629
|
-
new_body = []
|
|
1630
|
-
for stmt in node.body:
|
|
1631
|
-
if isinstance(stmt, ast.Global):
|
|
1632
|
-
global_vars.update(stmt.names)
|
|
1633
|
-
else:
|
|
1634
|
-
new_body.append(stmt)
|
|
1635
|
-
|
|
1636
|
-
node.body = new_body
|
|
1637
|
-
|
|
1638
|
-
# 3. Transform variable access
|
|
1639
|
-
if global_vars:
|
|
1640
|
-
|
|
1641
|
-
class GlobalToSelf(ast.NodeTransformer):
|
|
1642
|
-
def visit_Name(self, n: ast.Name) -> ast.AST:
|
|
1643
|
-
if n.id in global_vars:
|
|
1644
|
-
return ast.Attribute(
|
|
1645
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1646
|
-
attr=n.id,
|
|
1647
|
-
ctx=n.ctx,
|
|
1648
|
-
)
|
|
1649
|
-
return n
|
|
1650
|
-
|
|
1651
|
-
# Apply transformation
|
|
1652
|
-
transformer = GlobalToSelf()
|
|
1653
|
-
for i, stmt in enumerate(node.body):
|
|
1654
|
-
node.body[i] = transformer.visit(stmt)
|
|
1655
|
-
|
|
1656
|
-
# Fix locations
|
|
1657
|
-
for stmt in node.body:
|
|
1658
|
-
ast.fix_missing_locations(stmt)
|
|
1659
|
-
|
|
1660
|
-
return node
|
|
1661
|
-
|
|
1662
|
-
def _generate_render_method(self) -> ast.AsyncFunctionDef:
|
|
1663
|
-
"""Generate render method."""
|
|
1664
|
-
return ast.AsyncFunctionDef(
|
|
1665
|
-
name="render",
|
|
1666
|
-
args=ast.arguments(
|
|
1667
|
-
posonlyargs=[],
|
|
1668
|
-
args=[ast.arg(arg="self")],
|
|
1669
|
-
vararg=None,
|
|
1670
|
-
kwonlyargs=[],
|
|
1671
|
-
kw_defaults=[],
|
|
1672
|
-
defaults=[],
|
|
1673
|
-
),
|
|
1674
|
-
body=[
|
|
1675
|
-
ast.Return(
|
|
1676
|
-
value=ast.Call(
|
|
1677
|
-
func=ast.Name(id="Response", ctx=ast.Load()),
|
|
1678
|
-
args=[
|
|
1679
|
-
ast.Await(
|
|
1680
|
-
value=ast.Call(
|
|
1681
|
-
func=ast.Attribute(
|
|
1682
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1683
|
-
attr="_render_template",
|
|
1684
|
-
ctx=ast.Load(),
|
|
1685
|
-
),
|
|
1686
|
-
args=[],
|
|
1687
|
-
keywords=[],
|
|
1688
|
-
)
|
|
1689
|
-
)
|
|
1690
|
-
],
|
|
1691
|
-
keywords=[
|
|
1692
|
-
ast.keyword(
|
|
1693
|
-
arg="media_type", value=ast.Constant(value="text/html")
|
|
1694
|
-
)
|
|
1695
|
-
],
|
|
1696
|
-
)
|
|
1697
|
-
)
|
|
1698
|
-
],
|
|
1699
|
-
decorator_list=[],
|
|
1700
|
-
returns=None,
|
|
1701
|
-
)
|
|
1702
|
-
|
|
1703
|
-
def _generate_render_template_method(
|
|
1704
|
-
self,
|
|
1705
|
-
parsed: ParsedPyWire,
|
|
1706
|
-
known_methods: Optional[Set[str]] = None,
|
|
1707
|
-
known_globals: Optional[Set[str]] = None,
|
|
1708
|
-
async_methods: Optional[Set[str]] = None,
|
|
1709
|
-
component_map: Optional[Dict[str, str]] = None,
|
|
1710
|
-
) -> Tuple[
|
|
1711
|
-
Optional[Union[ast.FunctionDef, ast.AsyncFunctionDef]],
|
|
1712
|
-
List[ast.stmt],
|
|
1713
|
-
]:
|
|
1714
|
-
"""Generate _render_template method and binding/slot handlers."""
|
|
1715
|
-
if component_map is None:
|
|
1716
|
-
component_map = {}
|
|
1717
|
-
# Check for layout
|
|
1718
|
-
layout_directive = parsed.get_directive_by_type(LayoutDirective)
|
|
1719
|
-
if layout_directive:
|
|
1720
|
-
# assert isinstance(layout_directive, LayoutDirective) # Mypy narrowing issue
|
|
1721
|
-
pass
|
|
1722
|
-
|
|
1723
|
-
binding_funcs: List[ast.stmt] = []
|
|
1724
|
-
render_func = None
|
|
1725
|
-
|
|
1726
|
-
if layout_directive:
|
|
1727
|
-
layout_directive = cast(LayoutDirective, layout_directive)
|
|
1728
|
-
# === Layout Mode ===
|
|
1729
|
-
file_id = parsed.file_path or ""
|
|
1730
|
-
|
|
1731
|
-
# Ensure layout_id is generated for intermediate layouts
|
|
1732
|
-
import hashlib
|
|
1733
|
-
|
|
1734
|
-
layout_id = (
|
|
1735
|
-
hashlib.md5(str(parsed.file_path).encode()).hexdigest()
|
|
1736
|
-
if parsed.file_path
|
|
1737
|
-
else None
|
|
1738
|
-
)
|
|
1739
|
-
|
|
1740
|
-
slot_funcs_methods, aux_funcs = self.template_codegen.generate_slot_methods(
|
|
1741
|
-
parsed.template,
|
|
1742
|
-
file_id=file_id,
|
|
1743
|
-
known_globals=known_globals,
|
|
1744
|
-
layout_id=layout_id,
|
|
1745
|
-
component_map=component_map,
|
|
1746
|
-
)
|
|
1747
|
-
|
|
1748
|
-
file_hash = hashlib.md5(file_id.encode()).hexdigest()[:8] if file_id else ""
|
|
1749
|
-
|
|
1750
|
-
# Add slot methods directly (they are ASTs now)
|
|
1751
|
-
for slot_name, func_ast in slot_funcs_methods.items():
|
|
1752
|
-
binding_funcs.append(func_ast)
|
|
1753
|
-
|
|
1754
|
-
# Add aux funcs
|
|
1755
|
-
binding_funcs.extend(aux_funcs)
|
|
1756
|
-
|
|
1757
|
-
# Generate _init_slots
|
|
1758
|
-
|
|
1759
|
-
# Resolve parent layout path
|
|
1760
|
-
from pathlib import Path
|
|
1761
|
-
|
|
1762
|
-
parent_layout_path = layout_directive.layout_path
|
|
1763
|
-
if not Path(parent_layout_path).is_absolute():
|
|
1764
|
-
base_dir = (
|
|
1765
|
-
Path(parsed.file_path).parent if parsed.file_path else Path.cwd()
|
|
1766
|
-
)
|
|
1767
|
-
parent_layout_path = str((base_dir / parent_layout_path).resolve())
|
|
1768
|
-
else:
|
|
1769
|
-
parent_layout_path = str(Path(parent_layout_path).resolve())
|
|
1770
|
-
|
|
1771
|
-
def make_parent_layout_id() -> ast.Constant:
|
|
1772
|
-
import hashlib
|
|
1773
|
-
|
|
1774
|
-
parent_hash = hashlib.md5(parent_layout_path.encode()).hexdigest()
|
|
1775
|
-
return ast.Constant(value=parent_hash)
|
|
1776
|
-
|
|
1777
|
-
init_slots_body: List[ast.stmt] = []
|
|
1778
|
-
|
|
1779
|
-
# Chain super
|
|
1780
|
-
super_check = ast.If(
|
|
1781
|
-
test=ast.Call(
|
|
1782
|
-
func=ast.Name(id="hasattr", ctx=ast.Load()),
|
|
1783
|
-
args=[
|
|
1784
|
-
ast.Call(
|
|
1785
|
-
func=ast.Name(id="super", ctx=ast.Load()),
|
|
1786
|
-
args=[],
|
|
1787
|
-
keywords=[],
|
|
1788
|
-
),
|
|
1789
|
-
ast.Constant(value="_init_slots"),
|
|
1790
|
-
],
|
|
1791
|
-
keywords=[],
|
|
1792
|
-
),
|
|
1793
|
-
body=[
|
|
1794
|
-
ast.Expr(
|
|
1795
|
-
value=ast.Call(
|
|
1796
|
-
func=ast.Attribute(
|
|
1797
|
-
value=ast.Call(
|
|
1798
|
-
func=ast.Name(id="super", ctx=ast.Load()),
|
|
1799
|
-
args=[],
|
|
1800
|
-
keywords=[],
|
|
1801
|
-
),
|
|
1802
|
-
attr="_init_slots",
|
|
1803
|
-
ctx=ast.Load(),
|
|
1804
|
-
),
|
|
1805
|
-
args=[],
|
|
1806
|
-
keywords=[],
|
|
1807
|
-
)
|
|
1808
|
-
)
|
|
1809
|
-
],
|
|
1810
|
-
orelse=[],
|
|
1811
|
-
)
|
|
1812
|
-
init_slots_body.append(super_check)
|
|
1813
|
-
|
|
1814
|
-
for slot_name in slot_funcs_methods.keys():
|
|
1815
|
-
safe_name = (
|
|
1816
|
-
slot_name.replace("$", "_head_").replace("-", "_")
|
|
1817
|
-
if slot_name.startswith("$")
|
|
1818
|
-
else slot_name.replace("-", "_")
|
|
1819
|
-
)
|
|
1820
|
-
func_name = (
|
|
1821
|
-
f"_render_slot_fill_{safe_name}_{file_hash}"
|
|
1822
|
-
if file_hash
|
|
1823
|
-
else f"_render_slot_fill_{safe_name}"
|
|
1824
|
-
)
|
|
1825
|
-
|
|
1826
|
-
if slot_name == "$head":
|
|
1827
|
-
reg_call = ast.Expr(
|
|
1828
|
-
value=ast.Call(
|
|
1829
|
-
func=ast.Attribute(
|
|
1830
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1831
|
-
attr="register_head_slot",
|
|
1832
|
-
ctx=ast.Load(),
|
|
1833
|
-
),
|
|
1834
|
-
args=[
|
|
1835
|
-
make_parent_layout_id(),
|
|
1836
|
-
ast.Attribute(
|
|
1837
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1838
|
-
attr=func_name,
|
|
1839
|
-
ctx=ast.Load(),
|
|
1840
|
-
),
|
|
1841
|
-
],
|
|
1842
|
-
keywords=[],
|
|
1843
|
-
)
|
|
1844
|
-
)
|
|
1845
|
-
else:
|
|
1846
|
-
reg_call = ast.Expr(
|
|
1847
|
-
value=ast.Call(
|
|
1848
|
-
func=ast.Attribute(
|
|
1849
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1850
|
-
attr="register_slot",
|
|
1851
|
-
ctx=ast.Load(),
|
|
1852
|
-
),
|
|
1853
|
-
args=[
|
|
1854
|
-
make_parent_layout_id(),
|
|
1855
|
-
ast.Constant(value=slot_name),
|
|
1856
|
-
ast.Attribute(
|
|
1857
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1858
|
-
attr=func_name,
|
|
1859
|
-
ctx=ast.Load(),
|
|
1860
|
-
),
|
|
1861
|
-
],
|
|
1862
|
-
keywords=[],
|
|
1863
|
-
)
|
|
1864
|
-
)
|
|
1865
|
-
init_slots_body.append(reg_call)
|
|
1866
|
-
|
|
1867
|
-
init_slots_func = ast.FunctionDef(
|
|
1868
|
-
name="_init_slots",
|
|
1869
|
-
args=ast.arguments(
|
|
1870
|
-
posonlyargs=[],
|
|
1871
|
-
args=[ast.arg(arg="self")],
|
|
1872
|
-
vararg=None,
|
|
1873
|
-
kwonlyargs=[],
|
|
1874
|
-
kw_defaults=[],
|
|
1875
|
-
defaults=[],
|
|
1876
|
-
),
|
|
1877
|
-
body=init_slots_body,
|
|
1878
|
-
decorator_list=[],
|
|
1879
|
-
returns=None,
|
|
1880
|
-
)
|
|
1881
|
-
binding_funcs.append(init_slots_func)
|
|
1882
|
-
|
|
1883
|
-
# Handle !provide - Override render() to update context before layout rendering
|
|
1884
|
-
provide_directive = cast(
|
|
1885
|
-
Optional[ProvideDirective],
|
|
1886
|
-
parsed.get_directive_by_type(ProvideDirective),
|
|
1887
|
-
)
|
|
1888
|
-
if provide_directive:
|
|
1889
|
-
provide_body: List[ast.stmt] = []
|
|
1890
|
-
for key, val_expr in provide_directive.mapping.items():
|
|
1891
|
-
val_ast = self.template_codegen._transform_expr(
|
|
1892
|
-
val_expr, set(), known_globals
|
|
1893
|
-
)
|
|
1894
|
-
provide_body.append(
|
|
1895
|
-
ast.Assign(
|
|
1896
|
-
targets=[
|
|
1897
|
-
ast.Subscript(
|
|
1898
|
-
value=ast.Attribute(
|
|
1899
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1900
|
-
attr="context",
|
|
1901
|
-
ctx=ast.Load(),
|
|
1902
|
-
),
|
|
1903
|
-
slice=ast.Constant(value=key),
|
|
1904
|
-
ctx=ast.Store(),
|
|
1905
|
-
)
|
|
1906
|
-
],
|
|
1907
|
-
value=val_ast,
|
|
1908
|
-
)
|
|
1909
|
-
)
|
|
1910
|
-
|
|
1911
|
-
# return await super().render(init)
|
|
1912
|
-
render_call = ast.Call(
|
|
1913
|
-
func=ast.Attribute(
|
|
1914
|
-
value=ast.Call(
|
|
1915
|
-
func=ast.Name(id="super", ctx=ast.Load()),
|
|
1916
|
-
args=[],
|
|
1917
|
-
keywords=[],
|
|
1918
|
-
),
|
|
1919
|
-
attr="render",
|
|
1920
|
-
ctx=ast.Load(),
|
|
1921
|
-
),
|
|
1922
|
-
args=[ast.Name(id="init", ctx=ast.Load())],
|
|
1923
|
-
keywords=[],
|
|
1924
|
-
)
|
|
1925
|
-
provide_body.append(ast.Return(value=ast.Await(value=render_call)))
|
|
1926
|
-
|
|
1927
|
-
render_override = ast.AsyncFunctionDef(
|
|
1928
|
-
name="render",
|
|
1929
|
-
args=ast.arguments(
|
|
1930
|
-
posonlyargs=[],
|
|
1931
|
-
args=[
|
|
1932
|
-
ast.arg(arg="self"),
|
|
1933
|
-
ast.arg(
|
|
1934
|
-
arg="init",
|
|
1935
|
-
annotation=ast.Name(id="bool", ctx=ast.Load()),
|
|
1936
|
-
),
|
|
1937
|
-
],
|
|
1938
|
-
vararg=None,
|
|
1939
|
-
kwonlyargs=[],
|
|
1940
|
-
kw_defaults=[],
|
|
1941
|
-
defaults=[ast.Constant(value=True)],
|
|
1942
|
-
),
|
|
1943
|
-
body=provide_body,
|
|
1944
|
-
decorator_list=[],
|
|
1945
|
-
returns=None,
|
|
1946
|
-
)
|
|
1947
|
-
binding_funcs.append(render_override)
|
|
1948
|
-
|
|
1949
|
-
else:
|
|
1950
|
-
# === Standard Mode ===
|
|
1951
|
-
# We no longer aggressively generate layout_id/scope_id for everything
|
|
1952
|
-
# to avoid breaking existing tests.
|
|
1953
|
-
layout_id = None
|
|
1954
|
-
scope_id = None
|
|
1955
|
-
|
|
1956
|
-
if parsed.file_path:
|
|
1957
|
-
import hashlib
|
|
1958
|
-
|
|
1959
|
-
layout_id_hash = hashlib.md5(str(parsed.file_path).encode()).hexdigest()
|
|
1960
|
-
# Use as layout_id if we have slots to fill for ourselves (as a component)
|
|
1961
|
-
# Or for scoping if <style scoped> is present
|
|
1962
|
-
has_scoped_style = any(
|
|
1963
|
-
n.tag == "style" and "scoped" in n.attributes
|
|
1964
|
-
for n in parsed.template
|
|
1965
|
-
)
|
|
1966
|
-
if has_scoped_style:
|
|
1967
|
-
scope_id = layout_id_hash[:8]
|
|
1968
|
-
|
|
1969
|
-
# If we are a layout (referenced by others), we should have a LAYOUT_ID.
|
|
1970
|
-
# But we don't know if we ARE a layout here.
|
|
1971
|
-
# We'll assume if there are <slot> tags, we might be a layout.
|
|
1972
|
-
has_slots = self._has_slots_recursive(parsed.template)
|
|
1973
|
-
if has_slots:
|
|
1974
|
-
layout_id = layout_id_hash
|
|
1975
|
-
|
|
1976
|
-
# Extract Props to Unpack
|
|
1977
|
-
|
|
1978
|
-
prop_names = set()
|
|
1979
|
-
props_unpack_stmts = []
|
|
1980
|
-
|
|
1981
|
-
# Using imported PropsDirective from earlier context or get it again
|
|
1982
|
-
# We are inside the method, 'parsed' is available.
|
|
1983
|
-
props_directive = cast(
|
|
1984
|
-
Optional[PropsDirective], parsed.get_directive_by_type(PropsDirective)
|
|
1985
|
-
)
|
|
1986
|
-
if props_directive:
|
|
1987
|
-
for name, _, _ in props_directive.args:
|
|
1988
|
-
prop_names.add(name)
|
|
1989
|
-
# prop = self.prop
|
|
1990
|
-
props_unpack_stmts.append(
|
|
1991
|
-
ast.Assign(
|
|
1992
|
-
targets=[ast.Name(id=name, ctx=ast.Store())],
|
|
1993
|
-
value=ast.Attribute(
|
|
1994
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
1995
|
-
attr=name,
|
|
1996
|
-
ctx=ast.Load(),
|
|
1997
|
-
),
|
|
1998
|
-
)
|
|
1999
|
-
)
|
|
2000
|
-
|
|
2001
|
-
render_func, aux_funcs = self.template_codegen.generate_render_method(
|
|
2002
|
-
parsed.template,
|
|
2003
|
-
layout_id=layout_id or "",
|
|
2004
|
-
known_methods=known_methods,
|
|
2005
|
-
known_globals=known_globals,
|
|
2006
|
-
async_methods=async_methods,
|
|
2007
|
-
component_map=component_map,
|
|
2008
|
-
scope_id=scope_id,
|
|
2009
|
-
initial_locals=prop_names,
|
|
2010
|
-
)
|
|
2011
|
-
|
|
2012
|
-
# Prepend unpack statements to render_func body
|
|
2013
|
-
if render_func and props_unpack_stmts:
|
|
2014
|
-
render_func.body[0:0] = props_unpack_stmts
|
|
2015
|
-
|
|
2016
|
-
# Handle !provide - Update context values at start of render to catch state changes
|
|
2017
|
-
provide_directive = cast(
|
|
2018
|
-
Optional[ProvideDirective],
|
|
2019
|
-
parsed.get_directive_by_type(ProvideDirective),
|
|
2020
|
-
)
|
|
2021
|
-
if provide_directive and render_func:
|
|
2022
|
-
provide_stmts = []
|
|
2023
|
-
for key, val_expr in provide_directive.mapping.items():
|
|
2024
|
-
# Transform expression using known globals for this page scope
|
|
2025
|
-
# Note: val_expr is string. We need to parse it or use transform helper.
|
|
2026
|
-
|
|
2027
|
-
val_ast = self.template_codegen._transform_expr(
|
|
2028
|
-
val_expr, set(), known_globals
|
|
2029
|
-
)
|
|
2030
|
-
|
|
2031
|
-
provide_stmts.append(
|
|
2032
|
-
ast.Assign(
|
|
2033
|
-
targets=[
|
|
2034
|
-
ast.Subscript(
|
|
2035
|
-
value=ast.Attribute(
|
|
2036
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2037
|
-
attr="context",
|
|
2038
|
-
ctx=ast.Load(),
|
|
2039
|
-
),
|
|
2040
|
-
slice=ast.Constant(value=key),
|
|
2041
|
-
ctx=ast.Store(),
|
|
2042
|
-
)
|
|
2043
|
-
],
|
|
2044
|
-
value=val_ast,
|
|
2045
|
-
)
|
|
2046
|
-
)
|
|
2047
|
-
|
|
2048
|
-
# Insert after props unpacking (if any)
|
|
2049
|
-
insert_idx = len(props_unpack_stmts) if props_unpack_stmts else 0
|
|
2050
|
-
render_func.body[insert_idx:insert_idx] = provide_stmts
|
|
2051
|
-
|
|
2052
|
-
binding_funcs.extend(aux_funcs)
|
|
2053
|
-
|
|
2054
|
-
# SPA injection
|
|
2055
|
-
# (path_directive search removed if unused)
|
|
2056
|
-
|
|
2057
|
-
# Determine injection point (before final return)
|
|
2058
|
-
|
|
2059
|
-
spa_check = ast.If(
|
|
2060
|
-
test=ast.BoolOp(
|
|
2061
|
-
op=ast.And(),
|
|
2062
|
-
values=[
|
|
2063
|
-
ast.UnaryOp(
|
|
2064
|
-
op=ast.Not(),
|
|
2065
|
-
operand=ast.Call(
|
|
2066
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2067
|
-
args=[
|
|
2068
|
-
ast.Name(id="self", ctx=ast.Load()),
|
|
2069
|
-
ast.Constant(value="__no_spa__"),
|
|
2070
|
-
ast.Constant(value=False),
|
|
2071
|
-
],
|
|
2072
|
-
keywords=[],
|
|
2073
|
-
),
|
|
2074
|
-
),
|
|
2075
|
-
ast.UnaryOp(
|
|
2076
|
-
op=ast.Not(),
|
|
2077
|
-
operand=ast.Call(
|
|
2078
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2079
|
-
args=[
|
|
2080
|
-
ast.Name(id="self", ctx=ast.Load()),
|
|
2081
|
-
ast.Constant(value="__is_component__"),
|
|
2082
|
-
ast.Constant(value=False),
|
|
2083
|
-
],
|
|
2084
|
-
keywords=[],
|
|
2085
|
-
),
|
|
2086
|
-
),
|
|
2087
|
-
ast.BoolOp(
|
|
2088
|
-
op=ast.Or(),
|
|
2089
|
-
values=[
|
|
2090
|
-
ast.Call(
|
|
2091
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2092
|
-
args=[
|
|
2093
|
-
ast.Name(id="self", ctx=ast.Load()),
|
|
2094
|
-
ast.Constant(value="__spa_enabled__"),
|
|
2095
|
-
ast.Constant(value=False),
|
|
2096
|
-
],
|
|
2097
|
-
keywords=[],
|
|
2098
|
-
),
|
|
2099
|
-
ast.Call(
|
|
2100
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2101
|
-
args=[
|
|
2102
|
-
ast.Attribute(
|
|
2103
|
-
value=ast.Attribute(
|
|
2104
|
-
value=ast.Attribute(
|
|
2105
|
-
value=ast.Name(
|
|
2106
|
-
id="self", ctx=ast.Load()
|
|
2107
|
-
),
|
|
2108
|
-
attr="request",
|
|
2109
|
-
ctx=ast.Load(),
|
|
2110
|
-
),
|
|
2111
|
-
attr="app",
|
|
2112
|
-
ctx=ast.Load(),
|
|
2113
|
-
),
|
|
2114
|
-
attr="state",
|
|
2115
|
-
ctx=ast.Load(),
|
|
2116
|
-
),
|
|
2117
|
-
ast.Constant(value="enable_pjax"),
|
|
2118
|
-
ast.Constant(value=False),
|
|
2119
|
-
],
|
|
2120
|
-
keywords=[],
|
|
2121
|
-
),
|
|
2122
|
-
],
|
|
2123
|
-
),
|
|
2124
|
-
],
|
|
2125
|
-
),
|
|
2126
|
-
body=[
|
|
2127
|
-
# sibling_paths = ...
|
|
2128
|
-
ast.Assign(
|
|
2129
|
-
targets=[ast.Name(id="sibling_paths", ctx=ast.Store())],
|
|
2130
|
-
value=ast.Call(
|
|
2131
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2132
|
-
args=[
|
|
2133
|
-
ast.Name(id="self", ctx=ast.Load()),
|
|
2134
|
-
ast.Constant(value="__sibling_paths__"),
|
|
2135
|
-
ast.List(elts=[], ctx=ast.Load()),
|
|
2136
|
-
],
|
|
2137
|
-
keywords=[],
|
|
2138
|
-
),
|
|
2139
|
-
),
|
|
2140
|
-
# pjax_enabled = ...
|
|
2141
|
-
ast.Assign(
|
|
2142
|
-
targets=[ast.Name(id="pjax_enabled", ctx=ast.Store())],
|
|
2143
|
-
value=ast.Call(
|
|
2144
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2145
|
-
args=[
|
|
2146
|
-
ast.Attribute(
|
|
2147
|
-
value=ast.Attribute(
|
|
2148
|
-
value=ast.Attribute(
|
|
2149
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2150
|
-
attr="request",
|
|
2151
|
-
ctx=ast.Load(),
|
|
2152
|
-
),
|
|
2153
|
-
attr="app",
|
|
2154
|
-
ctx=ast.Load(),
|
|
2155
|
-
),
|
|
2156
|
-
attr="state",
|
|
2157
|
-
ctx=ast.Load(),
|
|
2158
|
-
),
|
|
2159
|
-
ast.Constant(value="enable_pjax"),
|
|
2160
|
-
ast.Constant(value=False),
|
|
2161
|
-
],
|
|
2162
|
-
keywords=[],
|
|
2163
|
-
),
|
|
2164
|
-
),
|
|
2165
|
-
# debug = ...
|
|
2166
|
-
ast.Assign(
|
|
2167
|
-
targets=[ast.Name(id="debug", ctx=ast.Store())],
|
|
2168
|
-
value=ast.Call(
|
|
2169
|
-
func=ast.Name(id="getattr", ctx=ast.Load()),
|
|
2170
|
-
args=[
|
|
2171
|
-
ast.Attribute(
|
|
2172
|
-
value=ast.Attribute(
|
|
2173
|
-
value=ast.Attribute(
|
|
2174
|
-
value=ast.Name(id="self", ctx=ast.Load()),
|
|
2175
|
-
attr="request",
|
|
2176
|
-
ctx=ast.Load(),
|
|
2177
|
-
),
|
|
2178
|
-
attr="app",
|
|
2179
|
-
ctx=ast.Load(),
|
|
2180
|
-
),
|
|
2181
|
-
attr="state",
|
|
2182
|
-
ctx=ast.Load(),
|
|
2183
|
-
),
|
|
2184
|
-
ast.Constant(value="debug"),
|
|
2185
|
-
ast.Constant(value=False),
|
|
2186
|
-
],
|
|
2187
|
-
keywords=[],
|
|
2188
|
-
),
|
|
2189
|
-
),
|
|
2190
|
-
# parts.append(script tag)
|
|
2191
|
-
ast.Expr(
|
|
2192
|
-
value=ast.Call(
|
|
2193
|
-
func=ast.Attribute(
|
|
2194
|
-
value=ast.Name(id="parts", ctx=ast.Load()),
|
|
2195
|
-
attr="append",
|
|
2196
|
-
ctx=ast.Load(),
|
|
2197
|
-
),
|
|
2198
|
-
args=[
|
|
2199
|
-
ast.Constant(
|
|
2200
|
-
value='<script id="_pywire_spa_meta" type="application/json">'
|
|
2201
|
-
)
|
|
2202
|
-
],
|
|
2203
|
-
keywords=[],
|
|
2204
|
-
)
|
|
2205
|
-
),
|
|
2206
|
-
# parts.append(json.dumps(...))
|
|
2207
|
-
ast.Expr(
|
|
2208
|
-
value=ast.Call(
|
|
2209
|
-
func=ast.Attribute(
|
|
2210
|
-
value=ast.Name(id="parts", ctx=ast.Load()),
|
|
2211
|
-
attr="append",
|
|
2212
|
-
ctx=ast.Load(),
|
|
2213
|
-
),
|
|
2214
|
-
args=[
|
|
2215
|
-
ast.Call(
|
|
2216
|
-
func=ast.Attribute(
|
|
2217
|
-
value=ast.Name(id="json", ctx=ast.Load()),
|
|
2218
|
-
attr="dumps",
|
|
2219
|
-
ctx=ast.Load(),
|
|
2220
|
-
),
|
|
2221
|
-
args=[
|
|
2222
|
-
ast.Dict(
|
|
2223
|
-
keys=[
|
|
2224
|
-
ast.Constant(value="sibling_paths"),
|
|
2225
|
-
ast.Constant(value="enable_pjax"),
|
|
2226
|
-
ast.Constant(value="debug"),
|
|
2227
|
-
],
|
|
2228
|
-
values=[
|
|
2229
|
-
ast.Name(
|
|
2230
|
-
id="sibling_paths", ctx=ast.Load()
|
|
2231
|
-
),
|
|
2232
|
-
ast.Name(
|
|
2233
|
-
id="pjax_enabled", ctx=ast.Load()
|
|
2234
|
-
),
|
|
2235
|
-
ast.Name(id="debug", ctx=ast.Load()),
|
|
2236
|
-
],
|
|
2237
|
-
)
|
|
2238
|
-
],
|
|
2239
|
-
keywords=[],
|
|
2240
|
-
)
|
|
2241
|
-
],
|
|
2242
|
-
keywords=[],
|
|
2243
|
-
)
|
|
2244
|
-
),
|
|
2245
|
-
ast.Expr(
|
|
2246
|
-
value=ast.Call(
|
|
2247
|
-
func=ast.Attribute(
|
|
2248
|
-
value=ast.Name(id="parts", ctx=ast.Load()),
|
|
2249
|
-
attr="append",
|
|
2250
|
-
ctx=ast.Load(),
|
|
2251
|
-
),
|
|
2252
|
-
args=[ast.Constant(value="</script>")],
|
|
2253
|
-
keywords=[],
|
|
2254
|
-
)
|
|
2255
|
-
),
|
|
2256
|
-
ast.Expr(
|
|
2257
|
-
value=ast.Call(
|
|
2258
|
-
func=ast.Attribute(
|
|
2259
|
-
value=ast.Name(id="parts", ctx=ast.Load()),
|
|
2260
|
-
attr="append",
|
|
2261
|
-
ctx=ast.Load(),
|
|
2262
|
-
),
|
|
2263
|
-
args=[
|
|
2264
|
-
ast.JoinedStr(
|
|
2265
|
-
values=[
|
|
2266
|
-
ast.Constant(value='<script src="'),
|
|
2267
|
-
ast.FormattedValue(
|
|
2268
|
-
value=ast.Call(
|
|
2269
|
-
func=cast(
|
|
2270
|
-
ast.Expr,
|
|
2271
|
-
ast.parse(
|
|
2272
|
-
"self.request.app.state.pywire._get_client_script_url"
|
|
2273
|
-
).body[0],
|
|
2274
|
-
).value,
|
|
2275
|
-
args=[],
|
|
2276
|
-
keywords=[],
|
|
2277
|
-
),
|
|
2278
|
-
conversion=-1,
|
|
2279
|
-
format_spec=None,
|
|
2280
|
-
),
|
|
2281
|
-
ast.Constant(value='"></script>'),
|
|
2282
|
-
]
|
|
2283
|
-
)
|
|
2284
|
-
],
|
|
2285
|
-
keywords=[],
|
|
2286
|
-
)
|
|
2287
|
-
),
|
|
2288
|
-
],
|
|
2289
|
-
orelse=[],
|
|
2290
|
-
)
|
|
2291
|
-
|
|
2292
|
-
# Insert before last statement
|
|
2293
|
-
if render_func.body and isinstance(render_func.body[-1], ast.Return):
|
|
2294
|
-
render_func.body.insert(-1, spa_check)
|
|
2295
|
-
else:
|
|
2296
|
-
render_func.body.append(spa_check)
|
|
2297
|
-
|
|
2298
|
-
# Add no-op _init_slots
|
|
2299
|
-
binding_funcs.append(
|
|
2300
|
-
ast.FunctionDef(
|
|
2301
|
-
name="_init_slots",
|
|
2302
|
-
args=ast.arguments(
|
|
2303
|
-
posonlyargs=[],
|
|
2304
|
-
args=[ast.arg(arg="self")],
|
|
2305
|
-
vararg=None,
|
|
2306
|
-
kwonlyargs=[],
|
|
2307
|
-
kw_defaults=[],
|
|
2308
|
-
defaults=[],
|
|
2309
|
-
),
|
|
2310
|
-
body=[ast.Pass()],
|
|
2311
|
-
decorator_list=[],
|
|
2312
|
-
returns=None,
|
|
2313
|
-
)
|
|
2314
|
-
)
|
|
2315
|
-
|
|
2316
|
-
if self.template_codegen.region_renderers:
|
|
2317
|
-
region_keys: List[ast.expr | None] = []
|
|
2318
|
-
region_vals: List[ast.expr] = []
|
|
2319
|
-
for (
|
|
2320
|
-
region_id,
|
|
2321
|
-
method_name,
|
|
2322
|
-
) in self.template_codegen.region_renderers.items():
|
|
2323
|
-
region_keys.append(ast.Constant(value=region_id))
|
|
2324
|
-
region_vals.append(ast.Constant(value=method_name))
|
|
2325
|
-
binding_funcs.append(
|
|
2326
|
-
ast.Assign(
|
|
2327
|
-
targets=[ast.Name(id="__region_renderers__", ctx=ast.Store())],
|
|
2328
|
-
value=ast.Dict(keys=region_keys, values=region_vals),
|
|
2329
|
-
)
|
|
2330
|
-
)
|
|
2331
|
-
|
|
2332
|
-
return render_func, binding_funcs
|
|
2333
|
-
|
|
2334
|
-
def _has_slots_recursive(self, nodes: List[TemplateNode]) -> bool:
|
|
2335
|
-
"""Check recursively if the template contains any <slot> elements."""
|
|
2336
|
-
for node in nodes:
|
|
2337
|
-
if node.tag == "slot":
|
|
2338
|
-
return True
|
|
2339
|
-
if self._has_slots_recursive(node.children):
|
|
2340
|
-
return True
|
|
2341
|
-
return False
|