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
@@ -2,46 +2,51 @@
2
2
 
3
3
  The ``pythonnative.sdk`` package collects the *stable* extension
4
4
  contract that third-party packages rely on: the
5
- [`ViewHandler`][pythonnative.sdk.ViewHandler] protocol, the
6
5
  [`Style`][pythonnative.sdk.Style] type, the
7
- [`@native_component`][pythonnative.sdk.native_component] registration
8
- decorator, and an
6
+ [`@native_component`][pythonnative.sdk.native_component] /
7
+ [`register_component`][pythonnative.sdk.register_component]
8
+ registration helpers, the
9
9
  [`element_factory`][pythonnative.sdk.element_factory] helper for
10
- producing strongly-typed element constructors.
10
+ producing strongly-typed element constructors, the
11
+ [`ViewHandler`][pythonnative.sdk.ViewHandler] protocol for off-device
12
+ stand-ins, and the native module registry
13
+ ([`native_module`][pythonnative.sdk.native_module],
14
+ [`register_python_module`][pythonnative.sdk.register_python_module]).
11
15
 
12
16
  A custom native component is three things:
13
17
 
14
18
  1. A typed, frozen [`Props`][pythonnative.sdk.Props] dataclass listing
15
19
  the public properties the component accepts.
16
- 2. One or more
17
- [`ViewHandler`][pythonnative.sdk.ViewHandler] subclasses (one per
18
- target platform) implementing creation, update, and child management
19
- for the underlying native widget.
20
- 3. A registration call (the
21
- [`@native_component`][pythonnative.sdk.native_component] decorator,
22
- or
23
- [`register_component`][pythonnative.sdk.register_component] for
24
- imperative use) that binds the props type and handler into the
25
- process-wide registry.
20
+ 2. A Swift ``PNComponentManager`` and a Kotlin ``ComponentManager``
21
+ registered under the component's name by the package's native
22
+ plugin (``pn_plugin.json`` next to ``ios/`` and ``android/`` source
23
+ folders; see ``docs/guides/custom-components.md``).
24
+ 3. A registration call in Python
25
+ ([`register_component`][pythonnative.sdk.register_component], or the
26
+ [`@native_component`][pythonnative.sdk.native_component] decorator
27
+ when you also supply a desktop
28
+ [`ViewHandler`][pythonnative.sdk.ViewHandler] for ``pn preview``)
29
+ that declares the element name and binds its props type.
26
30
 
27
31
  Once registered, the component appears alongside the built-ins: the
28
32
  reconciler, layout engine, and Fast Refresh treat it identically.
29
33
 
30
- PyPI packages can ship handlers without users importing them
31
- explicitly by declaring an entry point in the
32
- ``pythonnative.handlers`` group; PythonNative discovers and imports
33
- those modules the first time the registry is asked for a handler.
34
+ A native module (device API without a view) follows the same split: a
35
+ Swift / Kotlin class registered by name in the plugin, a Python facade
36
+ that calls ``native_module(name).call(...)``, and optionally a Python
37
+ implementation registered with ``register_python_module`` for desktop
38
+ and tests.
39
+
40
+ PyPI packages can ship both without users importing them explicitly
41
+ by declaring entry points in the ``pythonnative.handlers`` (Python
42
+ side) and ``pythonnative.plugins`` (native source) groups; ``pn build``
43
+ compiles the native sources into the app.
34
44
 
35
45
  Example:
