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
pyreact/core/hooks.py ADDED
@@ -0,0 +1,550 @@
1
+ """
2
+ Hooks Module
3
+ ============
4
+
5
+ This module implements React-style hooks for functional components.
6
+ Hooks allow you to use state and other React features without writing a class.
7
+ """
8
+
9
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
10
+ from functools import wraps
11
+ import uuid
12
+
13
+ # Global context for hooks
14
+ _current_component: Optional[Any] = None
15
+ _hook_index: int = 0
16
+
17
+
18
+ def _get_current_component() -> Any:
19
+ """Get the current component context"""
20
+ global _current_component
21
+ if _current_component is None:
22
+ raise RuntimeError('Hooks can only be called inside a component')
23
+ return _current_component
24
+
25
+
26
+ def _set_current_component(component: Any) -> None:
27
+ """Set the current component context"""
28
+ global _current_component, _hook_index
29
+ _current_component = component
30
+ _hook_index = 0
31
+
32
+
33
+ def _reset_hook_index() -> None:
34
+ """Reset hook index for new render"""
35
+ global _hook_index
36
+ _hook_index = 0
37
+
38
+
39
+ def use_state(
40
+ initial_value: Union[Any, Callable[[], Any]]
41
+ ) -> Tuple[Any, Callable[[Any], None]]:
42
+ """
43
+ Hook for state management
44
+
45
+ Returns a stateful value and a function to update it.
46
+
47
+ Args:
48
+ initial_value: Initial state value or function that returns initial value
49
+
50
+ Returns:
51
+ tuple: (current_value, setter_function)
52
+
53
+ Example:
54
+ def Counter(props):
55
+ count, set_count = use_state(0)
56
+ return h('div', None,
57
+ h('span', None, f"Count: {count}"),
58
+ h('button', {'onClick': lambda _: set_count(count + 1)}, '+')
59
+ )
60
+ """
61
+ global _hook_index
62
+ component = _get_current_component()
63
+ hook_index = _hook_index
64
+
65
+ # Initialize hook if needed
66
+ if hook_index >= len(component._hooks):
67
+ initial = initial_value() if callable(initial_value) else initial_value
68
+ component._hooks.append({
69
+ 'value': initial,
70
+ 'type': 'state'
71
+ })
72
+
73
+ hook = component._hooks[hook_index]
74
+ _hook_index += 1
75
+
76
+ def set_state(new_value: Union[Any, Callable[[Any], Any]]) -> None:
77
+ """Update state value"""
78
+ if callable(new_value):
79
+ hook['value'] = new_value(hook['value'])
80
+ else:
81
+ hook['value'] = new_value
82
+
83
+ # Trigger re-render
84
+ if hasattr(component, '_schedule_update'):
85
+ component._schedule_update()
86
+
87
+ return hook['value'], set_state
88
+
89
+
90
+ def use_reducer(
91
+ reducer: Callable[[Any, Any], Any],
92
+ initial_state: Any,
93
+ init: Optional[Callable[[Any], Any]] = None
94
+ ) -> Tuple[Any, Callable[[Any], None]]:
95
+ """
96
+ Hook for complex state management with reducer
97
+
98
+ Similar to Redux pattern.
99
+
100
+ Args:
101
+ reducer: Function (state, action) -> new_state
102
+ initial_state: Initial state value
103
+ init: Optional function to compute initial state lazily
104
+
105
+ Returns:
106
+ tuple: (state, dispatch_function)
107
+
108
+ Example:
109
+ def reducer(state, action):
110
+ if action['type'] == 'INCREMENT':
111
+ return {'count': state['count'] + 1}
112
+ elif action['type'] == 'DECREMENT':
113
+ return {'count': state['count'] - 1}
114
+ return state
115
+
116
+ def Counter(props):
117
+ state, dispatch = use_reducer(reducer, {'count': 0})
118
+ return h('div', None,
119
+ h('span', None, f"Count: {state['count']}"),
120
+ h('button', {'onClick': lambda _: dispatch({'type': 'INCREMENT'})}, '+')
121
+ )
122
+ """
123
+ global _hook_index
124
+ component = _get_current_component()
125
+ hook_index = _hook_index
126
+
127
+ # Initialize hook if needed
128
+ if hook_index >= len(component._hooks):
129
+ initial = init(initial_state) if init else initial_state
130
+ component._hooks.append({
131
+ 'value': initial,
132
+ 'type': 'reducer',
133
+ 'reducer': reducer
134
+ })
135
+
136
+ hook = component._hooks[hook_index]
137
+ _hook_index += 1
138
+
139
+ def dispatch(action: Any) -> None:
140
+ """Dispatch an action to the reducer"""
141
+ hook['value'] = hook['reducer'](hook['value'], action)
142
+ if hasattr(component, '_schedule_update'):
143
+ component._schedule_update()
144
+
145
+ return hook['value'], dispatch
146
+
147
+
148
+ def use_effect(
149
+ setup: Callable[[], Optional[Callable[[], None]]],
150
+ dependencies: Optional[List[Any]] = None
151
+ ) -> None:
152
+ """
153
+ Hook for side effects
154
+
155
+ Runs after render. Return a cleanup function to run before next effect or unmount.
156
+
157
+ Args:
158
+ setup: Function that returns cleanup function or None
159
+ dependencies: List of values that trigger effect when changed
160
+
161
+ Example:
162
+ def Timer(props):
163
+ seconds, set_seconds = use_state(0)
164
+
165
+ @use_effect([])
166
+ def setup_timer():
167
+ def tick():
168
+ set_seconds(lambda s: s + 1)
169
+ interval_id = setInterval(tick, 1000)
170
+ return lambda: clearInterval(interval_id)
171
+
172
+ return h('div', None, f"Seconds: {seconds}")
173
+ """
174
+ global _hook_index
175
+ component = _get_current_component()
176
+ hook_index = _hook_index
177
+
178
+ # Initialize hook if needed
179
+ if hook_index >= len(component._hooks):
180
+ component._hooks.append({
181
+ 'type': 'effect',
182
+ 'cleanup': None,
183
+ 'deps': None
184
+ })
185
+
186
+ hook = component._hooks[hook_index]
187
+ _hook_index += 1
188
+
189
+ # Check if dependencies changed
190
+ deps_changed = (
191
+ hook['deps'] is None or
192
+ dependencies is None or
193
+ _deps_changed(hook['deps'], dependencies)
194
+ )
195
+
196
+ if deps_changed:
197
+ # Run cleanup from previous effect
198
+ if hook['cleanup']:
199
+ try:
200
+ hook['cleanup']()
201
+ except Exception:
202
+ pass
203
+
204
+ # Run new setup
205
+ hook['cleanup'] = setup()
206
+ hook['deps'] = dependencies.copy() if dependencies else None
207
+
208
+
209
+ def use_layout_effect(
210
+ setup: Callable[[], Optional[Callable[[], None]]],
211
+ dependencies: Optional[List[Any]] = None
212
+ ) -> None:
213
+ """
214
+ Hook for synchronous effects after DOM mutations
215
+
216
+ Similar to use_effect but fires synchronously after all DOM mutations.
217
+ Use for DOM measurements or mutations.
218
+
219
+ Args:
220
+ setup: Function that returns cleanup function or None
221
+ dependencies: List of values that trigger effect when changed
222
+ """
223
+ # In this implementation, use_layout_effect behaves the same as use_effect
224
+ # A real implementation would schedule this to run synchronously after paint
225
+ use_effect(setup, dependencies)
226
+
227
+
228
+ def use_context(context: Any) -> Any:
229
+ """
230
+ Hook to consume context value
231
+
232
+ Args:
233
+ context: Context object created by create_context()
234
+
235
+ Returns:
236
+ Current context value
237
+
238
+ Example:
239
+ def Button(props):
240
+ theme = use_context(ThemeContext)
241
+ return h('button', {'className': f"btn-{theme}"}, 'Click')
242
+ """
243
+ global _hook_index
244
+ component = _get_current_component()
245
+ hook_index = _hook_index
246
+
247
+ # Initialize hook if needed
248
+ if hook_index >= len(component._hooks):
249
+ component._hooks.append({
250
+ 'type': 'context',
251
+ 'context': context,
252
+ 'value': context._default_value
253
+ })
254
+
255
+ hook = component._hooks[hook_index]
256
+ _hook_index += 1
257
+
258
+ # Get current context value
259
+ return hook['context']._get_value() if hasattr(hook['context'], '_get_value') else hook['value']
260
+
261
+
262
+ def use_ref(initial_value: Any = None) -> 'Ref':
263
+ """
264
+ Hook for mutable reference that persists across renders
265
+
266
+ Changing ref.current does NOT trigger re-render.
267
+
268
+ Args:
269
+ initial_value: Initial value for ref.current
270
+
271
+ Returns:
272
+ Ref object with .current property
273
+
274
+ Example:
275
+ def TextInput(props):
276
+ input_ref = use_ref(None)
277
+
278
+ def focus():
279
+ input_ref.current.focus()
280
+
281
+ return h('input', {'ref': input_ref, 'type': 'text'})
282
+ """
283
+ global _hook_index
284
+ component = _get_current_component()
285
+ hook_index = _hook_index
286
+
287
+ # Initialize hook if needed
288
+ if hook_index >= len(component._hooks):
289
+ ref = Ref()
290
+ ref.current = initial_value
291
+ component._hooks.append({
292
+ 'type': 'ref',
293
+ 'ref': ref
294
+ })
295
+
296
+ hook = component._hooks[hook_index]
297
+ _hook_index += 1
298
+
299
+ return hook['ref']
300
+
301
+
302
+ def use_memo(factory: Callable[[], Any], dependencies: List[Any]) -> Any:
303
+ """
304
+ Hook to memoize expensive computations
305
+
306
+ Only recomputes when dependencies change.
307
+
308
+ Args:
309
+ factory: Function that computes the value
310
+ dependencies: List of values that trigger recomputation
311
+
312
+ Returns:
313
+ Memoized value
314
+
315
+ Example:
316
+ def ExpensiveComponent(props):
317
+ result = use_memo(
318
+ lambda: expensive_computation(props['data']),
319
+ [props['data']]
320
+ )
321
+ return h('div', None, result)
322
+ """
323
+ global _hook_index
324
+ component = _get_current_component()
325
+ hook_index = _hook_index
326
+
327
+ # Initialize hook if needed
328
+ if hook_index >= len(component._hooks):
329
+ component._hooks.append({
330
+ 'type': 'memo',
331
+ 'value': factory(),
332
+ 'deps': dependencies.copy()
333
+ })
334
+
335
+ hook = component._hooks[hook_index]
336
+ _hook_index += 1
337
+
338
+ # Check if dependencies changed
339
+ if _deps_changed(hook['deps'], dependencies):
340
+ hook['value'] = factory()
341
+ hook['deps'] = dependencies.copy()
342
+
343
+ return hook['value']
344
+
345
+
346
+ def use_callback(callback: Callable, dependencies: List[Any]) -> Callable:
347
+ """
348
+ Hook to memoize callbacks
349
+
350
+ Returns a memoized callback that only changes when dependencies change.
351
+ Useful for passing callbacks to optimized child components.
352
+
353
+ Args:
354
+ callback: Function to memoize
355
+ dependencies: List of values that trigger callback update
356
+
357
+ Returns:
358
+ Memoized callback
359
+
360
+ Example:
361
+ def Parent(props):
362
+ def handle_click(item):
363
+ print(f"Clicked {item}")
364
+
365
+ memoized_click = use_callback(handle_click, [])
366
+ return h(Child, {'onClick': memoized_click})
367
+ """
368
+ return use_memo(lambda: callback, dependencies)
369
+
370
+
371
+ def use_imperative_handle(
372
+ ref: 'Ref',
373
+ create_handle: Callable[[], Any],
374
+ dependencies: Optional[List[Any]] = None
375
+ ) -> None:
376
+ """
377
+ Hook to customize the instance value exposed to parent components
378
+
379
+ Args:
380
+ ref: Ref passed from parent
381
+ create_handle: Function that returns object to expose
382
+ dependencies: List of values that trigger handle update
383
+
384
+ Example:
385
+ def FancyInput(props, ref):
386
+ input_ref = use_ref(None)
387
+
388
+ use_imperative_handle(ref, lambda: {
389
+ 'focus': lambda: input_ref.current.focus(),
390
+ 'scrollIntoView': lambda: input_ref.current.scrollIntoView()
391
+ }, [])
392
+
393
+ return h('input', {'ref': input_ref})
394
+ """
395
+ def effect():
396
+ if ref:
397
+ ref.current = create_handle()
398
+ return lambda: None
399
+
400
+ use_effect(effect, dependencies)
401
+
402
+
403
+ def use_debug_value(value: Any, formatter: Optional[Callable[[Any], str]] = None) -> None:
404
+ """
405
+ Hook to display a label in DevTools
406
+
407
+ Args:
408
+ value: Value to display
409
+ formatter: Optional function to format the value
410
+ """
411
+ # This is a no-op in production
412
+ # DevTools would read this value
413
+ pass
414
+
415
+
416
+ def use_id() -> str:
417
+ """
418
+ Hook to generate unique IDs for accessibility
419
+
420
+ Returns:
421
+ Unique ID string
422
+
423
+ Example:
424
+ def FormField(props):
425
+ id = use_id()
426
+ return h('div', None,
427
+ h('label', {'htmlFor': id}, props['label']),
428
+ h('input', {'id': id, 'type': 'text'})
429
+ )
430
+ """
431
+ global _hook_index
432
+ component = _get_current_component()
433
+ hook_index = _hook_index
434
+
435
+ # Initialize hook if needed
436
+ if hook_index >= len(component._hooks):
437
+ component._hooks.append({
438
+ 'type': 'id',
439
+ 'value': f"pyreact-{uuid.uuid4().hex[:8]}"
440
+ })
441
+
442
+ hook = component._hooks[hook_index]
443
+ _hook_index += 1
444
+
445
+ return hook['value']
446
+
447
+
448
+ def use_transition() -> Tuple[Callable[[Callable], None], bool]:
449
+ """
450
+ Hook for non-blocking UI updates
451
+
452
+ Returns:
453
+ tuple: (start_transition function, is_pending boolean)
454
+
455
+ Example:
456
+ def SearchResults(props):
457
+ is_pending, start_transition = use_transition()
458
+
459
+ def handle_change(e):
460
+ start_transition(lambda: set_query(e.target.value))
461
+
462
+ return h('div', None,
463
+ h('input', {'onChange': handle_change}),
464
+ is_pending and h('span', None, 'Loading...')
465
+ )
466
+ """
467
+ global _hook_index
468
+ component = _get_current_component()
469
+ hook_index = _hook_index
470
+
471
+ # Initialize hook if needed
472
+ if hook_index >= len(component._hooks):
473
+ component._hooks.append({
474
+ 'type': 'transition',
475
+ 'is_pending': False
476
+ })
477
+
478
+ hook = component._hooks[hook_index]
479
+ _hook_index += 1
480
+
481
+ def start_transition(callback: Callable) -> None:
482
+ """Start a non-blocking transition"""
483
+ hook['is_pending'] = True
484
+ if hasattr(component, '_schedule_update'):
485
+ component._schedule_update()
486
+
487
+ # Execute callback asynchronously
488
+ try:
489
+ callback()
490
+ finally:
491
+ hook['is_pending'] = False
492
+ if hasattr(component, '_schedule_update'):
493
+ component._schedule_update()
494
+
495
+ return start_transition, hook['is_pending']
496
+
497
+
498
+ def use_deferred_value(value: Any) -> Any:
499
+ """
500
+ Hook to defer a value for better performance
501
+
502
+ Returns a deferred version that lags behind the current value.
503
+
504
+ Args:
505
+ value: Value to defer
506
+
507
+ Returns:
508
+ Deferred value
509
+ """
510
+ global _hook_index
511
+ component = _get_current_component()
512
+ hook_index = _hook_index
513
+
514
+ # Initialize hook if needed
515
+ if hook_index >= len(component._hooks):
516
+ component._hooks.append({
517
+ 'type': 'deferred',
518
+ 'value': value,
519
+ 'deferred_value': value
520
+ })
521
+
522
+ hook = component._hooks[hook_index]
523
+ _hook_index += 1
524
+
525
+ # Update deferred value asynchronously
526
+ if hook['value'] != value:
527
+ hook['value'] = value
528
+ # In a real implementation, this would be scheduled
529
+ hook['deferred_value'] = value
530
+
531
+ return hook['deferred_value']
532
+
533
+
534
+ class Ref:
535
+ """Reference object for use_ref"""
536
+
537
+ def __init__(self):
538
+ self.current: Any = None
539
+
540
+ def __repr__(self) -> str:
541
+ return f"Ref(current={self.current!r})"
542
+
543
+
544
+ def _deps_changed(old_deps: Optional[List[Any]], new_deps: List[Any]) -> bool:
545
+ """Check if dependencies have changed"""
546
+ if old_deps is None:
547
+ return True
548
+ if len(old_deps) != len(new_deps):
549
+ return True
550
+ return any(a != b for a, b in zip(old_deps, new_deps))