pythonnative 0.36.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.
Files changed (174) hide show
  1. pythonnative/__init__.py +6 -4
  2. pythonnative/_ios_log.py +1 -1
  3. pythonnative/alerts.py +34 -88
  4. pythonnative/bootstrap.py +90 -0
  5. pythonnative/bridge/__init__.py +306 -0
  6. pythonnative/bridge/android.py +78 -0
  7. pythonnative/bridge/codec.py +164 -0
  8. pythonnative/bridge/fake.py +236 -0
  9. pythonnative/bridge/ios.py +138 -0
  10. pythonnative/components/layout.py +65 -3
  11. pythonnative/gestures.py +13 -10
  12. pythonnative/hooks.py +41 -26
  13. pythonnative/hosts/__init__.py +13 -36
  14. pythonnative/hosts/native.py +348 -0
  15. pythonnative/images.py +6 -5
  16. pythonnative/native_modules/__init__.py +30 -29
  17. pythonnative/native_modules/app_state.py +10 -6
  18. pythonnative/native_modules/battery.py +20 -84
  19. pythonnative/native_modules/biometrics.py +11 -124
  20. pythonnative/native_modules/camera.py +15 -291
  21. pythonnative/native_modules/clipboard.py +14 -109
  22. pythonnative/native_modules/desktop.py +322 -0
  23. pythonnative/native_modules/file_system.py +15 -21
  24. pythonnative/native_modules/haptics.py +19 -129
  25. pythonnative/native_modules/linking.py +24 -137
  26. pythonnative/native_modules/location.py +28 -180
  27. pythonnative/native_modules/net_info.py +25 -135
  28. pythonnative/native_modules/notifications.py +36 -257
  29. pythonnative/native_modules/permissions.py +20 -184
  30. pythonnative/native_modules/registry.py +513 -0
  31. pythonnative/native_modules/secure_store.py +18 -155
  32. pythonnative/native_modules/share.py +8 -113
  33. pythonnative/native_views/__init__.py +67 -86
  34. pythonnative/native_views/bridge_backend.py +264 -0
  35. pythonnative/platform.py +19 -15
  36. pythonnative/platform_metrics.py +3 -3
  37. pythonnative/project/android.py +0 -3
  38. pythonnative/project/builder.py +38 -16
  39. pythonnative/project/config.py +12 -2
  40. pythonnative/project/plugins.py +366 -0
  41. pythonnative/py.typed +0 -0
  42. pythonnative/runtime.py +35 -203
  43. pythonnative/sdk/__init__.py +48 -33
  44. pythonnative/sdk/_components.py +57 -76
  45. pythonnative/storage.py +18 -243
  46. pythonnative/templates/android_template/app/build.gradle +6 -9
  47. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  48. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  49. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  50. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  51. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  100. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  101. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  102. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  104. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  105. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  106. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  110. pythonnative/templates/android_template/settings.gradle +1 -0
  111. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  112. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  155. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  156. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  157. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  158. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  159. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  160. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  161. pythonnative/utils.py +4 -82
  162. pythonnative/virtual_rows.py +3 -3
  163. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  164. pythonnative-0.37.0.dist-info/RECORD +263 -0
  165. pythonnative/hosts/android.py +0 -269
  166. pythonnative/hosts/ios.py +0 -404
  167. pythonnative/native_views/android.py +0 -3702
  168. pythonnative/native_views/ios.py +0 -5253
  169. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  170. pythonnative-0.36.0.dist-info/RECORD +0 -155
  171. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  172. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  173. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  174. {pythonnative-0.36.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,101 @@
1
+ import XCTest
2
+ @testable import PythonNativeKit
3
+
4
+ final class PNGestureTests: XCTestCase {
5
+ override func tearDown() {
6
+ PNViewRegistry.shared.removeAll()
7
+ super.tearDown()
8
+ }
9
+
10
+ func testSpecsBecomeRecognizersWithRelationships() {
11
+ let manager = PNViewManager()
12
+ let view = manager.createView(tag: 1, props: [
13
+ "gestures": [
14
+ ["kind": "tap", "n_taps": 2, "simultaneous": [1], "wait_for": []],
15
+ ["kind": "pan", "min_distance": 10, "simultaneous": [0], "wait_for": []],
16
+ ["kind": "long_press", "min_duration_ms": 800, "simultaneous": [], "wait_for": [0]],
17
+ ["kind": "swipe", "direction": "left", "simultaneous": [], "wait_for": []],
18
+ ["kind": "pinch", "simultaneous": [], "wait_for": []],
19
+ ["kind": "rotation", "simultaneous": [], "wait_for": []],
20
+ ["kind": "fling", "direction": "down", "n_pointers": 2, "simultaneous": [], "wait_for": []],
21
+ ],
22
+ ])
23
+ let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
24
+ XCTAssertEqual(recognizers.count, 7)
25
+ XCTAssertEqual((recognizers[0] as? UITapGestureRecognizer)?.numberOfTapsRequired, 2)
26
+ XCTAssertTrue(recognizers[1] is UIPanGestureRecognizer)
27
+ XCTAssertEqual((recognizers[2] as? UILongPressGestureRecognizer)?.minimumPressDuration ?? 0, 0.8, accuracy: 0.001)
28
+ XCTAssertEqual((recognizers[3] as? UISwipeGestureRecognizer)?.direction, .left)
29
+ XCTAssertTrue(recognizers[4] is UIPinchGestureRecognizer)
30
+ XCTAssertTrue(recognizers[5] is UIRotationGestureRecognizer)
31
+ XCTAssertEqual((recognizers[6] as? UISwipeGestureRecognizer)?.numberOfTouchesRequired, 2)
32
+ XCTAssertEqual((recognizers[6] as? UISwipeGestureRecognizer)?.direction, .down)
33
+
34
+ let coordinator = PNGestureCoordinator.shared
35
+ XCTAssertEqual(coordinator.index(of: recognizers[2]), 2)
36
+ XCTAssertTrue(coordinator.allowsSimultaneous(recognizers[0], recognizers[1]))
37
+ XCTAssertFalse(coordinator.allowsSimultaneous(recognizers[0], recognizers[2]))
38
+ XCTAssertFalse(coordinator.allowsSimultaneous(recognizers[3], recognizers[4]))
39
+ XCTAssertTrue(view.isUserInteractionEnabled)
40
+ XCTAssertTrue(recognizers.allSatisfy { $0.view === view && !$0.cancelsTouchesInView })
41
+ }
42
+
43
+ func testRewiringReplacesRecognizersAndUnknownKindsAreSkipped() {
44
+ let manager = PNViewManager()
45
+ let view = manager.createView(tag: 2, props: ["gestures": [["kind": "tap"]]])
46
+ XCTAssertEqual(view.gestureRecognizers?.count, 1)
47
+ manager.update(view: view, changed: ["gestures": [["kind": "nonsense"], ["kind": "pan"]]])
48
+ let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
49
+ XCTAssertEqual(recognizers.count, 1)
50
+ XCTAssertTrue(recognizers[0] is UIPanGestureRecognizer)
51
+ XCTAssertEqual(PNGestureCoordinator.shared.index(of: recognizers[0]), 1, "index follows the spec position")
52
+ manager.update(view: view, changed: ["gestures": NSNull()])
53
+ XCTAssertEqual(view.gestureRecognizers?.count ?? 0, 0)
54
+ }
55
+
56
+ func testStateAndDirectionNames() {
57
+ XCTAssertEqual(PNGestureCoordinator.stateName(.began), "began")
58
+ XCTAssertEqual(PNGestureCoordinator.stateName(.changed), "changed")
59
+ XCTAssertEqual(PNGestureCoordinator.stateName(.ended), "ended")
60
+ XCTAssertEqual(PNGestureCoordinator.stateName(.cancelled), "cancelled")
61
+ XCTAssertEqual(PNGestureCoordinator.swipeDirection("up"), .up)
62
+ XCTAssertEqual(PNGestureCoordinator.swipeDirection("any"), [.left, .right, .up, .down])
63
+ }
64
+
65
+ func testAnyDirectionSwipeInstallsOneRecognizerPerDirection() {
66
+ let manager = PNViewManager()
67
+ let view = manager.createView(tag: 3, props: [
68
+ "gestures": [
69
+ ["kind": "tap", "simultaneous": [], "wait_for": [1]],
70
+ ["kind": "swipe", "direction": "any", "simultaneous": [], "wait_for": []],
71
+ ],
72
+ ])
73
+ let recognizers = PNViewState.existing(for: view)?.gestureRecognizers ?? []
74
+ XCTAssertEqual(recognizers.count, 5, "one tap plus four single-direction swipes")
75
+ let coordinator = PNGestureCoordinator.shared
76
+ let swipes = recognizers.compactMap { $0 as? UISwipeGestureRecognizer }
77
+ XCTAssertEqual(swipes.map { coordinator.direction(of: $0) }, ["left", "right", "up", "down"])
78
+ XCTAssertEqual(swipes.map { $0.direction }, [.left, .right, .up, .down])
79
+ XCTAssertTrue(swipes.allSatisfy { coordinator.index(of: $0) == 1 }, "every recognizer reports the spec index")
80
+ XCTAssertNil(coordinator.direction(of: recognizers[0]))
81
+ }
82
+
83
+ func testAnimatorTimingParameters() {
84
+ let timing = PNAnimator.timingParameters(kind: "timing", spec: ["duration_ms": 250, "easing": "linear"])
85
+ XCTAssertEqual(timing.duration, 0.25, accuracy: 0.0001)
86
+ let spring = PNAnimator.timingParameters(kind: "spring", spec: ["stiffness": 100, "damping": 10, "mass": 1, "from": 0, "to": 1])
87
+ XCTAssertGreaterThan(spring.duration, 0.15)
88
+ XCTAssertTrue(spring.parameters is UISpringTimingParameters)
89
+ XCTAssertTrue(PNAnimator.curve(for: [0.1, 0.2, 0.3, 0.4]) is UICubicTimingParameters)
90
+ }
91
+
92
+ func testAnimatorSetAppliesImmediately() {
93
+ PNTransaction.apply("[[\"c\", 50, \"View\", {}]]")
94
+ _ = PNAnimator.shared.handle(tag: 50, request: ["op": "set", "prop": "opacity", "value": 0.25])
95
+ XCTAssertEqual(PNViewRegistry.shared.view(for: 50)?.alpha ?? 0, 0.25, accuracy: 0.001)
96
+ _ = PNAnimator.shared.handle(tag: 50, request: ["op": "set", "prop": "translate_x", "value": 12])
97
+ XCTAssertEqual(PNViewRegistry.shared.view(for: 50)?.transform.tx ?? 0, 12, accuracy: 0.001)
98
+ let rejected = PNAnimator.shared.handle(tag: 50, request: ["op": "start", "id": 1, "prop": "opacity", "spec": ["kind": "mystery"]])
99
+ XCTAssertEqual((rejected as? [String: Any])?["ok"] as? Bool, false)
100
+ }
101
+ }
@@ -0,0 +1,124 @@
1
+ import XCTest
2
+ @testable import PythonNativeKit
3
+
4
+ final class PNManagerTests: XCTestCase {
5
+ override func tearDown() {
6
+ PNViewRegistry.shared.removeAll()
7
+ super.tearDown()
8
+ }
9
+
10
+ func testViewManagerAppliesCommonProps() {
11
+ let manager = PNViewManager()
12
+ let view = manager.createView(tag: 1, props: [
13
+ "background_color": "#ff0000",
14
+ "opacity": 0.5,
15
+ "border_width": 2,
16
+ "border_color": "#00ff00",
17
+ "border_radius": 8,
18
+ "test_id": "hero",
19
+ "accessibility_label": "Hero card",
20
+ ])
21
+ XCTAssertEqual(view.alpha, 0.5, accuracy: 0.001)
22
+ XCTAssertEqual(view.layer.borderWidth, 2)
23
+ XCTAssertEqual(view.layer.cornerRadius, 8)
24
+ XCTAssertEqual(view.accessibilityIdentifier, "hero")
25
+ XCTAssertEqual(view.accessibilityLabel, "Hero card")
26
+ XCTAssertEqual(PNColor.hexString(view.backgroundColor ?? .clear).lowercased(), "#ffff0000")
27
+
28
+ manager.update(view: view, changed: ["opacity": 1, "background_color": NSNull(), "display": "none"])
29
+ XCTAssertEqual(view.alpha, 1, accuracy: 0.001)
30
+ XCTAssertTrue(view.isHidden)
31
+ XCTAssertNil(PNViewState.existing(for: view)?.props["background_color"])
32
+ XCTAssertEqual(PNProps.double(PNViewState.existing(for: view)?.props["opacity"]), 1)
33
+ }
34
+
35
+ func testTransformPropBuildsAffineTransform() {
36
+ let manager = PNViewManager()
37
+ let view = manager.createView(tag: 2, props: [
38
+ "transform": [["translate_x": 10], ["translate_y": 5], ["scale": 2]],
39
+ ])
40
+ XCTAssertEqual(view.transform.tx, 10, accuracy: 0.001)
41
+ XCTAssertEqual(view.transform.ty, 5, accuracy: 0.001)
42
+ XCTAssertEqual(view.transform.a, 2, accuracy: 0.001)
43
+ XCTAssertEqual(view.transform.d, 2, accuracy: 0.001)
44
+
45
+ manager.update(view: view, changed: ["transform": [["rotate": "90deg"]]])
46
+ XCTAssertEqual(view.transform.b, 1, accuracy: 0.001)
47
+ XCTAssertEqual(view.transform.a, 0, accuracy: 0.001)
48
+
49
+ manager.update(view: view, changed: ["transform": NSNull()])
50
+ XCTAssertTrue(view.transform.isIdentity)
51
+ }
52
+
53
+ func testFrameSurvivesTransform() {
54
+ let manager = PNViewManager()
55
+ let view = manager.createView(tag: 3, props: ["transform": [["scale": 2]]])
56
+ manager.setFrame(view: view, x: 10, y: 20, w: 100, h: 50)
57
+ XCTAssertEqual(view.bounds.size, CGSize(width: 100, height: 50))
58
+ XCTAssertEqual(view.center, CGPoint(x: 60, y: 45))
59
+ }
60
+
61
+ func testTextMeasureAndTransform() {
62
+ let manager = PNTextManager()
63
+ let view = manager.createView(tag: 4, props: ["text": "hello world", "font_size": 20, "text_transform": "uppercase"])
64
+ XCTAssertEqual((view as? UILabel)?.text, "HELLO WORLD")
65
+ let unconstrained = manager.measure(view: view, maxW: .infinity, maxH: .infinity)
66
+ XCTAssertGreaterThan(unconstrained.width, 50)
67
+ XCTAssertGreaterThan(unconstrained.height, 15)
68
+ let narrow = manager.measure(view: view, maxW: 60, maxH: 1e6)
69
+ XCTAssertLessThanOrEqual(narrow.width, 60)
70
+ XCTAssertGreaterThan(narrow.height, unconstrained.height, "wrapped text grows in height")
71
+ XCTAssertEqual(PNTextManager.transform("hello big world", mode: "capitalize"), "Hello Big World")
72
+ }
73
+
74
+ func testTextSpansProduceAttributedString() {
75
+ let manager = PNTextManager()
76
+ let view = manager.createView(tag: 5, props: [
77
+ "spans": [["text": "Hi "], ["text": "there", "bold": true, "color": "#ff0000"]],
78
+ ])
79
+ let label = view as? UILabel
80
+ XCTAssertEqual(label?.attributedText?.string, "Hi there")
81
+ var range = NSRange()
82
+ let color = label?.attributedText?.attribute(.foregroundColor, at: 3, effectiveRange: &range) as? UIColor
83
+ XCTAssertNotNil(color)
84
+ XCTAssertEqual(range.location, 3)
85
+ XCTAssertEqual(range.length, 5)
86
+ }
87
+
88
+ func testVirtualListBindsRowsThroughRegistry() {
89
+ PNTransaction.apply("[[\"c\", 40, \"View\", {\"background_color\": \"#123456\"}]]")
90
+ let manager = PNVirtualListManager()
91
+ guard let table = manager.createView(tag: 41, props: ["count": 3, "row_height": 50]) as? UITableView else {
92
+ return XCTFail("expected a table")
93
+ }
94
+ XCTAssertEqual(table.dataSource?.tableView(table, numberOfRowsInSection: 0), 3)
95
+ XCTAssertEqual(table.delegate?.tableView?(table, heightForRowAt: IndexPath(row: 1, section: 0)), 50)
96
+ manager.update(view: table, changed: ["row_heights": [10, 20, 30]])
97
+ XCTAssertEqual(table.delegate?.tableView?(table, heightForRowAt: IndexPath(row: 2, section: 0)), 30)
98
+ }
99
+
100
+ func testScrollPayloadShape() {
101
+ let scroll = UIScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 200))
102
+ scroll.contentSize = CGSize(width: 100, height: 800)
103
+ scroll.contentOffset = CGPoint(x: 0, y: 40)
104
+ let payload = PNScrollPayload.make(scroll)
105
+ XCTAssertEqual(payload["y"] as? Double, 40)
106
+ XCTAssertEqual(payload["extent"] as? Double, 200)
107
+ XCTAssertEqual(payload["range"] as? Double, 800)
108
+ }
109
+
110
+ func testRegistryKnowsEveryBuiltinType() {
111
+ let names = PNRegistry.shared.componentNames
112
+ for type in ["View", "Column", "Row", "Text", "Button", "TextInput", "Image", "Switch", "ProgressBar",
113
+ "ActivityIndicator", "WebView", "Spacer", "ScrollView", "SafeAreaView", "Modal", "Portal",
114
+ "Slider", "TabBar", "Pressable", "StatusBar", "KeyboardAvoidingView", "Picker", "Checkbox",
115
+ "SegmentedControl", "DatePicker", "VirtualList"] {
116
+ XCTAssertTrue(names.contains(type), "missing manager for \(type)")
117
+ }
118
+ for module in ["Host", "Device", "Alert", "Storage", "SecureStore", "Clipboard", "Share", "Linking",
119
+ "Haptics", "Battery", "NetInfo", "AppState", "Permissions", "Notifications", "Camera",
120
+ "Location", "Biometrics"] {
121
+ XCTAssertTrue(PNRegistry.shared.moduleNames.contains(module), "missing module \(module)")
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,98 @@
1
+ import XCTest
2
+ @testable import PythonNativeKit
3
+
4
+ /// A module that settles inline or later depending on the method.
5
+ final class PNTestEchoModule: PNNativeModule {
6
+ static let name = "TestEcho"
7
+ static var pending: PNPromise?
8
+
9
+ init() {}
10
+
11
+ func call(_ method: String, args: [String: Any], promise: PNPromise) {
12
+ switch method {
13
+ case "echo": promise.resolve(args["value"])
14
+ case "fail": promise.reject("nope", code: "test_code")
15
+ case "later": PNTestEchoModule.pending = promise
16
+ default: promise.reject("unknown method \(method)")
17
+ }
18
+ }
19
+ }
20
+
21
+ final class PNModuleTests: XCTestCase {
22
+ override func setUp() {
23
+ super.setUp()
24
+ PNRegistry.shared.registerModule(PNTestEchoModule.self)
25
+ }
26
+
27
+ func testSynchronousResolveProducesOkEnvelope() {
28
+ let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "echo", envelope: ["call_id": 7, "args": ["value": 42]])
29
+ XCTAssertEqual(result["ok"] as? Bool, true)
30
+ XCTAssertEqual(result["value"] as? Int, 42)
31
+ XCTAssertNil(result["pending"])
32
+ }
33
+
34
+ func testRejectProducesErrorEnvelope() {
35
+ let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "fail", envelope: ["call_id": 8, "args": [:]])
36
+ XCTAssertEqual(result["ok"] as? Bool, false)
37
+ XCTAssertEqual(result["error"] as? String, "nope")
38
+ XCTAssertEqual(result["code"] as? String, "test_code")
39
+ }
40
+
41
+ func testUnknownModuleAndMethod() {
42
+ let missing = PNModuleDispatcher.shared.call(module: "Nope", method: "x", envelope: [:])
43
+ XCTAssertEqual(missing["ok"] as? Bool, false)
44
+ XCTAssertEqual(missing["code"] as? String, "unknown_module")
45
+ let method = PNModuleDispatcher.shared.call(module: "TestEcho", method: "zzz", envelope: [:])
46
+ XCTAssertEqual(method["ok"] as? Bool, false)
47
+ }
48
+
49
+ func testAsynchronousSettlementReturnsPending() {
50
+ let result = PNModuleDispatcher.shared.call(module: "TestEcho", method: "later", envelope: ["call_id": 9, "args": [:]])
51
+ XCTAssertEqual(result["pending"] as? Bool, true)
52
+ guard let promise = PNTestEchoModule.pending else { return XCTFail("promise not captured") }
53
+ XCTAssertFalse(promise.isSettled)
54
+ XCTAssertEqual(promise.callId, 9)
55
+ promise.resolve("done")
56
+ XCTAssertTrue(promise.isSettled)
57
+ promise.resolve("again")
58
+ XCTAssertEqual(promise.result?["value"] as? String, "done", "second settlement is ignored")
59
+ }
60
+
61
+ func testEnvelopeJSONRoundTrip() {
62
+ let ok = PNJSON.encode(["ok": true, "value": ["a": 1]])
63
+ let decoded = PNJSON.decodeObject(ok)
64
+ XCTAssertEqual(decoded["ok"] as? Bool, true)
65
+ XCTAssertEqual((decoded["value"] as? [String: Any])?["a"] as? Int, 1)
66
+ XCTAssertEqual(PNJSON.decodeObject(PNJSON.encode(["pending": true]))["pending"] as? Bool, true)
67
+ XCTAssertEqual(PNJSON.encode(Double.infinity), "null", "non-finite numbers never leave Swift as JSON numbers")
68
+ XCTAssertEqual(PNJSON.resolveInfinity("inf") as? Double, Double.infinity)
69
+ XCTAssertEqual(PNJSON.encode(nil), "null")
70
+ }
71
+
72
+ func testDeviceInfoShape() {
73
+ let info = DeviceModule.info()
74
+ XCTAssertEqual(info["os"] as? String, "ios")
75
+ XCTAssertNotNil(info["os_version"])
76
+ XCTAssertNotNil(info["app_dir"])
77
+ XCTAssertNotNil(info["cache_dir"])
78
+ XCTAssertNotNil(info["scale"])
79
+ }
80
+
81
+ func testStorageModuleRoundTrip() {
82
+ let storage = StorageModule()
83
+ _ = PNModuleDispatcher.shared.call(module: "Storage", method: "clear", envelope: [:])
84
+ let set = PNPromise(callId: 1, module: "Storage", method: "set")
85
+ storage.call("set", args: ["key": "k", "value": "v"], promise: set)
86
+ let get = PNPromise(callId: 2, module: "Storage", method: "get")
87
+ storage.call("get", args: ["key": "k"], promise: get)
88
+ XCTAssertEqual(get.result?["value"] as? String, "v")
89
+ let keys = PNPromise(callId: 3, module: "Storage", method: "all_keys")
90
+ storage.call("all_keys", args: [:], promise: keys)
91
+ XCTAssertEqual(keys.result?["value"] as? [String], ["k"])
92
+ let del = PNPromise(callId: 4, module: "Storage", method: "delete")
93
+ storage.call("delete", args: ["key": "k"], promise: del)
94
+ let missing = PNPromise(callId: 5, module: "Storage", method: "get")
95
+ storage.call("get", args: ["key": "k"], promise: missing)
96
+ XCTAssertTrue(missing.result?["value"] is NSNull)
97
+ }
98
+ }
@@ -0,0 +1,91 @@
1
+ import XCTest
2
+ @testable import PythonNativeKit
3
+
4
+ final class PNTransactionTests: XCTestCase {
5
+ override func tearDown() {
6
+ PNViewRegistry.shared.removeAll()
7
+ super.tearDown()
8
+ }
9
+
10
+ func testDecodesEveryOpcode() throws {
11
+ let json = """
12
+ [["c", 1, "View", {"background_color": "#ff0000"}],
13
+ ["u", 1, {"opacity": 0.5}],
14
+ ["c", 2, "Text", {"text": "hi"}],
15
+ ["i", 1, 2, 0],
16
+ ["f", 2, 1.5, 2.5, 100, 20],
17
+ ["d", 2]]
18
+ """
19
+ let ops = try PNTransaction.decode(json)
20
+ XCTAssertEqual(ops.count, 6)
21
+ XCTAssertEqual(ops[0], .create(tag: 1, type: "View", props: ["background_color": "#ff0000"]))
22
+ XCTAssertEqual(ops[1], .update(tag: 1, changed: ["opacity": 0.5]))
23
+ XCTAssertEqual(ops[3], .insert(parent: 1, child: 2, index: 0))
24
+ XCTAssertEqual(ops[4], .frame(tag: 2, x: 1.5, y: 2.5, w: 100, h: 20))
25
+ XCTAssertEqual(ops[5], .destroy(tag: 2))
26
+ }
27
+
28
+ func testInfinityStringsBecomeInfinity() throws {
29
+ let ops = try PNTransaction.decode("[[\"c\", 5, \"View\", {\"max_width\": \"inf\", \"nested\": {\"h\": \"inf\"}}]]")
30
+ guard case let .create(_, _, props) = ops[0] else { return XCTFail("expected create") }
31
+ XCTAssertEqual(props["max_width"] as? Double, Double.infinity)
32
+ XCTAssertEqual((props["nested"] as? [String: Any])?["h"] as? Double, Double.infinity)
33
+ }
34
+
35
+ func testMalformedOpsAreSkippedNotFatal() throws {
36
+ let ops = try PNTransaction.decode("[[\"c\", 1, \"View\", {}], [\"zz\", 9], [\"u\"], 42, [\"d\", 1]]")
37
+ XCTAssertEqual(ops.count, 2)
38
+ XCTAssertEqual(ops[0], .create(tag: 1, type: "View", props: [:]))
39
+ XCTAssertEqual(ops[1], .destroy(tag: 1))
40
+ }
41
+
42
+ func testNonArrayDocumentThrows() {
43
+ XCTAssertThrowsError(try PNTransaction.decode("{\"not\": \"an array\"}"))
44
+ }
45
+
46
+ func testApplyBuildsHierarchyAndIsolatesFailures() {
47
+ PNTransaction.apply("""
48
+ [["c", 10, "View", {"background_color": "#00ff00"}],
49
+ ["c", 11, "Text", {"text": "hello"}],
50
+ ["u", 999, {"opacity": 0.1}],
51
+ ["i", 10, 11, 0],
52
+ ["i", 10, 12345, 0],
53
+ ["f", 11, 4, 8, 120, 30],
54
+ ["c", 13, "NoSuchType", {}]]
55
+ """)
56
+ let parent = PNViewRegistry.shared.view(for: 10)
57
+ let child = PNViewRegistry.shared.view(for: 11)
58
+ XCTAssertNotNil(parent)
59
+ XCTAssertNotNil(child)
60
+ XCTAssertTrue(child?.superview === parent)
61
+ XCTAssertEqual(child?.frame, CGRect(x: 4, y: 8, width: 120, height: 30))
62
+ XCTAssertNotNil(PNViewRegistry.shared.view(for: 13), "unknown types get a placeholder view")
63
+ XCTAssertEqual(PNViewRegistry.shared.resolve(13)?.typeName, "NoSuchType")
64
+
65
+ PNTransaction.apply("[[\"d\", 11]]")
66
+ XCTAssertNil(PNViewRegistry.shared.view(for: 11))
67
+ XCTAssertEqual(parent?.subviews.count, 0)
68
+ }
69
+
70
+ func testInsertIsMoveAwareAndClamps() {
71
+ PNTransaction.apply("""
72
+ [["c", 20, "View", {}], ["c", 21, "View", {}], ["c", 22, "View", {}], ["c", 23, "View", {}],
73
+ ["i", 20, 21, 0], ["i", 20, 22, 1], ["i", 20, 23, 99]]
74
+ """)
75
+ let parent = PNViewRegistry.shared.view(for: 20)
76
+ XCTAssertEqual(parent?.subviews.map { PNViewState.existing(for: $0)?.tag }, [21, 22, 23])
77
+ PNTransaction.apply("[[\"i\", 20, 23, 0]]")
78
+ XCTAssertEqual(parent?.subviews.map { PNViewState.existing(for: $0)?.tag }, [23, 21, 22])
79
+ XCTAssertEqual(parent?.subviews.count, 3)
80
+ }
81
+
82
+ func testFrameClampsNonFiniteValues() {
83
+ PNTransaction.apply("[[\"c\", 30, \"View\", {}]]")
84
+ guard let record = PNViewRegistry.shared.resolve(30) else { return XCTFail("missing view") }
85
+ record.manager.setFrame(view: record.view, x: .nan, y: 5, w: .infinity, h: -3)
86
+ XCTAssertEqual(record.view.frame.origin.x, 0)
87
+ XCTAssertEqual(record.view.frame.origin.y, 5)
88
+ XCTAssertEqual(record.view.frame.size.height, 0)
89
+ XCTAssertTrue(record.view.frame.size.width.isFinite)
90
+ }
91
+ }
@@ -2,12 +2,13 @@
2
2
  // AppDelegate.swift
