-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphelix.py
More file actions
348 lines (264 loc) · 9.97 KB
/
phelix.py
File metadata and controls
348 lines (264 loc) · 9.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
"""
# Phelix (a.k.a fire components)
crystal.py version 2
with significant upgrades both in terms of
application architecture and compatibility
"""
from contextlib import contextmanager
from typing import Any, Callable, cast
_component_stack: list["Component"] = []
"stores the depths of current running components at runtime"
@contextmanager
def useRuntime(component: "Component"):
"""
(internal hook) it registers a component as running during the runtime.
allowing for other hooks to use the component object wirelessly
-> `useComponent`
"""
global _component_stack
_component_stack.append(component)
yield
_component_stack.pop()
return
def useComponent(*, index: int = -1) -> "Component":
"returns the current active component"
if index < -len(_component_stack) or index >= len(_component_stack):
raise IndexError("Component index out of range.")
return _component_stack[index]
class Component[RType]:
"a component object"
_state: dict[str, Any] = {}
previous_state: dict[str, Any] = {}
"previous local state of the component"
function: Callable[..., RType]
"render logic function passed by the user"
name: str
"name of the component"
app: "Application|None" = None
"the parent application"
_is_mounted: bool = False
"True if the component has rendered at least once other wise False"
_reactivity: tuple[Callable[["Component"], None], Callable[["Component"], None]] = (
lambda _: None,
lambda _: None,
)
_flagged_as_dirty: bool = False
@property
def is_mounted(self) -> bool:
"True if the component has rendered at least once other wise False"
return self._is_mounted
@is_mounted.setter
def is_mounted(self, s: bool):
if s == True:
self._reactivity[0](self)
self._is_mounted = s
@property
def state(self) -> dict[str, Any]:
"local state of the component"
return self._state
@state.setter
def state(self, new: dict[str, Any]) -> None:
self.previous_state = self.state.copy()
self._state = new
@property
def is_dirty(self) -> bool:
"returns wether the component state has changed or not"
return ( not (self.previous_state == self._state) ) or ( self._flagged_as_dirty )
@is_dirty.setter
def is_dirty(self, new: bool):
"set the component as dirty"
self._flagged_as_dirty = new
def useReactivity(
self,
update: Callable[["Component"], None],
mount: Callable[["Component"], None],
):
self._reactivity = (mount, update)
def __init__(self, function: Callable[..., RType]) -> None:
self.function = function
self.name = function.__name__
self.__doc__ = f"{function.__name__.capitalize()} Component"
def render(self, *args, **kwargs) -> RType:
"render the component"
with useRuntime(self):
result = self.function(*args, **kwargs)
self.is_mounted = True
if self.is_dirty:
self._flagged_as_dirty = False
self._reactivity[1](self)
return result
def __call__(self, *args: Any, **kwds: Any) -> RType:
return self.render(*args, **kwds)
def leafComponent[RType](function: Callable[..., RType]) -> Component[RType]:
"create a component without an application"
component = Component(function)
component.app = None
return component
def store(initial: dict[str, Any], reducer: dict[str, Callable[[dict[str, Any]], dict[str, Any]]]) -> tuple[Callable[[str], tuple[Any, Callable[[Any], Any]]], Callable[[str], None]]:
"an application level global state store"
data = initial
def write(new: dict[str, Any]):
nonlocal data
data = new
def __useState(name: str) -> tuple[Any, Callable[[Any], Any]]:
nonlocal data
def writer(new: Any) -> Any:
nonlocal data
write({**data, name: new})
useComponent().is_dirty = True
return new
return data.get(name, initial), writer
def useReducer(name: str):
nonlocal data
mutator = reducer.get(name, lambda x: x)
data = mutator(data)
return __useState, useReducer
class Application:
"a phelix application layer"
name: str
"name of the app"
components: dict[str, Component] = {}
"components of the app"
_root: Component | None = None
"root component of the app"
pages: dict[str, Component] = {}
"contains the other pages of the app"
active: Component | None = None
"contains the main component being rendered (defaults to `app._root`)"
def __getattribute__(self, name: str, /):
try:
return super().__getattribute__(name)
except AttributeError:
if name in self.components:
return self.components[name]
else:
raise
def __getitem__(self, name: str):
return self.components[name]
def __init__(self, name: str) -> None:
self.name = name
def component[RType](self, function: Callable[..., RType]) -> Component[RType]:
"create a new component"
component = Component(function)
component.app = self
self.components[component.name] = component
return component
def route(self, url: str):
"create a new app route"
def decorator[RType](function: Callable[..., RType]) -> Component[RType]:
component = Component(function)
component.app = self
self.pages[url] = component
self.active = component
return component
return decorator
def load_route(self, route: str):
"set the active page to application component at the specified route"
if not route.startswith("/"):
raise RuntimeError(f"{self.name}:load_route invalid route {route!r}")
if (route == "/") or (route == "/root"):
self.active = self._root
elif route in self.pages:
self.active = self.pages[route]
else:
self.active = self._root
return True
return False
def root[RType](self, function: Callable[..., RType]) -> Component[RType]:
"create the root component of the application"
component = Component(function)
component.app = self
self._root = component
self.active = self._root
return component
def render(self) -> Any:
"render the application from its root component"
assert self.active != None, f"{self.name}:@active component not defined!"
return self.active.render()
def useState() -> tuple[dict, Callable]:
"returns the helper functions for reading and writing the component state"
component = useComponent() # returns the active component
def write(state: dict[str, Any], *, ignore_compatibility: bool = False) -> None:
nonlocal component
if not ignore_compatibility:
if not all(key in state for key in component.state.keys()):
raise RuntimeError(
"incompatible state update with the previous component state"
)
component.state = state
return (component.state, write)
def useComponentName() -> str:
"returns the component name"
return useComponent().name
def useParentName() -> str:
"returns the parent component name"
return useParent().name
def useApp() -> Application | None:
"returns the current component active application if exists, otherwise returns `None`"
return useComponent().app
def usePreviousState() -> dict[str, Any]:
"Returns the previous state of the active component"
component = useComponent()
return component.previous_state
def useStateDiff() -> dict[str, tuple[Any, Any]]:
"return the diff of the current state and the previous state of the component"
current = useComponent().state
previous = usePreviousState()
diffs = {}
all_keys = set(previous.keys()).union(current.keys())
for key in all_keys:
old_val = previous.get(key)
new_val = current.get(key)
if old_val != new_val:
diffs[key] = (old_val, new_val)
return diffs
def useTemporary[T](initial: T) -> tuple[Callable[[], T], Callable[[T], None]]:
"creates a temporary state"
value: T = initial
def read() -> T:
nonlocal value
return value
def write(new: T):
nonlocal value
value = new
return (read, write)
def useIsMounted():
"returns True if the component has mounted otherwise False"
return useComponent().is_mounted
def onMount():
"like `useIsMounted` but instead it would return True if the component is being mounted"
return not useIsMounted()
def useEffect(callback: Callable[[], None], dependencies: list[str]) -> None:
"""
Executes a callback function when specified state keys change.
**Note**: The callback will not run on the initial mount of the component.
"""
component = useComponent()
if not component.is_mounted:
return
previous_state = component.previous_state
current_state = component.state
changed = False
for key in dependencies:
if previous_state.get(key, None) != current_state.get(key, None):
changed = True
break
if changed:
callback()
def useParent() -> Component:
"returns the parent component"
return useComponent(index=-2)
def useStateVar[T](name: str, initial: T) -> tuple[T, Callable[[T], T]]:
"create a state for the component"
state, write = useState()
def writer(new: T) -> T:
nonlocal state
write({**state, name: new})
return new
return cast(T, state.get(name, initial)), writer
def useRoute(route: str) -> bool:
"sets the current page/route of the application, returns True if an error occurred"
app = useApp()
if app is None:
raise RuntimeError("useRoute() must be used within an application context")
return app.load_route(route)