pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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.
- pulse/__init__.py +10 -24
- pulse/app.py +3 -25
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +40 -8
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/queries/client.py +7 -7
- pulse/queries/effect.py +16 -0
- pulse/queries/infinite_query.py +138 -29
- pulse/queries/mutation.py +1 -15
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +610 -174
- pulse/queries/store.py +11 -14
- pulse/react_component.py +167 -14
- pulse/reactive.py +19 -1
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +185 -59
- pulse/renderer.py +80 -158
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/types/event_handler.py +3 -2
- pulse/vdom.py +212 -13
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
- pulse_framework-0.1.47.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.44.dist-info/RECORD +0 -79
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
pulse/types/event_handler.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from collections.abc import
|
|
1
|
+
from collections.abc import Callable
|
|
2
2
|
from typing import (
|
|
3
|
+
Any,
|
|
3
4
|
TypeVar,
|
|
4
5
|
)
|
|
5
6
|
|
|
6
|
-
EventHandlerResult =
|
|
7
|
+
EventHandlerResult = Any
|
|
7
8
|
|
|
8
9
|
T1 = TypeVar("T1", contravariant=True)
|
|
9
10
|
T2 = TypeVar("T2", contravariant=True)
|
pulse/vdom.py
CHANGED
|
@@ -6,6 +6,8 @@ the TypeScript UINode format exactly, eliminating the need for translation.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import functools
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
9
11
|
import warnings
|
|
10
12
|
from collections.abc import Callable, Iterable, Sequence
|
|
11
13
|
from inspect import Parameter, signature
|
|
@@ -13,18 +15,71 @@ from types import NoneType
|
|
|
13
15
|
from typing import (
|
|
14
16
|
Any,
|
|
15
17
|
Generic,
|
|
18
|
+
Literal,
|
|
16
19
|
NamedTuple,
|
|
17
20
|
NotRequired,
|
|
18
21
|
ParamSpec,
|
|
19
22
|
TypeAlias,
|
|
20
23
|
TypedDict,
|
|
24
|
+
final,
|
|
21
25
|
overload,
|
|
22
26
|
override,
|
|
23
27
|
)
|
|
24
28
|
|
|
29
|
+
from pulse.env import env
|
|
25
30
|
from pulse.hooks.core import HookContext
|
|
26
31
|
from pulse.hooks.init import rewrite_init_blocks
|
|
27
32
|
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# Validation helpers (dev mode only)
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _check_json_safe_float(value: float, context: str) -> None:
|
|
39
|
+
"""Raise ValueError if a float is NaN or Infinity."""
|
|
40
|
+
if math.isnan(value):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Cannot use nan in {context}. "
|
|
43
|
+
+ "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
|
|
44
|
+
+ "Replace with None or a sentinel value before passing to components."
|
|
45
|
+
)
|
|
46
|
+
if math.isinf(value):
|
|
47
|
+
kind = "inf" if value > 0 else "-inf"
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Cannot use {kind} in {context}. "
|
|
50
|
+
+ "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
|
|
51
|
+
+ "Replace with None or a sentinel value before passing to components."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_value(value: Any, context: str) -> None:
|
|
56
|
+
"""Recursively validate a value for JSON-unsafe floats (NaN, Infinity)."""
|
|
57
|
+
if isinstance(value, float):
|
|
58
|
+
_check_json_safe_float(value, context)
|
|
59
|
+
elif isinstance(value, dict):
|
|
60
|
+
for v in value.values():
|
|
61
|
+
_validate_value(v, context)
|
|
62
|
+
elif isinstance(value, (list, tuple)):
|
|
63
|
+
for item in value:
|
|
64
|
+
_validate_value(item, context)
|
|
65
|
+
# Skip other types - they'll be handled by the serializer
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_props(props: dict[str, Any] | None, parent_name: str) -> None:
|
|
69
|
+
"""Validate all props for JSON-unsafe values."""
|
|
70
|
+
if not props:
|
|
71
|
+
return
|
|
72
|
+
for key, value in props.items():
|
|
73
|
+
_validate_value(value, f"{parent_name} prop '{key}'")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _validate_children(children: "Sequence[Element]", parent_name: str) -> None:
|
|
77
|
+
"""Validate primitive children for JSON-unsafe values."""
|
|
78
|
+
for child in children:
|
|
79
|
+
if isinstance(child, float):
|
|
80
|
+
_check_json_safe_float(child, f"{parent_name} children")
|
|
81
|
+
|
|
82
|
+
|
|
28
83
|
# ============================================================================
|
|
29
84
|
# Core VDOM
|
|
30
85
|
# ============================================================================
|
|
@@ -42,11 +97,16 @@ class Callback(NamedTuple):
|
|
|
42
97
|
n_args: int
|
|
43
98
|
|
|
44
99
|
|
|
45
|
-
|
|
46
|
-
return None
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
@final
|
|
49
101
|
class Node:
|
|
102
|
+
__slots__ = (
|
|
103
|
+
"tag",
|
|
104
|
+
"props",
|
|
105
|
+
"children",
|
|
106
|
+
"allow_children",
|
|
107
|
+
"key",
|
|
108
|
+
)
|
|
109
|
+
|
|
50
110
|
tag: str
|
|
51
111
|
props: dict[str, Any] | None
|
|
52
112
|
children: "Sequence[Element] | None"
|
|
@@ -75,6 +135,12 @@ class Node:
|
|
|
75
135
|
raise ValueError("key must be a string or None")
|
|
76
136
|
if not self.allow_children and children:
|
|
77
137
|
raise ValueError(f"{self.tag} cannot have children")
|
|
138
|
+
# Dev-only validation for JSON-unsafe values
|
|
139
|
+
if env.pulse_env == "dev":
|
|
140
|
+
parent_name = f"<{self.tag}>"
|
|
141
|
+
_validate_props(self.props, parent_name)
|
|
142
|
+
if self.children:
|
|
143
|
+
_validate_children(self.children, parent_name)
|
|
78
144
|
|
|
79
145
|
# --- Pretty printing helpers -------------------------------------------------
|
|
80
146
|
@override
|
|
@@ -199,6 +265,15 @@ class Component(Generic[P]):
|
|
|
199
265
|
if key is not None and not isinstance(key, str):
|
|
200
266
|
raise ValueError("key must be a string or None")
|
|
201
267
|
|
|
268
|
+
# Flatten children if component takes children (has *children parameter)
|
|
269
|
+
if self._takes_children and args:
|
|
270
|
+
flattened = _flatten_children(
|
|
271
|
+
args, # pyright: ignore[reportArgumentType]
|
|
272
|
+
parent_name=f"<{self.name}>",
|
|
273
|
+
warn_stacklevel=4,
|
|
274
|
+
)
|
|
275
|
+
args = tuple(flattened) # pyright: ignore[reportAssignmentType]
|
|
276
|
+
|
|
202
277
|
return ComponentNode(
|
|
203
278
|
fn=self.fn,
|
|
204
279
|
key=key,
|
|
@@ -217,7 +292,19 @@ class Component(Generic[P]):
|
|
|
217
292
|
return self.name
|
|
218
293
|
|
|
219
294
|
|
|
295
|
+
@final
|
|
220
296
|
class ComponentNode:
|
|
297
|
+
__slots__ = (
|
|
298
|
+
"fn",
|
|
299
|
+
"args",
|
|
300
|
+
"kwargs",
|
|
301
|
+
"key",
|
|
302
|
+
"name",
|
|
303
|
+
"takes_children",
|
|
304
|
+
"hooks",
|
|
305
|
+
"contents",
|
|
306
|
+
)
|
|
307
|
+
|
|
221
308
|
fn: Callable[..., Any]
|
|
222
309
|
args: tuple[Any, ...]
|
|
223
310
|
kwargs: dict[str, Any]
|
|
@@ -225,6 +312,7 @@ class ComponentNode:
|
|
|
225
312
|
name: str
|
|
226
313
|
takes_children: bool
|
|
227
314
|
hooks: HookContext
|
|
315
|
+
contents: "Element | None"
|
|
228
316
|
|
|
229
317
|
def __init__(
|
|
230
318
|
self,
|
|
@@ -242,8 +330,19 @@ class ComponentNode:
|
|
|
242
330
|
self.name = name or _infer_component_name(fn)
|
|
243
331
|
self.takes_children = takes_children
|
|
244
332
|
# Used for rendering
|
|
245
|
-
self.contents
|
|
333
|
+
self.contents = None
|
|
246
334
|
self.hooks = HookContext()
|
|
335
|
+
# Dev-only validation for JSON-unsafe values
|
|
336
|
+
if env.pulse_env == "dev":
|
|
337
|
+
parent_name = f"<{self.name}>"
|
|
338
|
+
# Validate kwargs (props)
|
|
339
|
+
_validate_props(self.kwargs, parent_name)
|
|
340
|
+
# Validate args (children passed positionally)
|
|
341
|
+
for arg in self.args:
|
|
342
|
+
if isinstance(arg, float):
|
|
343
|
+
_check_json_safe_float(arg, f"{parent_name} children")
|
|
344
|
+
elif isinstance(arg, (dict, list, tuple)):
|
|
345
|
+
_validate_value(arg, f"{parent_name} children")
|
|
247
346
|
|
|
248
347
|
def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
|
|
249
348
|
if not self.takes_children:
|
|
@@ -259,7 +358,7 @@ class ComponentNode:
|
|
|
259
358
|
children_arg = (children_arg,)
|
|
260
359
|
# Flatten children for ComponentNode as well
|
|
261
360
|
flattened_children = _flatten_children(
|
|
262
|
-
children_arg, parent_name=f"<{self.name}>"
|
|
361
|
+
children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
|
|
263
362
|
)
|
|
264
363
|
result = ComponentNode(
|
|
265
364
|
fn=self.fn,
|
|
@@ -310,33 +409,133 @@ Callbacks = dict[str, Callback]
|
|
|
310
409
|
VDOM: TypeAlias = VDOMNode | Primitive
|
|
311
410
|
Props = dict[str, Any]
|
|
312
411
|
|
|
412
|
+
|
|
413
|
+
# ============================================================================
|
|
414
|
+
# VDOM Operations (updates sent from server to client)
|
|
415
|
+
# ============================================================================
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class ReplaceOperation(TypedDict):
|
|
419
|
+
type: Literal["replace"]
|
|
420
|
+
path: str
|
|
421
|
+
data: VDOM
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# This payload makes it easy for the client to rebuild an array of React nodes
|
|
425
|
+
# from the previous children array:
|
|
426
|
+
# - Allocate array of size N
|
|
427
|
+
# - For i in 0..N-1, check the following scenarios
|
|
428
|
+
# - i matches the next index in `new` -> use provided tree
|
|
429
|
+
# - i matches the next index in `reuse` -> reuse previous child
|
|
430
|
+
# - otherwise, reuse the element at the same index
|
|
431
|
+
class ReconciliationOperation(TypedDict):
|
|
432
|
+
type: Literal["reconciliation"]
|
|
433
|
+
path: str
|
|
434
|
+
N: int
|
|
435
|
+
new: tuple[list[int], list[VDOM]]
|
|
436
|
+
reuse: tuple[list[int], list[int]]
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class UpdatePropsDelta(TypedDict, total=False):
|
|
440
|
+
# Only send changed/new keys under `set` and removed keys under `remove`
|
|
441
|
+
set: Props
|
|
442
|
+
remove: list[str]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class UpdatePropsOperation(TypedDict):
|
|
446
|
+
type: Literal["update_props"]
|
|
447
|
+
path: str
|
|
448
|
+
data: UpdatePropsDelta
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class PathDelta(TypedDict, total=False):
|
|
452
|
+
add: list[str]
|
|
453
|
+
remove: list[str]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class UpdateCallbacksOperation(TypedDict):
|
|
457
|
+
type: Literal["update_callbacks"]
|
|
458
|
+
path: str
|
|
459
|
+
data: PathDelta
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class UpdateRenderPropsOperation(TypedDict):
|
|
463
|
+
type: Literal["update_render_props"]
|
|
464
|
+
path: str
|
|
465
|
+
data: PathDelta
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class UpdateJsExprPathsOperation(TypedDict):
|
|
469
|
+
type: Literal["update_jsexpr_paths"]
|
|
470
|
+
path: str
|
|
471
|
+
data: PathDelta
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
VDOMOperation: TypeAlias = (
|
|
475
|
+
ReplaceOperation
|
|
476
|
+
| UpdatePropsOperation
|
|
477
|
+
| ReconciliationOperation
|
|
478
|
+
| UpdateCallbacksOperation
|
|
479
|
+
| UpdateRenderPropsOperation
|
|
480
|
+
| UpdateJsExprPathsOperation
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
313
484
|
# ----------------------------------------------------------------------------
|
|
314
485
|
# Component naming heuristics
|
|
315
486
|
# ----------------------------------------------------------------------------
|
|
316
487
|
|
|
317
488
|
|
|
318
|
-
def
|
|
319
|
-
"""
|
|
489
|
+
def _clean_parent_name_for_warning(parent_name: str) -> str:
|
|
490
|
+
"""Strip $$ prefix and hexadecimal suffix from ReactComponent tags in warning messages.
|
|
491
|
+
|
|
492
|
+
ReactComponent tags are in the format <$$ComponentName_1a2b> or <$$ComponentName_1a2b.prop>.
|
|
493
|
+
This function strips the $$ prefix and _1a2b suffix to show just the component name.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
# Match ReactComponent tags: <$$ComponentName_hex> or <$$ComponentName_hex.prop>
|
|
497
|
+
# Strip the $$ prefix and _hex suffix but keep the rest (hex digits are 0-9, a-f)
|
|
498
|
+
return re.sub(r"\$\$([^_]+)_[0-9a-f]+", r"\1", parent_name)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _flatten_children(
|
|
502
|
+
children: Children, *, parent_name: str, warn_stacklevel: int = 5
|
|
503
|
+
) -> Sequence[Element]:
|
|
504
|
+
"""Flatten children and emit warnings for unkeyed iterables (dev mode only).
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
children: The children sequence to flatten.
|
|
508
|
+
parent_name: Name of the parent element for error messages.
|
|
509
|
+
warn_stacklevel: Stack level for warnings. Adjust based on call site:
|
|
510
|
+
- 5 for Node.__init__ via tag factory (user -> tag factory -> Node.__init__ -> _flatten_children -> visit -> warn)
|
|
511
|
+
- 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
|
|
512
|
+
"""
|
|
320
513
|
flat: list[Element] = []
|
|
321
514
|
return_tuple = isinstance(children, tuple)
|
|
515
|
+
is_dev = env.pulse_env == "dev"
|
|
322
516
|
|
|
323
517
|
def visit(item: Child) -> None:
|
|
324
518
|
if isinstance(item, Iterable) and not isinstance(item, str):
|
|
325
519
|
# If any Node/ComponentNode yielded by this iterable lacks a key,
|
|
326
|
-
# emit a single warning for this iterable.
|
|
520
|
+
# emit a single warning for this iterable (dev mode only).
|
|
327
521
|
missing_key = False
|
|
328
522
|
for sub in item:
|
|
329
|
-
if
|
|
523
|
+
if (
|
|
524
|
+
is_dev
|
|
525
|
+
and isinstance(sub, (Node, ComponentNode))
|
|
526
|
+
and sub.key is None
|
|
527
|
+
):
|
|
330
528
|
missing_key = True
|
|
331
529
|
visit(sub)
|
|
332
530
|
if missing_key:
|
|
333
|
-
# Warn once per iterable without keys on its elements
|
|
531
|
+
# Warn once per iterable without keys on its elements.
|
|
532
|
+
clean_name = _clean_parent_name_for_warning(parent_name)
|
|
334
533
|
warnings.warn(
|
|
335
534
|
(
|
|
336
|
-
f"[Pulse] Iterable children of {
|
|
535
|
+
f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
|
|
337
536
|
"Add a stable 'key' to each element inside iterables to improve reconciliation."
|
|
338
537
|
),
|
|
339
|
-
stacklevel=
|
|
538
|
+
stacklevel=warn_stacklevel,
|
|
340
539
|
)
|
|
341
540
|
else:
|
|
342
541
|
# Not an iterable child: must be a Element or primitive
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
pulse/__init__.py,sha256=F97Jf6i99NXZe05ICjGbBjIaT4JURdh7dC1T1-Gnb0o,32790
|
|
2
|
+
pulse/app.py,sha256=57o_FrJjKxMDkbxKTzeCUtBRXHfGrCbSpJUnP0Z8evg,31474
|
|
3
|
+
pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
|
|
4
|
+
pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
pulse/cli/cmd.py,sha256=UBT7OoqWRU-idLOKkA9TDN8m8ugi1gwRMiUJTUmkVfU,14853
|
|
6
|
+
pulse/cli/dependencies.py,sha256=ZBqBAfMvMBQUvh4THdPDztTMQ_dyR52S1IuotP_eEZs,5623
|
|
7
|
+
pulse/cli/folder_lock.py,sha256=kvUmZBg869lwCTIZFoge9dhorv8qPXHTWwVv_jQg1k8,3477
|
|
8
|
+
pulse/cli/helpers.py,sha256=8bRlV3d7w3w-jHaFvFYt9Pzue6_CbKOq_Z3jBsBOeUk,8820
|
|
9
|
+
pulse/cli/models.py,sha256=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
|
|
10
|
+
pulse/cli/packages.py,sha256=e7ycwwJfdmB4pzrai4DHos6-JzyUgmE4DCZp0BqjdeI,6792
|
|
11
|
+
pulse/cli/processes.py,sha256=C1xU72oUanj-1Mkc9WmqESTsVUn_aUHG8URiPyRHSFM,7016
|
|
12
|
+
pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
|
|
13
|
+
pulse/cli/uvicorn_log_config.py,sha256=f7ikDc5foXh3TmFMrnfnW8yev48ZAdlo8F4F_aMVoVk,2391
|
|
14
|
+
pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
pulse/codegen/codegen.py,sha256=hK2gh2hX8ARMdQiQ3ILj6NMOuNiCbcuTdgNrtt4ex-8,9525
|
|
16
|
+
pulse/codegen/js.py,sha256=yw2RKQhiSBZr_FL-3WpZoAhcYCvuPEZXTzP5p9i7mCY,1540
|
|
17
|
+
pulse/codegen/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
pulse/codegen/templates/layout.py,sha256=nmWPQcO9SRXc3mCCVLCmykreSF96TqQfdDY7dvUBxRg,4737
|
|
19
|
+
pulse/codegen/templates/route.py,sha256=ionwEoTq0aAIE3lmnFyHLAlFnpKOT0QZm_XhTL_ZDNQ,10447
|
|
20
|
+
pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_qKAI8zAE,1129
|
|
21
|
+
pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
|
|
22
|
+
pulse/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
|
|
24
|
+
pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
|
|
25
|
+
pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
|
|
26
|
+
pulse/context.py,sha256=fMK6GdQY4q_3452v5DJli2f2_urVihnpzb-O-O9cJ1Q,1734
|
|
27
|
+
pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
|
|
28
|
+
pulse/decorators.py,sha256=ywNgLN6VFcKOM5fbFdUUzh-DWk4BuSXdD1BTfd1N-0U,4827
|
|
29
|
+
pulse/env.py,sha256=p3XI8KG1ZCcXPD3LJP7fW8JPYfyvoYY5ENwae2o0PiA,2889
|
|
30
|
+
pulse/form.py,sha256=UHVyp9fIZaM-Bi9_he8FBT8A2tI7bnRCn1dJkOat0zw,9022
|
|
31
|
+
pulse/helpers.py,sha256=sAIA_X2p6-AEg1Y87NejCZvV82_FtNpePX1Z0sFsdh8,15005
|
|
32
|
+
pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
pulse/hooks/core.py,sha256=QfYRz2O8-drNSQx_xnv8mK8ksWcw3LNM1H2hoInT0Rk,7457
|
|
34
|
+
pulse/hooks/effects.py,sha256=pVq5OndlhFLHLpM9Pn9Bp5rEpnpmJEpbIp2UaHHyJFQ,2428
|
|
35
|
+
pulse/hooks/init.py,sha256=iTNmEcFgZCXsLImPONbSNwc5asT7NQRz04b1Jopgzxs,11960
|
|
36
|
+
pulse/hooks/runtime.py,sha256=k5LZ8hnlNBMKOiEkQcAvs8BKwYxV6gwea2WCfju5K7Y,5106
|
|
37
|
+
pulse/hooks/setup.py,sha256=c_uVi0S0HPioEvjdWUaSdAGT9M3Cxpw8J-llvtmDOGo,4496
|
|
38
|
+
pulse/hooks/stable.py,sha256=mLNS6WyA4tC-65gNybPOE0DLEz1YlxOCddD9odElArU,1772
|
|
39
|
+
pulse/hooks/states.py,sha256=fFqN3gf7v7rY6QmieKWN1hVCQRRnL-5H4TeG9LTnKSc,6778
|
|
40
|
+
pulse/html/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
+
pulse/html/elements.py,sha256=YHXkVpfMAC4-0o61fK-E0LGTOM3KMCtBfpHHAwLx7dw,23241
|
|
42
|
+
pulse/html/events.py,sha256=SiZxaQV340hc5YGoKWXC5uCmbLsuijuEgnQz1hmdqYg,14700
|
|
43
|
+
pulse/html/props.py,sha256=FMXGHcO6GhTYp2Uw_6LpjDH5b-fVR88to3YlLvysjAM,26731
|
|
44
|
+
pulse/html/svg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
+
pulse/html/tags.py,sha256=NEH1otY0mURAl8STVunWzBC3fnmiwGqxcPucBPmtVzU,7568
|
|
46
|
+
pulse/html/tags.pyi,sha256=y0zHmPDmZbkPUbO8YJ8Yaw47NicoorOU-xjCTWWB_NM,14094
|
|
47
|
+
pulse/js/__init__.py,sha256=nI_FQpUlLUyCTmpj5cIbpNXjN7Oqwmp5je13ixB9pWw,3104
|
|
48
|
+
pulse/js/__init__.pyi,sha256=Xrub3LEm4JjyNTIyehm-Bfyy2gmpJgvFsEoRfpmq7bg,2624
|
|
49
|
+
pulse/js/_types.py,sha256=zkXtH1B7qURL24VFBPbyH3VIO1H2ptPaU2iE9ZHLDkQ,7402
|
|
50
|
+
pulse/js/array.py,sha256=WC7Z_A_dsf0_uyRTjuWqkTftBWXnNDdHzZJ4pvCqjsE,7102
|
|
51
|
+
pulse/js/console.py,sha256=rAbKq9uMYmsAsQ4j2crYpIoFkrfGHPWdPyU62zHQyWc,1818
|
|
52
|
+
pulse/js/date.py,sha256=lqPpZMhOR5bLxqEe9vyCWBKv6PPKfN_Ikwc-WAmu_NQ,3392
|
|
53
|
+
pulse/js/document.py,sha256=nS8C6e5rMMH8dKvwfO6WkfQElqrBxK-EKhl5JUUCzSA,3015
|
|
54
|
+
pulse/js/error.py,sha256=8zQPumWEssBK_gIcFOQu_TZGPNwcH5N6Yd6C0COElxM,2763
|
|
55
|
+
pulse/js/json.py,sha256=mPvJ0meHNuHrjXOJ7PfC_g1kOldAk_w7xYKA2dhwSUs,1945
|
|
56
|
+
pulse/js/map.py,sha256=Q8EAbkrlg1P30nKrx0MoLsmMS25Y860-lnoPzyZ0yLQ,2056
|
|
57
|
+
pulse/js/math.py,sha256=EwVnFN4MfHlY-QFz4_35BIsLEjfOyisTqoOfYHnp-mM,1801
|
|
58
|
+
pulse/js/navigator.py,sha256=viUQGCO18R_UvWhrdWj1x1IzDd6ZZYcb_m0bI5n8fms,1539
|
|
59
|
+
pulse/js/number.py,sha256=d64aZ8VEJSDN8vMzLvL0LnotRhG0bLrMg1IoLf23na8,1321
|
|
60
|
+
pulse/js/object.py,sha256=l5OzfKS_kiUnrzRk4HAhjOOVUAJ8q-VWZv-4pADDtjc,4808
|
|
61
|
+
pulse/js/promise.py,sha256=OnENcz7k0V6oGRtIxp-4LbGdurqd4aFMrFfWibm26Y0,4438
|
|
62
|
+
pulse/js/regexp.py,sha256=vHI8xI2QOT-dDHI6Held4YdwvfGs4i0_3Ce6gMyB7L0,1123
|
|
63
|
+
pulse/js/set.py,sha256=trMVxWfAhV07GKC8wn5Gqm31rk1SQtrJrNzSdDyfqi0,2830
|
|
64
|
+
pulse/js/string.py,sha256=fBd_CKq5nhc300mRa3YgNw0jpTEgGyaXRmGBiJgeu5w,928
|
|
65
|
+
pulse/js/weakmap.py,sha256=Q7kgPQx6rFqYfhIDyRfhuC12JmlKmO2n-OGSpl3g9ZY,1473
|
|
66
|
+
pulse/js/weakset.py,sha256=FJoVR0WtaOaHL7AXzJOb29F_sqG1K2mWxvR0RJk3mS0,1333
|
|
67
|
+
pulse/js/window.py,sha256=ayx3lBl54hTVanlkiC2wCVGNh0IDJqzPO7OlO11YUtI,4081
|
|
68
|
+
pulse/messages.py,sha256=PDsb07QDKvkMitAMgLmOk2c4JDb58Cq9WWCwbQ8unvg,3979
|
|
69
|
+
pulse/middleware.py,sha256=9uyAhVUEGMSwqWC3WXqs7x5JMMNEcSTTu3g7DjsR8w8,9812
|
|
70
|
+
pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
|
|
71
|
+
pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
|
|
72
|
+
pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
+
pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
|
+
pulse/queries/client.py,sha256=GGckE0P3YCBO4Mj-08AO_I9eXVC4sIDSNw_xTLrBFuE,15224
|
|
75
|
+
pulse/queries/common.py,sha256=Cr_NV0dWz5DQ7Qg771jvUms1o2-EnTYqjZJe4tVeoVk,1160
|
|
76
|
+
pulse/queries/effect.py,sha256=7KvV_yK7OHTWhfQbZFGzg_pRhyI2mn25pKIF9AmSmcU,1471
|
|
77
|
+
pulse/queries/infinite_query.py,sha256=oUHWjP2OliB7h8VDJooGocefHm4m9TDy4WaJesSrsdI,40457
|
|
78
|
+
pulse/queries/mutation.py,sha256=px1fprFL-RxNfbRSoRtdsOLkEbjSsMrJxGHKBIPYQTM,4959
|
|
79
|
+
pulse/queries/protocol.py,sha256=R8n238Ex9DbYIAVKB83a8FAPtnCiPNhWar-F01K2fTo,3345
|
|
80
|
+
pulse/queries/query.py,sha256=G8eXCaT5wuvVcstlqWU8VBxuuUUS7K1R5Y-VtDpMIG0,35065
|
|
81
|
+
pulse/queries/store.py,sha256=Ct7a-h1-Cq07zEfe9vw-LM85Fm7jIJx7CLAIlsiznlU,3444
|
|
82
|
+
pulse/react_component.py,sha256=m2WJwrCvzaHDC_o4PRvZ3pD3nwy9QshxtVn10vMRDQg,30603
|
|
83
|
+
pulse/reactive.py,sha256=v8a9IttkabeWwYrrHAx33zqzW9WC4WlS4iXbIh2KQkU,24374
|
|
84
|
+
pulse/reactive_extensions.py,sha256=T1V3AasHtvJkmGO55miC9RVPxDFIj7qrooMsn89x5SI,32076
|
|
85
|
+
pulse/render_session.py,sha256=UAI65k28ysZqCp7j6BBdi7MkG2wVKJXj4hIgjK7hsaM,18120
|
|
86
|
+
pulse/renderer.py,sha256=kUwrI5v9XMerVCjBmnYZSyfkDu8B8cW8jFjVnM93TS4,15702
|
|
87
|
+
pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
|
|
88
|
+
pulse/routing.py,sha256=XZdq4gjfYeuz1wKtjPza6YA8ya76_cQ58b2l4dBDbr4,13243
|
|
89
|
+
pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
|
|
90
|
+
pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
|
|
91
|
+
pulse/transpiler/__init__.py,sha256=sgfHxLwZEPj3rBBMtzD4997qx7GTr5Wt22-296e-uC8,6492
|
|
92
|
+
pulse/transpiler/builtins.py,sha256=V_H3bpgU22Yb_GzM6YvOutZ65O36xHLzRANl7uHRqUI,22401
|
|
93
|
+
pulse/transpiler/constants.py,sha256=GBYfTGgzDCuy-U5wC6iRYezSPK5UpZuyXrEp_yO6yTM,3188
|
|
94
|
+
pulse/transpiler/context.py,sha256=e-Nh0AKsq9_wVOI8gL_gn-UAP6HzcYN14zWLfNNzjWw,714
|
|
95
|
+
pulse/transpiler/errors.py,sha256=JC6tTEmnHf6JdyW4GIvfXB0IBLe7p3FvCLh14PocH28,43
|
|
96
|
+
pulse/transpiler/function.py,sha256=rP-MZl15_mwaGwPToPcFVYHXqyVdMn91cxtF7MKFPHA,7526
|
|
97
|
+
pulse/transpiler/ids.py,sha256=d91B_LFaAALKXHjGPmL8tJmDqGDFz7-GquYmnV9IZ0o,327
|
|
98
|
+
pulse/transpiler/imports.py,sha256=C4lSi5cRQcoo559_urDqbHAdeTiSeU3eEh89o2YDWhI,10848
|
|
99
|
+
pulse/transpiler/js_module.py,sha256=AjguCbKV_FnoanzojrW5-vlW-DQCTtyd-4FfBYQLxxQ,9603
|
|
100
|
+
pulse/transpiler/modules/__init__.py,sha256=YPM2WhILHXQFDSxDcbs-hHdhFh6i3N5ZJLEVGDZeR3Y,1065
|
|
101
|
+
pulse/transpiler/modules/asyncio.py,sha256=FetybIKGJhVJ3uEQJBw6Z2fsh6g7vA-9tqec4nn5FtI,1409
|
|
102
|
+
pulse/transpiler/modules/json.py,sha256=rlOPiJTpUiB2BGEpLqlP9gqO6jegpBd6vigE_h0iIfw,554
|
|
103
|
+
pulse/transpiler/modules/math.py,sha256=9_h6GGJA1SMIBKIB1AwoHErT-CqgSpyEWWneIcxRofE,9415
|
|
104
|
+
pulse/transpiler/modules/re.py,sha256=lAVy-0ej6PEbxNJ4Z0f9N6f9Fol3IsxPKoqBxh4Il-A,13115
|
|
105
|
+
pulse/transpiler/modules/tags.py,sha256=O3K9bLppCzYNlkXjeTvoQN7OI_HfW3xGy55CBT2Qn84,9079
|
|
106
|
+
pulse/transpiler/modules/typing.py,sha256=Vtiv33JzCe7_2K6IBpGdugI4f0sHJUq5I9qI_-sW80M,1639
|
|
107
|
+
pulse/transpiler/nodes.py,sha256=QAzIiu6R9nmBj-sYhiZf4-ffzdX8n_8Z7GswB7qXe5Y,33079
|
|
108
|
+
pulse/transpiler/py_module.py,sha256=bdSL_tUjOeYorWkOVPW4bx5tuFxYbWcGvuYQ5r8WnWk,3859
|
|
109
|
+
pulse/transpiler/transpiler.py,sha256=xwcJc5px1EShgXpCsqJyIynez2GdKynWy6zn2WaRZJA,29292
|
|
110
|
+
pulse/transpiler/utils.py,sha256=W5PAOWvmYdJCEw1eY7QEJRQFmNLVjFTdlCyWzmnTrCc,94
|
|
111
|
+
pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
|
+
pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
|
|
113
|
+
pulse/user_session.py,sha256=FITxLSEl3JU-jod6UWuUYC6EpnPG2rbaLCnIOdkQPtg,7803
|
|
114
|
+
pulse/vdom.py,sha256=yHMOYiXcaP3IVnYOAZrd_7cXWULKQ2fRQAiPDPSsOyU,18494
|
|
115
|
+
pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
|
|
116
|
+
pulse_framework-0.1.47.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
117
|
+
pulse_framework-0.1.47.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
|
|
118
|
+
pulse_framework-0.1.47.dist-info/METADATA,sha256=AMFl4QOZ07Oxunrd6GrbkKyUiFW6C04HOQuixmt_hcE,580
|
|
119
|
+
pulse_framework-0.1.47.dist-info/RECORD,,
|
pulse/codegen/imports.py
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
from collections.abc import Iterable
|
|
2
|
-
from dataclasses import dataclass, field
|
|
3
|
-
from operator import concat
|
|
4
|
-
|
|
5
|
-
from pulse.codegen.utils import NameRegistry
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Imported:
|
|
9
|
-
name: str
|
|
10
|
-
src: str
|
|
11
|
-
is_default: bool
|
|
12
|
-
prop: str | None
|
|
13
|
-
alias: str | None
|
|
14
|
-
|
|
15
|
-
def __init__(
|
|
16
|
-
self,
|
|
17
|
-
name: str,
|
|
18
|
-
src: str,
|
|
19
|
-
is_default: bool = False,
|
|
20
|
-
prop: str | None = None,
|
|
21
|
-
alias: str | None = None,
|
|
22
|
-
) -> None:
|
|
23
|
-
self.name = name
|
|
24
|
-
self.src = src
|
|
25
|
-
self.is_default = is_default
|
|
26
|
-
self.prop = prop
|
|
27
|
-
self.alias = alias
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def expr(self):
|
|
31
|
-
if self.prop:
|
|
32
|
-
return f"{self.alias or self.name}.{self.prop}"
|
|
33
|
-
return self.alias or self.name
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class ImportMember:
|
|
38
|
-
name: str
|
|
39
|
-
alias: str | None = None
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def identifier(self):
|
|
43
|
-
return self.alias or self.name
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass
|
|
47
|
-
class ImportStatement:
|
|
48
|
-
src: str
|
|
49
|
-
values: list[ImportMember] = field(default_factory=list)
|
|
50
|
-
types: list[ImportMember] = field(default_factory=list)
|
|
51
|
-
default_import: str | None = None
|
|
52
|
-
# When True, emit a side-effect import: `import "<src>";`
|
|
53
|
-
# Can be combined with named/default imports; side-effect line is emitted
|
|
54
|
-
# only when there are no named/default/type imports for the source.
|
|
55
|
-
side_effect: bool = False
|
|
56
|
-
# Optional ordering constraint: ensure this statement is emitted before
|
|
57
|
-
# any import statements whose `src` matches one of these values.
|
|
58
|
-
# Example: ImportStatement(src="@mantine/core/styles.css", side_effect=True,
|
|
59
|
-
# before=["@mantine/dates/styles.css"]) ensures
|
|
60
|
-
# core styles are imported before dates styles.
|
|
61
|
-
before: list[str] = field(default_factory=list)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class Imports:
|
|
65
|
-
names: NameRegistry
|
|
66
|
-
|
|
67
|
-
def __init__(
|
|
68
|
-
self,
|
|
69
|
-
imports: Iterable[ImportStatement | Imported],
|
|
70
|
-
names: NameRegistry | None = None,
|
|
71
|
-
) -> None:
|
|
72
|
-
self.names = names or NameRegistry()
|
|
73
|
-
# Map (src, name) -> identifier (either name or alias)
|
|
74
|
-
self._import_map: dict[tuple[str, str], str] = {}
|
|
75
|
-
self.sources: dict[str, ImportStatement] = {}
|
|
76
|
-
for stmt in imports:
|
|
77
|
-
if not isinstance(stmt, ImportStatement):
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if stmt.default_import:
|
|
81
|
-
stmt.default_import = self.names.register(stmt.default_import)
|
|
82
|
-
|
|
83
|
-
for imp in concat(stmt.values, stmt.types):
|
|
84
|
-
name = self.names.register(imp.name)
|
|
85
|
-
if name != imp.name:
|
|
86
|
-
imp.alias = name
|
|
87
|
-
self._import_map[(stmt.src, imp.name)] = name
|
|
88
|
-
|
|
89
|
-
self.sources[stmt.src] = stmt
|
|
90
|
-
|
|
91
|
-
def import_(
|
|
92
|
-
self, src: str, name: str, is_type: bool = False, is_default: bool = False
|
|
93
|
-
) -> str:
|
|
94
|
-
stmt = self.sources.get(src)
|
|
95
|
-
if not stmt:
|
|
96
|
-
stmt = ImportStatement(src)
|
|
97
|
-
self.sources[src] = stmt
|
|
98
|
-
|
|
99
|
-
if is_default:
|
|
100
|
-
if stmt.default_import:
|
|
101
|
-
return stmt.default_import
|
|
102
|
-
stmt.default_import = self.names.register(name)
|
|
103
|
-
return stmt.default_import
|
|
104
|
-
|
|
105
|
-
else:
|
|
106
|
-
if (src, name) in self._import_map:
|
|
107
|
-
return self._import_map[(src, name)]
|
|
108
|
-
|
|
109
|
-
unique_name = self.names.register(name)
|
|
110
|
-
alias = unique_name if unique_name != name else None
|
|
111
|
-
imp = ImportMember(name, alias)
|
|
112
|
-
if is_type:
|
|
113
|
-
stmt.types.append(imp)
|
|
114
|
-
else:
|
|
115
|
-
stmt.values.append(imp)
|
|
116
|
-
# Remember mapping so future imports of the same (src, name) reuse identifier
|
|
117
|
-
self._import_map[(src, name)] = imp.identifier
|
|
118
|
-
return imp.identifier
|
|
119
|
-
|
|
120
|
-
def add_statement(self, stmt: ImportStatement) -> None:
|
|
121
|
-
"""Merge an ImportStatement into the current Imports registry.
|
|
122
|
-
|
|
123
|
-
Ensures consistent aliasing via NameRegistry and de-duplicates
|
|
124
|
-
previously imported names from the same source.
|
|
125
|
-
"""
|
|
126
|
-
existing = self.sources.get(stmt.src)
|
|
127
|
-
if not existing:
|
|
128
|
-
# Normalize names through registry to avoid later conflicts
|
|
129
|
-
if stmt.default_import:
|
|
130
|
-
stmt.default_import = self.names.register(stmt.default_import)
|
|
131
|
-
for imp in concat(stmt.values, stmt.types):
|
|
132
|
-
name = self.names.register(imp.name)
|
|
133
|
-
if name != imp.name:
|
|
134
|
-
imp.alias = name
|
|
135
|
-
self._import_map[(stmt.src, imp.name)] = name
|
|
136
|
-
self.sources[stmt.src] = stmt
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
# Merge into existing statement for the same src
|
|
140
|
-
if stmt.default_import and not existing.default_import:
|
|
141
|
-
existing.default_import = self.names.register(stmt.default_import)
|
|
142
|
-
|
|
143
|
-
# Merge named imports
|
|
144
|
-
def _merge_list(
|
|
145
|
-
dst: list[ImportMember], src_list: list[ImportMember], is_type: bool = False
|
|
146
|
-
):
|
|
147
|
-
for imp in src_list:
|
|
148
|
-
key = (stmt.src, imp.name)
|
|
149
|
-
if key in self._import_map:
|
|
150
|
-
continue
|
|
151
|
-
unique = self.names.register(imp.name)
|
|
152
|
-
if unique != imp.name:
|
|
153
|
-
imp.alias = unique
|
|
154
|
-
self._import_map[key] = imp.alias or imp.name
|
|
155
|
-
dst.append(imp)
|
|
156
|
-
|
|
157
|
-
_merge_list(existing.values, stmt.values, is_type=False)
|
|
158
|
-
_merge_list(existing.types, stmt.types, is_type=True)
|
|
159
|
-
existing.side_effect = existing.side_effect or stmt.side_effect
|
|
160
|
-
# Merge ordering constraints
|
|
161
|
-
if stmt.before:
|
|
162
|
-
# Preserve order, avoid duplicates
|
|
163
|
-
seen = set(existing.before)
|
|
164
|
-
for s in stmt.before:
|
|
165
|
-
if s not in seen:
|
|
166
|
-
existing.before.append(s)
|
|
167
|
-
seen.add(s)
|
|
168
|
-
|
|
169
|
-
def ordered_sources(self) -> list[ImportStatement]:
|
|
170
|
-
"""Return sources ordered to satisfy `before` constraints.
|
|
171
|
-
|
|
172
|
-
Uses a stable topological sort (Kahn's algorithm) where insertion order
|
|
173
|
-
is preserved among nodes with equal dependency rank. Falls back to
|
|
174
|
-
insertion order if cycles are detected.
|
|
175
|
-
"""
|
|
176
|
-
# Build graph: edge u->v means u must come before v
|
|
177
|
-
keys = list(self.sources.keys())
|
|
178
|
-
index = {k: i for i, k in enumerate(keys)} # for stability
|
|
179
|
-
indegree: dict[str, int] = {k: 0 for k in keys}
|
|
180
|
-
adj: dict[str, list[str]] = {k: [] for k in keys}
|
|
181
|
-
for u, stmt in self.sources.items():
|
|
182
|
-
for v in stmt.before:
|
|
183
|
-
if v in adj: # only consider edges to imports present
|
|
184
|
-
adj[u].append(v)
|
|
185
|
-
indegree[v] += 1
|
|
186
|
-
|
|
187
|
-
# Kahn's algorithm
|
|
188
|
-
queue = [k for k, d in indegree.items() if d == 0]
|
|
189
|
-
# Stable ordering of initial nodes
|
|
190
|
-
queue.sort(key=lambda k: index[k])
|
|
191
|
-
ordered: list[str] = []
|
|
192
|
-
while queue:
|
|
193
|
-
u = queue.pop(0)
|
|
194
|
-
ordered.append(u)
|
|
195
|
-
for v in adj[u]:
|
|
196
|
-
indegree[v] -= 1
|
|
197
|
-
if indegree[v] == 0:
|
|
198
|
-
queue.append(v)
|
|
199
|
-
queue.sort(key=lambda k: index[k])
|
|
200
|
-
|
|
201
|
-
# If not all nodes processed, cycle detected; fall back to insertion order
|
|
202
|
-
if len(ordered) != len(keys):
|
|
203
|
-
ordered = keys
|
|
204
|
-
return [self.sources[k] for k in ordered]
|