pythonnative 0.40.0__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. pythonnative/__init__.py +411 -0
  2. pythonnative/_ios_log.py +92 -0
  3. pythonnative/_yoga.cp313-win_amd64.pyd +0 -0
  4. pythonnative/alerts.py +236 -0
  5. pythonnative/animated.py +1687 -0
  6. pythonnative/animation_graph.py +78 -0
  7. pythonnative/appearance.py +124 -0
  8. pythonnative/bootstrap.py +100 -0
  9. pythonnative/bridge/__init__.py +302 -0
  10. pythonnative/bridge/android.py +78 -0
  11. pythonnative/bridge/codec.py +170 -0
  12. pythonnative/bridge/commits.py +114 -0
  13. pythonnative/bridge/fake.py +243 -0
  14. pythonnative/bridge/ios.py +138 -0
  15. pythonnative/bridge/web.py +489 -0
  16. pythonnative/cli/__init__.py +7 -0
  17. pythonnative/cli/pn.py +1214 -0
  18. pythonnative/component.py +271 -0
  19. pythonnative/components/__init__.py +111 -0
  20. pythonnative/components/_base.py +166 -0
  21. pythonnative/components/controls.py +521 -0
  22. pythonnative/components/layout.py +518 -0
  23. pythonnative/components/lists.py +785 -0
  24. pythonnative/components/media.py +204 -0
  25. pythonnative/components/overlays.py +107 -0
  26. pythonnative/components/pressable.py +252 -0
  27. pythonnative/components/structural.py +167 -0
  28. pythonnative/components/text.py +291 -0
  29. pythonnative/devclient.py +824 -0
  30. pythonnative/devserver/__init__.py +34 -0
  31. pythonnative/devserver/server.py +718 -0
  32. pythonnative/devserver/static/animation_graph.js +82 -0
  33. pythonnative/devserver/static/bridge.js +147 -0
  34. pythonnative/devserver/static/colors.js +70 -0
  35. pythonnative/devserver/static/host.js +552 -0
  36. pythonnative/devserver/static/index.html +60 -0
  37. pythonnative/devserver/static/layout.js +85 -0
  38. pythonnative/devserver/static/preview.css +415 -0
  39. pythonnative/devserver/static/renderer.js +1994 -0
  40. pythonnative/devserver/static/shell.js +345 -0
  41. pythonnative/devserver/static/yoga/LICENSE +21 -0
  42. pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
  43. pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
  44. pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
  45. pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
  46. pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
  47. pythonnative/devserver/static/yoga/src/index.js +16 -0
  48. pythonnative/devserver/static/yoga/src/index.js.map +1 -0
  49. pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
  50. pythonnative/devserver/static/yoga/src/load.js +17 -0
  51. pythonnative/devserver/static/yoga/src/load.js.map +1 -0
  52. pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
  53. pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
  54. pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
  55. pythonnative/devserver/watcher.py +267 -0
  56. pythonnative/devserver/ws.py +421 -0
  57. pythonnative/diagnostics.py +241 -0
  58. pythonnative/element.py +159 -0
  59. pythonnative/equality.py +19 -0
  60. pythonnative/events.py +231 -0
  61. pythonnative/gestures.py +1333 -0
  62. pythonnative/hooks.py +1621 -0
  63. pythonnative/hosts/__init__.py +63 -0
  64. pythonnative/hosts/base.py +596 -0
  65. pythonnative/hosts/native.py +302 -0
  66. pythonnative/hot_reload.py +585 -0
  67. pythonnative/layout.py +328 -0
  68. pythonnative/mutations.py +142 -0
  69. pythonnative/native/android/build.gradle +52 -0
  70. pythonnative/native/android/gradle.properties +3 -0
  71. pythonnative/native/android/settings.gradle +12 -0
  72. pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
  73. pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
  74. pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
  75. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
  76. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
  77. pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
  78. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
  79. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  80. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
  81. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  82. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
  83. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
  84. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  85. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
  86. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  87. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  88. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  89. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
  90. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  91. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
  92. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
  93. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  94. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  95. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  96. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  97. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  98. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
  99. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  100. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  101. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
  102. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  103. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  104. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
  105. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  106. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  107. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  108. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  109. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
  110. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  111. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  112. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
  113. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  114. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  115. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  116. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  117. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
  118. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
  119. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  120. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  121. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
  122. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  123. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
  124. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  125. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  126. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  127. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  128. pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  129. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
  130. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  131. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
  132. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
  133. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  134. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
  135. pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  136. pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  137. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  138. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  139. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
  140. pythonnative/native/ios/Package.swift +23 -0
  141. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
  142. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
  143. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
  144. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
  145. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
  146. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
  147. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  148. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
  149. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  150. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  151. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  152. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  153. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
  154. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  155. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  156. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
  157. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  158. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  159. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
  160. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  161. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  162. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  163. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  164. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  165. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
  166. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  167. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
  168. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
  169. pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
  170. pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  171. pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
  172. pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  173. pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
  174. pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  175. pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  176. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
  177. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  178. pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  179. pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  180. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  181. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
  182. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  183. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  184. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  185. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  186. pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
  187. pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  188. pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
  189. pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  190. pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
  191. pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
  192. pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  193. pythonnative/native/yoga/LICENSE +21 -0
  194. pythonnative/native/yoga/Package.swift +9 -0
  195. pythonnative/native/yoga/VENDORED.md +2 -0
  196. pythonnative/native/yoga/include/PNStyle.h +10 -0
  197. pythonnative/native/yoga/include/module.modulemap +5 -0
  198. pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
  199. pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
  200. pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
  201. pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
  202. pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
  203. pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
  204. pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
  205. pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
  206. pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
  207. pythonnative/native/yoga/python.cpp +16 -0
  208. pythonnative/native/yoga/style.cpp +272 -0
  209. pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
  210. pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
  211. pythonnative/native/yoga/yoga/YGConfig.h +158 -0
  212. pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
  213. pythonnative/native/yoga/yoga/YGEnums.h +142 -0
  214. pythonnative/native/yoga/yoga/YGMacros.h +99 -0
  215. pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
  216. pythonnative/native/yoga/yoga/YGNode.h +304 -0
  217. pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
  218. pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
  219. pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
  220. pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
  221. pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
  222. pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
  223. pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
  224. pythonnative/native/yoga/yoga/YGValue.h +84 -0
  225. pythonnative/native/yoga/yoga/Yoga.h +21 -0
  226. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
  227. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
  228. pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
  229. pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
  230. pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
  231. pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
  232. pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
  233. pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
  234. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
  235. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
  236. pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
  237. pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
  238. pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
  239. pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
  240. pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
  241. pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
  242. pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
  243. pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
  244. pythonnative/native/yoga/yoga/config/Config.h +92 -0
  245. pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
  246. pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
  247. pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
  248. pythonnative/native/yoga/yoga/debug/Log.h +34 -0
  249. pythonnative/native/yoga/yoga/enums/Align.h +47 -0
  250. pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
  251. pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
  252. pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
  253. pythonnative/native/yoga/yoga/enums/Display.h +41 -0
  254. pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
  255. pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
  256. pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
  257. pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
  258. pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
  259. pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
  260. pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
  261. pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
  262. pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
  263. pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
  264. pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
  265. pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
  266. pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
  267. pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
  268. pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
  269. pythonnative/native/yoga/yoga/event/event.cpp +87 -0
  270. pythonnative/native/yoga/yoga/event/event.h +130 -0
  271. pythonnative/native/yoga/yoga/module.modulemap +21 -0
  272. pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
  273. pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
  274. pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
  275. pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
  276. pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
  277. pythonnative/native/yoga/yoga/node/Node.h +337 -0
  278. pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
  279. pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
  280. pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
  281. pythonnative/native/yoga/yoga/style/Style.h +757 -0
  282. pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
  283. pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
  284. pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
  285. pythonnative/native_modules/__init__.py +124 -0
  286. pythonnative/native_modules/app_state.py +99 -0
  287. pythonnative/native_modules/battery.py +75 -0
  288. pythonnative/native_modules/biometrics.py +42 -0
  289. pythonnative/native_modules/camera.py +65 -0
  290. pythonnative/native_modules/clipboard.py +46 -0
  291. pythonnative/native_modules/fallback.py +324 -0
  292. pythonnative/native_modules/file_system.py +145 -0
  293. pythonnative/native_modules/haptics.py +75 -0
  294. pythonnative/native_modules/linking.py +123 -0
  295. pythonnative/native_modules/location.py +75 -0
  296. pythonnative/native_modules/net_info.py +112 -0
  297. pythonnative/native_modules/notifications.py +129 -0
  298. pythonnative/native_modules/permissions.py +108 -0
  299. pythonnative/native_modules/registry.py +526 -0
  300. pythonnative/native_modules/secure_store.py +52 -0
  301. pythonnative/native_modules/share.py +51 -0
  302. pythonnative/native_views/__init__.py +375 -0
  303. pythonnative/native_views/base.py +316 -0
  304. pythonnative/native_views/bridge_backend.py +277 -0
  305. pythonnative/navigation/__init__.py +103 -0
  306. pythonnative/navigation/container.py +154 -0
  307. pythonnative/navigation/handle.py +540 -0
  308. pythonnative/navigation/hooks.py +133 -0
  309. pythonnative/navigation/host.py +58 -0
  310. pythonnative/navigation/linking.py +200 -0
  311. pythonnative/navigation/navigators.py +659 -0
  312. pythonnative/navigation/screen.py +149 -0
  313. pythonnative/navigation/state.py +269 -0
  314. pythonnative/net.py +248 -0
  315. pythonnative/platform.py +166 -0
  316. pythonnative/platform_metrics.py +252 -0
  317. pythonnative/preview.py +287 -0
  318. pythonnative/profiling.py +101 -0
  319. pythonnative/project/__init__.py +68 -0
  320. pythonnative/project/android.py +557 -0
  321. pythonnative/project/builder.py +817 -0
  322. pythonnative/project/config.py +773 -0
  323. pythonnative/project/deps.py +719 -0
  324. pythonnative/project/devices.py +304 -0
  325. pythonnative/project/doctor.py +283 -0
  326. pythonnative/project/fingerprint.py +171 -0
  327. pythonnative/project/icons.py +247 -0
  328. pythonnative/project/ios.py +325 -0
  329. pythonnative/project/lockfile.py +100 -0
  330. pythonnative/project/permissions.py +361 -0
  331. pythonnative/project/plugins.py +519 -0
  332. pythonnative/project/runtime_assets.py +183 -0
  333. pythonnative/py.typed +0 -0
  334. pythonnative/query.py +153 -0
  335. pythonnative/reconciler/__init__.py +29 -0
  336. pythonnative/reconciler/boundaries.py +375 -0
  337. pythonnative/reconciler/children.py +88 -0
  338. pythonnative/reconciler/core.py +1165 -0
  339. pythonnative/reconciler/layout_pass.py +393 -0
  340. pythonnative/reconciler/vnode.py +266 -0
  341. pythonnative/refresh.py +55 -0
  342. pythonnative/runtime.py +306 -0
  343. pythonnative/scheduler.py +159 -0
  344. pythonnative/sdk/__init__.py +153 -0
  345. pythonnative/sdk/_components.py +427 -0
  346. pythonnative/sdk/builtins.py +91 -0
  347. pythonnative/sdk/codegen.py +149 -0
  348. pythonnative/sdk/module_codegen.py +184 -0
  349. pythonnative/sdk/schema.py +224 -0
  350. pythonnative/sdk/templates/contracts.kt +46 -0
  351. pythonnative/sdk/templates/contracts.swift +46 -0
  352. pythonnative/storage.py +184 -0
  353. pythonnative/style.py +841 -0
  354. pythonnative/suspense.py +183 -0
  355. pythonnative/templates/android_template/app/build.gradle +75 -0
  356. pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
  357. pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
  358. pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
  359. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
  360. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
  361. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
  362. pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  363. pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
  364. pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
  365. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  366. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  367. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  368. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  369. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  370. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  371. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  372. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  373. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  374. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  375. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  376. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  377. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
  378. pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
  379. pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
  380. pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
  381. pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
  382. pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
  383. pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
  384. pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
  385. pythonnative/templates/android_template/build.gradle +10 -0
  386. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  387. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
  388. pythonnative/templates/android_template/gradle.properties +23 -0
  389. pythonnative/templates/android_template/gradlew +185 -0
  390. pythonnative/templates/android_template/gradlew.bat +89 -0
  391. pythonnative/templates/android_template/settings.gradle +17 -0
  392. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
  393. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  394. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  395. pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
  396. pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
  397. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  398. pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
  399. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
  400. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
  401. pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
  402. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
  403. pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  404. pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
  405. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
  406. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
  407. pythonnative/testing/__init__.py +53 -0
  408. pythonnative/testing/backend.py +276 -0
  409. pythonnative/testing/harness.py +369 -0
  410. pythonnative/utils.py +145 -0
  411. pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
  412. pythonnative-0.40.0.dist-info/METADATA +150 -0
  413. pythonnative-0.40.0.dist-info/RECORD +418 -0
  414. pythonnative-0.40.0.dist-info/WHEEL +5 -0
  415. pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
  416. pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
  417. pythonnative-0.40.0.dist-info/top_level.txt +1 -0
  418. pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
