pythonnative 0.40.0__cp314-cp314-win_amd64.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.
- pythonnative/__init__.py +411 -0
- pythonnative/_ios_log.py +92 -0
- pythonnative/_yoga.cp314-win_amd64.pyd +0 -0
- pythonnative/alerts.py +236 -0
- pythonnative/animated.py +1687 -0
- pythonnative/animation_graph.py +78 -0
- pythonnative/appearance.py +124 -0
- pythonnative/bootstrap.py +100 -0
- pythonnative/bridge/__init__.py +302 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +170 -0
- pythonnative/bridge/commits.py +114 -0
- pythonnative/bridge/fake.py +243 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/bridge/web.py +489 -0
- pythonnative/cli/__init__.py +7 -0
- pythonnative/cli/pn.py +1214 -0
- pythonnative/component.py +271 -0
- pythonnative/components/__init__.py +111 -0
- pythonnative/components/_base.py +166 -0
- pythonnative/components/controls.py +521 -0
- pythonnative/components/layout.py +518 -0
- pythonnative/components/lists.py +785 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/devclient.py +824 -0
- pythonnative/devserver/__init__.py +34 -0
- pythonnative/devserver/server.py +718 -0
- pythonnative/devserver/static/animation_graph.js +82 -0
- pythonnative/devserver/static/bridge.js +147 -0
- pythonnative/devserver/static/colors.js +70 -0
- pythonnative/devserver/static/host.js +552 -0
- pythonnative/devserver/static/index.html +60 -0
- pythonnative/devserver/static/layout.js +85 -0
- pythonnative/devserver/static/preview.css +415 -0
- pythonnative/devserver/static/renderer.js +1994 -0
- pythonnative/devserver/static/shell.js +345 -0
- pythonnative/devserver/static/yoga/LICENSE +21 -0
- pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
- pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
- pythonnative/devserver/static/yoga/src/index.js +16 -0
- pythonnative/devserver/static/yoga/src/index.js.map +1 -0
- pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
- pythonnative/devserver/static/yoga/src/load.js +17 -0
- pythonnative/devserver/static/yoga/src/load.js.map +1 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
- pythonnative/devserver/watcher.py +267 -0
- pythonnative/devserver/ws.py +421 -0
- pythonnative/diagnostics.py +241 -0
- pythonnative/element.py +159 -0
- pythonnative/equality.py +19 -0
- pythonnative/events.py +231 -0
- pythonnative/gestures.py +1333 -0
- pythonnative/hooks.py +1621 -0
- pythonnative/hosts/__init__.py +63 -0
- pythonnative/hosts/base.py +596 -0
- pythonnative/hosts/native.py +302 -0
- pythonnative/hot_reload.py +585 -0
- pythonnative/layout.py +328 -0
- pythonnative/mutations.py +142 -0
- pythonnative/native/android/build.gradle +52 -0
- pythonnative/native/android/gradle.properties +3 -0
- pythonnative/native/android/settings.gradle +12 -0
- pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
- pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
- pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
- pythonnative/native/ios/Package.swift +23 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/native/yoga/LICENSE +21 -0
- pythonnative/native/yoga/Package.swift +9 -0
- pythonnative/native/yoga/VENDORED.md +2 -0
- pythonnative/native/yoga/include/PNStyle.h +10 -0
- pythonnative/native/yoga/include/module.modulemap +5 -0
- pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/python.cpp +16 -0
- pythonnative/native/yoga/style.cpp +272 -0
- pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
- pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
- pythonnative/native/yoga/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
- pythonnative/native/yoga/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
- pythonnative/native/yoga/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
- pythonnative/native/yoga/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
- pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
- pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
- pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
- pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
- pythonnative/native/yoga/yoga/config/Config.h +92 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
- pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
- pythonnative/native/yoga/yoga/debug/Log.h +34 -0
- pythonnative/native/yoga/yoga/enums/Align.h +47 -0
- pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
- pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
- pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
- pythonnative/native/yoga/yoga/enums/Display.h +41 -0
- pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
- pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
- pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
- pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
- pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
- pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
- pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
- pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
- pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
- pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
- pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
- pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
- pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
- pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
- pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
- pythonnative/native/yoga/yoga/event/event.cpp +87 -0
- pythonnative/native/yoga/yoga/event/event.h +130 -0
- pythonnative/native/yoga/yoga/module.modulemap +21 -0
- pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
- pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
- pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
- pythonnative/native/yoga/yoga/node/Node.h +337 -0
- pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
- pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
- pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
- pythonnative/native/yoga/yoga/style/Style.h +757 -0
- pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
- pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
- pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
- pythonnative/native_modules/__init__.py +124 -0
- pythonnative/native_modules/app_state.py +99 -0
- pythonnative/native_modules/battery.py +75 -0
- pythonnative/native_modules/biometrics.py +42 -0
- pythonnative/native_modules/camera.py +65 -0
- pythonnative/native_modules/clipboard.py +46 -0
- pythonnative/native_modules/fallback.py +324 -0
- pythonnative/native_modules/file_system.py +145 -0
- pythonnative/native_modules/haptics.py +75 -0
- pythonnative/native_modules/linking.py +123 -0
- pythonnative/native_modules/location.py +75 -0
- pythonnative/native_modules/net_info.py +112 -0
- pythonnative/native_modules/notifications.py +129 -0
- pythonnative/native_modules/permissions.py +108 -0
- pythonnative/native_modules/registry.py +526 -0
- pythonnative/native_modules/secure_store.py +52 -0
- pythonnative/native_modules/share.py +51 -0
- pythonnative/native_views/__init__.py +375 -0
- pythonnative/native_views/base.py +316 -0
- pythonnative/native_views/bridge_backend.py +277 -0
- pythonnative/navigation/__init__.py +103 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +540 -0
- pythonnative/navigation/hooks.py +133 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +659 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +269 -0
- pythonnative/net.py +248 -0
- pythonnative/platform.py +166 -0
- pythonnative/platform_metrics.py +252 -0
- pythonnative/preview.py +287 -0
- pythonnative/profiling.py +101 -0
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +557 -0
- pythonnative/project/builder.py +817 -0
- pythonnative/project/config.py +773 -0
- pythonnative/project/deps.py +719 -0
- pythonnative/project/devices.py +304 -0
- pythonnative/project/doctor.py +283 -0
- pythonnative/project/fingerprint.py +171 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +325 -0
- pythonnative/project/lockfile.py +100 -0
- pythonnative/project/permissions.py +361 -0
- pythonnative/project/plugins.py +519 -0
- pythonnative/project/runtime_assets.py +183 -0
- pythonnative/py.typed +0 -0
- pythonnative/query.py +153 -0
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +375 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1165 -0
- pythonnative/reconciler/layout_pass.py +393 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/refresh.py +55 -0
- pythonnative/runtime.py +306 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/__init__.py +153 -0
- pythonnative/sdk/_components.py +427 -0
- pythonnative/sdk/builtins.py +91 -0
- pythonnative/sdk/codegen.py +149 -0
- pythonnative/sdk/module_codegen.py +184 -0
- pythonnative/sdk/schema.py +224 -0
- pythonnative/sdk/templates/contracts.kt +46 -0
- pythonnative/sdk/templates/contracts.swift +46 -0
- pythonnative/storage.py +184 -0
- pythonnative/style.py +841 -0
- pythonnative/suspense.py +183 -0
- pythonnative/templates/android_template/app/build.gradle +75 -0
- pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
- pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
- pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
- pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
- pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
- pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
- pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
- pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
- pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
- pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
- pythonnative/templates/android_template/build.gradle +10 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
- pythonnative/templates/android_template/gradle.properties +23 -0
- pythonnative/templates/android_template/gradlew +185 -0
- pythonnative/templates/android_template/gradlew.bat +89 -0
- pythonnative/templates/android_template/settings.gradle +17 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
- pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
- pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +369 -0
- pythonnative/utils.py +145 -0
- pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
- pythonnative-0.40.0.dist-info/METADATA +150 -0
- pythonnative-0.40.0.dist-info/RECORD +418 -0
- pythonnative-0.40.0.dist-info/WHEEL +5 -0
- pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
- pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
- pythonnative-0.40.0.dist-info/top_level.txt +1 -0
- pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
"""The [`Reconciler`][pythonnative.reconciler.core.Reconciler]: element trees in, native mutations out.
|
|
2
|
+
|
|
3
|
+
One reconciler owns one mounted tree (a screen, a list row, a test
|
|
4
|
+
render). Each pass runs the same phases in order:
|
|
5
|
+
|
|
6
|
+
1. **Render**: component bodies run for the subtrees that need it,
|
|
7
|
+
producing [`Element`][pythonnative.Element] descriptions. Hook state
|
|
8
|
+
is installed for the duration of each body.
|
|
9
|
+
2. **Diff and stage**: elements are compared with the mounted
|
|
10
|
+
[`VNode`][pythonnative.reconciler.VNode] in their slot. Creates,
|
|
11
|
+
prop updates, attach/detach, and destroys are *staged* into an op
|
|
12
|
+
list instead of being applied immediately.
|
|
13
|
+
3. **Commit**: the staged ops go to the backend in one batch, refs are
|
|
14
|
+
populated, the layout pass runs (once a viewport is known), then
|
|
15
|
+
effects flush: ``use_layout_effect`` first, ``use_effect`` after.
|
|
16
|
+
|
|
17
|
+
A render requested while a pass is in flight (an effect setting
|
|
18
|
+
state, a boundary reset) is queued and drained before the call
|
|
19
|
+
returns, bounded so runaway update loops surface as an error rather
|
|
20
|
+
than a hang. Hosts never observe a half-committed tree.
|
|
21
|
+
|
|
22
|
+
The class is assembled from mixins to keep each concern readable:
|
|
23
|
+
this module holds the render/diff/commit pipeline,
|
|
24
|
+
``boundaries`` handles
|
|
25
|
+
``ErrorBoundary`` and ``Suspense``, and
|
|
26
|
+
``layout_pass`` runs flexbox.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
from contextlib import contextmanager
|
|
33
|
+
from functools import partial
|
|
34
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple
|
|
35
|
+
|
|
36
|
+
from .. import diagnostics
|
|
37
|
+
from ..component import Component
|
|
38
|
+
from ..element import ERROR_BOUNDARY, FRAGMENT, SUSPENSE, Element
|
|
39
|
+
from ..events import extract_events, get_event_registry
|
|
40
|
+
from ..hooks import Context, HookState, install_hook_state, restore_hook_state
|
|
41
|
+
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, UpdateOp
|
|
42
|
+
from ..profiling import profiled
|
|
43
|
+
from ..runtime import _scope
|
|
44
|
+
from ..scheduler import TransitionQueue, schedule_trigger
|
|
45
|
+
from ..suspense import Suspend
|
|
46
|
+
from .boundaries import BoundaryMixin, HydrationMap
|
|
47
|
+
from .children import plan_child_moves
|
|
48
|
+
from .layout_pass import LayoutMixin, affects_layout
|
|
49
|
+
from .vnode import VNode, next_tag, normalize_children, shallow_equal_props
|
|
50
|
+
|
|
51
|
+
__all__ = ["Reconciler"]
|
|
52
|
+
|
|
53
|
+
# Props the reconciler consumes itself and never forwards to the
|
|
54
|
+
# native handler. ``ref`` is populated with the native view after
|
|
55
|
+
# commit, mirroring React's ``ref`` semantics.
|
|
56
|
+
_RECONCILER_OWNED_PROPS = frozenset({"ref"})
|
|
57
|
+
|
|
58
|
+
# Hard ceiling on renders drained inside one host call, so a component
|
|
59
|
+
# that sets state unconditionally in an effect surfaces as an error
|
|
60
|
+
# instead of hanging the UI thread.
|
|
61
|
+
_MAX_RENDERS_PER_PASS = 25
|
|
62
|
+
|
|
63
|
+
_MISSING = object()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Reconciler(BoundaryMixin, LayoutMixin):
|
|
67
|
+
"""Owns one mounted tree and translates element diffs into native mutations.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
backend: An object implementing the registry protocol
|
|
71
|
+
(``apply_mutations``, ``resolve_view``, ``measure_intrinsic``,
|
|
72
|
+
``command``). PythonNative ships Android, iOS, and browser
|
|
73
|
+
registries; tests use
|
|
74
|
+
[`FakeBackend`][pythonnative.testing.FakeBackend].
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
backend: The backend passed at construction.
|
|
78
|
+
on_render_requested: Optional callback hosts set to be told a
|
|
79
|
+
re-render is wanted (to hop onto the UI thread or guard
|
|
80
|
+
with a red box). When ``None`` the reconciler re-renders
|
|
81
|
+
inline. Whatever the callback does, it should end up
|
|
82
|
+
calling [`flush_dirty`][pythonnative.reconciler.core.Reconciler.flush_dirty].
|
|
83
|
+
on_back_registered: Optional callback fired when the first
|
|
84
|
+
[`use_back_handler`][pythonnative.use_back_handler]
|
|
85
|
+
registers, so hosts can enable hardware back interception.
|
|
86
|
+
transitions: Deferred-render queue behind
|
|
87
|
+
[`use_transition`][pythonnative.use_transition]; owned per
|
|
88
|
+
reconciler so screens never delay each other.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, backend: Any) -> None:
|
|
92
|
+
self.backend = backend
|
|
93
|
+
self.root: Optional[VNode] = None
|
|
94
|
+
self._tag_nodes: Dict[int, VNode] = {}
|
|
95
|
+
self._effect_states: Dict[int, HookState] = {}
|
|
96
|
+
self._native_children: Dict[int, List[int]] = {}
|
|
97
|
+
if hasattr(backend, "on_layout"):
|
|
98
|
+
backend.on_layout = self._accept_native_layout
|
|
99
|
+
self.on_render_requested: Optional[Callable[[], None]] = None
|
|
100
|
+
self.on_back_registered: Optional[Callable[[], None]] = None
|
|
101
|
+
self.transitions = TransitionQueue()
|
|
102
|
+
|
|
103
|
+
self._events = get_event_registry()
|
|
104
|
+
self._ops: List[Mutation] = []
|
|
105
|
+
self._created: List[VNode] = []
|
|
106
|
+
self._rendering = False
|
|
107
|
+
self._render_queued = False
|
|
108
|
+
# Component nodes whose own state changed since the last flush,
|
|
109
|
+
# keyed by ``id`` to dedupe while keeping a strong reference.
|
|
110
|
+
self._dirty_nodes: Dict[int, VNode] = {}
|
|
111
|
+
# Error boundaries whose ``reset`` was called.
|
|
112
|
+
self._dirty_boundaries: Dict[int, VNode] = {}
|
|
113
|
+
# Suspense nodes whose awaited work completed.
|
|
114
|
+
self._dirty_suspense: Dict[int, VNode] = {}
|
|
115
|
+
# Hydration map active while a Suspense boundary retries its
|
|
116
|
+
# content, and hook states salvaged while a Suspend unwinds.
|
|
117
|
+
self._hydration: Optional[HydrationMap] = None
|
|
118
|
+
self._suspense_salvage: Optional[HydrationMap] = None
|
|
119
|
+
# Tags destroyed during the current pass; tags are never
|
|
120
|
+
# reused, so stale entries can never alias a live view.
|
|
121
|
+
self._destroyed_tags: Set[int] = set()
|
|
122
|
+
# ``use_back_handler`` registrations, oldest first.
|
|
123
|
+
self._back_handlers: List[Callable[[], bool]] = []
|
|
124
|
+
self._init_layout_state()
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
# Public API
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def root_tag(self) -> Optional[int]:
|
|
132
|
+
"""Tag of the root native view, or ``None`` when nothing is mounted."""
|
|
133
|
+
return self.root.tag if self.root is not None else None
|
|
134
|
+
|
|
135
|
+
def root_view(self) -> Any:
|
|
136
|
+
"""The root native view object, or ``None`` before mount."""
|
|
137
|
+
return self.root.native_view if self.root is not None else None
|
|
138
|
+
|
|
139
|
+
def mount(self, element: Element) -> Any:
|
|
140
|
+
"""Build native views for ``element`` and return the root native view.
|
|
141
|
+
|
|
142
|
+
Any previously mounted tree is unmounted first.
|
|
143
|
+
"""
|
|
144
|
+
if self.root is not None:
|
|
145
|
+
self.unmount()
|
|
146
|
+
self._log(f"mount: {element!r}")
|
|
147
|
+
with self._pass():
|
|
148
|
+
self._destroyed_tags.clear()
|
|
149
|
+
try:
|
|
150
|
+
self.root = self._create_tree(element)
|
|
151
|
+
except Suspend as signal:
|
|
152
|
+
self._discard_salvage()
|
|
153
|
+
raise self._missing_suspense_error(signal) from None
|
|
154
|
+
self._drain_dirty()
|
|
155
|
+
self._commit()
|
|
156
|
+
self._warn_on_multiple_roots()
|
|
157
|
+
return self.root_view()
|
|
158
|
+
|
|
159
|
+
def reconcile(self, element: Element) -> Any:
|
|
160
|
+
"""Diff ``element`` against the mounted tree and patch native views.
|
|
161
|
+
|
|
162
|
+
Pending state updates are drained as part of the pass, so hosts
|
|
163
|
+
only call this when the root element itself changed (new props
|
|
164
|
+
from outside the tree).
|
|
165
|
+
"""
|
|
166
|
+
if self.root is None:
|
|
167
|
+
return self.mount(element)
|
|
168
|
+
with self._pass():
|
|
169
|
+
# A full pass covers every dirty component; reactive context
|
|
170
|
+
# may re-add entries during it, which the drain picks up.
|
|
171
|
+
self._dirty_nodes.clear()
|
|
172
|
+
self._destroyed_tags.clear()
|
|
173
|
+
try:
|
|
174
|
+
self.root = self._reconcile_node(self.root, element)
|
|
175
|
+
except Suspend as signal:
|
|
176
|
+
self._discard_salvage()
|
|
177
|
+
raise self._missing_suspense_error(signal) from None
|
|
178
|
+
self._drain_dirty()
|
|
179
|
+
self._commit()
|
|
180
|
+
self._warn_on_multiple_roots()
|
|
181
|
+
return self.root_view()
|
|
182
|
+
|
|
183
|
+
@profiled("render")
|
|
184
|
+
def flush_dirty(self) -> Any:
|
|
185
|
+
"""Re-render only the components whose state changed, then commit.
|
|
186
|
+
|
|
187
|
+
This is the hot path for state-driven updates: each dirty
|
|
188
|
+
component re-runs its own body and reconciles its subtree in
|
|
189
|
+
place; the batch commits as one native transaction. Returns the
|
|
190
|
+
(possibly replaced) root native view.
|
|
191
|
+
"""
|
|
192
|
+
if self.root is None:
|
|
193
|
+
return None
|
|
194
|
+
if self._rendering:
|
|
195
|
+
self._render_queued = True
|
|
196
|
+
return self.root_view()
|
|
197
|
+
if not self._has_dirty_work():
|
|
198
|
+
return self.root_view()
|
|
199
|
+
with self._pass():
|
|
200
|
+
self._destroyed_tags.clear()
|
|
201
|
+
self._drain_dirty()
|
|
202
|
+
self._commit()
|
|
203
|
+
return self.root_view()
|
|
204
|
+
|
|
205
|
+
def unmount(self) -> None:
|
|
206
|
+
"""Tear down the mounted tree, running effect cleanups and destroying native views."""
|
|
207
|
+
root = self.root
|
|
208
|
+
if root is None:
|
|
209
|
+
return
|
|
210
|
+
self._destroy_tree(root)
|
|
211
|
+
self.root = None
|
|
212
|
+
self._dirty_nodes.clear()
|
|
213
|
+
self._dirty_boundaries.clear()
|
|
214
|
+
self._dirty_suspense.clear()
|
|
215
|
+
self._back_handlers.clear()
|
|
216
|
+
self.transitions.clear()
|
|
217
|
+
self._flush_ops()
|
|
218
|
+
|
|
219
|
+
def dispatch_command(self, tag: Optional[int], name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
220
|
+
"""Run an imperative command against the view registered under ``tag``."""
|
|
221
|
+
if tag is None:
|
|
222
|
+
return None
|
|
223
|
+
return self.backend.command(tag, name, args or {})
|
|
224
|
+
|
|
225
|
+
def dispatch_back_press(self) -> bool:
|
|
226
|
+
"""Offer the system back action to registered handlers, newest first.
|
|
227
|
+
|
|
228
|
+
Returns ``True`` if a handler consumed the event (the platform
|
|
229
|
+
should not run its default behavior).
|
|
230
|
+
"""
|
|
231
|
+
for handler in reversed(list(self._back_handlers)):
|
|
232
|
+
if handler():
|
|
233
|
+
return True
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def walk(self) -> Iterator[VNode]:
|
|
237
|
+
"""Yield every mounted node in depth-first, document order."""
|
|
238
|
+
stack = [self.root] if self.root is not None else []
|
|
239
|
+
while stack:
|
|
240
|
+
node = stack.pop()
|
|
241
|
+
yield node
|
|
242
|
+
stack.extend(reversed(node.children))
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# RenderOwner protocol (called by hooks)
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def mark_dirty(self, vnode: VNode) -> None:
|
|
249
|
+
"""Queue ``vnode`` (a component) for a local re-render on the next flush."""
|
|
250
|
+
if vnode is None or vnode.hook_state is None or not vnode.mounted:
|
|
251
|
+
return
|
|
252
|
+
self._dirty_nodes[id(vnode)] = vnode
|
|
253
|
+
|
|
254
|
+
def request_render(self) -> None:
|
|
255
|
+
"""Ask for a flush: via the host callback when set, inline otherwise."""
|
|
256
|
+
if self._rendering:
|
|
257
|
+
self._render_queued = True
|
|
258
|
+
return
|
|
259
|
+
if self.on_render_requested is not None:
|
|
260
|
+
self.on_render_requested()
|
|
261
|
+
else:
|
|
262
|
+
self.flush_dirty()
|
|
263
|
+
|
|
264
|
+
def register_back_handler(self, handler: Callable[[], bool]) -> Callable[[], None]:
|
|
265
|
+
"""Register a back-press handler; returns an unregister callable."""
|
|
266
|
+
self._back_handlers.append(handler)
|
|
267
|
+
if len(self._back_handlers) == 1 and self.on_back_registered is not None:
|
|
268
|
+
self.on_back_registered()
|
|
269
|
+
|
|
270
|
+
def unregister() -> None:
|
|
271
|
+
try:
|
|
272
|
+
self._back_handlers.remove(handler)
|
|
273
|
+
except ValueError:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
return unregister
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
# Pass bookkeeping
|
|
280
|
+
# ------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
@contextmanager
|
|
283
|
+
def _pass(self) -> Iterator[None]:
|
|
284
|
+
"""Mark a pass in flight and drain renders requested during it."""
|
|
285
|
+
if self._rendering:
|
|
286
|
+
self._render_queued = True
|
|
287
|
+
yield
|
|
288
|
+
return
|
|
289
|
+
self._rendering = True
|
|
290
|
+
try:
|
|
291
|
+
yield
|
|
292
|
+
passes = 0
|
|
293
|
+
while self._render_queued and self.root is not None:
|
|
294
|
+
self._render_queued = False
|
|
295
|
+
if not self._has_dirty_work():
|
|
296
|
+
continue
|
|
297
|
+
passes += 1
|
|
298
|
+
if passes > _MAX_RENDERS_PER_PASS:
|
|
299
|
+
raise RuntimeError(
|
|
300
|
+
"Too many re-renders: a component keeps requesting updates while committing "
|
|
301
|
+
"(for example an effect that sets state unconditionally)."
|
|
302
|
+
)
|
|
303
|
+
self._destroyed_tags.clear()
|
|
304
|
+
self._drain_dirty()
|
|
305
|
+
self._commit()
|
|
306
|
+
finally:
|
|
307
|
+
self._rendering = False
|
|
308
|
+
self._render_queued = False
|
|
309
|
+
|
|
310
|
+
def _has_dirty_work(self) -> bool:
|
|
311
|
+
return bool(self._dirty_nodes or self._dirty_boundaries or self._dirty_suspense)
|
|
312
|
+
|
|
313
|
+
def _drain_dirty(self) -> None:
|
|
314
|
+
"""Process dirty components and boundary retries until none remain."""
|
|
315
|
+
guard = 0
|
|
316
|
+
while self._has_dirty_work():
|
|
317
|
+
guard += 1
|
|
318
|
+
if guard > 100:
|
|
319
|
+
diagnostics.warn(
|
|
320
|
+
"Update loop did not settle after 100 iterations; a component is "
|
|
321
|
+
"likely setting state unconditionally during render or effects."
|
|
322
|
+
)
|
|
323
|
+
self._dirty_nodes.clear()
|
|
324
|
+
self._dirty_boundaries.clear()
|
|
325
|
+
self._dirty_suspense.clear()
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
boundaries = list(self._dirty_boundaries.values())
|
|
329
|
+
self._dirty_boundaries.clear()
|
|
330
|
+
for boundary in boundaries:
|
|
331
|
+
if boundary.mounted:
|
|
332
|
+
with self._providers_above(boundary):
|
|
333
|
+
self._local_update(
|
|
334
|
+
boundary, partial(self._reconcile_error_boundary, boundary, boundary.element)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
suspended = list(self._dirty_suspense.values())
|
|
338
|
+
self._dirty_suspense.clear()
|
|
339
|
+
for node in suspended:
|
|
340
|
+
if node.mounted and node.suspense_showing_fallback:
|
|
341
|
+
with self._providers_above(node):
|
|
342
|
+
try:
|
|
343
|
+
self._local_update(node, partial(self._attempt_suspense_content, node))
|
|
344
|
+
except Exception as exc:
|
|
345
|
+
self._route_error(node, exc)
|
|
346
|
+
|
|
347
|
+
pending = sorted(self._dirty_nodes.values(), key=VNode.depth)
|
|
348
|
+
self._dirty_nodes.clear()
|
|
349
|
+
for vnode in pending:
|
|
350
|
+
hs = vnode.hook_state
|
|
351
|
+
if not vnode.mounted or hs is None or not hs._dirty:
|
|
352
|
+
continue # already covered by a dirty ancestor's pass
|
|
353
|
+
try:
|
|
354
|
+
self._update_component(vnode)
|
|
355
|
+
except Suspend as signal:
|
|
356
|
+
self._route_suspend(vnode, signal)
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
self._route_error(vnode, exc)
|
|
359
|
+
|
|
360
|
+
def _commit(self) -> None:
|
|
361
|
+
"""Apply the staged transaction and run the post-commit phases."""
|
|
362
|
+
self._flush_ops()
|
|
363
|
+
self._run_layout()
|
|
364
|
+
self._flush_ops()
|
|
365
|
+
self._dispatch_layout_events()
|
|
366
|
+
self._flush_layout_effects()
|
|
367
|
+
self._flush_ops()
|
|
368
|
+
self._flush_passive_effects()
|
|
369
|
+
self._flush_ops()
|
|
370
|
+
|
|
371
|
+
@profiled("commit")
|
|
372
|
+
def _flush_ops(self) -> None:
|
|
373
|
+
"""Send pending ops to the backend and resolve created views."""
|
|
374
|
+
ops = self._ops
|
|
375
|
+
created = self._created
|
|
376
|
+
if ops:
|
|
377
|
+
self._ops = []
|
|
378
|
+
self._created = []
|
|
379
|
+
self.backend.apply_mutations(ops)
|
|
380
|
+
elif created:
|
|
381
|
+
self._created = []
|
|
382
|
+
for node in created:
|
|
383
|
+
if not node.mounted or node.tag is None:
|
|
384
|
+
continue
|
|
385
|
+
node.native_view = self.backend.resolve_view(node.tag)
|
|
386
|
+
self._attach_ref(node.element, node.native_view, node.tag)
|
|
387
|
+
ancestor = node.parent
|
|
388
|
+
scope = None
|
|
389
|
+
while ancestor is not None:
|
|
390
|
+
if scope is None and ancestor.hook_state is not None:
|
|
391
|
+
scope = ancestor.hook_state.task_scope
|
|
392
|
+
if not ancestor.is_native:
|
|
393
|
+
self._refresh_identity(ancestor)
|
|
394
|
+
ancestor = ancestor.parent
|
|
395
|
+
self._events.set_scope(node.tag, scope)
|
|
396
|
+
|
|
397
|
+
def _flush_layout_effects(self) -> None:
|
|
398
|
+
for state in sorted(
|
|
399
|
+
self._effect_states.values(), key=lambda hs: hs.vnode.depth() if hs.vnode else 0, reverse=True
|
|
400
|
+
):
|
|
401
|
+
if state.vnode is not None and state.vnode.mounted:
|
|
402
|
+
state.flush_layout_effects()
|
|
403
|
+
|
|
404
|
+
def _flush_passive_effects(self) -> None:
|
|
405
|
+
states, self._effect_states = self._effect_states, {}
|
|
406
|
+
for state in sorted(states.values(), key=lambda hs: hs.vnode.depth() if hs.vnode else 0, reverse=True):
|
|
407
|
+
if state.vnode is not None and state.vnode.mounted:
|
|
408
|
+
state.flush_pending_effects()
|
|
409
|
+
|
|
410
|
+
# ------------------------------------------------------------------
|
|
411
|
+
# Tree creation
|
|
412
|
+
# ------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def _create_tree(self, element: Element) -> VNode:
|
|
415
|
+
type_obj = element.type
|
|
416
|
+
if isinstance(type_obj, str):
|
|
417
|
+
return self._create_native(element)
|
|
418
|
+
if isinstance(type_obj, Component):
|
|
419
|
+
return self._create_component(element)
|
|
420
|
+
if isinstance(type_obj, Context):
|
|
421
|
+
return self._create_provider(element)
|
|
422
|
+
if type_obj is ERROR_BOUNDARY:
|
|
423
|
+
return self._create_error_boundary(element)
|
|
424
|
+
if type_obj is SUSPENSE:
|
|
425
|
+
return self._create_suspense(element)
|
|
426
|
+
if type_obj is FRAGMENT:
|
|
427
|
+
return self._create_wrapper(element, "Fragment")
|
|
428
|
+
raise TypeError(
|
|
429
|
+
f"Unsupported element type {type_obj!r}. Element types are native view names, "
|
|
430
|
+
"@component functions, contexts (providers), or the structural types."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def _create_native(self, element: Element) -> VNode:
|
|
434
|
+
tag = next_tag()
|
|
435
|
+
clean_props, events = self._split_props(element.props)
|
|
436
|
+
node = VNode(element, [], tag=tag)
|
|
437
|
+
self._tag_nodes[tag] = node
|
|
438
|
+
node.clean_props = clean_props
|
|
439
|
+
if events:
|
|
440
|
+
self._events.set_events(tag, events)
|
|
441
|
+
self._ops.append(CreateOp(tag, element.type, clean_props))
|
|
442
|
+
self._created.append(node)
|
|
443
|
+
|
|
444
|
+
index = 0
|
|
445
|
+
try:
|
|
446
|
+
for child_el in normalize_children(element.children, owner=element.type):
|
|
447
|
+
child = self._create_tree(child_el)
|
|
448
|
+
child.parent = node
|
|
449
|
+
node.children.append(child)
|
|
450
|
+
for root in self._native_roots(child):
|
|
451
|
+
if root.tag is not None:
|
|
452
|
+
self._ops.append(InsertOp(tag, root.tag, index))
|
|
453
|
+
index += 1
|
|
454
|
+
except Suspend:
|
|
455
|
+
# Don't leak the container (and already-built siblings) when
|
|
456
|
+
# a child suspends; hook states are salvaged for the retry.
|
|
457
|
+
if self._suspense_salvage is None:
|
|
458
|
+
self._suspense_salvage = {}
|
|
459
|
+
self._destroy_tree(node, salvage=self._suspense_salvage)
|
|
460
|
+
raise
|
|
461
|
+
except Exception:
|
|
462
|
+
self._destroy_tree(node)
|
|
463
|
+
raise
|
|
464
|
+
self._native_children[tag] = self._flattened_child_tags(node)
|
|
465
|
+
return node
|
|
466
|
+
|
|
467
|
+
def _create_component(self, element: Element) -> VNode:
|
|
468
|
+
# A retrying Suspense boundary may have preserved this
|
|
469
|
+
# component's hook state from the attempt that suspended;
|
|
470
|
+
# reclaiming it keeps cached resources warm.
|
|
471
|
+
hook_state = self._take_hydrated_hook_state(element) or HookState()
|
|
472
|
+
rendered = self._render_component_body(hook_state, element)
|
|
473
|
+
try:
|
|
474
|
+
children = self._create_child_list(rendered)
|
|
475
|
+
except Suspend:
|
|
476
|
+
# The body rendered fine but a descendant suspended: salvage
|
|
477
|
+
# this component's own state too, so the retry re-adopts it.
|
|
478
|
+
if self._suspense_salvage is None:
|
|
479
|
+
self._suspense_salvage = {}
|
|
480
|
+
self._suspense_salvage.setdefault((id(element.type), element.key), []).append(hook_state)
|
|
481
|
+
raise
|
|
482
|
+
node = VNode(element, children)
|
|
483
|
+
for child in children:
|
|
484
|
+
child.parent = node
|
|
485
|
+
node.hook_state = hook_state
|
|
486
|
+
node.rendered = rendered
|
|
487
|
+
self._refresh_identity(node)
|
|
488
|
+
hook_state.vnode = node
|
|
489
|
+
hook_state.owner = self
|
|
490
|
+
return node
|
|
491
|
+
|
|
492
|
+
def _create_provider(self, element: Element) -> VNode:
|
|
493
|
+
context: Context = element.type
|
|
494
|
+
context._push(element.props.get("value"))
|
|
495
|
+
try:
|
|
496
|
+
children = self._create_child_list(
|
|
497
|
+
normalize_children(element.children, owner=self._provider_label(context))
|
|
498
|
+
)
|
|
499
|
+
finally:
|
|
500
|
+
context._pop()
|
|
501
|
+
node = VNode(element, children)
|
|
502
|
+
for child in children:
|
|
503
|
+
child.parent = node
|
|
504
|
+
self._refresh_identity(node)
|
|
505
|
+
return node
|
|
506
|
+
|
|
507
|
+
def _create_wrapper(self, element: Element, owner: str) -> VNode:
|
|
508
|
+
children = self._create_child_list(normalize_children(element.children, owner=owner))
|
|
509
|
+
node = VNode(element, children)
|
|
510
|
+
for child in children:
|
|
511
|
+
child.parent = node
|
|
512
|
+
self._refresh_identity(node)
|
|
513
|
+
return node
|
|
514
|
+
|
|
515
|
+
def _create_child_list(self, elements: List[Element]) -> List[VNode]:
|
|
516
|
+
"""Create nodes for ``elements``, cleaning up on mid-list failure.
|
|
517
|
+
|
|
518
|
+
When the failure is a ``Suspend``, already-built siblings are
|
|
519
|
+
torn down with their hook states *salvaged*, so the boundary's
|
|
520
|
+
retry re-mounts them with their caches intact.
|
|
521
|
+
"""
|
|
522
|
+
nodes: List[VNode] = []
|
|
523
|
+
try:
|
|
524
|
+
for el in elements:
|
|
525
|
+
nodes.append(self._create_tree(el))
|
|
526
|
+
except Suspend:
|
|
527
|
+
if self._suspense_salvage is None:
|
|
528
|
+
self._suspense_salvage = {}
|
|
529
|
+
for node in nodes:
|
|
530
|
+
self._destroy_tree(node, salvage=self._suspense_salvage)
|
|
531
|
+
raise
|
|
532
|
+
except Exception:
|
|
533
|
+
for node in nodes:
|
|
534
|
+
self._destroy_tree(node)
|
|
535
|
+
raise
|
|
536
|
+
return nodes
|
|
537
|
+
|
|
538
|
+
# ------------------------------------------------------------------
|
|
539
|
+
# Component bodies
|
|
540
|
+
# ------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
@profiled("component")
|
|
543
|
+
def _render_component_body(self, hook_state: HookState, element: Element) -> List[Element]:
|
|
544
|
+
"""Render synchronously or suspend on a real, component-owned task."""
|
|
545
|
+
from ..profiling import count
|
|
546
|
+
|
|
547
|
+
count("components.rendered")
|
|
548
|
+
self._effect_states[id(hook_state)] = hook_state
|
|
549
|
+
component: Component = element.type
|
|
550
|
+
label = component.display_name
|
|
551
|
+
hook_state.owner = self
|
|
552
|
+
if component.is_async:
|
|
553
|
+
previous = hook_state._async_task
|
|
554
|
+
inputs = (element.props, element.children)
|
|
555
|
+
from ..equality import equal
|
|
556
|
+
|
|
557
|
+
if previous is not None and not equal(hook_state._async_inputs, inputs):
|
|
558
|
+
previous.cancel()
|
|
559
|
+
previous = hook_state._async_task = None
|
|
560
|
+
if previous is None:
|
|
561
|
+
|
|
562
|
+
async def render_body() -> Any:
|
|
563
|
+
hook_state.begin_render(label)
|
|
564
|
+
token = install_hook_state(hook_state)
|
|
565
|
+
try:
|
|
566
|
+
result = await component.render(element)
|
|
567
|
+
hook_state.finish_render()
|
|
568
|
+
return result
|
|
569
|
+
except BaseException:
|
|
570
|
+
hook_state.abort_render()
|
|
571
|
+
raise
|
|
572
|
+
finally:
|
|
573
|
+
restore_hook_state(token)
|
|
574
|
+
|
|
575
|
+
previous = hook_state.task_scope.create_task(render_body())
|
|
576
|
+
hook_state._async_task = previous
|
|
577
|
+
hook_state._async_inputs = inputs
|
|
578
|
+
if not previous.done():
|
|
579
|
+
signal = Suspend(previous, hook_state=hook_state, label=label)
|
|
580
|
+
signal.key = (id(element.type), element.key)
|
|
581
|
+
raise signal
|
|
582
|
+
hook_state._async_task = None
|
|
583
|
+
hook_state._dirty = False
|
|
584
|
+
return normalize_children(previous.result(), owner=label)
|
|
585
|
+
|
|
586
|
+
hook_state.begin_render(label)
|
|
587
|
+
token = install_hook_state(hook_state)
|
|
588
|
+
scope_token = _scope.set(hook_state.task_scope)
|
|
589
|
+
try:
|
|
590
|
+
rendered = component.render(element)
|
|
591
|
+
hook_state.finish_render()
|
|
592
|
+
except Suspend as signal:
|
|
593
|
+
hook_state.abort_render()
|
|
594
|
+
if signal.hook_state is None:
|
|
595
|
+
signal.hook_state = hook_state
|
|
596
|
+
if signal.key is None:
|
|
597
|
+
signal.key = (id(element.type), element.key)
|
|
598
|
+
if not signal.label:
|
|
599
|
+
signal.label = label
|
|
600
|
+
raise
|
|
601
|
+
finally:
|
|
602
|
+
_scope.reset(scope_token)
|
|
603
|
+
restore_hook_state(token)
|
|
604
|
+
hook_state._dirty = False
|
|
605
|
+
return normalize_children(rendered, owner=label)
|
|
606
|
+
|
|
607
|
+
def _register_component_retry(self, vnode: VNode, signal: Suspend) -> None:
|
|
608
|
+
"""Re-render ``vnode`` once the work it suspended on completes.
|
|
609
|
+
|
|
610
|
+
Update-time suspensions keep the previous content on screen (no
|
|
611
|
+
fallback flash) and re-run the body when the awaited work is
|
|
612
|
+
done, matching transition semantics.
|
|
613
|
+
"""
|
|
614
|
+
hook_state = vnode.hook_state
|
|
615
|
+
|
|
616
|
+
def _on_done(_waitable: Any) -> None:
|
|
617
|
+
if not vnode.mounted or vnode.hook_state is not hook_state or hook_state is None:
|
|
618
|
+
return
|
|
619
|
+
hook_state._dirty = True
|
|
620
|
+
self.mark_dirty(vnode)
|
|
621
|
+
schedule_trigger(self.request_render)
|
|
622
|
+
|
|
623
|
+
signal.waitable.add_done_callback(_on_done)
|
|
624
|
+
|
|
625
|
+
def _update_component(self, vnode: VNode) -> None:
|
|
626
|
+
"""Re-run one component's body and reconcile its subtree in place.
|
|
627
|
+
|
|
628
|
+
A local update starts in the middle of the tree, so the provider
|
|
629
|
+
chain above the component is re-established before the body
|
|
630
|
+
runs. When the body itself suspends, the previous subtree stays
|
|
631
|
+
on screen and the component re-renders once the awaited work
|
|
632
|
+
completes; suspensions from descendant mounts propagate to the
|
|
633
|
+
caller, which routes them to the nearest Suspense boundary.
|
|
634
|
+
"""
|
|
635
|
+
hook_state = vnode.hook_state
|
|
636
|
+
if hook_state is None or not vnode.is_component:
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
def work() -> None:
|
|
640
|
+
try:
|
|
641
|
+
rendered = self._render_component_body(hook_state, vnode.element)
|
|
642
|
+
except Suspend as signal:
|
|
643
|
+
if signal.hook_state is hook_state:
|
|
644
|
+
self._register_component_retry(vnode, signal)
|
|
645
|
+
return
|
|
646
|
+
raise
|
|
647
|
+
children = self._reconcile_child_list(vnode.children, rendered)
|
|
648
|
+
for child in children:
|
|
649
|
+
child.parent = vnode
|
|
650
|
+
vnode.children = children
|
|
651
|
+
vnode.rendered = rendered
|
|
652
|
+
self._refresh_identity(vnode)
|
|
653
|
+
hook_state.vnode = vnode
|
|
654
|
+
hook_state.owner = self
|
|
655
|
+
|
|
656
|
+
with self._providers_above(vnode):
|
|
657
|
+
self._local_update(vnode, work)
|
|
658
|
+
|
|
659
|
+
def _local_update(self, node: VNode, work: Callable[[], Any]) -> None:
|
|
660
|
+
"""Run ``work`` on ``node`` and repair the surrounding native structure.
|
|
661
|
+
|
|
662
|
+
Captures the nearest native container's child tags before the
|
|
663
|
+
work, then refreshes transparent ancestors' delegated identity
|
|
664
|
+
and re-syncs that container's children afterwards, so only the
|
|
665
|
+
moves the update actually caused reach the native side.
|
|
666
|
+
"""
|
|
667
|
+
before_roots = [root.tag for root in self._native_roots(node)]
|
|
668
|
+
container = self._nearest_native_ancestor(node)
|
|
669
|
+
before = self._native_children.get(container.tag, []) if container is not None else []
|
|
670
|
+
work()
|
|
671
|
+
current = node.parent
|
|
672
|
+
while current is not None and current is not container:
|
|
673
|
+
self._refresh_identity(current)
|
|
674
|
+
current = current.parent
|
|
675
|
+
after_roots = [root.tag for root in self._native_roots(node)]
|
|
676
|
+
if before_roots == after_roots:
|
|
677
|
+
return
|
|
678
|
+
if container is not None and container.tag is not None:
|
|
679
|
+
if self._sync_native_children(container.tag, before, self._flattened_child_roots(container)):
|
|
680
|
+
self._mark_layout_dirty(container)
|
|
681
|
+
|
|
682
|
+
def _route_error(self, vnode: VNode, exc: BaseException) -> None:
|
|
683
|
+
"""Route a local render failure to the nearest ``ErrorBoundary`` ancestor.
|
|
684
|
+
|
|
685
|
+
Without an enclosing boundary the exception propagates, exactly
|
|
686
|
+
as it would during a full render.
|
|
687
|
+
"""
|
|
688
|
+
node = vnode.parent
|
|
689
|
+
while node is not None:
|
|
690
|
+
if node.is_error_boundary:
|
|
691
|
+
with self._providers_above(node):
|
|
692
|
+
self._local_update(node, partial(self._activate_boundary, node, exc))
|
|
693
|
+
return
|
|
694
|
+
node = node.parent
|
|
695
|
+
raise exc
|
|
696
|
+
|
|
697
|
+
def _route_suspend(self, vnode: VNode, signal: Suspend) -> None:
|
|
698
|
+
"""Route a suspension from a local update to the nearest Suspense ancestor.
|
|
699
|
+
|
|
700
|
+
The child reconcile that suspended may have left the boundary's
|
|
701
|
+
content partially updated, so the whole content is torn down and
|
|
702
|
+
rebuilt through the fallback-and-retry path (hook states are
|
|
703
|
+
preserved for the retry).
|
|
704
|
+
"""
|
|
705
|
+
node = vnode.parent
|
|
706
|
+
while node is not None:
|
|
707
|
+
if node.is_suspense:
|
|
708
|
+
with self._providers_above(node):
|
|
709
|
+
|
|
710
|
+
def work(n: VNode = node) -> None:
|
|
711
|
+
self._teardown_suspense_content(n)
|
|
712
|
+
self._suspend_boundary(n, signal)
|
|
713
|
+
|
|
714
|
+
self._local_update(node, work)
|
|
715
|
+
return
|
|
716
|
+
node = node.parent
|
|
717
|
+
self._discard_salvage()
|
|
718
|
+
raise self._missing_suspense_error(signal) from None
|
|
719
|
+
|
|
720
|
+
@contextmanager
|
|
721
|
+
def _providers_above(self, vnode: VNode) -> Iterator[None]:
|
|
722
|
+
"""Push the provided values of every provider above ``vnode``, outermost first."""
|
|
723
|
+
chain: List[Tuple[Context, Any]] = []
|
|
724
|
+
node = vnode.parent
|
|
725
|
+
while node is not None:
|
|
726
|
+
if node.is_provider:
|
|
727
|
+
chain.append((node.element.type, node.element.props.get("value")))
|
|
728
|
+
node = node.parent
|
|
729
|
+
chain.reverse()
|
|
730
|
+
for context, value in chain:
|
|
731
|
+
context._push(value)
|
|
732
|
+
try:
|
|
733
|
+
yield
|
|
734
|
+
finally:
|
|
735
|
+
for context, _value in reversed(chain):
|
|
736
|
+
context._pop()
|
|
737
|
+
|
|
738
|
+
# ------------------------------------------------------------------
|
|
739
|
+
# Reconciliation
|
|
740
|
+
# ------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
def _reconcile_node(self, old: VNode, new_el: Element) -> VNode:
|
|
743
|
+
if not self._same_type(old.element, new_el):
|
|
744
|
+
new_node = self._create_tree(new_el)
|
|
745
|
+
self._destroy_tree(old)
|
|
746
|
+
return new_node
|
|
747
|
+
if old.is_native:
|
|
748
|
+
from ..equality import equal
|
|
749
|
+
from ..sdk.schema import COMPONENTS
|
|
750
|
+
|
|
751
|
+
schema = COMPONENTS.get(new_el.type)
|
|
752
|
+
if schema is not None and any(
|
|
753
|
+
field.get("native", {}).get("recreate") and not equal(old.element.props.get(key), new_el.props.get(key))
|
|
754
|
+
for key, field in schema.props.items()
|
|
755
|
+
):
|
|
756
|
+
new_node = self._create_tree(new_el)
|
|
757
|
+
self._destroy_tree(old)
|
|
758
|
+
return new_node
|
|
759
|
+
return self._reconcile_native(old, new_el)
|
|
760
|
+
if old.is_component:
|
|
761
|
+
return self._reconcile_component(old, new_el)
|
|
762
|
+
if old.is_provider:
|
|
763
|
+
return self._reconcile_provider(old, new_el)
|
|
764
|
+
if old.is_error_boundary:
|
|
765
|
+
return self._reconcile_error_boundary(old, new_el)
|
|
766
|
+
if old.is_suspense:
|
|
767
|
+
return self._reconcile_suspense(old, new_el)
|
|
768
|
+
return self._reconcile_wrapper(old, new_el, "Fragment")
|
|
769
|
+
|
|
770
|
+
def _reconcile_native(self, old: VNode, new_el: Element) -> VNode:
|
|
771
|
+
new_clean, events = self._split_props(new_el.props)
|
|
772
|
+
if old.tag is not None:
|
|
773
|
+
self._events.set_events(old.tag, events)
|
|
774
|
+
changed = self._diff_props(old.clean_props, new_clean)
|
|
775
|
+
if changed:
|
|
776
|
+
if old.tag is not None:
|
|
777
|
+
self._ops.append(UpdateOp(old.tag, changed))
|
|
778
|
+
old.measure_cache = None
|
|
779
|
+
if affects_layout(old.element.type, changed):
|
|
780
|
+
self._mark_layout_dirty(old)
|
|
781
|
+
old.clean_props = new_clean
|
|
782
|
+
|
|
783
|
+
old_ref = old.element.props.get("ref")
|
|
784
|
+
new_ref = new_el.props.get("ref")
|
|
785
|
+
if old_ref is not new_ref:
|
|
786
|
+
self._clear_ref(old_ref)
|
|
787
|
+
self._attach_ref(new_el, old.native_view, old.tag)
|
|
788
|
+
|
|
789
|
+
before = self._flattened_child_tags(old)
|
|
790
|
+
children = self._reconcile_child_list(old.children, normalize_children(new_el.children, owner=new_el.type))
|
|
791
|
+
old.children = children
|
|
792
|
+
for child in children:
|
|
793
|
+
child.parent = old
|
|
794
|
+
old.element = new_el
|
|
795
|
+
if old.tag is not None and self._sync_native_children(old.tag, before, self._flattened_child_roots(old)):
|
|
796
|
+
self._mark_layout_dirty(old)
|
|
797
|
+
return old
|
|
798
|
+
|
|
799
|
+
def _reconcile_component(self, old: VNode, new_el: Element) -> VNode:
|
|
800
|
+
if self._can_skip_memoized(old, new_el):
|
|
801
|
+
old.element = new_el
|
|
802
|
+
return old
|
|
803
|
+
hook_state = old.hook_state or HookState()
|
|
804
|
+
rendered = self._render_component_body(hook_state, new_el)
|
|
805
|
+
children = self._reconcile_child_list(old.children, rendered)
|
|
806
|
+
old.children = children
|
|
807
|
+
for child in children:
|
|
808
|
+
child.parent = old
|
|
809
|
+
old.element = new_el
|
|
810
|
+
old.hook_state = hook_state
|
|
811
|
+
old.rendered = rendered
|
|
812
|
+
self._refresh_identity(old)
|
|
813
|
+
hook_state.vnode = old
|
|
814
|
+
hook_state.owner = self
|
|
815
|
+
return old
|
|
816
|
+
|
|
817
|
+
def _reconcile_provider(self, old: VNode, new_el: Element) -> VNode:
|
|
818
|
+
context: Context = new_el.type
|
|
819
|
+
old_value = old.element.props.get("value", _MISSING)
|
|
820
|
+
new_value = new_el.props.get("value")
|
|
821
|
+
if self._value_changed(old_value, new_value):
|
|
822
|
+
self._mark_context_consumers(old, context)
|
|
823
|
+
context._push(new_value)
|
|
824
|
+
try:
|
|
825
|
+
children = self._reconcile_child_list(
|
|
826
|
+
old.children, normalize_children(new_el.children, owner=self._provider_label(context))
|
|
827
|
+
)
|
|
828
|
+
finally:
|
|
829
|
+
context._pop()
|
|
830
|
+
old.children = children
|
|
831
|
+
for child in children:
|
|
832
|
+
child.parent = old
|
|
833
|
+
old.element = new_el
|
|
834
|
+
self._refresh_identity(old)
|
|
835
|
+
return old
|
|
836
|
+
|
|
837
|
+
def _reconcile_wrapper(self, old: VNode, new_el: Element, owner: str) -> VNode:
|
|
838
|
+
children = self._reconcile_child_list(old.children, normalize_children(new_el.children, owner=owner))
|
|
839
|
+
old.children = children
|
|
840
|
+
for child in children:
|
|
841
|
+
child.parent = old
|
|
842
|
+
old.element = new_el
|
|
843
|
+
self._refresh_identity(old)
|
|
844
|
+
return old
|
|
845
|
+
|
|
846
|
+
def _mark_context_consumers(self, provider: VNode, context: Context) -> None:
|
|
847
|
+
"""Mark every descendant that read ``context`` for re-render.
|
|
848
|
+
|
|
849
|
+
Descent is pruned at nested providers of the same context, since
|
|
850
|
+
their subtrees read the inner (unchanged) value.
|
|
851
|
+
"""
|
|
852
|
+
target = id(context)
|
|
853
|
+
|
|
854
|
+
def walk(node: VNode) -> None:
|
|
855
|
+
for child in node.children:
|
|
856
|
+
if child.is_provider and child.element.type is context:
|
|
857
|
+
continue
|
|
858
|
+
hs = child.hook_state
|
|
859
|
+
if hs is not None and target in hs.context_deps:
|
|
860
|
+
hs._dirty = True
|
|
861
|
+
self._dirty_nodes[id(child)] = child
|
|
862
|
+
walk(child)
|
|
863
|
+
|
|
864
|
+
walk(provider)
|
|
865
|
+
|
|
866
|
+
@staticmethod
|
|
867
|
+
def _can_skip_memoized(old: VNode, new_el: Element) -> bool:
|
|
868
|
+
"""Whether a memoized component can reuse its last render.
|
|
869
|
+
|
|
870
|
+
Requires the component to be memoized, to have rendered before,
|
|
871
|
+
to have no pending state change (``hook_state._dirty``), and
|
|
872
|
+
props that compare equal under its comparator (shallow equality
|
|
873
|
+
by default), children included.
|
|
874
|
+
"""
|
|
875
|
+
component: Component = new_el.type
|
|
876
|
+
if not component.memoized or old.rendered is None or old.hook_state is None or old.hook_state._dirty:
|
|
877
|
+
return False
|
|
878
|
+
compare = component.props_equal or shallow_equal_props
|
|
879
|
+
try:
|
|
880
|
+
return bool(compare(old.element.props, new_el.props)) and old.element.children == new_el.children
|
|
881
|
+
except Exception:
|
|
882
|
+
return False
|
|
883
|
+
|
|
884
|
+
def _reconcile_child_list(self, old_children: List[VNode], new_children: List[Element]) -> List[VNode]:
|
|
885
|
+
"""Match, reconcile, create, and destroy one level of children.
|
|
886
|
+
|
|
887
|
+
Keyed children match by key; unkeyed children match by position
|
|
888
|
+
among the unkeyed siblings. Type changes replace the node. This
|
|
889
|
+
pass is purely structural: native attachment order is derived
|
|
890
|
+
afterwards by the nearest native container via
|
|
891
|
+
[`_sync_native_children`][pythonnative.reconciler.core.Reconciler._sync_native_children].
|
|
892
|
+
|
|
893
|
+
On failure, any freshly created replacement nodes are destroyed
|
|
894
|
+
before the exception propagates so an enclosing boundary can
|
|
895
|
+
swap in its fallback without leaking native views.
|
|
896
|
+
"""
|
|
897
|
+
old_by_key: Dict[Any, VNode] = {}
|
|
898
|
+
old_unkeyed: List[VNode] = []
|
|
899
|
+
for child in old_children:
|
|
900
|
+
if child.element.key is not None:
|
|
901
|
+
old_by_key[child.element.key] = child
|
|
902
|
+
else:
|
|
903
|
+
old_unkeyed.append(child)
|
|
904
|
+
|
|
905
|
+
result: List[VNode] = []
|
|
906
|
+
fresh: List[VNode] = []
|
|
907
|
+
used_keys: Set[Any] = set()
|
|
908
|
+
unkeyed_iter = iter(old_unkeyed)
|
|
909
|
+
try:
|
|
910
|
+
for new_el in new_children:
|
|
911
|
+
matched: Optional[VNode] = None
|
|
912
|
+
if new_el.key is not None:
|
|
913
|
+
matched = old_by_key.get(new_el.key)
|
|
914
|
+
if matched is not None:
|
|
915
|
+
used_keys.add(new_el.key)
|
|
916
|
+
else:
|
|
917
|
+
matched = next(unkeyed_iter, None)
|
|
918
|
+
|
|
919
|
+
if matched is None:
|
|
920
|
+
node = self._create_tree(new_el)
|
|
921
|
+
fresh.append(node)
|
|
922
|
+
result.append(node)
|
|
923
|
+
elif not self._same_type(matched.element, new_el):
|
|
924
|
+
node = self._create_tree(new_el)
|
|
925
|
+
self._destroy_tree(matched)
|
|
926
|
+
fresh.append(node)
|
|
927
|
+
result.append(node)
|
|
928
|
+
else:
|
|
929
|
+
result.append(self._reconcile_node(matched, new_el))
|
|
930
|
+
except Exception:
|
|
931
|
+
for node in fresh:
|
|
932
|
+
self._destroy_tree(node)
|
|
933
|
+
raise
|
|
934
|
+
|
|
935
|
+
for key, node in old_by_key.items():
|
|
936
|
+
if key not in used_keys:
|
|
937
|
+
self._destroy_tree(node)
|
|
938
|
+
for node in unkeyed_iter:
|
|
939
|
+
self._destroy_tree(node)
|
|
940
|
+
return result
|
|
941
|
+
|
|
942
|
+
# ------------------------------------------------------------------
|
|
943
|
+
# Destruction
|
|
944
|
+
# ------------------------------------------------------------------
|
|
945
|
+
|
|
946
|
+
def _destroy_tree(self, node: VNode, salvage: Optional[HydrationMap] = None) -> None:
|
|
947
|
+
"""Tear down a subtree, destroying native views and cleaning hook state.
|
|
948
|
+
|
|
949
|
+
Args:
|
|
950
|
+
node: Root of the subtree to destroy.
|
|
951
|
+
salvage: When given (a Suspense boundary is unwinding),
|
|
952
|
+
component hook states move into this map keyed by
|
|
953
|
+
``(component identity, element key)`` instead of being
|
|
954
|
+
cleaned up, so the boundary's retry can re-adopt them.
|
|
955
|
+
"""
|
|
956
|
+
if not node.mounted:
|
|
957
|
+
return
|
|
958
|
+
node.mounted = False
|
|
959
|
+
self._dirty_nodes.pop(id(node), None)
|
|
960
|
+
self._dirty_boundaries.pop(id(node), None)
|
|
961
|
+
self._dirty_suspense.pop(id(node), None)
|
|
962
|
+
if node.suspense_hydration:
|
|
963
|
+
self._dispose_hydration(node.suspense_hydration)
|
|
964
|
+
node.suspense_hydration = None
|
|
965
|
+
hs = node.hook_state
|
|
966
|
+
if hs is not None:
|
|
967
|
+
self._effect_states.pop(id(hs), None)
|
|
968
|
+
if salvage is not None and node.is_component:
|
|
969
|
+
salvage.setdefault((id(node.element.type), node.element.key), []).append(hs)
|
|
970
|
+
else:
|
|
971
|
+
hs.cleanup_all_effects()
|
|
972
|
+
hs.detach()
|
|
973
|
+
self._clear_ref(node.element.props.get("ref"))
|
|
974
|
+
for child in node.children:
|
|
975
|
+
self._destroy_tree(child, salvage=salvage)
|
|
976
|
+
if node.is_native and node.tag is not None:
|
|
977
|
+
self._native_children.pop(node.tag, None)
|
|
978
|
+
self._tag_nodes.pop(node.tag, None)
|
|
979
|
+
self._events.clear(node.tag)
|
|
980
|
+
self._ops.append(DestroyOp(node.tag))
|
|
981
|
+
self._destroyed_tags.add(node.tag)
|
|
982
|
+
node.children = []
|
|
983
|
+
node.parent = None
|
|
984
|
+
node.layout_node = None
|
|
985
|
+
|
|
986
|
+
# ------------------------------------------------------------------
|
|
987
|
+
# Native structure helpers
|
|
988
|
+
# ------------------------------------------------------------------
|
|
989
|
+
|
|
990
|
+
def _native_roots(self, node: VNode) -> List[VNode]:
|
|
991
|
+
"""The native views ``node`` contributes to its native parent, in order.
|
|
992
|
+
|
|
993
|
+
Native elements contribute themselves, except ``Portal``, whose
|
|
994
|
+
handler self-attaches to a top-level overlay. Transparent
|
|
995
|
+
wrappers contribute the concatenation of their children's roots.
|
|
996
|
+
"""
|
|
997
|
+
if node.is_native:
|
|
998
|
+
return [] if node.element.type == "Portal" else [node]
|
|
999
|
+
roots: List[VNode] = []
|
|
1000
|
+
for child in node.children:
|
|
1001
|
+
roots.extend(self._native_roots(child))
|
|
1002
|
+
return roots
|
|
1003
|
+
|
|
1004
|
+
def _flattened_child_roots(self, node: VNode) -> List[VNode]:
|
|
1005
|
+
roots: List[VNode] = []
|
|
1006
|
+
for child in node.children:
|
|
1007
|
+
roots.extend(self._native_roots(child))
|
|
1008
|
+
return roots
|
|
1009
|
+
|
|
1010
|
+
def _flattened_child_tags(self, node: VNode) -> List[int]:
|
|
1011
|
+
return [root.tag for root in self._flattened_child_roots(node) if root.tag is not None]
|
|
1012
|
+
|
|
1013
|
+
@staticmethod
|
|
1014
|
+
def _nearest_native_ancestor(node: VNode) -> Optional[VNode]:
|
|
1015
|
+
current = node.parent
|
|
1016
|
+
while current is not None:
|
|
1017
|
+
if current.is_native:
|
|
1018
|
+
return current
|
|
1019
|
+
current = current.parent
|
|
1020
|
+
return None
|
|
1021
|
+
|
|
1022
|
+
def _refresh_identity(self, node: VNode) -> None:
|
|
1023
|
+
"""Point a wrapper's ``tag`` / ``native_view`` at its first native root."""
|
|
1024
|
+
if node.is_native:
|
|
1025
|
+
return
|
|
1026
|
+
for root in self._native_roots(node):
|
|
1027
|
+
node.tag = root.tag
|
|
1028
|
+
node.native_view = root.native_view
|
|
1029
|
+
return
|
|
1030
|
+
node.tag = None
|
|
1031
|
+
node.native_view = None
|
|
1032
|
+
|
|
1033
|
+
def _sync_native_children(self, parent_tag: int, before_tags: List[int], after_roots: List[VNode]) -> bool:
|
|
1034
|
+
"""Emit the inserts that turn the parent's native child list into ``after_roots``.
|
|
1035
|
+
|
|
1036
|
+
Children destroyed this pass have already had their views torn
|
|
1037
|
+
down, so they're dropped from the "before" picture; the
|
|
1038
|
+
remaining difference is expressed as the minimal move-aware
|
|
1039
|
+
insert sequence (longest-increasing-subsequence based, so a
|
|
1040
|
+
keyed reorder of *n* children costs at most *n* minus the
|
|
1041
|
+
length of the already-ordered run).
|
|
1042
|
+
|
|
1043
|
+
Returns whether the native child list changed at all.
|
|
1044
|
+
"""
|
|
1045
|
+
self._native_children[parent_tag] = [root.tag for root in after_roots if root.tag is not None]
|
|
1046
|
+
surviving = [t for t in before_tags if t not in self._destroyed_tags]
|
|
1047
|
+
after_tags = [r.tag for r in after_roots if r.tag is not None]
|
|
1048
|
+
if surviving == after_tags:
|
|
1049
|
+
return len(surviving) != len(before_tags)
|
|
1050
|
+
for tag, index in plan_child_moves(surviving, after_tags):
|
|
1051
|
+
self._ops.append(InsertOp(parent_tag, tag, index))
|
|
1052
|
+
return True
|
|
1053
|
+
|
|
1054
|
+
def _warn_on_multiple_roots(self) -> None:
|
|
1055
|
+
if not diagnostics.is_dev() or self.root is None:
|
|
1056
|
+
return
|
|
1057
|
+
roots = self._native_roots(self.root)
|
|
1058
|
+
if len(roots) > 1:
|
|
1059
|
+
diagnostics.warn_once(
|
|
1060
|
+
f"The screen root rendered {len(roots)} native views; only the first "
|
|
1061
|
+
"is attached to the window. Wrap your root in a View/Column (Portals "
|
|
1062
|
+
"are exempt and may appear anywhere).",
|
|
1063
|
+
key=f"multi-root:{id(self)}",
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
# ------------------------------------------------------------------
|
|
1067
|
+
# Props and refs
|
|
1068
|
+
# ------------------------------------------------------------------
|
|
1069
|
+
|
|
1070
|
+
@staticmethod
|
|
1071
|
+
def _split_props(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Callable[..., Any]]]:
|
|
1072
|
+
"""Strip reconciler-owned keys, then split event callables from native props."""
|
|
1073
|
+
if not props:
|
|
1074
|
+
return {}, {}
|
|
1075
|
+
stripped = {key: value for key, value in props.items() if key not in _RECONCILER_OWNED_PROPS}
|
|
1076
|
+
clean, events = extract_events(stripped)
|
|
1077
|
+
from ..animated import AnimatedEvent
|
|
1078
|
+
|
|
1079
|
+
bindings = {
|
|
1080
|
+
name: {field: id(value) for field, value in callback._bindings.items()}
|
|
1081
|
+
for name, callback in events.items()
|
|
1082
|
+
if isinstance(callback, AnimatedEvent)
|
|
1083
|
+
}
|
|
1084
|
+
if bindings:
|
|
1085
|
+
clean["_pn_animated_events"] = bindings
|
|
1086
|
+
return clean, events
|
|
1087
|
+
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _diff_props(old: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:
|
|
1090
|
+
"""Return only the props that changed between two clean prop dicts (removed props map to ``None``)."""
|
|
1091
|
+
changed: Dict[str, Any] = {}
|
|
1092
|
+
for key, new_val in new.items():
|
|
1093
|
+
if key not in old:
|
|
1094
|
+
changed[key] = new_val
|
|
1095
|
+
continue
|
|
1096
|
+
old_val = old[key]
|
|
1097
|
+
try:
|
|
1098
|
+
if callable(new_val) or callable(old_val):
|
|
1099
|
+
if old_val is not new_val:
|
|
1100
|
+
changed[key] = new_val
|
|
1101
|
+
elif old_val != new_val:
|
|
1102
|
+
changed[key] = new_val
|
|
1103
|
+
except Exception:
|
|
1104
|
+
changed[key] = new_val
|
|
1105
|
+
for key in old:
|
|
1106
|
+
if key not in new:
|
|
1107
|
+
changed[key] = None
|
|
1108
|
+
return changed
|
|
1109
|
+
|
|
1110
|
+
@staticmethod
|
|
1111
|
+
def _value_changed(old_value: Any, new_value: Any) -> bool:
|
|
1112
|
+
if old_value is _MISSING:
|
|
1113
|
+
return True
|
|
1114
|
+
if old_value is new_value:
|
|
1115
|
+
return False
|
|
1116
|
+
try:
|
|
1117
|
+
return bool(old_value != new_value)
|
|
1118
|
+
except Exception:
|
|
1119
|
+
return True
|
|
1120
|
+
|
|
1121
|
+
@staticmethod
|
|
1122
|
+
def _attach_ref(element: Element, native_view: Any, tag: Optional[int]) -> None:
|
|
1123
|
+
ref = element.props.get("ref")
|
|
1124
|
+
if ref is None:
|
|
1125
|
+
return
|
|
1126
|
+
if hasattr(ref, "current"):
|
|
1127
|
+
ref.current = native_view
|
|
1128
|
+
try:
|
|
1129
|
+
ref._pn_tag = tag
|
|
1130
|
+
except Exception:
|
|
1131
|
+
pass
|
|
1132
|
+
elif diagnostics.is_dev():
|
|
1133
|
+
diagnostics.warn_once(
|
|
1134
|
+
f"Ignoring ref of type {type(ref).__name__}; pass the Ref returned by use_ref().",
|
|
1135
|
+
key=f"badref:{type(ref).__name__}",
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
@staticmethod
|
|
1139
|
+
def _clear_ref(ref: Any) -> None:
|
|
1140
|
+
if ref is None or not hasattr(ref, "current"):
|
|
1141
|
+
return
|
|
1142
|
+
try:
|
|
1143
|
+
ref.current = None
|
|
1144
|
+
ref._pn_tag = None
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1147
|
+
|
|
1148
|
+
@staticmethod
|
|
1149
|
+
def _same_type(old_el: Element, new_el: Element) -> bool:
|
|
1150
|
+
if isinstance(old_el.type, str):
|
|
1151
|
+
return old_el.type == new_el.type
|
|
1152
|
+
return old_el.type is new_el.type
|
|
1153
|
+
|
|
1154
|
+
@staticmethod
|
|
1155
|
+
def _provider_label(context: Context) -> str:
|
|
1156
|
+
return f"{context.name}.Provider" if context.name else "Provider"
|
|
1157
|
+
|
|
1158
|
+
@staticmethod
|
|
1159
|
+
def _log(msg: str) -> None:
|
|
1160
|
+
if os.environ.get("PYTHONNATIVE_DEBUG", "").lower() not in {"1", "true", "yes", "on"}:
|
|
1161
|
+
return
|
|
1162
|
+
try:
|
|
1163
|
+
print(f"[PN] reconciler: {msg}", flush=True)
|
|
1164
|
+
except Exception:
|
|
1165
|
+
pass
|