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
@@ -0,0 +1,718 @@
1
+ """The dev server: HTTP for static assets, WebSocket for live peers.
2
+
3
+ One [`DevServer`][pythonnative.devserver.server.DevServer] runs on its
4
+ own thread with a private ``asyncio`` loop, so it works both inside
5
+ ``pn start`` (whose main thread runs the browser preview's app) and in
6
+ tests. It exposes:
7
+
8
+ - ``GET /``: the browser preview page.
9
+ - ``GET /static/<name>``: preview assets (JS, CSS).
10
+ - ``GET /manifest``: ``{"version", "entry", "files": {path: sha256}}``.
11
+ - ``GET /file/<path>``: raw bytes of one synced source file.
12
+ - ``GET /status``: server, project, and connected-peer information.
13
+ - ``WS /ws?role=client``: the dev-client protocol (see below).
14
+ - ``WS /ws?role=preview``: the browser preview's bridge channel; the
15
+ server only relays text frames between the page and the
16
+ [`PreviewChannel`][pythonnative.devserver.server.PreviewChannel]
17
+ handler installed by the preview.
18
+
19
+ Dev-client protocol (JSON objects, one per text frame):
20
+
21
+ - client -> server ``hello``: ``{"type": "hello", "platform", "device",
22
+ "app", "files": {path: sha256}}`` describing what the client already
23
+ holds in its overlay.
24
+ - server -> client ``sync``: ``{"type": "sync", "version", "entry",
25
+ "files": [{"path", "sha256", "content", "encoding"}], "removed": [...]}``
26
+ bringing the client up to date. The same shape is sent as
27
+ ``"update"`` whenever the watcher sees a change.
28
+ - client -> server ``log`` (``level``, ``text``), ``error`` (``phase``,
29
+ ``text``), ``reloaded`` (``version``, ``mode``, ``modules``): streamed
30
+ to the terminal.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import asyncio
36
+ import base64
37
+ import json
38
+ import mimetypes
39
+ import os
40
+ import socket
41
+ import sys
42
+ import threading
43
+ import time
44
+ from dataclasses import dataclass, field
45
+ from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Protocol, Sequence
46
+ from urllib.parse import parse_qs, unquote, urlsplit
47
+
48
+ from . import ws
49
+ from .watcher import FileWatcher, SourceChange, SourceSnapshot, snapshot_sources
50
+
51
+ __all__ = [
52
+ "DEFAULT_PORT",
53
+ "DevServer",
54
+ "PreviewChannel",
55
+ "PreviewPeer",
56
+ "ServerInfo",
57
+ "lan_addresses",
58
+ ]
59
+
60
+ DEFAULT_PORT = 8765
61
+ """Default port for ``pn start`` / ``pn preview``."""
62
+
63
+ _MAX_HEAD = 64 * 1024
64
+ _STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
65
+
66
+ Logger = Callable[[str], None]
67
+
68
+
69
+ def lan_addresses() -> List[str]:
70
+ """Best-effort list of this machine's non-loopback IPv4 addresses.
71
+
72
+ Used to print a URL a physical device on the same Wi-Fi can reach.
73
+ """
74
+ found: List[str] = []
75
+ try:
76
+ probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77
+ try:
78
+ # No packets are sent; connecting a UDP socket just selects
79
+ # the interface that routes to the internet.
80
+ probe.connect(("10.255.255.255", 1))
81
+ found.append(probe.getsockname()[0])
82
+ finally:
83
+ probe.close()
84
+ except OSError:
85
+ pass
86
+ try:
87
+ for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
88
+ addr = str(info[4][0])
89
+ if not addr.startswith("127.") and addr not in found:
90
+ found.append(addr)
91
+ except OSError:
92
+ pass
93
+ return found
94
+
95
+
96
+ @dataclass
97
+ class ServerInfo:
98
+ """What the server is serving and where."""
99
+
100
+ host: str
101
+ port: int
102
+ project_root: str
103
+ entry_module: str
104
+ project_name: str = ""
105
+
106
+ def url(self, host: Optional[str] = None) -> str:
107
+ """The HTTP base URL, substituting ``host`` for the bind address."""
108
+ return f"http://{host or self.display_host}:{self.port}"
109
+
110
+ def ws_url(self, host: Optional[str] = None) -> str:
111
+ """The dev-client WebSocket URL."""
112
+ return f"ws://{host or self.display_host}:{self.port}/ws?role=client"
113
+
114
+ @property
115
+ def display_host(self) -> str:
116
+ """A host suitable for a URL (``0.0.0.0`` becomes ``localhost``)."""
117
+ return "localhost" if self.host in ("", "0.0.0.0", "::") else self.host
118
+
119
+
120
+ # ======================================================================
121
+ # Preview channel
122
+ # ======================================================================
123
+
124
+
125
+ class PreviewPeer:
126
+ """A connected browser preview page; ``send`` is safe from any thread."""
127
+
128
+ def __init__(self, server: "DevServer", writer: asyncio.StreamWriter) -> None:
129
+ self._server = server
130
+ self._writer = writer
131
+ self.closed = threading.Event()
132
+
133
+ def send(self, text: str) -> None:
134
+ """Queue one text frame to the page (dropped once the peer is closed)."""
135
+ if self.closed.is_set():
136
+ return
137
+ self._server._call_soon(self._send_now, text)
138
+
139
+ def _send_now(self, text: str) -> None:
140
+ if self.closed.is_set():
141
+ return
142
+ try:
143
+ self._writer.write(ws.encode_frame(ws.TEXT, text.encode("utf-8")))
144
+ except Exception:
145
+ self.closed.set()
146
+
147
+ def close(self) -> None:
148
+ """Ask the server to close this page's socket."""
149
+ self._server._call_soon(self._close_now)
150
+
151
+ def _close_now(self) -> None:
152
+ if self.closed.is_set():
153
+ return
154
+ self.closed.set()
155
+ try:
156
+ self._writer.write(ws.encode_close())
157
+ self._writer.close()
158
+ except Exception:
159
+ pass
160
+
161
+
162
+ class PreviewChannel(Protocol):
163
+ """What the preview installs to receive the page's bridge traffic.
164
+
165
+ Every method is called on the server thread; implementations hop
166
+ to their own thread as needed.
167
+ """
168
+
169
+ def on_preview_connected(self, peer: PreviewPeer, info: Dict[str, Any]) -> None:
170
+ """A page connected (``info`` carries its query parameters)."""
171
+
172
+ def on_preview_message(self, peer: PreviewPeer, text: str) -> None:
173
+ """A text frame arrived from the page."""
174
+
175
+ def on_preview_disconnected(self, peer: PreviewPeer) -> None:
176
+ """The page went away."""
177
+
178
+
179
+ # ======================================================================
180
+ # Dev clients
181
+ # ======================================================================
182
+
183
+
184
+ @dataclass
185
+ class DevClient:
186
+ """One connected on-device dev client."""
187
+
188
+ id: int
189
+ writer: asyncio.StreamWriter
190
+ platform: str = "unknown"
191
+ device: str = ""
192
+ app: str = ""
193
+ connected_at: float = field(default_factory=time.time)
194
+ files: Dict[str, str] = field(default_factory=dict)
195
+
196
+ def label(self) -> str:
197
+ """A short human label for log lines."""
198
+ parts = [self.platform]
199
+ if self.device:
200
+ parts.append(self.device)
201
+ return " ".join(parts)
202
+
203
+
204
+ # ======================================================================
205
+ # The server
206
+ # ======================================================================
207
+
208
+
209
+ class DevServer:
210
+ """Serve sources, assets, and the live protocol for one project.
211
+
212
+ Args:
213
+ project_root: Directory containing ``app/`` and ``pythonnative.toml``.
214
+ entry_module: The app's entry module (``"app.main"``).
215
+ host: Bind address. ``0.0.0.0`` so devices on the LAN can reach
216
+ it; pass ``127.0.0.1`` to stay local.
217
+ port: TCP port; ``0`` picks a free one.
218
+ project_name: Shown in the preview page and ``/status``.
219
+ static_dir: Where the preview page's assets live.
220
+ log: Where to print client logs and connection events.
221
+ watch: Whether to run the file watcher.
222
+ """
223
+
224
+ def __init__(
225
+ self,
226
+ project_root: str,
227
+ entry_module: str = "app.main",
228
+ *,
229
+ host: str = "0.0.0.0",
230
+ port: int = DEFAULT_PORT,
231
+ project_name: str = "",
232
+ static_dir: Optional[str] = None,
233
+ log: Optional[Logger] = None,
234
+ watch: bool = True,
235
+ ) -> None:
236
+ self.project_root = os.path.abspath(project_root)
237
+ self.entry_module = entry_module
238
+ self.project_name = project_name
239
+ self.static_dir = static_dir or _STATIC_DIR
240
+ self.log: Logger = log or (lambda line: print(line, file=sys.stderr, flush=True))
241
+ self._host = host
242
+ self._port = port
243
+ self._watch = watch
244
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
245
+ self._thread: Optional[threading.Thread] = None
246
+ self._server: Optional[asyncio.base_events.Server] = None
247
+ self._started = threading.Event()
248
+ self._start_error: Optional[BaseException] = None
249
+ self._clients: Dict[int, DevClient] = {}
250
+ self._client_ids = 0
251
+ self._preview: Optional[PreviewChannel] = None
252
+ self._preview_peer: Optional[PreviewPeer] = None
253
+ self._watcher: Optional[FileWatcher] = None
254
+ self._snapshot: SourceSnapshot = snapshot_sources(self.project_root)
255
+ self._snapshot_lock = threading.Lock()
256
+ self._change_listeners: List[Callable[[SourceChange, SourceSnapshot], None]] = []
257
+ self.info = ServerInfo(
258
+ host=host,
259
+ port=port,
260
+ project_root=self.project_root,
261
+ entry_module=entry_module,
262
+ project_name=project_name,
263
+ )
264
+
265
+ # -- lifecycle -------------------------------------------------------
266
+
267
+ def start(self) -> ServerInfo:
268
+ """Bind the socket on a background thread and return the address.
269
+
270
+ Raises:
271
+ OSError: When the port is taken (the CLI prints a hint).
272
+ """
273
+ if self._thread is not None:
274
+ return self.info
275
+ self._thread = threading.Thread(target=self._run, name="pn-dev-server", daemon=True)
276
+ self._thread.start()
277
+ self._started.wait(timeout=10.0)
278
+ if self._start_error is not None:
279
+ error, self._start_error = self._start_error, None
280
+ self._thread = None
281
+ raise error
282
+ if self._watch:
283
+ self._watcher = FileWatcher(self.project_root, self._on_files_changed)
284
+ with self._snapshot_lock:
285
+ self._snapshot = self._watcher.snapshot
286
+ self._watcher.start()
287
+ return self.info
288
+
289
+ def stop(self) -> None:
290
+ """Close every connection and stop the thread."""
291
+ watcher, self._watcher = self._watcher, None
292
+ if watcher is not None:
293
+ watcher.stop()
294
+ loop = self._loop
295
+ if loop is not None and not loop.is_closed():
296
+ loop.call_soon_threadsafe(self._shutdown)
297
+ thread, self._thread = self._thread, None
298
+ if thread is not None:
299
+ thread.join(timeout=5.0)
300
+
301
+ def _run(self) -> None:
302
+ loop = asyncio.new_event_loop()
303
+ self._loop = loop
304
+ asyncio.set_event_loop(loop)
305
+ try:
306
+ server = loop.run_until_complete(asyncio.start_server(self._handle_connection, self._host, self._port))
307
+ except BaseException as exc:
308
+ self._start_error = exc
309
+ self._started.set()
310
+ loop.close()
311
+ return
312
+ self._server = server
313
+ sockets: Sequence[Any] = server.sockets or []
314
+ if sockets:
315
+ self.info.port = int(sockets[0].getsockname()[1])
316
+ self._started.set()
317
+ try:
318
+ loop.run_forever()
319
+ finally:
320
+ try:
321
+ loop.run_until_complete(loop.shutdown_asyncgens())
322
+ except Exception:
323
+ pass
324
+ loop.close()
325
+
326
+ def _shutdown(self) -> None:
327
+ loop = self._loop
328
+ if self._server is not None:
329
+ self._server.close()
330
+ for client in list(self._clients.values()):
331
+ try:
332
+ client.writer.write(ws.encode_close())
333
+ client.writer.close()
334
+ except Exception:
335
+ pass
336
+ self._clients.clear()
337
+ if self._preview_peer is not None:
338
+ self._preview_peer._close_now()
339
+ if loop is not None:
340
+ loop.call_soon(loop.stop)
341
+
342
+ def _call_soon(self, fn: Callable[..., Any], *args: Any) -> None:
343
+ loop = self._loop
344
+ if loop is None or loop.is_closed():
345
+ return
346
+ try:
347
+ loop.call_soon_threadsafe(fn, *args)
348
+ except RuntimeError:
349
+ pass
350
+
351
+ # -- state -----------------------------------------------------------
352
+
353
+ @property
354
+ def snapshot(self) -> SourceSnapshot:
355
+ """The current source snapshot."""
356
+ with self._snapshot_lock:
357
+ return self._snapshot
358
+
359
+ @property
360
+ def clients(self) -> List[DevClient]:
361
+ """Connected dev clients (a copy)."""
362
+ return list(self._clients.values())
363
+
364
+ def set_preview_channel(self, channel: Optional[PreviewChannel]) -> None:
365
+ """Install the handler for the browser preview's bridge traffic."""
366
+ self._preview = channel
367
+
368
+ def add_change_listener(self, listener: Callable[[SourceChange, SourceSnapshot], None]) -> Callable[[], None]:
369
+ """Be told (on the watcher thread) when sources change; returns an unsubscribe."""
370
+ self._change_listeners.append(listener)
371
+
372
+ def _remove() -> None:
373
+ try:
374
+ self._change_listeners.remove(listener)
375
+ except ValueError:
376
+ pass
377
+
378
+ return _remove
379
+
380
+ def manifest(self) -> Dict[str, Any]:
381
+ """The ``/manifest`` document."""
382
+ snap = self.snapshot
383
+ return {"version": snap.version, "entry": self.entry_module, "files": dict(snap.files)}
384
+
385
+ def status(self) -> Dict[str, Any]:
386
+ """The ``/status`` document."""
387
+ snap = self.snapshot
388
+ return {
389
+ "name": self.project_name,
390
+ "entry": self.entry_module,
391
+ "project_root": self.project_root,
392
+ "version": snap.version,
393
+ "file_count": len(snap.files),
394
+ "clients": [
395
+ {"id": c.id, "platform": c.platform, "device": c.device, "app": c.app, "connected_at": c.connected_at}
396
+ for c in self._clients.values()
397
+ ],
398
+ "preview_connected": self._preview_peer is not None and not self._preview_peer.closed.is_set(),
399
+ "lan": lan_addresses(),
400
+ "port": self.info.port,
401
+ }
402
+
403
+ # -- file changes ----------------------------------------------------
404
+
405
+ def _on_files_changed(self, change: SourceChange, snapshot: SourceSnapshot) -> None:
406
+ with self._snapshot_lock:
407
+ self._snapshot = snapshot
408
+ summary = ", ".join(change.changed + [f"-{p}" for p in change.removed])
409
+ self.log(f"[pn] changed: {summary}")
410
+ for listener in list(self._change_listeners):
411
+ try:
412
+ listener(change, snapshot)
413
+ except Exception as exc:
414
+ self.log(f"[pn] change listener failed: {exc!r}")
415
+ if self._clients:
416
+ message = self._sync_message("update", snapshot, change.changed, change.removed)
417
+ self._call_soon(self._broadcast, message)
418
+
419
+ def _sync_message(self, kind: str, snapshot: SourceSnapshot, paths: Sequence[str], removed: Sequence[str]) -> str:
420
+ files: List[Dict[str, Any]] = []
421
+ for path in paths:
422
+ data = snapshot.read(path)
423
+ if data is None:
424
+ continue
425
+ files.append(_encode_file(path, snapshot.files.get(path, ""), data))
426
+ return json.dumps(
427
+ {
428
+ "type": kind,
429
+ "version": snapshot.version,
430
+ "entry": self.entry_module,
431
+ "files": files,
432
+ "removed": list(removed),
433
+ }
434
+ )
435
+
436
+ def _broadcast(self, message: str) -> None:
437
+ frame = ws.encode_frame(ws.TEXT, message.encode("utf-8"))
438
+ for client in list(self._clients.values()):
439
+ try:
440
+ client.writer.write(frame)
441
+ except Exception:
442
+ self._drop_client(client)
443
+
444
+ def _drop_client(self, client: DevClient) -> None:
445
+ if self._clients.pop(client.id, None) is not None:
446
+ self.log(f"[pn] {client.label()} disconnected")
447
+ try:
448
+ client.writer.close()
449
+ except Exception:
450
+ pass
451
+
452
+ # -- connections -----------------------------------------------------
453
+
454
+ async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
455
+ try:
456
+ head = await reader.readuntil(b"\r\n\r\n")
457
+ except (asyncio.IncompleteReadError, asyncio.LimitOverrunError, ConnectionError):
458
+ writer.close()
459
+ return
460
+ if len(head) > _MAX_HEAD:
461
+ writer.close()
462
+ return
463
+ start_line, headers = ws.parse_http_headers(head)
464
+ parts = start_line.split()
465
+ if len(parts) < 2:
466
+ writer.close()
467
+ return
468
+ method, target = parts[0], parts[1]
469
+ url = urlsplit(target)
470
+ path = unquote(url.path)
471
+ query = {k: v[-1] for k, v in parse_qs(url.query).items()}
472
+ if "websocket" in headers.get("upgrade", "").lower():
473
+ await self._serve_websocket(reader, writer, headers, path, query)
474
+ return
475
+ try:
476
+ await self._serve_http(writer, method, path, query)
477
+ finally:
478
+ try:
479
+ writer.close()
480
+ except Exception:
481
+ pass
482
+
483
+ async def _serve_http(self, writer: asyncio.StreamWriter, method: str, path: str, query: Dict[str, str]) -> None:
484
+ if method not in ("GET", "HEAD"):
485
+ _respond(writer, 405, b"method not allowed", "text/plain")
486
+ return
487
+ if path in ("/", "/index.html"):
488
+ _respond_file(writer, os.path.join(self.static_dir, "index.html"))
489
+ return
490
+ if path.startswith("/static/"):
491
+ name = path[len("/static/") :]
492
+ root = os.path.realpath(self.static_dir)
493
+ candidate = os.path.realpath(os.path.join(root, name))
494
+ if (
495
+ not name
496
+ or any(part.startswith(".") for part in name.split("/"))
497
+ or os.path.commonpath([root, candidate]) != root
498
+ ):
499
+ _respond(writer, 404, b"not found", "text/plain")
500
+ return
501
+ _respond_file(writer, candidate)
502
+ return
503
+ if path == "/manifest":
504
+ _respond_json(writer, self.manifest())
505
+ return
506
+ if path == "/status":
507
+ _respond_json(writer, self.status())
508
+ return
509
+ if path.startswith("/file/"):
510
+ rel = path[len("/file/") :]
511
+ snap = self.snapshot
512
+ if rel not in snap.files:
513
+ _respond(writer, 404, b"not found", "text/plain")
514
+ return
515
+ data = snap.read(rel)
516
+ if data is None:
517
+ _respond(writer, 404, b"not found", "text/plain")
518
+ return
519
+ _respond(writer, 200, data, mimetypes.guess_type(rel)[0] or "application/octet-stream")
520
+ return
521
+ _respond(writer, 404, b"not found", "text/plain")
522
+
523
+ async def _serve_websocket(
524
+ self,
525
+ reader: asyncio.StreamReader,
526
+ writer: asyncio.StreamWriter,
527
+ headers: Dict[str, str],
528
+ path: str,
529
+ query: Dict[str, str],
530
+ ) -> None:
531
+ try:
532
+ writer.write(ws.server_handshake(headers))
533
+ await writer.drain()
534
+ except (ws.HandshakeError, ConnectionError):
535
+ writer.close()
536
+ return
537
+ role = query.get("role", "client")
538
+ if path != "/ws":
539
+ writer.write(ws.encode_close(1008, "unknown path"))
540
+ writer.close()
541
+ return
542
+ if role == "preview":
543
+ await self._preview_session(reader, writer, query)
544
+ else:
545
+ await self._client_session(reader, writer)
546
+
547
+ async def _read_messages(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> AsyncIterator[str]:
548
+ """Yield text messages until the peer closes; answers pings."""
549
+ decoder = ws.FrameDecoder()
550
+ while True:
551
+ try:
552
+ chunk = await reader.read(65536)
553
+ except (ConnectionError, asyncio.CancelledError, OSError):
554
+ return
555
+ if not chunk:
556
+ return
557
+ try:
558
+ messages = list(decoder.feed(chunk))
559
+ except ws.WebSocketError:
560
+ try:
561
+ writer.write(ws.encode_close(1002, "protocol error"))
562
+ except Exception:
563
+ pass
564
+ return
565
+ for opcode, payload in messages:
566
+ if opcode == ws.TEXT:
567
+ try:
568
+ yield payload.decode("utf-8")
569
+ except UnicodeDecodeError:
570
+ continue
571
+ elif opcode == ws.PING:
572
+ writer.write(ws.encode_frame(ws.PONG, payload))
573
+ elif opcode == ws.CLOSE:
574
+ try:
575
+ writer.write(ws.encode_close())
576
+ except Exception:
577
+ pass
578
+ return
579
+
580
+ async def _client_session(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
581
+ self._client_ids += 1
582
+ client = DevClient(id=self._client_ids, writer=writer)
583
+ self._clients[client.id] = client
584
+ try:
585
+ async for text in self._read_messages(reader, writer):
586
+ try:
587
+ message = json.loads(text)
588
+ except ValueError:
589
+ continue
590
+ if isinstance(message, dict):
591
+ self._on_client_message(client, message)
592
+ finally:
593
+ self._drop_client(client)
594
+
595
+ def _on_client_message(self, client: DevClient, message: Dict[str, Any]) -> None:
596
+ kind = message.get("type")
597
+ if kind == "hello":
598
+ client.platform = str(message.get("platform") or "unknown")
599
+ client.device = str(message.get("device") or "")
600
+ client.app = str(message.get("app") or "")
601
+ files = message.get("files")
602
+ client.files = {str(k): str(v) for k, v in files.items()} if isinstance(files, dict) else {}
603
+ self.log(f"[pn] {client.label()} connected")
604
+ snap = self.snapshot
605
+ changed = sorted(p for p, digest in snap.files.items() if client.files.get(p) != digest)
606
+ removed = sorted(p for p in client.files if p not in snap.files)
607
+ try:
608
+ client.writer.write(
609
+ ws.encode_frame(ws.TEXT, self._sync_message("sync", snap, changed, removed).encode("utf-8"))
610
+ )
611
+ except Exception:
612
+ self._drop_client(client)
613
+ return
614
+ if kind == "log":
615
+ level = str(message.get("level") or "info")
616
+ text = str(message.get("text") or "").rstrip("\n")
617
+ prefix = f"[{client.label()}]"
618
+ if level in ("error", "warn", "warning"):
619
+ prefix = f"[{client.label()} {level}]"
620
+ for line in text.split("\n"):
621
+ self.log(f"{prefix} {line}")
622
+ return
623
+ if kind == "error":
624
+ phase = str(message.get("phase") or "runtime")
625
+ text = str(message.get("text") or "").rstrip("\n")
626
+ self.log(f"[{client.label()}] error during {phase}:")
627
+ for line in text.split("\n"):
628
+ self.log(f" {line}")
629
+ return
630
+ if kind == "reloaded":
631
+ mode = str(message.get("mode") or "reload")
632
+ modules = message.get("modules") or []
633
+ what = ", ".join(str(m) for m in modules) if isinstance(modules, list) and modules else "app"
634
+ self.log(f"[{client.label()}] {mode}: {what}")
635
+ return
636
+
637
+ async def _preview_session(
638
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, query: Dict[str, str]
639
+ ) -> None:
640
+ channel = self._preview
641
+ if channel is None:
642
+ writer.write(ws.encode_close(1013, "no preview running"))
643
+ writer.close()
644
+ return
645
+ previous = self._preview_peer
646
+ if previous is not None and not previous.closed.is_set():
647
+ # One page at a time: the newest tab takes over the app.
648
+ try:
649
+ previous._send_now(json.dumps(["dev", {"type": "superseded"}]))
650
+ except Exception:
651
+ pass
652
+ previous._close_now()
653
+ channel.on_preview_disconnected(previous)
654
+ peer = PreviewPeer(self, writer)
655
+ self._preview_peer = peer
656
+ channel.on_preview_connected(peer, dict(query))
657
+ try:
658
+ async for text in self._read_messages(reader, writer):
659
+ channel.on_preview_message(peer, text)
660
+ finally:
661
+ peer.closed.set()
662
+ if self._preview_peer is peer:
663
+ self._preview_peer = None
664
+ channel.on_preview_disconnected(peer)
665
+ try:
666
+ writer.close()
667
+ except Exception:
668
+ pass
669
+
670
+
671
+ # ======================================================================
672
+ # HTTP helpers
673
+ # ======================================================================
674
+
675
+
676
+ def _encode_file(path: str, digest: str, data: bytes) -> Dict[str, Any]:
677
+ try:
678
+ return {"path": path, "sha256": digest, "content": data.decode("utf-8"), "encoding": "utf8"}
679
+ except UnicodeDecodeError:
680
+ return {"path": path, "sha256": digest, "content": base64.b64encode(data).decode("ascii"), "encoding": "base64"}
681
+
682
+
683
+ _STATUS_TEXT = {200: "OK", 404: "Not Found", 405: "Method Not Allowed", 500: "Internal Server Error"}
684
+
685
+
686
+ def _respond(writer: asyncio.StreamWriter, status: int, body: bytes, content_type: str) -> None:
687
+ head = (
688
+ f"HTTP/1.1 {status} {_STATUS_TEXT.get(status, 'OK')}\r\n"
689
+ f"Content-Type: {content_type}\r\n"
690
+ f"Content-Length: {len(body)}\r\n"
691
+ "Cache-Control: no-store\r\n"
692
+ "Access-Control-Allow-Origin: *\r\n"
693
+ "Connection: close\r\n"
694
+ "\r\n"
695
+ ).encode("ascii")
696
+ try:
697
+ writer.write(head + body)
698
+ except Exception:
699
+ pass
700
+
701
+
702
+ def _respond_json(writer: asyncio.StreamWriter, document: Any) -> None:
703
+ _respond(writer, 200, json.dumps(document).encode("utf-8"), "application/json; charset=utf-8")
704
+
705
+
706
+ def _respond_file(writer: asyncio.StreamWriter, path: str) -> None:
707
+ try:
708
+ with open(path, "rb") as handle:
709
+ data = handle.read()
710
+ except OSError:
711
+ _respond(writer, 404, b"not found", "text/plain")
712
+ return
713
+ content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
714
+ if path.endswith(".js"):
715
+ content_type = "text/javascript"
716
+ if content_type.startswith("text/") and "charset" not in content_type:
717
+ content_type += "; charset=utf-8"
718
+ _respond(writer, 200, data, content_type)