pythonnative 0.22.1__py3-none-any.whl → 0.24.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 (35) hide show
  1. pythonnative/__init__.py +43 -50
  2. pythonnative/animated.py +1 -1
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +3 -3
  5. pythonnative/components.py +873 -466
  6. pythonnative/diagnostics.py +214 -0
  7. pythonnative/element.py +5 -2
  8. pythonnative/events.py +13 -8
  9. pythonnative/hooks.py +454 -49
  10. pythonnative/hot_reload.py +9 -1
  11. pythonnative/images.py +196 -0
  12. pythonnative/mutations.py +3 -12
  13. pythonnative/native_views/__init__.py +1 -7
  14. pythonnative/native_views/android.py +651 -46
  15. pythonnative/native_views/desktop.py +116 -20
  16. pythonnative/native_views/ios.py +974 -52
  17. pythonnative/navigation.py +41 -17
  18. pythonnative/preview.py +74 -7
  19. pythonnative/project/config.py +1 -9
  20. pythonnative/reconciler.py +863 -441
  21. pythonnative/screen.py +409 -27
  22. pythonnative/storage.py +3 -3
  23. pythonnative/style.py +148 -6
  24. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  25. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  26. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  27. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  28. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
  29. pythonnative/virtual_rows.py +137 -0
  30. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
  31. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
  32. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
  33. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
  34. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
  35. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ """Developer diagnostics: dev mode, warnings, and error reporting.
2
+
3
+ PythonNative distinguishes **dev mode** (the `pn preview` window, `pn
4
+ run` with hot reload, or any process with ``PN_DEV=1``) from production.
5
+ Dev mode turns on:
6
+
7
+ - **Validation warnings** ([`warn`][pythonnative.diagnostics.warn] /
8
+ [`warn_once`][pythonnative.diagnostics.warn_once]): unknown style
9
+ keys, duplicate list keys, and similar mistakes are printed once with
10
+ a suggestion instead of failing silently.
11
+ - **Hook-order checking**: calling hooks conditionally corrupts slot
12
+ state; in dev mode the mismatch raises a
13
+ [`HookOrderError`][pythonnative.diagnostics.HookOrderError]
14
+ immediately instead of cross-wiring state.
15
+ - **The RedBox**: uncaught errors from render, effects, and event
16
+ handlers are routed to the screen host, which presents a full-screen
17
+ error overlay (see `pythonnative.screen`) instead of crashing or
18
+ swallowing the traceback.
19
+
20
+ In production none of this runs: validation is skipped, hook-order
21
+ checks are skipped, and errors propagate exactly as raised.
22
+
23
+ This module has no dependencies on the rest of PythonNative, so any
24
+ module (hooks, reconciler, events) may import it freely.
25
+ """
26
+
27
+ import os
28
+ import sys
29
+ import threading
30
+ import traceback
31
+ from collections import deque
32
+ from typing import Any, Callable, Deque, List, Optional, Set, Tuple
33
+
34
+ __all__ = [
35
+ "HookOrderError",
36
+ "set_dev_mode",
37
+ "is_dev",
38
+ "warn",
39
+ "warn_once",
40
+ "get_warnings",
41
+ "clear_warnings",
42
+ "set_error_reporter",
43
+ "report_error",
44
+ ]
45
+
46
+
47
+ class HookOrderError(RuntimeError):
48
+ """Raised in dev mode when hooks are called in a different order than the previous render.
49
+
50
+ Hooks map to state slots by call order, so calling them inside
51
+ conditionals or loops (or returning early between hook calls)
52
+ silently cross-wires state in production. Dev mode detects the
53
+ mismatch and raises this error with the offending component and
54
+ slot so the bug is caught at the source.
55
+ """
56
+
57
+
58
+ # ======================================================================
59
+ # Dev mode
60
+ # ======================================================================
61
+
62
+ # Tri-state: ``None`` means "not explicitly set, consult the environment".
63
+ _dev_mode: Optional[bool] = None
64
+
65
+
66
+ def set_dev_mode(enabled: bool) -> None:
67
+ """Explicitly enable or disable dev mode for this process.
68
+
69
+ Called automatically by ``pn preview`` and by the screen host's
70
+ ``enable_hot_reload`` (which the device templates invoke on debug
71
+ builds). An explicit call wins over the ``PN_DEV`` environment
72
+ variable.
73
+
74
+ Args:
75
+ enabled: ``True`` to turn on dev diagnostics.
76
+ """
77
+ global _dev_mode
78
+ _dev_mode = bool(enabled)
79
+
80
+
81
+ def is_dev() -> bool:
82
+ """Return whether dev diagnostics are active.
83
+
84
+ Resolution order: an explicit
85
+ [`set_dev_mode`][pythonnative.diagnostics.set_dev_mode] call, then
86
+ the ``PN_DEV`` environment variable, then ``False``.
87
+ """
88
+ if _dev_mode is not None:
89
+ return _dev_mode
90
+ return os.environ.get("PN_DEV", "").lower() in {"1", "true", "yes", "on"}
91
+
92
+
93
+ # ======================================================================
94
+ # Warnings (LogBox-lite)
95
+ # ======================================================================
96
+
97
+ _MAX_WARNINGS = 200
98
+
99
+ _warn_lock = threading.Lock()
100
+ _warned_keys: Set[str] = set()
101
+ _warnings: Deque[str] = deque(maxlen=_MAX_WARNINGS)
102
+
103
+
104
+ def warn(message: str) -> None:
105
+ """Print a dev warning and record it in the warning log.
106
+
107
+ No-op in production. Warnings are prefixed with ``[PN] WARN`` on
108
+ stderr and retained (most recent 200) for inspection via
109
+ [`get_warnings`][pythonnative.diagnostics.get_warnings].
110
+
111
+ Args:
112
+ message: Human-readable description of the problem, ideally
113
+ with a suggestion for the fix.
114
+ """
115
+ if not is_dev():
116
+ return
117
+ with _warn_lock:
118
+ _warnings.append(message)
119
+ try:
120
+ print(f"[PN] WARN: {message}", file=sys.stderr, flush=True)
121
+ except Exception:
122
+ pass
123
+
124
+
125
+ def warn_once(message: str, key: Optional[str] = None) -> None:
126
+ """Like [`warn`][pythonnative.diagnostics.warn], but at most once per ``key``.
127
+
128
+ Use for per-render validation (style keys, list keys) so a warning
129
+ fires once instead of sixty times a second.
130
+
131
+ Args:
132
+ message: The warning message.
133
+ key: Dedupe key, e.g. ``"style:Text:font_siez"``. Defaults to
134
+ the message itself.
135
+ """
136
+ if not is_dev():
137
+ return
138
+ dedupe = key if key is not None else message
139
+ with _warn_lock:
140
+ if dedupe in _warned_keys:
141
+ return
142
+ _warned_keys.add(dedupe)
143
+ warn(message)
144
+
145
+
146
+ def get_warnings() -> List[str]:
147
+ """Return a snapshot of the recorded warnings (oldest first)."""
148
+ with _warn_lock:
149
+ return list(_warnings)
150
+
151
+
152
+ def clear_warnings() -> None:
153
+ """Drop all recorded warnings and dedupe keys (test helper)."""
154
+ with _warn_lock:
155
+ _warnings.clear()
156
+ _warned_keys.clear()
157
+
158
+
159
+ # ======================================================================
160
+ # Error reporting (RedBox routing)
161
+ # ======================================================================
162
+ #
163
+ # Screen hosts register themselves as error reporters. When an error
164
+ # escapes user code in a context that would otherwise be swallowed
165
+ # (event handlers) or crash the process (async tasks), dev mode routes
166
+ # it to the most recently registered reporter, which shows the RedBox.
167
+ # Reporters form a stack: the top entry is the most recently created
168
+ # (and therefore frontmost) screen.
169
+
170
+ _reporter_lock = threading.Lock()
171
+ _reporters: List[Tuple[int, Callable[[BaseException, str], None]]] = []
172
+
173
+
174
+ def set_error_reporter(owner: Any, reporter: Optional[Callable[[BaseException, str], None]]) -> None:
175
+ """Register or unregister ``owner``'s RedBox reporter.
176
+
177
+ Args:
178
+ owner: Any object identifying the registration (a screen
179
+ host); keyed by ``id(owner)``.
180
+ reporter: ``reporter(exc, phase)`` callable, or ``None`` to
181
+ unregister the owner's reporter.
182
+ """
183
+ key = id(owner)
184
+ with _reporter_lock:
185
+ _reporters[:] = [(k, r) for (k, r) in _reporters if k != key]
186
+ if reporter is not None:
187
+ _reporters.append((key, reporter))
188
+
189
+
190
+ def report_error(exc: BaseException, phase: str = "runtime") -> bool:
191
+ """Route ``exc`` to the active RedBox reporter (dev mode only).
192
+
193
+ Args:
194
+ exc: The exception to display.
195
+ phase: Where it came from: ``"render"``, ``"effect"``,
196
+ ``"event"``, or ``"async"``.
197
+
198
+ Returns:
199
+ ``True`` when a reporter accepted the error, ``False`` when no
200
+ reporter is registered (or dev mode is off), in which case the
201
+ caller should fall back to its default behavior.
202
+ """
203
+ if not is_dev():
204
+ return False
205
+ with _reporter_lock:
206
+ reporter = _reporters[-1][1] if _reporters else None
207
+ if reporter is None:
208
+ return False
209
+ try:
210
+ reporter(exc, phase)
211
+ return True
212
+ except Exception:
213
+ traceback.print_exc()
214
+ return False
pythonnative/element.py CHANGED
@@ -36,7 +36,10 @@ class Element:
36
36
  components decorated with [`component`][pythonnative.component].