36
46
  ```python
37
47
  from dataclasses import dataclass
38
48
  import pythonnative as pn
39
- from pythonnative.sdk import (
40
- Props,
41
- ViewHandler,
42
- element_factory,
43
- native_component,
44
- )
49
+ from pythonnative.sdk import Props, element_factory, register_component
45
50
 
46
51
 
47
52
  @dataclass(frozen=True)
@@ -51,17 +56,10 @@ Example:
51
56
  style: pn.StyleProp = None
52
57
 
53
58
 
54
- @native_component("Badge", props=BadgeProps, platforms=("ios",))
55
- class IOSBadgeHandler(ViewHandler):
56
- def create(self, props):
57
- ...
58
-
59
- def update(self, view, changed):
60
- ...
61
-
62
-
59
+ register_component(name="Badge", props=BadgeProps)
63
60
  Badge = element_factory("Badge")
64
61
 
62
+
65
63
  @pn.component
66
64
  def App():
67
65
  return pn.Column(
@@ -72,6 +70,14 @@ Example:
72
70
  """
73
71
 
74
72
  from ..element import Element
73
+ from ..native_modules.registry import (
74
+ NativeModule,
75
+ NativeModuleError,
76
+ PythonModule,
77
+ emit,
78
+ native_module,
79
+ register_python_module,
80
+ )
75
81
  from ..native_views.base import ViewHandler, parse_color_int
76
82
  from ..style import (
77
83
  Color,
@@ -91,6 +97,7 @@ from ._components import (
91
97
  ENTRY_POINT_GROUP,
92
98
  Props,
93
99
  element_factory,
100
+ get_desktop_handler,
94
101
  get_props_type,
95
102
  install_into_registry,
96
103
  list_components,
@@ -122,10 +129,18 @@ __all__ = [
122
129
  "ENTRY_POINT_GROUP",
123
130
  "Props",
124
131
  "element_factory",
132
+ "get_desktop_handler",
125
133
  "get_props_type",
126
134
  "install_into_registry",
127
135
  "list_components",
128
136
  "native_component",
129
137
  "register_component",
130
138
  "unregister_component",
139
+ # Native-module SDK
140
+ "NativeModule",
141
+ "NativeModuleError",
142
+ "PythonModule",
143
+ "emit",
144
+ "native_module",
145
+ "register_python_module",
131
146
  ]
@@ -4,26 +4,28 @@ Implements the [`@native_component`][pythonnative.sdk.native_component]
4
4
  decorator and supporting helpers that let third-party packages contribute
5
5
  new element types to the reconciler.
6
6
 
7
- The registration model is intentionally small. A custom component is a
7
+ On device a custom component is rendered by a Swift
8
+ ``PNComponentManager`` and a Kotlin ``ComponentManager`` registered
9
+ under the same type name by the package's native plugin (see
10
+ ``docs/guides/custom-components.md``). The Python side only needs to
11
+ know the element name and, optionally, its typed props and a
12
+ [`ViewHandler`][pythonnative.sdk.ViewHandler] that renders a stand-in
13
+ in ``pn preview`` and unit tests. Registration is therefore a
8
14
  three-part agreement:
9
15
 
10
- 1. A typed, immutable
11
- [`Props`][pythonnative.sdk.Props] dataclass declaring the
12
- component's public surface.
13
- 2. One or more
14
- [`ViewHandler`][pythonnative.sdk.ViewHandler] subclasses
15
- (one per platform) implementing the platform-side rendering.
16
- 3. A name (string) used by the reconciler to look up the handler.
17
-
18
- The decorator stores the (name, props_type, handler_instance) tuple
19
- in a process-wide registry. The
20
- [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
21
- calls
16
+ 1. A typed, immutable [`Props`][pythonnative.sdk.Props] dataclass
17
+ declaring the component's public surface.
18
+ 2. An element factory built with
19
+ [`element_factory`][pythonnative.sdk.element_factory].
20
+ 3. Optionally, a desktop [`ViewHandler`][pythonnative.sdk.ViewHandler]
21
+ so the component also renders off device.
22
+
23
+ The registry is process-wide. The view backend calls
22
24
  [`install_into_registry`][pythonnative.sdk.install_into_registry] on
23
25
  first use; that helper performs entry-point discovery (importing any
24
26
  modules registered under
25
27
  [`ENTRY_POINT_GROUP`][pythonnative.sdk.ENTRY_POINT_GROUP]) and copies
26
- every handler matching the active platform into the registry.
28
+ every desktop handler into the registry.
27
29
 
28
30
  Example:
29
31
  ```python
@@ -39,10 +41,10 @@ Example:
39
41
  style: pn.StyleProp = None
40
42
 
41
43
 