3
3
  // ios_template
4
4
  //
5
- // Application-level callbacks. Remote-notification registration
6
- // results are forwarded to pythonnative.native_modules.notifications,
7
- // which resolves the awaitable returned by
8
- // Notifications.get_device_token().
5
+ // Application-level callbacks. Battery monitoring and APNs
6
+ // registration results are forwarded to the Swift native modules in
7
+ // PythonNativeKit (BatteryModule, NotificationsModule), which relay
8
+ // them to Python over the bridge.
9
9
  //
10
10
 
11
+ import PythonNativeKit
11
12
  import UIKit
12
13
 
13
14
  @main
@@ -16,67 +17,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
16
17
  _ application: UIApplication,
17
18
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
18
19
  ) -> Bool {
19
- registerBatteryObservers()
20
+ BatteryModule.startMonitoring()
20
21
  return true
21
22
  }
22
23
 
23
- // MARK: - Battery forwarding
24
-
25
- // UIDevice only posts battery notifications while monitoring is on;
26
- // Battery.add_listener subscribers receive {"level", "state"}.
27
- private func registerBatteryObservers() {
28
- UIDevice.current.isBatteryMonitoringEnabled = true
29
- let center = NotificationCenter.default
30
- for name in [UIDevice.batteryLevelDidChangeNotification, UIDevice.batteryStateDidChangeNotification] {
31
- center.addObserver(forName: name, object: nil, queue: .main) { _ in
32
- self.dispatchBattery()
33
- }
34
- }
35
- }
36
-
37
- private func dispatchBattery() {
38
- guard PythonRuntime.shared.started else { return }
39
- let device = UIDevice.current
40
- let state: String
41
- switch device.batteryState {
42
- case .unplugged: state = "unplugged"
43
- case .charging: state = "charging"
44
- case .full: state = "full"
45
- default: state = "unknown"
46
- }
47
- PythonRuntime.shared.notify(
48
- module: "pythonnative.native_modules.battery",
49
- function: "dispatch_battery",
50
- Double(device.batteryLevel),
51
- state
52
- )
53
- }
54
-
55
24
  // MARK: - Remote notifications (APNs)