37
37
  props: Dict of properties passed to the native handler or
38
38
  component function.
39
- children: Ordered list of child `Element` instances.
39
+ children: Ordered list of child `Element` instances. `None` and
40
+ `False` entries are permitted and dropped during
41
+ reconciliation, so conditional children (`cond and Text(...)`)
42
+ need no special casing.
40
43
  key: Optional stable identity used by the reconciler when
41
44
  diffing keyed lists. Two elements with the same `type` and
42
45
  `key` are treated as the same logical node across renders.
@@ -48,7 +51,7 @@ class Element:
48
51
  self,
49
52
  type_name: Union[str, Any],
50
53
  props: Dict[str, Any],
51
- children: List["Element"],
54
+ children: List[Any],
52
55
  key: Optional[str] = None,
53
56
  ) -> None:
54
57
  self.type = type_name
pythonnative/events.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Tag-based event routing between native views and Python callbacks.
2
2
 
3
- Before the batched-commit overhaul, every event prop (``on_click``,
3
+ Before the batched-commit overhaul, every event prop (``on_press``,
4
4
  ``on_change``, …) was wired by storing the Python callable on (or next
5
5
  to) the native view, and every re-render re-pushed fresh closures across
6
6
  the bridge. This module replaces that with a single dispatch channel:
@@ -78,19 +78,24 @@ class EventRegistry:
78
78
 
79
79
  Returns:
80
80
  ``True`` when a callback existed and was invoked (even if
81
- it raised: exceptions are swallowed so a buggy app
82
- callback can't crash the platform's UI thread), ``False``
83
- when nothing is registered.
81
+ it raised: exceptions never propagate into the platform's
82
+ UI thread; in dev mode they are routed to the RedBox via
83
+ [`report_error`][pythonnative.diagnostics.report_error],
84
+ otherwise the traceback is printed), ``False`` when nothing
85
+ is registered.
84
86
  """
85
87
  callback = self.get(tag, name)
86
88
  if callback is None:
87
89
  return False
88
90
  try:
89
91
  callback(*args)
90
- except Exception:
91
- import traceback
92
+ except Exception as exc:
93
+ from . import diagnostics
92
94
 
93
- traceback.print_exc()
95
+ if not diagnostics.report_error(exc, phase=f"event {name!r}"):
96
+ import traceback
97
+
98
+ traceback.print_exc()
94
99
  return True
95
100
 
96
101
  def reset(self) -> None:
@@ -115,7 +120,7 @@ def dispatch_event(tag: int, name: str, *args: Any) -> bool:
115
120
 
116
121
  Args:
117
122
  tag: The view's reconciler-assigned tag.
118
- name: Event name, the original prop name (``"on_click"``,
123
+ name: Event name, the original prop name (``"on_press"``,
119
124
  ``"on_change"``, …) or a gesture channel (``"gesture:0"``).
120
125
  *args: Positional arguments forwarded to the user callback,
121
126
  preserving each prop's documented signature.