pythonnative 0.4.0__py3-none-any.whl → 0.6.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 (52) hide show
  1. pythonnative/__init__.py +94 -66
  2. pythonnative/cli/pn.py +153 -24
  3. pythonnative/components.py +563 -0
  4. pythonnative/element.py +53 -0
  5. pythonnative/hooks.py +287 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +1334 -0
  13. pythonnative/page.py +320 -247
  14. pythonnative/reconciler.py +262 -0
  15. pythonnative/style.py +115 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/utils.py +21 -29
  20. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
  21. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
  22. pythonnative/activity_indicator_view.py +0 -71
  23. pythonnative/button.py +0 -113
  24. pythonnative/collection_view.py +0 -0
  25. pythonnative/date_picker.py +0 -76
  26. pythonnative/image_view.py +0 -78
  27. pythonnative/label.py +0 -133
  28. pythonnative/list_view.py +0 -76
  29. pythonnative/material_activity_indicator_view.py +0 -71
  30. pythonnative/material_bottom_navigation_view.py +0 -0
  31. pythonnative/material_button.py +0 -69
  32. pythonnative/material_date_picker.py +0 -87
  33. pythonnative/material_progress_view.py +0 -70
  34. pythonnative/material_search_bar.py +0 -69
  35. pythonnative/material_switch.py +0 -69
  36. pythonnative/material_time_picker.py +0 -76
  37. pythonnative/material_toolbar.py +0 -0
  38. pythonnative/picker_view.py +0 -69
  39. pythonnative/progress_view.py +0 -70
  40. pythonnative/scroll_view.py +0 -101
  41. pythonnative/search_bar.py +0 -69
  42. pythonnative/stack_view.py +0 -199
  43. pythonnative/switch.py +0 -68
  44. pythonnative/text_field.py +0 -132
  45. pythonnative/text_view.py +0 -135
  46. pythonnative/time_picker.py +0 -77
  47. pythonnative/view.py +0 -173
  48. pythonnative/web_view.py +0 -60
  49. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
  50. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
  51. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
  52. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,563 @@
