pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 +9 -23
- pulse/app.py +6 -25
- pulse/cli/processes.py +1 -0
- 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 +51 -27
- 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/proxy.py +21 -8
- pulse/react_component.py +167 -14
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +144 -34
- pulse/renderer.py +80 -115
- 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/vdom.py +112 -6
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
- pulse_framework-0.1.48.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.46.dist-info/RECORD +0 -80
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, TypeAlias, override
|
|
4
|
+
|
|
5
|
+
from pulse.transpiler.context import is_interpreted_mode
|
|
6
|
+
from pulse.transpiler.errors import JSCompilationError
|
|
7
|
+
from pulse.transpiler.ids import generate_id
|
|
8
|
+
from pulse.transpiler.nodes import (
|
|
9
|
+
JSArray,
|
|
10
|
+
JSBoolean,
|
|
11
|
+
JSExpr,
|
|
12
|
+
JSIdentifier,
|
|
13
|
+
JSNew,
|
|
14
|
+
JSNumber,
|
|
15
|
+
JSString,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
JsPrimitive: TypeAlias = bool | int | float | str | None
|
|
19
|
+
JsValue: TypeAlias = "JsPrimitive | list[JsValue] | tuple[JsValue, ...] | set[JsValue] | frozenset[JsValue] | dict[str, JsValue]"
|
|
20
|
+
JsVar: TypeAlias = "JsValue | JSExpr"
|
|
21
|
+
|
|
22
|
+
# Global cache for deduplication across all transpiled functions
|
|
23
|
+
CONSTANTS_CACHE: dict[int, "JsConstant"] = {} # id(value) -> JsConstant
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JsConstant(JSExpr):
|
|
27
|
+
"""Wrapper for constant values used in transpiled JS functions."""
|
|
28
|
+
|
|
29
|
+
is_primary: ClassVar[bool] = True
|
|
30
|
+
|
|
31
|
+
value: object
|
|
32
|
+
expr: JSExpr
|
|
33
|
+
id: str
|
|
34
|
+
name: str # Original Python variable name (set by codegen)
|
|
35
|
+
|
|
36
|
+
def __init__(self, value: object, expr: JSExpr, name: str = "") -> None:
|
|
37
|
+
self.value = value
|
|
38
|
+
self.expr = expr
|
|
39
|
+
self.id = generate_id()
|
|
40
|
+
self.name = name
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def js_name(self) -> str:
|
|
44
|
+
"""Unique JS identifier for this constant."""
|
|
45
|
+
return f"{self.name}_{self.id}" if self.name else f"_const_{self.id}"
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def emit(self) -> str:
|
|
49
|
+
"""Emit JS code for this constant.
|
|
50
|
+
|
|
51
|
+
In normal mode: returns the unique JS name (e.g., "CONSTANT_1")
|
|
52
|
+
In interpreted mode: returns a get_object call (e.g., "get_object('CONSTANT_1')")
|
|
53
|
+
"""
|
|
54
|
+
base = self.js_name
|
|
55
|
+
if is_interpreted_mode():
|
|
56
|
+
return f"get_object('{base}')"
|
|
57
|
+
return base
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _value_to_expr(value: JsValue) -> JSExpr:
|
|
61
|
+
"""Convert a Python value to a JSExpr (no caching)."""
|
|
62
|
+
if value is None:
|
|
63
|
+
return JSIdentifier("undefined")
|
|
64
|
+
elif isinstance(value, bool):
|
|
65
|
+
return JSBoolean(value)
|
|
66
|
+
elif isinstance(value, (int, float)):
|
|
67
|
+
return JSNumber(value)
|
|
68
|
+
elif isinstance(value, str):
|
|
69
|
+
return JSString(value)
|
|
70
|
+
elif isinstance(value, (list, tuple)):
|
|
71
|
+
return JSArray([_value_to_expr(v) for v in value])
|
|
72
|
+
elif isinstance(value, (set, frozenset)):
|
|
73
|
+
return JSNew(
|
|
74
|
+
JSIdentifier("Set"),
|
|
75
|
+
[JSArray([_value_to_expr(v) for v in value])],
|
|
76
|
+
)
|
|
77
|
+
elif isinstance(value, dict):
|
|
78
|
+
entries: list[JSExpr] = []
|
|
79
|
+
for k, v in value.items():
|
|
80
|
+
if not isinstance(k, str):
|
|
81
|
+
raise JSCompilationError("Only string keys supported in constant dicts")
|
|
82
|
+
entries.append(JSArray([JSString(k), _value_to_expr(v)]))
|
|
83
|
+
return JSNew(JSIdentifier("Map"), [JSArray(entries)])
|
|
84
|
+
else:
|
|
85
|
+
raise JSCompilationError(
|
|
86
|
+
f"Unsupported global constant: {type(value).__name__} (value: {value!r})"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def const_to_js(value: JsValue, name: str = "") -> JsConstant:
|
|
91
|
+
"""Convert a Python value to a JsConstant (cached by identity)."""
|
|
92
|
+
value_id = id(value)
|
|
93
|
+
if value_id in CONSTANTS_CACHE:
|
|
94
|
+
return CONSTANTS_CACHE[value_id]
|
|
95
|
+
|
|
96
|
+
expr = _value_to_expr(value)
|
|
97
|
+
result = JsConstant(value, expr, name)
|
|
98
|
+
CONSTANTS_CACHE[value_id] = result
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def jsify(value: JsVar) -> JSExpr:
|
|
103
|
+
if not isinstance(value, JSExpr):
|
|
104
|
+
return const_to_js(value).expr
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def registered_constants() -> list[JsConstant]:
|
|
109
|
+
"""Get all registered JS constants."""
|
|
110
|
+
return list(CONSTANTS_CACHE.values())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Context for JSExpr emit mode."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
|
|
7
|
+
# When True, JSExpr.emit() returns code suitable for client-side interpretation
|
|
8
|
+
# (e.g., "get_object('Button_1')" instead of "Button_1")
|
|
9
|
+
_interpreted_mode: ContextVar[bool] = ContextVar(
|
|
10
|
+
"jsexpr_interpreted_mode", default=False
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_interpreted_mode() -> bool:
|
|
15
|
+
"""Check if we're in interpreted mode."""
|
|
16
|
+
return _interpreted_mode.get()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def interpreted_mode() -> Iterator[None]:
|
|
21
|
+
"""Context manager to enable interpreted mode for JSExpr.emit()."""
|
|
22
|
+
token = _interpreted_mode.set(True)
|
|
23
|
+
try:
|
|
24
|
+
yield
|
|
25
|
+
finally:
|
|
26
|
+
_interpreted_mode.reset(token)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import inspect
|
|
5
|
+
import textwrap
|
|
6
|
+
import types as pytypes
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Callable,
|
|
10
|
+
ClassVar,
|
|
11
|
+
Generic,
|
|
12
|
+
TypeAlias,
|
|
13
|
+
TypeVar,
|
|
14
|
+
TypeVarTuple,
|
|
15
|
+
override,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Import module registrations to ensure they're available for dependency analysis
|
|
19
|
+
import pulse.transpiler.modules # noqa: F401
|
|
20
|
+
from pulse.helpers import getsourcecode
|
|
21
|
+
from pulse.transpiler.builtins import BUILTINS
|
|
22
|
+
from pulse.transpiler.constants import JsConstant, const_to_js
|
|
23
|
+
from pulse.transpiler.context import is_interpreted_mode
|
|
24
|
+
from pulse.transpiler.errors import JSCompilationError
|
|
25
|
+
from pulse.transpiler.ids import generate_id
|
|
26
|
+
from pulse.transpiler.imports import Import
|
|
27
|
+
from pulse.transpiler.js_module import JS_MODULES
|
|
28
|
+
from pulse.transpiler.nodes import JSEXPR_REGISTRY, JSExpr, JSTransformer
|
|
29
|
+
from pulse.transpiler.py_module import (
|
|
30
|
+
PY_MODULES,
|
|
31
|
+
PyModuleExpr,
|
|
32
|
+
)
|
|
33
|
+
from pulse.transpiler.transpiler import JsTranspiler
|
|
34
|
+
|
|
35
|
+
Args = TypeVarTuple("Args")
|
|
36
|
+
R = TypeVar("R")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
AnyJsFunction: TypeAlias = "JsFunction[*tuple[Any, ...], Any]"
|
|
40
|
+
|
|
41
|
+
# Global cache for deduplication across all transpiled functions
|
|
42
|
+
# Registered BEFORE analyzing deps to handle mutual recursion
|
|
43
|
+
FUNCTION_CACHE: dict[Callable[..., object], AnyJsFunction] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JsFunction(JSExpr, Generic[*Args, R]):
|
|
47
|
+
is_primary: ClassVar[bool] = True
|
|
48
|
+
|
|
49
|
+
fn: Callable[[*Args], R]
|
|
50
|
+
id: str
|
|
51
|
+
deps: dict[str, JSExpr]
|
|
52
|
+
|
|
53
|
+
def __init__(self, fn: Callable[[*Args], R]) -> None:
|
|
54
|
+
self.fn = fn
|
|
55
|
+
self.id = generate_id()
|
|
56
|
+
|
|
57
|
+
# Register self in cache BEFORE analyzing deps (handles cycles)
|
|
58
|
+
FUNCTION_CACHE[fn] = self
|
|
59
|
+
|
|
60
|
+
# Analyze code object and resolve globals + closure vars
|
|
61
|
+
effective_globals, all_names = analyze_code_object(fn)
|
|
62
|
+
|
|
63
|
+
# Build dependencies dictionary - all values are JSExpr
|
|
64
|
+
deps: dict[str, JSExpr] = {}
|
|
65
|
+
|
|
66
|
+
for name in all_names:
|
|
67
|
+
value = effective_globals.get(name)
|
|
68
|
+
|
|
69
|
+
if value is None:
|
|
70
|
+
# Not in globals - check builtins (allows user to shadow builtins)
|
|
71
|
+
# Note: co_names includes both global names AND attribute names (e.g., 'input'
|
|
72
|
+
# from 'tags.input'). We only add supported builtins; unsupported ones are
|
|
73
|
+
# skipped since they might be attribute accesses handled during transpilation.
|
|
74
|
+
if name in BUILTINS:
|
|
75
|
+
deps[name] = BUILTINS[name]
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Already a JSExpr (JsFunction, JsConstant, Import, JSMember, etc.)
|
|
79
|
+
if isinstance(value, JSExpr):
|
|
80
|
+
deps[name] = value
|
|
81
|
+
elif inspect.ismodule(value):
|
|
82
|
+
if value in JS_MODULES:
|
|
83
|
+
# import pulse.js.math as Math -> JSIdentifier or Import
|
|
84
|
+
deps[name] = JS_MODULES[value].to_js_expr()
|
|
85
|
+
elif value in PY_MODULES:
|
|
86
|
+
deps[name] = PyModuleExpr(PY_MODULES[value])
|
|
87
|
+
else:
|
|
88
|
+
raise JSCompilationError(
|
|
89
|
+
f"Could not resolve JavaScript module import for '{name}' (value: {value!r}). "
|
|
90
|
+
+ "Neither a registered Python module nor a known JS wrapper. "
|
|
91
|
+
+ "Check your import statement and module configuration."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
elif id(value) in JSEXPR_REGISTRY:
|
|
95
|
+
# JSEXPR_REGISTRY always contains JSExpr (wrapping happens in JSExpr.register)
|
|
96
|
+
deps[name] = JSEXPR_REGISTRY[id(value)]
|
|
97
|
+
elif inspect.isfunction(value):
|
|
98
|
+
deps[name] = javascript(value)
|
|
99
|
+
elif callable(value):
|
|
100
|
+
raise JSCompilationError(
|
|
101
|
+
f"Callable object '{name}' (type: {type(value).__name__}) is not supported. "
|
|
102
|
+
+ "Only functions can be transpiled."
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
deps[name] = const_to_js(value, name)
|
|
106
|
+
|
|
107
|
+
self.deps = deps
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def js_name(self) -> str:
|
|
111
|
+
"""Unique JS identifier for this function."""
|
|
112
|
+
return f"{self.fn.__name__}_{self.id}"
|
|
113
|
+
|
|
114
|
+
@override
|
|
115
|
+
def emit(self) -> str:
|
|
116
|
+
"""Emit JS code for this function reference.
|
|
117
|
+
|
|
118
|
+
In normal mode: returns the unique JS name (e.g., "myFunc_1")
|
|
119
|
+
In interpreted mode: returns a get_object call (e.g., "get_object('myFunc_1')")
|
|
120
|
+
"""
|
|
121
|
+
base = self.js_name
|
|
122
|
+
if is_interpreted_mode():
|
|
123
|
+
return f"get_object('{base}')"
|
|
124
|
+
return base
|
|
125
|
+
|
|
126
|
+
def imports(self) -> dict[str, Import]:
|
|
127
|
+
"""Get all Import dependencies."""
|
|
128
|
+
return {k: v for k, v in self.deps.items() if isinstance(v, Import)}
|
|
129
|
+
|
|
130
|
+
def functions(self) -> dict[str, AnyJsFunction]:
|
|
131
|
+
"""Get all JsFunction dependencies."""
|
|
132
|
+
return {k: v for k, v in self.deps.items() if isinstance(v, JsFunction)}
|
|
133
|
+
|
|
134
|
+
def constants(self) -> dict[str, JsConstant]:
|
|
135
|
+
"""Get all JsConstant dependencies."""
|
|
136
|
+
return {k: v for k, v in self.deps.items() if isinstance(v, JsConstant)}
|
|
137
|
+
|
|
138
|
+
def modules(self) -> dict[str, PyModuleExpr]:
|
|
139
|
+
"""Get all PyModuleExpr dependencies."""
|
|
140
|
+
return {k: v for k, v in self.deps.items() if isinstance(v, PyModuleExpr)}
|
|
141
|
+
|
|
142
|
+
def module_functions(self) -> dict[str, JSTransformer]:
|
|
143
|
+
"""Get all module function JSTransformer dependencies (named imports from modules)."""
|
|
144
|
+
from pulse.transpiler.builtins import BUILTINS
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
k: v
|
|
148
|
+
for k, v in self.deps.items()
|
|
149
|
+
if isinstance(v, JSTransformer) and v.name not in BUILTINS
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def transpile(self) -> str:
|
|
153
|
+
"""Transpile this JsFunction to JavaScript code.
|
|
154
|
+
|
|
155
|
+
Returns the complete JavaScript function code.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
# Get source code
|
|
159
|
+
src = getsourcecode(self.fn)
|
|
160
|
+
src = textwrap.dedent(src)
|
|
161
|
+
|
|
162
|
+
# Parse to AST
|
|
163
|
+
module = ast.parse(src)
|
|
164
|
+
fndefs = [
|
|
165
|
+
n
|
|
166
|
+
for n in module.body
|
|
167
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
168
|
+
]
|
|
169
|
+
if not fndefs:
|
|
170
|
+
raise JSCompilationError("No function definition found in source")
|
|
171
|
+
fndef = fndefs[-1]
|
|
172
|
+
|
|
173
|
+
# Get argument names
|
|
174
|
+
arg_names = [arg.arg for arg in fndef.args.args]
|
|
175
|
+
|
|
176
|
+
# Transpile - pass deps directly, transpiler handles dispatch
|
|
177
|
+
visitor = JsTranspiler(fndef, args=arg_names, deps=self.deps)
|
|
178
|
+
js_fn = visitor.transpile(name=self.js_name)
|
|
179
|
+
return js_fn.emit()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def analyze_code_object(
|
|
183
|
+
fn: Callable[..., object],
|
|
184
|
+
) -> tuple[dict[str, Any], set[str]]:
|
|
185
|
+
"""Analyze code object and resolve globals + closure variables.
|
|
186
|
+
|
|
187
|
+
Returns a tuple of:
|
|
188
|
+
- effective_globals: dict mapping names to their values (includes closure vars)
|
|
189
|
+
- all_names: set of all names referenced in the code (including nested functions)
|
|
190
|
+
"""
|
|
191
|
+
code = fn.__code__
|
|
192
|
+
|
|
193
|
+
# Collect all names from code object and nested functions in one pass
|
|
194
|
+
seen_codes: set[int] = set()
|
|
195
|
+
all_names: set[str] = set()
|
|
196
|
+
|
|
197
|
+
def walk_code(c: pytypes.CodeType) -> None:
|
|
198
|
+
if id(c) in seen_codes:
|
|
199
|
+
return
|
|
200
|
+
seen_codes.add(id(c))
|
|
201
|
+
all_names.update(c.co_names)
|
|
202
|
+
all_names.update(c.co_freevars) # Include closure variables
|
|
203
|
+
for const in c.co_consts:
|
|
204
|
+
if isinstance(const, pytypes.CodeType):
|
|
205
|
+
walk_code(const)
|
|
206
|
+
|
|
207
|
+
walk_code(code)
|
|
208
|
+
|
|
209
|
+
# Build effective globals dict: start with function's globals, then add closure values
|
|
210
|
+
effective_globals = dict(fn.__globals__)
|
|
211
|
+
|
|
212
|
+
# Resolve closure variables from closure cells
|
|
213
|
+
if code.co_freevars and fn.__closure__:
|
|
214
|
+
closure = fn.__closure__
|
|
215
|
+
for i, freevar_name in enumerate(code.co_freevars):
|
|
216
|
+
if i < len(closure):
|
|
217
|
+
cell = closure[i]
|
|
218
|
+
# Get the value from the closure cell
|
|
219
|
+
try:
|
|
220
|
+
effective_globals[freevar_name] = cell.cell_contents
|
|
221
|
+
except ValueError:
|
|
222
|
+
# Cell is empty (unbound), skip it
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
return effective_globals, all_names
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def javascript(fn: Callable[[*Args], R]) -> JsFunction[*Args, R]:
|
|
229
|
+
"""Decorator to convert a function into a JsFunction.
|
|
230
|
+
|
|
231
|
+
Usage:
|
|
232
|
+
@javascript
|
|
233
|
+
def my_func(x: int) -> int:
|
|
234
|
+
return x + 1
|
|
235
|
+
|
|
236
|
+
# my_func is now a JsFunction instance
|
|
237
|
+
"""
|
|
238
|
+
result = FUNCTION_CACHE.get(fn)
|
|
239
|
+
if not result:
|
|
240
|
+
result = JsFunction(fn)
|
|
241
|
+
FUNCTION_CACHE[fn] = result
|
|
242
|
+
return result # pyright: ignore[reportReturnType]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def registered_functions() -> list[AnyJsFunction]:
|
|
246
|
+
"""Get all registered JS functions."""
|
|
247
|
+
return list(FUNCTION_CACHE.values())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
X = JsFunction[int]
|
pulse/transpiler/ids.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Unique ID generator for JavaScript codegen."""
|
|
2
|
+
|
|
3
|
+
from itertools import count
|
|
4
|
+
|
|
5
|
+
_counter = count(1)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_id() -> str:
|
|
9
|
+
"""Generate unique hex ID like '1', '2', 'a', 'ff', etc."""
|
|
10
|
+
return f"{next(_counter):x}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def reset_id_counter() -> None:
|
|
14
|
+
"""Reset counter (for testing)."""
|
|
15
|
+
global _counter
|
|
16
|
+
_counter = count(1)
|