42
- @native_component("Badge", props=BadgeProps, platforms=("ios",))
43
- class IOSBadgeHandler(ViewHandler):
44
+ @native_component("Badge", props=BadgeProps)
45
+ class BadgePreviewHandler(ViewHandler):
44
46
  def create(self, tag, props):
45
- ...
47
+ ... # Tkinter stand-in for pn preview
46
48
 
47
49
  def update(self, view, changed):
48
50
  ...
@@ -102,8 +104,8 @@ class Props:
102
104
  # Internal registry
103
105
  # ---------------------------------------------------------------------- #
104
106
 
105
- # name -> (props_type or None, {platform_name: handler_instance})
106
- _REGISTRY: Dict[str, Tuple[Optional[type], Dict[str, ViewHandler]]] = {}
107
+ # name -> (props_type or None, desktop handler or None)
108
+ _REGISTRY: Dict[str, Tuple[Optional[type], Optional[ViewHandler]]] = {}
107
109
 
108
110
  # Caches `_install_into_registry` runs to avoid repeated entry-point
109
111
  # discovery once the registry has been populated for a given platform.
@@ -117,27 +119,22 @@ def native_component(
117
119
  name: str,
118
120
  *,
119
121
  props: Optional[type] = None,
120
- platforms: Optional[Tuple[str, ...]] = None,
121
122
  ) -> Callable[[Type[H]], Type[H]]:
122
- """Decorator that registers a [`ViewHandler`][pythonnative.sdk.ViewHandler] under ``name``.
123
+ """Decorator that registers a desktop [`ViewHandler`][pythonnative.sdk.ViewHandler] under ``name``.
123
124
 
124
125
  The handler class is instantiated immediately and stored in the
125
- process-wide registry. Decorate the same ``name`` once per platform
126
- when shipping platform-specific implementations; the decorator
127
- accumulates entries in a ``{platform: handler}`` mapping per name.
126
+ process-wide registry as the component's off-device renderer. The
127
+ on-device renderers are the Swift and Kotlin component managers the
128
+ package's native plugin registers under the same ``name``.
128
129
 
129
130
  Args:
130
- name: Element type name (e.g., ``"Badge"``). Must be a valid
131
- identifier-like string. Used by the reconciler at lookup time.
131
+ name: Element type name (e.g., ``"Badge"``). Used by the
132
+ reconciler and by native component managers at lookup time.
132
133
  props: Optional dataclass type describing the component's
133
134
  typed props. When supplied, the
134
135
  [`element_factory`][pythonnative.sdk.element_factory] helper
135
136
  uses this type to validate kwargs and produce frozen prop
136
137
  instances.
137
- platforms: Tuple of platform identifiers
138
- (``"ios"`` / ``"android"``) the handler implements. Defaults
139
- to ``("android", "ios")`` so a single cross-platform handler
140
- registers everywhere.
141
138
 
142
139
  Returns:
143
140
  A decorator that, when applied to a
