pyreact-framework 1.0.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.
Files changed (41) hide show
  1. pyreact/__init__.py +144 -0
  2. pyreact/cli/__init__.py +3 -0
  3. pyreact/cli/main.py +512 -0
  4. pyreact/core/__init__.py +80 -0
  5. pyreact/core/component.py +372 -0
  6. pyreact/core/context.py +173 -0
  7. pyreact/core/element.py +208 -0
  8. pyreact/core/error_boundary.py +145 -0
  9. pyreact/core/hooks.py +550 -0
  10. pyreact/core/memo.py +221 -0
  11. pyreact/core/portal.py +159 -0
  12. pyreact/core/reconciler.py +399 -0
  13. pyreact/core/refs.py +213 -0
  14. pyreact/core/renderer.py +112 -0
  15. pyreact/core/scheduler.py +304 -0
  16. pyreact/devtools/__init__.py +18 -0
  17. pyreact/devtools/debugger.py +314 -0
  18. pyreact/devtools/profiler.py +288 -0
  19. pyreact/dom/__init__.py +64 -0
  20. pyreact/dom/attributes.py +317 -0
  21. pyreact/dom/dom_operations.py +333 -0
  22. pyreact/dom/events.py +349 -0
  23. pyreact/server/__init__.py +34 -0
  24. pyreact/server/hydration.py +216 -0
  25. pyreact/server/ssr.py +344 -0
  26. pyreact/styles/__init__.py +19 -0
  27. pyreact/styles/css_module.py +231 -0
  28. pyreact/styles/styled.py +303 -0
  29. pyreact/testing/__init__.py +71 -0
  30. pyreact/testing/fire_event.py +355 -0
  31. pyreact/testing/screen.py +267 -0
  32. pyreact/testing/test_renderer.py +232 -0
  33. pyreact/utils/__init__.py +17 -0
  34. pyreact/utils/diff.py +182 -0
  35. pyreact/utils/object_pool.py +216 -0
  36. pyreact_framework-1.0.0.dist-info/METADATA +363 -0
  37. pyreact_framework-1.0.0.dist-info/RECORD +41 -0
  38. pyreact_framework-1.0.0.dist-info/WHEEL +5 -0
  39. pyreact_framework-1.0.0.dist-info/entry_points.txt +2 -0
  40. pyreact_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
  41. pyreact_framework-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,399 @@
1
+ """Reconciler Module
2
+
3
+ This module implements the diff algorithm for comparing VNodes and efficiently updating the DOM.
4
+ """
5
+
6
+ from typing import Any, Callable, Dict, List, Optional, Union, Set
7
+ from .element import VNode
8
+ from .component import Component
9
+ from ..dom import dom_operations
10
+
11
+
12
+ class Reconciler:
13
+ """
14
+ Reconciler implements the diff algorithm for efficient DOM updates.
15
+
16
+ The algorithm follows these principles:
17
+ 1. Elements of different types → replace completely
18
+ 2. Elements of same type → update attributes/props
19
+ 3. Children with keys → reorder/move instead of recreate
20
+ 4. Components → compare props and decide if re-render needed
21
+ """
22
+
23
+ def __init__(self):
24
+ self._component_instances: Dict[int, Component] = {}
25
+
26
+ def diff(
27
+ self,
28
+ old_vnode: Optional[VNode],
29
+ new_vnode: Optional[VNode],
30
+ parent_dom: Any,
31
+ index: int = 0
32
+ ) -> Optional[VNode]:
33
+ """
34
+ Compare two VNodes and apply changes to DOM
35
+
36
+ Args:
37
+ old_vnode: Previous VNode (or None for new)
38
+ new_vnode: New VNode (or None for removal)
39
+ parent_dom: Parent DOM element
40
+ index: Child index in parent
41
+
42
+ Returns:
43
+ VNode: The new VNode (may be reused or new)
44
+ """
45
+ # Case 1: New node is None → remove
46
+ if new_vnode is None:
47
+ if old_vnode:
48
+ self._remove_node(parent_dom, old_vnode, index)
49
+ return None
50
+
51
+ # Case 2: Old node is None → create
52
+ if old_vnode is None:
53
+ new_dom = self.create_dom(new_vnode)
54
+ self._insert_node(parent_dom, new_dom, index)
55
+ return new_vnode
56
+
57
+ # Case 3: Different types → replace
58
+ if self._get_type(old_vnode) != self._get_type(new_vnode):
59
+ new_dom = self.create_dom(new_vnode)
60
+ self._replace_node(parent_dom, new_dom, old_vnode, index)
61
+ return new_vnode
62
+
63
+ # Case 4: Same type → update
64
+ if self._is_component(new_vnode):
65
+ return self._update_component(old_vnode, new_vnode)
66
+ else:
67
+ return self._update_dom_element(old_vnode, new_vnode)
68
+
69
+ def create_dom(self, vnode: VNode) -> Any:
70
+ """
71
+ Create a DOM node from a VNode
72
+
73
+ Args:
74
+ vnode: Virtual node to create
75
+
76
+ Returns:
77
+ DOM node
78
+ """
79
+ # Text node
80
+ if isinstance(vnode.type, str) and vnode.type == '#text':
81
+ dom = self._create_text_node(vnode.children[0] if vnode.children else '')
82
+ vnode._dom_node = dom
83
+ return dom
84
+
85
+ # Component
86
+ if callable(vnode.type) and not isinstance(vnode.type, str):
87
+ return self._create_component_dom(vnode)
88
+
89
+ # HTML element
90
+ dom = self._create_element(vnode.type)
91
+ vnode._dom_node = dom
92
+
93
+ # Apply props
94
+ self._apply_props(dom, {}, vnode.props)
95
+
96
+ # Apply ref
97
+ if vnode.ref:
98
+ vnode.ref.current = dom
99
+
100
+ # Create children
101
+ for child in vnode.children:
102
+ if isinstance(child, str):
103
+ text_node = self._create_text_node(child)
104
+ dom.append_child(text_node)
105
+ elif isinstance(child, VNode):
106
+ child_dom = self.create_dom(child)
107
+ dom.append_child(child_dom)
108
+
109
+ return dom
110
+
111
+ def _create_component_dom(self, vnode: VNode) -> Any:
112
+ """Create DOM for a component"""
113
+ from .hooks import _set_current_component, _reset_hook_index
114
+
115
+ component_type = vnode.type
116
+
117
+ # Instantiate component
118
+ if isinstance(component_type, type):
119
+ # Class component
120
+ component = component_type(vnode.props)
121
+ else:
122
+ # Function component - wrap in a simple component
123
+ component = _FunctionComponent(vnode.type, vnode.props)
124
+
125
+ component._vnode = vnode
126
+ component._hooks = []
127
+ vnode._component_instance = component
128
+
129
+ # Set component context for hooks
130
+ _set_current_component(component)
131
+ _reset_hook_index()
132
+
133
+ # Render component
134
+ rendered = component.render()
135
+
136
+ # Reset context
137
+ _set_current_component(None)
138
+
139
+ if rendered is None:
140
+ # Render nothing
141
+ return self._create_comment('empty')
142
+
143
+ dom = self.create_dom(rendered)
144
+ component._dom_node = dom
145
+ vnode._dom_node = dom
146
+
147
+ # Call lifecycle
148
+ component.component_did_mount()
149
+
150
+ return dom
151
+
152
+ def _update_component(self, old_vnode: VNode, new_vnode: VNode) -> VNode:
153
+ """Update a component"""
154
+ from .hooks import _set_current_component, _reset_hook_index
155
+
156
+ old_component = old_vnode._component_instance
157
+ new_props = new_vnode.props
158
+
159
+ # Check if should update
160
+ should_update = True
161
+ if hasattr(old_component, 'should_component_update'):
162
+ should_update = old_component.should_component_update(
163
+ new_props, old_component.state
164
+ )
165
+
166
+ # Update props
167
+ old_component.props = new_props
168
+ new_vnode._component_instance = old_component
169
+ new_vnode._dom_node = old_vnode._dom_node
170
+
171
+ if should_update:
172
+ # Set component context for hooks
173
+ _set_current_component(old_component)
174
+ _reset_hook_index()
175
+
176
+ # Re-render
177
+ old_rendered = old_vnode._component_instance._vnode
178
+ new_rendered = old_component.render()
179
+
180
+ # Reset context
181
+ _set_current_component(None)
182
+
183
+ if new_rendered is None:
184
+ # Remove
185
+ if old_rendered:
186
+ self._remove_node(
187
+ old_vnode._dom_node.parent_node,
188
+ old_rendered,
189
+ 0
190
+ )
191
+ else:
192
+ # Diff
193
+ self.diff(old_rendered, new_rendered, old_vnode._dom_node.parent_node)
194
+ old_component._vnode = new_rendered
195
+
196
+ # Call lifecycle
197
+ if hasattr(old_component, 'component_did_update'):
198
+ old_component.component_did_update(old_vnode.props, old_component.state)
199
+
200
+ return new_vnode
201
+
202
+ def _update_dom_element(self, old_vnode: VNode, new_vnode: VNode) -> VNode:
203
+ """Update a DOM element"""
204
+ dom = old_vnode._dom_node
205
+ new_vnode._dom_node = dom
206
+
207
+ # Update props
208
+ self._apply_props(dom, old_vnode.props, new_vnode.props)
209
+
210
+ # Update ref
211
+ if new_vnode.ref != old_vnode.ref:
212
+ if old_vnode.ref:
213
+ old_vnode.ref.current = None
214
+ if new_vnode.ref:
215
+ new_vnode.ref.current = dom
216
+
217
+ # Reconcile children
218
+ self._reconcile_children(old_vnode, new_vnode, dom)
219
+
220
+ return new_vnode
221
+
222
+ def _reconcile_children(
223
+ self,
224
+ old_vnode: VNode,
225
+ new_vnode: VNode,
226
+ parent_dom: Any
227
+ ) -> None:
228
+ """
229
+ Reconcile children using keys for minimal DOM operations
230
+ """
231
+ old_children = old_vnode.children
232
+ new_children = new_vnode.children
233
+
234
+ # Build key map for old children
235
+ old_keyed: Dict[Union[str, int], tuple] = {}
236
+ old_index = 0
237
+ for child in old_children:
238
+ if isinstance(child, VNode) and child.key is not None:
239
+ old_keyed[child.key] = (old_index, child)
240
+ old_index += 1
241
+
242
+ # Track which old children are used
243
+ used_keys: Set[Union[str, int]] = set()
244
+
245
+ # Process new children
246
+ new_index = 0
247
+ for new_child in new_children:
248
+ child_key = new_child.key if isinstance(new_child, VNode) else None
249
+
250
+ if child_key is not None and child_key in old_keyed:
251
+ # Reuse existing child
252
+ old_idx, old_child = old_keyed[child_key]
253
+ used_keys.add(child_key)
254
+
255
+ # Move if needed
256
+ if old_idx != new_index:
257
+ self._move_child(parent_dom, old_idx, new_index)
258
+
259
+ # Diff
260
+ self.diff(old_child, new_child, parent_dom, new_index)
261
+ else:
262
+ # Create new child
263
+ if isinstance(new_child, str):
264
+ text_node = self._create_text_node(new_child)
265
+ self._insert_node(parent_dom, text_node, new_index)
266
+ elif isinstance(new_child, VNode):
267
+ child_dom = self.create_dom(new_child)
268
+ self._insert_node(parent_dom, child_dom, new_index)
269
+
270
+ new_index += 1
271
+
272
+ # Remove unused old children
273
+ for key, (idx, child) in old_keyed.items():
274
+ if key not in used_keys:
275
+ self._remove_node(parent_dom, child, idx)
276
+
277
+ def unmount(self, vnode: VNode) -> None:
278
+ """
279
+ Unmount a VNode and its children
280
+
281
+ Args:
282
+ vnode: VNode to unmount
283
+ """
284
+ if vnode._component_instance:
285
+ component = vnode._component_instance
286
+ component.component_will_unmount()
287
+
288
+ # Unmount children
289
+ for child in vnode.children:
290
+ if isinstance(child, VNode):
291
+ self.unmount(child)
292
+
293
+ # Clear ref
294
+ if vnode.ref:
295
+ vnode.ref.current = None
296
+
297
+ # DOM Operations - using dom_operations module
298
+
299
+ def _create_element(self, tag: str) -> Any:
300
+ """Create a DOM element"""
301
+ return dom_operations.create_element(tag)
302
+
303
+ def _create_text_node(self, text: str) -> Any:
304
+ """Create a text node"""
305
+ return dom_operations.create_text_node(text)
306
+
307
+ def _create_comment(self, text: str) -> Any:
308
+ """Create a comment node"""
309
+ # For now, use a text node as fallback
310
+ return dom_operations.create_text_node(f'<!-- {text} -->')
311
+
312
+ def _apply_props(self, dom: Any, old_props: Dict, new_props: Dict) -> None:
313
+ """Apply props to DOM element"""
314
+ # Remove old props
315
+ for key in old_props:
316
+ if key not in new_props:
317
+ self._remove_prop(dom, key, old_props[key])
318
+
319
+ # Set new props
320
+ for key, value in new_props.items():
321
+ if old_props.get(key) != value:
322
+ self._set_prop(dom, key, value)
323
+
324
+ def _set_prop(self, dom: Any, key: str, value: Any) -> None:
325
+ """Set a single prop on DOM element"""
326
+ if key == 'className':
327
+ dom.set_attribute('class', value)
328
+ elif key == 'style' and isinstance(value, dict):
329
+ for style_key, style_value in value.items():
330
+ dom.set_style(style_key, style_value)
331
+ elif key.startswith('on'):
332
+ # Event handler
333
+ event_name = key[2:].lower()
334
+ dom.add_event_listener(event_name, value)
335
+ elif key == 'dangerouslySetInnerHTML':
336
+ dom.set_inner_html(value.get('__html', ''))
337
+ else:
338
+ dom.set_attribute(key, value)
339
+
340
+ def _remove_prop(self, dom: Any, key: str, value: Any) -> None:
341
+ """Remove a prop from DOM element"""
342
+ if key == 'className':
343
+ dom.remove_attribute('class')
344
+ elif key == 'style':
345
+ dom.remove_attribute('style')
346
+ elif key.startswith('on'):
347
+ event_name = key[2:].lower()
348
+ dom.remove_event_listener(event_name)
349
+ else:
350
+ dom.remove_attribute(key)
351
+
352
+ def _insert_node(self, parent: Any, node: Any, index: int) -> None:
353
+ """Insert node at index"""
354
+ parent.insert_child(node, index)
355
+
356
+ def _remove_node(self, parent: Any, vnode: VNode, index: int) -> None:
357
+ """Remove node at index"""
358
+ parent.remove_child_at(index)
359
+ self.unmount(vnode)
360
+
361
+ def _replace_node(self, parent: Any, new_node: Any, old_vnode: VNode, index: int) -> None:
362
+ """Replace node at index"""
363
+ self.unmount(old_vnode)
364
+ parent.replace_child_at(new_node, index)
365
+
366
+ def _move_child(self, parent: Any, old_index: int, new_index: int) -> None:
367
+ """Move child from old_index to new_index"""
368
+ parent.move_child(old_index, new_index)
369
+
370
+ def _get_type(self, vnode: VNode) -> Any:
371
+ """Get the type of a VNode for comparison"""
372
+ return vnode.type
373
+
374
+ def _is_component(self, vnode: VNode) -> bool:
375
+ """Check if VNode is a component"""
376
+ return callable(vnode.type) and not isinstance(vnode.type, str)
377
+
378
+
379
+ class _FunctionComponent:
380
+ """Wrapper for function components"""
381
+
382
+ def __init__(self, render_fn: Callable, props: Dict):
383
+ self.render_fn = render_fn
384
+ self.props = props
385
+ self.state = {}
386
+ self._vnode = None
387
+ self._dom_node = None
388
+
389
+ def render(self) -> Optional[VNode]:
390
+ return self.render_fn(self.props)
391
+
392
+ def component_did_mount(self) -> None:
393
+ pass
394
+
395
+ def component_will_unmount(self) -> None:
396
+ pass
397
+
398
+ def should_component_update(self, next_props, next_state) -> bool:
399
+ return self.props != next_props
pyreact/core/refs.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ Refs Module
3
+ ===========
4
+
5
+ This module implements the Ref system for accessing DOM nodes
6
+ and component instances directly.
7
+ """
8
+
9
+ from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .element import VNode
13
+
14
+
15
+ class Ref:
16
+ """
17
+ Reference to a DOM node or component instance
18
+
19
+ Refs provide a way to access DOM nodes or component instances
20
+ directly without using props.
21
+
22
+ Example:
23
+ class TextInput(Component):
24
+ def __init__(self, props):
25
+ super().__init__(props)
26
+ self.input_ref = create_ref()
27
+
28
+ def focus(self):
29
+ self.input_ref.current.focus()
30
+
31
+ def render(self):
32
+ return h('input', {'ref': self.input_ref, 'type': 'text'})
33
+ """
34
+
35
+ def __init__(self):
36
+ self.current: Any = None
37
+
38
+ def __repr__(self) -> str:
39
+ return f"Ref(current={self.current!r})"
40
+
41
+
42
+ def create_ref() -> Ref:
43
+ """
44
+ Create a new Ref object
45
+
46
+ Returns:
47
+ Ref: A new reference object
48
+
49
+ Example:
50
+ input_ref = create_ref()
51
+ h('input', {'ref': input_ref})
52
+ """
53
+ return Ref()
54
+
55
+
56
+ def forward_ref(render: Callable) -> Callable:
57
+ """
58
+ Forward a ref through a component to a DOM element
59
+
60
+ Allows a component to forward a ref to one of its children.
61
+
62
+ Args:
63
+ render: Function (props, ref) -> VNode
64
+
65
+ Returns:
66
+ Component that forwards the ref
67
+
68
+ Example:
69
+ @forward_ref
70
+ def FancyInput(props, ref):
71
+ return h('input', {
72
+ 'ref': ref,
73
+ 'className': 'fancy-input',
74
+ **props
75
+ })
76
+
77
+ # Usage
78
+ input_ref = create_ref()
79
+ h(FancyInput, {'ref': input_ref, 'placeholder': 'Type here'})
80
+ """
81
+ def wrapper(props: Dict[str, Any]) -> 'VNode':
82
+ ref = props.pop('ref', None)
83
+ return render(props, ref)
84
+
85
+ wrapper._forward_ref = True
86
+ wrapper.__name__ = render.__name__
87
+ wrapper.__doc__ = render.__doc__
88
+
89
+ return wrapper
90
+
91
+
92
+ def use_imperative_handle(
93
+ ref: Optional[Ref],
94
+ create_handle: Callable[[], Any],
95
+ dependencies: Optional[list] = None
96
+ ) -> None:
97
+ """
98
+ Customize the instance value exposed to parent components
99
+
100
+ Use with forward_ref to expose specific methods to the parent.
101
+
102
+ Args:
103
+ ref: Ref passed from parent
104
+ create_handle: Function that returns object to expose
105
+ dependencies: List of values that trigger handle update
106
+
107
+ Example:
108
+ @forward_ref
109
+ def FancyInput(props, ref):
110
+ input_ref = use_ref(None)
111
+
112
+ use_imperative_handle(ref, lambda: {
113
+ 'focus': lambda: input_ref.current.focus(),
114
+ 'scrollIntoView': lambda: input_ref.current.scrollIntoView()
115
+ }, [])
116
+
117
+ return h('input', {'ref': input_ref, **props})
118
+ """
119
+ from .hooks import use_effect
120
+
121
+ def effect():
122
+ if ref:
123
+ ref.current = create_handle()
124
+ return lambda: None
125
+
126
+ use_effect(effect, dependencies)
127
+
128
+
129
+ class CallbackRef:
130
+ """
131
+ Callback-based ref
132
+
133
+ Called with the DOM node when it's mounted or unmounted.
134
+
135
+ Example:
136
+ def set_input_ref(node):
137
+ if node:
138
+ node.focus()
139
+
140
+ h('input', {'ref': set_input_ref})
141
+ """
142
+
143
+ def __init__(self, callback: Callable[[Optional[Any]], None]):
144
+ self.callback = callback
145
+ self.current: Optional[Any] = None
146
+
147
+ def __call__(self, node: Optional[Any]) -> None:
148
+ """Called when ref is set"""
149
+ self.current = node
150
+ self.callback(node)
151
+
152
+
153
+ def create_callback_ref(callback: Callable[[Optional[Any]], None]) -> CallbackRef:
154
+ """
155
+ Create a callback ref
156
+
157
+ Args:
158
+ callback: Function called with the DOM node
159
+
160
+ Returns:
161
+ CallbackRef: A callback-based reference
162
+
163
+ Example:
164
+ def on_input_mount(node):
165
+ if node:
166
+ node.focus()
167
+
168
+ ref = create_callback_ref(on_input_mount)
169
+ h('input', {'ref': ref})
170
+ """
171
+ return CallbackRef(callback)
172
+
173
+
174
+ def is_ref(value: Any) -> bool:
175
+ """
176
+ Check if a value is a valid ref
177
+
178
+ Args:
179
+ value: Value to check
180
+
181
+ Returns:
182
+ bool: True if value is a ref
183
+ """
184
+ return isinstance(value, (Ref, CallbackRef)) or callable(value)
185
+
186
+
187
+ def attach_ref(ref: Any, value: Any) -> None:
188
+ """
189
+ Attach a value to a ref
190
+
191
+ Args:
192
+ ref: Ref object or callback
193
+ value: Value to attach
194
+ """
195
+ if ref is None:
196
+ return
197
+
198
+ if isinstance(ref, Ref):
199
+ ref.current = value
200
+ elif callable(ref):
201
+ ref(value)
202
+ elif isinstance(ref, CallbackRef):
203
+ ref(value)
204
+
205
+
206
+ def detach_ref(ref: Any) -> None:
207
+ """
208
+ Detach a value from a ref
209
+
210
+ Args:
211
+ ref: Ref object or callback
212
+ """
213
+ attach_ref(ref, None)