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.
Files changed (176) 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/cli/pn.py +24 -6
  11. pythonnative/components/layout.py +65 -3
  12. pythonnative/gestures.py +13 -10
  13. pythonnative/hooks.py +41 -26
  14. pythonnative/hosts/__init__.py +13 -36
  15. pythonnative/hosts/native.py +348 -0
  16. pythonnative/images.py +6 -5
  17. pythonnative/mutations.py +21 -0
  18. pythonnative/native_modules/__init__.py +30 -29
  19. pythonnative/native_modules/app_state.py +10 -6
  20. pythonnative/native_modules/battery.py +20 -84
  21. pythonnative/native_modules/biometrics.py +11 -124
  22. pythonnative/native_modules/camera.py +15 -291
  23. pythonnative/native_modules/clipboard.py +14 -109
  24. pythonnative/native_modules/desktop.py +322 -0
  25. pythonnative/native_modules/file_system.py +15 -21
  26. pythonnative/native_modules/haptics.py +19 -129
  27. pythonnative/native_modules/linking.py +24 -137
  28. pythonnative/native_modules/location.py +28 -180
  29. pythonnative/native_modules/net_info.py +25 -135
  30. pythonnative/native_modules/notifications.py +36 -257
  31. pythonnative/native_modules/permissions.py +20 -184
  32. pythonnative/native_modules/registry.py +513 -0
  33. pythonnative/native_modules/secure_store.py +18 -155
  34. pythonnative/native_modules/share.py +8 -113
  35. pythonnative/native_views/__init__.py +67 -86
  36. pythonnative/native_views/bridge_backend.py +264 -0
  37. pythonnative/platform.py +19 -15
  38. pythonnative/platform_metrics.py +3 -3
  39. pythonnative/project/android.py +0 -3
  40. pythonnative/project/builder.py +38 -16
  41. pythonnative/project/config.py +12 -2
  42. pythonnative/project/plugins.py +366 -0
  43. pythonnative/py.typed +0 -0
  44. pythonnative/runtime.py +35 -203
  45. pythonnative/sdk/__init__.py +48 -33
  46. pythonnative/sdk/_components.py +57 -76
  47. pythonnative/storage.py +18 -243
  48. pythonnative/templates/android_template/app/build.gradle +6 -9
  49. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
  50. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
  51. pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
  52. pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
  53. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
  54. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  55. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  56. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
  57. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  58. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
  59. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  60. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  61. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  62. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
  63. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  64. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
  65. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
  66. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  67. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  68. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  69. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  70. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  71. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
  72. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  73. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  74. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
  75. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  76. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  77. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  78. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  79. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  80. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  81. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
  82. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  83. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  84. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
  85. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  86. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  87. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  88. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  89. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  90. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  91. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
  92. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  93. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
  94. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  95. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
  96. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  97. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  98. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  99. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
  100. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
  101. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  102. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
  103. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
  104. pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  105. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
  106. pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
  107. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  108. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  109. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  110. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  111. pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
  112. pythonnative/templates/android_template/settings.gradle +1 -0
  113. pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
  114. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
  115. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
  116. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
  117. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
  118. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  119. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
  120. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  121. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  122. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  123. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  124. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
  125. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  126. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  127. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  128. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  129. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
  130. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  131. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  132. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  133. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  134. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  135. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
  136. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  137. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  138. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  139. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
  140. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  141. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  142. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
  143. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
  144. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  145. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  146. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  147. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
  148. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  149. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  150. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  151. pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  152. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  153. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  154. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
  155. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
  156. pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  157. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
  158. pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
  159. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
  160. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
  161. pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
  162. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
  163. pythonnative/utils.py +4 -82
  164. pythonnative/virtual_rows.py +3 -3
  165. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
  166. pythonnative-0.37.0.dist-info/RECORD +263 -0
  167. pythonnative/hosts/android.py +0 -269
  168. pythonnative/hosts/ios.py +0 -404
  169. pythonnative/native_views/android.py +0 -3702
  170. pythonnative/native_views/ios.py +0 -5253
  171. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
  172. pythonnative-0.35.0.dist-info/RECORD +0 -155
  173. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
  174. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
  175. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
  176. {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,366 @@
1
+ """Native plugin staging for ``pn build``.
2
+
3
+ A PyPI package can ship Swift and Kotlin next to its Python facade. It
4
+ declares an entry point in the ``pythonnative.plugins`` group whose
5
+ value is an importable package directory containing:
6
+
7
+ ```text
8
+ my_blur/native/
9
+ pn_plugin.json
10
+ ios/BlurViewManager.swift
11
+ android/com/example/myblur/BlurViewManager.kt
12
+ ```
13
+
14
+ ``pn_plugin.json`` names the registration entry on each platform:
15
+
16
+ ```json
17
+ {"ios": {"entry": "MyBlurPlugin"}, "android": {"entry": "com.example.myblur.MyBlurPlugin"}}
18
+ ```
19
+
20
+ At build time this module copies each plugin's sources into the staged
21
+ native project (``PythonNativeKit/Sources/PythonNativeKit/Plugins/<name>``
22
+ on iOS, ``pythonnative/src/main/java`` on Android) and regenerates the
23
+ registration file that calls every entry's ``register``. SwiftPM and
24
+ Gradle compile whatever lands under those roots, so no project-file
25
+ surgery is needed.
26
+
27
+ Projects can also bundle plugins that aren't installed as distributions
28
+ by listing directories under ``[plugins].paths`` in ``pythonnative.toml``;
29
+ each path must contain the same ``pn_plugin.json`` layout.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import re
36
+ import shutil
37
+ from dataclasses import dataclass, field
38
+ from importlib import import_module
39
+ from pathlib import Path
40
+ from typing import Callable, Dict, Iterable, List, Optional, Sequence
41
+
42
+ __all__ = [
43
+ "ENTRY_POINT_GROUP",
44
+ "MANIFEST_NAME",
45
+ "NativePlugin",
46
+ "PluginError",
47
+ "discover_plugins",
48
+ "load_plugin",
49
+ "stage_android_plugins",
50
+ "stage_ios_plugins",
51
+ ]
52
+
53
+ ENTRY_POINT_GROUP = "pythonnative.plugins"
54
+ """Entry-point group whose values name plugin directories (as packages)."""
55
+
56
+ MANIFEST_NAME = "pn_plugin.json"
57
+ """File name of the per-plugin manifest."""
58
+
59
+ Logger = Callable[[str], None]
60
+
61
+ _IOS_PLUGIN_DIR = ("PythonNativeKit", "Sources", "PythonNativeKit", "Plugins")
62
+ _IOS_REGISTRATION = (*_IOS_PLUGIN_DIR, "PNPluginRegistration.swift")
63
+ _ANDROID_SOURCE_ROOT = ("pythonnative", "src", "main", "java")
64
+ _ANDROID_REGISTRATION = (*_ANDROID_SOURCE_ROOT, "com", "pythonnative", "runtime", "plugins", "GeneratedPlugins.kt")
65
+
66
+ _SWIFT_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
67
+ _KOTLIN_FQN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$")
68
+
69
+
70
+ class PluginError(RuntimeError):
71
+ """A plugin manifest or source layout is invalid."""
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class NativePlugin:
76
+ """A discovered native plugin.
77
+
78
+ Attributes:
79
+ name: Stable identifier (the entry-point name or the directory
80
+ name for path plugins). Used for the iOS subfolder.
81
+ root: Directory containing ``pn_plugin.json``.
82
+ ios_entry: Swift type conforming to ``PNPlugin``, or ``None``
83
+ when the plugin has no iOS side.
84
+ android_entry: Fully qualified Kotlin object implementing
85
+ ``PNPlugin``, or ``None`` when the plugin has no Android side.
86
+ ios_sources: Swift files under ``ios/``.
87
+ android_sources: Kotlin files under ``android/`` (relative
88
+ paths are preserved so packages land in the right folders).
89
+ """
90
+
91
+ name: str
92
+ root: Path
93
+ ios_entry: Optional[str] = None
94
+ android_entry: Optional[str] = None
95
+ ios_sources: Sequence[Path] = field(default_factory=tuple)
96
+ android_sources: Sequence[Path] = field(default_factory=tuple)
97
+
98
+ @property
99
+ def has_ios(self) -> bool:
100
+ """Whether this plugin contributes Swift code."""
101
+ return self.ios_entry is not None
102
+
103
+ @property
104
+ def has_android(self) -> bool:
105
+ """Whether this plugin contributes Kotlin code."""
106
+ return self.android_entry is not None
107
+
108
+
109
+ # ======================================================================
110
+ # Discovery
111
+ # ======================================================================
112
+
113
+
114
+ def load_plugin(root: Path, *, name: Optional[str] = None) -> NativePlugin:
115
+ """Read ``pn_plugin.json`` under ``root`` and collect its sources.
116
+
117
+ Args:
118
+ root: Directory containing the manifest.
119
+ name: Identifier override (defaults to the directory name).
120
+
121
+ Returns:
122
+ The parsed plugin.
123
+
124
+ Raises:
125
+ PluginError: If the manifest is missing or malformed, or an
126
+ entry is declared without any matching sources.
127
+ """
128
+ root = Path(root).resolve()
129
+ manifest_path = root / MANIFEST_NAME
130
+ if not manifest_path.is_file():
131
+ raise PluginError(f"{root} has no {MANIFEST_NAME}.")
132
+ try:
133
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
134
+ except (OSError, ValueError) as exc:
135
+ raise PluginError(f"Could not parse {manifest_path}: {exc}") from exc
136
+ if not isinstance(manifest, dict):
137
+ raise PluginError(f"{manifest_path} must contain a JSON object.")
138
+
139
+ plugin_name = name or manifest.get("name") or root.name
140
+ if not _SWIFT_IDENT.match(str(plugin_name).replace("-", "_")):
141
+ raise PluginError(f"Plugin name {plugin_name!r} must be an identifier.")
142
+ plugin_name = str(plugin_name).replace("-", "_")
143
+
144
+ ios_entry = _entry(manifest, "ios", manifest_path)
145
+ android_entry = _entry(manifest, "android", manifest_path)
146
+ if ios_entry is None and android_entry is None:
147
+ raise PluginError(f"{manifest_path} declares neither an 'ios' nor an 'android' entry.")
148
+
149
+ ios_sources: List[Path] = []
150
+ android_sources: List[Path] = []
151
+ if ios_entry is not None:
152
+ if not _SWIFT_IDENT.match(ios_entry):
153
+ raise PluginError(f"{manifest_path}: ios.entry {ios_entry!r} is not a Swift type name.")
154
+ ios_sources = sorted(p.relative_to(root) for p in (root / "ios").rglob("*.swift") if p.is_file())
155
+ if not ios_sources:
156
+ raise PluginError(f"{root} declares an iOS entry but ios/ contains no .swift files.")
157
+ if android_entry is not None:
158
+ if not _KOTLIN_FQN.match(android_entry):
159
+ raise PluginError(f"{manifest_path}: android.entry {android_entry!r} must be a fully qualified name.")
160
+ android_sources = sorted(
161
+ p.relative_to(root) for p in (root / "android").rglob("*") if p.is_file() and p.suffix in (".kt", ".java")
162
+ )
163
+ if not android_sources:
164
+ raise PluginError(f"{root} declares an Android entry but android/ contains no .kt or .java files.")
165
+
166
+ return NativePlugin(
167
+ name=plugin_name,
168
+ root=root,
169
+ ios_entry=ios_entry,
170
+ android_entry=android_entry,
171
+ ios_sources=tuple(ios_sources),
172
+ android_sources=tuple(android_sources),
173
+ )
174
+
175
+
176
+ def _entry(manifest: Dict[str, object], platform: str, manifest_path: Path) -> Optional[str]:
177
+ section = manifest.get(platform)
178
+ if section is None:
179
+ return None
180
+ if not isinstance(section, dict) or not isinstance(section.get("entry"), str):
181
+ raise PluginError(f"{manifest_path}: '{platform}' must be an object with a string 'entry'.")
182
+ return str(section["entry"])
183
+
184
+
185
+ def discover_plugins(
186
+ *,
187
+ extra_paths: Iterable[Path] = (),
188
+ log: Optional[Logger] = None,
189
+ ) -> List[NativePlugin]:
190
+ """Find every plugin visible to this interpreter.
191
+
192
+ Args:
193
+ extra_paths: Plugin directories to include in addition to the
194
+ ``pythonnative.plugins`` entry points (from
195
+ ``[plugins].paths`` in the project config).
196
+ log: Optional progress logger; broken entry points are reported
197
+ here and skipped rather than failing the build.
198
+
199
+ Returns:
200
+ Plugins in a stable order (entry points sorted by name, then
201
+ explicit paths in the order given).
202
+ """
203
+ emit: Logger = log or (lambda _message: None)
204
+ plugins: List[NativePlugin] = []
205
+ seen: set = set()
206
+
207
+ for ep_name, target in sorted(_entry_points().items()):
208
+ try:
209
+ root = _resolve_entry_point(target)
210
+ plugin = load_plugin(root, name=ep_name)
211
+ except (PluginError, ImportError, AttributeError) as exc:
212
+ emit(f"Skipping native plugin {ep_name!r}: {exc}")
213
+ continue
214
+ if plugin.name in seen:
215
+ emit(f"Skipping duplicate native plugin {plugin.name!r}.")
216
+ continue
217
+ seen.add(plugin.name)
218
+ plugins.append(plugin)
219
+
220
+ for path in extra_paths:
221
+ plugin = load_plugin(Path(path))
222
+ if plugin.name in seen:
223
+ raise PluginError(f"Native plugin {plugin.name!r} is declared twice.")
224
+ seen.add(plugin.name)
225
+ plugins.append(plugin)
226
+
227
+ return plugins
228
+
229
+
230
+ def _entry_points() -> Dict[str, str]:
231
+ try:
232
+ from importlib.metadata import entry_points
233
+ except ImportError: # pragma: no cover - Python < 3.8
234
+ return {}
235
+ try:
236
+ eps = entry_points()
237
+ except Exception:
238
+ return {}
239
+ selected = []
240
+ if hasattr(eps, "select"):
241
+ selected = list(eps.select(group=ENTRY_POINT_GROUP))
242
+ else: # pragma: no cover - importlib.metadata < 3.10
243
+ getter = getattr(eps, "get", None)
244
+ if callable(getter):
245
+ selected = list(getter(ENTRY_POINT_GROUP, []))
246
+ return {ep.name: ep.value for ep in selected}
247
+
248
+
249
+ def _resolve_entry_point(target: str) -> Path:
250
+ """Turn ``"pkg.subpkg"`` (or ``"pkg.subpkg:attr"``) into a directory."""
251
+ module_name, _, attr = target.partition(":")
252
+ module = import_module(module_name.strip())
253
+ if attr:
254
+ value = getattr(module, attr.strip())
255
+ return Path(str(value))
256
+ location = getattr(module, "__file__", None)
257
+ if location is None:
258
+ raise PluginError(f"{module_name!r} is a namespace package; point the entry point at a regular package.")
259
+ return Path(location).resolve().parent
260
+
261
+
262
+ # ======================================================================
263
+ # Staging
264
+ # ======================================================================
265
+
266
+
267
+ def stage_ios_plugins(project_dir: Path, plugins: Sequence[NativePlugin], *, log: Optional[Logger] = None) -> None:
268
+ """Copy Swift sources into ``PythonNativeKit`` and write the registration file.
269
+
270
+ Args:
271
+ project_dir: The staged ``ios_template`` directory.
272
+ plugins: Plugins to bundle; ones without an iOS side are skipped.
273
+ log: Optional progress logger.
274
+ """
275
+ emit: Logger = log or (lambda _message: None)
276
+ plugins_dir = Path(project_dir).joinpath(*_IOS_PLUGIN_DIR)
277
+ registration = Path(project_dir).joinpath(*_IOS_REGISTRATION)
278
+ plugins_dir.mkdir(parents=True, exist_ok=True)
279
+
280
+ # Wipe previous plugin folders so removed plugins don't linger in an
281
+ # incremental build directory; the registration file is regenerated.
282
+ for child in plugins_dir.iterdir():
283
+ if child.is_dir():
284
+ shutil.rmtree(child)
285
+
286
+ bundled = [p for p in plugins if p.has_ios]
287
+ for plugin in bundled:
288
+ target = plugins_dir / plugin.name
289
+ for rel in plugin.ios_sources:
290
+ dest = target / Path(*rel.parts[1:]) # strip the leading "ios/"
291
+ dest.parent.mkdir(parents=True, exist_ok=True)
292
+ shutil.copy2(plugin.root / rel, dest)
293
+ registration.write_text(_render_swift_registration(bundled), encoding="utf-8")
294
+ if bundled:
295
+ emit(
296
+ f"Bundled {len(bundled)} native plugin(s) into PythonNativeKit: " + ", ".join(p.name for p in bundled) + "."
297
+ )
298
+
299
+
300
+ def stage_android_plugins(project_dir: Path, plugins: Sequence[NativePlugin], *, log: Optional[Logger] = None) -> None:
301
+ """Copy Kotlin sources into the ``pythonnative`` module and write the registration file.
302
+
303
+ Args:
304
+ project_dir: The staged ``android_template`` directory.
305
+ plugins: Plugins to bundle; ones without an Android side are skipped.
306
+ log: Optional progress logger.
307
+ """
308
+ emit: Logger = log or (lambda _message: None)
309
+ source_root = Path(project_dir).joinpath(*_ANDROID_SOURCE_ROOT)
310
+ registration = Path(project_dir).joinpath(*_ANDROID_REGISTRATION)
311
+ source_root.mkdir(parents=True, exist_ok=True)
312
+
313
+ bundled = [p for p in plugins if p.has_android]
314
+ for plugin in bundled:
315
+ for rel in plugin.android_sources:
316
+ dest = source_root / Path(*rel.parts[1:]) # strip the leading "android/"
317
+ dest.parent.mkdir(parents=True, exist_ok=True)
318
+ shutil.copy2(plugin.root / rel, dest)
319
+ registration.parent.mkdir(parents=True, exist_ok=True)
320
+ registration.write_text(_render_kotlin_registration(bundled), encoding="utf-8")
321
+ if bundled:
322
+ emit(
323
+ f"Bundled {len(bundled)} native plugin(s) into the pythonnative module: "
324
+ + ", ".join(p.name for p in bundled)
325
+ + "."
326
+ )
327
+
328
+
329
+ def _render_swift_registration(plugins: Sequence[NativePlugin]) -> str:
330
+ calls = "".join(f" {p.ios_entry}.register(into: registry)\n" for p in plugins)
331
+ body = f" {{\n{calls} }}" if calls else " {}"
332
+ return (
333
+ "// Generated by `pn build`. Do not edit: the PythonNative build tool\n"
334
+ "// overwrites this file with one `register(into:)` call per bundled plugin.\n"
335
+ "\n"
336
+ "/// Registration hook for plugins copied into `Sources/Plugins` by `pn build`.\n"
337
+ "public enum PNGeneratedPlugins {\n"
338
+ " /// Register every generated plugin into `registry`.\n"
339
+ f" public static func registerAll(into registry: PNRegistry){body}\n"
340
+ "}\n"
341
+ )
342
+
343
+
344
+ def _render_kotlin_registration(plugins: Sequence[NativePlugin]) -> str:
345
+ calls = "".join(f" {p.android_entry}.register(registry)\n" for p in plugins)
346
+ if calls:
347
+ method = f" fun registerAll(registry: PNRegistry) {{\n{calls} }}\n"
348
+ else:
349
+ method = ' @Suppress("UNUSED_PARAMETER")\n fun registerAll(registry: PNRegistry) {}\n'
350
+ return (
351
+ "package com.pythonnative.runtime.plugins\n"
352
+ "\n"
353
+ "import com.pythonnative.runtime.bridge.PNRegistry\n"
354
+ "\n"
355
+ "/**\n"
356
+ " * Plugin registrations generated by `pn build`.\n"
357
+ " *\n"
358
+ " * The build tool overwrites this file with one `register` call per\n"
359
+ " * plugin entry declared in the project's `pythonnative.plugins` entry\n"
360
+ " * points. Nothing else may depend on its content beyond [registerAll].\n"
361
+ " */\n"
362
+ "object GeneratedPlugins {\n"
363
+ " /** Register every generated plugin with `registry`. */\n"
364
+ f"{method}"
365
+ "}\n"
366
+ )
pythonnative/py.typed ADDED
File without changes
pythonnative/runtime.py CHANGED
@@ -47,12 +47,10 @@ Example:
47
47
  from __future__ import annotations
48
48
 
49
49
  import asyncio
50
- import ctypes
51
- import ctypes.util
52
50
  import inspect
53
51
  import threading
54
52
  import time as _time
55
- from typing import Any, Awaitable, Callable, Coroutine, Dict, Optional, TypeVar, Union
53
+ from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar, Union
56
54
 
57
55
  T = TypeVar("T")
58
56
 
@@ -463,28 +461,41 @@ def _has_pump_dispatcher() -> bool:
463
461
 
464
462
 
465
463
  def _dispatch_to_main_queue(fn: Callable[[], None]) -> None:
466
- """Enqueue ``fn`` on the platform main queue (never runs inline)."""
464
+ """Enqueue ``fn`` on the platform main queue (never runs inline).
465
+
466
+ On device this is the bridge's
467
+ [`post_to_main`][pythonnative.bridge.post_to_main]: native schedules
468
+ a ``pump`` callback on the next main-queue turn and Python drains
469
+ its queue then. Off device the ``pn preview`` dispatcher takes it;
470
+ headless tests have no dispatcher and drive the loop with
471
+ [`drain`][pythonnative.runtime.drain] instead.
472
+ """
467
473
  from .platform import Platform
468
474
 
469
- if Platform.is_ios:
470
- _ios_dispatch_async(fn)
471
- elif Platform.is_android:
472
- _android_post_to_main(fn)
475
+ if Platform.is_ios or Platform.is_android:
476
+ from .bridge import post_to_main
477
+
478
+ post_to_main(fn)
473
479
  elif _desktop_main_dispatch is not None:
474
480
  _desktop_main_dispatch(fn)
475
- # Headless: nothing to dispatch to; drain()/run_blocking() pump.
481
+
482
+
483
+ def _is_main_thread() -> bool:
484
+ """Whether the calling thread is the platform main thread.
485
+
486
+ Python is initialized on the platform main thread by both app
487
+ templates, so ``threading.main_thread()`` is the UIKit / Android
488
+ main thread on device.
489
+ """
490
+ return threading.current_thread() is threading.main_thread()
476
491
 
477
492
 
478
493
  def call_on_main_thread(fn: Callable[[], None]) -> None:
479
494
  """Run ``fn()`` on the platform UI thread.
480
495
 
481
- - **iOS**: dispatches ``fn`` onto the main dispatch queue via
482
- ``libdispatch.dispatch_async_f`` (called through
483
- :class:`ctypes.PyDLL` to keep the GIL held), or runs inline when
484
- already on the main thread.
485
- - **Android**: posts a ``Runnable`` to
486
- ``Handler(Looper.getMainLooper())``, or runs inline on the main
487
- looper thread.
496
+ - **iOS / Android**: runs inline when already on the main thread,
497
+ otherwise queues ``fn`` through the bridge (one native crossing
498
+ per batch of queued callables).
488
499
  - **Desktop**: enqueues ``fn`` for the ``pn preview`` poll loop
489
500
  (or runs inline if no preview is live).
490
501
  - **Tests**: runs ``fn()`` inline.
@@ -498,199 +509,20 @@ def call_on_main_thread(fn: Callable[[], None]) -> None:
498
509
  """
499
510
  from .platform import Platform
500
511
 
501
- if Platform.is_ios:
502
- _ios_call_on_main(fn)
503
- elif Platform.is_android:
504
- _android_call_on_main(fn)
512
+ if Platform.is_ios or Platform.is_android:
513
+ if _is_main_thread():
514
+ try:
515
+ fn()
516
+ except Exception as exc:
517
+ print(f"[pn.runtime] main-inline callback raised: {exc!r}")
518
+ return
519
+ _dispatch_to_main_queue(fn)
505
520
  elif Platform.is_desktop and _desktop_main_dispatch is not None:
506
521
  _desktop_main_dispatch(fn)
507
522
  else:
508
523
  fn()
509
524
 
510
525
 
511
- # ----------------------------------------------------------------------
512
- # iOS main-queue dispatch (PyDLL-based, no GIL release)
513
- # ----------------------------------------------------------------------
514
- #
515
- # rubicon-objc loads libobjc via ctypes.CDLL, which RELEASES THE GIL
516
- # around every ObjC method call. On the iOS simulator (and to a lesser
517
- # extent on-device), a thread is then parked by the kernel until the
518
- # next wakeup-coalescing tick (~500ms), making cross-thread ObjC calls
519
- # absurdly slow. To avoid this, we call libdispatch's
520
- # ``dispatch_async_f`` directly via ``ctypes.PyDLL`` (which keeps the
521
- # GIL held), and pass a ``CFUNCTYPE`` trampoline as the work function.
522
- # The trampoline runs on the main thread, looks up the pending Python
523
- # callable by an integer key, and invokes it.
524
-
525
- _dispatch_lib: Optional[Any] = None
526
- _dispatch_async_f_c: Optional[Any] = None
527
- _dispatch_main_q_ptr: int = 0
528
- _DISPATCH_FN_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
529
- _main_pending: Dict[int, Callable[[], None]] = {}
530
- _main_pending_lock = threading.Lock()
531
- _main_next_id: int = 0
532
-
533
-
534
- def _main_trampoline_py(ctx: int) -> None:
535
- """Run on the main thread; pop the queued callable and call it."""
536
- with _main_pending_lock:
537
- fn = _main_pending.pop(ctx, None)
538
- if fn is None:
539
- return
540
- try:
541
- fn()
542
- except Exception as exc:
543
- print(f"[pn.runtime] main-thread trampoline raised: {exc!r}")
544
-
545
-
546
- # Keep a strong reference to the C-callable trampoline forever; if it
547
- # gets GC'd while libdispatch holds the function pointer, we crash.
548
- _main_trampoline_c = _DISPATCH_FN_TYPE(_main_trampoline_py)
549
-
550
-
551
- def _ensure_libdispatch_loaded() -> bool:
552
- """Locate libdispatch + the main queue. Returns True on success."""
553
- global _dispatch_lib, _dispatch_async_f_c, _dispatch_main_q_ptr
554
- if _dispatch_lib is not None:
555
- return True
556
- try:
557
- # PyDLL keeps the GIL held during C calls, preventing the
558
- # calling thread from being parked after dispatch_async_f.
559
- lib = ctypes.PyDLL("/usr/lib/system/libdispatch.dylib")
560
- except OSError:
561
- try:
562
- lib = ctypes.PyDLL(None) # main program (libdispatch is in libSystem)
563
- except OSError as exc:
564
- print(f"[pn.runtime] could not load libdispatch via PyDLL: {exc!r}")
565
- return False
566
- try:
567
- fn = lib.dispatch_async_f
568
- fn.restype = None
569
- fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
570
- # The main queue is a struct global named ``_dispatch_main_q``
571
- # in libdispatch. dispatch_async_f wants a pointer to it.
572
- sym = ctypes.c_void_p.in_dll(lib, "_dispatch_main_q")
573
- main_q_ptr = ctypes.addressof(sym)
574
- except (AttributeError, ValueError) as exc:
575
- print(f"[pn.runtime] libdispatch missing expected symbol: {exc!r}")
576
- return False
577
- _dispatch_lib = lib
578
- _dispatch_async_f_c = fn
579
- _dispatch_main_q_ptr = main_q_ptr
580
- return True
581
-
582
-
583
- _pthread_main_np_c: Optional[Any] = None
584
-
585
-
586
- def _ios_is_main_thread() -> bool:
587
- """Cheap main-thread check using pthread_main_np (no GIL release)."""
588
- try:
589
- # pthread_main_np is in libc; calling via PyDLL keeps the GIL.
590
- global _pthread_main_np_c
591
- if _pthread_main_np_c is None:
592
- libc = ctypes.PyDLL(ctypes.util.find_library("c") or "/usr/lib/libc.dylib")
593
- f = libc.pthread_main_np
594
- f.restype = ctypes.c_int
595
- f.argtypes = []
596
- _pthread_main_np_c = f
597
- return bool(_pthread_main_np_c())
598
- except Exception:
599
- # Fall back to threading module (Python-side check).
600
- return threading.current_thread().name == "MainThread"
601
-
602
-
603
- def _ios_dispatch_async(fn: Callable[[], None]) -> None:
604
- """Enqueue ``fn`` on the iOS main dispatch queue (always queued)."""
605
- global _main_next_id
606
- if not _ensure_libdispatch_loaded():
607
- print("[pn.runtime] libdispatch unavailable; running callback inline")
608
- try:
609
- fn()
610
- except Exception as exc:
611
- print(f"[pn.runtime] inline fallback raised: {exc!r}")
612
- return
613
- with _main_pending_lock:
614
- _main_next_id += 1
615
- key = _main_next_id
616
- _main_pending[key] = fn
617
- # dispatch_async_f(queue, context, work): non-blocking; just
618
- # enqueues the work onto the main queue and returns.
619
- assert _dispatch_async_f_c is not None
620
- _dispatch_async_f_c(_dispatch_main_q_ptr, key, _main_trampoline_c)
621
-
622
-
623
- def _ios_call_on_main(fn: Callable[[], None]) -> None:
624
- """Run ``fn`` on the iOS main thread (inline when already there)."""
625
- if _ios_is_main_thread():
626
- try:
627
- fn()
628
- except Exception as exc:
629
- print(f"[pn.runtime] main-inline callback raised: {exc!r}")
630
- return
631
- _ios_dispatch_async(fn)
632
-
633
-
634
- # Cached Android main-queue plumbing. Posting a pump is a hot path (it
635
- # runs for every batch of scheduled loop work), so the Runnable proxy
636
- # class and the main-looper Handler are created once and reused;
637
- # defining a ``dynamic_proxy`` class per call would register a new JNI
638
- # class every time.
639
- _android_runnable_cls: Optional[Any] = None
640
- _android_main_handler: Optional[Any] = None
641
-
642
-
643
- def _ensure_android_main_handler() -> Any:
644
- global _android_runnable_cls, _android_main_handler
645
- if _android_main_handler is not None:
646
- return _android_main_handler
647
- from java import dynamic_proxy, jclass
648
-
649
- Runnable = jclass("java.lang.Runnable")
650
-
651
- class _PNMainRunnable(dynamic_proxy(Runnable)): # type: ignore[misc]
652
- def __init__(self, fn: Callable[[], None]) -> None:
653
- super().__init__()
654
- self._fn = fn
655
-
656
- def run(self) -> None:
657
- try:
658
- self._fn()
659
- except Exception as exc: # pragma: no cover - last resort
660
- print(f"[pn.runtime] main-thread runnable raised: {exc!r}")
661
-
662
- Looper = jclass("android.os.Looper")
663
- Handler = jclass("android.os.Handler")
664
- _android_runnable_cls = _PNMainRunnable
665
- _android_main_handler = Handler(Looper.getMainLooper())
666
- return _android_main_handler
667
-
668
-
669
- def _android_post_to_main(fn: Callable[[], None]) -> None:
670
- """Enqueue ``fn`` on the Android main looper (always queued)."""
671
- try:
672
- handler = _ensure_android_main_handler()
673
- assert _android_runnable_cls is not None
674
- handler.post(_android_runnable_cls(fn))
675
- except Exception as exc:
676
- print(f"[pn.runtime] _android_post_to_main failed, falling back inline: {exc!r}")
677
- fn()
678
-
679
-
680
- def _android_call_on_main(fn: Callable[[], None]) -> None:
681
- """Run ``fn`` on the Android main thread (inline when already there)."""
682
- try:
683
- from java import jclass
684
-
685
- Looper = jclass("android.os.Looper")
686
- if Looper.myLooper() == Looper.getMainLooper():
687
- fn()
688
- return
689
- except Exception:
690
- pass
691
- _android_post_to_main(fn)
692
-
693
-
694
526
  __all__ = [
695
527
  "call_on_main_thread",
696
528
  "call_threadsafe",