@@ -147,31 +144,12 @@ def native_component(
147
144
  Raises:
148
145
  TypeError: If the decorated object is not a class subclassing
149
146
  ``ViewHandler``.
150
-
151
- Example:
152
- ```python
153
- from dataclasses import dataclass
154
- from pythonnative.sdk import Props, ViewHandler, native_component
155
-
156
-
157
- @dataclass(frozen=True)
158
- class BadgeProps(Props):
159
- text: str = ""
160
- color: str = "#FF3B30"
161
-
162
-
163
- @native_component("Badge", props=BadgeProps, platforms=("ios",))
164
- class IOSBadgeHandler(ViewHandler):
165
- def create(self, tag, props):
166
- ...
167
- ```
168
147
  """
169
- plats: Tuple[str, ...] = platforms if platforms is not None else ("android", "ios")
170
148
 
171
149
  def decorator(handler_cls: Type[H]) -> Type[H]:
172
150
  if not isinstance(handler_cls, type) or not issubclass(handler_cls, ViewHandler):
173
151
  raise TypeError(f"@native_component({name!r}) must decorate a ViewHandler subclass; got {handler_cls!r}")
174
- register_component(name=name, props=props, handlers={plat: handler_cls() for plat in plats})
152
+ register_component(name=name, props=props, handler=handler_cls())
175
153
  return handler_cls
176
154
 
177
155
  return decorator
@@ -181,42 +159,41 @@ def register_component(
181
159
  *,
182
160
  name: str,
183
161
  props: Optional[type] = None,
184
- handlers: Dict[str, ViewHandler],
162
+ handler: Optional[ViewHandler] = None,
185
163
  ) -> None:
186
164
  """Register a custom native component imperatively.
187
165
 
188
- Equivalent to applying [`@native_component`][pythonnative.sdk.native_component]
189
- one or more times, but useful when constructing handlers
190
- programmatically (e.g., parameterized handler instances). Subsequent
191
- calls for the same ``name`` merge their ``handlers`` into the
192
- existing entry, replacing any previously-registered handler for the
193
- same platform.
166
+ Declares ``name`` as an element type so
167
+ [`element_factory`][pythonnative.sdk.element_factory] can build it.
168
+ ``handler`` is the optional desktop / test renderer; native
169
+ rendering always comes from the platform component managers.
170
+ Subsequent calls for the same ``name`` merge: a later ``props`` or
171
+ ``handler`` replaces the earlier one, ``None`` leaves it alone.
194
172
 
195
173
  Args:
196
174
  name: Element type name.
197
175
  props: Optional dataclass type describing the typed props.
198
- handlers: ``{platform_name: handler_instance}`` mapping. Common
199
- keys are ``"ios"`` and ``"android"``.
176
+ handler: Optional [`ViewHandler`][pythonnative.sdk.ViewHandler]
177
+ instance used off device.
200
178
 
201
179
  Raises:
202
- TypeError: If any handler is not a
203
- [`ViewHandler`][pythonnative.sdk.ViewHandler] instance, or
180
+ TypeError: If ``handler`` is not a ``ViewHandler`` instance, or
204
181
  if ``props`` is not a dataclass type.
205
182
  """
206
183
  if props is not None and not (isinstance(props, type) and is_dataclass(props)):
207
184
  raise TypeError(f"register_component({name!r}): props must be a @dataclass type, got {props!r}")
208
- for plat, handler in handlers.items():
209
- if not isinstance(handler, ViewHandler):
210
- raise TypeError(f"register_component({name!r}): handler for {plat!r} must be a ViewHandler instance")
185
+ if handler is not None and not isinstance(handler, ViewHandler):
186
+ raise TypeError(f"register_component({name!r}): handler must be a ViewHandler instance")
211
187
 
212
188
  existing = _REGISTRY.get(name)
213
189
  if existing is None:
214
- _REGISTRY[name] = (props, dict(handlers))
190
+ _REGISTRY[name] = (props, handler)
215
191
  return
216
- existing_props, plat_map = existing
217
- new_props = props if props is not None else existing_props
218
- plat_map.update(handlers)
219
- _REGISTRY[name] = (new_props, plat_map)
192
+ existing_props, existing_handler = existing
193
+ _REGISTRY[name] = (
194
+ props if props is not None else existing_props,
195
+ handler if handler is not None else existing_handler,
196
+ )
220
197
 
221
198
 
222
199
  def unregister_component(name: str) -> None:
@@ -247,11 +224,17 @@ def get_props_type(name: str) -> Optional[type]:
247
224
  return entry[0] if entry is not None else None
248
225
 
249
226
 
250
- def install_into_registry(registry: Any, platform_name: str) -> None:
251
- """Copy registered handlers into a [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry].
227
+ def get_desktop_handler(name: str) -> Optional[ViewHandler]:
228
+ """Return the registered off-device handler for ``name`` (or ``None``)."""
229
+ entry = _REGISTRY.get(name)
230
+ return entry[1] if entry is not None else None
231
+
232
+
233
+ def install_into_registry(registry: Any) -> None:
234
+ """Copy registered desktop handlers into a view registry.
252
235
 
253
236
  Called once by the registry on first use. Triggers entry-point
254
- discovery on the first call so PyPI-installed handlers register
237
+ discovery on the first call so PyPI-installed components register
255
238
  themselves before the registry snapshot is taken.
256
239
 
257
240
  Args:
@@ -259,12 +242,9 @@ def install_into_registry(registry: Any, platform_name: str) -> None:
259
242
  [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]
260
243
  (or duck-compatible object) with a ``register(name, handler)``
261
244
  method.
262
- platform_name: The active platform identifier
263
- (``"ios"`` or ``"android"``).
264
245
  """
265
246
  _discover_entry_points()
266
- for name, (_props_type, plat_map) in _REGISTRY.items():
267
- handler = plat_map.get(platform_name)
247
+ for name, (_props_type, handler) in _REGISTRY.items():
268
248
  if handler is not None:
269
249
  registry.register(name, handler)
270
250
 
@@ -418,6 +398,7 @@ __all__ = [
418
398
  "ENTRY_POINT_GROUP",
419
399
  "Props",
420
400
  "element_factory",
401
+ "get_desktop_handler",
421
402
  "get_props_type",
422
403
  "install_into_registry",
423
404
  "list_components",
pythonnative/storage.py CHANGED
@@ -1,19 +1,19 @@
1
1
  """Cross-platform key/value persistence (``AsyncStorage``).
2
2
 
3
3
  Mirrors React Native's ``AsyncStorage`` API but with native ``async``
4
- coroutines and a JSON convenience layer. Values are persisted using
5
- the platform-appropriate key/value store:
4
+ coroutines and a JSON convenience layer. Values are persisted by the
5
+ native ``Storage`` module:
6
6
 
7
7
  - **iOS**: ``NSUserDefaults`` (standard user defaults).
8
8
  - **Android**: ``SharedPreferences`` (file ``"pn_async_storage"``).
9
9
  - **Desktop / tests**: an in-memory dict optionally backed by a JSON
10
- file under
11
- [`FileSystem.app_dir`][pythonnative.native_modules.file_system.FileSystem.app_dir]
12
- for inter-run persistence during local development.
10
+ file under ``PN_STORAGE_DIR`` for inter-run persistence during local
11
+ development (see
12
+ [`DesktopStorage`][pythonnative.native_modules.desktop.DesktopStorage]).
13
13
 
14
- All operations are coroutines so they don't block the framework loop;
15
- the underlying native calls are dispatched via
16
- :func:`asyncio.to_thread`.
14
+ Every operation is a coroutine. The platform stores answer in
15
+ microseconds, so calls complete inline on the framework loop without
16
+ a thread hop.
17
17
 
18
18
  Example:
19
19
  ```python
@@ -34,220 +34,16 @@ Example:
34
34
 
35
35
  from __future__ import annotations
36
36
 
37
- import asyncio
38
37
  import json
39
- import os
40
- import threading
41
38
  from typing import Any, Callable, List, Optional, Tuple, TypeVar
42
39
 
43
- from .utils import IS_ANDROID, IS_IOS
40
+ from .native_modules.registry import NativeModule, native_module
44
41
 
45
42
  T = TypeVar("T")
46
43
 
47
- # ======================================================================
48
- # Backend selection
49
- # ======================================================================
50
44
 
51
-
52
- _DEFAULTS_SUITE = "pn_async_storage"
53
-
54
-
55
- # Cache the NSUserDefaults class lookup. rubicon.objc's
56
- # ``ObjCClass("NSUserDefaults")`` walks the ObjC runtime metadata which
57
- # takes hundreds of milliseconds on first call; resolving once at module
58
- # import keeps every later get/set/delete in the sub-millisecond range.
59
- _ios_defaults: Any = None
60
-
61
-
62
- def _ios_get_defaults() -> Any:
63
- global _ios_defaults
64
- if _ios_defaults is None:
65
- from rubicon.objc import ObjCClass
66
-
67
- _ios_defaults = ObjCClass("NSUserDefaults").standardUserDefaults
68
- return _ios_defaults
69
-
70
-
71
- def _ios_set(key: str, value: str) -> None:
72
- defaults = _ios_get_defaults()
73
- defaults.setObject_forKey_(value, key)
74
- # ``synchronize()`` is documented as unnecessary on modern iOS and
75
- # can block for seconds while it flushes to disk on a busy system;
76
- # NSUserDefaults already coalesces writes asynchronously.
77
-
78
-
79
- def _ios_get(key: str) -> Optional[str]:
80
- defaults = _ios_get_defaults()
81
- val = defaults.stringForKey_(key)
82
- if val is None:
83
- return None
84
- try:
85
- return str(val)
86
- except Exception:
87
- return None
88
-
89
-
90
- def _ios_delete(key: str) -> None:
91
- defaults = _ios_get_defaults()
92
- defaults.removeObjectForKey_(key)
93
-
94
-
95
- def _ios_all_keys() -> List[str]:
96
- defaults = _ios_get_defaults()
97
- rep = defaults.dictionaryRepresentation()
98
- if rep is None:
99
- return []
100
- try:
101
- keys = rep.allKeys
102
- return [str(keys.objectAtIndex_(i)) for i in range(keys.count)]
103
- except Exception:
104
- return []
105
-
106
-
107
- def _ios_clear() -> None:
108
- for key in _ios_all_keys():
109
- _ios_delete(key)
110
-
111
-
112
- def _android_prefs() -> Any:
113
- from java import jclass
114
-
115
- from .utils import get_android_context
116
-
117
- Context = jclass("android.content.Context")
118
- ctx = get_android_context()
119
- return ctx.getSharedPreferences(_DEFAULTS_SUITE, Context.MODE_PRIVATE)
120
-
121
-
122
- def _android_set(key: str, value: str) -> None:
123
- prefs = _android_prefs()
124
- editor = prefs.edit()
125
- editor.putString(key, value)
126
- editor.apply()
127
-
128
-
129
- def _android_get(key: str) -> Optional[str]:
130
- prefs = _android_prefs()
131
- if not prefs.contains(key):
132
- return None
133
- try:
134
- return str(prefs.getString(key, None))
135
- except Exception:
136
- return None
137
-
138
-
139
- def _android_delete(key: str) -> None:
140
- prefs = _android_prefs()
141
- editor = prefs.edit()
142
- editor.remove(key)
143
- editor.apply()
144
-
145
-
146
- def _android_all_keys() -> List[str]:
147
- prefs = _android_prefs()
148
- all_map = prefs.getAll()
149
- keys = all_map.keySet()
150
- it = keys.iterator()
151
- out: List[str] = []
152
- while it.hasNext():
153
- out.append(str(it.next()))
154
- return out
155
-
156
-
157
- def _android_clear() -> None:
158
- prefs = _android_prefs()
159
- editor = prefs.edit()
160
- editor.clear()
161
- editor.apply()
162
-
163
-
164
- # ======================================================================
165
- # Desktop / test backend
166
- # ======================================================================
167
-
168
-
169
- _desktop_lock = threading.Lock()
170
- _desktop_store: dict = {}
171
- _desktop_loaded = False
172
-
173
-
174
- def _desktop_path() -> Optional[str]:
175
- # Tests can opt out of disk persistence by leaving the env var unset.
176
- base = os.environ.get("PN_STORAGE_DIR")
177
- if not base:
178
- return None
179
- try:
180
- os.makedirs(base, exist_ok=True)
181
- except OSError:
182
- return None
183
- return os.path.join(base, "pn_async_storage.json")
184
-
185
-
186
- def _desktop_load() -> None:
187
- global _desktop_loaded
188
- if _desktop_loaded:
189
- return
190
- _desktop_loaded = True
191
- path = _desktop_path()
192
- if path is None or not os.path.exists(path):
193
- return
194
- try:
195
- with open(path, encoding="utf-8") as f:
196
- data = json.load(f)
197
- if isinstance(data, dict):
198
- _desktop_store.update({str(k): str(v) for k, v in data.items()})
199
- except OSError:
200
- return
201
- except json.JSONDecodeError:
202
- return
203
-
204
-
205
- def _desktop_persist() -> None:
206
- path = _desktop_path()
207
- if path is None:
208
- return
209
- try:
210
- with open(path, "w", encoding="utf-8") as f:
211
- json.dump(_desktop_store, f)
212
- except OSError:
213
- pass
214
-
215
-
216
- def _desktop_set(key: str, value: str) -> None:
217
- with _desktop_lock:
218
- _desktop_load()
219
- _desktop_store[key] = value
220
- _desktop_persist()
221
-
222
-
223
- def _desktop_get(key: str) -> Optional[str]:
224
- with _desktop_lock:
225
- _desktop_load()
226
- return _desktop_store.get(key)
227
-
228
-
229
- def _desktop_delete(key: str) -> None:
230
- with _desktop_lock:
231
- _desktop_load()
232
- _desktop_store.pop(key, None)
233
- _desktop_persist()
234
-
235
-
236
- def _desktop_all_keys() -> List[str]:
237
- with _desktop_lock:
238
- _desktop_load()
239
- return list(_desktop_store.keys())
240
-
241
-
242
- def _desktop_clear() -> None:
243
- with _desktop_lock:
244
- _desktop_store.clear()
245
- _desktop_persist()
246
-
247
-
248
- # ======================================================================
249
- # Public API
250
- # ======================================================================
45
+ def _storage() -> NativeModule:
46
+ return native_module("Storage")
251
47
 
252
48
 
253
49
  class AsyncStorage:
@@ -265,11 +61,8 @@ class AsyncStorage:
265
61
  @staticmethod
266
62
  async def get(key: str) -> Optional[str]:
267
63
  """Return the string stored at ``key``, or ``None`` if missing."""
268
- if IS_IOS:
269
- return await asyncio.to_thread(_ios_get, key)
270
- if IS_ANDROID:
271
- return await asyncio.to_thread(_android_get, key)
272
- return await asyncio.to_thread(_desktop_get, key)
64
+ value = _storage().call("get", key=key)
65
+ return None if value is None else str(value)
273
66
 
274
67
  @staticmethod
275
68
  async def set(key: str, value: str) -> None:
@@ -280,41 +73,23 @@ class AsyncStorage:
280
73
  """
281
74
  if not isinstance(value, str):
282
75
  raise TypeError("AsyncStorage.set requires a str value; use set_json for richer types")
283
- if IS_IOS:
284
- await asyncio.to_thread(_ios_set, key, value)
285
- elif IS_ANDROID:
286
- await asyncio.to_thread(_android_set, key, value)
287
- else:
288
- await asyncio.to_thread(_desktop_set, key, value)
76
+ _storage().call("set", key=key, value=value)
289
77
 
290
78
  @staticmethod
291
79
  async def delete(key: str) -> None:
292
80
  """Remove the entry at ``key`` if present (no-op otherwise)."""
293
- if IS_IOS:
294
- await asyncio.to_thread(_ios_delete, key)
295
- elif IS_ANDROID:
296
- await asyncio.to_thread(_android_delete, key)
297
- else:
298
- await asyncio.to_thread(_desktop_delete, key)
81
+ _storage().call("delete", key=key)
299
82
 
300
83
  @staticmethod
301
84
  async def all_keys() -> List[str]:
302
85
  """Return every persisted key (order is platform-dependent)."""
303
- if IS_IOS:
304
- return await asyncio.to_thread(_ios_all_keys)
305
- if IS_ANDROID:
306
- return await asyncio.to_thread(_android_all_keys)
307
- return await asyncio.to_thread(_desktop_all_keys)
86
+ keys = _storage().call("all_keys")
87
+ return [str(k) for k in keys] if isinstance(keys, list) else []
308
88
 
309
89
  @staticmethod
310
90
  async def clear() -> None:
311
91
  """Remove every entry written through ``AsyncStorage``."""
312
- if IS_IOS:
313
- await asyncio.to_thread(_ios_clear)
314
- elif IS_ANDROID:
315
- await asyncio.to_thread(_android_clear)
316
- else:
317
- await asyncio.to_thread(_desktop_clear)
92
+ _storage().call("clear")
318
93
 
319
94
  @staticmethod
320
95
  async def get_json(key: str) -> Any: