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.
- pyreact/__init__.py +144 -0
- pyreact/cli/__init__.py +3 -0
- pyreact/cli/main.py +512 -0
- pyreact/core/__init__.py +80 -0
- pyreact/core/component.py +372 -0
- pyreact/core/context.py +173 -0
- pyreact/core/element.py +208 -0
- pyreact/core/error_boundary.py +145 -0
- pyreact/core/hooks.py +550 -0
- pyreact/core/memo.py +221 -0
- pyreact/core/portal.py +159 -0
- pyreact/core/reconciler.py +399 -0
- pyreact/core/refs.py +213 -0
- pyreact/core/renderer.py +112 -0
- pyreact/core/scheduler.py +304 -0
- pyreact/devtools/__init__.py +18 -0
- pyreact/devtools/debugger.py +314 -0
- pyreact/devtools/profiler.py +288 -0
- pyreact/dom/__init__.py +64 -0
- pyreact/dom/attributes.py +317 -0
- pyreact/dom/dom_operations.py +333 -0
- pyreact/dom/events.py +349 -0
- pyreact/server/__init__.py +34 -0
- pyreact/server/hydration.py +216 -0
- pyreact/server/ssr.py +344 -0
- pyreact/styles/__init__.py +19 -0
- pyreact/styles/css_module.py +231 -0
- pyreact/styles/styled.py +303 -0
- pyreact/testing/__init__.py +71 -0
- pyreact/testing/fire_event.py +355 -0
- pyreact/testing/screen.py +267 -0
- pyreact/testing/test_renderer.py +232 -0
- pyreact/utils/__init__.py +17 -0
- pyreact/utils/diff.py +182 -0
- pyreact/utils/object_pool.py +216 -0
- pyreact_framework-1.0.0.dist-info/METADATA +363 -0
- pyreact_framework-1.0.0.dist-info/RECORD +41 -0
- pyreact_framework-1.0.0.dist-info/WHEEL +5 -0
- pyreact_framework-1.0.0.dist-info/entry_points.txt +2 -0
- pyreact_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyreact_framework-1.0.0.dist-info/top_level.txt +1 -0
pyreact/core/memo.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memo Module
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
This module implements memoization utilities for optimizing
|
|
6
|
+
component re-renders.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from .element import VNode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def memo(
|
|
15
|
+
component: Callable,
|
|
16
|
+
are_props_equal: Optional[Callable[[Dict, Dict], bool]] = None
|
|
17
|
+
) -> Callable:
|
|
18
|
+
"""
|
|
19
|
+
Memoize a component to prevent unnecessary re-renders
|
|
20
|
+
|
|
21
|
+
Similar to React.memo, wraps a component and only re-renders
|
|
22
|
+
when props change.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
component: Component function to memoize
|
|
26
|
+
are_props_equal: Optional custom comparison function
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Memoized component
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
@memo
|
|
33
|
+
def ExpensiveComponent(props):
|
|
34
|
+
return h('div', None, expensive_computation(props['data']))
|
|
35
|
+
|
|
36
|
+
# With custom comparison
|
|
37
|
+
@memo(are_props_equal=lambda prev, next: prev['id'] == next['id'])
|
|
38
|
+
def UserAvatar(props):
|
|
39
|
+
return h('img', {'src': props['user']['avatar']})
|
|
40
|
+
"""
|
|
41
|
+
@wraps(component)
|
|
42
|
+
def memoized(props: Dict[str, Any]) -> VNode:
|
|
43
|
+
# Check if we have previous props
|
|
44
|
+
if memoized._prev_props is not None:
|
|
45
|
+
# Compare props
|
|
46
|
+
if are_props_equal:
|
|
47
|
+
should_update = not are_props_equal(memoized._prev_props, props)
|
|
48
|
+
else:
|
|
49
|
+
should_update = not _shallow_equal(memoized._prev_props, props)
|
|
50
|
+
|
|
51
|
+
if not should_update:
|
|
52
|
+
return memoized._prev_result
|
|
53
|
+
|
|
54
|
+
# Render and cache
|
|
55
|
+
result = component(props)
|
|
56
|
+
memoized._prev_props = props.copy()
|
|
57
|
+
memoized._prev_result = result
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
memoized._prev_props = None
|
|
61
|
+
memoized._prev_result = None
|
|
62
|
+
memoized._component = component
|
|
63
|
+
|
|
64
|
+
return memoized
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def lazy(loader: Callable[[], Any]) -> Callable:
|
|
68
|
+
"""
|
|
69
|
+
Lazy load a component
|
|
70
|
+
|
|
71
|
+
Returns a component that is loaded on demand.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
loader: Async function that returns the component module
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Lazy component
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
HeavyComponent = lazy(lambda: import_module('./HeavyComponent.py'))
|
|
81
|
+
|
|
82
|
+
h(Suspense, {'fallback': h('div', None, 'Loading...')},
|
|
83
|
+
h(HeavyComponent, None)
|
|
84
|
+
)
|
|
85
|
+
"""
|
|
86
|
+
from .component import Component
|
|
87
|
+
|
|
88
|
+
class LazyComponent(Component):
|
|
89
|
+
"""Lazy-loaded component wrapper"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, props):
|
|
92
|
+
super().__init__(props)
|
|
93
|
+
self.state = {
|
|
94
|
+
'loaded': False,
|
|
95
|
+
'component': None,
|
|
96
|
+
'error': None
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def component_did_mount(self):
|
|
100
|
+
"""Load the component"""
|
|
101
|
+
import asyncio
|
|
102
|
+
|
|
103
|
+
async def load():
|
|
104
|
+
try:
|
|
105
|
+
module = await loader()
|
|
106
|
+
component = getattr(module, 'default', module)
|
|
107
|
+
self.set_state({
|
|
108
|
+
'loaded': True,
|
|
109
|
+
'component': component
|
|
110
|
+
})
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.set_state({'error': e})
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
loop = asyncio.get_event_loop()
|
|
116
|
+
except RuntimeError:
|
|
117
|
+
loop = asyncio.new_event_loop()
|
|
118
|
+
asyncio.set_event_loop(loop)
|
|
119
|
+
|
|
120
|
+
loop.run_until_complete(load())
|
|
121
|
+
|
|
122
|
+
def render(self):
|
|
123
|
+
if self.state['error']:
|
|
124
|
+
return self.props.get('fallback', VNode('div', None, ['Error loading component']))
|
|
125
|
+
|
|
126
|
+
if not self.state['loaded']:
|
|
127
|
+
return self.props.get('loading', VNode('div', None, ['Loading...']))
|
|
128
|
+
|
|
129
|
+
Component = self.state['component']
|
|
130
|
+
return VNode(Component, self.props)
|
|
131
|
+
|
|
132
|
+
return LazyComponent
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Suspense:
|
|
136
|
+
"""
|
|
137
|
+
Suspense boundary for handling loading states
|
|
138
|
+
|
|
139
|
+
Wraps components that may suspend (like lazy components)
|
|
140
|
+
and shows a fallback while loading.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
h(Suspense, {'fallback': h('div', None, 'Loading...')},
|
|
144
|
+
h(LazyComponent, None)
|
|
145
|
+
)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, props: Dict[str, Any]):
|
|
149
|
+
self.props = props
|
|
150
|
+
self.fallback = props.get('fallback', VNode('div', None, ['Loading...']))
|
|
151
|
+
self.children = props.get('children', [])
|
|
152
|
+
|
|
153
|
+
def render(self) -> VNode:
|
|
154
|
+
"""Render fallback or children"""
|
|
155
|
+
# In a real implementation, this would check if children are suspended
|
|
156
|
+
return self.children if self.children else self.fallback
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _shallow_equal(obj1: Dict[str, Any], obj2: Dict[str, Any]) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Perform shallow comparison of two objects
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
obj1: First object
|
|
165
|
+
obj2: Second object
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
bool: True if objects are shallowly equal
|
|
169
|
+
"""
|
|
170
|
+
if obj1 is obj2:
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
if len(obj1) != len(obj2):
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
for key in obj1:
|
|
177
|
+
if key not in obj2 or obj1[key] != obj2[key]:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def are_props_shallow_equal(prev_props: Dict, next_props: Dict) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Check if props are shallowly equal
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
prev_props: Previous props
|
|
189
|
+
next_props: Next props
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
bool: True if props are shallowly equal
|
|
193
|
+
"""
|
|
194
|
+
return _shallow_equal(prev_props, next_props)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def shallow_compare(
|
|
198
|
+
prev_props: Dict,
|
|
199
|
+
next_props: Dict,
|
|
200
|
+
prev_state: Optional[Dict] = None,
|
|
201
|
+
next_state: Optional[Dict] = None
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Compare props and state for PureComponent
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
prev_props: Previous props
|
|
208
|
+
next_props: Next props
|
|
209
|
+
prev_state: Previous state (optional)
|
|
210
|
+
next_state: Next state (optional)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
bool: True if props and state are shallowly equal
|
|
214
|
+
"""
|
|
215
|
+
props_equal = _shallow_equal(prev_props, next_props)
|
|
216
|
+
|
|
217
|
+
if prev_state is not None and next_state is not None:
|
|
218
|
+
state_equal = _shallow_equal(prev_state, next_state)
|
|
219
|
+
return props_equal and state_equal
|
|
220
|
+
|
|
221
|
+
return props_equal
|
pyreact/core/portal.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Portal Module
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
This module implements Portals for rendering children into a different
|
|
6
|
+
DOM node than their parent.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from .element import VNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Portal:
|
|
14
|
+
"""
|
|
15
|
+
Portal for rendering into a different DOM container
|
|
16
|
+
|
|
17
|
+
Portals provide a way to render children into a DOM node that exists
|
|
18
|
+
outside the parent component's DOM hierarchy.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
def Modal(props):
|
|
22
|
+
return create_portal(
|
|
23
|
+
h('div', {'className': 'modal-overlay'},
|
|
24
|
+
h('div', {'className': 'modal-content'},
|
|
25
|
+
props['children']
|
|
26
|
+
)
|
|
27
|
+
),
|
|
28
|
+
document.body
|
|
29
|
+
)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, children: Any, container: Any):
|
|
33
|
+
"""
|
|
34
|
+
Initialize portal
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
children: VNode or list of VNodes to render
|
|
38
|
+
container: DOM node to render into
|
|
39
|
+
"""
|
|
40
|
+
self.children = children
|
|
41
|
+
self.container = container
|
|
42
|
+
self._dom_nodes: list = []
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return f"Portal(container={self.container!r})"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_portal(children: Any, container: Any) -> Portal:
|
|
49
|
+
"""
|
|
50
|
+
Create a portal to render children into a different container
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
children: VNode or list of VNodes to render
|
|
54
|
+
container: DOM node to render into
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Portal: A portal object
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
def Tooltip(props):
|
|
61
|
+
return create_portal(
|
|
62
|
+
h('div', {'className': 'tooltip'},
|
|
63
|
+
props['content']
|
|
64
|
+
),
|
|
65
|
+
document.getElementById('tooltip-root')
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
return Portal(children, container)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_portal(element: Any) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Check if an element is a portal
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
element: Element to check
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: True if element is a portal
|
|
80
|
+
"""
|
|
81
|
+
return isinstance(element, Portal)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def unmount_portal(portal: Portal) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Unmount a portal's children
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
portal: Portal to unmount
|
|
90
|
+
"""
|
|
91
|
+
for dom_node in portal._dom_nodes:
|
|
92
|
+
if dom_node and dom_node.parentNode:
|
|
93
|
+
dom_node.parentNode.removeChild(dom_node)
|
|
94
|
+
portal._dom_nodes.clear()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def render_portal(portal: Portal, renderer: Any) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Render a portal's children into its container
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
portal: Portal to render
|
|
103
|
+
renderer: Renderer instance
|
|
104
|
+
"""
|
|
105
|
+
# Clear existing content
|
|
106
|
+
while portal.container.firstChild:
|
|
107
|
+
portal.container.removeChild(portal.container.firstChild)
|
|
108
|
+
|
|
109
|
+
# Render children
|
|
110
|
+
if isinstance(portal.children, VNode):
|
|
111
|
+
dom = renderer.create_dom(portal.children)
|
|
112
|
+
portal.container.appendChild(dom)
|
|
113
|
+
portal._dom_nodes.append(dom)
|
|
114
|
+
elif isinstance(portal.children, list):
|
|
115
|
+
for child in portal.children:
|
|
116
|
+
if isinstance(child, VNode):
|
|
117
|
+
dom = renderer.create_dom(child)
|
|
118
|
+
portal.container.appendChild(dom)
|
|
119
|
+
portal._dom_nodes.append(dom)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PortalManager:
|
|
123
|
+
"""
|
|
124
|
+
Manager for all portals in the application
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self._portals: Dict[int, Portal] = {}
|
|
129
|
+
|
|
130
|
+
def register(self, portal: Portal) -> int:
|
|
131
|
+
"""Register a portal"""
|
|
132
|
+
portal_id = id(portal)
|
|
133
|
+
self._portals[portal_id] = portal
|
|
134
|
+
return portal_id
|
|
135
|
+
|
|
136
|
+
def unregister(self, portal: Portal) -> None:
|
|
137
|
+
"""Unregister a portal"""
|
|
138
|
+
portal_id = id(portal)
|
|
139
|
+
if portal_id in self._portals:
|
|
140
|
+
del self._portals[portal_id]
|
|
141
|
+
|
|
142
|
+
def get_portal(self, portal_id: int) -> Optional[Portal]:
|
|
143
|
+
"""Get a portal by ID"""
|
|
144
|
+
return self._portals.get(portal_id)
|
|
145
|
+
|
|
146
|
+
def unmount_all(self) -> None:
|
|
147
|
+
"""Unmount all portals"""
|
|
148
|
+
for portal in self._portals.values():
|
|
149
|
+
unmount_portal(portal)
|
|
150
|
+
self._portals.clear()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Global portal manager
|
|
154
|
+
_portal_manager = PortalManager()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_portal_manager() -> PortalManager:
|
|
158
|
+
"""Get the global portal manager"""
|
|
159
|
+
return _portal_manager
|