pythonnative/cli/pn.py ADDED
@@ -0,0 +1,1214 @@
1
+ """`pn` CLI: scaffold, diagnose, start, run, and build PythonNative apps.
2
+
3
+ The console script `pn` (declared in `pyproject.toml`) dispatches to:
4
+
5
+ - `pn init [name]`: scaffold a new project (``pythonnative.toml`` +
6
+ ``app/``) into ``./name/``, or into the current directory when no name
7
+ is given.
8
+ - `pn doctor [platform]`: diagnose the local toolchain and config.
9
+ - `pn deps [platform]`: resolve ``[requirements].packages`` for every
10
+ device target and report which wheels would be used (or why a package
11
+ can't be installed), without building anything.
12
+ - `pn start`: run the dev server. It renders the app in a browser tab
13
+ and syncs every save to every connected debug build (simulators,
14
+ emulators, physical devices) with Fast Refresh; device logs stream
15
+ back into the same terminal.
16
+ - `pn preview`: `pn start` plus opening the browser preview.
17
+ - `pn devices [platform]`: list connected devices, emulators, and
18
+ simulators, as a table or as JSON with `--json`.
19
+ - `pn run android|ios [--device D]`: stage + build + install + launch a
20
+ debug build that connects to the dev server. The native project is
21
+ only rebuilt when something outside ``app/`` changed.
22
+ - `pn logs android|ios [--device D]`: stream logs from the running app
23
+ without rebuilding.
24
+ - `pn build android|ios`: produce standalone artifacts (signed APK/AAB,
25
+ or an iOS archive/IPA, optionally uploaded to App Store Connect).
26
+ - `pn app-id android|ios`: print the resolved application/bundle id
27
+ (handy for scripts and CI).
28
+ - `pn clean`: remove the local `build/` directory.
29
+
30
+ The heavy lifting lives in the ``pythonnative.project`` and
31
+ ``pythonnative.devserver`` packages; this module is a thin,
32
+ side-effect-y shell that wires arguments to them and handles the
33
+ device-facing steps (simulator boot, launch, log streaming) that can't
34
+ be unit tested.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import argparse
40
+ import dataclasses
41
+ import json
42
+ import os
43
+ import re
44
+ import shutil
45
+ import subprocess
46
+ import sys
47
+ from importlib.metadata import version as pkg_version
48
+ from pathlib import Path
49
+ from typing import Any, Dict, List, Optional, Sequence, TextIO
50
+ from urllib.request import urlopen
51
+
52
+ from ..project import builder as builder_mod
53
+ from ..project import deps as deps_mod
54
+ from ..project import devices as devices_mod
55
+ from ..project import doctor as doctor_mod
56
+ from ..project import fingerprint as fingerprint_mod
57
+ from ..project.android import collect_logcat_filters
58
+ from ..project.config import CONFIG_FILENAME, AppConfig, ConfigError, render_default_toml
59
+
60
+ DEFAULT_DEV_PORT = 8765
61
+ """Port `pn start` listens on unless `--port` says otherwise."""
62
+
63
+
64
+ # ======================================================================
65
+ # init
66
+ # ======================================================================
67
+
68
+ _MAIN_TEMPLATE = """from typing import TypedDict
69
+
70
+ import pythonnative as pn
71
+
72
+ Stack = pn.create_stack_navigator()
73
+
74
+
75
+ class DetailParams(TypedDict):
76
+ count: int
77
+
78
+
79
+ @pn.component
80
+ def HomeScreen():
81
+ count, set_count = pn.use_state(0)
82
+ nav = pn.use_navigation()
83
+ theme = pn.use_theme()
84
+ return pn.ScrollView(
85
+ pn.Column(
86
+ pn.Text("Hello from PythonNative!", style={"font_size": theme.font_size_title, "bold": True}),
87
+ pn.Text(f"Tapped {count} times"),
88
+ pn.Button("Tap me", on_press=lambda: set_count(count + 1)),
89
+ pn.Button("Open detail", on_press=lambda: nav.navigate("Detail", count=count)),
90
+ style={"spacing": theme.spacing_large, "padding": 16, "align_items": "stretch"},
91
+ )
92
+ )
93
+
94
+
95
+ @pn.component
96
+ def DetailScreen():
97
+ nav = pn.use_navigation()
98
+ route = pn.use_route(DetailParams)
99
+ return pn.Column(
100
+ pn.Text(f"Detail: count was {route.params['count']}", style={"font_size": 20}),
101
+ pn.Button("Back", on_press=nav.go_back),
102
+ style={"spacing": 12, "padding": 16},
103
+ )
104
+
105
+
106
+ @pn.component
107
+ def App():
108
+ return pn.NavigationContainer(
109
+ Stack.Navigator(
110
+ Stack.Screen("Home", HomeScreen, title="Home"),
111
+ Stack.Screen("Detail", DetailScreen, title="Detail"),
112
+ )
113
+ )
114
+ """
115
+
116
+ _GITIGNORE = "# PythonNative\n__pycache__/\n*.pyc\n.venv/\nbuild/\n.DS_Store\n"
117
+
118
+
119
+ _NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$")
120
+ """Legal ``pn init`` project names, in the spirit of ``flutter create`` / ``cargo new``."""
121
+
122
+ _FALLBACK_NAME = "my_app"
123
+
124
+
125
+ def _sanitize_name(name: str) -> str:
126
+ """Return a legal project name derived from ``name``.
127
+
128
+ Lowercases, collapses each run of illegal characters to one
129
+ underscore, trims leading and trailing ``_`` and ``-``, and prefixes
130
+ a name that doesn't start with a letter. The result always matches
131
+ ``_NAME_RE``, falling back to ``_FALLBACK_NAME`` when nothing usable
132
+ survives.
133
+
134
+ Args:
135
+ name: The rejected name, which may be empty.
136
+
137
+ Returns:
138
+ A name suitable for suggesting back to the user.
139
+ """
140
+ slug = re.sub(r"[^a-z0-9_-]+", "_", name.lower()).strip("_-")
141
+ if not slug:
142
+ return _FALLBACK_NAME
143
+ if not slug[0].isascii() or not slug[0].isalpha():
144
+ slug = f"app_{slug}"
145
+ return slug
146
+
147
+
148
+ def _app_id_from_name(name: str) -> str:
149
+ slug = re.sub(r"[^a-z0-9_]", "", name.lower())
150
+ if not slug or not slug[0].isalpha():
151
+ slug = "app" + slug
152
+ return f"com.example.{slug}"
153
+
154
+
155
+ def init_project(args: argparse.Namespace) -> None:
156
+ """Scaffold a new PythonNative project.
157
+
158
+ Given a name, this creates ``./<name>/`` and scaffolds into it. Without
159
+ one, it scaffolds into the current directory and names the project after
160
+ it. Either way it writes ``app/main.py``, ``pythonnative.toml``, and
161
+ ``.gitignore``.
162
+
163
+ A name you pass has to match ``^[a-z][a-z0-9_-]*$``: lowercase letters,
164
+ digits, ``-``, and ``_``, starting with a letter. Anything else is
165
+ refused with a legal suggestion. That keeps the directory name and the
166
+ ``name`` field in the generated config identical, in the same spirit as
167
+ ``flutter create`` and ``cargo new``. The name taken from the current
168
+ directory when you pass none is used as-is, so an existing directory
169
+ with any name still works.
170
+
171
+ The name also has to be a single directory name, so the project always
172
+ lands inside the current directory. Anything that reads as a path, such
173
+ as ``a/b``, ``..``, or ``/tmp/app``, is refused, and so is a name that
174
+ resolves somewhere else, such as a symlink to another directory.
175
+
176
+ It won't scaffold into a target directory that already holds files, and it
177
+ won't overwrite any of the three paths above; pass ``--force`` to override
178
+ both. An existing but empty target directory is fine. A plain file at
179
+ ``./<name>`` is always refused, since ``--force`` can't turn it into a
180
+ directory, and ``--force`` lifts neither of the rules above.
181
+
182
+ Args:
183
+ args: Parsed namespace with ``name`` (optional) and ``force``.
184
+ """
185
+ name: Optional[str] = getattr(args, "name", None)
186
+ force: bool = getattr(args, "force", False)
187
+
188
+ # Lexical check, before anything reads the filesystem. ``Path("..").name``
189
+ # is "..", so ".." needs naming explicitly; the rest (absolute, nested,
190
+ # trailing separator, ".") fall out of the name check.
191
+ if name and (name in (os.curdir, os.pardir) or Path(name).name != name):
192
+ print(f"Refusing to treat a path as a project name: {name!r}. Use a single directory name like my_app.")
193
+ sys.exit(1)
194
+
195
+ # Charset check, still lexical, so it stays ahead of ``Path.cwd()`` below.
196
+ # ``is not None`` rather than truthiness: "" is invalid under the pattern,
197
+ # and falling through to the no-name path would silently scaffold here.
198
+ # ``fullmatch``, not ``match``: ``$`` also matches before a trailing
199
+ # newline, so ``match`` would accept "app\n" and create a directory
200
+ # whose name contains one.
201
+ if name is not None and not _NAME_RE.fullmatch(name):
202
+ print(
203
+ f"Invalid project name: {name!r}. Use lowercase letters, digits, '-', and '_', "
204
+ f"starting with a letter. Try: {_sanitize_name(name)}"
205
+ )
206
+ sys.exit(1)
207
+
208
+ cwd = Path.cwd()
209
+ target = cwd / name if name else cwd
210
+ project_name: str = name or cwd.name
211
+
212
+ # A lexically clean name can still resolve elsewhere, and ``exists()`` and
213
+ # ``is_dir()`` below follow symlinks. Check containment rather than just
214
+ # ``is_symlink()`` so the whole class is closed, not one spelling of it.
215
+ if name and (target.is_symlink() or target.resolve().parent != cwd.resolve()):
216
+ print(
217
+ f"Refusing to scaffold through a link or outside the current directory: {name}. Use a plain directory name."
218
+ )
219
+ sys.exit(1)
220
+
221
+ app_dir = target / "app"
222
+ config_path = target / CONFIG_FILENAME
223
+ gitignore_path = target / ".gitignore"
224
+
225
+ if name and target.exists():
226
+ if not target.is_dir():
227
+ print(f"Refusing to overwrite existing file: {name}. Remove it or choose a different name.")
228
+ sys.exit(1)
229
+ if any(target.iterdir()) and not force:
230
+ print(f"Refusing to overwrite existing non-empty directory: {name}/. Use --force to overwrite.")
231
+ sys.exit(1)
232
+
233
+ if not force:
234
+ existing = [
235
+ label
236
+ for label, path in (("app/", app_dir), (CONFIG_FILENAME, config_path), (".gitignore", gitignore_path))
237
+ if path.exists()
238
+ ]
239
+ if existing:
240
+ print(f"Refusing to overwrite existing: {', '.join(existing)}. Use --force to overwrite.")
241
+ sys.exit(1)
242
+
243
+ app_dir.mkdir(parents=True, exist_ok=True)
244
+ main_py = app_dir / "main.py"
245
+ if force or not main_py.exists():
246
+ main_py.write_text(_MAIN_TEMPLATE, encoding="utf-8")
247
+
248
+ config_path.write_text(
249
+ render_default_toml(name=project_name, app_id=_app_id_from_name(project_name)),
250
+ encoding="utf-8",
251
+ )
252
+ if force or not gitignore_path.exists():
253
+ gitignore_path.write_text(_GITIGNORE, encoding="utf-8")
254
+
255
+ print(f"Initialized PythonNative project in {target}.")
256
+ next_steps = "pn start (browser preview + dev server) | pn run android | pn run ios"
257
+ if name:
258
+ next_steps = f"cd {name} | {next_steps}"
259
+ print(f"Next: {next_steps}")
260
+
261
+
262
+ # ======================================================================
263
+ # doctor / app-id
264
+ # ======================================================================
265
+
266
+
267
+ def doctor_command(args: argparse.Namespace) -> None:
268
+ """Run toolchain/config diagnostics and exit non-zero on errors.
269
+
270
+ Args:
271
+ args: Parsed namespace with optional ``platform``.
272
+ """
273
+ platform: Optional[str] = getattr(args, "platform", None)
274
+ results = doctor_mod.run_doctor(Path.cwd(), platform=platform)
275
+ print("PythonNative doctor\n")
276
+ for result in results:
277
+ print(result.format())
278
+ level = doctor_mod.worst_level(results)
279
+ print()
280
+ if level == doctor_mod.ERROR:
281
+ print("Found problems that will block builds. Address the [x] items above.")
282
+ sys.exit(1)
283
+ if level == doctor_mod.WARN:
284
+ print("Ready, with warnings. Review the [!] items above.")
285
+ else:
286
+ print("Everything looks good.")
287
+
288
+
289
+ def app_id_command(args: argparse.Namespace) -> None:
290
+ """Print the resolved application id (Android) or bundle id (iOS).
291
+
292
+ Args:
293
+ args: Parsed namespace with ``platform``.
294
+ """
295
+ config = _load_config_or_exit()
296
+ print(config.application_id if args.platform == "android" else config.bundle_id)
297
+
298
+
299
+ # ======================================================================
300
+ # deps
301
+ # ======================================================================
302
+
303
+
304
+ def deps_command(args: argparse.Namespace) -> None:
305
+ """Report how ``[requirements].packages`` resolve for each device target.
306
+
307
+ Runs pip in its cross-platform dry-run mode once per target (iOS
308
+ device, iOS Simulator, and one per Android ABI) and prints the
309
+ wheel each package would use, flagging binary wheels and their
310
+ source index. Exits non-zero when any target can't be satisfied,
311
+ so it doubles as a CI gate. ``--json`` emits the same data as a
312
+ machine-readable document.
313
+
314
+ Args:
315
+ args: Parsed namespace with optional ``platform``, ``json``, and
316
+ ``python`` (the interpreter to run pip with).
317
+ """
318
+ platform: Optional[str] = getattr(args, "platform", None)
319
+ as_json: bool = getattr(args, "json", False)
320
+ python: Optional[str] = getattr(args, "python", None)
321
+
322
+ config = _load_config_or_exit()
323
+ targets = deps_mod.targets_for(config, platform, all_simulator_archs=getattr(args, "lock", False))
324
+ runner = builder_mod.SubprocessRunner()
325
+ if not as_json and config.requirements:
326
+ print(
327
+ f"Resolving {len(config.requirements)} requirement(s) for Python {config.python_version} "
328
+ f"across {len(targets)} target(s)...\n"
329
+ )
330
+ resolutions = deps_mod.resolve_all(config, targets, runner=runner, python=python)
331
+ if getattr(args, "lock", False):
332
+ from ..project.lockfile import write
333
+
334
+ path = write(config, resolutions)
335
+ if not as_json:
336
+ print(f"Wrote {path}")
337
+
338
+ if as_json:
339
+ print(
340
+ json.dumps(
341
+ {
342
+ "python_version": config.python_version,
343
+ "requirements": list(config.requirements),
344
+ "targets": [res.to_dict() for res in resolutions],
345
+ },
346
+ indent=2,
347
+ )
348
+ )
349
+ else:
350
+ print(deps_mod.format_report(resolutions, requirements=config.requirements))
351
+ if any(not res.ok for res in resolutions):
352
+ sys.exit(1)
353
+
354
+
355
+ # ======================================================================
356
+ # start / preview
357
+ # ======================================================================
358
+
359
+
360
+ def start_command(args: argparse.Namespace, *, open_browser: bool = False) -> None:
361
+ """Run the dev server (and the browser preview) for the current project.
362
+
363
+ Re-execs under ``PN_PLATFORM=web`` so every module binds to the
364
+ browser backend, then hands off to ``pythonnative.preview.serve``.
365
+
366
+ Args:
367
+ args: Parsed namespace (``entry``, ``host``, ``port``, ``open``,
368
+ ``no_open``).
369
+ open_browser: Open the preview page once the server is up
370
+ (``pn preview`` sets this; ``--open`` does too).
371
+ """
372
+ if os.environ.get("PN_PLATFORM") != "web":
373
+ try:
374
+ completed = subprocess.run(
375
+ [sys.executable, "-m", "pythonnative.cli.pn", *sys.argv[1:]],
376
+ env={**os.environ, "PN_PLATFORM": "web"},
377
+ )
378
+ except KeyboardInterrupt:
379
+ sys.exit(130)
380
+ sys.exit(completed.returncode)
381
+
382
+ project_dir = Path.cwd()
383
+ entry: Optional[str] = getattr(args, "entry", None)
384
+ project_name = ""
385
+ requirements: List[str] = []
386
+ try:
387
+ config = AppConfig.load(project_dir)
388
+ entry = entry or config.entry_module
389
+ project_name = config.name
390
+ requirements = list(config.requirements)
391
+ except ConfigError:
392
+ entry = entry or "app.main"
393
+ missing = _missing_requirements(requirements)
394
+ if missing:
395
+ print(f"Warning: {', '.join(missing)} from [requirements] is not installed in this Python environment.")
396
+ print(
397
+ " The browser preview imports your app here, so install it first (e.g. `pip install "
398
+ + " ".join(missing)
399
+ + "`)."
400
+ )
401
+ if not (project_dir / "app").is_dir():
402
+ print(f"Error: no app/ directory in {project_dir}. Run 'pn init' first or cd into a PythonNative project.")
403
+ sys.exit(1)
404
+
405
+ from pythonnative.preview import serve
406
+
407
+ open_browser = open_browser or bool(getattr(args, "open", False))
408
+ if getattr(args, "no_open", False):
409
+ open_browser = False
410
+ try:
411
+ serve(
412
+ entry,
413
+ project_root=str(project_dir),
414
+ host=getattr(args, "host", "0.0.0.0") or "0.0.0.0",
415
+ port=int(getattr(args, "port", DEFAULT_DEV_PORT) or 0),
416
+ project_name=project_name,
417
+ open_browser=open_browser,
418
+ )
419
+ except OSError as exc:
420
+ print(f"Error: could not start the dev server: {exc}")
421
+ print("Is another 'pn start' running? Pick a different port with --port.")
422
+ sys.exit(1)
423
+ except RuntimeError as exc:
424
+ print(f"Error: {exc}")
425
+ sys.exit(1)
426
+
427
+
428
+ def _missing_requirements(requirements: Sequence[str]) -> List[str]:
429
+ """Names from ``[requirements]`` that aren't installed in this interpreter.
430
+
431
+ Requirement strings may carry version specifiers or extras
432
+ (``"httpx[http2]>=0.27"``); only the distribution name is checked.
433
+ """
434
+ import importlib.metadata as metadata
435
+
436
+ missing: List[str] = []
437
+ for requirement in requirements:
438
+ name = re.split(r"[\s\[<>=!~;@]", requirement.strip(), maxsplit=1)[0]
439
+ if not name:
440
+ continue
441
+ try:
442
+ metadata.distribution(name)
443
+ except metadata.PackageNotFoundError:
444
+ missing.append(name)
445
+ return missing
446
+
447
+
448
+ def preview_command(args: argparse.Namespace) -> None:
449
+ """``pn preview``: ``pn start`` that also opens the browser preview."""
450
+ start_command(args, open_browser=True)
451
+
452
+
453
+ def _running_dev_server(port: int) -> Optional[Dict[str, Any]]:
454
+ """Return ``/status`` of a dev server on ``localhost:port``, or ``None``."""
455
+ try:
456
+ with urlopen(f"http://127.0.0.1:{port}/status", timeout=0.5) as response:
457
+ data = json.loads(response.read().decode("utf-8"))
458
+ except Exception:
459
+ return None
460
+ return data if isinstance(data, dict) else None
461
+
462
+
463
+ # ======================================================================
464
+ # devices
465
+ # ======================================================================
466
+
467
+
468
+ def _print_no_devices_hints(stream: TextIO) -> None:
469
+ """Print the "no devices" guidance to ``stream``.
470
+
471
+ Shared by both output modes so the wording can't drift between the
472
+ table (which sends it to stdout) and ``--json`` (stderr).
473
+
474
+ Args:
475
+ stream: Where to write, ``sys.stdout`` or ``sys.stderr``.
476
+ """
477
+ print("No devices found.", file=stream)
478
+ print("Android: start an emulator or connect a device with USB debugging enabled.", file=stream)
479
+ print("iOS: open Xcode once to install Simulators, or plug in a device.", file=stream)
480
+
481
+
482
+ def devices_command(args: argparse.Namespace) -> None:
483
+ """List connected devices, emulators, and simulators.
484
+
485
+ Prints an aligned table and exits 1 when nothing is connected.
486
+
487
+ With ``--json``, stdout carries a JSON array and nothing else, one
488
+ object per device (see ``Device.to_dict``), so it stays parseable.
489
+ The "no devices" hints go to stderr instead, an empty result prints
490
+ ``[]``, and the exit status is 0 either way, since "no devices" is a
491
+ valid answer for a script rather than a failure.
492
+
493
+ Args:
494
+ args: Parsed namespace with optional ``platform`` and ``json``.
495
+ """
496
+ platform: Optional[str] = getattr(args, "platform", None)
497
+ as_json: bool = getattr(args, "json", False)
498
+ devices = devices_mod.list_devices(platform)
499
+
500
+ if as_json:
501
+ if not devices:
502
+ _print_no_devices_hints(sys.stderr)
503
+ print(json.dumps([device.to_dict() for device in devices], indent=2))
504
+ return
505
+
506
+ if not devices:
507
+ _print_no_devices_hints(sys.stdout)
508
+ sys.exit(1)
509
+ print(f" {'IDENTIFIER':<40} {'KIND':<10} {'STATE':<10} NAME")
510
+ for device in devices:
511
+ print(device.format())
512
+ print("\nTarget one with: pn run <platform> --device <identifier or name>")
513
+
514
+
515
+ def _resolve_device(platform: str, query: Optional[str]) -> Optional[devices_mod.Device]:
516
+ """Resolve ``--device`` to a concrete target, exiting on a bad query."""
517
+ if not query:
518
+ return None
519
+ device = devices_mod.find_device(devices_mod.list_devices(platform), query)
520
+ if device is None:
521
+ print(f"Error: no {platform} device matches {query!r}. Run 'pn devices {platform}' to list targets.")
522
+ sys.exit(1)
523
+ return device
524
+
525
+
526
+ # ======================================================================
527
+ # run
528
+ # ======================================================================
529
+
530
+
531
+ def _dev_server_url_for(platform: str, device: Optional[devices_mod.Device], port: int) -> str:
532
+ """The WebSocket URL a launched app should use to reach ``pn start``.
533
+
534
+ Simulators share the host's loopback. Android emulators and USB
535
+ devices reach it through ``adb reverse`` (set up by the caller), so
536
+ ``localhost`` works for every Android target. A physical iOS device
537
+ is on the LAN, so it gets the first LAN address.
538
+ """
539
+ if platform == "ios" and device is not None and device.kind == "device":
540
+ from ..devserver import lan_addresses
541
+
542
+ for address in lan_addresses():
543
+ return f"ws://{address}:{port}/ws?role=client"
544
+ return f"ws://localhost:{port}/ws?role=client"
545
+
546
+
547
+ def run_project(args: argparse.Namespace) -> None:
548
+ """Stage, build, install, and launch a debug build that talks to ``pn start``.
549
+
550
+ The native toolchain only runs when a native input changed (see
551
+ ``pythonnative.project.fingerprint``) or when no dev server is up
552
+ to deliver the latest sources; otherwise the previous artifact is
553
+ reinstalled, which turns the edit/relaunch loop from minutes into
554
+ seconds.
555
+
556
+ Args:
557
+ args: Parsed namespace (``platform``, ``device``,
558
+ ``prepare_only``, ``no_logs``, ``rebuild``, ``dev_server``,
559
+ ``port``, ``dev_client``).
560
+ """
561
+ platform: str = args.platform
562
+ prepare_only: bool = getattr(args, "prepare_only", False)
563
+ show_logs: bool = not getattr(args, "no_logs", False)
564
+ force_rebuild: bool = getattr(args, "rebuild", False)
565
+ dev_client: bool = getattr(args, "dev_client", False)
566
+ port: int = int(getattr(args, "port", DEFAULT_DEV_PORT) or DEFAULT_DEV_PORT)
567
+ device = _resolve_device(platform, getattr(args, "device", None))
568
+
569
+ config = _load_config_or_exit()
570
+ if dev_client:
571
+ # A dev client is the same native app whose entry module is the
572
+ # connect screen; the real app arrives from the dev server.
573
+ config = dataclasses.replace(config, entry_point="pythonnative/devclient.py")
574
+ builder = builder_mod.Builder(config, log=print)
575
+
576
+ # Resolve third-party packages only for the destination being built
577
+ # (device wheels and Simulator wheels differ); prepare-only keeps both
578
+ # slices so the staged project builds for either in Xcode.
579
+ if prepare_only:
580
+ ios_sdks: tuple = deps_mod.IOS_SDKS
581
+ elif device is not None and device.kind == "device":
582
+ ios_sdks = ("iphoneos",)
583
+ else:
584
+ ios_sdks = ("iphonesimulator",)
585
+
586
+ explicit_server: Optional[str] = getattr(args, "dev_server", None)
587
+ status = _running_dev_server(port) if not explicit_server else None
588
+ if explicit_server:
589
+ server_url: Optional[str] = explicit_server
590
+ elif status is not None:
591
+ server_url = _dev_server_url_for(platform, device, int(status.get("port") or port))
592
+ else:
593
+ server_url = None
594
+ if not prepare_only:
595
+ print(
596
+ f"Note: no dev server on port {port}. Run 'pn start' in another terminal and relaunch, or the "
597
+ "app will run its bundled sources without Fast Refresh."
598
+ )
599
+
600
+ fingerprint = _native_fingerprint(config, platform, builder, ios_sdks=ios_sdks, dev_client=dev_client)
601
+ build_dir = builder.build_root / platform
602
+ stamp = fingerprint_mod.read_stamp(build_dir)
603
+ artifact = Path(stamp["artifact"]) if stamp and stamp.get("artifact") else None
604
+ reuse = (
605
+ not prepare_only
606
+ and not force_rebuild
607
+ and server_url is not None
608
+ and stamp is not None
609
+ and stamp.get("fingerprint") == fingerprint
610
+ and artifact is not None
611
+ and artifact.exists()
612
+ )
613
+
614
+ prepared: Optional[builder_mod.PreparedProject] = None
615
+ if reuse:
616
+ print(f"Native inputs unchanged; reinstalling {artifact} (use --rebuild to force a build).")
617
+ else:
618
+ try:
619
+ prepared = builder.prepare(platform, ios_sdks=ios_sdks)
620
+ except builder_mod.BuildError as exc:
621
+ print(f"Error: {exc}")
622
+ sys.exit(1)
623
+ if prepare_only:
624
+ print(f"Prepared {platform} project in {prepared.project_dir} (prepare-only).")
625
+ return
626
+
627
+ app_id = config.application_id if platform == "android" else config.bundle_id
628
+ if server_url:
629
+ print(f"Dev server: {server_url} (saves in app/ apply with Fast Refresh).")
630
+ try:
631
+ if platform == "android":
632
+ artifact = _run_android(
633
+ builder, prepared, artifact=artifact, app_id=app_id, device=device, server_url=server_url, port=port
634
+ )
635
+ elif device is not None and device.kind == "device":
636
+ artifact = _run_ios_device(
637
+ builder, prepared, artifact=artifact, app_id=app_id, device=device, server_url=server_url
638
+ )
639
+ else:
640
+ artifact = _run_ios_simulator(
641
+ builder,
642
+ prepared,
643
+ artifact=artifact,
644
+ app_id=app_id,
645
+ device=device,
646
+ server_url=server_url,
647
+ show_logs=show_logs,
648
+ )
649
+ except builder_mod.BuildError as exc:
650
+ print(f"Error: {exc}")
651
+ sys.exit(1)
652
+ if artifact is not None and not reuse:
653
+ fingerprint_mod.write_stamp(build_dir, fingerprint, artifact=artifact)
654
+
655
+ if show_logs and platform == "android":
656
+ _stream_logs_until_interrupt(platform, app_id, device)
657
+
658
+
659
+ def _native_fingerprint(
660
+ config: AppConfig,
661
+ platform: str,
662
+ builder: builder_mod.Builder,
663
+ *,
664
+ ios_sdks: tuple,
665
+ dev_client: bool,
666
+ ) -> str:
667
+ return fingerprint_mod.compute(
668
+ config,
669
+ platform,
670
+ template_root=builder_mod.template_source(platform),
671
+ lib_root=builder.dev_lib_root,
672
+ ios_sdks=ios_sdks,
673
+ extra={"dev_client": "1" if dev_client else "0"},
674
+ )
675
+
676
+
677
+ def _run_android(
678
+ builder: builder_mod.Builder,
679
+ prepared: Optional[builder_mod.PreparedProject],
680
+ *,
681
+ artifact: Optional[Path],
682
+ app_id: str,
683
+ device: Optional[devices_mod.Device],
684
+ server_url: Optional[str],
685
+ port: int,
686
+ ) -> Optional[Path]:
687
+ if device is not None:
688
+ # Both Gradle's install task and every adb call below honor
689
+ # ANDROID_SERIAL, so exporting it targets the whole run.
690
+ os.environ["ANDROID_SERIAL"] = device.identifier
691
+ if prepared is not None:
692
+ builder.install_android_debug(prepared)
693
+ artifact = builder.android_debug_apk(prepared)
694
+ elif artifact is not None:
695
+ install = subprocess.run(["adb", "install", "-r", str(artifact)], check=False)
696
+ if install.returncode != 0:
697
+ raise builder_mod.BuildError("adb install failed; run again with --rebuild.")
698
+ if server_url and "localhost" in server_url:
699
+ # Emulators and USB devices reach the host through adb's reverse tunnel.
700
+ subprocess.run(["adb", "reverse", f"tcp:{port}", f"tcp:{port}"], check=False, capture_output=True)
701
+ command = ["adb", "shell", "am", "start", "-n", f"{app_id}/.MainActivity"]
702
+ if server_url:
703
+ command += ["--es", "pn_dev_server", server_url]
704
+ subprocess.run(command, check=True)
705
+ return artifact
706
+
707
+
708
+ def _run_ios_simulator(
709
+ builder: builder_mod.Builder,
710
+ prepared: Optional[builder_mod.PreparedProject],
711
+ *,
712
+ artifact: Optional[Path],
713
+ app_id: str,
714
+ device: Optional[devices_mod.Device],
715
+ server_url: Optional[str],
716
+ show_logs: bool,
717
+ ) -> Optional[Path]:
718
+ if prepared is not None:
719
+ artifact = builder.build_ios_simulator(prepared)
720
+ if artifact is None:
721
+ raise builder_mod.BuildError("No simulator build to install; run again with --rebuild.")
722
+ udid = device.identifier if device is not None else _select_ios_simulator()
723
+ if udid is None:
724
+ print("No available iOS Simulators found; open the project in Xcode to run.")
725
+ return artifact
726
+ subprocess.run(["xcrun", "simctl", "boot", udid], check=False, capture_output=True)
727
+ subprocess.run(["xcrun", "simctl", "install", udid, str(artifact)], check=False)
728
+ env = {**os.environ, "SIMCTL_CHILD_PYTHONUNBUFFERED": "1"}
729
+ if server_url:
730
+ env["SIMCTL_CHILD_PN_DEV_SERVER"] = server_url
731
+ command = ["xcrun", "simctl", "launch", "--terminate-running-process"]
732
+ if show_logs:
733
+ # A console PTY streams Python's stdout here; the launch blocks.
734
+ command.append("--console-pty")
735
+ command += [udid, app_id]
736
+ if not show_logs:
737
+ subprocess.run(command, env=env, check=False)
738
+ print("Launched iOS app on Simulator.")
739
+ return artifact
740
+ print("Launched iOS app on Simulator. Streaming logs (Ctrl+C to stop)...")
741
+ try:
742
+ subprocess.run(command, env=env, check=False)
743
+ except KeyboardInterrupt:
744
+ print()
745
+ subprocess.run(["xcrun", "simctl", "terminate", udid, app_id], check=False, capture_output=True)
746
+ print("Stopped log streaming.")
747
+ return artifact
748
+
749
+
750
+ def _run_ios_device(
751
+ builder: builder_mod.Builder,
752
+ prepared: Optional[builder_mod.PreparedProject],
753
+ *,
754
+ artifact: Optional[Path],
755
+ app_id: str,
756
+ device: devices_mod.Device,
757
+ server_url: Optional[str],
758
+ ) -> Optional[Path]:
759
+ """Build, install, and launch on a physical iOS device via devicectl."""
760
+ if prepared is not None:
761
+ artifact = builder.build_ios_device(prepared)
762
+ if artifact is None:
763
+ raise builder_mod.BuildError("No device build to install; run again with --rebuild.")
764
+ print(f"Installing on {device.name}...")
765
+ install = subprocess.run(
766
+ ["xcrun", "devicectl", "device", "install", "app", "--device", device.identifier, str(artifact)],
767
+ check=False,
768
+ )
769
+ if install.returncode != 0:
770
+ print(
771
+ "Error: install failed. Make sure the device is unlocked, paired with this Mac, "
772
+ "and has Developer Mode enabled (Settings > Privacy & Security > Developer Mode)."
773
+ )
774
+ sys.exit(1)
775
+ command = ["xcrun", "devicectl", "device", "process", "launch", "--terminate-existing"]
776
+ if server_url:
777
+ command += ["--environment-variables", json.dumps({"PN_DEV_SERVER": server_url})]
778
+ command += ["--device", device.identifier, app_id]
779
+ launch = subprocess.run(command, check=False)
780
+ if launch.returncode != 0:
781
+ print("Error: launch failed. Launch the app from the home screen to see details.")
782
+ sys.exit(1)
783
+ print(f"Launched on {device.name}. Logs stream to the 'pn start' terminal (or Console.app).")
784
+ return artifact
785
+
786
+
787
+ def _stream_logs_until_interrupt(platform: str, app_id: str, device: Optional[devices_mod.Device]) -> None:
788
+ """Tail the app's stdout on a simulator/emulator until Ctrl+C."""
789
+ if platform == "android":
790
+ proc = _start_android_log_stream()
791
+ else:
792
+ udid = device.identifier if device is not None else None
793
+ proc = _start_ios_log_stream(app_id, udid=udid)
794
+ if proc is None:
795
+ return
796
+ try:
797
+ proc.wait()
798
+ except KeyboardInterrupt:
799
+ print()
800
+ _terminate_subprocess(proc)
801
+ print("Stopped log streaming.")
802
+
803
+
804
+ # ======================================================================
805
+ # build
806
+ # ======================================================================
807
+
808
+
809
+ def build_project(args: argparse.Namespace) -> None:
810
+ """Build standalone, distributable artifacts for ``platform``.
811
+
812
+ Args:
813
+ args: Parsed namespace (``platform``, ``debug``, ``upload``).
814
+ """
815
+ platform: str = args.platform
816
+ debug: bool = getattr(args, "debug", False)
817
+ upload: bool = getattr(args, "upload", False)
818
+
819
+ config = _load_config_or_exit()
820
+ builder = builder_mod.Builder(config, log=print)
821
+
822
+ if upload and (platform != "ios" or debug):
823
+ print("Error: --upload applies to 'pn build ios' release builds only.")
824
+ sys.exit(1)
825
+ if upload and config.ios.signing.export_method != "app-store":
826
+ print('Error: --upload requires [ios.signing] export_method = "app-store" in pythonnative.toml.')
827
+ sys.exit(1)
828
+
829
+ try:
830
+ prepared = builder.prepare(
831
+ platform,
832
+ release=not debug,
833
+ ios_sdks=("iphonesimulator",) if debug else ("iphoneos",),
834
+ )
835
+ if platform == "android":
836
+ artifacts = builder.build_android(prepared, debug=debug)
837
+ else:
838
+ if debug:
839
+ app_path = builder.build_ios_simulator(prepared)
840
+ artifacts = builder_mod.BuildArtifacts(paths=[app_path])
841
+ else:
842
+ artifacts = builder.build_ios_archive(prepared, upload=upload)
843
+ except builder_mod.BuildError as exc:
844
+ print(f"Error: {exc}")
845
+ sys.exit(1)
846
+
847
+ if upload:
848
+ print("\nUploaded to App Store Connect. Check the build's status at appstoreconnect.apple.com.")
849
+ if not artifacts.paths:
850
+ if not upload:
851
+ print("Build completed, but no artifacts were found. Check the build output above.")
852
+ return
853
+ print("\nBuilt artifacts:")
854
+ for path in artifacts.paths:
855
+ print(f" {path}")
856
+
857
+
858
+ # ======================================================================
859
+ # logs
860
+ # ======================================================================
861
+
862
+
863
+ def logs_command(args: argparse.Namespace) -> None:
864
+ """Stream logs from the running app without rebuilding.
865
+
866
+ Args:
867
+ args: Parsed namespace (``platform``, ``device``).
868
+ """
869
+ platform: str = args.platform
870
+ device = _resolve_device(platform, getattr(args, "device", None))
871
+ if platform == "android":
872
+ if device is not None:
873
+ # Both adb and logcat below honor ANDROID_SERIAL, so exporting
874
+ # it targets the whole log stream at the chosen device.
875
+ os.environ["ANDROID_SERIAL"] = device.identifier
876
+ proc = _start_android_log_stream()
877
+ if proc is None:
878
+ sys.exit(1)
879
+ try:
880
+ proc.wait()
881
+ except KeyboardInterrupt:
882
+ print()
883
+ _terminate_subprocess(proc)
884
+ print("Stopped log streaming.")
885
+ return
886
+
887
+ # iOS: relaunch the app on the booted simulator with a console PTY so
888
+ # Python's stdout/stderr stream to this terminal.
889
+ if device is not None and device.kind == "device":
890
+ print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
891
+ sys.exit(1)
892
+ config = _load_config_or_exit()
893
+ udid = device.identifier if device is not None else None
894
+ proc = _start_ios_log_stream(config.bundle_id, udid=udid)
895
+ if proc is None:
896
+ print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
897
+ sys.exit(1)
898
+ try:
899
+ proc.wait()
900
+ except KeyboardInterrupt:
901
+ print()
902
+ _terminate_subprocess(proc)
903
+ print("Stopped log streaming.")
904
+
905
+
906
+ # ======================================================================
907
+ # clean
908
+ # ======================================================================
909
+
910
+
911
+ def clean_project(args: argparse.Namespace) -> None:
912
+ """Remove the local ``build/`` directory.
913
+
914
+ Args:
915
+ args: Parsed namespace (unused).
916
+ """
917
+ build_dir = Path.cwd() / "build"
918
+ if build_dir.exists():
919
+ shutil.rmtree(build_dir)
920
+ print("Removed build/ directory.")
921
+ else:
922
+ print("No build/ directory to remove.")
923
+
924
+
925
+ # ======================================================================
926
+ # Config helpers
927
+ # ======================================================================
928
+
929
+
930
+ def _load_config_or_exit(project_dir: Optional[Path] = None) -> AppConfig:
931
+ try:
932
+ return AppConfig.load(project_dir or Path.cwd())
933
+ except ConfigError as exc:
934
+ print(f"Error: {exc}")
935
+ sys.exit(1)
936
+
937
+
938
+ # ======================================================================
939
+ # Device log streaming
940
+ # ======================================================================
941
+
942
+
943
+ def _start_android_log_stream() -> Optional[subprocess.Popen]:
944
+ """Clear logcat and stream Python-relevant tags to the terminal.
945
+
946
+ Returns:
947
+ The ``adb logcat`` process, or ``None`` if ``adb`` is missing.
948
+ """
949
+ try:
950
+ subprocess.run(["adb", "logcat", "-c"], check=False, capture_output=True)
951
+ except FileNotFoundError:
952
+ print("Note: 'adb' not found on PATH; skipping log streaming.")
953
+ return None
954
+ try:
955
+ proc = subprocess.Popen(["adb", "logcat", *collect_logcat_filters()])
956
+ except FileNotFoundError:
957
+ return None
958
+ print("Streaming Python logs from device (Ctrl+C to stop)...")
959
+ return proc
960
+
961
+
962
+ def _booted_ios_udid() -> Optional[str]:
963
+ """Return a booted iOS Simulator's UDID, or ``None`` if none is booted."""
964
+ try:
965
+ result = subprocess.run(
966
+ ["xcrun", "simctl", "list", "devices", "booted", "--json"],
967
+ check=False,
968
+ capture_output=True,
969
+ text=True,
970
+ )
971
+ except FileNotFoundError:
972
+ return None
973
+ try:
974
+ data = json.loads(result.stdout or "{}")
975
+ except json.JSONDecodeError:
976
+ return None
977
+ for _runtime, devices in (data.get("devices") or {}).items():
978
+ for device in devices or []:
979
+ if device.get("state") == "Booted" and device.get("udid"):
980
+ return str(device["udid"])
981
+ return None
982
+
983
+
984
+ def _select_ios_simulator() -> Optional[str]:
985
+ """Return a simulator UDID to target (booted first, else an iPhone)."""
986
+ booted = _booted_ios_udid()
987
+ if booted:
988
+ return booted
989
+ try:
990
+ result = subprocess.run(
991
+ ["xcrun", "simctl", "list", "devices", "available", "--json"],
992
+ check=False,
993
+ capture_output=True,
994
+ text=True,
995
+ )
996
+ except FileNotFoundError:
997
+ return None
998
+ try:
999
+ data = json.loads(result.stdout or "{}")
1000
+ except json.JSONDecodeError:
1001
+ return None
1002
+ devices: List[Dict[str, Any]] = [d for lst in (data.get("devices") or {}).values() for d in (lst or [])]
1003
+ for device in devices:
1004
+ if "iphone 15" in (device.get("name") or "").lower() and device.get("isAvailable"):
1005
+ return device.get("udid")
1006
+ for device in devices:
1007
+ if device.get("isAvailable") and (device.get("name") or "").lower().startswith("iphone"):
1008
+ return device.get("udid")
1009
+ return None
1010
+
1011
+
1012
+ def _start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> Optional[subprocess.Popen]:
1013
+ """Re-launch the iOS app with a console PTY so its stdio streams here.
1014
+
1015
+ Args:
1016
+ bundle_id: The app's bundle identifier.
1017
+ udid: A specific simulator UDID to target. Falls back to the
1018
+ booted simulator when not given.
1019
+
1020
+ Returns:
1021
+ The launched process, or ``None`` when no simulator is booted.
1022
+ """
1023
+ if udid is None:
1024
+ udid = _booted_ios_udid()
1025
+ if udid is None:
1026
+ print("Note: no booted iOS Simulator found; skipping log streaming.")
1027
+ return None
1028
+ env = {**os.environ, "SIMCTL_CHILD_PYTHONUNBUFFERED": "1"}
1029
+ try:
1030
+ proc = subprocess.Popen(
1031
+ ["xcrun", "simctl", "launch", "--console-pty", "--terminate-running-process", udid, bundle_id],
1032
+ env=env,
1033
+ )
1034
+ except FileNotFoundError:
1035
+ print("Note: 'xcrun' not found on PATH; skipping iOS log streaming.")
1036
+ return None
1037
+ print("Streaming iOS app logs from the simulator (Ctrl+C to stop)...")
1038
+ return proc
1039
+
1040
+
1041
+ def _terminate_subprocess(proc: Optional[subprocess.Popen]) -> None:
1042
+ """Politely stop a subprocess, escalating to ``SIGKILL`` if needed."""
1043
+ if proc is None or proc.poll() is not None:
1044
+ return
1045
+ proc.terminate()
1046
+ try:
1047
+ proc.wait(timeout=3)
1048
+ except subprocess.TimeoutExpired:
1049
+ proc.kill()
1050
+
1051
+
1052
+ # ======================================================================
1053
+ # Argument parsing
1054
+ # ======================================================================
1055
+
1056
+
1057
+ def codegen_command(args: argparse.Namespace) -> None:
1058
+ """Generate native contracts after importing extension schema modules."""
1059
+ import importlib
1060
+
1061
+ from ..sdk.codegen import generate
1062
+
1063
+ for name in args.module:
1064
+ importlib.import_module(name)
1065
+ for path in generate(args.output):
1066
+ print(path)
1067
+
1068
+
1069
+ def _build_parser() -> argparse.ArgumentParser:
1070
+ parser = argparse.ArgumentParser(prog="pn", description="PythonNative CLI")
1071
+ parser.add_argument(
1072
+ "--version",
1073
+ "-V",
1074
+ action="version",
1075
+ version=f"pn {pkg_version('pythonnative')}",
1076
+ )
1077
+ subparsers = parser.add_subparsers()
1078
+
1079
+ parser_codegen = subparsers.add_parser("codegen", help="Generate typed native contracts")
1080
+ parser_codegen.add_argument("--output", type=Path, default=Path("generated"))
1081
+ parser_codegen.add_argument(
1082
+ "--module", action="append", default=[], help="Import a module declaring extension schemas"
1083
+ )
1084
+ parser_codegen.set_defaults(func=codegen_command)
1085
+
1086
+ parser_init = subparsers.add_parser("init", help="Scaffold a new project")
1087
+ parser_init.add_argument(
1088
+ "name",
1089
+ nargs="?",
1090
+ help="Project name, matching ^[a-z][a-z0-9_-]*$; creates ./<name>/ (default: current directory)",
1091
+ )
1092
+ parser_init.add_argument("--force", action="store_true", help="Overwrite existing files or a non-empty directory")
1093
+ parser_init.set_defaults(func=init_project)
1094
+
1095
+ parser_doctor = subparsers.add_parser("doctor", help="Diagnose the local toolchain and config")
1096
+ parser_doctor.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict checks to a platform")
1097
+ parser_doctor.set_defaults(func=doctor_command)
1098
+
1099
+ parser_deps = subparsers.add_parser(
1100
+ "deps", help="Check which wheels [requirements].packages resolve to on each device target"
1101
+ )
1102
+ parser_deps.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
1103
+ parser_deps.add_argument("--json", action="store_true", help="Print a JSON report for scripting")
1104
+ parser_deps.add_argument(
1105
+ "--python",
1106
+ help="Interpreter to run pip with (default: the one running pn; any version works, pip cross-resolves)",
1107
+ )
1108
+ parser_deps.add_argument(
1109
+ "--lock",
1110
+ action="store_true",
1111
+ help="Lock exact wheel versions and hashes, including both iOS Simulator architectures",
1112
+ )
1113
+ parser_deps.set_defaults(func=deps_command)
1114
+
1115
+ def _add_server_args(sub: argparse.ArgumentParser) -> None:
1116
+ sub.add_argument(
1117
+ "entry",
1118
+ nargs="?",
1119
+ help="Entry module (e.g. app.main); defaults to the project entry point",
1120
+ )
1121
+ sub.add_argument(
1122
+ "--port", type=int, default=DEFAULT_DEV_PORT, help=f"Port to listen on (default: {DEFAULT_DEV_PORT})"
1123
+ )
1124
+ sub.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0 so devices can connect)")
1125
+
1126
+ parser_start = subparsers.add_parser(
1127
+ "start", help="Run the dev server: browser preview + Fast Refresh for every connected debug build"
1128
+ )
1129
+ _add_server_args(parser_start)
1130
+ parser_start.add_argument("--open", action="store_true", help="Also open the browser preview")
1131
+ parser_start.set_defaults(func=start_command)
1132
+
1133
+ parser_preview = subparsers.add_parser("preview", help="Run the dev server and open the browser preview")
1134
+ _add_server_args(parser_preview)
1135
+ parser_preview.add_argument("--no-open", action="store_true", help="Don't open the browser automatically")
1136
+ parser_preview.set_defaults(func=preview_command)
1137
+
1138
+ parser_devices = subparsers.add_parser("devices", help="List devices, emulators, and simulators")
1139
+ parser_devices.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
1140
+ parser_devices.add_argument(
1141
+ "--json", action="store_true", help="Print a JSON array to stdout for scripting (hints go to stderr)"
1142
+ )
1143
+ parser_devices.set_defaults(func=devices_command)
1144
+
1145
+ parser_run = subparsers.add_parser("run", help="Build, install, and launch on a device/simulator")
1146
+ parser_run.add_argument("platform", choices=["android", "ios"])
1147
+ parser_run.add_argument(
1148
+ "--device",
1149
+ "-d",
1150
+ help="Target device: an identifier or name from 'pn devices' "
1151
+ "(physical iOS devices need [ios].development_team)",
1152
+ )
1153
+ parser_run.add_argument("--prepare-only", action="store_true", help="Stage + configure without building")
1154
+ parser_run.add_argument("--no-logs", action="store_true", help="Don't stream device logs after launch")
1155
+ parser_run.add_argument(
1156
+ "--rebuild", action="store_true", help="Run the native toolchain even when no native input changed"
1157
+ )
1158
+ parser_run.add_argument(
1159
+ "--dev-server",
1160
+ help="Dev server WebSocket URL for the app (default: the 'pn start' found on --port, via localhost/LAN)",
1161
+ )
1162
+ parser_run.add_argument(
1163
+ "--port", type=int, default=DEFAULT_DEV_PORT, help=f"Port 'pn start' listens on (default: {DEFAULT_DEV_PORT})"
1164
+ )
1165
+ parser_run.add_argument(
1166
+ "--dev-client",
1167
+ action="store_true",
1168
+ help="Build a dev client: a shell app that shows a connect screen and loads the app from any dev server",
1169
+ )
1170
+ parser_run.set_defaults(func=run_project)
1171
+
1172
+ parser_logs = subparsers.add_parser("logs", help="Stream logs from the running app")
1173
+ parser_logs.add_argument("platform", choices=["android", "ios"])
1174
+ parser_logs.add_argument(
1175
+ "--device",
1176
+ "-d",
1177
+ help="Target device: an identifier or name from 'pn devices' "
1178
+ "(physical iOS devices aren't supported for log streaming)",
1179
+ )
1180
+ parser_logs.set_defaults(func=logs_command)
1181
+
1182
+ parser_build = subparsers.add_parser("build", help="Build distributable artifacts")
1183
+ parser_build.add_argument("platform", choices=["android", "ios"])
1184
+ parser_build.add_argument("--debug", action="store_true", help="Build the debug variant instead of release")
1185
+ parser_build.add_argument(
1186
+ "--upload",
1187
+ action="store_true",
1188
+ help='Upload the iOS release build to App Store Connect (needs export_method = "app-store")',
1189
+ )
1190
+ parser_build.set_defaults(func=build_project)
1191
+
1192
+ parser_app_id = subparsers.add_parser("app-id", help="Print the resolved application/bundle id")
1193
+ parser_app_id.add_argument("platform", choices=["android", "ios"])
1194
+ parser_app_id.set_defaults(func=app_id_command)
1195
+
1196
+ parser_clean = subparsers.add_parser("clean", help="Remove the local build/ directory")
1197
+ parser_clean.set_defaults(func=clean_project)
1198
+
1199
+ return parser
1200
+
1201
+
1202
+ def main() -> None:
1203
+ """Entry point for the ``pn`` console script."""
1204
+ parser = _build_parser()
1205
+ args = parser.parse_args()
1206
+ func = getattr(args, "func", None)
1207
+ if func is None:
1208
+ parser.print_help()
1209
+ sys.exit(1)
1210
+ func(args)
1211
+
1212
+
1213
+ if __name__ == "__main__":
1214
+ main()