56
25
 
57
26
  func application(
58
27
  _ application: UIApplication,
59
28
  didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
60
29
  ) {
61
- let token = deviceToken.map { String(format: "%02x", $0) }.joined()
62
- guard PythonRuntime.shared.started else { return }
63
- PythonRuntime.shared.notify(
64
- module: "pythonnative.native_modules.notifications",
65
- function: "dispatch_device_token",
66
- token
67
- )
30
+ NotificationsModule.deliverToken(deviceToken)
68
31
  }
69
32
 
70
33
  func application(
71
34
  _ application: UIApplication,
72
35
  didFailToRegisterForRemoteNotificationsWithError error: Error
73
36
  ) {
74
- guard PythonRuntime.shared.started else { return }
75
- PythonRuntime.shared.notify(
76
- module: "pythonnative.native_modules.notifications",
77
- function: "dispatch_device_token_error",
78
- String(describing: error)
79
- )
37
+ NotificationsModule.deliverError(error)
80
38
  }
81
39
 
82
40
  // MARK: - UISceneSession lifecycle
@@ -2,11 +2,11 @@
2
2
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
3
  <plist version="1.0">
4
4
  <dict>
5
- <!-- PythonNative's StatusBar element drives the bar through the legacy
6
- UIApplication setters, which UIKit only honors when view-controller-based
7
- status bar appearance is turned off. -->
5
+ <!-- PythonNative's StatusBar element is applied through PNViewController's
6
+ preferredStatusBarStyle / prefersStatusBarHidden overrides, so the
7
+ modern view-controller-based appearance must stay enabled. -->
8
8
  <key>UIViewControllerBasedStatusBarAppearance</key>
