pythonnative 0.13.0__tar.gz → 0.13.1__tar.gz
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.
- {pythonnative-0.13.0/src/pythonnative.egg-info → pythonnative-0.13.1}/PKG-INFO +1 -1
- {pythonnative-0.13.0 → pythonnative-0.13.1}/pyproject.toml +2 -1
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/__init__.py +1 -1
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/hot_reload.py +153 -3
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_views/__init__.py +72 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_views/ios.py +49 -5
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/screen.py +47 -12
- {pythonnative-0.13.0 → pythonnative-0.13.1/src/pythonnative.egg-info}/PKG-INFO +1 -1
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_hot_reload.py +239 -6
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_native_views.py +95 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/LICENSE +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/README.md +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/setup.cfg +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/_ios_log.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/alerts.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/animated.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/cli/pn.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/components.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/element.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/hooks.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/layout.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_modules/__init__.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_modules/camera.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_modules/file_system.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_modules/location.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_modules/notifications.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_views/android.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/native_views/base.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/navigation.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/platform.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/platform_metrics.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/reconciler.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/style.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative/utils.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative.egg-info/SOURCES.txt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_alert.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_animated.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_cli.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_components.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_element.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_hooks.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_ios_log.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_layout.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_metric_hooks.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_navigation.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_new_components.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_platform.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_platform_metrics.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_reconciler.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_ref.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_screen.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_smoke.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_style.py +0 -0
- {pythonnative-0.13.0 → pythonnative-0.13.1}/tests/test_utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonnative"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.1"
|
|
8
8
|
description = "Cross-platform native UI toolkit for Android and iOS"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Owen Carey" }
|
|
@@ -62,6 +62,7 @@ Documentation = "https://docs.pythonnative.com/"
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
|
|
65
66
|
[tool.setuptools.packages.find]
|
|
66
67
|
where = ["src"]
|
|
67
68
|
|
|
@@ -196,10 +196,19 @@ class ModuleReloader:
|
|
|
196
196
|
"""Reload changed Python modules on device and trigger a re-render.
|
|
197
197
|
|
|
198
198
|
Designed to be invoked from device-side glue when a hot-reload
|
|
199
|
-
push completes.
|
|
200
|
-
|
|
199
|
+
push completes. All public methods are static; the class holds a
|
|
200
|
+
single piece of process-wide state — the manifest version that
|
|
201
|
+
has most recently been applied to ``sys.modules`` — so that
|
|
202
|
+
multiple screen hosts polling the same manifest do not each
|
|
203
|
+
re-execute the user-app modules. The first host to see a new
|
|
204
|
+
version pays the ``reload_modules`` cost; subsequent hosts on the
|
|
205
|
+
same version refresh only their own reconciler tree against the
|
|
206
|
+
already-fresh modules.
|
|
201
207
|
"""
|
|
202
208
|
|
|
209
|
+
_last_reloaded_version: Optional[str] = None
|
|
210
|
+
_reload_lock = threading.Lock()
|
|
211
|
+
|
|
203
212
|
@staticmethod
|
|
204
213
|
def reload_module(module_name: str) -> bool:
|
|
205
214
|
"""Reload a single module by its dotted name.
|
|
@@ -254,6 +263,139 @@ class ModuleReloader:
|
|
|
254
263
|
reloaded.append(module_name)
|
|
255
264
|
return reloaded
|
|
256
265
|
|
|
266
|
+
@staticmethod
|
|
267
|
+
def reload_modules_for_version(
|
|
268
|
+
module_names: Sequence[str],
|
|
269
|
+
version: Optional[str],
|
|
270
|
+
) -> List[str]:
|
|
271
|
+
"""Reload ``module_names`` for ``version``, deduping across hosts.
|
|
272
|
+
|
|
273
|
+
Each native screen host on iOS / Android runs its own poll
|
|
274
|
+
loop and would otherwise call
|
|
275
|
+
[`reload_modules`][pythonnative.hot_reload.ModuleReloader.reload_modules]
|
|
276
|
+
independently for the same manifest version. That re-executes
|
|
277
|
+
every user-app module N times (once per host) per file change,
|
|
278
|
+
producing N different generations of the same function objects
|
|
279
|
+
in ``sys.modules`` and leaving each host's reconciler tree
|
|
280
|
+
pointing at a different generation. Beyond the wasted work,
|
|
281
|
+
the inconsistent state has been observed to crash UIKit on iOS
|
|
282
|
+
with ``CALayerInvalidGeometry`` (NaN values fed into ``setFrame_:``
|
|
283
|
+
during the interleaved renders).
|
|
284
|
+
|
|
285
|
+
This helper serializes on
|
|
286
|
+
[`_reload_lock`][pythonnative.hot_reload.ModuleReloader] and uses
|
|
287
|
+
[`_last_reloaded_version`][pythonnative.hot_reload.ModuleReloader]
|
|
288
|
+
to ensure only the *first* host to see a given ``version``
|
|
289
|
+
actually re-executes the modules. Subsequent hosts on the same
|
|
290
|
+
version get back the already-fresh entries from ``sys.modules``
|
|
291
|
+
so their own
|
|
292
|
+
[`refresh_in_place`][pythonnative.hot_reload.ModuleReloader.refresh_in_place]
|
|
293
|
+
pass can still rewrite their tree against the same generation.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
module_names: Dotted module names to reload.
|
|
297
|
+
version: Manifest version this reload is processing. When
|
|
298
|
+
``None`` (e.g. tests calling reload directly) the call
|
|
299
|
+
falls back to the unconditional
|
|
300
|
+
[`reload_modules`][pythonnative.hot_reload.ModuleReloader.reload_modules]
|
|
301
|
+
behavior.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The list of module names that are currently fresh in
|
|
305
|
+
``sys.modules`` — either freshly reloaded by this call, or
|
|
306
|
+
already reloaded by an earlier host for the same version.
|
|
307
|
+
"""
|
|
308
|
+
with ModuleReloader._reload_lock:
|
|
309
|
+
if version is not None and version == ModuleReloader._last_reloaded_version:
|
|
310
|
+
return [name for name in module_names if name in sys.modules]
|
|
311
|
+
reloaded = ModuleReloader.reload_modules(module_names)
|
|
312
|
+
if reloaded and version is not None:
|
|
313
|
+
ModuleReloader._last_reloaded_version = version
|
|
314
|
+
return reloaded
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def expand_reload_targets(changed_modules: Sequence[str], component_path: str) -> List[str]:
|
|
318
|
+
"""Expand a manifest of changed modules into the full reload order.
|
|
319
|
+
|
|
320
|
+
When a user edits ``app/screens/home.py``, only that file is in
|
|
321
|
+
the manifest. But the entry-point module ``app.main`` has
|
|
322
|
+
bindings like ``from app.screens.home import HomeScreen`` that
|
|
323
|
+
need to be re-evaluated against the freshly-loaded
|
|
324
|
+
``app.screens.home``; likewise other user-app modules may carry
|
|
325
|
+
transitive bindings (e.g. through a shared ``app/theme.py``)
|
|
326
|
+
that go stale if only the changed file is reloaded.
|
|
327
|
+
|
|
328
|
+
This helper computes the full ordered reload list:
|
|
329
|
+
|
|
330
|
+
1. Explicitly changed modules first (in the order given), so
|
|
331
|
+
their fresh source replaces the cached version in
|
|
332
|
+
``sys.modules`` before any dependent modules re-execute.
|
|
333
|
+
2. All other currently-imported modules under the entry-point's
|
|
334
|
+
top-level package, deepest first. The depth heuristic biases
|
|
335
|
+
toward leaves so re-executing a screen file picks up the
|
|
336
|
+
newest shared utilities before the file that imports it does.
|
|
337
|
+
3. The entry-point module itself, last, so its
|
|
338
|
+
``from ... import`` bindings rebind against everything that
|
|
339
|
+
was refreshed in steps 1 and 2.
|
|
340
|
+
|
|
341
|
+
Modules outside the entry-point's top-level package
|
|
342
|
+
(``pythonnative.*``, stdlib, third-party) are never included;
|
|
343
|
+
framework code is not reloaded.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
changed_modules: Modules reported as changed by the host
|
|
347
|
+
file-watcher (already in dotted form).
|
|
348
|
+
component_path: The host's entry-point identifier, either a
|
|
349
|
+
module path (``"app.main"``) or a dotted attribute path
|
|
350
|
+
(``"app.main.RootScreen"``).
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The ordered list of modules to feed to
|
|
354
|
+
[`reload_modules`][pythonnative.hot_reload.ModuleReloader.reload_modules].
|
|
355
|
+
"""
|
|
356
|
+
entry_module: Optional[str] = None
|
|
357
|
+
if component_path in sys.modules:
|
|
358
|
+
entry_module = component_path
|
|
359
|
+
elif "." in component_path:
|
|
360
|
+
parent = component_path.rsplit(".", 1)[0]
|
|
361
|
+
if parent in sys.modules:
|
|
362
|
+
entry_module = parent
|
|
363
|
+
|
|
364
|
+
app_prefix: Optional[str] = None
|
|
365
|
+
if entry_module:
|
|
366
|
+
app_prefix = entry_module.split(".")[0]
|
|
367
|
+
else:
|
|
368
|
+
for m in changed_modules:
|
|
369
|
+
if m:
|
|
370
|
+
app_prefix = m.split(".")[0]
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
app_modules: Set[str] = set()
|
|
374
|
+
if app_prefix:
|
|
375
|
+
for name in list(sys.modules):
|
|
376
|
+
if name == app_prefix or name.startswith(app_prefix + "."):
|
|
377
|
+
app_modules.add(name)
|
|
378
|
+
|
|
379
|
+
ordered: List[str] = []
|
|
380
|
+
seen: Set[str] = set()
|
|
381
|
+
for m in changed_modules:
|
|
382
|
+
if m and m not in seen:
|
|
383
|
+
ordered.append(m)
|
|
384
|
+
seen.add(m)
|
|
385
|
+
|
|
386
|
+
others = [m for m in app_modules if m not in seen and m != entry_module]
|
|
387
|
+
others.sort(key=lambda m: (-m.count("."), m))
|
|
388
|
+
for m in others:
|
|
389
|
+
ordered.append(m)
|
|
390
|
+
seen.add(m)
|
|
391
|
+
|
|
392
|
+
if entry_module:
|
|
393
|
+
if entry_module in seen:
|
|
394
|
+
ordered.remove(entry_module)
|
|
395
|
+
ordered.append(entry_module)
|
|
396
|
+
|
|
397
|
+
return ordered
|
|
398
|
+
|
|
257
399
|
@staticmethod
|
|
258
400
|
def file_to_module(file_path: str, base_dir: str = "") -> Optional[str]:
|
|
259
401
|
"""Convert a file path to a dotted module name.
|
|
@@ -491,5 +633,13 @@ class ModuleReloader:
|
|
|
491
633
|
files = manifest.get("files", [])
|
|
492
634
|
modules = ModuleReloader.modules_from_files(files if isinstance(files, list) else [])
|
|
493
635
|
|
|
494
|
-
|
|
636
|
+
# Stash the version on the host so `_reload_host` can dedupe
|
|
637
|
+
# `reload_modules` across multiple hosts polling the same
|
|
638
|
+
# manifest. See `reload_modules_for_version`.
|
|
639
|
+
previous_pending = getattr(screen_instance, "_hot_reload_pending_version", None)
|
|
640
|
+
try:
|
|
641
|
+
screen_instance._hot_reload_pending_version = version
|
|
642
|
+
ModuleReloader.reload_screen(screen_instance, [str(module) for module in modules])
|
|
643
|
+
finally:
|
|
644
|
+
screen_instance._hot_reload_pending_version = previous_pending
|
|
495
645
|
return version
|
|
@@ -23,10 +23,67 @@ A mock registry can be installed via
|
|
|
23
23
|
reconciler with no real native views.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
import math
|
|
27
|
+
import sys
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
26
30
|
from typing import Any, Dict, Optional, Tuple
|
|
27
31
|
|
|
28
32
|
from .base import ViewHandler
|
|
29
33
|
|
|
34
|
+
# ======================================================================
|
|
35
|
+
# Tripwire log rate limiter
|
|
36
|
+
# ======================================================================
|
|
37
|
+
#
|
|
38
|
+
# Defensive NaN/Inf guards in ``set_frame`` and ``_apply_transform`` log
|
|
39
|
+
# a single line per occurrence. That's fine for one-off events, but
|
|
40
|
+
# ``Animated.View`` drives transforms at ~60 Hz; once an
|
|
41
|
+
# ``Animated.Value`` enters a stuck NaN state (e.g., a spring tick
|
|
42
|
+
# corrupted across a Fast Refresh), the tripwire would otherwise emit
|
|
43
|
+
# thousands of identical lines per second and drown the dev console.
|
|
44
|
+
#
|
|
45
|
+
# We instead log the first occurrence immediately, then suppress
|
|
46
|
+
# further messages with the same ``label`` for
|
|
47
|
+
# ``_TRIPWIRE_RATE_LIMIT_S`` seconds, and append a
|
|
48
|
+
# ``(+N similar in last Xs)`` suffix to the next message that escapes
|
|
49
|
+
# the window. The first sample plus a count is enough to diagnose; the
|
|
50
|
+
# bounded log keeps the dev console usable.
|
|
51
|
+
|
|
52
|
+
_TRIPWIRE_RATE_LIMIT_S: float = 1.0
|
|
53
|
+
_TRIPWIRE_LOG_LOCK = threading.Lock()
|
|
54
|
+
_TRIPWIRE_LAST_LOG_TIME: Dict[str, float] = {}
|
|
55
|
+
_TRIPWIRE_SUPPRESSED_COUNT: Dict[str, int] = {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _tripwire_log(label: str, message: str) -> None:
|
|
59
|
+
"""Emit ``message`` to stderr, rate-limited per ``label``.
|
|
60
|
+
|
|
61
|
+
The first call for a given ``label`` always emits. Calls within
|
|
62
|
+
``_TRIPWIRE_RATE_LIMIT_S`` seconds are silently counted. The next
|
|
63
|
+
call after the window appends ``(+N similar in last Xs)`` and
|
|
64
|
+
resets the counter.
|
|
65
|
+
"""
|
|
66
|
+
now = time.monotonic()
|
|
67
|
+
write = False
|
|
68
|
+
suppressed = 0
|
|
69
|
+
with _TRIPWIRE_LOG_LOCK:
|
|
70
|
+
last = _TRIPWIRE_LAST_LOG_TIME.get(label)
|
|
71
|
+
if last is None or now - last >= _TRIPWIRE_RATE_LIMIT_S:
|
|
72
|
+
write = True
|
|
73
|
+
suppressed = _TRIPWIRE_SUPPRESSED_COUNT.get(label, 0)
|
|
74
|
+
_TRIPWIRE_SUPPRESSED_COUNT[label] = 0
|
|
75
|
+
_TRIPWIRE_LAST_LOG_TIME[label] = now
|
|
76
|
+
else:
|
|
77
|
+
_TRIPWIRE_SUPPRESSED_COUNT[label] = _TRIPWIRE_SUPPRESSED_COUNT.get(label, 0) + 1
|
|
78
|
+
if not write:
|
|
79
|
+
return
|
|
80
|
+
if suppressed > 0:
|
|
81
|
+
message = f"{message} (+{suppressed} similar in last {_TRIPWIRE_RATE_LIMIT_S:g}s)"
|
|
82
|
+
try:
|
|
83
|
+
print(message, file=sys.stderr, flush=True)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
30
87
|
|
|
31
88
|
class NativeViewRegistry:
|
|
32
89
|
"""Map element type names to platform-specific view handlers.
|
|
@@ -136,6 +193,21 @@ class NativeViewRegistry:
|
|
|
136
193
|
coordinates computed by ``pythonnative.layout`` in points
|
|
137
194
|
relative to the parent's content origin.
|
|
138
195
|
"""
|
|
196
|
+
# Tripwire: log non-finite layout values so we can diagnose
|
|
197
|
+
# crashes like iOS `CALayerInvalidGeometry` without losing the
|
|
198
|
+
# repro. Handlers are responsible for clamping before applying.
|
|
199
|
+
# Rate-limited via ``_tripwire_log`` to avoid 60 Hz floods when
|
|
200
|
+
# an animated value is stuck at NaN.
|
|
201
|
+
try:
|
|
202
|
+
finite = math.isfinite(x) and math.isfinite(y) and math.isfinite(width) and math.isfinite(height)
|
|
203
|
+
except (TypeError, ValueError):
|
|
204
|
+
finite = False
|
|
205
|
+
if not finite:
|
|
206
|
+
_tripwire_log(
|
|
207
|
+
"set_frame:nan",
|
|
208
|
+
f"[set_frame:nan] type={type_name!r} " f"x={x!r} y={y!r} w={width!r} h={height!r}",
|
|
209
|
+
)
|
|
210
|
+
|
|
139
211
|
handler = self._handlers.get(type_name)
|
|
140
212
|
if handler is not None:
|
|
141
213
|
handler.set_frame(native_view, x, y, width, height)
|
|
@@ -28,8 +28,28 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
28
28
|
|
|
29
29
|
from rubicon.objc import SEL, ObjCClass, objc_method
|
|
30
30
|
|
|
31
|
+
from . import _tripwire_log
|
|
31
32
|
from .base import ViewHandler, _safe_max, parse_color_int
|
|
32
33
|
|
|
34
|
+
|
|
35
|
+
def _safe_finite(value: Any, default: float = 0.0) -> float:
|
|
36
|
+
"""Coerce ``value`` to a finite float, falling back to ``default``.
|
|
37
|
+
|
|
38
|
+
Used as a defensive guard around every call into UIKit that takes a
|
|
39
|
+
geometry value. Without this, a single NaN or inf produced upstream
|
|
40
|
+
(layout edge case, stale prop during a reload, etc.) crashes the
|
|
41
|
+
process via `CALayerInvalidGeometry`. Clamping to ``default``
|
|
42
|
+
converts that into a recoverable visual glitch and lets the
|
|
43
|
+
`[set_frame:nan]` / `[set_transform:nan]` tripwire logs surface
|
|
44
|
+
where the bad value came from.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
f = float(value)
|
|
48
|
+
except (TypeError, ValueError):
|
|
49
|
+
return default
|
|
50
|
+
return f if math.isfinite(f) else default
|
|
51
|
+
|
|
52
|
+
|
|
33
53
|
NSObject = ObjCClass("NSObject")
|
|
34
54
|
UIColor = ObjCClass("UIColor")
|
|
35
55
|
UIFont = ObjCClass("UIFont")
|
|
@@ -350,8 +370,32 @@ def _apply_transform(view: Any, props: Dict[str, Any]) -> None:
|
|
|
350
370
|
return
|
|
351
371
|
try:
|
|
352
372
|
transform = _make_transform(spec)
|
|
373
|
+
a = float(transform.a)
|
|
374
|
+
b = float(transform.b)
|
|
375
|
+
c = float(transform.c)
|
|
376
|
+
d = float(transform.d)
|
|
377
|
+
tx = float(transform.tx)
|
|
378
|
+
ty = float(transform.ty)
|
|
379
|
+
if not (
|
|
380
|
+
math.isfinite(a)
|
|
381
|
+
and math.isfinite(b)
|
|
382
|
+
and math.isfinite(c)
|
|
383
|
+
and math.isfinite(d)
|
|
384
|
+
and math.isfinite(tx)
|
|
385
|
+
and math.isfinite(ty)
|
|
386
|
+
):
|
|
387
|
+
# Tripwire: a NaN/inf transform crashes UIKit. Log
|
|
388
|
+
# (rate-limited to avoid 60 Hz spam from stuck Animated
|
|
389
|
+
# values) and fall back to identity so the app keeps
|
|
390
|
+
# running.
|
|
391
|
+
_tripwire_log(
|
|
392
|
+
"set_transform:nan",
|
|
393
|
+
f"[set_transform:nan] spec={spec!r} -> " f"(a={a!r}, b={b!r}, c={c!r}, d={d!r}, tx={tx!r}, ty={ty!r})",
|
|
394
|
+
)
|
|
395
|
+
view.setTransform_((1.0, 0.0, 0.0, 1.0, 0.0, 0.0))
|
|
396
|
+
return
|
|
353
397
|
# rubicon-objc accepts the C struct as a tuple of its fields.
|
|
354
|
-
view.setTransform_((
|
|
398
|
+
view.setTransform_((a, b, c, d, tx, ty))
|
|
355
399
|
except Exception:
|
|
356
400
|
pass
|
|
357
401
|
|
|
@@ -450,10 +494,10 @@ class IOSViewHandler(ViewHandler):
|
|
|
450
494
|
if native_view is None:
|
|
451
495
|
return
|
|
452
496
|
try:
|
|
453
|
-
frame_x =
|
|
454
|
-
frame_y =
|
|
455
|
-
frame_w =
|
|
456
|
-
frame_h =
|
|
497
|
+
frame_x = _safe_finite(x, 0.0)
|
|
498
|
+
frame_y = _safe_finite(y, 0.0)
|
|
499
|
+
frame_w = max(0.0, _safe_finite(width, 0.0))
|
|
500
|
+
frame_h = max(0.0, _safe_finite(height, 0.0))
|
|
457
501
|
native_view.setTranslatesAutoresizingMaskIntoConstraints_(True)
|
|
458
502
|
native_view.setFrame_(((frame_x, frame_y), (frame_w, frame_h)))
|
|
459
503
|
_clamp_view_corner_radius(native_view, frame_w, frame_h)
|
|
@@ -341,6 +341,25 @@ def _hot_reload_tick(host: Any) -> bool:
|
|
|
341
341
|
if not manifest_exists and last is None:
|
|
342
342
|
return False
|
|
343
343
|
|
|
344
|
+
# The iOS template polls every 0.5s per UIViewController, so this
|
|
345
|
+
# tick fires several times per second per host. The per-tick log is
|
|
346
|
+
# gated behind ``PYTHONNATIVE_DEBUG`` to keep normal output quiet
|
|
347
|
+
# while preserving the breadcrumb when investigating reload races.
|
|
348
|
+
if _debug_enabled():
|
|
349
|
+
manifest_version: Optional[str] = None
|
|
350
|
+
if manifest_exists:
|
|
351
|
+
try:
|
|
352
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
353
|
+
raw_version = json.load(f).get("version", "")
|
|
354
|
+
manifest_version = str(raw_version) if raw_version else None
|
|
355
|
+
except Exception:
|
|
356
|
+
manifest_version = None
|
|
357
|
+
action = "reload" if (manifest_version is not None and manifest_version != last) else "skip"
|
|
358
|
+
_log_pn(
|
|
359
|
+
f"_hot_reload_tick: host=0x{id(host):x} component={host._component_path} "
|
|
360
|
+
f"last={last!r} manifest={manifest_version!r} action={action}"
|
|
361
|
+
)
|
|
362
|
+
|
|
344
363
|
next_version = ModuleReloader.reload_from_manifest(
|
|
345
364
|
host,
|
|
346
365
|
manifest_path,
|
|
@@ -348,10 +367,6 @@ def _hot_reload_tick(host: Any) -> bool:
|
|
|
348
367
|
)
|
|
349
368
|
if next_version == last:
|
|
350
369
|
return False
|
|
351
|
-
_log_pn(
|
|
352
|
-
f"_hot_reload_tick: triggered reload "
|
|
353
|
-
f"(manifest_exists={manifest_exists}, last={last!r}, next={next_version!r})"
|
|
354
|
-
)
|
|
355
370
|
host._hot_reload_last_version = next_version
|
|
356
371
|
return True
|
|
357
372
|
|
|
@@ -365,6 +380,19 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
|
|
|
365
380
|
next render then runs the new bodies through the existing hook
|
|
366
381
|
slots, so component state survives.
|
|
367
382
|
|
|
383
|
+
The reload set is **expanded** to include every currently-imported
|
|
384
|
+
module under the entry-point's top-level package (see
|
|
385
|
+
[`expand_reload_targets`][pythonnative.hot_reload.ModuleReloader.expand_reload_targets]).
|
|
386
|
+
This catches transitive ``from ... import`` bindings that would
|
|
387
|
+
otherwise remain stale: if ``app/main.py`` does
|
|
388
|
+
``from app.screens.home import HomeScreen`` and the user edits
|
|
389
|
+
``home.py``, reloading just ``app.screens.home`` leaves
|
|
390
|
+
``app.main.HomeScreen`` pointing at the pre-edit function, so the
|
|
391
|
+
new render emits stale element types and the reconciler is forced
|
|
392
|
+
to unmount and remount the screen (losing state and showing old
|
|
393
|
+
code). Reloading every user-app module in dependency-friendly
|
|
394
|
+
order, with the entry-point last, keeps every binding fresh.
|
|
395
|
+
|
|
368
396
|
If Fast Refresh fails (the new module raised at import time, no
|
|
369
397
|
replacements could be located, or the next render itself
|
|
370
398
|
threw), the host falls back to a full remount: a brand-new
|
|
@@ -374,14 +402,20 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
|
|
|
374
402
|
"""
|
|
375
403
|
from .hot_reload import ModuleReloader
|
|
376
404
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
405
|
+
requested = list(changed_modules or [])
|
|
406
|
+
targets = ModuleReloader.expand_reload_targets(requested, host._component_path)
|
|
407
|
+
|
|
408
|
+
pending_version = getattr(host, "_hot_reload_pending_version", None)
|
|
409
|
+
already_loaded = pending_version is not None and pending_version == ModuleReloader._last_reloaded_version
|
|
410
|
+
_log_pn(
|
|
411
|
+
f"_reload_host: host=0x{id(host):x} component={host._component_path} "
|
|
412
|
+
f"requested={requested!r} targets={len(targets)} version={pending_version!r} "
|
|
413
|
+
f"action={'reuse_modules' if already_loaded else 'reload_modules'}"
|
|
414
|
+
)
|
|
381
415
|
|
|
382
|
-
reloaded = ModuleReloader.
|
|
416
|
+
reloaded = ModuleReloader.reload_modules_for_version(targets, pending_version)
|
|
383
417
|
if not reloaded:
|
|
384
|
-
_log_pn(f"_reload_host: no modules could be reloaded from {
|
|
418
|
+
_log_pn(f"_reload_host: no modules could be reloaded from {targets!r}; aborting")
|
|
385
419
|
return
|
|
386
420
|
|
|
387
421
|
try:
|
|
@@ -392,10 +426,11 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
|
|
|
392
426
|
host._component = new_component
|
|
393
427
|
|
|
394
428
|
if host._reconciler is None:
|
|
429
|
+
_log_pn(f"_reload_host: host=0x{id(host):x} reconciler=None; skipping refresh")
|
|
395
430
|
return
|
|
396
431
|
|
|
397
432
|
if _try_fast_refresh(host, reloaded):
|
|
398
|
-
print(f"[hot-reload] Fast Refresh: {', '.join(reloaded)}", file=sys.stderr)
|
|
433
|
+
print(f"[hot-reload] Fast Refresh: {', '.join(requested) or ', '.join(reloaded)}", file=sys.stderr)
|
|
399
434
|
return
|
|
400
435
|
|
|
401
436
|
_full_remount(host, reloaded)
|
|
@@ -1080,7 +1115,7 @@ else:
|
|
|
1080
1115
|
nav = getattr(self.native_instance, "navigationController", None)
|
|
1081
1116
|
if nav is None:
|
|
1082
1117
|
raise RuntimeError(
|
|
1083
|
-
"No UINavigationController available;
|
|
1118
|
+
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
1084
1119
|
)
|
|
1085
1120
|
nav.pushViewController_animated_(next_vc, True)
|
|
1086
1121
|
|