pyxpo 0.1.0a1__tar.gz

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 (189) hide show
  1. pyxpo-0.1.0a1/LICENSE +21 -0
  2. pyxpo-0.1.0a1/MANIFEST.in +12 -0
  3. pyxpo-0.1.0a1/PKG-INFO +136 -0
  4. pyxpo-0.1.0a1/README.md +107 -0
  5. pyxpo-0.1.0a1/pyproject.toml +54 -0
  6. pyxpo-0.1.0a1/setup.cfg +4 -0
  7. pyxpo-0.1.0a1/src/pyxpo/__init__.py +106 -0
  8. pyxpo-0.1.0a1/src/pyxpo/__main__.py +6 -0
  9. pyxpo-0.1.0a1/src/pyxpo/_internal/__init__.py +0 -0
  10. pyxpo-0.1.0a1/src/pyxpo/_internal/modname.py +15 -0
  11. pyxpo-0.1.0a1/src/pyxpo/_internal/svgpath.py +117 -0
  12. pyxpo-0.1.0a1/src/pyxpo/_keys.py +53 -0
  13. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_bridge.c +115 -0
  14. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_bridge.h +26 -0
  15. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_layout.c +918 -0
  16. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_layout.h +47 -0
  17. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_utf.c +80 -0
  18. pyxpo-0.1.0a1/src/pyxpo/_native/common/pyxpo_utf.h +18 -0
  19. pyxpo-0.1.0a1/src/pyxpo/animated/__init__.py +38 -0
  20. pyxpo-0.1.0a1/src/pyxpo/animated/api.py +237 -0
  21. pyxpo-0.1.0a1/src/pyxpo/animated/easing.py +135 -0
  22. pyxpo-0.1.0a1/src/pyxpo/animated/engine.py +351 -0
  23. pyxpo-0.1.0a1/src/pyxpo/animated/nodes.py +167 -0
  24. pyxpo-0.1.0a1/src/pyxpo/animated/runtime.py +183 -0
  25. pyxpo-0.1.0a1/src/pyxpo/apis/__init__.py +1 -0
  26. pyxpo-0.1.0a1/src/pyxpo/apis/_bridge.py +57 -0
  27. pyxpo-0.1.0a1/src/pyxpo/apis/_env.py +77 -0
  28. pyxpo-0.1.0a1/src/pyxpo/apis/_events.py +22 -0
  29. pyxpo-0.1.0a1/src/pyxpo/apis/alert.py +46 -0
  30. pyxpo-0.1.0a1/src/pyxpo/apis/appearance.py +27 -0
  31. pyxpo-0.1.0a1/src/pyxpo/apis/appstate.py +36 -0
  32. pyxpo-0.1.0a1/src/pyxpo/apis/back_handler.py +51 -0
  33. pyxpo-0.1.0a1/src/pyxpo/apis/clipboard.py +15 -0
  34. pyxpo-0.1.0a1/src/pyxpo/apis/constants.py +71 -0
  35. pyxpo-0.1.0a1/src/pyxpo/apis/dimensions.py +33 -0
  36. pyxpo-0.1.0a1/src/pyxpo/apis/i18n_manager.py +18 -0
  37. pyxpo-0.1.0a1/src/pyxpo/apis/keyboard.py +43 -0
  38. pyxpo-0.1.0a1/src/pyxpo/apis/linking.py +35 -0
  39. pyxpo-0.1.0a1/src/pyxpo/apis/pan_responder.py +72 -0
  40. pyxpo-0.1.0a1/src/pyxpo/apis/pixel_ratio.py +24 -0
  41. pyxpo-0.1.0a1/src/pyxpo/apis/platform.py +36 -0
  42. pyxpo-0.1.0a1/src/pyxpo/apis/splash_screen.py +17 -0
  43. pyxpo-0.1.0a1/src/pyxpo/apis/toast_android.py +22 -0
  44. pyxpo-0.1.0a1/src/pyxpo/apis/vibration.py +21 -0
  45. pyxpo-0.1.0a1/src/pyxpo/app.py +384 -0
  46. pyxpo-0.1.0a1/src/pyxpo/app_config/__init__.py +33 -0
  47. pyxpo-0.1.0a1/src/pyxpo/app_config/colors.py +30 -0
  48. pyxpo-0.1.0a1/src/pyxpo/app_config/images.py +120 -0
  49. pyxpo-0.1.0a1/src/pyxpo/app_config/resolve.py +142 -0
  50. pyxpo-0.1.0a1/src/pyxpo/app_config/schema.py +238 -0
  51. pyxpo-0.1.0a1/src/pyxpo/build/__init__.py +0 -0
  52. pyxpo-0.1.0a1/src/pyxpo/build/android.py +332 -0
  53. pyxpo-0.1.0a1/src/pyxpo/build/android_config.py +196 -0
  54. pyxpo-0.1.0a1/src/pyxpo/build/app_json.py +25 -0
  55. pyxpo-0.1.0a1/src/pyxpo/build/autolink.py +336 -0
  56. pyxpo-0.1.0a1/src/pyxpo/build/elfcheck.py +40 -0
  57. pyxpo-0.1.0a1/src/pyxpo/build/icons.py +95 -0
  58. pyxpo-0.1.0a1/src/pyxpo/build/ios.py +299 -0
  59. pyxpo-0.1.0a1/src/pyxpo/build/ios_config.py +100 -0
  60. pyxpo-0.1.0a1/src/pyxpo/build/ios_wrap.py +97 -0
  61. pyxpo-0.1.0a1/src/pyxpo/build/prebuild.py +117 -0
  62. pyxpo-0.1.0a1/src/pyxpo/build/runtime.py +298 -0
  63. pyxpo-0.1.0a1/src/pyxpo/build/runtime_manifest.json +25 -0
  64. pyxpo-0.1.0a1/src/pyxpo/build/splash.py +116 -0
  65. pyxpo-0.1.0a1/src/pyxpo/channel.py +40 -0
  66. pyxpo-0.1.0a1/src/pyxpo/channel_native.py +21 -0
  67. pyxpo-0.1.0a1/src/pyxpo/cli.py +399 -0
  68. pyxpo-0.1.0a1/src/pyxpo/component.py +182 -0
  69. pyxpo-0.1.0a1/src/pyxpo/components/__init__.py +1 -0
  70. pyxpo-0.1.0a1/src/pyxpo/components/_common.py +16 -0
  71. pyxpo-0.1.0a1/src/pyxpo/components/_handles.py +53 -0
  72. pyxpo-0.1.0a1/src/pyxpo/components/_native.py +29 -0
  73. pyxpo-0.1.0a1/src/pyxpo/components/activity_indicator.py +18 -0
  74. pyxpo-0.1.0a1/src/pyxpo/components/button.py +23 -0
  75. pyxpo-0.1.0a1/src/pyxpo/components/image.py +36 -0
  76. pyxpo-0.1.0a1/src/pyxpo/components/keyboard_avoiding_view.py +25 -0
  77. pyxpo-0.1.0a1/src/pyxpo/components/lists.py +245 -0
  78. pyxpo-0.1.0a1/src/pyxpo/components/modal.py +70 -0
  79. pyxpo-0.1.0a1/src/pyxpo/components/picker.py +53 -0
  80. pyxpo-0.1.0a1/src/pyxpo/components/pressable.py +79 -0
  81. pyxpo-0.1.0a1/src/pyxpo/components/refresh_control.py +12 -0
  82. pyxpo-0.1.0a1/src/pyxpo/components/safe_area_view.py +28 -0
  83. pyxpo-0.1.0a1/src/pyxpo/components/scroll_view.py +28 -0
  84. pyxpo-0.1.0a1/src/pyxpo/components/slider.py +23 -0
  85. pyxpo-0.1.0a1/src/pyxpo/components/status_bar.py +70 -0
  86. pyxpo-0.1.0a1/src/pyxpo/components/switch.py +19 -0
  87. pyxpo-0.1.0a1/src/pyxpo/components/text.py +81 -0
  88. pyxpo-0.1.0a1/src/pyxpo/components/text_input.py +8 -0
  89. pyxpo-0.1.0a1/src/pyxpo/components/touchables.py +87 -0
  90. pyxpo-0.1.0a1/src/pyxpo/components/view.py +5 -0
  91. pyxpo-0.1.0a1/src/pyxpo/config.py +76 -0
  92. pyxpo-0.1.0a1/src/pyxpo/config_plugins.py +230 -0
  93. pyxpo-0.1.0a1/src/pyxpo/dev/__init__.py +1 -0
  94. pyxpo-0.1.0a1/src/pyxpo/dev/client.py +76 -0
  95. pyxpo-0.1.0a1/src/pyxpo/dev/reloader.py +257 -0
  96. pyxpo-0.1.0a1/src/pyxpo/dev/server.py +245 -0
  97. pyxpo-0.1.0a1/src/pyxpo/element.py +75 -0
  98. pyxpo-0.1.0a1/src/pyxpo/fonts.py +53 -0
  99. pyxpo-0.1.0a1/src/pyxpo/gesture_handler.py +350 -0
  100. pyxpo-0.1.0a1/src/pyxpo/hooks.py +424 -0
  101. pyxpo-0.1.0a1/src/pyxpo/libs.py +42 -0
  102. pyxpo-0.1.0a1/src/pyxpo/module_api.py +44 -0
  103. pyxpo-0.1.0a1/src/pyxpo/module_testing.py +51 -0
  104. pyxpo-0.1.0a1/src/pyxpo/modules.py +53 -0
  105. pyxpo-0.1.0a1/src/pyxpo/native_config.py +32 -0
  106. pyxpo-0.1.0a1/src/pyxpo/ops.py +130 -0
  107. pyxpo-0.1.0a1/src/pyxpo/platforms.py +11 -0
  108. pyxpo-0.1.0a1/src/pyxpo/reconciler.py +469 -0
  109. pyxpo-0.1.0a1/src/pyxpo/renderers/__init__.py +0 -0
  110. pyxpo-0.1.0a1/src/pyxpo/renderers/console.py +64 -0
  111. pyxpo-0.1.0a1/src/pyxpo/renderers/mock.py +211 -0
  112. pyxpo-0.1.0a1/src/pyxpo/router/__init__.py +18 -0
  113. pyxpo-0.1.0a1/src/pyxpo/router/discovery.py +208 -0
  114. pyxpo-0.1.0a1/src/pyxpo/router/entry.py +23 -0
  115. pyxpo-0.1.0a1/src/pyxpo/router/hooks.py +143 -0
  116. pyxpo-0.1.0a1/src/pyxpo/router/layouts/__init__.py +0 -0
  117. pyxpo-0.1.0a1/src/pyxpo/router/layouts/drawer.py +107 -0
  118. pyxpo-0.1.0a1/src/pyxpo/router/layouts/header.py +53 -0
  119. pyxpo-0.1.0a1/src/pyxpo/router/layouts/slot.py +9 -0
  120. pyxpo-0.1.0a1/src/pyxpo/router/layouts/stack.py +62 -0
  121. pyxpo-0.1.0a1/src/pyxpo/router/layouts/tabs.py +84 -0
  122. pyxpo-0.1.0a1/src/pyxpo/router/link.py +36 -0
  123. pyxpo-0.1.0a1/src/pyxpo/router/matching.py +122 -0
  124. pyxpo-0.1.0a1/src/pyxpo/router/root.py +85 -0
  125. pyxpo-0.1.0a1/src/pyxpo/router/router.py +57 -0
  126. pyxpo-0.1.0a1/src/pyxpo/router/runtime.py +59 -0
  127. pyxpo-0.1.0a1/src/pyxpo/router/screen.py +114 -0
  128. pyxpo-0.1.0a1/src/pyxpo/router/store.py +469 -0
  129. pyxpo-0.1.0a1/src/pyxpo/router/theme.py +32 -0
  130. pyxpo-0.1.0a1/src/pyxpo/safe_area.py +28 -0
  131. pyxpo-0.1.0a1/src/pyxpo/style.py +387 -0
  132. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/cpp/CMakeLists.txt +13 -0
  133. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/cpp/pyxpo_jni.c +151 -0
  134. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/cpp/pyxpo_layout_jni.c +111 -0
  135. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/module-api/build.gradle.kts +16 -0
  136. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/module-api/src/main/AndroidManifest.xml +1 -0
  137. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/module-api/src/main/java/dev/pyxpo/moduleapi/PxModule.kt +32 -0
  138. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/Animated.kt +374 -0
  139. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/AppConfig.kt +63 -0
  140. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/BoxDrawable.kt +133 -0
  141. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/Images.kt +99 -0
  142. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/MainActivity.kt +142 -0
  143. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/NativeLayout.kt +63 -0
  144. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/OpApplier.kt +1244 -0
  145. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PxControls.kt +135 -0
  146. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PxModuleHost.kt +94 -0
  147. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PxModuleRegistry.kt +6 -0
  148. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PxPerf.kt +32 -0
  149. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PxViews.kt +197 -0
  150. pyxpo-0.1.0a1/src/pyxpo/templates/android/gen/runtime/java/dev/pyxpo/runtime/PyxpoRuntime.kt +92 -0
  151. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/app/build.gradle.kts +79 -0
  152. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/app/src/main/AndroidManifest.xml +47 -0
  153. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/build.gradle.kts +4 -0
  154. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/gradle/wrapper/gradle-wrapper.jar +0 -0
  155. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/gradle/wrapper/gradle-wrapper.properties +7 -0
  156. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/gradle.properties +3 -0
  157. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/gradlew +252 -0
  158. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/gradlew.bat +94 -0
  159. pyxpo-0.1.0a1/src/pyxpo/templates/android/user/settings.gradle.kts +11 -0
  160. pyxpo-0.1.0a1/src/pyxpo/templates/app/app/+not-found.py.tmpl +20 -0
  161. pyxpo-0.1.0a1/src/pyxpo/templates/app/app/__init__.py.tmpl +0 -0
  162. pyxpo-0.1.0a1/src/pyxpo/templates/app/app/_layout.py.tmpl +13 -0
  163. pyxpo-0.1.0a1/src/pyxpo/templates/app/app/details/[id].py.tmpl +20 -0
  164. pyxpo-0.1.0a1/src/pyxpo/templates/app/app/index.py.tmpl +24 -0
  165. pyxpo-0.1.0a1/src/pyxpo/templates/app/gitignore.tmpl +2 -0
  166. pyxpo-0.1.0a1/src/pyxpo/templates/app/pyproject.toml.tmpl +9 -0
  167. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/Animated.swift +414 -0
  168. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/AppConfig.swift +71 -0
  169. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/Images.swift +35 -0
  170. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/OpApplier.swift +974 -0
  171. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/PxModule.swift +78 -0
  172. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/PxModuleRegistry.swift +2 -0
  173. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/PxViews.swift +405 -0
  174. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/PyxpoRuntime.h +18 -0
  175. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/runtime/PyxpoRuntime.m +140 -0
  176. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/scripts/dylib-Info-template.plist +26 -0
  177. pyxpo-0.1.0a1/src/pyxpo/templates/ios/gen/scripts/install_python.sh +21 -0
  178. pyxpo-0.1.0a1/src/pyxpo/templates/ios/user/App/AppDelegate.swift +121 -0
  179. pyxpo-0.1.0a1/src/pyxpo/templates/ios/user/App/Info.plist +38 -0
  180. pyxpo-0.1.0a1/src/pyxpo/templates/ios/user/App/Pyxpo-Bridging-Header.h +2 -0
  181. pyxpo-0.1.0a1/src/pyxpo/templates/ios/user/project.yml +52 -0
  182. pyxpo-0.1.0a1/src/pyxpo/term.py +39 -0
  183. pyxpo-0.1.0a1/src/pyxpo/vector_icons.py +89 -0
  184. pyxpo-0.1.0a1/src/pyxpo.egg-info/PKG-INFO +136 -0
  185. pyxpo-0.1.0a1/src/pyxpo.egg-info/SOURCES.txt +187 -0
  186. pyxpo-0.1.0a1/src/pyxpo.egg-info/dependency_links.txt +1 -0
  187. pyxpo-0.1.0a1/src/pyxpo.egg-info/entry_points.txt +2 -0
  188. pyxpo-0.1.0a1/src/pyxpo.egg-info/requires.txt +6 -0
  189. pyxpo-0.1.0a1/src/pyxpo.egg-info/top_level.txt +1 -0
