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,718 @@
|
|
|
1
|
+
"""The dev server: HTTP for static assets, WebSocket for live peers.
|
|
2
|
+
|
|
3
|
+
One [`DevServer`][pythonnative.devserver.server.DevServer] runs on its
|
|
4
|
+
own thread with a private ``asyncio`` loop, so it works both inside
|
|
5
|
+
``pn start`` (whose main thread runs the browser preview's app) and in
|
|
6
|
+
tests. It exposes:
|
|
7
|
+
|
|
8
|
+
- ``GET /``: the browser preview page.
|
|
9
|
+
- ``GET /static/<name>``: preview assets (JS, CSS).
|
|
10
|
+
- ``GET /manifest``: ``{"version", "entry", "files": {path: sha256}}``.
|
|
11
|
+
- ``GET /file/<path>``: raw bytes of one synced source file.
|
|
12
|
+
- ``GET /status``: server, project, and connected-peer information.
|
|
13
|
+
- ``WS /ws?role=client``: the dev-client protocol (see below).
|
|
14
|
+
- ``WS /ws?role=preview``: the browser preview's bridge channel; the
|
|
15
|
+
server only relays text frames between the page and the
|
|
16
|
+
[`PreviewChannel`][pythonnative.devserver.server.PreviewChannel]
|
|
17
|
+
handler installed by the preview.
|
|
18
|
+
|
|
19
|
+
Dev-client protocol (JSON objects, one per text frame):
|
|
20
|
+
|
|
21
|
+
- client -> server ``hello``: ``{"type": "hello", "platform", "device",
|
|
22
|
+
"app", "files": {path: sha256}}`` describing what the client already
|
|
23
|
+
holds in its overlay.
|
|
24
|
+
- server -> client ``sync``: ``{"type": "sync", "version", "entry",
|
|
25
|
+
"files": [{"path", "sha256", "content", "encoding"}], "removed": [...]}``
|
|
26
|
+
bringing the client up to date. The same shape is sent as
|
|
27
|
+
``"update"`` whenever the watcher sees a change.
|
|
28
|
+
- client -> server ``log`` (``level``, ``text``), ``error`` (``phase``,
|
|
29
|
+
``text``), ``reloaded`` (``version``, ``mode``, ``modules``): streamed
|
|
30
|
+
to the terminal.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import base64
|
|
37
|
+
import json
|
|
38
|
+
import mimetypes
|
|
39
|
+
import os
|
|
40
|
+
import socket
|
|
41
|
+
import sys
|
|
42
|
+
import threading
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Protocol, Sequence
|
|
46
|
+
from urllib.parse import parse_qs, unquote, urlsplit
|
|
47
|
+
|
|
48
|
+
from . import ws
|
|
49
|
+
from .watcher import FileWatcher, SourceChange, SourceSnapshot, snapshot_sources
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DEFAULT_PORT",
|
|
53
|
+
"DevServer",
|
|
54
|
+
"PreviewChannel",
|
|
55
|
+
"PreviewPeer",
|
|
56
|
+
"ServerInfo",
|
|
57
|
+
"lan_addresses",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
DEFAULT_PORT = 8765
|
|
61
|
+
"""Default port for ``pn start`` / ``pn preview``."""
|
|
62
|
+
|
|
63
|
+
_MAX_HEAD = 64 * 1024
|
|
64
|
+
_STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|
65
|
+
|
|
66
|
+
Logger = Callable[[str], None]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def lan_addresses() -> List[str]:
|
|
70
|
+
"""Best-effort list of this machine's non-loopback IPv4 addresses.
|
|
71
|
+
|
|
72
|
+
Used to print a URL a physical device on the same Wi-Fi can reach.
|
|
73
|
+
"""
|
|
74
|
+
found: List[str] = []
|
|
75
|
+
try:
|
|
76
|
+
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
77
|
+
try:
|
|
78
|
+
# No packets are sent; connecting a UDP socket just selects
|
|
79
|
+
# the interface that routes to the internet.
|
|
80
|
+
probe.connect(("10.255.255.255", 1))
|
|
81
|
+
found.append(probe.getsockname()[0])
|
|
82
|
+
finally:
|
|
83
|
+
probe.close()
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
try:
|
|
87
|
+
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
|
88
|
+
addr = str(info[4][0])
|
|
89
|
+
if not addr.startswith("127.") and addr not in found:
|
|
90
|
+
found.append(addr)
|
|
91
|
+
except OSError:
|
|
92
|
+
pass
|
|
93
|
+
return found
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ServerInfo:
|
|
98
|
+
"""What the server is serving and where."""
|
|
99
|
+
|
|
100
|
+
host: str
|
|
101
|
+
port: int
|
|
102
|
+
project_root: str
|
|
103
|
+
entry_module: str
|
|
104
|
+
project_name: str = ""
|
|
105
|
+
|
|
106
|
+
def url(self, host: Optional[str] = None) -> str:
|
|
107
|
+
"""The HTTP base URL, substituting ``host`` for the bind address."""
|
|
108
|
+
return f"http://{host or self.display_host}:{self.port}"
|
|
109
|
+
|
|
110
|
+
def ws_url(self, host: Optional[str] = None) -> str:
|
|
111
|
+
"""The dev-client WebSocket URL."""
|
|
112
|
+
return f"ws://{host or self.display_host}:{self.port}/ws?role=client"
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def display_host(self) -> str:
|
|
116
|
+
"""A host suitable for a URL (``0.0.0.0`` becomes ``localhost``)."""
|
|
117
|
+
return "localhost" if self.host in ("", "0.0.0.0", "::") else self.host
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ======================================================================
|
|
121
|
+
# Preview channel
|
|
122
|
+
# ======================================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PreviewPeer:
|
|
126
|
+
"""A connected browser preview page; ``send`` is safe from any thread."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, server: "DevServer", writer: asyncio.StreamWriter) -> None:
|
|
129
|
+
self._server = server
|
|
130
|
+
self._writer = writer
|
|
131
|
+
self.closed = threading.Event()
|
|
132
|
+
|
|
133
|
+
def send(self, text: str) -> None:
|
|
134
|
+
"""Queue one text frame to the page (dropped once the peer is closed)."""
|
|
135
|
+
if self.closed.is_set():
|
|
136
|
+
return
|
|
137
|
+
self._server._call_soon(self._send_now, text)
|
|
138
|
+
|
|
139
|
+
def _send_now(self, text: str) -> None:
|
|
140
|
+
if self.closed.is_set():
|
|
141
|
+
return
|
|
142
|
+
try:
|
|
143
|
+
self._writer.write(ws.encode_frame(ws.TEXT, text.encode("utf-8")))
|
|
144
|
+
except Exception:
|
|
145
|
+
self.closed.set()
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
"""Ask the server to close this page's socket."""
|
|
149
|
+
self._server._call_soon(self._close_now)
|
|
150
|
+
|
|
151
|
+
def _close_now(self) -> None:
|
|
152
|
+
if self.closed.is_set():
|
|
153
|
+
return
|
|
154
|
+
self.closed.set()
|
|
155
|
+
try:
|
|
156
|
+
self._writer.write(ws.encode_close())
|
|
157
|
+
self._writer.close()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class PreviewChannel(Protocol):
|
|
163
|
+
"""What the preview installs to receive the page's bridge traffic.
|
|
164
|
+
|
|
165
|
+
Every method is called on the server thread; implementations hop
|
|
166
|
+
to their own thread as needed.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def on_preview_connected(self, peer: PreviewPeer, info: Dict[str, Any]) -> None:
|
|
170
|
+
"""A page connected (``info`` carries its query parameters)."""
|
|
171
|
+
|
|
172
|
+
def on_preview_message(self, peer: PreviewPeer, text: str) -> None:
|
|
173
|
+
"""A text frame arrived from the page."""
|
|
174
|
+
|
|
175
|
+
def on_preview_disconnected(self, peer: PreviewPeer) -> None:
|
|
176
|
+
"""The page went away."""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ======================================================================
|
|
180
|
+
# Dev clients
|
|
181
|
+
# ======================================================================
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class DevClient:
|
|
186
|
+
"""One connected on-device dev client."""
|
|
187
|
+
|
|
188
|
+
id: int
|
|
189
|
+
writer: asyncio.StreamWriter
|
|
190
|
+
platform: str = "unknown"
|
|
191
|
+
device: str = ""
|
|
192
|
+
app: str = ""
|
|
193
|
+
connected_at: float = field(default_factory=time.time)
|
|
194
|
+
files: Dict[str, str] = field(default_factory=dict)
|
|
195
|
+
|
|
196
|
+
def label(self) -> str:
|
|
197
|
+
"""A short human label for log lines."""
|
|
198
|
+
parts = [self.platform]
|
|
199
|
+
if self.device:
|
|
200
|
+
parts.append(self.device)
|
|
201
|
+
return " ".join(parts)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ======================================================================
|
|
205
|
+
# The server
|
|
206
|
+
# ======================================================================
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class DevServer:
|
|
210
|
+
"""Serve sources, assets, and the live protocol for one project.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project_root: Directory containing ``app/`` and ``pythonnative.toml``.
|
|
214
|
+
entry_module: The app's entry module (``"app.main"``).
|
|
215
|
+
host: Bind address. ``0.0.0.0`` so devices on the LAN can reach
|
|
216
|
+
it; pass ``127.0.0.1`` to stay local.
|
|
217
|
+
port: TCP port; ``0`` picks a free one.
|
|
218
|
+
project_name: Shown in the preview page and ``/status``.
|
|
219
|
+
static_dir: Where the preview page's assets live.
|
|
220
|
+
log: Where to print client logs and connection events.
|
|
221
|
+
watch: Whether to run the file watcher.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def __init__(
|
|
225
|
+
self,
|
|
226
|
+
project_root: str,
|
|
227
|
+
entry_module: str = "app.main",
|
|
228
|
+
*,
|
|
229
|
+
host: str = "0.0.0.0",
|
|
230
|
+
port: int = DEFAULT_PORT,
|
|
231
|
+
project_name: str = "",
|
|
232
|
+
static_dir: Optional[str] = None,
|
|
233
|
+
log: Optional[Logger] = None,
|
|
234
|
+
watch: bool = True,
|
|
235
|
+
) -> None:
|
|
236
|
+
self.project_root = os.path.abspath(project_root)
|
|
237
|
+
self.entry_module = entry_module
|
|
238
|
+
self.project_name = project_name
|
|
239
|
+
self.static_dir = static_dir or _STATIC_DIR
|
|
240
|
+
self.log: Logger = log or (lambda line: print(line, file=sys.stderr, flush=True))
|
|
241
|
+
self._host = host
|
|
242
|
+
self._port = port
|
|
243
|
+
self._watch = watch
|
|
244
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
245
|
+
self._thread: Optional[threading.Thread] = None
|
|
246
|
+
self._server: Optional[asyncio.base_events.Server] = None
|
|
247
|
+
self._started = threading.Event()
|
|
248
|
+
self._start_error: Optional[BaseException] = None
|
|
249
|
+
self._clients: Dict[int, DevClient] = {}
|
|
250
|
+
self._client_ids = 0
|
|
251
|
+
self._preview: Optional[PreviewChannel] = None
|
|
252
|
+
self._preview_peer: Optional[PreviewPeer] = None
|
|
253
|
+
self._watcher: Optional[FileWatcher] = None
|
|
254
|
+
self._snapshot: SourceSnapshot = snapshot_sources(self.project_root)
|
|
255
|
+
self._snapshot_lock = threading.Lock()
|
|
256
|
+
self._change_listeners: List[Callable[[SourceChange, SourceSnapshot], None]] = []
|
|
257
|
+
self.info = ServerInfo(
|
|
258
|
+
host=host,
|
|
259
|
+
port=port,
|
|
260
|
+
project_root=self.project_root,
|
|
261
|
+
entry_module=entry_module,
|
|
262
|
+
project_name=project_name,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# -- lifecycle -------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
def start(self) -> ServerInfo:
|
|
268
|
+
"""Bind the socket on a background thread and return the address.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
OSError: When the port is taken (the CLI prints a hint).
|
|
272
|
+
"""
|
|
273
|
+
if self._thread is not None:
|
|
274
|
+
return self.info
|
|
275
|
+
self._thread = threading.Thread(target=self._run, name="pn-dev-server", daemon=True)
|
|
276
|
+
self._thread.start()
|
|
277
|
+
self._started.wait(timeout=10.0)
|
|
278
|
+
if self._start_error is not None:
|
|
279
|
+
error, self._start_error = self._start_error, None
|
|
280
|
+
self._thread = None
|
|
281
|
+
raise error
|
|
282
|
+
if self._watch:
|
|
283
|
+
self._watcher = FileWatcher(self.project_root, self._on_files_changed)
|
|
284
|
+
with self._snapshot_lock:
|
|
285
|
+
self._snapshot = self._watcher.snapshot
|
|
286
|
+
self._watcher.start()
|
|
287
|
+
return self.info
|
|
288
|
+
|
|
289
|
+
def stop(self) -> None:
|
|
290
|
+
"""Close every connection and stop the thread."""
|
|
291
|
+
watcher, self._watcher = self._watcher, None
|
|
292
|
+
if watcher is not None:
|
|
293
|
+
watcher.stop()
|
|
294
|
+
loop = self._loop
|
|
295
|
+
if loop is not None and not loop.is_closed():
|
|
296
|
+
loop.call_soon_threadsafe(self._shutdown)
|
|
297
|
+
thread, self._thread = self._thread, None
|
|
298
|
+
if thread is not None:
|
|
299
|
+
thread.join(timeout=5.0)
|
|
300
|
+
|
|
301
|
+
def _run(self) -> None:
|
|
302
|
+
loop = asyncio.new_event_loop()
|
|
303
|
+
self._loop = loop
|
|
304
|
+
asyncio.set_event_loop(loop)
|
|
305
|
+
try:
|
|
306
|
+
server = loop.run_until_complete(asyncio.start_server(self._handle_connection, self._host, self._port))
|
|
307
|
+
except BaseException as exc:
|
|
308
|
+
self._start_error = exc
|
|
309
|
+
self._started.set()
|
|
310
|
+
loop.close()
|
|
311
|
+
return
|
|
312
|
+
self._server = server
|
|
313
|
+
sockets: Sequence[Any] = server.sockets or []
|
|
314
|
+
if sockets:
|
|
315
|
+
self.info.port = int(sockets[0].getsockname()[1])
|
|
316
|
+
self._started.set()
|
|
317
|
+
try:
|
|
318
|
+
loop.run_forever()
|
|
319
|
+
finally:
|
|
320
|
+
try:
|
|
321
|
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
loop.close()
|
|
325
|
+
|
|
326
|
+
def _shutdown(self) -> None:
|
|
327
|
+
loop = self._loop
|
|
328
|
+
if self._server is not None:
|
|
329
|
+
self._server.close()
|
|
330
|
+
for client in list(self._clients.values()):
|
|
331
|
+
try:
|
|
332
|
+
client.writer.write(ws.encode_close())
|
|
333
|
+
client.writer.close()
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
self._clients.clear()
|
|
337
|
+
if self._preview_peer is not None:
|
|
338
|
+
self._preview_peer._close_now()
|
|
339
|
+
if loop is not None:
|
|
340
|
+
loop.call_soon(loop.stop)
|
|
341
|
+
|
|
342
|
+
def _call_soon(self, fn: Callable[..., Any], *args: Any) -> None:
|
|
343
|
+
loop = self._loop
|
|
344
|
+
if loop is None or loop.is_closed():
|
|
345
|
+
return
|
|
346
|
+
try:
|
|
347
|
+
loop.call_soon_threadsafe(fn, *args)
|
|
348
|
+
except RuntimeError:
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
# -- state -----------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def snapshot(self) -> SourceSnapshot:
|
|
355
|
+
"""The current source snapshot."""
|
|
356
|
+
with self._snapshot_lock:
|
|
357
|
+
return self._snapshot
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def clients(self) -> List[DevClient]:
|
|
361
|
+
"""Connected dev clients (a copy)."""
|
|
362
|
+
return list(self._clients.values())
|
|
363
|
+
|
|
364
|
+
def set_preview_channel(self, channel: Optional[PreviewChannel]) -> None:
|
|
365
|
+
"""Install the handler for the browser preview's bridge traffic."""
|
|
366
|
+
self._preview = channel
|
|
367
|
+
|
|
368
|
+
def add_change_listener(self, listener: Callable[[SourceChange, SourceSnapshot], None]) -> Callable[[], None]:
|
|
369
|
+
"""Be told (on the watcher thread) when sources change; returns an unsubscribe."""
|
|
370
|
+
self._change_listeners.append(listener)
|
|
371
|
+
|
|
372
|
+
def _remove() -> None:
|
|
373
|
+
try:
|
|
374
|
+
self._change_listeners.remove(listener)
|
|
375
|
+
except ValueError:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
return _remove
|
|
379
|
+
|
|
380
|
+
def manifest(self) -> Dict[str, Any]:
|
|
381
|
+
"""The ``/manifest`` document."""
|
|
382
|
+
snap = self.snapshot
|
|
383
|
+
return {"version": snap.version, "entry": self.entry_module, "files": dict(snap.files)}
|
|
384
|
+
|
|
385
|
+
def status(self) -> Dict[str, Any]:
|
|
386
|
+
"""The ``/status`` document."""
|
|
387
|
+
snap = self.snapshot
|
|
388
|
+
return {
|
|
389
|
+
"name": self.project_name,
|
|
390
|
+
"entry": self.entry_module,
|
|
391
|
+
"project_root": self.project_root,
|
|
392
|
+
"version": snap.version,
|
|
393
|
+
"file_count": len(snap.files),
|
|
394
|
+
"clients": [
|
|
395
|
+
{"id": c.id, "platform": c.platform, "device": c.device, "app": c.app, "connected_at": c.connected_at}
|
|
396
|
+
for c in self._clients.values()
|
|
397
|
+
],
|
|
398
|
+
"preview_connected": self._preview_peer is not None and not self._preview_peer.closed.is_set(),
|
|
399
|
+
"lan": lan_addresses(),
|
|
400
|
+
"port": self.info.port,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# -- file changes ----------------------------------------------------
|
|
404
|
+
|
|
405
|
+
def _on_files_changed(self, change: SourceChange, snapshot: SourceSnapshot) -> None:
|
|
406
|
+
with self._snapshot_lock:
|
|
407
|
+
self._snapshot = snapshot
|
|
408
|
+
summary = ", ".join(change.changed + [f"-{p}" for p in change.removed])
|
|
409
|
+
self.log(f"[pn] changed: {summary}")
|
|
410
|
+
for listener in list(self._change_listeners):
|
|
411
|
+
try:
|
|
412
|
+
listener(change, snapshot)
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
self.log(f"[pn] change listener failed: {exc!r}")
|
|
415
|
+
if self._clients:
|
|
416
|
+
message = self._sync_message("update", snapshot, change.changed, change.removed)
|
|
417
|
+
self._call_soon(self._broadcast, message)
|
|
418
|
+
|
|
419
|
+
def _sync_message(self, kind: str, snapshot: SourceSnapshot, paths: Sequence[str], removed: Sequence[str]) -> str:
|
|
420
|
+
files: List[Dict[str, Any]] = []
|
|
421
|
+
for path in paths:
|
|
422
|
+
data = snapshot.read(path)
|
|
423
|
+
if data is None:
|
|
424
|
+
continue
|
|
425
|
+
files.append(_encode_file(path, snapshot.files.get(path, ""), data))
|
|
426
|
+
return json.dumps(
|
|
427
|
+
{
|
|
428
|
+
"type": kind,
|
|
429
|
+
"version": snapshot.version,
|
|
430
|
+
"entry": self.entry_module,
|
|
431
|
+
"files": files,
|
|
432
|
+
"removed": list(removed),
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _broadcast(self, message: str) -> None:
|
|
437
|
+
frame = ws.encode_frame(ws.TEXT, message.encode("utf-8"))
|
|
438
|
+
for client in list(self._clients.values()):
|
|
439
|
+
try:
|
|
440
|
+
client.writer.write(frame)
|
|
441
|
+
except Exception:
|
|
442
|
+
self._drop_client(client)
|
|
443
|
+
|
|
444
|
+
def _drop_client(self, client: DevClient) -> None:
|
|
445
|
+
if self._clients.pop(client.id, None) is not None:
|
|
446
|
+
self.log(f"[pn] {client.label()} disconnected")
|
|
447
|
+
try:
|
|
448
|
+
client.writer.close()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
# -- connections -----------------------------------------------------
|
|
453
|
+
|
|
454
|
+
async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
455
|
+
try:
|
|
456
|
+
head = await reader.readuntil(b"\r\n\r\n")
|
|
457
|
+
except (asyncio.IncompleteReadError, asyncio.LimitOverrunError, ConnectionError):
|
|
458
|
+
writer.close()
|
|
459
|
+
return
|
|
460
|
+
if len(head) > _MAX_HEAD:
|
|
461
|
+
writer.close()
|
|
462
|
+
return
|
|
463
|
+
start_line, headers = ws.parse_http_headers(head)
|
|
464
|
+
parts = start_line.split()
|
|
465
|
+
if len(parts) < 2:
|
|
466
|
+
writer.close()
|
|
467
|
+
return
|
|
468
|
+
method, target = parts[0], parts[1]
|
|
469
|
+
url = urlsplit(target)
|
|
470
|
+
path = unquote(url.path)
|
|
471
|
+
query = {k: v[-1] for k, v in parse_qs(url.query).items()}
|
|
472
|
+
if "websocket" in headers.get("upgrade", "").lower():
|
|
473
|
+
await self._serve_websocket(reader, writer, headers, path, query)
|
|
474
|
+
return
|
|
475
|
+
try:
|
|
476
|
+
await self._serve_http(writer, method, path, query)
|
|
477
|
+
finally:
|
|
478
|
+
try:
|
|
479
|
+
writer.close()
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
async def _serve_http(self, writer: asyncio.StreamWriter, method: str, path: str, query: Dict[str, str]) -> None:
|
|
484
|
+
if method not in ("GET", "HEAD"):
|
|
485
|
+
_respond(writer, 405, b"method not allowed", "text/plain")
|
|
486
|
+
return
|
|
487
|
+
if path in ("/", "/index.html"):
|
|
488
|
+
_respond_file(writer, os.path.join(self.static_dir, "index.html"))
|
|
489
|
+
return
|
|
490
|
+
if path.startswith("/static/"):
|
|
491
|
+
name = path[len("/static/") :]
|
|
492
|
+
root = os.path.realpath(self.static_dir)
|
|
493
|
+
candidate = os.path.realpath(os.path.join(root, name))
|
|
494
|
+
if (
|
|
495
|
+
not name
|
|
496
|
+
or any(part.startswith(".") for part in name.split("/"))
|
|
497
|
+
or os.path.commonpath([root, candidate]) != root
|
|
498
|
+
):
|
|
499
|
+
_respond(writer, 404, b"not found", "text/plain")
|
|
500
|
+
return
|
|
501
|
+
_respond_file(writer, candidate)
|
|
502
|
+
return
|
|
503
|
+
if path == "/manifest":
|
|
504
|
+
_respond_json(writer, self.manifest())
|
|
505
|
+
return
|
|
506
|
+
if path == "/status":
|
|
507
|
+
_respond_json(writer, self.status())
|
|
508
|
+
return
|
|
509
|
+
if path.startswith("/file/"):
|
|
510
|
+
rel = path[len("/file/") :]
|
|
511
|
+
snap = self.snapshot
|
|
512
|
+
if rel not in snap.files:
|
|
513
|
+
_respond(writer, 404, b"not found", "text/plain")
|
|
514
|
+
return
|
|
515
|
+
data = snap.read(rel)
|
|
516
|
+
if data is None:
|
|
517
|
+
_respond(writer, 404, b"not found", "text/plain")
|
|
518
|
+
return
|
|
519
|
+
_respond(writer, 200, data, mimetypes.guess_type(rel)[0] or "application/octet-stream")
|
|
520
|
+
return
|
|
521
|
+
_respond(writer, 404, b"not found", "text/plain")
|
|
522
|
+
|
|
523
|
+
async def _serve_websocket(
|
|
524
|
+
self,
|
|
525
|
+
reader: asyncio.StreamReader,
|
|
526
|
+
writer: asyncio.StreamWriter,
|
|
527
|
+
headers: Dict[str, str],
|
|
528
|
+
path: str,
|
|
529
|
+
query: Dict[str, str],
|
|
530
|
+
) -> None:
|
|
531
|
+
try:
|
|
532
|
+
writer.write(ws.server_handshake(headers))
|
|
533
|
+
await writer.drain()
|
|
534
|
+
except (ws.HandshakeError, ConnectionError):
|
|
535
|
+
writer.close()
|
|
536
|
+
return
|
|
537
|
+
role = query.get("role", "client")
|
|
538
|
+
if path != "/ws":
|
|
539
|
+
writer.write(ws.encode_close(1008, "unknown path"))
|
|
540
|
+
writer.close()
|
|
541
|
+
return
|
|
542
|
+
if role == "preview":
|
|
543
|
+
await self._preview_session(reader, writer, query)
|
|
544
|
+
else:
|
|
545
|
+
await self._client_session(reader, writer)
|
|
546
|
+
|
|
547
|
+
async def _read_messages(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> AsyncIterator[str]:
|
|
548
|
+
"""Yield text messages until the peer closes; answers pings."""
|
|
549
|
+
decoder = ws.FrameDecoder()
|
|
550
|
+
while True:
|
|
551
|
+
try:
|
|
552
|
+
chunk = await reader.read(65536)
|
|
553
|
+
except (ConnectionError, asyncio.CancelledError, OSError):
|
|
554
|
+
return
|
|
555
|
+
if not chunk:
|
|
556
|
+
return
|
|
557
|
+
try:
|
|
558
|
+
messages = list(decoder.feed(chunk))
|
|
559
|
+
except ws.WebSocketError:
|
|
560
|
+
try:
|
|
561
|
+
writer.write(ws.encode_close(1002, "protocol error"))
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
return
|
|
565
|
+
for opcode, payload in messages:
|
|
566
|
+
if opcode == ws.TEXT:
|
|
567
|
+
try:
|
|
568
|
+
yield payload.decode("utf-8")
|
|
569
|
+
except UnicodeDecodeError:
|
|
570
|
+
continue
|
|
571
|
+
elif opcode == ws.PING:
|
|
572
|
+
writer.write(ws.encode_frame(ws.PONG, payload))
|
|
573
|
+
elif opcode == ws.CLOSE:
|
|
574
|
+
try:
|
|
575
|
+
writer.write(ws.encode_close())
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
async def _client_session(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
581
|
+
self._client_ids += 1
|
|
582
|
+
client = DevClient(id=self._client_ids, writer=writer)
|
|
583
|
+
self._clients[client.id] = client
|
|
584
|
+
try:
|
|
585
|
+
async for text in self._read_messages(reader, writer):
|
|
586
|
+
try:
|
|
587
|
+
message = json.loads(text)
|
|
588
|
+
except ValueError:
|
|
589
|
+
continue
|
|
590
|
+
if isinstance(message, dict):
|
|
591
|
+
self._on_client_message(client, message)
|
|
592
|
+
finally:
|
|
593
|
+
self._drop_client(client)
|
|
594
|
+
|
|
595
|
+
def _on_client_message(self, client: DevClient, message: Dict[str, Any]) -> None:
|
|
596
|
+
kind = message.get("type")
|
|
597
|
+
if kind == "hello":
|
|
598
|
+
client.platform = str(message.get("platform") or "unknown")
|
|
599
|
+
client.device = str(message.get("device") or "")
|
|
600
|
+
client.app = str(message.get("app") or "")
|
|
601
|
+
files = message.get("files")
|
|
602
|
+
client.files = {str(k): str(v) for k, v in files.items()} if isinstance(files, dict) else {}
|
|
603
|
+
self.log(f"[pn] {client.label()} connected")
|
|
604
|
+
snap = self.snapshot
|
|
605
|
+
changed = sorted(p for p, digest in snap.files.items() if client.files.get(p) != digest)
|
|
606
|
+
removed = sorted(p for p in client.files if p not in snap.files)
|
|
607
|
+
try:
|
|
608
|
+
client.writer.write(
|
|
609
|
+
ws.encode_frame(ws.TEXT, self._sync_message("sync", snap, changed, removed).encode("utf-8"))
|
|
610
|
+
)
|
|
611
|
+
except Exception:
|
|
612
|
+
self._drop_client(client)
|
|
613
|
+
return
|
|
614
|
+
if kind == "log":
|
|
615
|
+
level = str(message.get("level") or "info")
|
|
616
|
+
text = str(message.get("text") or "").rstrip("\n")
|
|
617
|
+
prefix = f"[{client.label()}]"
|
|
618
|
+
if level in ("error", "warn", "warning"):
|
|
619
|
+
prefix = f"[{client.label()} {level}]"
|
|
620
|
+
for line in text.split("\n"):
|
|
621
|
+
self.log(f"{prefix} {line}")
|
|
622
|
+
return
|
|
623
|
+
if kind == "error":
|
|
624
|
+
phase = str(message.get("phase") or "runtime")
|
|
625
|
+
text = str(message.get("text") or "").rstrip("\n")
|
|
626
|
+
self.log(f"[{client.label()}] error during {phase}:")
|
|
627
|
+
for line in text.split("\n"):
|
|
628
|
+
self.log(f" {line}")
|
|
629
|
+
return
|
|
630
|
+
if kind == "reloaded":
|
|
631
|
+
mode = str(message.get("mode") or "reload")
|
|
632
|
+
modules = message.get("modules") or []
|
|
633
|
+
what = ", ".join(str(m) for m in modules) if isinstance(modules, list) and modules else "app"
|
|
634
|
+
self.log(f"[{client.label()}] {mode}: {what}")
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
async def _preview_session(
|
|
638
|
+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, query: Dict[str, str]
|
|
639
|
+
) -> None:
|
|
640
|
+
channel = self._preview
|
|
641
|
+
if channel is None:
|
|
642
|
+
writer.write(ws.encode_close(1013, "no preview running"))
|
|
643
|
+
writer.close()
|
|
644
|
+
return
|
|
645
|
+
previous = self._preview_peer
|
|
646
|
+
if previous is not None and not previous.closed.is_set():
|
|
647
|
+
# One page at a time: the newest tab takes over the app.
|
|
648
|
+
try:
|
|
649
|
+
previous._send_now(json.dumps(["dev", {"type": "superseded"}]))
|
|
650
|
+
except Exception:
|
|
651
|
+
pass
|
|
652
|
+
previous._close_now()
|
|
653
|
+
channel.on_preview_disconnected(previous)
|
|
654
|
+
peer = PreviewPeer(self, writer)
|
|
655
|
+
self._preview_peer = peer
|
|
656
|
+
channel.on_preview_connected(peer, dict(query))
|
|
657
|
+
try:
|
|
658
|
+
async for text in self._read_messages(reader, writer):
|
|
659
|
+
channel.on_preview_message(peer, text)
|
|
660
|
+
finally:
|
|
661
|
+
peer.closed.set()
|
|
662
|
+
if self._preview_peer is peer:
|
|
663
|
+
self._preview_peer = None
|
|
664
|
+
channel.on_preview_disconnected(peer)
|
|
665
|
+
try:
|
|
666
|
+
writer.close()
|
|
667
|
+
except Exception:
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# ======================================================================
|
|
672
|
+
# HTTP helpers
|
|
673
|
+
# ======================================================================
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _encode_file(path: str, digest: str, data: bytes) -> Dict[str, Any]:
|
|
677
|
+
try:
|
|
678
|
+
return {"path": path, "sha256": digest, "content": data.decode("utf-8"), "encoding": "utf8"}
|
|
679
|
+
except UnicodeDecodeError:
|
|
680
|
+
return {"path": path, "sha256": digest, "content": base64.b64encode(data).decode("ascii"), "encoding": "base64"}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
_STATUS_TEXT = {200: "OK", 404: "Not Found", 405: "Method Not Allowed", 500: "Internal Server Error"}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _respond(writer: asyncio.StreamWriter, status: int, body: bytes, content_type: str) -> None:
|
|
687
|
+
head = (
|
|
688
|
+
f"HTTP/1.1 {status} {_STATUS_TEXT.get(status, 'OK')}\r\n"
|
|
689
|
+
f"Content-Type: {content_type}\r\n"
|
|
690
|
+
f"Content-Length: {len(body)}\r\n"
|
|
691
|
+
"Cache-Control: no-store\r\n"
|
|
692
|
+
"Access-Control-Allow-Origin: *\r\n"
|
|
693
|
+
"Connection: close\r\n"
|
|
694
|
+
"\r\n"
|
|
695
|
+
).encode("ascii")
|
|
696
|
+
try:
|
|
697
|
+
writer.write(head + body)
|
|
698
|
+
except Exception:
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _respond_json(writer: asyncio.StreamWriter, document: Any) -> None:
|
|
703
|
+
_respond(writer, 200, json.dumps(document).encode("utf-8"), "application/json; charset=utf-8")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _respond_file(writer: asyncio.StreamWriter, path: str) -> None:
|
|
707
|
+
try:
|
|
708
|
+
with open(path, "rb") as handle:
|
|
709
|
+
data = handle.read()
|
|
710
|
+
except OSError:
|
|
711
|
+
_respond(writer, 404, b"not found", "text/plain")
|
|
712
|
+
return
|
|
713
|
+
content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
|
|
714
|
+
if path.endswith(".js"):
|
|
715
|
+
content_type = "text/javascript"
|
|
716
|
+
if content_type.startswith("text/") and "charset" not in content_type:
|
|
717
|
+
content_type += "; charset=utf-8"
|
|
718
|
+
_respond(writer, 200, data, content_type)
|