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,170 @@
|
|
|
1
|
+
"""JSON encoding for the native bridge.
|
|
2
|
+
|
|
3
|
+
The bridge speaks JSON in both directions (see ``docs/concepts/bridge.md``).
|
|
4
|
+
Prop values coming out of the reconciler are almost, but not quite,
|
|
5
|
+
JSON: they may contain ``frozenset`` event-name sets, tuples, floats
|
|
6
|
+
that are infinite, and Python callables that native must never see.
|
|
7
|
+
[`to_jsonable`][pythonnative.bridge.codec.to_jsonable] normalizes a
|
|
8
|
+
value into something ``json.dumps`` accepts and reports the keys it had
|
|
9
|
+
to drop so the backend can keep them Python-side.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
from typing import Any, Dict, List, Sequence, Tuple, Union
|
|
17
|
+
|
|
18
|
+
from ..mutations import CreateOp, DestroyOp, InsertOp, Mutation, SetFrameOp, UpdateOp
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"INF",
|
|
22
|
+
"NEG_INF",
|
|
23
|
+
"dumps",
|
|
24
|
+
"encode_transaction",
|
|
25
|
+
"loads",
|
|
26
|
+
"split_props",
|
|
27
|
+
"to_jsonable",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
INF = "inf"
|
|
31
|
+
"""Wire spelling of ``math.inf`` (JSON has no infinity literal)."""
|
|
32
|
+
|
|
33
|
+
NEG_INF = "-inf"
|
|
34
|
+
"""Wire spelling of ``-math.inf``."""
|
|
35
|
+
|
|
36
|
+
_SCALARS = (str, int, bool, type(None))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_jsonable(value: Any) -> Any:
|
|
40
|
+
"""Return a JSON-serializable copy of ``value``.
|
|
41
|
+
|
|
42
|
+
- ``set`` / ``frozenset`` / ``tuple`` become lists (sets are sorted
|
|
43
|
+
when their members are strings so the output is deterministic).
|
|
44
|
+
- Infinite floats become the strings ``"inf"`` / ``"-inf"``; NaN
|
|
45
|
+
becomes ``None`` (native clamps missing geometry to zero).
|
|
46
|
+
- Dataclass-like objects with ``to_json()`` are converted through
|
|
47
|
+
it.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: When ``value`` (or something nested in it) has no
|
|
51
|
+
JSON representation; callers decide whether to drop it.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, _SCALARS):
|
|
54
|
+
return value
|
|
55
|
+
if isinstance(value, float):
|
|
56
|
+
if math.isfinite(value):
|
|
57
|
+
return value
|
|
58
|
+
if math.isnan(value):
|
|
59
|
+
return None
|
|
60
|
+
return INF if value > 0 else NEG_INF
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
return {str(k): to_jsonable(v) for k, v in value.items()}
|
|
63
|
+
if isinstance(value, (list, tuple)):
|
|
64
|
+
return [to_jsonable(v) for v in value]
|
|
65
|
+
if isinstance(value, (set, frozenset)):
|
|
66
|
+
items = list(value)
|
|
67
|
+
try:
|
|
68
|
+
items.sort()
|
|
69
|
+
except TypeError:
|
|
70
|
+
pass
|
|
71
|
+
return [to_jsonable(v) for v in items]
|
|
72
|
+
to_json = getattr(value, "to_json", None)
|
|
73
|
+
if callable(to_json):
|
|
74
|
+
return to_jsonable(to_json())
|
|
75
|
+
raise TypeError(f"value of type {type(value).__name__} is not bridge-serializable")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def split_props(props: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
79
|
+
"""Split ``props`` into ``(wire_props, python_props)``.
|
|
80
|
+
|
|
81
|
+
``wire_props`` is JSON-ready; ``python_props`` holds every prop
|
|
82
|
+
that can't cross the bridge (callables such as ``render_row``,
|
|
83
|
+
arbitrary objects). The backend keeps the latter in a per-tag
|
|
84
|
+
sidecar so native-backed handlers that need them (virtualized
|
|
85
|
+
list rows) can still reach them.
|
|
86
|
+
"""
|
|
87
|
+
wire: Dict[str, Any] = {}
|
|
88
|
+
python: Dict[str, Any] = {}
|
|
89
|
+
for key, value in props.items():
|
|
90
|
+
if callable(value) and not isinstance(value, type):
|
|
91
|
+
from ..animated import AnimatedEvent
|
|
92
|
+
|
|
93
|
+
if isinstance(value, AnimatedEvent):
|
|
94
|
+
wire.setdefault("_pn_animated_events", {})[key] = {
|
|
95
|
+
name: id(node) for name, node in value._bindings.items()
|
|
96
|
+
}
|
|
97
|
+
python[key] = value
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
wire[key] = to_jsonable(value)
|
|
101
|
+
except TypeError:
|
|
102
|
+
python[key] = value
|
|
103
|
+
return wire, python
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def encode_transaction(
|
|
107
|
+
ops: Sequence[Mutation], prop_filter: Any = None
|
|
108
|
+
) -> Tuple[str, List[Tuple[int, Dict[str, Any]]]]:
|
|
109
|
+
"""Encode a mutation batch as the bridge's transaction JSON.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
ops: Ordered mutations from the reconciler.
|
|
113
|
+
prop_filter: Unused hook kept for symmetry with the test
|
|
114
|
+
registry; reserved for per-type prop rewriting.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
``(json_text, python_props)`` where ``python_props`` lists
|
|
118
|
+
``(tag, props)`` pairs for values that stayed Python-side (for
|
|
119
|
+
``c`` and ``u`` ops). An update that *removes* a Python-side
|
|
120
|
+
prop reports it as ``None`` so the sidecar can drop it.
|
|
121
|
+
"""
|
|
122
|
+
del prop_filter
|
|
123
|
+
encoded: List[Any] = []
|
|
124
|
+
sidecar: List[Tuple[int, Dict[str, Any]]] = []
|
|
125
|
+
for op in ops:
|
|
126
|
+
if isinstance(op, CreateOp):
|
|
127
|
+
wire, python = split_props(op.props)
|
|
128
|
+
encoded.append(["c", op.tag, op.type_name, wire])
|
|
129
|
+
if python:
|
|
130
|
+
sidecar.append((op.tag, python))
|
|
131
|
+
elif isinstance(op, UpdateOp):
|
|
132
|
+
wire, python = split_props(op.changed_props)
|
|
133
|
+
encoded.append(["u", op.tag, wire])
|
|
134
|
+
if python:
|
|
135
|
+
sidecar.append((op.tag, python))
|
|
136
|
+
elif isinstance(op, InsertOp):
|
|
137
|
+
encoded.append(["i", op.parent_tag, op.child_tag, op.index])
|
|
138
|
+
elif isinstance(op, DestroyOp):
|
|
139
|
+
encoded.append(["d", op.tag])
|
|
140
|
+
elif isinstance(op, SetFrameOp):
|
|
141
|
+
encoded.append(["f", op.tag, _num(op.x), _num(op.y), _num(op.width), _num(op.height)])
|
|
142
|
+
else: # pragma: no cover - the reconciler only emits the five op kinds
|
|
143
|
+
raise TypeError(f"Unknown mutation op: {op!r}")
|
|
144
|
+
return dumps(encoded), sidecar
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _num(value: float) -> float:
|
|
148
|
+
"""Clamp a frame coordinate to a finite float (native treats NaN/inf as 0)."""
|
|
149
|
+
try:
|
|
150
|
+
f = float(value)
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
return 0.0
|
|
153
|
+
return f if math.isfinite(f) else 0.0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def dumps(value: Any) -> str:
|
|
157
|
+
"""Compact ``json.dumps`` used for every bridge payload."""
|
|
158
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def loads(text: Union[str, bytes, bytearray, None]) -> Any:
|
|
162
|
+
"""Parse a bridge payload (``str`` or UTF-8 bytes); empty / ``None`` input yields ``None``."""
|
|
163
|
+
if text is None:
|
|
164
|
+
return None
|
|
165
|
+
if isinstance(text, (bytes, bytearray)):
|
|
166
|
+
text = text.decode("utf-8")
|
|
167
|
+
text = text.strip()
|
|
168
|
+
if not text:
|
|
169
|
+
return None
|
|
170
|
+
return json.loads(text)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Versioned commit validation and surface ownership.
|
|
2
|
+
|
|
3
|
+
A commit is validated in full before it changes a native tree. Revisions are
|
|
4
|
+
strictly consecutive within an application and surface. A failed native mount
|
|
5
|
+
poisons that surface until the host explicitly resets and remounts it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import math
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
PROTOCOL_VERSION = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CommitError(RuntimeError):
|
|
19
|
+
"""A malformed, stale, or failed native transaction."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ViewState:
|
|
24
|
+
"""Wire-visible view state used to validate a whole transaction."""
|
|
25
|
+
|
|
26
|
+
type_name: str
|
|
27
|
+
props: dict[str, Any]
|
|
28
|
+
parent: int | None = None
|
|
29
|
+
children: list[int] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CommitState:
|
|
34
|
+
"""Committed revision and structural state for one native surface."""
|
|
35
|
+
|
|
36
|
+
application: str = ""
|
|
37
|
+
surface: int = 0
|
|
38
|
+
revision: int = 0
|
|
39
|
+
views: dict[int, ViewState] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def prepare(self, envelope: Any) -> CommitState:
|
|
42
|
+
"""Validate without mutating this state and return the candidate state."""
|
|
43
|
+
if not isinstance(envelope, dict) or envelope.get("version") != PROTOCOL_VERSION:
|
|
44
|
+
raise CommitError("Expected a protocol v2 commit envelope; rebuild the native client")
|
|
45
|
+
application, surface, revision = (envelope.get(k) for k in ("application", "surface", "revision"))
|
|
46
|
+
if not isinstance(application, str) or not application or type(surface) is not int or surface <= 0:
|
|
47
|
+
raise CommitError("Invalid application or surface identity")
|
|
48
|
+
if self.application and (application, surface) != (self.application, self.surface):
|
|
49
|
+
raise CommitError("Commit belongs to another application or surface")
|
|
50
|
+
if type(revision) is not int or revision != self.revision + 1:
|
|
51
|
+
raise CommitError(f"Expected revision {self.revision + 1}, received {revision!r}")
|
|
52
|
+
ops = envelope.get("ops")
|
|
53
|
+
if not isinstance(ops, list):
|
|
54
|
+
raise CommitError("Commit ops must be a list")
|
|
55
|
+
result = CommitState(application, surface, revision, copy.deepcopy(self.views))
|
|
56
|
+
for index, op in enumerate(ops):
|
|
57
|
+
try:
|
|
58
|
+
result._validate_op(op)
|
|
59
|
+
except (KeyError, TypeError, ValueError, IndexError) as exc:
|
|
60
|
+
raise CommitError(f"Invalid op {index}: {exc}") from exc
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
def _validate_op(self, op: Any) -> None:
|
|
64
|
+
if not isinstance(op, list) or len(op) < 2:
|
|
65
|
+
raise ValueError("Expected an operation array")
|
|
66
|
+
code, tag = op[:2]
|
|
67
|
+
lengths = {"c": 4, "u": 3, "i": 4, "d": 2, "f": 6}
|
|
68
|
+
if code not in lengths or len(op) != lengths[code] or type(tag) is not int or tag <= 0:
|
|
69
|
+
raise ValueError("Invalid opcode, arity, or tag")
|
|
70
|
+
if code == "c":
|
|
71
|
+
if tag in self.views or not isinstance(op[2], str) or not op[2] or not isinstance(op[3], dict):
|
|
72
|
+
raise ValueError("Invalid or duplicate create")
|
|
73
|
+
self.views[tag] = ViewState(op[2], dict(op[3]))
|
|
74
|
+
return
|
|
75
|
+
view = self.views[tag]
|
|
76
|
+
if code == "u":
|
|
77
|
+
if not isinstance(op[2], dict):
|
|
78
|
+
raise ValueError("Update requires a property mapping")
|
|
79
|
+
for name, value in op[2].items():
|
|
80
|
+
if value is None:
|
|
81
|
+
view.props.pop(name, None)
|
|
82
|
+
else:
|
|
83
|
+
view.props[name] = value
|
|
84
|
+
elif code == "i":
|
|
85
|
+
child_tag, index = op[2:]
|
|
86
|
+
if type(child_tag) is not int or type(index) is not int or index < 0:
|
|
87
|
+
raise ValueError("Invalid child or insertion index")
|
|
88
|
+
child = self.views[child_tag]
|
|
89
|
+
ancestor: int | None = tag
|
|
90
|
+
while ancestor is not None:
|
|
91
|
+
if ancestor == child_tag:
|
|
92
|
+
raise ValueError("Insertion creates a cycle")
|
|
93
|
+
ancestor = self.views[ancestor].parent
|
|
94
|
+
if child.parent is not None:
|
|
95
|
+
self.views[child.parent].children.remove(child_tag)
|
|
96
|
+
if index > len(view.children):
|
|
97
|
+
raise ValueError("Insertion index exceeds child count")
|
|
98
|
+
view.children.insert(index, child_tag)
|
|
99
|
+
child.parent = tag
|
|
100
|
+
elif code == "d":
|
|
101
|
+
if view.children:
|
|
102
|
+
raise ValueError("Destroy children before their parent")
|
|
103
|
+
if view.parent is not None:
|
|
104
|
+
self.views[view.parent].children.remove(tag)
|
|
105
|
+
del self.views[tag]
|
|
106
|
+
elif code == "f":
|
|
107
|
+
if any(type(n) not in (float, int) or not math.isfinite(n) for n in op[2:]):
|
|
108
|
+
raise ValueError("Frame values must be finite numbers")
|
|
109
|
+
if op[4] < 0 or op[5] < 0:
|
|
110
|
+
raise ValueError("Frame sizes must be nonnegative")
|
|
111
|
+
|
|
112
|
+
def acknowledgement(self) -> dict[str, Any]:
|
|
113
|
+
"""Identify the exact revision native successfully mounted."""
|
|
114
|
+
return {"ok": True, "application": self.application, "surface": self.surface, "revision": self.revision}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""An in-process stand-in for the native side of the bridge.
|
|
2
|
+
|
|
3
|
+
[`FakeTransport`][pythonnative.bridge.fake.FakeTransport] decodes the
|
|
4
|
+
same JSON the Swift and Kotlin runtimes receive and keeps a tiny view
|
|
5
|
+
tree so tests can assert on what *would* have reached native: created
|
|
6
|
+
types and props, insert order, frames, commands, animation requests,
|
|
7
|
+
and module calls. It also lets tests play the native side, firing
|
|
8
|
+
events and module results back through
|
|
9
|
+
[`native_callback`][pythonnative.bridge.native_callback].
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from . import codec
|
|
18
|
+
from .commits import PROTOCOL_VERSION, CommitState
|
|
19
|
+
|
|
20
|
+
__all__ = ["FakeNativeView", "FakeTransport"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FakeNativeView:
|
|
24
|
+
"""One decoded native view: type, merged props, children, and frame."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, tag: int, type_name: str, props: Dict[str, Any]) -> None:
|
|
27
|
+
self.tag = tag
|
|
28
|
+
self.type_name = type_name
|
|
29
|
+
self.props: Dict[str, Any] = dict(props)
|
|
30
|
+
self.children: List["FakeNativeView"] = []
|
|
31
|
+
self.parent: Optional["FakeNativeView"] = None
|
|
32
|
+
self.frame: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0)
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return f"<native {self.type_name} tag={self.tag}>"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ModuleHandler = Callable[[str, Dict[str, Any]], Any]
|
|
39
|
+
"""``handler(method, args) -> value`` for a fake native module."""
|
|
40
|
+
|
|
41
|
+
DEFAULT_MEASURE: Dict[str, Tuple[float, float]] = {
|
|
42
|
+
"Text": (60.0, 16.0),
|
|
43
|
+
"Button": (80.0, 32.0),
|
|
44
|
+
"Image": (40.0, 40.0),
|
|
45
|
+
"TextInput": (120.0, 32.0),
|
|
46
|
+
"TabBar": (320.0, 49.0),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FakeTransport:
|
|
51
|
+
"""Decode bridge traffic into inspectable Python structures.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
views: Live views by tag.
|
|
55
|
+
transactions: Every decoded transaction (list of op lists).
|
|
56
|
+
commands: ``(tag, name, args)`` tuples in call order.
|
|
57
|
+
animations: ``(tag, request)`` tuples in call order.
|
|
58
|
+
calls: ``(module, method, args)`` tuples in call order.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
name = "fake"
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self, *, version: int = PROTOCOL_VERSION, measure: Optional[Dict[str, Tuple[float, float]]] = None
|
|
65
|
+
) -> None:
|
|
66
|
+
self._version = version
|
|
67
|
+
self.commit_state = CommitState()
|
|
68
|
+
self.measure_table = dict(DEFAULT_MEASURE if measure is None else measure)
|
|
69
|
+
self.views: Dict[int, FakeNativeView] = {}
|
|
70
|
+
self.transactions: List[List[Any]] = []
|
|
71
|
+
self.commands: List[Tuple[int, str, Dict[str, Any]]] = []
|
|
72
|
+
self.animations: List[Tuple[int, Dict[str, Any]]] = []
|
|
73
|
+
self.calls: List[Tuple[str, str, Dict[str, Any]]] = []
|
|
74
|
+
self.command_results: Dict[str, Any] = {}
|
|
75
|
+
self.animate_results: Dict[str, Any] = {"start": {"ok": True}}
|
|
76
|
+
self.module_handlers: Dict[str, ModuleHandler] = {}
|
|
77
|
+
self.pending_calls: List[Tuple[str, int, str, Dict[str, Any]]] = []
|
|
78
|
+
self.callback: Optional[Callable[[str, int, str, str], Optional[str]]] = None
|
|
79
|
+
|
|
80
|
+
# -- Transport protocol ----------------------------------------------
|
|
81
|
+
|
|
82
|
+
def protocol_version(self) -> int:
|
|
83
|
+
"""Return the protocol version compiled into the native library."""
|
|
84
|
+
return self._version
|
|
85
|
+
|
|
86
|
+
def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
|
|
87
|
+
"""Install ``callback`` as the native -> Python entry point."""
|
|
88
|
+
self.callback = callback
|
|
89
|
+
|
|
90
|
+
def apply(self, transaction_json: str) -> str:
|
|
91
|
+
"""Validate a full revision before applying any native mutations."""
|
|
92
|
+
envelope = json.loads(transaction_json)
|
|
93
|
+
if self.commit_state.application and envelope.get("application") != self.commit_state.application:
|
|
94
|
+
self.commit_state = CommitState()
|
|
95
|
+
self.views.clear()
|
|
96
|
+
candidate = self.commit_state.prepare(envelope)
|
|
97
|
+
ops = envelope["ops"]
|
|
98
|
+
self.transactions.append(ops)
|
|
99
|
+
for op in ops:
|
|
100
|
+
self._apply_one(op)
|
|
101
|
+
self.commit_state = candidate
|
|
102
|
+
return codec.dumps(candidate.acknowledgement())
|
|
103
|
+
|
|
104
|
+
def _apply_one(self, op: List[Any]) -> None:
|
|
105
|
+
code = op[0]
|
|
106
|
+
if code == "c":
|
|
107
|
+
_, tag, type_name, props = op
|
|
108
|
+
if tag in self.views:
|
|
109
|
+
raise AssertionError(f"create: tag {tag} already exists")
|
|
110
|
+
self.views[tag] = FakeNativeView(tag, type_name, props)
|
|
111
|
+
elif code == "u":
|
|
112
|
+
_, tag, changed = op
|
|
113
|
+
view = self._require(tag, "update")
|
|
114
|
+
for key, value in changed.items():
|
|
115
|
+
if value is None:
|
|
116
|
+
view.props.pop(key, None)
|
|
117
|
+
else:
|
|
118
|
+
view.props[key] = value
|
|
119
|
+
elif code == "i":
|
|
120
|
+
_, parent_tag, child_tag, index = op
|
|
121
|
+
parent = self._require(parent_tag, "insert")
|
|
122
|
+
child = self._require(child_tag, "insert")
|
|
123
|
+
if child.parent is not None:
|
|
124
|
+
child.parent.children.remove(child)
|
|
125
|
+
index = max(0, min(int(index), len(parent.children)))
|
|
126
|
+
parent.children.insert(index, child)
|
|
127
|
+
child.parent = parent
|
|
128
|
+
elif code == "d":
|
|
129
|
+
_, tag = op
|
|
130
|
+
view = self.views.pop(tag, None)
|
|
131
|
+
if view is None:
|
|
132
|
+
raise AssertionError(f"destroy: unknown tag {tag}")
|
|
133
|
+
if view.parent is not None:
|
|
134
|
+
view.parent.children.remove(view)
|
|
135
|
+
view.parent = None
|
|
136
|
+
elif code == "f":
|
|
137
|
+
_, tag, x, y, w, h = op
|
|
138
|
+
self._require(tag, "set_frame").frame = (float(x), float(y), float(w), float(h))
|
|
139
|
+
else:
|
|
140
|
+
raise AssertionError(f"unknown opcode {code!r}")
|
|
141
|
+
|
|
142
|
+
def _require(self, tag: int, what: str) -> FakeNativeView:
|
|
143
|
+
view = self.views.get(tag)
|
|
144
|
+
if view is None:
|
|
145
|
+
raise AssertionError(f"{what}: unknown tag {tag}")
|
|
146
|
+
return view
|
|
147
|
+
|
|
148
|
+
def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
149
|
+
"""Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
|
|
150
|
+
view = self.views.get(tag)
|
|
151
|
+
if view is None:
|
|
152
|
+
return (0.0, 0.0)
|
|
153
|
+
return self.measure_table.get(view.type_name, (0.0, 0.0))
|
|
154
|
+
|
|
155
|
+
def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
|
|
156
|
+
"""Run an imperative command on one view; returns its JSON result or ``None``."""
|
|
157
|
+
args = codec.loads(args_json) or {}
|
|
158
|
+
self.commands.append((tag, name, args))
|
|
159
|
+
result = self.command_results.get(name)
|
|
160
|
+
return None if result is None else codec.dumps(result)
|
|
161
|
+
|
|
162
|
+
def animate(self, tag: int, request_json: str) -> Optional[str]:
|
|
163
|
+
"""Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
|
|
164
|
+
request = codec.loads(request_json) or {}
|
|
165
|
+
self.animations.append((tag, request))
|
|
166
|
+
result = self.animate_results.get(str(request.get("op")))
|
|
167
|
+
return None if result is None else codec.dumps(result)
|
|
168
|
+
|
|
169
|
+
def call(self, module: str, method: str, args_json: str) -> Optional[str]:
|
|
170
|
+
"""Call a native module method with a ``{"call_id", "args"}`` envelope."""
|
|
171
|
+
envelope = codec.loads(args_json) or {}
|
|
172
|
+
args = envelope.get("args") or {}
|
|
173
|
+
call_id = int(envelope.get("call_id", 0) or 0)
|
|
174
|
+
self.calls.append((module, method, args))
|
|
175
|
+
handler = self.module_handlers.get(module)
|
|
176
|
+
if handler is None:
|
|
177
|
+
return codec.dumps({"ok": True, "value": None})
|
|
178
|
+
try:
|
|
179
|
+
value = handler(method, args)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
return codec.dumps({"ok": False, "error": str(exc)})
|
|
182
|
+
if value is _PENDING:
|
|
183
|
+
self.pending_calls.append((module, call_id, method, args))
|
|
184
|
+
return codec.dumps({"pending": True})
|
|
185
|
+
return codec.dumps({"ok": True, "value": codec.to_jsonable(value)})
|
|
186
|
+
|
|
187
|
+
# -- Playing the native side ------------------------------------------
|
|
188
|
+
|
|
189
|
+
def fire(self, tag: int, name: str, *args: Any) -> Any:
|
|
190
|
+
"""Emit a view event as native would; returns the decoded handler result."""
|
|
191
|
+
self._event_sequence = getattr(self, "_event_sequence", 0) + 1
|
|
192
|
+
envelope = self.commit_state.acknowledgement()
|
|
193
|
+
envelope.pop("ok")
|
|
194
|
+
envelope.update(sequence=self._event_sequence, args=[codec.to_jsonable(a) for a in args])
|
|
195
|
+
return self._callback("event", tag, name, codec.dumps(envelope))
|
|
196
|
+
|
|
197
|
+
def emit_module_event(self, module: str, event: str, payload: Any = None) -> None:
|
|
198
|
+
"""Push an unsolicited module event (``AppState`` ``change``, ...) into Python."""
|
|
199
|
+
self._callback("module", 0, module, codec.dumps({"event": event, "payload": codec.to_jsonable(payload)}))
|
|
200
|
+
|
|
201
|
+
def resolve_pending(self, call_id: int, value: Any = None, *, error: Optional[str] = None) -> None:
|
|
202
|
+
"""Settle a call that returned ``pending``."""
|
|
203
|
+
module = next((m for m, cid, _, _ in self.pending_calls if cid == call_id), None)
|
|
204
|
+
if module is None:
|
|
205
|
+
raise AssertionError(f"no pending call with id {call_id}")
|
|
206
|
+
self.pending_calls = [p for p in self.pending_calls if p[1] != call_id]
|
|
207
|
+
body: Dict[str, Any] = {"call_id": call_id, "ok": error is None}
|
|
208
|
+
if error is None:
|
|
209
|
+
body["value"] = codec.to_jsonable(value)
|
|
210
|
+
else:
|
|
211
|
+
body["error"] = error
|
|
212
|
+
self._callback("module", 0, module, codec.dumps(body))
|
|
213
|
+
|
|
214
|
+
def host_event(self, screen: int, event: str, payload: Any = None) -> Any:
|
|
215
|
+
"""Deliver a screen lifecycle event as the native host would; returns the decoded result."""
|
|
216
|
+
return self._callback("host", screen, event, codec.dumps(codec.to_jsonable(payload)))
|
|
217
|
+
|
|
218
|
+
def complete_animation(self, anim_id: int, finished: bool = True) -> None:
|
|
219
|
+
"""Report a native animation as finished (or interrupted) to Python."""
|
|
220
|
+
self._callback("animation", 0, "", codec.dumps({"id": anim_id, "finished": finished}))
|
|
221
|
+
|
|
222
|
+
def _callback(self, kind: str, tag: int, name: str, payload: str) -> Any:
|
|
223
|
+
if self.callback is None:
|
|
224
|
+
raise AssertionError("FakeTransport has no callback; install it with pythonnative.bridge.set_transport")
|
|
225
|
+
return codec.loads(self.callback(kind, tag, name, payload))
|
|
226
|
+
|
|
227
|
+
# -- Introspection -----------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def find(self, type_name: str) -> List[FakeNativeView]:
|
|
230
|
+
"""Every live view whose type is ``type_name``, in creation order."""
|
|
231
|
+
return [v for v in self.views.values() if v.type_name == type_name]
|
|
232
|
+
|
|
233
|
+
def roots(self) -> List[FakeNativeView]:
|
|
234
|
+
"""Views with no parent (the attached screen roots and detached subtrees)."""
|
|
235
|
+
return [v for v in self.views.values() if v.parent is None]
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def pending() -> object:
|
|
239
|
+
"""Sentinel a module handler returns to answer ``pending``."""
|
|
240
|
+
return _PENDING
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
_PENDING = object()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""iOS transport: C-ABI calls into ``PythonNativeKit`` through ``ctypes``.
|
|
2
|
+
|
|
3
|
+
The Swift package exports ``@_cdecl`` symbols (see
|
|
4
|
+
``docs/concepts/bridge.md``). They're resolved from the running process
|
|
5
|
+
with ``ctypes.CDLL(None)``: ``PyDLL`` keeps the GIL held across the
|
|
6
|
+
call, so the main thread is never parked while native applies a
|
|
7
|
+
transaction, and the Python callback registered with
|
|
8
|
+
``pn_bridge_set_callback`` can re-enter Python safely.
|
|
9
|
+
|
|
10
|
+
Strings returned by native are ``strdup``'d; this module copies them
|
|
11
|
+
and hands the pointer back to ``pn_bridge_free``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import ctypes
|
|
17
|
+
from typing import Any, Callable, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
__all__ = ["IOSTransport"]
|
|
20
|
+
|
|
21
|
+
# const char *(*)(const char *kind, int64_t tag, const char *name, const char *payload_json)
|
|
22
|
+
_CALLBACK_TYPE = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int64, ctypes.c_char_p, ctypes.c_char_p)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IOSTransport:
|
|
26
|
+
"""Bind the ``pn_bridge_*`` symbols and expose them as Python methods."""
|
|
27
|
+
|
|
28
|
+
name = "ios"
|
|
29
|
+
|
|
30
|
+
def __init__(self, lib: Any = None) -> None:
|
|
31
|
+
self._lib = lib if lib is not None else ctypes.CDLL(None)
|
|
32
|
+
self._apply = self._sym("pn_bridge_apply", ctypes.c_void_p, [ctypes.c_char_p])
|
|
33
|
+
self._measure = self._sym(
|
|
34
|
+
"pn_bridge_measure",
|
|
35
|
+
None,
|
|
36
|
+
[
|
|
37
|
+
ctypes.c_int64,
|
|
38
|
+
ctypes.c_double,
|
|
39
|
+
ctypes.c_double,
|
|
40
|
+
ctypes.POINTER(ctypes.c_double),
|
|
41
|
+
ctypes.POINTER(ctypes.c_double),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
self._command = self._sym(
|
|
45
|
+
"pn_bridge_command", ctypes.c_void_p, [ctypes.c_int64, ctypes.c_char_p, ctypes.c_char_p]
|
|
46
|
+
)
|
|
47
|
+
self._animate = self._sym("pn_bridge_animate", ctypes.c_void_p, [ctypes.c_int64, ctypes.c_char_p])
|
|
48
|
+
self._call = self._sym("pn_bridge_call", ctypes.c_void_p, [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p])
|
|
49
|
+
self._free = self._sym("pn_bridge_free", None, [ctypes.c_void_p])
|
|
50
|
+
self._set_callback = self._sym("pn_bridge_set_callback", None, [_CALLBACK_TYPE])
|
|
51
|
+
self._version = self._sym("pn_bridge_protocol_version", ctypes.c_int, [])
|
|
52
|
+
# Strong references so the C trampoline and its last return
|
|
53
|
+
# buffer outlive the calls native makes into them.
|
|
54
|
+
self._callback_c: Any = None
|
|
55
|
+
self._last_result: Any = None
|
|
56
|
+
self._out_w = ctypes.c_double(0.0)
|
|
57
|
+
self._out_h = ctypes.c_double(0.0)
|
|
58
|
+
|
|
59
|
+
def _sym(self, name: str, restype: Any, argtypes: Any) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
fn = getattr(self._lib, name)
|
|
62
|
+
except AttributeError as exc:
|
|
63
|
+
raise RuntimeError(
|
|
64
|
+
f"PythonNativeKit symbol {name!r} is missing from the app binary. Rebuild the app with "
|
|
65
|
+
"'pn run ios' or 'pn build ios' so the Swift package is linked."
|
|
66
|
+
) from exc
|
|
67
|
+
fn.restype = restype
|
|
68
|
+
fn.argtypes = argtypes
|
|
69
|
+
return fn
|
|
70
|
+
|
|
71
|
+
# -- protocol -------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def protocol_version(self) -> int:
|
|
74
|
+
"""Return the protocol version compiled into the native library."""
|
|
75
|
+
return int(self._version())
|
|
76
|
+
|
|
77
|
+
def apply(self, transaction_json: str) -> str:
|
|
78
|
+
"""Apply one serialized transaction (a JSON array of ops)."""
|
|
79
|
+
return self._take(self._apply(transaction_json.encode("utf-8"))) or ""
|
|
80
|
+
|
|
81
|
+
def measure(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
82
|
+
"""Return the intrinsic ``(width, height)`` of the view ``tag`` under the constraints."""
|
|
83
|
+
self._measure(
|
|
84
|
+
int(tag), float(max_width), float(max_height), ctypes.byref(self._out_w), ctypes.byref(self._out_h)
|
|
85
|
+
)
|
|
86
|
+
return (float(self._out_w.value), float(self._out_h.value))
|
|
87
|
+
|
|
88
|
+
def command(self, tag: int, name: str, args_json: str) -> Optional[str]:
|
|
89
|
+
"""Run an imperative command on one view; returns its JSON result or ``None``."""
|
|
90
|
+
return self._take(self._command(int(tag), name.encode("utf-8"), args_json.encode("utf-8")))
|
|
91
|
+
|
|
92
|
+
def animate(self, tag: int, request_json: str) -> Optional[str]:
|
|
93
|
+
"""Handle an animation request (``set`` / ``start`` / ``cancel``) for one view."""
|
|
94
|
+
return self._take(self._animate(int(tag), request_json.encode("utf-8")))
|
|
95
|
+
|
|
96
|
+
def call(self, module: str, method: str, args_json: str) -> Optional[str]:
|
|
97
|
+
"""Call a native module method with a ``{"call_id", "args"}`` envelope."""
|
|
98
|
+
return self._take(self._call(module.encode("utf-8"), method.encode("utf-8"), args_json.encode("utf-8")))
|
|
99
|
+
|
|
100
|
+
def set_callback(self, callback: Callable[[str, int, str, str], Optional[str]]) -> None:
|
|
101
|
+
"""Install ``callback`` as the native -> Python entry point."""
|
|
102
|
+
|
|
103
|
+
def trampoline(kind: bytes, tag: int, name: bytes, payload: bytes) -> Optional[int]:
|
|
104
|
+
try:
|
|
105
|
+
result = callback(
|
|
106
|
+
_decode(kind),
|
|
107
|
+
int(tag),
|
|
108
|
+
_decode(name),
|
|
109
|
+
_decode(payload),
|
|
110
|
+
)
|
|
111
|
+
except Exception as exc: # pragma: no cover - last-resort guard
|
|
112
|
+
print(f"[pn.bridge] callback raised: {exc!r}")
|
|
113
|
+
return None
|
|
114
|
+
if result is None:
|
|
115
|
+
return None
|
|
116
|
+
buf = ctypes.create_string_buffer(result.encode("utf-8"))
|
|
117
|
+
self._last_result = buf
|
|
118
|
+
return ctypes.addressof(buf)
|
|
119
|
+
|
|
120
|
+
self._callback_c = _CALLBACK_TYPE(trampoline)
|
|
121
|
+
self._set_callback(self._callback_c)
|
|
122
|
+
|
|
123
|
+
# -- helpers --------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _take(self, ptr: Optional[int]) -> Optional[str]:
|
|
126
|
+
"""Copy a native ``char*`` result and free it."""
|
|
127
|
+
if not ptr:
|
|
128
|
+
return None
|
|
129
|
+
try:
|
|
130
|
+
return ctypes.string_at(ptr).decode("utf-8")
|
|
131
|
+
finally:
|
|
132
|
+
self._free(ptr)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _decode(value: Optional[bytes]) -> str:
|
|
136
|
+
if not value:
|
|
137
|
+
return ""
|
|
138
|
+
return value.decode("utf-8", "replace")
|