pyxpo-0.1.0a1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vishal Chaubey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ include LICENSE README.md
2
+ recursive-include src/pyxpo/templates *
3
+ recursive-include src/pyxpo/_native *
4
+ include src/pyxpo/build/runtime_manifest.json
5
+ prune dist
6
+ prune native
7
+ prune .cache
8
+ prune tests
9
+ prune wheels
10
+ prune examples
11
+ prune packages
12
+ global-exclude __pycache__ *.pyc .DS_Store
pyxpo-0.1.0a1/PKG-INFO ADDED
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyxpo
3
+ Version: 0.1.0a1
4
+ Summary: Python-first mobile app framework with React-style components and native widgets
5
+ Author: Vishal Chaubey
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pyxpo/pyxpo
8
+ Project-URL: Issues, https://github.com/pyxpo/pyxpo/issues
9
+ Keywords: mobile,android,ios,react-native,expo,python,ui,framework
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: User Interfaces
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: tomli>=2; python_version < "3.11"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # Pyxpo
31
+
32
+ React-Native-style Python framework: declarative components + hooks -> reconciler -> batched JSON ops -> native widgets.
33
+ See DESIGN.md (architecture) and PLAN.md (roadmap). Android and iOS are both supported.
34
+
35
+ ## Install
36
+ ```
37
+ pip install pyxpo
38
+ pyxpo create demo && cd demo
39
+ pyxpo doctor # check Xcode / Android SDK / Gradle
40
+ pyxpo run android # or: pyxpo run ios
41
+ ```
42
+ The first mobile build downloads the prebuilt CPython runtime (about 20-60 MB per target) into `~/.cache/pyxpo`.
43
+
44
+ ## Develop pyxpo itself
45
+ ```
46
+ tools/dev_install.sh # .venv with core + every module package in packages/, all editable
47
+ .venv/bin/pyxpo create demo && cd demo && ../.venv/bin/pyxpo run desktop # type 1 + Enter to press the button, q to quit
48
+ .venv/bin/pytest
49
+ ```
50
+
51
+ A project is an `app/` folder of routes, like Expo Router:
52
+
53
+ ```
54
+ app/
55
+ _layout.py # Stack / Tabs / Drawer / Slot around the routes below
56
+ index.py # "/"
57
+ details/[id].py # "/details/7" -> useLocalSearchParams()["id"]
58
+ +not-found.py # unmatched URLs
59
+ ```
60
+
61
+ ```python
62
+ # app/index.py
63
+ from pyxpo import Button, Text, View, useState
64
+ from pyxpo.router import Link
65
+
66
+ def Index():
67
+ n, setN = useState(0)
68
+ return View(Text("Count: ", n), Button(title="Add", onPress=lambda: setN(n + 1)),
69
+ Link("Details", href="/details/%d" % n))
70
+
71
+ default = Index
72
+ ```
73
+
74
+ Hooks: use_state, use_effect, use_ref, use_memo, use_callback, use_context, use_async. Lists need `key=`.
75
+ `pyxpo add-lib <pkg>` checks mobile wheel availability (note: full tensorflow/torch cannot run on mobile).
76
+
77
+ ## Native modules
78
+
79
+ Features beyond the core are optional native modules, each its own pip package (like Expo's `expo-image-picker`).
80
+ Only the modules an app lists are compiled into it.
81
+
82
+ | Package | Import | `modules` entry |
83
+ |---|---|---|
84
+ | `pyxpo-file-system` | `from pyxpo_file_system import File, Directory, Paths` | `file-system` |
85
+ | `pyxpo-image-manipulator` | `from pyxpo_image_manipulator import ImageManipulator` | `image-manipulator` |
86
+ | `pyxpo-image-picker` | `from pyxpo_image_picker import ImagePicker` | `image-picker` |
87
+ | `pyxpo-media-library` | `from pyxpo_media_library import MediaLibrary` | `media-library` |
88
+ | `pyxpo-secure-store` | `import pyxpo_secure_store as SecureStore` | `secure-store` |
89
+ | `pyxpo-sharing` | `from pyxpo_sharing import Share` | `sharing` |
90
+ | `pyxpo-storage` | `from pyxpo_storage import storage` | `storage` |
91
+
92
+ ```
93
+ pip install pyxpo-image-picker
94
+ ```
95
+ ```toml
96
+ [tool.pyxpo]
97
+ modules = ["image-picker"]
98
+ ```
99
+ Their sources live in `packages/`; see each package's README. Third-party modules are pip packages that register a
100
+ `pyxpo.modules` entry point the same way.
101
+
102
+ ## React Native style API
103
+ Every RN spelling is an alias; the snake_case API keeps working. No JSX (not valid Python), so children are call arguments.
104
+
105
+ ```python
106
+ from pyxpo import StyleSheet, View, Text, Pressable, FlatList, SectionList, useState, useEffect, component
107
+
108
+ styles = StyleSheet.create( # or StyleSheet.create({"title": {...}})
109
+ title=dict(fontSize=24, fontWeight="bold", marginBottom=12),
110
+ )
111
+
112
+ @component
113
+ def App():
114
+ count, setCount = useState(0)
115
+ useEffect(lambda: print("count", count), [count])
116
+ return View(
117
+ Text("Hello", style=styles.title),
118
+ Pressable(Text("+"), onPress=lambda: setCount(count + 1),
119
+ style=lambda state: {"opacity": 0.5 if state.pressed else 1}),
120
+ FlatList(data=[1, 2], renderItem=lambda p: Text(p["item"]), keyExtractor=lambda i, _: i),
121
+ )
122
+ ```
123
+
124
+ - Style keys and props accept camelCase (`backgroundColor`, `onPress`, `numberOfLines`, `placeholderTextColor`, `testID`). Giving both spellings of one prop raises `TypeError`. User components accept a camelCase key when its snake_case form is a parameter.
125
+ - Hooks: `useState`, `useEffect`, `useLayoutEffect`, `useReducer`, `useRef`, `useMemo`, `useCallback`, `useContext`, `useId`, `useImperativeHandle`, `useSyncExternalStore`, `useTransition`, `useDeferredValue`. Effects are exception-safe: one failing effect or cleanup does not stop the others, errors go to `ErrorBoundary(on_error=...)` or `app.on_error`. Child effects run before parent effects. Effects must be sync functions (an `async def` effect runs as a task and warns).
126
+ - `ErrorBoundary(*children, fallback=lambda error, reset: ...)`.
127
+ - `FlatList` `renderItem` with one parameter gets `{"item", "index"}`; with two it gets `(item, index)`. `SectionList(sections=[{"title", "data"}], renderItem, renderSectionHeader)` (no sticky headers yet).
128
+ - Modules: `Platform.OS`, `Dimensions.addEventListener("change", fn)` (returns an object with `.remove()`), `BackHandler.addEventListener("hardwareBackPress", fn)`, `Keyboard.addListener("keyboardDidShow", fn)`, `Linking.openURL`, `Alert.alert(title, msg, [{"text", "onPress"}])`.
129
+ - RN style values that have no native equivalent (`borderStyle`, `tintColor`, `resizeMode`, ...) log a warning and are ignored. `Pressable` `hit_slop` and `delay_long_press` are accepted but not applied natively yet.
130
+ - Example: `examples/rn_style`.
131
+
132
+ ## Routing (`pyxpo.router`, Expo Router)
133
+ - Files under `app/` are routes: `index`, `[id]` (dynamic), `[...rest]` (catch-all), `(group)/` (no URL segment), `_layout.py` (navigator), `+not-found.py`. A route module exports `default` (or has a single `@component`).
134
+ - Layouts: `Stack`, `Tabs`, `Drawer`, `Slot`; configure with `Stack.Screen(name=..., options={...})` or `screenOptions`. Options follow react-navigation (`title`, `headerShown`, `headerRight`, `tabBarIcon`, ...). A layout may export `unstable_settings = {"initialRouteName": ...}`.
135
+ - `router.push/navigate/replace/back/dismiss/dismissAll/setParams`, `<Link href=...>`, `<Redirect href=...>`; hooks `useRouter`, `useLocalSearchParams`, `useGlobalSearchParams`, `usePathname`, `useSegments`, `useNavigation`, `useIsFocused`, `useFocusEffect`. Theme: `ThemeProvider`, `DefaultTheme`, `DarkTheme`, `useTheme`.
136
+ - Android hardware back, the stack edge-swipe and the drawer all go through one navigation store. iOS Swift controls and the native stack are not ported yet.
@@ -0,0 +1,107 @@
1
+ # Pyxpo
2
+
3
+ React-Native-style Python framework: declarative components + hooks -> reconciler -> batched JSON ops -> native widgets.
4
+ See DESIGN.md (architecture) and PLAN.md (roadmap). Android and iOS are both supported.
5
+
6
+ ## Install
7
+ ```
8
+ pip install pyxpo
9
+ pyxpo create demo && cd demo
10
+ pyxpo doctor # check Xcode / Android SDK / Gradle
11
+ pyxpo run android # or: pyxpo run ios
12
+ ```
13
+ The first mobile build downloads the prebuilt CPython runtime (about 20-60 MB per target) into `~/.cache/pyxpo`.
14
+
15
+ ## Develop pyxpo itself
16
+ ```
17
+ tools/dev_install.sh # .venv with core + every module package in packages/, all editable
18
+ .venv/bin/pyxpo create demo && cd demo && ../.venv/bin/pyxpo run desktop # type 1 + Enter to press the button, q to quit
19
+ .venv/bin/pytest
20
+ ```
21
+
22
+ A project is an `app/` folder of routes, like Expo Router:
23
+
24
+ ```
25
+ app/
26
+ _layout.py # Stack / Tabs / Drawer / Slot around the routes below
27
+ index.py # "/"
28
+ details/[id].py # "/details/7" -> useLocalSearchParams()["id"]
29
+ +not-found.py # unmatched URLs
30
+ ```
31
+
32
+ ```python
33
+ # app/index.py
34
+ from pyxpo import Button, Text, View, useState
35
+ from pyxpo.router import Link
36
+
37
+ def Index():
38
+ n, setN = useState(0)
39
+ return View(Text("Count: ", n), Button(title="Add", onPress=lambda: setN(n + 1)),
40
+ Link("Details", href="/details/%d" % n))
41
+
42
+ default = Index
43
+ ```
44
+
45
+ Hooks: use_state, use_effect, use_ref, use_memo, use_callback, use_context, use_async. Lists need `key=`.
46
+ `pyxpo add-lib <pkg>` checks mobile wheel availability (note: full tensorflow/torch cannot run on mobile).
47
+
48
+ ## Native modules
49
+
50
+ Features beyond the core are optional native modules, each its own pip package (like Expo's `expo-image-picker`).
51
+ Only the modules an app lists are compiled into it.
52
+
53
+ | Package | Import | `modules` entry |
54
+ |---|---|---|
55
+ | `pyxpo-file-system` | `from pyxpo_file_system import File, Directory, Paths` | `file-system` |
56
+ | `pyxpo-image-manipulator` | `from pyxpo_image_manipulator import ImageManipulator` | `image-manipulator` |
57
+ | `pyxpo-image-picker` | `from pyxpo_image_picker import ImagePicker` | `image-picker` |
58
+ | `pyxpo-media-library` | `from pyxpo_media_library import MediaLibrary` | `media-library` |
59
+ | `pyxpo-secure-store` | `import pyxpo_secure_store as SecureStore` | `secure-store` |
60
+ | `pyxpo-sharing` | `from pyxpo_sharing import Share` | `sharing` |
61
+ | `pyxpo-storage` | `from pyxpo_storage import storage` | `storage` |
62
+
63
+ ```
64
+ pip install pyxpo-image-picker
65
+ ```
66
+ ```toml
67
+ [tool.pyxpo]
68
+ modules = ["image-picker"]
69
+ ```
70
+ Their sources live in `packages/`; see each package's README. Third-party modules are pip packages that register a
71
+ `pyxpo.modules` entry point the same way.
72
+
73
+ ## React Native style API
74
+ Every RN spelling is an alias; the snake_case API keeps working. No JSX (not valid Python), so children are call arguments.
75
+
76
+ ```python
77
+ from pyxpo import StyleSheet, View, Text, Pressable, FlatList, SectionList, useState, useEffect, component
78
+
79
+ styles = StyleSheet.create( # or StyleSheet.create({"title": {...}})
80
+ title=dict(fontSize=24, fontWeight="bold", marginBottom=12),
81
+ )
82
+
83
+ @component
84
+ def App():
85
+ count, setCount = useState(0)
86
+ useEffect(lambda: print("count", count), [count])
87
+ return View(
88
+ Text("Hello", style=styles.title),
89
+ Pressable(Text("+"), onPress=lambda: setCount(count + 1),
90
+ style=lambda state: {"opacity": 0.5 if state.pressed else 1}),
91
+ FlatList(data=[1, 2], renderItem=lambda p: Text(p["item"]), keyExtractor=lambda i, _: i),
92
+ )
93
+ ```
94
+
95
+ - Style keys and props accept camelCase (`backgroundColor`, `onPress`, `numberOfLines`, `placeholderTextColor`, `testID`). Giving both spellings of one prop raises `TypeError`. User components accept a camelCase key when its snake_case form is a parameter.
96
+ - Hooks: `useState`, `useEffect`, `useLayoutEffect`, `useReducer`, `useRef`, `useMemo`, `useCallback`, `useContext`, `useId`, `useImperativeHandle`, `useSyncExternalStore`, `useTransition`, `useDeferredValue`. Effects are exception-safe: one failing effect or cleanup does not stop the others, errors go to `ErrorBoundary(on_error=...)` or `app.on_error`. Child effects run before parent effects. Effects must be sync functions (an `async def` effect runs as a task and warns).
97
+ - `ErrorBoundary(*children, fallback=lambda error, reset: ...)`.
98
+ - `FlatList` `renderItem` with one parameter gets `{"item", "index"}`; with two it gets `(item, index)`. `SectionList(sections=[{"title", "data"}], renderItem, renderSectionHeader)` (no sticky headers yet).
99
+ - Modules: `Platform.OS`, `Dimensions.addEventListener("change", fn)` (returns an object with `.remove()`), `BackHandler.addEventListener("hardwareBackPress", fn)`, `Keyboard.addListener("keyboardDidShow", fn)`, `Linking.openURL`, `Alert.alert(title, msg, [{"text", "onPress"}])`.
100
+ - RN style values that have no native equivalent (`borderStyle`, `tintColor`, `resizeMode`, ...) log a warning and are ignored. `Pressable` `hit_slop` and `delay_long_press` are accepted but not applied natively yet.
101
+ - Example: `examples/rn_style`.
102
+
103
+ ## Routing (`pyxpo.router`, Expo Router)
104
+ - Files under `app/` are routes: `index`, `[id]` (dynamic), `[...rest]` (catch-all), `(group)/` (no URL segment), `_layout.py` (navigator), `+not-found.py`. A route module exports `default` (or has a single `@component`).
105
+ - Layouts: `Stack`, `Tabs`, `Drawer`, `Slot`; configure with `Stack.Screen(name=..., options={...})` or `screenOptions`. Options follow react-navigation (`title`, `headerShown`, `headerRight`, `tabBarIcon`, ...). A layout may export `unstable_settings = {"initialRouteName": ...}`.
106
+ - `router.push/navigate/replace/back/dismiss/dismissAll/setParams`, `<Link href=...>`, `<Redirect href=...>`; hooks `useRouter`, `useLocalSearchParams`, `useGlobalSearchParams`, `usePathname`, `useSegments`, `useNavigation`, `useIsFocused`, `useFocusEffect`. Theme: `ThemeProvider`, `DefaultTheme`, `DarkTheme`, `useTheme`.
107
+ - Android hardware back, the stack edge-swipe and the drawer all go through one navigation store. iOS Swift controls and the native stack are not ported yet.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyxpo"
7
+ dynamic = ["version"]
8
+ description = "Python-first mobile app framework with React-style components and native widgets"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Vishal Chaubey" }]
13
+ keywords = ["mobile", "android", "ios", "react-native", "expo", "python", "ui", "framework"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: MacOS",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: User Interfaces",
26
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
27
+ ]
28
+ requires-python = ">=3.9"
29
+ dependencies = ["tomli>=2; python_version < '3.11'"]
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/pyxpo/pyxpo"
36
+ Issues = "https://github.com/pyxpo/pyxpo/issues"
37
+
38
+ [project.scripts]
39
+ pyxpo = "pyxpo.cli:main"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.dynamic]
45
+ version = { attr = "pyxpo.__version__" }
46
+
47
+ [tool.setuptools.package-data]
48
+ pyxpo = ["templates/**/*", "_native/**/*", "build/runtime_manifest.json"]
49
+
50
+ [tool.setuptools.exclude-package-data]
51
+ "*" = ["__pycache__", "*.pyc"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,106 @@
1
+ """Pyxpo: React-Native-style mobile UI framework for Python.
2
+
3
+ The top-level namespace mirrors `react-native` (components, APIs, hooks); routing lives in `pyxpo.router`
4
+ (the `expo-router` equivalent) and icons in `pyxpo.vector_icons`.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os as _os
9
+
10
+ from .component import enable_registry as _enable_registry
11
+
12
+ if _os.environ.get("PYXPO_DEV_URL"):
13
+ _enable_registry(True) # keep Component identity across hot reloads; must run before app code
14
+
15
+ from .animated import Animated, Easing, use_animated_value
16
+ from .app import App, create_app, run_app
17
+ from .channel import Channel, LocalChannel
18
+ from .component import Component, ErrorBoundary, component, memo
19
+ from .element import Element
20
+
21
+ from .components.activity_indicator import ActivityIndicator
22
+ from .components.button import Button
23
+ from .components.image import Image, ImageBackground
24
+ from .components.keyboard_avoiding_view import KeyboardAvoidingView
25
+ from .components.lists import FlatList, SectionList, VirtualizedList
26
+ from .components.modal import Modal
27
+ from .components.picker import Picker
28
+ from .components.pressable import Pressable
29
+ from .components.refresh_control import RefreshControl
30
+ from .components.safe_area_view import SafeAreaView
31
+ from .components.scroll_view import ScrollView
32
+ from .components.slider import Slider
33
+ from .components.status_bar import StatusBar
34
+ from .components.switch import Switch
35
+ from .components.text import Text
36
+ from .components.text_input import TextInput
37
+ from .components.touchables import (TouchableHighlight, TouchableNativeFeedback, TouchableOpacity,
38
+ TouchableWithoutFeedback)
39
+ from .components.view import View
40
+
41
+ from .apis.alert import Alert
42
+ from .apis.appearance import Appearance
43
+ from .apis.appstate import AppState
44
+ from .apis.back_handler import BackHandler
45
+ from .apis.clipboard import Clipboard
46
+ from .apis.dimensions import Dimensions
47
+ from .apis.i18n_manager import I18nManager
48
+ from .apis.keyboard import Keyboard
49
+ from .apis.linking import Linking
50
+ from .apis.pan_responder import PanResponder
51
+ from .apis.pixel_ratio import PixelRatio
52
+ from .apis.platform import Platform
53
+ from .apis.toast_android import ToastAndroid
54
+ from .apis.splash_screen import SplashScreen
55
+ from .apis.constants import Constants
56
+ from .apis.vibration import Vibration
57
+
58
+ from .style import StyleSheet, processColor
59
+ from .hooks import (HookError, createContext, create_context, use_async, use_callback, use_context, use_debug_value,
60
+ use_deferred_value, use_effect, use_id, use_imperative_handle, use_layout_effect, use_memo,
61
+ use_reducer, use_ref, use_state, use_sync_external_store, use_transition, use_color_scheme,
62
+ use_safe_area_frame, use_safe_area_insets, use_window_dimensions, useAsync, useCallback, useColorScheme, useContext,
63
+ useDebugValue, useDeferredValue, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo,
64
+ useReducer, useRef, useSafeAreaFrame, useSafeAreaInsets, useState, useSyncExternalStore, useTransition,
65
+ useWindowDimensions)
66
+
67
+ useAnimatedValue = use_animated_value
68
+
69
+ __version__ = "0.1.0a1"
70
+
71
+ __all__ = [
72
+ # app
73
+ "App", "create_app", "run_app", "Channel", "LocalChannel", "Component", "component", "memo", "Element",
74
+ # components
75
+ "View", "Text", "TextInput", "Image", "ImageBackground", "ScrollView", "FlatList", "SectionList",
76
+ "VirtualizedList", "Pressable", "TouchableOpacity", "TouchableHighlight", "TouchableWithoutFeedback",
77
+ "TouchableNativeFeedback", "Button", "Switch", "ActivityIndicator", "Modal", "KeyboardAvoidingView",
78
+ "SafeAreaView", "StatusBar", "RefreshControl", "Picker", "Slider", "ErrorBoundary",
79
+ # apis
80
+ "Alert", "Animated", "Easing", "Appearance", "AppState", "BackHandler", "Clipboard", "Dimensions",
81
+ "I18nManager", "Keyboard", "Linking", "PanResponder", "PixelRatio", "Platform", "StyleSheet", "ToastAndroid",
82
+ "SplashScreen", "Constants", "Vibration", "processColor",
83
+ # hooks
84
+ "HookError", "createContext", "create_context", "use_async", "use_callback", "use_context", "use_debug_value",
85
+ "use_deferred_value", "use_effect", "use_id", "use_imperative_handle", "use_layout_effect", "use_memo",
86
+ "use_reducer", "use_ref", "use_state", "use_sync_external_store", "use_transition", "use_color_scheme",
87
+ "use_safe_area_frame", "use_safe_area_insets", "use_window_dimensions", "use_animated_value", "useAnimatedValue", "useAsync",
88
+ "useCallback", "useColorScheme", "useContext", "useDebugValue", "useDeferredValue", "useEffect", "useId",
89
+ "useImperativeHandle", "useLayoutEffect", "useMemo", "useReducer", "useRef", "useSafeAreaFrame", "useSafeAreaInsets", "useState",
90
+ "useSyncExternalStore", "useTransition", "useWindowDimensions",
91
+ ]
92
+
93
+
94
+ _MOVED = {"Share": "sharing", "MediaLibrary": "media-library", "ImagePicker": "image-picker", "ImageManipulator": "image-manipulator",
95
+ "ImageTooLargeError": "image-manipulator"}
96
+
97
+
98
+ def __getattr__(name: str):
99
+ if name in _MOVED:
100
+ mod = _MOVED[name]
101
+ raise AttributeError(
102
+ "pyxpo.%s moved to the optional native module package pyxpo-%s: run `pip install pyxpo-%s`, "
103
+ "add '%s' to [tool.pyxpo] modules in pyproject.toml, and use `from pyxpo_%s import %s`"
104
+ % (name, mod, mod, mod, mod.replace("-", "_"), name)
105
+ )
106
+ raise AttributeError("module 'pyxpo' has no attribute %r" % name)
@@ -0,0 +1,6 @@
1
+ """`python -m pyxpo` runs the CLI (native build scripts of ejected projects call it this way)."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
File without changes
@@ -0,0 +1,15 @@
1
+ """Module naming shared by the router's route loader and the hot reloader."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Optional
5
+
6
+
7
+ def module_name_for(rel: str) -> Optional[str]:
8
+ """`app/(tabs)/[id].py` -> `app.(tabs).[id]`; `app/__init__.py` -> `app`. None for non-.py paths.
9
+
10
+ Names with brackets or parentheses are not importable with `import`; the router loads them by file path.
11
+ """
12
+ if not rel.endswith(".py"):
13
+ return None
14
+ name = rel[:-3].replace("/", ".")
15
+ return name[: -len(".__init__")] if name.endswith(".__init__") else name
@@ -0,0 +1,117 @@
1
+ """SVG path data -> absolute move/line/cubic/close commands, which the native `Path` view replays."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+ from typing import Any, Dict, List
7
+
8
+ _TOKEN = re.compile(r"[MmLlHhVvCcSsQqAaZz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?")
9
+ _ARGC = {"M": 2, "L": 2, "H": 1, "V": 1, "C": 6, "S": 4, "Q": 4, "A": 7, "Z": 0}
10
+
11
+
12
+ def _arc(x1: float, y1: float, rx: float, ry: float, phi: float, fa: int, fs: int, x2: float, y2: float) -> List[List[float]]:
13
+ if rx == 0 or ry == 0 or (x1 == x2 and y1 == y2):
14
+ return [[1, x2, y2]]
15
+ rx, ry = abs(rx), abs(ry)
16
+ cp, sp = math.cos(math.radians(phi)), math.sin(math.radians(phi))
17
+ dx, dy = (x1 - x2) / 2, (y1 - y2) / 2
18
+ x1p, y1p = cp * dx + sp * dy, -sp * dx + cp * dy
19
+ lam = x1p ** 2 / rx ** 2 + y1p ** 2 / ry ** 2
20
+ if lam > 1:
21
+ s = math.sqrt(lam)
22
+ rx, ry = rx * s, ry * s
23
+ num = rx ** 2 * ry ** 2 - rx ** 2 * y1p ** 2 - ry ** 2 * x1p ** 2
24
+ den = rx ** 2 * y1p ** 2 + ry ** 2 * x1p ** 2
25
+ co = math.sqrt(max(0.0, num / den)) * (-1 if fa == fs else 1)
26
+ cxp, cyp = co * rx * y1p / ry, -co * ry * x1p / rx
27
+ cx, cy = cp * cxp - sp * cyp + (x1 + x2) / 2, sp * cxp + cp * cyp + (y1 + y2) / 2
28
+
29
+ def ang(ux: float, uy: float, vx: float, vy: float) -> float:
30
+ a = math.atan2(ux * vy - uy * vx, ux * vx + uy * vy)
31
+ return a
32
+
33
+ t1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)
34
+ dt = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry)
35
+ if not fs and dt > 0:
36
+ dt -= 2 * math.pi
37
+ elif fs and dt < 0:
38
+ dt += 2 * math.pi
39
+ n = max(1, int(math.ceil(abs(dt) / (math.pi / 2) - 1e-9)))
40
+ step = dt / n
41
+ k = 4 / 3 * math.tan(step / 4)
42
+ out: List[List[float]] = []
43
+ t = t1
44
+ for _ in range(n):
45
+ c1, s1, c2, s2 = math.cos(t), math.sin(t), math.cos(t + step), math.sin(t + step)
46
+ p = [(c1 - k * s1), (s1 + k * c1), (c2 + k * s2), (s2 - k * c2), c2, s2]
47
+ pts = []
48
+ for i in range(0, 6, 2):
49
+ ux, uy = p[i] * rx, p[i + 1] * ry
50
+ pts += [cp * ux - sp * uy + cx, sp * ux + cp * uy + cy]
51
+ out.append([2] + pts)
52
+ t += step
53
+ out[-1][-2:] = [x2, y2]
54
+ return out
55
+
56
+
57
+ def parse_path(d: str) -> List[List[float]]:
58
+ """SVG path data -> commands: [0,x,y] move, [1,x,y] line, [2,x1,y1,x2,y2,x,y] cubic, [3] close."""
59
+ toks = _TOKEN.findall(d)
60
+ out: List[List[float]] = []
61
+ i = 0
62
+ cmd = ""
63
+ x = y = sx = sy = 0.0
64
+ lcx = lcy = None # last control point (for S)
65
+ prev = ""
66
+ while i < len(toks):
67
+ if toks[i].isalpha():
68
+ cmd = toks[i]
69
+ i += 1
70
+ if cmd in "Zz":
71
+ out.append([3])
72
+ x, y = sx, sy
73
+ prev = "Z"
74
+ continue
75
+ up = cmd.upper()
76
+ n = _ARGC[up]
77
+ a = [float(t) for t in toks[i:i + n]]
78
+ if len(a) < n:
79
+ break
80
+ i += n
81
+ rel = cmd.islower()
82
+ ox, oy = (x, y) if rel else (0.0, 0.0)
83
+ if up == "M":
84
+ x, y = a[0] + ox, a[1] + oy
85
+ sx, sy = x, y
86
+ out.append([0, x, y])
87
+ cmd = "l" if rel else "L"
88
+ prev = "M"
89
+ continue
90
+ if up == "L":
91
+ x, y = a[0] + ox, a[1] + oy
92
+ out.append([1, x, y])
93
+ elif up == "H":
94
+ x = a[0] + ox
95
+ out.append([1, x, y])
96
+ elif up == "V":
97
+ y = a[0] + oy
98
+ out.append([1, x, y])
99
+ elif up == "C":
100
+ c = [a[0] + ox, a[1] + oy, a[2] + ox, a[3] + oy, a[4] + ox, a[5] + oy]
101
+ out.append([2] + c)
102
+ lcx, lcy, x, y = c[2], c[3], c[4], c[5]
103
+ elif up == "S":
104
+ c1x, c1y = (2 * x - lcx, 2 * y - lcy) if prev == "C" and lcx is not None else (x, y)
105
+ c = [c1x, c1y, a[0] + ox, a[1] + oy, a[2] + ox, a[3] + oy]
106
+ out.append([2] + c)
107
+ lcx, lcy, x, y = c[2], c[3], c[4], c[5]
108
+ elif up == "Q":
109
+ qx, qy, ex, ey = a[0] + ox, a[1] + oy, a[2] + ox, a[3] + oy
110
+ out.append([2, x + 2 / 3 * (qx - x), y + 2 / 3 * (qy - y), ex + 2 / 3 * (qx - ex), ey + 2 / 3 * (qy - ey), ex, ey])
111
+ x, y = ex, ey
112
+ elif up == "A":
113
+ ex, ey = a[5] + ox, a[6] + oy
114
+ out.extend(_arc(x, y, a[0], a[1], a[2], int(a[3]), int(a[4]), ex, ey))
115
+ x, y = ex, ey
116
+ prev = up
117
+ return out
@@ -0,0 +1,53 @@
1
+ """camelCase -> snake_case name conversion shared by style keys and props (React Native spellings are aliases)."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any, Dict
6
+
7
+ _SPLIT = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
8
+ _SNAKE: Dict[str, str] = {}
9
+ # names whose RN spelling does not follow the mechanical rule
10
+ _OVERRIDES: Dict[str, str] = {"testID": "test_id", "nativeID": "native_id"}
11
+
12
+
13
+ def to_snake(name: str) -> str:
14
+ """`fontSize` -> `font_size`; names that are already lower case pass through untouched."""
15
+ if name.islower():
16
+ return name
17
+ hit = _SNAKE.get(name)
18
+ if hit is None:
19
+ hit = _OVERRIDES.get(name) or _SPLIT.sub("_", name).lower()
20
+ if len(_SNAKE) > 4096:
21
+ _SNAKE.clear()
22
+ _SNAKE[name] = hit
23
+ return hit
24
+
25
+
26
+ # RN prop names that map to a differently named pyxpo prop (everything else follows to_snake)
27
+ PROP_OVERRIDES: Dict[str, str] = {
28
+ "placeholderTextColor": "placeholder_color", "returnKeyType": "return_key", "secureTextEntry": "secure",
29
+ "viewBox": "viewbox", "testID": "test_id", "nativeID": "native_id",
30
+ }
31
+
32
+
33
+ def prop_name(k: str) -> str:
34
+ return k if k.islower() else (PROP_OVERRIDES.get(k) or to_snake(k))
35
+
36
+
37
+ def alias_props(props: Dict[str, Any]) -> Dict[str, Any]:
38
+ """Converts camelCase prop names to the snake_case ones. Giving both spellings raises like a duplicate kwarg."""
39
+ out: Dict[str, Any] = {}
40
+ for k, v in props.items():
41
+ n = prop_name(k)
42
+ if n in out or (n != k and n in props):
43
+ raise TypeError("got both %r and %r" % (k, n))
44
+ out[n] = v
45
+ return out
46
+
47
+
48
+ def snake_aliases(cls: Any) -> Any:
49
+ """Gives every camelCase static method of `cls` a snake_case twin (`addEventListener` -> `add_event_listener`)."""
50
+ for name, val in list(vars(cls).items()):
51
+ if not name.startswith("_") and not name.islower() and isinstance(val, (staticmethod, classmethod)):
52
+ setattr(cls, to_snake(name), val)
53
+ return cls