1
+ """Built-in element-creating functions for declarative UI composition.
2
+
3
+ Each function returns an :class:`Element` describing a native UI widget.
4
+ These are pure data — no native views are created until the reconciler
5
+ mounts the element tree.
6
+
7
+ Layout properties (``width``, ``height``, ``flex``, ``margin``,
8
+ ``min_width``, ``max_width``, ``min_height``, ``max_height``,
9
+ ``align_self``) are supported by all components.
10
+ """
11
+
12
+ from typing import Any, Callable, Dict, List, Optional, Union
13
+
14
+ from .element import Element
15
+
16
+ # ======================================================================
17
+ # Shared helpers
18
+ # ======================================================================
19
+
20
+ PaddingValue = Union[int, float, Dict[str, Union[int, float]]]
21
+ MarginValue = Union[int, float, Dict[str, Union[int, float]]]
22
+
23
+
24
+ def _filter_none(**kwargs: Any) -> Dict[str, Any]:
25
+ """Return *kwargs* with ``None``-valued entries removed."""
26
+ return {k: v for k, v in kwargs.items() if v is not None}
27
+
28
+
29
+ def _layout_props(
30
+ width: Optional[float] = None,
31
+ height: Optional[float] = None,
32
+ flex: Optional[float] = None,
33
+ margin: Optional[MarginValue] = None,
34
+ min_width: Optional[float] = None,
35
+ max_width: Optional[float] = None,
36
+ min_height: Optional[float] = None,
37
+ max_height: Optional[float] = None,
38
+ align_self: Optional[str] = None,
39
+ ) -> Dict[str, Any]:
40
+ """Collect common layout props into a dict (excluding Nones)."""
41
+ return _filter_none(
42
+ width=width,
43
+ height=height,
44
+ flex=flex,
45
+ margin=margin,
46
+ min_width=min_width,
47
+ max_width=max_width,
48
+ min_height=min_height,
49
+ max_height=max_height,
50
+ align_self=align_self,
51
+ )
52
+
53
+
54
+ # ======================================================================
55
+ # Leaf components
56
+ # ======================================================================
57
+
58
+
59
+ def Text(
60
+ text: str = "",
61
+ *,
62
+ font_size: Optional[float] = None,
63
+ color: Optional[str] = None,
64
+ bold: bool = False,
65
+ text_align: Optional[str] = None,
66
+ background_color: Optional[str] = None,
67
+ max_lines: Optional[int] = None,
68
+ width: Optional[float] = None,
69
+ height: Optional[float] = None,
70
+ flex: Optional[float] = None,
71
+ margin: Optional[MarginValue] = None,
72
+ min_width: Optional[float] = None,
73
+ max_width: Optional[float] = None,
74
+ min_height: Optional[float] = None,
75
+ max_height: Optional[float] = None,
76
+ align_self: Optional[str] = None,
77
+ key: Optional[str] = None,
78
+ ) -> Element:
79
+ """Display text."""
80
+ props = _filter_none(
81
+ text=text,
82
+ font_size=font_size,
83
+ color=color,
84
+ bold=bold or None,
85
+ text_align=text_align,
86
+ background_color=background_color,
87
+ max_lines=max_lines,
88
+ )
89
+ props.update(
90
+ _layout_props(
91
+ width=width,
92
+ height=height,
93
+ flex=flex,
94
+ margin=margin,
95
+ min_width=min_width,
96
+ max_width=max_width,
97
+ min_height=min_height,
98
+ max_height=max_height,
99
+ align_self=align_self,
100
+ )
101
+ )
102
+ return Element("Text", props, [], key=key)
103
+
104
+
105
+ def Button(
106
+ title: str = "",
107
+ *,
108
+ on_click: Optional[Callable[[], None]] = None,
109
+ color: Optional[str] = None,
110
+ background_color: Optional[str] = None,
111
+ font_size: Optional[float] = None,
112
+ enabled: bool = True,
113
+ width: Optional[float] = None,
114
+ height: Optional[float] = None,
115
+ flex: Optional[float] = None,
116
+ margin: Optional[MarginValue] = None,
117
+ min_width: Optional[float] = None,
118
+ max_width: Optional[float] = None,
119
+ min_height: Optional[float] = None,
120
+ max_height: Optional[float] = None,
121
+ align_self: Optional[str] = None,
122
+ key: Optional[str] = None,
123
+ ) -> Element:
124
+ """Create a tappable button."""
125
+ props: Dict[str, Any] = {"title": title}
126
+ if on_click is not None:
127
+ props["on_click"] = on_click
128
+ if color is not None:
129
+ props["color"] = color
130
+ if background_color is not None:
131
+ props["background_color"] = background_color
132
+ if font_size is not None:
133
+ props["font_size"] = font_size
134
+ if not enabled:
135
+ props["enabled"] = False
136
+ props.update(
137
+ _layout_props(
138
+ width=width,
139
+ height=height,
140
+ flex=flex,
141
+ margin=margin,
142
+ min_width=min_width,
143
+ max_width=max_width,
144
+ min_height=min_height,
145
+ max_height=max_height,
146
+ align_self=align_self,
147
+ )
148
+ )
149
+ return Element("Button", props, [], key=key)
150
+
151
+
152
+ def TextInput(
153
+ *,
154
+ value: str = "",
155
+ placeholder: str = "",
156
+ on_change: Optional[Callable[[str], None]] = None,
157
+ secure: bool = False,
158
+ font_size: Optional[float] = None,
159
+ color: Optional[str] = None,
160
+ background_color: Optional[str] = None,
161
+ width: Optional[float] = None,
162
+ height: Optional[float] = None,
163
+ flex: Optional[float] = None,
164
+ margin: Optional[MarginValue] = None,
165
+ min_width: Optional[float] = None,
166
+ max_width: Optional[float] = None,
167
+ min_height: Optional[float] = None,
168
+ max_height: Optional[float] = None,
169
+ align_self: Optional[str] = None,
170
+ key: Optional[str] = None,
171
+ ) -> Element:
172
+ """Create a single-line text entry field."""
173
+ props: Dict[str, Any] = {"value": value}
174
+ if placeholder:
175
+ props["placeholder"] = placeholder
176
+ if on_change is not None:
177
+ props["on_change"] = on_change
178
+ if secure:
179
+ props["secure"] = True
180
+ if font_size is not None:
181
+ props["font_size"] = font_size
182
+ if color is not None:
183
+ props["color"] = color
184
+ if background_color is not None:
185
+ props["background_color"] = background_color
186
+ props.update(
187
+ _layout_props(
188
+ width=width,
189
+ height=height,
190
+ flex=flex,
191
+ margin=margin,
192
+ min_width=min_width,
193
+ max_width=max_width,
194
+ min_height=min_height,
195
+ max_height=max_height,
196
+ align_self=align_self,
197
+ )
198
+ )
199
+ return Element("TextInput", props, [], key=key)
200
+
201
+
202
+ def Image(
203
+ source: str = "",
204
+ *,
205
+ width: Optional[float] = None,
206
+ height: Optional[float] = None,
207
+ scale_type: Optional[str] = None,
208
+ background_color: Optional[str] = None,
209
+ flex: Optional[float] = None,
210
+ margin: Optional[MarginValue] = None,
211
+ min_width: Optional[float] = None,
212
+ max_width: Optional[float] = None,
213
+ min_height: Optional[float] = None,
214
+ max_height: Optional[float] = None,
215
+ align_self: Optional[str] = None,
216
+ key: Optional[str] = None,
217
+ ) -> Element:
218
+ """Display an image from a resource path or URL."""
219
+ props = _filter_none(
220
+ source=source or None,
221
+ width=width,
222
+ height=height,
223
+ scale_type=scale_type,
224
+ background_color=background_color,
225
+ )
226
+ props.update(
227
+ _layout_props(
228
+ flex=flex,
229
+ margin=margin,
230
+ min_width=min_width,
231
+ max_width=max_width,
232
+ min_height=min_height,
233
+ max_height=max_height,
234
+ align_self=align_self,
235
+ )
236
+ )
237
+ return Element("Image", props, [], key=key)
238
+
239
+
240
+ def Switch(
241
+ *,
242
+ value: bool = False,
243
+ on_change: Optional[Callable[[bool], None]] = None,
244
+ width: Optional[float] = None,
245
+ height: Optional[float] = None,
246
+ flex: Optional[float] = None,
247
+ margin: Optional[MarginValue] = None,
248
+ align_self: Optional[str] = None,
249
+ key: Optional[str] = None,
250
+ ) -> Element:
251
+ """Create a toggle switch."""
252
+ props: Dict[str, Any] = {"value": value}
253
+ if on_change is not None:
254
+ props["on_change"] = on_change
255
+ props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
256
+ return Element("Switch", props, [], key=key)
257
+
258
+
259
+ def ProgressBar(
260
+ *,
261
+ value: float = 0.0,
262
+ background_color: Optional[str] = None,
263
+ width: Optional[float] = None,
264
+ height: Optional[float] = None,
265
+ flex: Optional[float] = None,
266
+ margin: Optional[MarginValue] = None,
267
+ align_self: Optional[str] = None,
268
+ key: Optional[str] = None,
269
+ ) -> Element:
270
+ """Show determinate progress (0.0 – 1.0)."""
271
+ props = _filter_none(value=value, background_color=background_color)
272
+ props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
273
+ return Element("ProgressBar", props, [], key=key)
274
+
275
+
276
+ def ActivityIndicator(
277
+ *,
278
+ animating: bool = True,
279
+ width: Optional[float] = None,
280
+ height: Optional[float] = None,
281
+ margin: Optional[MarginValue] = None,
282
+ align_self: Optional[str] = None,
283
+ key: Optional[str] = None,
284
+ ) -> Element:
285
+ """Show an indeterminate loading spinner."""
286
+ props: Dict[str, Any] = {"animating": animating}
287
+ props.update(_layout_props(width=width, height=height, margin=margin, align_self=align_self))
288
+ return Element("ActivityIndicator", props, [], key=key)
289
+
290
+
291
+ def WebView(
292
+ *,
293
+ url: str = "",
294
+ width: Optional[float] = None,
295
+ height: Optional[float] = None,
296
+ flex: Optional[float] = None,
297
+ margin: Optional[MarginValue] = None,
298
+ align_self: Optional[str] = None,
299
+ key: Optional[str] = None,
300
+ ) -> Element:
301
+ """Embed web content."""
302
+ props: Dict[str, Any] = {}
303
+ if url:
304
+ props["url"] = url
305
+ props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
306
+ return Element("WebView", props, [], key=key)
307
+
308
+
309
+ def Spacer(
310
+ *,
311
+ size: Optional[float] = None,
312
+ flex: Optional[float] = None,
313
+ key: Optional[str] = None,
314
+ ) -> Element:
315
+ """Insert empty space with an optional fixed size."""
316
+ props = _filter_none(size=size, flex=flex)
317
+ return Element("Spacer", props, [], key=key)
318
+
319
+
320
+ # ======================================================================
321
+ # Container components
322
+ # ======================================================================
323
+
324
+
325
+ def Column(
326
+ *children: Element,
327
+ spacing: float = 0,
328
+ padding: Optional[PaddingValue] = None,
329
+ alignment: Optional[str] = None,
330
+ background_color: Optional[str] = None,
331
+ width: Optional[float] = None,
332
+ height: Optional[float] = None,
333
+ flex: Optional[float] = None,
334
+ margin: Optional[MarginValue] = None,
335
+ min_width: Optional[float] = None,
336
+ max_width: Optional[float] = None,
337
+ min_height: Optional[float] = None,
338
+ max_height: Optional[float] = None,
339
+ align_self: Optional[str] = None,
340
+ key: Optional[str] = None,
341
+ ) -> Element:
342
+ """Arrange children vertically."""
343
+ props = _filter_none(
344
+ spacing=spacing or None,
345
+ padding=padding,
346
+ alignment=alignment,
347
+ background_color=background_color,
348
+ )
349
+ props.update(
350
+ _layout_props(
351
+ width=width,
352
+ height=height,
353
+ flex=flex,
354
+ margin=margin,
355
+ min_width=min_width,
356
+ max_width=max_width,
357
+ min_height=min_height,
358
+ max_height=max_height,
359
+ align_self=align_self,
360
+ )
361
+ )
362
+ return Element("Column", props, list(children), key=key)
363
+
364
+
365
+ def Row(
366
+ *children: Element,
367
+ spacing: float = 0,
368
+ padding: Optional[PaddingValue] = None,
369
+ alignment: Optional[str] = None,
370
+ background_color: Optional[str] = None,
371
+ width: Optional[float] = None,
372
+ height: Optional[float] = None,
373
+ flex: Optional[float] = None,
374
+ margin: Optional[MarginValue] = None,
375
+ min_width: Optional[float] = None,
376
+ max_width: Optional[float] = None,
377
+ min_height: Optional[float] = None,
378
+ max_height: Optional[float] = None,
379
+ align_self: Optional[str] = None,
380
+ key: Optional[str] = None,
381
+ ) -> Element:
382
+ """Arrange children horizontally."""
383
+ props = _filter_none(
384
+ spacing=spacing or None,
385
+ padding=padding,
386
+ alignment=alignment,
387
+ background_color=background_color,
388
+ )
389
+ props.update(
390
+ _layout_props(
391
+ width=width,
392
+ height=height,
393
+ flex=flex,
394
+ margin=margin,
395
+ min_width=min_width,
396
+ max_width=max_width,
397
+ min_height=min_height,
398
+ max_height=max_height,
399
+ align_self=align_self,
400
+ )
401
+ )
402
+ return Element("Row", props, list(children), key=key)
403
+
404
+
405
+ def ScrollView(
406
+ child: Optional[Element] = None,
407
+ *,
408
+ background_color: Optional[str] = None,
409
+ width: Optional[float] = None,
410
+ height: Optional[float] = None,
411
+ flex: Optional[float] = None,
412
+ margin: Optional[MarginValue] = None,
413
+ align_self: Optional[str] = None,
414
+ key: Optional[str] = None,
415
+ ) -> Element:
416
+ """Wrap a single child in a scrollable container."""
417
+ children = [child] if child is not None else []
418
+ props = _filter_none(background_color=background_color)
419
+ props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
420
+ return Element("ScrollView", props, children, key=key)
421
+
422
+
423
+ def View(
424
+ *children: Element,
425
+ background_color: Optional[str] = None,
426
+ padding: Optional[PaddingValue] = None,
427
+ width: Optional[float] = None,
428
+ height: Optional[float] = None,
429
+ flex: Optional[float] = None,
430
+ margin: Optional[MarginValue] = None,
431
+ min_width: Optional[float] = None,
432
+ max_width: Optional[float] = None,
433
+ min_height: Optional[float] = None,
434
+ max_height: Optional[float] = None,
435
+ align_self: Optional[str] = None,
436
+ key: Optional[str] = None,
437
+ ) -> Element:
438
+ """Generic container view (``UIView`` / ``android.view.View``)."""
439
+ props = _filter_none(
440
+ background_color=background_color,
441
+ padding=padding,
442
+ )
443
+ props.update(
444
+ _layout_props(
445
+ width=width,
446
+ height=height,
447
+ flex=flex,
448
+ margin=margin,
449
+ min_width=min_width,
450
+ max_width=max_width,
451
+ min_height=min_height,
452
+ max_height=max_height,
453
+ align_self=align_self,
454
+ )
455
+ )
456
+ return Element("View", props, list(children), key=key)
457
+
458
+
459
+ def SafeAreaView(
460
+ *children: Element,
461
+ background_color: Optional[str] = None,
462
+ padding: Optional[PaddingValue] = None,
463
+ key: Optional[str] = None,
464
+ ) -> Element:
465
+ """Container that respects safe area insets (notch, status bar)."""
466
+ props = _filter_none(background_color=background_color, padding=padding)
467
+ return Element("SafeAreaView", props, list(children), key=key)
468
+
469
+
470
+ def Modal(
471
+ *children: Element,
472
+ visible: bool = False,
473
+ on_dismiss: Optional[Callable[[], None]] = None,
474
+ title: Optional[str] = None,
475
+ background_color: Optional[str] = None,
476
+ key: Optional[str] = None,
477
+ ) -> Element:
478
+ """Overlay modal dialog.
479
+
480
+ The modal is shown when ``visible=True`` and hidden when ``False``.
481
+ """
482
+ props: Dict[str, Any] = {"visible": visible}
483
+ if on_dismiss is not None:
484
+ props["on_dismiss"] = on_dismiss
485
+ if title is not None:
486
+ props["title"] = title
487
+ if background_color is not None:
488
+ props["background_color"] = background_color
489
+ return Element("Modal", props, list(children), key=key)
490
+
491
+
492
+ def Slider(
493
+ *,
494
+ value: float = 0.0,
495
+ min_value: float = 0.0,
496
+ max_value: float = 1.0,
497
+ on_change: Optional[Callable[[float], None]] = None,
498
+ width: Optional[float] = None,
499
+ margin: Optional[MarginValue] = None,
500
+ align_self: Optional[str] = None,
501
+ key: Optional[str] = None,
502
+ ) -> Element:
503
+ """Continuous value slider."""
504
+ props: Dict[str, Any] = {
505
+ "value": value,
506
+ "min_value": min_value,
507
+ "max_value": max_value,
508
+ }
509
+ if on_change is not None:
510
+ props["on_change"] = on_change
511
+ props.update(_layout_props(width=width, margin=margin, align_self=align_self))
512
+ return Element("Slider", props, [], key=key)
513
+
514
+
515
+ def Pressable(
516
+ child: Optional[Element] = None,
517
+ *,
518
+ on_press: Optional[Callable[[], None]] = None,
519
+ on_long_press: Optional[Callable[[], None]] = None,
520
+ key: Optional[str] = None,
521
+ ) -> Element:
522
+ """Wrapper that adds press handling to any child element."""
523
+ props: Dict[str, Any] = {}
524
+ if on_press is not None:
525
+ props["on_press"] = on_press
526
+ if on_long_press is not None:
527
+ props["on_long_press"] = on_long_press
528
+ children = [child] if child is not None else []
529
+ return Element("Pressable", props, children, key=key)
530
+
531
+
532
+ def FlatList(
533
+ *,
534
+ data: Optional[List[Any]] = None,
535
+ render_item: Optional[Callable[[Any, int], Element]] = None,
536
+ key_extractor: Optional[Callable[[Any, int], str]] = None,
537
+ separator_height: float = 0,
538
+ background_color: Optional[str] = None,
539
+ width: Optional[float] = None,
540
+ height: Optional[float] = None,
541
+ flex: Optional[float] = None,
542
+ margin: Optional[MarginValue] = None,
543
+ align_self: Optional[str] = None,
544
+ key: Optional[str] = None,
545
+ ) -> Element:
546
+ """Scrollable list that renders items from *data* using *render_item*.
547
+
548
+ Each item is rendered by calling ``render_item(item, index)``. If
549
+ ``key_extractor`` is provided, it is called as ``key_extractor(item, index)``
550
+ to produce a stable key for each child element. This enables the
551
+ reconciler to preserve widget state across data changes.
552
+ """
553
+ items: List[Element] = []
554
+ for i, item in enumerate(data or []):
555
+ el = render_item(item, i) if render_item else Text(str(item))
556
+ if key_extractor is not None:
557
+ el = Element(el.type, el.props, el.children, key=key_extractor(item, i))
558
+ items.append(el)
559
+
560
+ inner = Column(*items, spacing=separator_height)
561
+ sv_props = _filter_none(background_color=background_color)
562
+ sv_props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
563
+ return Element("ScrollView", sv_props, [inner], key=key)
@@ -0,0 +1,53 @@
1
+ """Lightweight element descriptors for the virtual view tree.
2
+
3
+ An Element is an immutable description of a UI node — analogous to a React
4
+ element. It captures a type (name string **or** component function), a props
5
+ dictionary, and an ordered list of children without creating any native
6
+ platform objects. The reconciler consumes these trees to determine what
7
+ native views must be created, updated, or removed.
8
+ """
9
+
10
+ from typing import Any, Dict, List, Optional, Union
11
+
12
+
13
+ class Element:
14
+ """Immutable description of a single UI node.
15
+
16
+ ``type_name`` may be a *string* (e.g. ``"Text"``) for built-in native
17
+ elements or a *callable* for function components decorated with
18
+ :func:`~pythonnative.hooks.component`.
19
+ """
20
+
21
+ __slots__ = ("type", "props", "children", "key")
22
+
23
+ def __init__(
24
+ self,
25
+ type_name: Union[str, Any],
26
+ props: Dict[str, Any],
27
+ children: List["Element"],
28
+ key: Optional[str] = None,
29
+ ) -> None:
30
+ self.type = type_name
31
+ self.props = props
32
+ self.children = children
33
+ self.key = key
34
+
35
+ def __repr__(self) -> str:
36
+ t = self.type if isinstance(self.type, str) else getattr(self.type, "__name__", repr(self.type))
37
+ return f"Element({t!r}, props={set(self.props)}, children={len(self.children)})"
38
+
39
+ def __eq__(self, other: object) -> bool:
40
+ if not isinstance(other, Element):
41
+ return NotImplemented
42
+ return (
43
+ self.type == other.type
44
+ and self.props == other.props
45
+ and self.children == other.children
46
+ and self.key == other.key
47
+ )
48
+
49
+ def __ne__(self, other: object) -> bool:
50
+ result = self.__eq__(other)
51
+ if result is NotImplemented:
52
+ return result
53
+ return not result