pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.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 +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""The on-device view backend: every mutation crosses the bridge as one transaction.
|
|
2
|
+
|
|
3
|
+
[`BridgeBackend`][pythonnative.native_views.bridge_backend.BridgeBackend]
|
|
4
|
+
implements the same protocol the reconciler expects from
|
|
5
|
+
[`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
|
|
6
|
+
(``apply_mutations``, ``resolve_view``, ``measure_intrinsic``,
|
|
7
|
+
``command``, and the animation hooks) but owns no native objects. It
|
|
8
|
+
serializes each commit with
|
|
9
|
+
[`encode_transaction`][pythonnative.bridge.codec.encode_transaction]
|
|
10
|
+
and hands the JSON to the platform transport; Swift and Kotlin
|
|
11
|
+
component managers do the rest.
|
|
12
|
+
|
|
13
|
+
Python keeps two small pieces of per-tag state:
|
|
14
|
+
|
|
15
|
+
- A **prop sidecar** for values that can't cross the bridge (the
|
|
16
|
+
``render_row`` callable of a ``VirtualList``).
|
|
17
|
+
- **Row pools** for virtualized lists, so the synchronous
|
|
18
|
+
``on_bind_row`` protocol can mount a row subtree and answer with its
|
|
19
|
+
root tag.
|
|
20
|
+
|
|
21
|
+
Views are addressed by tag everywhere. ``resolve_view`` returns a
|
|
22
|
+
[`NativeViewRef`][pythonnative.native_views.bridge_backend.NativeViewRef],
|
|
23
|
+
the opaque handle refs receive.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import math
|
|
29
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
30
|
+
|
|
31
|
+
from ..bridge import codec, get_transport
|
|
32
|
+
from ..mutations import CreateOp, DestroyOp, Mutation, UpdateOp
|
|
33
|
+
|
|
34
|
+
__all__ = ["BridgeBackend", "NativeViewRef"]
|
|
35
|
+
|
|
36
|
+
_MEASURE_UNBOUNDED = 1e6
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NativeViewRef:
|
|
40
|
+
"""Opaque handle to a native view living on the other side of the bridge.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
tag: The reconciler-assigned tag; pass it to
|
|
44
|
+
``Reconciler.dispatch_command`` or ``get_registry().command``.
|
|
45
|
+
type_name: The element type (``"Text"``, ``"ScrollView"``, ...).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
__slots__ = ("tag", "type_name")
|
|
49
|
+
|
|
50
|
+
def __init__(self, tag: int, type_name: str) -> None:
|
|
51
|
+
self.tag = tag
|
|
52
|
+
self.type_name = type_name
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"<NativeViewRef {self.type_name} tag={self.tag}>"
|
|
56
|
+
|
|
57
|
+
def __int__(self) -> int:
|
|
58
|
+
return self.tag
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BridgeBackend:
|
|
62
|
+
"""Registry protocol implementation that forwards to the native runtime."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, transport: Any = None) -> None:
|
|
65
|
+
self._transport = transport
|
|
66
|
+
self._types: Dict[int, str] = {}
|
|
67
|
+
self._refs: Dict[int, NativeViewRef] = {}
|
|
68
|
+
self._python_props: Dict[int, Dict[str, Any]] = {}
|
|
69
|
+
self._row_pools: Dict[int, Any] = {}
|
|
70
|
+
self._handlers: Dict[str, Any] = {}
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def transport(self) -> Any:
|
|
74
|
+
"""The transport in use (resolved lazily on first access)."""
|
|
75
|
+
if self._transport is None:
|
|
76
|
+
self._transport = get_transport()
|
|
77
|
+
return self._transport
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# Registration (kept for protocol parity with NativeViewRegistry)
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def register(self, type_name: str, handler: Any) -> None:
|
|
84
|
+
"""Record a Python handler for diagnostics only.
|
|
85
|
+
|
|
86
|
+
On device, rendering is native; a Python ``ViewHandler`` can't
|
|
87
|
+
create platform views. The registration is kept so
|
|
88
|
+
``handler_for`` can answer introspection questions and so the
|
|
89
|
+
SDK's install step doesn't fail, but it is never invoked.
|
|
90
|
+
"""
|
|
91
|
+
self._handlers[type_name] = handler
|
|
92
|
+
|
|
93
|
+
def handler_for(self, type_name: str) -> Any:
|
|
94
|
+
"""Return the diagnostic Python handler registered for ``type_name``."""
|
|
95
|
+
return self._handlers.get(type_name)
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Tag table
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def resolve_view(self, tag: int) -> Optional[NativeViewRef]:
|
|
102
|
+
"""Return the handle for ``tag`` (``None`` if the view is gone)."""
|
|
103
|
+
return self._refs.get(tag)
|
|
104
|
+
|
|
105
|
+
def type_of(self, tag: int) -> Optional[str]:
|
|
106
|
+
"""Return the element type registered for ``tag``."""
|
|
107
|
+
return self._types.get(tag)
|
|
108
|
+
|
|
109
|
+
def live_view_count(self) -> int:
|
|
110
|
+
"""Number of views currently alive on the native side."""
|
|
111
|
+
return len(self._types)
|
|
112
|
+
|
|
113
|
+
def python_props(self, tag: int) -> Dict[str, Any]:
|
|
114
|
+
"""Props held Python-side for ``tag`` (callables never sent to native)."""
|
|
115
|
+
return self._python_props.get(tag, {})
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Commit channel
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def apply_mutations(self, ops: Sequence[Mutation]) -> None:
|
|
122
|
+
"""Serialize ``ops`` and apply them natively in one crossing."""
|
|
123
|
+
if not ops:
|
|
124
|
+
return
|
|
125
|
+
# Bookkeeping first so a native failure never leaves Python
|
|
126
|
+
# believing a destroyed tag is still live.
|
|
127
|
+
destroyed: List[int] = []
|
|
128
|
+
for op in ops:
|
|
129
|
+
if isinstance(op, CreateOp):
|
|
130
|
+
self._types[op.tag] = op.type_name
|
|
131
|
+
self._refs[op.tag] = NativeViewRef(op.tag, op.type_name)
|
|
132
|
+
elif isinstance(op, DestroyOp):
|
|
133
|
+
destroyed.append(op.tag)
|
|
134
|
+
payload, sidecar = codec.encode_transaction(ops)
|
|
135
|
+
for tag, props in sidecar:
|
|
136
|
+
self._python_props.setdefault(tag, {}).update(props)
|
|
137
|
+
for op in ops:
|
|
138
|
+
if isinstance(op, UpdateOp):
|
|
139
|
+
# A prop that changed from a callable to None arrives
|
|
140
|
+
# as None in the wire dict; drop the stale sidecar copy.
|
|
141
|
+
bucket = self._python_props.get(op.tag)
|
|
142
|
+
if bucket:
|
|
143
|
+
for key, value in op.changed_props.items():
|
|
144
|
+
if value is None:
|
|
145
|
+
bucket.pop(key, None)
|
|
146
|
+
if not bucket:
|
|
147
|
+
self._python_props.pop(op.tag, None)
|
|
148
|
+
self.transport.apply(payload)
|
|
149
|
+
for tag in destroyed:
|
|
150
|
+
self._forget(tag)
|
|
151
|
+
|
|
152
|
+
def _forget(self, tag: int) -> None:
|
|
153
|
+
self._types.pop(tag, None)
|
|
154
|
+
self._refs.pop(tag, None)
|
|
155
|
+
self._python_props.pop(tag, None)
|
|
156
|
+
pool = self._row_pools.pop(tag, None)
|
|
157
|
+
if pool is not None:
|
|
158
|
+
pool.release_all()
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Imperative escape hatches
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def measure_intrinsic(self, tag: int, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
165
|
+
"""Ask native for the natural size of ``tag`` under the constraints."""
|
|
166
|
+
if tag not in self._types:
|
|
167
|
+
return (0.0, 0.0)
|
|
168
|
+
w, h = self.transport.measure(tag, _bound(max_width), _bound(max_height))
|
|
169
|
+
return (max(0.0, float(w)), max(0.0, float(h)))
|
|
170
|
+
|
|
171
|
+
def command(self, tag: int, name: str, args: Optional[Dict[str, Any]] = None) -> Any:
|
|
172
|
+
"""Run an imperative command on one view; returns its JSON result or ``None``."""
|
|
173
|
+
if tag not in self._types:
|
|
174
|
+
return None
|
|
175
|
+
result = self.transport.command(tag, name, codec.dumps(codec.to_jsonable(args or {})))
|
|
176
|
+
return codec.loads(result)
|
|
177
|
+
|
|
178
|
+
def set_animated_property(self, tag: int, prop_name: str, value: Any) -> None:
|
|
179
|
+
"""Write one animated property value without animating (a Python-driven frame)."""
|
|
180
|
+
if tag not in self._types:
|
|
181
|
+
return
|
|
182
|
+
self.transport.animate(tag, codec.dumps({"op": "set", "prop": prop_name, "value": codec.to_jsonable(value)}))
|
|
183
|
+
|
|
184
|
+
def start_animation(self, tag: int, anim_id: int, prop_name: str, spec: Dict[str, Any]) -> bool:
|
|
185
|
+
"""Start a native-driven animation; returns whether native accepted it."""
|
|
186
|
+
if tag not in self._types:
|
|
187
|
+
return False
|
|
188
|
+
request = {"op": "start", "id": int(anim_id), "prop": prop_name, "spec": codec.to_jsonable(spec)}
|
|
189
|
+
result = codec.loads(self.transport.animate(tag, codec.dumps(request)))
|
|
190
|
+
return bool(isinstance(result, dict) and result.get("ok"))
|
|
191
|
+
|
|
192
|
+
def cancel_animation(self, tag: int, anim_id: int) -> Any:
|
|
193
|
+
"""Cancel a native animation and return its presentation value, if known."""
|
|
194
|
+
if tag not in self._types:
|
|
195
|
+
return None
|
|
196
|
+
result = codec.loads(self.transport.animate(tag, codec.dumps({"op": "cancel", "id": int(anim_id)})))
|
|
197
|
+
if isinstance(result, dict):
|
|
198
|
+
return result.get("value")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
# Internal (native-originated) events
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def handle_internal_event(self, tag: int, name: str, args: List[Any]) -> Any:
|
|
206
|
+
"""Serve request-style events that no user callback owns.
|
|
207
|
+
|
|
208
|
+
Currently the virtualized-list row protocol:
|
|
209
|
+
|
|
210
|
+
- ``on_bind_row`` ``[{"container", "index", "width", "height"}]``
|
|
211
|
+
mounts or rebinds the row subtree and returns ``{"root": tag}``.
|
|
212
|
+
- ``on_unbind_row`` ``[{"container"}]`` releases it.
|
|
213
|
+
"""
|
|
214
|
+
if name == "on_bind_row":
|
|
215
|
+
return self._bind_row(tag, args[0] if args else {})
|
|
216
|
+
if name == "on_unbind_row":
|
|
217
|
+
info = args[0] if args else {}
|
|
218
|
+
pool = self._row_pools.get(tag)
|
|
219
|
+
if pool is not None and isinstance(info, dict):
|
|
220
|
+
pool.release(int(info.get("container", 0)))
|
|
221
|
+
return None
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def _bind_row(self, tag: int, info: Any) -> Dict[str, Any]:
|
|
225
|
+
if not isinstance(info, dict):
|
|
226
|
+
return {"root": None}
|
|
227
|
+
render_row = self._python_props.get(tag, {}).get("render_row")
|
|
228
|
+
if not callable(render_row):
|
|
229
|
+
return {"root": None}
|
|
230
|
+
from ..virtual_rows import RowHostPool
|
|
231
|
+
|
|
232
|
+
pool = self._row_pools.get(tag)
|
|
233
|
+
if pool is None:
|
|
234
|
+
pool = RowHostPool()
|
|
235
|
+
self._row_pools[tag] = pool
|
|
236
|
+
index = int(info.get("index", 0))
|
|
237
|
+
width = float(info.get("width", 0.0) or 0.0)
|
|
238
|
+
height = float(info.get("height", 0.0) or 0.0)
|
|
239
|
+
root = pool.bind(int(info.get("container", 0)), lambda: render_row(index), width, height)
|
|
240
|
+
root_tag = getattr(root, "tag", None)
|
|
241
|
+
return {"root": int(root_tag) if root_tag is not None else None}
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
# Test helpers
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
def _reset_for_tests(self) -> None:
|
|
248
|
+
for pool in list(self._row_pools.values()):
|
|
249
|
+
pool.release_all()
|
|
250
|
+
self._row_pools.clear()
|
|
251
|
+
self._types.clear()
|
|
252
|
+
self._refs.clear()
|
|
253
|
+
self._python_props.clear()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _bound(value: float) -> float:
|
|
257
|
+
"""Clamp ``math.inf`` to the wire's "unconstrained" sentinel."""
|
|
258
|
+
try:
|
|
259
|
+
f = float(value)
|
|
260
|
+
except (TypeError, ValueError):
|
|
261
|
+
return _MEASURE_UNBOUNDED
|
|
262
|
+
if not math.isfinite(f) or f > _MEASURE_UNBOUNDED:
|
|
263
|
+
return _MEASURE_UNBOUNDED
|
|
264
|
+
return max(0.0, f)
|
pythonnative/platform.py
CHANGED
|
@@ -43,33 +43,37 @@ def _detect_os() -> str:
|
|
|
43
43
|
def _detect_version() -> str:
|
|
44
44
|
"""Return a human-readable platform version string.
|
|
45
45
|
|
|
46
|
-
On
|
|
47
|
-
|
|
46
|
+
On device this asks the native ``Device`` module (``UIDevice
|
|
47
|
+
.systemVersion`` / ``Build.VERSION.RELEASE``). Off-device (the test
|
|
48
48
|
environment), returns the host Python's version so user code can
|
|
49
49
|
still introspect *something*.
|
|
50
50
|
"""
|
|
51
|
-
if IS_IOS:
|
|
51
|
+
if IS_IOS or IS_ANDROID:
|
|
52
52
|
try:
|
|
53
|
-
from
|
|
53
|
+
from .native_modules.registry import native_module
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
info = native_module("Device").call("info")
|
|
56
|
+
if isinstance(info, dict) and info.get("os_version"):
|
|
57
|
+
return str(info["os_version"])
|
|
57
58
|
except Exception:
|
|
58
59
|
pass
|
|
59
60
|
sim_version = os.environ.get("SIMULATOR_RUNTIME_VERSION")
|
|
60
61
|
if sim_version:
|
|
61
62
|
return sim_version
|
|
62
|
-
if IS_ANDROID:
|
|
63
|
-
try:
|
|
64
|
-
from java import jclass
|
|
65
|
-
|
|
66
|
-
Build = jclass("android.os.Build$VERSION")
|
|
67
|
-
return str(Build.RELEASE)
|
|
68
|
-
except Exception:
|
|
69
|
-
pass
|
|
70
63
|
return f"python-{sys.version_info.major}.{sys.version_info.minor}"
|
|
71
64
|
|
|
72
65
|
|
|
66
|
+
class _LazyVersion:
|
|
67
|
+
"""Resolve ``Platform.Version`` on first access (it needs the bridge on device)."""
|
|
68
|
+
|
|
69
|
+
_value: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
def __get__(self, obj: object, owner: Optional[type] = None) -> str:
|
|
72
|
+
if _LazyVersion._value is None:
|
|
73
|
+
_LazyVersion._value = _detect_version()
|
|
74
|
+
return _LazyVersion._value
|
|
75
|
+
|
|
76
|
+
|
|
73
77
|
class Platform:
|
|
74
78
|
"""Platform-aware constants and the ``select`` dispatcher.
|
|
75
79
|
|
|
@@ -82,7 +86,7 @@ class Platform:
|
|
|
82
86
|
OS: str = _detect_os()
|
|
83
87
|
"""``"ios"``, ``"android"``, ``"desktop"``, or ``"test"``."""
|
|
84
88
|
|
|
85
|
-
Version: str =
|
|
89
|
+
Version: str = _LazyVersion() # type: ignore[assignment]
|
|
86
90
|
"""Best-effort OS version string (``"17.4"``, ``"14"``, ``"python-3.11"``)."""
|
|
87
91
|
|
|
88
92
|
is_ios: bool = IS_IOS
|
pythonnative/platform_metrics.py
CHANGED
|
@@ -245,8 +245,8 @@ def ios_tab_bar_height() -> float:
|
|
|
245
245
|
deliberately extends the root view past the bottom safe area for
|
|
246
246
|
this very reason; the tab bar absorbs the inset and UIKit
|
|
247
247
|
renders the pill with internal padding for the home indicator.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
The Swift ``TabBarManager`` applies the same formula natively;
|
|
249
|
+
this Python copy keeps the layout engine's expectations testable
|
|
250
|
+
off device.
|
|
251
251
|
"""
|
|
252
252
|
return IOS_TAB_BAR_BASE_HEIGHT_PT + get_safe_area_insets().bottom
|
pythonnative/project/android.py
CHANGED
pythonnative/project/builder.py
CHANGED
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
import compileall
|
|
29
29
|
import os
|
|
30
30
|
import platform as platform_module
|
|
31
|
+
import shutil
|
|
31
32
|
import subprocess
|
|
32
33
|
import sys
|
|
33
34
|
from dataclasses import dataclass, field
|
|
@@ -37,6 +38,7 @@ from typing import Callable, List, Optional, Sequence, Union
|
|
|
37
38
|
|
|
38
39
|
from . import android as android_config
|
|
39
40
|
from . import ios as ios_config
|
|
41
|
+
from . import plugins as native_plugins
|
|
40
42
|
from . import runtime_assets
|
|
41
43
|
from .android import AndroidLayout
|
|
42
44
|
from .config import AppConfig
|
|
@@ -177,6 +179,20 @@ class BuildArtifacts:
|
|
|
177
179
|
|
|
178
180
|
_TEMPLATE_NAMES = {"android": "android_template", "ios": "ios_template"}
|
|
179
181
|
|
|
182
|
+
# A source checkout accumulates Gradle, SwiftPM, and Xcode outputs next to
|
|
183
|
+
# the native library sources; a staged project must start from a clean tree.
|
|
184
|
+
_TEMPLATE_IGNORE = shutil.ignore_patterns(
|
|
185
|
+
"build",
|
|
186
|
+
".build",
|
|
187
|
+
".gradle",
|
|
188
|
+
".swiftpm",
|
|
189
|
+
"xcuserdata",
|
|
190
|
+
"DerivedData",
|
|
191
|
+
"__pycache__",
|
|
192
|
+
"*.pyc",
|
|
193
|
+
".DS_Store",
|
|
194
|
+
)
|
|
195
|
+
|
|
180
196
|
|
|
181
197
|
def stage_template(template_name: str, destination: Path) -> Path:
|
|
182
198
|
"""Copy a bundled native template into ``destination``.
|
|
@@ -196,22 +212,20 @@ def stage_template(template_name: str, destination: Path) -> Path:
|
|
|
196
212
|
Raises:
|
|
197
213
|
BuildError: If no bundled copy can be located.
|
|
198
214
|
"""
|
|
199
|
-
import shutil
|
|
200
|
-
|
|
201
215
|
dest_path = destination / template_name
|
|
202
216
|
destination.mkdir(parents=True, exist_ok=True)
|
|
203
217
|
|
|
204
218
|
# Dev-first: local source package templates.
|
|
205
219
|
local = Path(__file__).resolve().parents[1] / "templates" / template_name
|
|
206
220
|
if local.is_dir():
|
|
207
|
-
shutil.copytree(local, dest_path, dirs_exist_ok=True)
|
|
221
|
+
shutil.copytree(local, dest_path, dirs_exist_ok=True, ignore=_TEMPLATE_IGNORE)
|
|
208
222
|
return dest_path
|
|
209
223
|
|
|
210
224
|
try:
|
|
211
225
|
candidate = resources.files("pythonnative").joinpath("templates").joinpath(template_name)
|
|
212
226
|
with resources.as_file(candidate) as resolved:
|
|
213
227
|
if Path(resolved).is_dir():
|
|
214
|
-
shutil.copytree(resolved, dest_path, dirs_exist_ok=True)
|
|
228
|
+
shutil.copytree(resolved, dest_path, dirs_exist_ok=True, ignore=_TEMPLATE_IGNORE)
|
|
215
229
|
return dest_path
|
|
216
230
|
except (ModuleNotFoundError, FileNotFoundError, OSError):
|
|
217
231
|
pass
|
|
@@ -280,6 +294,7 @@ class Builder:
|
|
|
280
294
|
build_dir = self.build_root / platform
|
|
281
295
|
build_dir.mkdir(parents=True, exist_ok=True)
|
|
282
296
|
project_dir = stage_template(_TEMPLATE_NAMES[platform], build_dir)
|
|
297
|
+
plugins = self._discover_plugins()
|
|
283
298
|
|
|
284
299
|
if platform == "android":
|
|
285
300
|
layout = android_config.configure(
|
|
@@ -288,6 +303,7 @@ class Builder:
|
|
|
288
303
|
dev_lib_root=self.dev_lib_root,
|
|
289
304
|
log=self.log,
|
|
290
305
|
)
|
|
306
|
+
native_plugins.stage_android_plugins(project_dir, plugins, log=self.log)
|
|
291
307
|
return PreparedProject(
|
|
292
308
|
platform=platform,
|
|
293
309
|
build_dir=build_dir,
|
|
@@ -297,6 +313,7 @@ class Builder:
|
|
|
297
313
|
)
|
|
298
314
|
|
|
299
315
|
ios_layout = ios_config.configure(project_dir, self.config, log=self.log)
|
|
316
|
+
native_plugins.stage_ios_plugins(project_dir, plugins, log=self.log)
|
|
300
317
|
self._stage_ios_python(project_dir, release=release)
|
|
301
318
|
self._link_ios_runtime(project_dir)
|
|
302
319
|
return PreparedProject(
|
|
@@ -307,6 +324,20 @@ class Builder:
|
|
|
307
324
|
ios=ios_layout,
|
|
308
325
|
)
|
|
309
326
|
|
|
327
|
+
def _discover_plugins(self) -> List[native_plugins.NativePlugin]:
|
|
328
|
+
"""Collect native plugins from entry points and ``[plugins].paths``.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
BuildError: If a project-local plugin directory is invalid.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
return native_plugins.discover_plugins(
|
|
335
|
+
extra_paths=[self.config.resolve_path(p) for p in self.config.plugin_paths],
|
|
336
|
+
log=self.log,
|
|
337
|
+
)
|
|
338
|
+
except native_plugins.PluginError as exc:
|
|
339
|
+
raise BuildError(f"Invalid native plugin: {exc}") from exc
|
|
340
|
+
|
|
310
341
|
def _stage_ios_python(self, project_dir: Path, *, release: bool) -> None:
|
|
311
342
|
"""Stage ``app/`` and ``app_packages/`` at the Xcode project root.
|
|
312
343
|
|
|
@@ -314,8 +345,6 @@ class Builder:
|
|
|
314
345
|
and the "Install Python runtime" build phase converts any
|
|
315
346
|
binary modules inside them into signed frameworks.
|
|
316
347
|
"""
|
|
317
|
-
import shutil
|
|
318
|
-
|
|
319
348
|
app_dir = project_dir / "app"
|
|
320
349
|
packages_dir = project_dir / "app_packages"
|
|
321
350
|
for directory in (app_dir, packages_dir):
|
|
@@ -336,14 +365,9 @@ class Builder:
|
|
|
336
365
|
ignore=android_config.LIB_IGNORE,
|
|
337
366
|
)
|
|
338
367
|
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
[sys.executable, "-m", "pip", "install", "--no-deps", "--upgrade", "rubicon-objc", "-t", str(packages_dir)],
|
|
343
|
-
capture=True,
|
|
344
|
-
)
|
|
345
|
-
if not result.ok:
|
|
346
|
-
raise BuildError(f"pip install rubicon-objc failed:\n{result.stderr}")
|
|
368
|
+
# The framework has no Python-side native dependencies (the
|
|
369
|
+
# bridge into PythonNativeKit is ctypes); only user requirements
|
|
370
|
+
# are installed into the site directory.
|
|
347
371
|
if self.config.requirements:
|
|
348
372
|
result = self.runner.run(
|
|
349
373
|
[sys.executable, "-m", "pip", "install", "-t", str(packages_dir), *self.config.requirements],
|
|
@@ -389,8 +413,6 @@ class Builder:
|
|
|
389
413
|
framework is hundreds of megabytes); staging falls back to a
|
|
390
414
|
copy when symlinks are unavailable.
|
|
391
415
|
"""
|
|
392
|
-
import shutil
|
|
393
|
-
|
|
394
416
|
runtime = self._ios_runtime()
|
|
395
417
|
link = project_dir / "Python.xcframework"
|
|
396
418
|
if link.is_symlink() or link.is_file():
|
pythonnative/project/config.py
CHANGED
|
@@ -34,6 +34,9 @@ splash = "assets/splash.png" # splash/launch image
|
|
|
34
34
|
[requirements]
|
|
35
35
|
packages = ["humanize", "httpx"]
|
|
36
36
|
|
|
37
|
+
[plugins]
|
|
38
|
+
paths = ["native/my_plugin"] # local native plugins (pn_plugin.json + ios/ + android/)
|
|
39
|
+
|
|
37
40
|
[ios]
|
|
38
41
|
deployment_target = "13.0"
|
|
39
42
|
development_team = "ABCDE12345"
|
|
@@ -235,6 +238,9 @@ class AppConfig:
|
|
|
235
238
|
icon: Optional source icon path (``[assets].icon``).
|
|
236
239
|
splash: Optional splash image path (``[assets].splash``).
|
|
237
240
|
requirements: Third-party pip packages (``[requirements].packages``).
|
|
241
|
+
plugin_paths: Project-local native plugin directories
|
|
242
|
+
(``[plugins].paths``); installed packages contribute plugins
|
|
243
|
+
through the ``pythonnative.plugins`` entry point group instead.
|
|
238
244
|
ios: Nested [`IOSConfig`][pythonnative.project.config.IOSConfig].
|
|
239
245
|
android: Nested
|
|
240
246
|
[`AndroidConfig`][pythonnative.project.config.AndroidConfig].
|
|
@@ -254,6 +260,7 @@ class AppConfig:
|
|
|
254
260
|
icon: Optional[str] = None
|
|
255
261
|
splash: Optional[str] = None
|
|
256
262
|
requirements: List[str] = field(default_factory=list)
|
|
263
|
+
plugin_paths: List[str] = field(default_factory=list)
|
|
257
264
|
ios: IOSConfig = field(default_factory=IOSConfig)
|
|
258
265
|
android: AndroidConfig = field(default_factory=AndroidConfig)
|
|
259
266
|
project_root: Path = field(default_factory=Path.cwd)
|
|
@@ -345,8 +352,8 @@ class AppConfig:
|
|
|
345
352
|
|
|
346
353
|
Args:
|
|
347
354
|
data: A parsed TOML mapping (top-level tables: ``app``,
|
|
348
|
-
``permissions``, ``assets``, ``requirements``, ``
|
|
349
|
-
``android``).
|
|
355
|
+
``permissions``, ``assets``, ``requirements``, ``plugins``,
|
|
356
|
+
``ios``, ``android``).
|
|
350
357
|
project_root: Directory the config came from, used to resolve
|
|
351
358
|
relative paths.
|
|
352
359
|
|
|
@@ -382,6 +389,9 @@ class AppConfig:
|
|
|
382
389
|
requirements = _expect_table(data, "requirements", optional=True)
|
|
383
390
|
config.requirements = _opt_str_list(requirements, "packages")
|
|
384
391
|
|
|
392
|
+
plugins = _expect_table(data, "plugins", optional=True)
|
|
393
|
+
config.plugin_paths = _opt_str_list(plugins, "paths")
|
|
394
|
+
|
|
385
395
|
config.validate()
|
|
386
396
|
return config
|
|
387
397
|
|