pythonnative 0.40.0__cp313-cp313-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.cp313-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,824 @@
|
|
|
1
|
+
"""The on-device dev client: sync sources from ``pn start`` and Fast Refresh.
|
|
2
|
+
|
|
3
|
+
A debug build of a PythonNative app is a *dev client*. On launch it
|
|
4
|
+
connects to the dev server (``pn start`` / ``pn preview`` / ``pn run``)
|
|
5
|
+
over WebSocket, reports the sources it already holds, receives whatever
|
|
6
|
+
is missing, and from then on applies every save as a Fast Refresh. Its
|
|
7
|
+
``print`` output and errors stream back to the terminal running the
|
|
8
|
+
server, so the device log viewer is optional.
|
|
9
|
+
|
|
10
|
+
How the pieces fit:
|
|
11
|
+
|
|
12
|
+
- The native template configures a writable **overlay** directory
|
|
13
|
+
(``pythonnative.hot_reload.configure_dev_environment``) ahead of the
|
|
14
|
+
bundled sources on ``sys.path`` and exposes the server URL the CLI
|
|
15
|
+
baked in as ``PN_DEV_SERVER``. ``pythonnative.bootstrap.start(dev=True)``
|
|
16
|
+
calls [`start_if_configured`][pythonnative.devclient.start_if_configured].
|
|
17
|
+
- A build made with ``pn run <platform> --dev-client`` has no app of
|
|
18
|
+
its own: its bundled ``app/main.py`` renders
|
|
19
|
+
[`ConnectScreen`][pythonnative.devclient.ConnectScreen], where the
|
|
20
|
+
developer types (or picks) a server URL. Once the first sync lands,
|
|
21
|
+
the real ``app.main`` from the overlay shadows the placeholder and
|
|
22
|
+
the screen remounts into the developer's app. The URL is remembered
|
|
23
|
+
for the next launch.
|
|
24
|
+
|
|
25
|
+
All network I/O runs on a daemon thread; file writes happen there too,
|
|
26
|
+
and only the reload itself hops to the main thread through
|
|
27
|
+
``call_on_application_thread``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import base64
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import socket
|
|
37
|
+
import sys
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
40
|
+
import traceback
|
|
41
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
42
|
+
|
|
43
|
+
from .devserver import ws
|
|
44
|
+
from .devserver.watcher import is_synced_file, modules_for_paths
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"ConnectScreen",
|
|
48
|
+
"DevClient",
|
|
49
|
+
"current",
|
|
50
|
+
"normalize_server_url",
|
|
51
|
+
"saved_server_url",
|
|
52
|
+
"start",
|
|
53
|
+
"start_if_configured",
|
|
54
|
+
"stop",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
SERVER_URL_ENV = "PN_DEV_SERVER"
|
|
58
|
+
"""Environment variable carrying the dev server URL baked in by ``pn run``."""
|
|
59
|
+
|
|
60
|
+
DEV_CLIENT_ENV = "PN_DEV_CLIENT"
|
|
61
|
+
"""Set to ``1`` in ``--dev-client`` builds (the connect screen remembers URLs)."""
|
|
62
|
+
|
|
63
|
+
_SAVED_URL_FILE = "server.json"
|
|
64
|
+
_RECONNECT_DELAYS = (0.5, 1.0, 2.0, 3.0, 5.0)
|
|
65
|
+
|
|
66
|
+
Logger = Callable[[str], None]
|
|
67
|
+
|
|
68
|
+
_current: Optional["DevClient"] = None
|
|
69
|
+
_lock = threading.Lock()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ======================================================================
|
|
73
|
+
# URLs
|
|
74
|
+
# ======================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def normalize_server_url(text: str) -> str:
|
|
78
|
+
"""Turn whatever the developer typed into a dev-client WebSocket URL.
|
|
79
|
+
|
|
80
|
+
Accepts ``192.168.1.20``, ``192.168.1.20:8765``, ``http://host:port``,
|
|
81
|
+
``ws://host:port``, and full URLs with a path; the result always
|
|
82
|
+
ends in ``/ws?role=client``.
|
|
83
|
+
"""
|
|
84
|
+
value = (text or "").strip()
|
|
85
|
+
if not value:
|
|
86
|
+
raise ValueError("empty server address")
|
|
87
|
+
if "://" not in value:
|
|
88
|
+
value = "ws://" + value
|
|
89
|
+
value = value.replace("http://", "ws://", 1).replace("https://", "wss://", 1)
|
|
90
|
+
scheme, _, rest = value.partition("://")
|
|
91
|
+
hostport, _, path = rest.partition("/")
|
|
92
|
+
if ":" not in hostport:
|
|
93
|
+
from .devserver.server import DEFAULT_PORT
|
|
94
|
+
|
|
95
|
+
hostport = f"{hostport}:{DEFAULT_PORT}"
|
|
96
|
+
if not path or path.startswith("?"):
|
|
97
|
+
path = "ws?role=client"
|
|
98
|
+
elif "role=" not in path:
|
|
99
|
+
path = path.rstrip("/")
|
|
100
|
+
path = (path + ("&" if "?" in path else "?") + "role=client") if path.startswith("ws") else "ws?role=client"
|
|
101
|
+
return f"{scheme}://{hostport}/{path}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _saved_url_path(overlay: str) -> str:
|
|
105
|
+
return os.path.join(overlay, _SAVED_URL_FILE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def saved_server_url(overlay: Optional[str] = None) -> Optional[str]:
|
|
109
|
+
"""The URL remembered by a previous connection (``--dev-client`` builds)."""
|
|
110
|
+
from .hot_reload import overlay_root
|
|
111
|
+
|
|
112
|
+
root = overlay or overlay_root()
|
|
113
|
+
if not root:
|
|
114
|
+
return None
|
|
115
|
+
try:
|
|
116
|
+
with open(_saved_url_path(root), encoding="utf-8") as handle:
|
|
117
|
+
data = json.load(handle)
|
|
118
|
+
except (OSError, ValueError):
|
|
119
|
+
return None
|
|
120
|
+
url = data.get("url") if isinstance(data, dict) else None
|
|
121
|
+
return str(url) if url else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _save_server_url(overlay: str, url: str) -> None:
|
|
125
|
+
try:
|
|
126
|
+
os.makedirs(overlay, exist_ok=True)
|
|
127
|
+
with open(_saved_url_path(overlay), "w", encoding="utf-8") as handle:
|
|
128
|
+
json.dump({"url": url, "saved_at": time.time()}, handle)
|
|
129
|
+
except OSError:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ======================================================================
|
|
134
|
+
# Log forwarding
|
|
135
|
+
# ======================================================================
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _Tee:
|
|
139
|
+
"""A text stream that writes through and also forwards whole lines."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, inner: Any, forward: Callable[[str], None]) -> None:
|
|
142
|
+
self._inner = inner
|
|
143
|
+
self._forward = forward
|
|
144
|
+
self._buffer = ""
|
|
145
|
+
|
|
146
|
+
def write(self, text: str) -> int:
|
|
147
|
+
try:
|
|
148
|
+
self._inner.write(text)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
self._buffer += text
|
|
152
|
+
if "\n" in self._buffer:
|
|
153
|
+
lines = self._buffer.split("\n")
|
|
154
|
+
self._buffer = lines.pop()
|
|
155
|
+
for line in lines:
|
|
156
|
+
if line:
|
|
157
|
+
self._forward(line)
|
|
158
|
+
return len(text)
|
|
159
|
+
|
|
160
|
+
def flush(self) -> None:
|
|
161
|
+
try:
|
|
162
|
+
self._inner.flush()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
if self._buffer:
|
|
166
|
+
line, self._buffer = self._buffer, ""
|
|
167
|
+
self._forward(line)
|
|
168
|
+
|
|
169
|
+
def isatty(self) -> bool:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def fileno(self) -> int:
|
|
173
|
+
return self._inner.fileno()
|
|
174
|
+
|
|
175
|
+
def __getattr__(self, name: str) -> Any:
|
|
176
|
+
return getattr(self._inner, name)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ======================================================================
|
|
180
|
+
# The client
|
|
181
|
+
# ======================================================================
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class DevClient:
|
|
185
|
+
"""Keep this process in sync with a dev server.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
url: Server URL (any form accepted by
|
|
189
|
+
[`normalize_server_url`][pythonnative.devclient.normalize_server_url]).
|
|
190
|
+
overlay: Writable directory that shadows the bundled sources.
|
|
191
|
+
entry_module: The app's entry module, for the ``hello`` message.
|
|
192
|
+
forward_logs: Mirror ``print`` output to the server.
|
|
193
|
+
log: Local logger for the client's own status lines.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
url: str,
|
|
199
|
+
overlay: str,
|
|
200
|
+
*,
|
|
201
|
+
entry_module: str = "app.main",
|
|
202
|
+
forward_logs: bool = True,
|
|
203
|
+
log: Optional[Logger] = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
self.url = normalize_server_url(url)
|
|
206
|
+
self.overlay = os.path.abspath(overlay)
|
|
207
|
+
self.entry_module = entry_module
|
|
208
|
+
self._forward_logs = forward_logs
|
|
209
|
+
self._log: Logger = log or self._default_log
|
|
210
|
+
self._thread: Optional[threading.Thread] = None
|
|
211
|
+
self._stop = threading.Event()
|
|
212
|
+
self._socket: Optional[ws.WebSocketClient] = None
|
|
213
|
+
self._state = "idle"
|
|
214
|
+
self._state_lock = threading.Lock()
|
|
215
|
+
self._listeners: List[Callable[[str, str], None]] = []
|
|
216
|
+
self._outbox: List[str] = []
|
|
217
|
+
self._outbox_lock = threading.Lock()
|
|
218
|
+
self._tee_installed = False
|
|
219
|
+
self._orig_stdout: Any = None
|
|
220
|
+
self._orig_stderr: Any = None
|
|
221
|
+
self._device = ""
|
|
222
|
+
self.synced_version: Optional[str] = None
|
|
223
|
+
self.synced_once = threading.Event()
|
|
224
|
+
|
|
225
|
+
# -- state -----------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def state(self) -> str:
|
|
229
|
+
"""``"idle"``, ``"connecting"``, ``"connected"``, ``"syncing"``, or ``"disconnected"``."""
|
|
230
|
+
with self._state_lock:
|
|
231
|
+
return self._state
|
|
232
|
+
|
|
233
|
+
def add_listener(self, callback: Callable[[str, str], None]) -> Callable[[], None]:
|
|
234
|
+
"""Subscribe to ``(state, detail)`` changes (called on the client thread)."""
|
|
235
|
+
self._listeners.append(callback)
|
|
236
|
+
|
|
237
|
+
def _remove() -> None:
|
|
238
|
+
try:
|
|
239
|
+
self._listeners.remove(callback)
|
|
240
|
+
except ValueError:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
return _remove
|
|
244
|
+
|
|
245
|
+
def _set_state(self, state: str, detail: str = "") -> None:
|
|
246
|
+
with self._state_lock:
|
|
247
|
+
self._state = state
|
|
248
|
+
for listener in list(self._listeners):
|
|
249
|
+
try:
|
|
250
|
+
listener(state, detail)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# -- lifecycle -------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
def start(self) -> None:
|
|
257
|
+
"""Connect on a daemon thread (idempotent)."""
|
|
258
|
+
if self._thread is not None:
|
|
259
|
+
return
|
|
260
|
+
self._stop.clear()
|
|
261
|
+
# Native modules expect the main thread; ``start`` runs there (the
|
|
262
|
+
# bootstrap or a tap handler), the client thread never does.
|
|
263
|
+
self._device = _device_name()
|
|
264
|
+
self._install_tee()
|
|
265
|
+
self._thread = threading.Thread(target=self._run, name="pn-dev-client", daemon=True)
|
|
266
|
+
self._thread.start()
|
|
267
|
+
|
|
268
|
+
def stop(self) -> None:
|
|
269
|
+
"""Disconnect and stop the thread."""
|
|
270
|
+
self._stop.set()
|
|
271
|
+
sock = self._socket
|
|
272
|
+
if sock is not None:
|
|
273
|
+
try:
|
|
274
|
+
sock.close()
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
thread, self._thread = self._thread, None
|
|
278
|
+
if thread is not None and thread is not threading.current_thread():
|
|
279
|
+
thread.join(timeout=3.0)
|
|
280
|
+
self._remove_tee()
|
|
281
|
+
self._set_state("idle")
|
|
282
|
+
|
|
283
|
+
def _run(self) -> None:
|
|
284
|
+
attempt = 0
|
|
285
|
+
while not self._stop.is_set():
|
|
286
|
+
self._set_state("connecting", self.url)
|
|
287
|
+
try:
|
|
288
|
+
self._session()
|
|
289
|
+
attempt = 0
|
|
290
|
+
except (OSError, ws.WebSocketError) as exc:
|
|
291
|
+
detail = f"{type(exc).__name__}: {exc}"
|
|
292
|
+
self._set_state("disconnected", detail)
|
|
293
|
+
if attempt == 0:
|
|
294
|
+
self._log(f"[pn dev] cannot reach {self.url} ({detail}); retrying")
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
self._set_state("disconnected", repr(exc))
|
|
297
|
+
self._log(f"[pn dev] client error: {exc!r}")
|
|
298
|
+
traceback.print_exc(file=self._local_stderr())
|
|
299
|
+
if self._stop.is_set():
|
|
300
|
+
break
|
|
301
|
+
delay = _RECONNECT_DELAYS[min(attempt, len(_RECONNECT_DELAYS) - 1)]
|
|
302
|
+
attempt += 1
|
|
303
|
+
self._stop.wait(delay)
|
|
304
|
+
|
|
305
|
+
def _session(self) -> None:
|
|
306
|
+
client = ws.WebSocketClient(self.url, timeout=1.0)
|
|
307
|
+
client.connect()
|
|
308
|
+
self._socket = client
|
|
309
|
+
try:
|
|
310
|
+
client.send_text(json.dumps(self._hello()))
|
|
311
|
+
self._set_state("connected", self.url)
|
|
312
|
+
self._flush_outbox()
|
|
313
|
+
while not self._stop.is_set():
|
|
314
|
+
try:
|
|
315
|
+
text = client.recv()
|
|
316
|
+
except socket.timeout:
|
|
317
|
+
self._flush_outbox()
|
|
318
|
+
continue
|
|
319
|
+
if text is None:
|
|
320
|
+
break
|
|
321
|
+
self._flush_outbox()
|
|
322
|
+
try:
|
|
323
|
+
message = json.loads(text)
|
|
324
|
+
except ValueError:
|
|
325
|
+
continue
|
|
326
|
+
if isinstance(message, dict):
|
|
327
|
+
self._handle(message)
|
|
328
|
+
finally:
|
|
329
|
+
self._socket = None
|
|
330
|
+
try:
|
|
331
|
+
client.close()
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
if not self._stop.is_set():
|
|
335
|
+
self._set_state("disconnected", "connection closed")
|
|
336
|
+
|
|
337
|
+
# -- protocol --------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
def _hello(self) -> Dict[str, Any]:
|
|
340
|
+
from .platform import Platform
|
|
341
|
+
|
|
342
|
+
self._seed_overlay_from_bundle()
|
|
343
|
+
return {
|
|
344
|
+
"type": "hello",
|
|
345
|
+
"platform": Platform.OS,
|
|
346
|
+
"device": self._device,
|
|
347
|
+
"app": self.entry_module,
|
|
348
|
+
"files": self._overlay_manifest(),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def _seed_overlay_from_bundle(self) -> None:
|
|
352
|
+
"""Copy the bundled sources into an empty overlay before the first ``hello``.
|
|
353
|
+
|
|
354
|
+
The overlay must be a complete tree (a partial ``app/`` without
|
|
355
|
+
``__init__.py`` would lose to the bundled package on ``sys.path``),
|
|
356
|
+
so the first sync used to download every file and reload every
|
|
357
|
+
module even when the build was made from the very sources the
|
|
358
|
+
server holds. Seeding from the bundle lets the manifest report
|
|
359
|
+
what the app already runs; the server then sends only real
|
|
360
|
+
differences, and an up-to-date build connects without a reload.
|
|
361
|
+
"""
|
|
362
|
+
top = self.entry_module.split(".")[0]
|
|
363
|
+
if not top or self._overlay_has_files(top):
|
|
364
|
+
return
|
|
365
|
+
root = _bundled_package_root(top, exclude=self.overlay)
|
|
366
|
+
if root is None:
|
|
367
|
+
self._log(f"[pn dev] bundled sources for '{top}' not found; the server will send everything")
|
|
368
|
+
return
|
|
369
|
+
seeded = 0
|
|
370
|
+
|
|
371
|
+
def copy_tree(node: Any, rel: str) -> None:
|
|
372
|
+
nonlocal seeded
|
|
373
|
+
try:
|
|
374
|
+
children = list(node.iterdir())
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
self._log(f"[pn dev] could not list bundled {rel}: {exc}")
|
|
377
|
+
return
|
|
378
|
+
for child in children:
|
|
379
|
+
child_rel = f"{rel}/{child.name}"
|
|
380
|
+
if child.is_dir():
|
|
381
|
+
if child.name != "__pycache__":
|
|
382
|
+
copy_tree(child, child_rel)
|
|
383
|
+
continue
|
|
384
|
+
if not is_synced_file(child_rel):
|
|
385
|
+
continue
|
|
386
|
+
target = os.path.join(self.overlay, *child_rel.split("/"))
|
|
387
|
+
try:
|
|
388
|
+
data = child.read_bytes()
|
|
389
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
390
|
+
with open(target, "wb") as handle:
|
|
391
|
+
handle.write(data)
|
|
392
|
+
seeded += 1
|
|
393
|
+
except Exception as exc:
|
|
394
|
+
self._log(f"[pn dev] could not seed {child_rel}: {exc}")
|
|
395
|
+
|
|
396
|
+
copy_tree(root, top)
|
|
397
|
+
self._log(f"[pn dev] seeded the overlay with {seeded} bundled file(s) from {root}")
|
|
398
|
+
|
|
399
|
+
def _overlay_has_files(self, top: str) -> bool:
|
|
400
|
+
"""Whether the overlay already holds sources for ``top``.
|
|
401
|
+
|
|
402
|
+
``configure_dev_environment`` pre-creates the (empty) package
|
|
403
|
+
directory, so the directory's existence says nothing; byte-code
|
|
404
|
+
caches don't count either.
|
|
405
|
+
"""
|
|
406
|
+
for dirpath, dirnames, filenames in os.walk(os.path.join(self.overlay, top)):
|
|
407
|
+
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
|
408
|
+
if filenames:
|
|
409
|
+
return True
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
def _overlay_manifest(self) -> Dict[str, str]:
|
|
413
|
+
files: Dict[str, str] = {}
|
|
414
|
+
base = self.overlay
|
|
415
|
+
for dirpath, dirnames, filenames in os.walk(base):
|
|
416
|
+
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
|
417
|
+
for name in filenames:
|
|
418
|
+
full = os.path.join(dirpath, name)
|
|
419
|
+
rel = os.path.relpath(full, base).replace(os.sep, "/")
|
|
420
|
+
if rel == _SAVED_URL_FILE or not is_synced_file(rel):
|
|
421
|
+
continue
|
|
422
|
+
try:
|
|
423
|
+
with open(full, "rb") as handle:
|
|
424
|
+
files[rel] = hashlib.sha256(handle.read()).hexdigest()
|
|
425
|
+
except OSError:
|
|
426
|
+
continue
|
|
427
|
+
return files
|
|
428
|
+
|
|
429
|
+
def _handle(self, message: Dict[str, Any]) -> None:
|
|
430
|
+
kind = message.get("type")
|
|
431
|
+
if kind not in ("sync", "update"):
|
|
432
|
+
return
|
|
433
|
+
self._set_state("syncing", str(message.get("version") or ""))
|
|
434
|
+
files = message.get("files") or []
|
|
435
|
+
removed = message.get("removed") or []
|
|
436
|
+
written: List[str] = []
|
|
437
|
+
for entry in files:
|
|
438
|
+
if not isinstance(entry, dict):
|
|
439
|
+
continue
|
|
440
|
+
path = str(entry.get("path") or "")
|
|
441
|
+
if not path or not self._safe_relpath(path):
|
|
442
|
+
continue
|
|
443
|
+
data = _decode_content(entry)
|
|
444
|
+
if data is None:
|
|
445
|
+
continue
|
|
446
|
+
target = os.path.join(self.overlay, *path.split("/"))
|
|
447
|
+
try:
|
|
448
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
449
|
+
with open(target, "wb") as handle:
|
|
450
|
+
handle.write(data)
|
|
451
|
+
written.append(path)
|
|
452
|
+
except OSError as exc:
|
|
453
|
+
self._log(f"[pn dev] could not write {path}: {exc}")
|
|
454
|
+
for path in removed:
|
|
455
|
+
rel = str(path)
|
|
456
|
+
if not self._safe_relpath(rel):
|
|
457
|
+
continue
|
|
458
|
+
try:
|
|
459
|
+
os.remove(os.path.join(self.overlay, *rel.split("/")))
|
|
460
|
+
except OSError:
|
|
461
|
+
pass
|
|
462
|
+
written.append(rel)
|
|
463
|
+
version = str(message.get("version") or "")
|
|
464
|
+
self.synced_version = version
|
|
465
|
+
self.synced_once.set()
|
|
466
|
+
_save_server_url(self.overlay, self.url)
|
|
467
|
+
self._set_state("connected", version)
|
|
468
|
+
if not written:
|
|
469
|
+
# The app already runs these exact sources (a fresh build, or an
|
|
470
|
+
# overlay from the last session): nothing to reload.
|
|
471
|
+
return
|
|
472
|
+
self._log(f"[pn dev] synced {len(written)} file(s) from {self.url}")
|
|
473
|
+
modules = modules_for_paths(written)
|
|
474
|
+
if modules:
|
|
475
|
+
self._schedule_reload(modules, version)
|
|
476
|
+
|
|
477
|
+
def _schedule_reload(self, modules: List[str], version: str) -> None:
|
|
478
|
+
from .runtime import call_on_application_thread
|
|
479
|
+
|
|
480
|
+
def _apply() -> None:
|
|
481
|
+
from .hot_reload import apply_reload
|
|
482
|
+
|
|
483
|
+
result = apply_reload(modules)
|
|
484
|
+
if result.mode == "error":
|
|
485
|
+
self.send({"type": "error", "phase": "hot reload", "text": result.error or "unknown error"})
|
|
486
|
+
elif result.mode != "none":
|
|
487
|
+
self.send(
|
|
488
|
+
{"type": "reloaded", "version": version, "mode": result.mode, "modules": result.reloaded or modules}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
call_on_application_thread(_apply)
|
|
492
|
+
|
|
493
|
+
def _safe_relpath(self, rel: str) -> bool:
|
|
494
|
+
parts = rel.split("/")
|
|
495
|
+
return bool(parts) and all(part and part not in (".", "..") for part in parts)
|
|
496
|
+
|
|
497
|
+
# -- outbound --------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
def send(self, message: Dict[str, Any]) -> None:
|
|
500
|
+
"""Queue a message to the server (dropped when the outbox overflows offline)."""
|
|
501
|
+
text = json.dumps(message)
|
|
502
|
+
with self._outbox_lock:
|
|
503
|
+
if len(self._outbox) > 500:
|
|
504
|
+
del self._outbox[:100]
|
|
505
|
+
self._outbox.append(text)
|
|
506
|
+
sock = self._socket
|
|
507
|
+
if sock is not None and threading.current_thread() is self._thread:
|
|
508
|
+
self._flush_outbox()
|
|
509
|
+
|
|
510
|
+
def _flush_outbox(self) -> None:
|
|
511
|
+
sock = self._socket
|
|
512
|
+
if sock is None:
|
|
513
|
+
return
|
|
514
|
+
while True:
|
|
515
|
+
with self._outbox_lock:
|
|
516
|
+
if not self._outbox:
|
|
517
|
+
return
|
|
518
|
+
text = self._outbox.pop(0)
|
|
519
|
+
try:
|
|
520
|
+
sock.send_text(text)
|
|
521
|
+
except Exception:
|
|
522
|
+
with self._outbox_lock:
|
|
523
|
+
self._outbox.insert(0, text)
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
def log(self, text: str, level: str = "info") -> None:
|
|
527
|
+
"""Forward one log line to the server."""
|
|
528
|
+
self.send({"type": "log", "level": level, "text": text})
|
|
529
|
+
|
|
530
|
+
def report_error(self, phase: str, text: str) -> None:
|
|
531
|
+
"""Forward an error report (a traceback) to the server."""
|
|
532
|
+
self.send({"type": "error", "phase": phase, "text": text})
|
|
533
|
+
|
|
534
|
+
# -- stdout / stderr tee -----------------------------------------------
|
|
535
|
+
|
|
536
|
+
def _local_stderr(self) -> Any:
|
|
537
|
+
"""The process's real stderr, bypassing the tee (so status lines stay local).
|
|
538
|
+
|
|
539
|
+
Not ``sys.__stderr__``: embedded interpreters replace ``sys.stderr``
|
|
540
|
+
with a stream that reaches the platform log (logcat, the Xcode
|
|
541
|
+
console) while the original file descriptor goes nowhere.
|
|
542
|
+
"""
|
|
543
|
+
stream = sys.stderr
|
|
544
|
+
while isinstance(stream, _Tee):
|
|
545
|
+
stream = stream._inner
|
|
546
|
+
return stream if stream is not None else sys.__stderr__
|
|
547
|
+
|
|
548
|
+
def _default_log(self, line: str) -> None:
|
|
549
|
+
try:
|
|
550
|
+
print(line, file=self._local_stderr(), flush=True)
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
def _install_tee(self) -> None:
|
|
555
|
+
if not self._forward_logs or self._tee_installed:
|
|
556
|
+
return
|
|
557
|
+
self._tee_installed = True
|
|
558
|
+
self._orig_stdout, self._orig_stderr = sys.stdout, sys.stderr
|
|
559
|
+
sys.stdout = _Tee(self._orig_stdout, lambda line: self.log(line, "info"))
|
|
560
|
+
sys.stderr = _Tee(self._orig_stderr, lambda line: self.log(line, "error"))
|
|
561
|
+
|
|
562
|
+
def _remove_tee(self) -> None:
|
|
563
|
+
if not self._tee_installed:
|
|
564
|
+
return
|
|
565
|
+
self._tee_installed = False
|
|
566
|
+
if isinstance(sys.stdout, _Tee):
|
|
567
|
+
sys.stdout = self._orig_stdout
|
|
568
|
+
if isinstance(sys.stderr, _Tee):
|
|
569
|
+
sys.stderr = self._orig_stderr
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _device_name() -> str:
|
|
573
|
+
"""A short device label for the server's client list (``iPhone``, ``Pixel 8``)."""
|
|
574
|
+
try:
|
|
575
|
+
from .native_modules.registry import native_module
|
|
576
|
+
|
|
577
|
+
info = native_module("Device").call("info")
|
|
578
|
+
if isinstance(info, dict):
|
|
579
|
+
return str(info.get("model") or info.get("os") or "")
|
|
580
|
+
except Exception:
|
|
581
|
+
pass
|
|
582
|
+
return ""
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _bundled_package_root(top: str, *, exclude: str) -> Optional[Any]:
|
|
586
|
+
"""A ``Traversable`` for the bundled top-level package ``top``, ignoring the overlay.
|
|
587
|
+
|
|
588
|
+
Resolves through ``sys.path`` the way the import system would once the
|
|
589
|
+
overlay is out of the picture, without importing anything, then asks
|
|
590
|
+
the loader for its resource reader. On iOS that is a plain directory;
|
|
591
|
+
under Chaquopy the ``.py`` sources live in the APK's asset archive and
|
|
592
|
+
only the reader can list and read them (``os.walk`` on the extracted
|
|
593
|
+
directory sees data files alone).
|
|
594
|
+
"""
|
|
595
|
+
import importlib.machinery
|
|
596
|
+
|
|
597
|
+
excluded = os.path.abspath(exclude)
|
|
598
|
+
search = [p for p in sys.path if p and os.path.abspath(p) != excluded]
|
|
599
|
+
try:
|
|
600
|
+
spec = importlib.machinery.PathFinder.find_spec(top, search)
|
|
601
|
+
except Exception:
|
|
602
|
+
return None
|
|
603
|
+
if spec is None or not spec.submodule_search_locations or spec.loader is None:
|
|
604
|
+
return None
|
|
605
|
+
get_reader = getattr(spec.loader, "get_resource_reader", None)
|
|
606
|
+
if get_reader is None:
|
|
607
|
+
return None
|
|
608
|
+
try:
|
|
609
|
+
reader = get_reader(top)
|
|
610
|
+
files = getattr(reader, "files", None)
|
|
611
|
+
root = files() if files is not None else None
|
|
612
|
+
except Exception:
|
|
613
|
+
return None
|
|
614
|
+
if root is None or not root.is_dir():
|
|
615
|
+
return None
|
|
616
|
+
return root
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _decode_content(entry: Dict[str, Any]) -> Optional[bytes]:
|
|
620
|
+
content = entry.get("content")
|
|
621
|
+
if not isinstance(content, str):
|
|
622
|
+
return None
|
|
623
|
+
if entry.get("encoding") == "base64":
|
|
624
|
+
try:
|
|
625
|
+
return base64.b64decode(content)
|
|
626
|
+
except (ValueError, TypeError):
|
|
627
|
+
return None
|
|
628
|
+
return content.encode("utf-8")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ======================================================================
|
|
632
|
+
# Process-wide client
|
|
633
|
+
# ======================================================================
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def current() -> Optional[DevClient]:
|
|
637
|
+
"""The running dev client, if any."""
|
|
638
|
+
return _current
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def start(url: str, overlay: Optional[str] = None, **kwargs: Any) -> DevClient:
|
|
642
|
+
"""Start (or replace) the process-wide dev client for ``url``."""
|
|
643
|
+
global _current
|
|
644
|
+
from .hot_reload import configure_dev_environment, overlay_root
|
|
645
|
+
|
|
646
|
+
root = overlay or overlay_root()
|
|
647
|
+
if root is None:
|
|
648
|
+
root = configure_dev_environment(os.path.join(os.path.expanduser("~"), ".pythonnative"))
|
|
649
|
+
with _lock:
|
|
650
|
+
previous = _current
|
|
651
|
+
if previous is not None:
|
|
652
|
+
previous.stop()
|
|
653
|
+
client = DevClient(url, root, **kwargs)
|
|
654
|
+
_current = client
|
|
655
|
+
client.start()
|
|
656
|
+
return client
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def stop() -> None:
|
|
660
|
+
"""Stop the process-wide dev client."""
|
|
661
|
+
global _current
|
|
662
|
+
with _lock:
|
|
663
|
+
client, _current = _current, None
|
|
664
|
+
if client is not None:
|
|
665
|
+
client.stop()
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def start_if_configured(entry_module: Optional[str] = None) -> Optional[DevClient]:
|
|
669
|
+
"""Start the dev client when the build points at a server.
|
|
670
|
+
|
|
671
|
+
Called by ``bootstrap.start(dev=True)``. The URL comes from
|
|
672
|
+
``PN_DEV_SERVER`` (baked in by ``pn run``) or, for ``--dev-client``
|
|
673
|
+
builds, from the URL saved by a previous connection. Returns the
|
|
674
|
+
client, or ``None`` when nothing is configured.
|
|
675
|
+
"""
|
|
676
|
+
from .hot_reload import overlay_root
|
|
677
|
+
|
|
678
|
+
root = overlay_root()
|
|
679
|
+
if root is None:
|
|
680
|
+
return None
|
|
681
|
+
url = os.environ.get(SERVER_URL_ENV) or saved_server_url(root)
|
|
682
|
+
if not url:
|
|
683
|
+
return None
|
|
684
|
+
entry = entry_module or os.environ.get("PN_ENTRY_MODULE") or "app.main"
|
|
685
|
+
try:
|
|
686
|
+
return start(url, root, entry_module=entry)
|
|
687
|
+
except Exception as exc:
|
|
688
|
+
print(f"[pn dev] could not start the dev client: {exc!r}", file=sys.stderr)
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# ======================================================================
|
|
693
|
+
# Connect screen (dev-client builds)
|
|
694
|
+
# ======================================================================
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _connect_screen() -> Any:
|
|
698
|
+
from . import components as c
|
|
699
|
+
from .component import component
|
|
700
|
+
from .hooks import use_effect, use_state
|
|
701
|
+
|
|
702
|
+
@component
|
|
703
|
+
def ConnectScreen() -> Any:
|
|
704
|
+
client = current()
|
|
705
|
+
initial = client.url if client is not None else (saved_server_url() or "")
|
|
706
|
+
url, set_url = use_state(initial.replace("/ws?role=client", "") if initial else "")
|
|
707
|
+
status, set_status = use_state(client.state if client is not None else "idle")
|
|
708
|
+
detail, set_detail = use_state("")
|
|
709
|
+
|
|
710
|
+
def _subscribe() -> Optional[Callable[[], None]]:
|
|
711
|
+
live = current()
|
|
712
|
+
if live is None:
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
def _on_state(state: str, info: str) -> None:
|
|
716
|
+
from .runtime import call_on_application_thread
|
|
717
|
+
|
|
718
|
+
def _update() -> None:
|
|
719
|
+
set_status(state)
|
|
720
|
+
set_detail(info)
|
|
721
|
+
|
|
722
|
+
call_on_application_thread(_update)
|
|
723
|
+
|
|
724
|
+
return live.add_listener(_on_state)
|
|
725
|
+
|
|
726
|
+
use_effect(_subscribe, [client])
|
|
727
|
+
|
|
728
|
+
def _connect() -> None:
|
|
729
|
+
try:
|
|
730
|
+
normalized = normalize_server_url(url)
|
|
731
|
+
except ValueError as exc:
|
|
732
|
+
set_status("error")
|
|
733
|
+
set_detail(str(exc))
|
|
734
|
+
return
|
|
735
|
+
start(normalized)
|
|
736
|
+
set_status("connecting")
|
|
737
|
+
set_detail(normalized)
|
|
738
|
+
|
|
739
|
+
lan_hint = "Run `pn start` on your computer and enter its address (for example 192.168.1.20:8765)."
|
|
740
|
+
colors = {
|
|
741
|
+
"idle": "#8E8E93",
|
|
742
|
+
"connecting": "#FF9F0A",
|
|
743
|
+
"connected": "#30D158",
|
|
744
|
+
"syncing": "#0A84FF",
|
|
745
|
+
"disconnected": "#FF453A",
|
|
746
|
+
"error": "#FF453A",
|
|
747
|
+
}
|
|
748
|
+
return c.Column(
|
|
749
|
+
c.Column(
|
|
750
|
+
c.Text("PythonNative", style={"font_size": 30, "bold": True, "color": "#FFFFFF"}),
|
|
751
|
+
c.Text("Dev client", style={"font_size": 17, "color": "#8E8E93"}),
|
|
752
|
+
style={"spacing": 4, "padding_top": 72},
|
|
753
|
+
),
|
|
754
|
+
c.Column(
|
|
755
|
+
c.Text("Dev server address", style={"font_size": 13, "color": "#8E8E93"}),
|
|
756
|
+
c.TextInput(
|
|
757
|
+
value=url,
|
|
758
|
+
on_change=set_url,
|
|
759
|
+
placeholder="192.168.1.20:8765",
|
|
760
|
+
auto_correct=False,
|
|
761
|
+
auto_capitalize="none",
|
|
762
|
+
keyboard_type="url",
|
|
763
|
+
style={
|
|
764
|
+
"background_color": "#1C1C1E",
|
|
765
|
+
"color": "#FFFFFF",
|
|
766
|
+
"padding": 12,
|
|
767
|
+
"border_radius": 10,
|
|
768
|
+
"font_size": 16,
|
|
769
|
+
},
|
|
770
|
+
),
|
|
771
|
+
c.Button(
|
|
772
|
+
"Connect",
|
|
773
|
+
on_press=_connect,
|
|
774
|
+
style={"background_color": "#0A84FF", "color": "#FFFFFF", "padding": 12, "border_radius": 10},
|
|
775
|
+
),
|
|
776
|
+
style={"spacing": 8, "align_items": "stretch"},
|
|
777
|
+
),
|
|
778
|
+
c.Column(
|
|
779
|
+
c.Row(
|
|
780
|
+
c.View(
|
|
781
|
+
style={"width": 10, "height": 10, "border_radius": 5, "background_color": colors.get(status)}
|
|
782
|
+
),
|
|
783
|
+
c.Text(status, style={"color": "#FFFFFF", "font_size": 15}),
|
|
784
|
+
style={"spacing": 8, "align_items": "center"},
|
|
785
|
+
),
|
|
786
|
+
c.Text(detail, style={"color": "#8E8E93", "font_size": 12}),
|
|
787
|
+
style={"spacing": 4},
|
|
788
|
+
),
|
|
789
|
+
c.Text(lan_hint, style={"color": "#636366", "font_size": 12}),
|
|
790
|
+
style={"flex": 1, "padding": 24, "spacing": 28, "background_color": "#000000"},
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
return ConnectScreen
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class _LazyConnectScreen:
|
|
797
|
+
"""Import-light proxy so ``from pythonnative.devclient import ConnectScreen`` stays cheap."""
|
|
798
|
+
|
|
799
|
+
_component: Any = None
|
|
800
|
+
|
|
801
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
802
|
+
if _LazyConnectScreen._component is None:
|
|
803
|
+
_LazyConnectScreen._component = _connect_screen()
|
|
804
|
+
return _LazyConnectScreen._component(*args, **kwargs)
|
|
805
|
+
|
|
806
|
+
def __getattr__(self, name: str) -> Any:
|
|
807
|
+
if _LazyConnectScreen._component is None:
|
|
808
|
+
_LazyConnectScreen._component = _connect_screen()
|
|
809
|
+
return getattr(_LazyConnectScreen._component, name)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
ConnectScreen: Any = _LazyConnectScreen()
|
|
813
|
+
"""Root component of ``--dev-client`` builds until the first sync arrives."""
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def placeholder_main_source() -> str:
|
|
817
|
+
"""Source of the ``app/main.py`` staged into ``--dev-client`` builds."""
|
|
818
|
+
return (
|
|
819
|
+
'"""Placeholder entry module for a PythonNative dev-client build.\n\n'
|
|
820
|
+
"The real app arrives from the dev server and shadows this file.\n"
|
|
821
|
+
'"""\n\n'
|
|
822
|
+
"from pythonnative.devclient import ConnectScreen as App\n\n"
|
|
823
|
+
'__all__ = ["App"]\n'
|
|
824
|
+
)
|