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,304 @@
|
|
|
1
|
+
"""AST node definitions for PyWire compiler."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ASTNode:
|
|
10
|
+
"""Base for all AST nodes."""
|
|
11
|
+
|
|
12
|
+
line: int
|
|
13
|
+
column: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Directive(ASTNode):
|
|
18
|
+
"""Base for directives."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PathDirective(Directive):
|
|
25
|
+
"""!path { 'name': '/route/{param}' } or !path '/route'"""
|
|
26
|
+
|
|
27
|
+
routes: Dict[str, str] # {'name': '/route/{param}'}
|
|
28
|
+
is_simple_string: bool = False
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return f"PathDirective(routes={self.routes}, simple={self.is_simple_string})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class NoSpaDirective(Directive):
|
|
36
|
+
"""!no_spa - disables client-side SPA navigation for this page."""
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return "NoSpaDirective()"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class LayoutDirective(Directive):
|
|
44
|
+
"""!layout "path/to/layout.pywire" """
|
|
45
|
+
|
|
46
|
+
layout_path: str
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
return f"LayoutDirective(path={self.layout_path})"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ComponentDirective(Directive):
|
|
54
|
+
"""!component 'path/to/component' as Name"""
|
|
55
|
+
|
|
56
|
+
path: str
|
|
57
|
+
component_name: str # PascalCase name (e.g. 'Badge')
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
return f"ComponentDirective(name={self.component_name}, path={self.path})"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class PropsDirective(Directive):
|
|
65
|
+
"""!props(name: type, arg=default)"""
|
|
66
|
+
|
|
67
|
+
# List of (name, type_hint_str, default_value_str_or_None)
|
|
68
|
+
args: List[Tuple[str, str, Optional[str]]]
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
return f"PropsDirective(args={self.args})"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class InjectDirective(Directive):
|
|
76
|
+
"""!inject { local: 'GLOBAL' }"""
|
|
77
|
+
|
|
78
|
+
mapping: Dict[str, str] # {local_var: global_key}
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
return f"InjectDirective(mapping={self.mapping})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ProvideDirective(Directive):
|
|
86
|
+
"""!provide { 'GLOBAL': local }"""
|
|
87
|
+
|
|
88
|
+
mapping: Dict[str, str] # {global_key: local_var_expr}
|
|
89
|
+
|
|
90
|
+
def __str__(self) -> str:
|
|
91
|
+
return f"ProvideDirective(mapping={self.mapping})"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class SpecialAttribute(ASTNode):
|
|
96
|
+
"""Base for special attributes ($, @, :)."""
|
|
97
|
+
|
|
98
|
+
name: str
|
|
99
|
+
value: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class KeyAttribute(SpecialAttribute):
|
|
104
|
+
"""$key={unique_id}."""
|
|
105
|
+
|
|
106
|
+
expr: str
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
return f"KeyAttribute(expr={self.expr})"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class IfAttribute(SpecialAttribute):
|
|
114
|
+
"""$if={condition}."""
|
|
115
|
+
|
|
116
|
+
condition: str
|
|
117
|
+
|
|
118
|
+
def __str__(self) -> str:
|
|
119
|
+
return f"IfAttribute(condition={self.condition})"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class ShowAttribute(SpecialAttribute):
|
|
124
|
+
"""$show={condition}."""
|
|
125
|
+
|
|
126
|
+
condition: str
|
|
127
|
+
|
|
128
|
+
def __str__(self) -> str:
|
|
129
|
+
return f"ShowAttribute(condition={self.condition})"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class ForAttribute(SpecialAttribute):
|
|
134
|
+
"""$for={item in items}"."""
|
|
135
|
+
|
|
136
|
+
is_template_tag: bool # <template $for>
|
|
137
|
+
loop_vars: str # "item" or "key, value"
|
|
138
|
+
iterable: str # "items" or "items.items()"
|
|
139
|
+
key: Optional[str] = None
|
|
140
|
+
|
|
141
|
+
def __str__(self) -> str:
|
|
142
|
+
return f"ForAttribute(vars={self.loop_vars}, in={self.iterable})"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class FieldValidationRules:
|
|
147
|
+
"""Validation rules for a single form field."""
|
|
148
|
+
|
|
149
|
+
name: str
|
|
150
|
+
required: bool = False
|
|
151
|
+
required_expr: Optional[str] = None # For required={condition}
|
|
152
|
+
pattern: Optional[str] = None
|
|
153
|
+
minlength: Optional[int] = None
|
|
154
|
+
maxlength: Optional[int] = None
|
|
155
|
+
min_value: Optional[str] = None # String to support dates
|
|
156
|
+
min_expr: Optional[str] = None # For min={expr}
|
|
157
|
+
max_value: Optional[str] = None
|
|
158
|
+
max_expr: Optional[str] = None # For max={expr}
|
|
159
|
+
step: Optional[str] = None
|
|
160
|
+
input_type: str = "text" # email, url, number, date, etc.
|
|
161
|
+
title: Optional[str] = None # Custom error message
|
|
162
|
+
max_size: Optional[int] = None # Max file size in bytes
|
|
163
|
+
allowed_types: Optional[List[str]] = None # Allowed MIME types or extensions
|
|
164
|
+
|
|
165
|
+
def __str__(self) -> str:
|
|
166
|
+
return f"FieldValidationRules(name={self.name}, required={self.required})"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class FormValidationSchema:
|
|
171
|
+
"""Schema containing all validation rules for a form."""
|
|
172
|
+
|
|
173
|
+
fields: Dict[str, FieldValidationRules] = field(default_factory=dict)
|
|
174
|
+
model_name: Optional[str] = None # For $model={ClassName}
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str:
|
|
177
|
+
return (
|
|
178
|
+
f"FormValidationSchema(fields={len(self.fields)}, model={self.model_name})"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class ModelAttribute(SpecialAttribute):
|
|
184
|
+
"""$model={ModelClassName} - Pydantic model binding."""
|
|
185
|
+
|
|
186
|
+
model_name: str
|
|
187
|
+
|
|
188
|
+
def __str__(self) -> str:
|
|
189
|
+
return f"ModelAttribute(model={self.model_name})"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class EventAttribute(SpecialAttribute):
|
|
194
|
+
"""@click={handler_name} or @click={handler(arg1)}."""
|
|
195
|
+
|
|
196
|
+
event_type: str # 'click', 'submit', etc.
|
|
197
|
+
handler_name: str
|
|
198
|
+
args: List[str] = field(
|
|
199
|
+
default_factory=list
|
|
200
|
+
) # List of python expressions for arguments
|
|
201
|
+
modifiers: List[str] = field(
|
|
202
|
+
default_factory=list
|
|
203
|
+
) # List of modifiers (e.g. ['prevent', 'stop'])
|
|
204
|
+
# Form-specific fields
|
|
205
|
+
validation_schema: Optional[FormValidationSchema] = None # Set for @submit handlers
|
|
206
|
+
|
|
207
|
+
def __str__(self) -> str:
|
|
208
|
+
return (
|
|
209
|
+
f"EventAttribute(event={self.event_type}, modifiers={self.modifiers}, "
|
|
210
|
+
f"handler={self.handler_name}, args={self.args})"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class ReactiveAttribute(SpecialAttribute):
|
|
216
|
+
"""
|
|
217
|
+
attr={expression}
|
|
218
|
+
Represents a reactive attribute where the value is a python expression.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
expr: str
|
|
222
|
+
|
|
223
|
+
def __str__(self) -> str:
|
|
224
|
+
return f"ReactiveAttribute(name={self.name}, expr={self.expr})"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class SpreadAttribute(SpecialAttribute):
|
|
229
|
+
"""
|
|
230
|
+
{**attrs} (preprocessed to __pywire_spread__="{**attrs}")
|
|
231
|
+
Represents a spread of attributes.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
expr: str
|
|
235
|
+
|
|
236
|
+
def __str__(self) -> str:
|
|
237
|
+
return f"SpreadAttribute(expr={self.expr})"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class InterpolationNode(ASTNode):
|
|
242
|
+
"""Represents {variable} in text.
|
|
243
|
+
|
|
244
|
+
Use {$html expr} syntax for raw/unescaped output.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
expression: str # Python expression to evaluate
|
|
248
|
+
is_raw: bool = False # If True, output is not HTML-escaped (use {$html expr})
|
|
249
|
+
|
|
250
|
+
def __str__(self) -> str:
|
|
251
|
+
raw_str = ", raw=True" if self.is_raw else ""
|
|
252
|
+
return f"InterpolationNode(expr={self.expression}{raw_str})"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class TemplateNode(ASTNode):
|
|
257
|
+
"""HTML element or text node."""
|
|
258
|
+
|
|
259
|
+
tag: Optional[str] # None for text nodes
|
|
260
|
+
attributes: Dict[str, str] = field(default_factory=dict) # Regular HTML attributes
|
|
261
|
+
special_attributes: List[Union[SpecialAttribute, "InterpolationNode"]] = field(
|
|
262
|
+
default_factory=list
|
|
263
|
+
)
|
|
264
|
+
children: List["TemplateNode"] = field(default_factory=list)
|
|
265
|
+
text_content: Optional[str] = None
|
|
266
|
+
is_raw: bool = False
|
|
267
|
+
|
|
268
|
+
def __str__(self) -> str:
|
|
269
|
+
if self.tag:
|
|
270
|
+
return (
|
|
271
|
+
f"TemplateNode(tag={self.tag}, attrs={len(self.attributes)}, "
|
|
272
|
+
f"special={len(self.special_attributes)}, "
|
|
273
|
+
f"children={len(self.children)})"
|
|
274
|
+
)
|
|
275
|
+
return f"TemplateNode(text={self.text_content[:30] if self.text_content else None})"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass
|
|
279
|
+
class ParsedPyWire:
|
|
280
|
+
"""Top-level parsed document."""
|
|
281
|
+
|
|
282
|
+
directives: List[Directive] = field(default_factory=list)
|
|
283
|
+
template: List[TemplateNode] = field(default_factory=list)
|
|
284
|
+
python_code: str = "" # Raw Python section (above ---html---)
|
|
285
|
+
python_ast: Optional[ast.Module] = None # Parsed Python AST
|
|
286
|
+
file_path: str = ""
|
|
287
|
+
|
|
288
|
+
def get_directive_by_type(self, directive_type: type) -> Optional[Directive]:
|
|
289
|
+
"""Get first directive of specified type."""
|
|
290
|
+
for directive in self.directives:
|
|
291
|
+
if isinstance(directive, directive_type):
|
|
292
|
+
return directive
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def get_directives_by_type(self, directive_type: type) -> List[Directive]:
|
|
296
|
+
"""Get all directives of specified type."""
|
|
297
|
+
return [d for d in self.directives if isinstance(d, directive_type)]
|
|
298
|
+
|
|
299
|
+
def __str__(self) -> str:
|
|
300
|
+
return (
|
|
301
|
+
f"ParsedPyWire(directives={len(self.directives)}, "
|
|
302
|
+
f"template_nodes={len(self.template)}, "
|
|
303
|
+
f"python_lines={len(self.python_code.splitlines())})"
|
|
304
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Base attribute parser."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pywire.compiler.ast_nodes import SpecialAttribute
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttributeParser(ABC):
|
|
10
|
+
"""Base class for parsing special attributes - extensible for $, @, : types."""
|
|
11
|
+
|
|
12
|
+
PREFIX: str # '@', '$', or ':'
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
16
|
+
"""Check if this parser can handle the attribute."""
|
|
17
|
+
return attr_name.startswith(self.PREFIX)
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def parse(
|
|
21
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
22
|
+
) -> Optional[SpecialAttribute]:
|
|
23
|
+
"""Parse attribute. Returns None if not applicable."""
|
|
24
|
+
pass
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Conditional attribute parsers ($if, $show)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pywire.compiler.ast_nodes import IfAttribute, ShowAttribute, SpecialAttribute
|
|
6
|
+
from pywire.compiler.attributes.base import AttributeParser
|
|
7
|
+
from pywire.compiler.exceptions import PyWireSyntaxError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConditionalAttributeParser(AttributeParser):
|
|
11
|
+
"""Parses $if and $show attributes."""
|
|
12
|
+
|
|
13
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
14
|
+
"""Check if attribute is $if or $show."""
|
|
15
|
+
return attr_name in ("$if", "$show")
|
|
16
|
+
|
|
17
|
+
def parse(
|
|
18
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
19
|
+
) -> Optional[SpecialAttribute]:
|
|
20
|
+
"""Parse conditional attribute."""
|
|
21
|
+
if not (attr_value.startswith("{") and attr_value.endswith("}")):
|
|
22
|
+
raise PyWireSyntaxError(
|
|
23
|
+
f"Value for '{attr_name}' must be wrapped in brackets: {attr_name}={{expr}}",
|
|
24
|
+
line=line,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
expr = attr_value[1:-1].strip()
|
|
28
|
+
|
|
29
|
+
if attr_name == "$if":
|
|
30
|
+
return IfAttribute(
|
|
31
|
+
name=attr_name, value=attr_value, condition=expr, line=line, column=col
|
|
32
|
+
)
|
|
33
|
+
elif attr_name == "$show":
|
|
34
|
+
return ShowAttribute(
|
|
35
|
+
name=attr_name, value=attr_value, condition=expr, line=line, column=col
|
|
36
|
+
)
|
|
37
|
+
return None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Event attribute parser."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from pywire.compiler.ast_nodes import EventAttribute
|
|
7
|
+
from pywire.compiler.attributes.base import AttributeParser
|
|
8
|
+
from pywire.compiler.exceptions import PyWireSyntaxError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventAttributeParser(AttributeParser):
|
|
12
|
+
"""Parses @event attributes (click, submit, etc.)."""
|
|
13
|
+
|
|
14
|
+
PREFIX = "@"
|
|
15
|
+
PATTERN = re.compile(r"^@(\w+)$")
|
|
16
|
+
|
|
17
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
18
|
+
"""Check if attribute starts with @."""
|
|
19
|
+
return attr_name.startswith(self.PREFIX)
|
|
20
|
+
|
|
21
|
+
def parse(
|
|
22
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
23
|
+
) -> Optional[EventAttribute]:
|
|
24
|
+
"""Parse @click.prevent.stop={handler_name} attribute."""
|
|
25
|
+
# Remove @ prefix
|
|
26
|
+
full_event = attr_name[1:]
|
|
27
|
+
parts = full_event.split(".")
|
|
28
|
+
event_type = parts[0]
|
|
29
|
+
modifiers = [m for m in parts[1:] if m]
|
|
30
|
+
|
|
31
|
+
if not (attr_value.startswith("{") and attr_value.endswith("}")):
|
|
32
|
+
raise PyWireSyntaxError(
|
|
33
|
+
f"Event handler for '{attr_name}' must be wrapped in brackets: "
|
|
34
|
+
f"{attr_name}={{expr}}",
|
|
35
|
+
line=line,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
handler_name = attr_value[1:-1].strip() # Strip brackets and whitespace
|
|
39
|
+
|
|
40
|
+
# Parse handler args if present (future: handler(arg1, arg2))
|
|
41
|
+
handler_args: List[str] = []
|
|
42
|
+
if "(" in handler_name:
|
|
43
|
+
# Future: parse args
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
return EventAttribute(
|
|
47
|
+
name=attr_name,
|
|
48
|
+
value=attr_value,
|
|
49
|
+
event_type=event_type,
|
|
50
|
+
handler_name=handler_name,
|
|
51
|
+
modifiers=modifiers,
|
|
52
|
+
args=handler_args,
|
|
53
|
+
line=line,
|
|
54
|
+
column=col,
|
|
55
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Form attribute parsers for $model and $field."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pywire.compiler.ast_nodes import ModelAttribute
|
|
6
|
+
from pywire.compiler.attributes.base import AttributeParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelAttributeParser(AttributeParser):
|
|
10
|
+
"""Parses $model={ModelClassName} attribute for Pydantic binding."""
|
|
11
|
+
|
|
12
|
+
PREFIX = "$model"
|
|
13
|
+
|
|
14
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
15
|
+
"""Check if attribute is $model."""
|
|
16
|
+
return attr_name == "$model"
|
|
17
|
+
|
|
18
|
+
def parse(
|
|
19
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
20
|
+
) -> Optional[ModelAttribute]:
|
|
21
|
+
"""Parse $model={ClassName} attribute."""
|
|
22
|
+
if attr_name != "$model":
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
model_name = attr_value.strip().strip("\"'")
|
|
26
|
+
if model_name.startswith("{") and model_name.endswith("}"):
|
|
27
|
+
model_name = model_name[1:-1].strip()
|
|
28
|
+
if not model_name:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
return ModelAttribute(
|
|
32
|
+
name=attr_name,
|
|
33
|
+
value=attr_value,
|
|
34
|
+
model_name=model_name,
|
|
35
|
+
line=line,
|
|
36
|
+
column=col,
|
|
37
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Loop attribute parsers ($for, $key)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pywire.compiler.ast_nodes import ForAttribute, KeyAttribute, SpecialAttribute
|
|
6
|
+
from pywire.compiler.attributes.base import AttributeParser
|
|
7
|
+
from pywire.compiler.exceptions import PyWireSyntaxError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoopAttributeParser(AttributeParser):
|
|
11
|
+
"""Parses $for attributes."""
|
|
12
|
+
|
|
13
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
14
|
+
"""Check if attribute is $for."""
|
|
15
|
+
return attr_name == "$for"
|
|
16
|
+
|
|
17
|
+
def parse(
|
|
18
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
19
|
+
) -> Optional[SpecialAttribute]:
|
|
20
|
+
"""Parse $for attribute."""
|
|
21
|
+
if not (attr_value.startswith("{") and attr_value.endswith("}")):
|
|
22
|
+
raise PyWireSyntaxError(
|
|
23
|
+
f"Value for '{attr_name}' must be wrapped in brackets: "
|
|
24
|
+
f"{attr_name}={{item in items}}",
|
|
25
|
+
line=line,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expr = attr_value[1:-1].strip()
|
|
29
|
+
# Parse "item in items" or "key, value in items"
|
|
30
|
+
parts = expr.split(" in ", 1)
|
|
31
|
+
if len(parts) != 2:
|
|
32
|
+
# We don't raise error here, just return nothing or let it be
|
|
33
|
+
# handled as valid attribute?
|
|
34
|
+
# Ideally validation happens here.
|
|
35
|
+
# But creating AST node blindly is safer if we want to defer errors.
|
|
36
|
+
# But "item in items" is pretty fundamental.
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Invalid $for syntax at line {line}: '{attr_value}'. Expected 'item in items'."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
loop_vars = parts[0].strip()
|
|
42
|
+
iterable = parts[1].strip()
|
|
43
|
+
|
|
44
|
+
return ForAttribute(
|
|
45
|
+
name=attr_name,
|
|
46
|
+
value=attr_value,
|
|
47
|
+
is_template_tag=False, # Populated later
|
|
48
|
+
loop_vars=loop_vars,
|
|
49
|
+
iterable=iterable,
|
|
50
|
+
line=line,
|
|
51
|
+
column=col,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class KeyAttributeParser(AttributeParser):
|
|
56
|
+
"""Parses $key attributes."""
|
|
57
|
+
|
|
58
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
59
|
+
"""Check if attribute is $key."""
|
|
60
|
+
return attr_name == "$key"
|
|
61
|
+
|
|
62
|
+
def parse(
|
|
63
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
64
|
+
) -> Optional[SpecialAttribute]:
|
|
65
|
+
"""Parse $key attribute."""
|
|
66
|
+
if not (attr_value.startswith("{") and attr_value.endswith("}")):
|
|
67
|
+
raise PyWireSyntaxError(
|
|
68
|
+
f"Value for '{attr_name}' must be wrapped in brackets: {attr_name}={{expr}}",
|
|
69
|
+
line=line,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expr = attr_value[1:-1].strip()
|
|
73
|
+
return KeyAttribute(
|
|
74
|
+
name=attr_name, value=attr_value, expr=expr, line=line, column=col
|
|
75
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Reactive attribute parser (:attr)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pywire.compiler.ast_nodes import ReactiveAttribute, SpecialAttribute
|
|
6
|
+
from pywire.compiler.attributes.base import AttributeParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReactiveAttributeParser(AttributeParser):
|
|
10
|
+
"""Parses reactive attributes starting with :"""
|
|
11
|
+
|
|
12
|
+
def can_parse(self, attr_name: str) -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Check if attribute starts with : but is NOT a directive like :class (if we had those)
|
|
15
|
+
or other special chars.
|
|
16
|
+
Actually requirements say: ":attribute"
|
|
17
|
+
"it explicitly does NOT support framework attributes like anything starting with @ or $"
|
|
18
|
+
But @ and $ are handled by other parsers anyway.
|
|
19
|
+
So we just check for starting with :
|
|
20
|
+
"""
|
|
21
|
+
# Disabled: :prop="val" syntax is deprecated.
|
|
22
|
+
# Reactive attributes must use prop={expr} syntax which is handled by parser fallback.
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def parse(
|
|
26
|
+
self, attr_name: str, attr_value: str, line: int, col: int
|
|
27
|
+
) -> Optional[SpecialAttribute]:
|
|
28
|
+
"""Parse attr={expr}"."""
|
|
29
|
+
# Strip the leading :
|
|
30
|
+
real_name = attr_name[1:]
|
|
31
|
+
|
|
32
|
+
return ReactiveAttribute(
|
|
33
|
+
name=real_name, value=attr_value, expr=attr_value, line=line, column=col
|
|
34
|
+
)
|
pywire/compiler/build.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Build system for production."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pywire.compiler.build_artifacts import BuildSummary
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_project(
|
|
13
|
+
optimize: bool = False,
|
|
14
|
+
pages_dir: Optional[Path] = None,
|
|
15
|
+
out_dir: Optional[Path] = None,
|
|
16
|
+
) -> BuildSummary:
|
|
17
|
+
"""Build project for production."""
|
|
18
|
+
if pages_dir is None:
|
|
19
|
+
pages_dir = Path("pages")
|
|
20
|
+
|
|
21
|
+
from pywire.cli.validate import validate_project
|
|
22
|
+
from pywire.compiler.build_artifacts import build_artifacts
|
|
23
|
+
|
|
24
|
+
errors = validate_project(pages_dir=pages_dir)
|
|
25
|
+
if errors:
|
|
26
|
+
raise ValueError(f"Build failed with {len(errors)} errors")
|
|
27
|
+
|
|
28
|
+
return build_artifacts(pages_dir=pages_dir, out_dir=out_dir, optimize=optimize)
|