9
- <false/>
9
+ <true/>
10
10
  <key>UIApplicationSceneManifest</key>
11
11
  <dict>
12
12
  <key>UIApplicationSupportsMultipleScenes</key>
@@ -6,8 +6,15 @@
6
6
  // Python.xcframework is linked at build time, so a missing runtime is
7
7
  // a build error, never a silent runtime fallback.
8
8
  //
9
+ // After the interpreter starts, `pythonnative.bootstrap.start()` binds
10
+ // the PythonNativeKit C exports, registers the Python callback, and
11
+ // verifies the protocol version. Everything else (screens, modules,
12
+ // deep links) flows over that bridge; nothing else here calls Python
13
+ // functions by name.
14
+ //
9
15
 
10
16
  import Foundation
17
+ import PythonNativeKit
11
18
 
12
19
  enum PythonRuntimeError: Error, CustomStringConvertible {
13
20
  case startup(String)
@@ -62,28 +69,54 @@ final class PythonRuntime {
62
69
 
63
70
  private(set) var started = false
64
71
  private(set) var startupError: String? = nil
65
- private var pendingURLs: [String] = []
66
72
 
67
73
  private init() {}
68
74
 
69
75
  // MARK: - Startup
70
76
 
71
- /// Initialize the embedded interpreter once. Throws with a full
72
- /// description when the bundle is not packaged correctly; there is
77
+ /// Initialize the embedded interpreter once and complete the bridge
78
+ /// handshake. Throws with a full description when the bundle is not
79
+ /// packaged correctly or the protocol versions disagree; there is
73
80
  /// deliberately no fallback mode.
74
81
  func ensureStarted() throws {
75
- if started { return }
76
82
  if let error = startupError { throw PythonRuntimeError.startup(error) }
83
+ if started { return }
77
84
  do {
78
85
  try start()
86
+ // The interpreter is live from here on; a failed handshake is
87
+ // recorded as a startup error but Python is never re-initialized.
79
88
  started = true
80
- flushPendingURLs()
89
+ try connectBridge()
81
90
  } catch let error as PythonRuntimeError {
82
91
  startupError = error.description
83
92
  throw error
84
93
  }
85
94
  }
86
95
 
96
+ /// Run `pythonnative.bootstrap.start(dev, strict=True)`: it binds the
97
+ /// `pn_bridge_*` symbols, registers the native -> Python callback,
98
+ /// verifies the protocol version against `pn_bridge_protocol_version()`,
99
+ /// routes `print()` to the console, and warms the asyncio runtime.
100
+ private func connectBridge() throws {
101
+ #if DEBUG
102
+ let dev = true
103
+ #else
104
+ let dev = false
105
+ #endif
106
+ do {
107
+ try callList(module: "pythonnative.bootstrap", function: "start", [dev, true])
108
+ } catch let error as PythonRuntimeError {
109
+ throw PythonRuntimeError.startup(
110
+ "Bridge bootstrap failed (native protocol v\(pn_bridge_protocol_version())):\n\(error.description)"
111
+ )
112
+ }
113
+ guard PNBridge.shared.hasCallback else {
114
+ throw PythonRuntimeError.startup(
115
+ "pythonnative.bridge.handshake() returned without registering the native callback."
116
+ )
117
+ }
118
+ }
119
+
87
120
  private func start() throws {
88
121
  guard let resourcePath = Bundle.main.resourcePath else {
89
122
  throw PythonRuntimeError.startup("The app bundle has no resource path; the build is corrupt.")
@@ -250,16 +283,6 @@ final class PythonRuntime {
250
283
  return try Self.invoke(fnObj, args: args, context: "\(module).\(function)")
251
284
  }
252
285
 
253
- /// Like `call`, but logs failures instead of throwing. For host
254
- /// notifications where the app must keep running.
255
- func notify(module: String, function: String, _ args: Any...) {
256
- do {
257
- _ = try callList(module: module, function: function, args)
258
- } catch {
259
- NSLog("[PN] \(module).\(function) failed: \(error)")
260
- }
261
- }
262
-
263
286
  /// Shared invocation helper. The caller must hold the GIL.
264
287
  fileprivate static func invoke(
265
288
  _ callable: UnsafeMutablePointer<PyObject>,
@@ -300,26 +323,6 @@ final class PythonRuntime {
300
323
  }
301
324
  }
302
325
 
303
- // MARK: - Deep links
304
-
305
- /// Deliver a deep-link URL to the Python side, buffering it until
306
- /// the interpreter is running.
307
- func deliverURL(_ url: String) {
308
- guard started else {
309
- pendingURLs.append(url)
310
- return
311
- }
312
- notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
313
- }
314
-
315
- private func flushPendingURLs() {
316
- let urls = pendingURLs
317
- pendingURLs = []
318
- for url in urls {
319
- notify(module: "pythonnative.native_modules.linking", function: "dispatch_url", url)
320
- }
321
- }
322
-
323
326
  // MARK: - Error formatting
324
327
 
325
328
  /// Format and clear the pending Python exception (caller holds the GIL).
@@ -384,14 +387,3 @@ final class PythonRuntime {
384
387
  return "unknown error"
385
388
  }
386
389
  }
387
-
388
- /// Exported for the Python side: `pythonnative.runtime` wakes the guest
389
- /// asyncio loop by resolving this symbol via `ctypes.CDLL(None)` and
390
- /// calling it whenever loop work is scheduled.
391
- @_cdecl("pn_schedule_render_drain")
392
- public func pn_schedule_render_drain() {
393
- DispatchQueue.main.async {
394
- guard PythonRuntime.shared.started else { return }
395
- PythonRuntime.shared.notify(module: "pythonnative.hosts", function: "drain_ios_scheduled_renders")
396
- }
397
- }