pythonnative 0.30.0__tar.gz → 0.31.0__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 (171) hide show
  1. {pythonnative-0.30.0/src/pythonnative.egg-info → pythonnative-0.31.0}/PKG-INFO +1 -1
  2. {pythonnative-0.30.0 → pythonnative-0.31.0}/pyproject.toml +1 -1
  3. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/__init__.py +1 -1
  4. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/cli/pn.py +37 -6
  5. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/devices.py +14 -1
  6. {pythonnative-0.30.0 → pythonnative-0.31.0/src/pythonnative.egg-info}/PKG-INFO +1 -1
  7. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_cli.py +198 -3
  8. {pythonnative-0.30.0 → pythonnative-0.31.0}/LICENSE +0 -0
  9. {pythonnative-0.30.0 → pythonnative-0.31.0}/README.md +0 -0
  10. {pythonnative-0.30.0 → pythonnative-0.31.0}/setup.cfg +0 -0
  11. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/_ios_log.py +0 -0
  12. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/alerts.py +0 -0
  13. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/animated.py +0 -0
  14. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/appearance.py +0 -0
  15. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/cli/__init__.py +0 -0
  16. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/components.py +0 -0
  17. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/diagnostics.py +0 -0
  18. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/element.py +0 -0
  19. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/events.py +0 -0
  20. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/gestures.py +0 -0
  21. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/hooks.py +0 -0
  22. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/hot_reload.py +0 -0
  23. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/images.py +0 -0
  24. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/layout.py +0 -0
  25. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/mutations.py +0 -0
  26. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/__init__.py +0 -0
  27. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/app_state.py +0 -0
  28. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/battery.py +0 -0
  29. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/biometrics.py +0 -0
  30. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/camera.py +0 -0
  31. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/clipboard.py +0 -0
  32. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/file_system.py +0 -0
  33. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/haptics.py +0 -0
  34. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/linking.py +0 -0
  35. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/location.py +0 -0
  36. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/net_info.py +0 -0
  37. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/notifications.py +0 -0
  38. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/permissions.py +0 -0
  39. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/secure_store.py +0 -0
  40. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_modules/share.py +0 -0
  41. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_views/__init__.py +0 -0
  42. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_views/android.py +0 -0
  43. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_views/base.py +0 -0
  44. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_views/desktop.py +0 -0
  45. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/native_views/ios.py +0 -0
  46. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/navigation.py +0 -0
  47. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/net.py +0 -0
  48. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/platform.py +0 -0
  49. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/platform_metrics.py +0 -0
  50. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/preview.py +0 -0
  51. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/__init__.py +0 -0
  52. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/android.py +0 -0
  53. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/builder.py +0 -0
  54. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/config.py +0 -0
  55. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/doctor.py +0 -0
  56. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/icons.py +0 -0
  57. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/ios.py +0 -0
  58. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/permissions.py +0 -0
  59. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/project/runtime_assets.py +0 -0
  60. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/reconciler.py +0 -0
  61. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/runtime.py +0 -0
  62. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/screen.py +0 -0
  63. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/sdk/__init__.py +0 -0
  64. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/sdk/_components.py +0 -0
  65. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/storage.py +0 -0
  66. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/style.py +0 -0
  67. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/suspense.py +0 -0
  68. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/build.gradle +0 -0
  69. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  70. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
  71. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  72. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +0 -0
  73. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -0
  74. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +0 -0
  75. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +0 -0
  76. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +0 -0
  77. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +0 -0
  78. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +0 -0
  79. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  80. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
  81. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -0
  82. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
  83. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
  84. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  85. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  86. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  87. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  88. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  89. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  90. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  91. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  92. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  93. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  94. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +0 -0
  95. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  96. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  97. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  98. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  99. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  100. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  101. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
  102. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
  103. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  104. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  105. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  106. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  107. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  108. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  109. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  110. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  111. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  112. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  113. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  114. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/BridgingHeader.h +0 -0
  115. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  116. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +0 -0
  117. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +0 -0
  118. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -0
  119. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  120. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  121. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  122. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  123. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  124. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/utils.py +0 -0
  125. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative/virtual_rows.py +0 -0
  126. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative.egg-info/SOURCES.txt +0 -0
  127. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  128. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  129. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative.egg-info/requires.txt +0 -0
  130. {pythonnative-0.30.0 → pythonnative-0.31.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  131. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_alert.py +0 -0
  132. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_animated.py +0 -0
  133. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_animated_graph.py +0 -0
  134. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_appearance.py +0 -0
  135. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_async_hooks.py +0 -0
  136. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_components.py +0 -0
  137. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_core_semantics.py +0 -0
  138. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_desktop_backend.py +0 -0
  139. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_element.py +0 -0
  140. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_events.py +0 -0
  141. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_extended_components.py +0 -0
  142. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_gesture_composition.py +0 -0
  143. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_gestures.py +0 -0
  144. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_hooks.py +0 -0
  145. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_hot_reload.py +0 -0
  146. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_images.py +0 -0
  147. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_incremental_render.py +0 -0
  148. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_interaction_props.py +0 -0
  149. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_ios_log.py +0 -0
  150. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_layout.py +0 -0
  151. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_metric_hooks.py +0 -0
  152. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_mutations.py +0 -0
  153. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_native_lists.py +0 -0
  154. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_native_modules.py +0 -0
  155. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_native_views.py +0 -0
  156. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_navigation.py +0 -0
  157. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_net.py +0 -0
  158. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_new_components.py +0 -0
  159. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_platform.py +0 -0
  160. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_platform_metrics.py +0 -0
  161. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_reconciler.py +0 -0
  162. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_ref.py +0 -0
  163. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_runtime.py +0 -0
  164. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_screen.py +0 -0
  165. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_sdk.py +0 -0
  166. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_smoke.py +0 -0
  167. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_storage.py +0 -0
  168. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_style.py +0 -0
  169. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_suspense.py +0 -0
  170. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_transitions.py +0 -0
  171. {pythonnative-0.30.0 → pythonnative-0.31.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.30.0"
7
+ version = "0.31.0"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
@@ -60,7 +60,7 @@ Example:
60
60
  ```
61
61
  """
62
62
 
63
- __version__ = "0.30.0"
63
+ __version__ = "0.31.0"
64
64
 
65
65
  from . import appearance, diagnostics, gestures, images, runtime, sdk
66
66
  from .alerts import Alert
@@ -9,7 +9,7 @@ The console script `pn` (declared in `pyproject.toml`) dispatches to:
9
9
  - `pn preview [component]`: render the app in a desktop (Tkinter) window
10
10
  with Fast Refresh, the fast inner dev loop, no device required.
11
11
  - `pn devices [platform]`: list connected devices, emulators, and
12
- simulators.
12
+ simulators, as a table or as JSON with `--json`.
13
13
  - `pn run android|ios [--device D]`: stage + build + install + launch on
14
14
  a device, emulator, or simulator, with optional on-device hot reload.
15
15
  - `pn logs android|ios`: stream logs from the running app without
@@ -38,7 +38,7 @@ import sys
38
38
  import time
39
39
  from importlib.metadata import version as pkg_version
40
40
  from pathlib import Path
41
- from typing import Any, Dict, List, Optional
41
+ from typing import Any, Dict, List, Optional, TextIO
42
42
 
43
43
  from ..project import builder as builder_mod
44
44
  from ..project import devices as devices_mod
@@ -298,18 +298,46 @@ def _preview_entry(project_dir: Path) -> str:
298
298
  # ======================================================================
299
299
 
300
300
 
301
+ def _print_no_devices_hints(stream: TextIO) -> None:
302
+ """Print the "no devices" guidance to ``stream``.
303
+
304
+ Shared by both output modes so the wording can't drift between the
305
+ table (which sends it to stdout) and ``--json`` (stderr).
306
+
307
+ Args:
308
+ stream: Where to write, ``sys.stdout`` or ``sys.stderr``.
309
+ """
310
+ print("No devices found.", file=stream)
311
+ print("Android: start an emulator or connect a device with USB debugging enabled.", file=stream)
312
+ print("iOS: open Xcode once to install Simulators, or plug in a device.", file=stream)
313
+
314
+
301
315
  def devices_command(args: argparse.Namespace) -> None:
302
316
  """List connected devices, emulators, and simulators.
303
317
 
318
+ Prints an aligned table and exits 1 when nothing is connected.
319
+
320
+ With ``--json``, stdout carries a JSON array and nothing else, one
321
+ object per device (see ``Device.to_dict``), so it stays parseable.
322
+ The "no devices" hints go to stderr instead, an empty result prints
323
+ ``[]``, and the exit status is 0 either way, since "no devices" is a
324
+ valid answer for a script rather than a failure.
325
+
304
326
  Args:
305
- args: Parsed namespace with optional ``platform``.
327
+ args: Parsed namespace with optional ``platform`` and ``json``.
306
328
  """
307
329
  platform: Optional[str] = getattr(args, "platform", None)
330
+ as_json: bool = getattr(args, "json", False)
308
331
  devices = devices_mod.list_devices(platform)
332
+
333
+ if as_json:
334
+ if not devices:
335
+ _print_no_devices_hints(sys.stderr)
336
+ print(json.dumps([device.to_dict() for device in devices], indent=2))
337
+ return
338
+
309
339
  if not devices:
310
- print("No devices found.")
311
- print("Android: start an emulator or connect a device with USB debugging enabled.")
312
- print("iOS: open Xcode once to install Simulators, or plug in a device.")
340
+ _print_no_devices_hints(sys.stdout)
313
341
  sys.exit(1)
314
342
  print(f" {'IDENTIFIER':<40} {'KIND':<10} {'STATE':<10} NAME")
315
343
  for device in devices:
@@ -943,6 +971,9 @@ def _build_parser() -> argparse.ArgumentParser:
943
971
 
944
972
  parser_devices = subparsers.add_parser("devices", help="List devices, emulators, and simulators")
945
973
  parser_devices.add_argument("platform", nargs="?", choices=["android", "ios"], help="Restrict to a platform")
974
+ parser_devices.add_argument(
975
+ "--json", action="store_true", help="Print a JSON array to stdout for scripting (hints go to stderr)"
976
+ )
946
977
  parser_devices.set_defaults(func=devices_command)
947
978
 
948
979
  parser_run = subparsers.add_parser("run", help="Build, install, and launch on a device/simulator")
@@ -18,7 +18,7 @@ import json
18
18
  import re
19
19
  import subprocess
20
20
  import tempfile
21
- from dataclasses import dataclass
21
+ from dataclasses import asdict, dataclass
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, List, Optional
24
24
 
@@ -64,6 +64,19 @@ class Device:
64
64
  return self.state in ("booted", "shutdown")
65
65
  return self.state in ("booted", "connected")
66
66
 
67
+ def to_dict(self) -> Dict[str, Any]:
68
+ """Return a JSON-serializable view of this target.
69
+
70
+ Derived from the dataclass fields so a field added later is
71
+ carried into the payload automatically, plus the computed
72
+ ``is_ready`` that consumers would otherwise have to re-derive
73
+ from ``kind`` and ``state``.
74
+
75
+ Returns:
76
+ The declared fields in declaration order, then ``is_ready``.
77
+ """
78
+ return {**asdict(self), "is_ready": self.is_ready}
79
+
67
80
  def format(self) -> str:
68
81
  """Return one aligned listing row for the CLI."""
69
82
  os_part = f" ({self.os_version})" if self.os_version else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -1,20 +1,23 @@
1
+ import argparse
2
+ import json
1
3
  import os
2
4
  import shutil
3
5
  import subprocess
4
6
  import sys
5
7
  import tempfile
6
8
  from pathlib import Path
7
- from typing import List
9
+ from typing import Callable, Dict, List, Optional
8
10
 
9
11
  import pytest
10
12
 
11
13
  import pythonnative.cli.pn as pn_cli
12
14
  import pythonnative.hot_reload as hot_reload_module
15
+ from pythonnative.project.devices import Device
13
16
 
14
17
 
15
- def run_pn(args: List[str], cwd: str) -> "subprocess.CompletedProcess[str]":
18
+ def run_pn(args: List[str], cwd: str, env: Optional[Dict[str, str]] = None) -> "subprocess.CompletedProcess[str]":
16
19
  cmd = [sys.executable, "-m", "pythonnative.cli.pn"] + args
17
- return subprocess.run(cmd, cwd=cwd, check=False, capture_output=True, text=True)
20
+ return subprocess.run(cmd, cwd=cwd, check=False, capture_output=True, text=True, env=env)
18
21
 
19
22
 
20
23
  def test_cli_version(tmp_path: Path) -> None:
@@ -339,6 +342,198 @@ def test_booted_ios_udid_handles_xcrun_missing(monkeypatch: pytest.MonkeyPatch)
339
342
  assert pn_cli._booted_ios_udid() is None
340
343
 
341
344
 
345
+ # `pn devices` tests run devices_command() in-process rather than through run_pn,
346
+ # because run_pn spawns a child interpreter that monkeypatch can't reach: it would
347
+ # re-import the real list_devices and shell out to adb/xcrun, making the result
348
+ # depend on whatever hardware the test machine has attached. The one run_pn test
349
+ # below empties PATH instead, so every list_* call hits FileNotFoundError.
350
+
351
+ # A naive `state == "booted"` readiness check disagrees with is_ready on two of
352
+ # these: the connected Android device (ready, not booted) and the shutdown
353
+ # simulator (ready, because simulators boot on demand).
354
+ _FAKE_DEVICES = [
355
+ Device("android", "device", "R5CT12345XYZ", "Pixel 8 Pro", "", "connected"),
356
+ Device("android", "device", "OFFLINE99", "Galaxy S24", "", "offline"),
357
+ Device("ios", "simulator", "ABC-123", "iPhone 17 Pro", "iOS 26.4", "booted"),
358
+ Device("ios", "simulator", "DEF-456", "iPad Pro 13-inch (M4)", "iOS 26.4", "shutdown"),
359
+ ]
360
+
361
+ _NO_DEVICES_HINTS = (
362
+ "No devices found.\n"
363
+ "Android: start an emulator or connect a device with USB debugging enabled.\n"
364
+ "iOS: open Xcode once to install Simulators, or plug in a device.\n"
365
+ )
366
+
367
+ _DEVICE_TABLE = (
368
+ " IDENTIFIER KIND STATE NAME\n"
369
+ " R5CT12345XYZ device connected Pixel 8 Pro\n"
370
+ " OFFLINE99 device offline Galaxy S24\n"
371
+ " ABC-123 simulator booted iPhone 17 Pro (iOS 26.4)\n"
372
+ " DEF-456 simulator shutdown iPad Pro 13-inch (M4) (iOS 26.4)\n"
373
+ "\n"
374
+ "Target one with: pn run <platform> --device <identifier or name>\n"
375
+ )
376
+
377
+
378
+ def _fake_list_devices(
379
+ devices: List[Device], calls: Optional[List[Optional[str]]] = None
380
+ ) -> Callable[..., List[Device]]:
381
+ """Stand in for list_devices, filtering by platform and recording the argument."""
382
+
383
+ def _list(platform: Optional[str] = None) -> List[Device]:
384
+ if calls is not None:
385
+ calls.append(platform)
386
+ return [device for device in devices if platform in (None, device.platform)]
387
+
388
+ return _list
389
+
390
+
391
+ def test_devices_json_emits_parseable_array(
392
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
393
+ ) -> None:
394
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
395
+
396
+ # Returns rather than exiting, so a populated listing stays exit 0.
397
+ pn_cli.devices_command(argparse.Namespace(platform=None, json=True))
398
+
399
+ payload = json.loads(capsys.readouterr().out)
400
+ assert [entry["identifier"] for entry in payload] == [
401
+ "R5CT12345XYZ",
402
+ "OFFLINE99",
403
+ "ABC-123",
404
+ "DEF-456",
405
+ ]
406
+ for entry in payload:
407
+ assert set(entry) == {
408
+ "platform",
409
+ "kind",
410
+ "identifier",
411
+ "name",
412
+ "os_version",
413
+ "state",
414
+ "is_ready",
415
+ }
416
+ ready = {entry["identifier"]: entry["is_ready"] for entry in payload}
417
+ assert ready == {"R5CT12345XYZ": True, "OFFLINE99": False, "ABC-123": True, "DEF-456": True}
418
+
419
+ # The two entries a naive `state == "booted"` check would get wrong.
420
+ by_id = {entry["identifier"]: entry for entry in payload}
421
+ assert by_id["DEF-456"]["state"] == "shutdown" and by_id["DEF-456"]["is_ready"] is True
422
+ assert by_id["R5CT12345XYZ"]["state"] == "connected" and by_id["R5CT12345XYZ"]["is_ready"] is True
423
+ assert by_id["OFFLINE99"]["kind"] == "device" and by_id["OFFLINE99"]["is_ready"] is False
424
+
425
+
426
+ def test_devices_json_empty_prints_array_and_hints_to_stderr(
427
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
428
+ ) -> None:
429
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices([]))
430
+
431
+ # No SystemExit: an empty result is exit 0 under --json, unlike the table.
432
+ pn_cli.devices_command(argparse.Namespace(platform=None, json=True))
433
+
434
+ captured = capsys.readouterr()
435
+ assert captured.out == "[]\n"
436
+ assert json.loads(captured.out) == []
437
+ assert captured.err == _NO_DEVICES_HINTS
438
+
439
+
440
+ @pytest.mark.parametrize(
441
+ ("platform", "expected"),
442
+ [("android", ["R5CT12345XYZ", "OFFLINE99"]), ("ios", ["ABC-123", "DEF-456"])],
443
+ )
444
+ def test_devices_json_respects_platform_filter(
445
+ monkeypatch: pytest.MonkeyPatch,
446
+ capsys: pytest.CaptureFixture[str],
447
+ platform: str,
448
+ expected: List[str],
449
+ ) -> None:
450
+ calls: List[Optional[str]] = []
451
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES, calls))
452
+
453
+ pn_cli.devices_command(argparse.Namespace(platform=platform, json=True))
454
+
455
+ assert calls == [platform]
456
+ payload = json.loads(capsys.readouterr().out)
457
+ assert {entry["platform"] for entry in payload} == {platform}
458
+ assert [entry["identifier"] for entry in payload] == expected
459
+
460
+
461
+ @pytest.mark.parametrize("devices", [[], _FAKE_DEVICES], ids=["empty", "populated"])
462
+ def test_devices_json_keeps_human_text_off_stdout(
463
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], devices: List[Device]
464
+ ) -> None:
465
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(devices))
466
+
467
+ pn_cli.devices_command(argparse.Namespace(platform=None, json=True))
468
+
469
+ out = capsys.readouterr().out
470
+ json.loads(out)
471
+ # Every non-JSON source the table mode prints: the three hints, the column
472
+ # header, each aligned row, and the trailer.
473
+ for hint in _NO_DEVICES_HINTS.splitlines():
474
+ assert hint not in out
475
+ assert "IDENTIFIER" not in out
476
+ assert "Target one with:" not in out
477
+ for device in devices:
478
+ assert device.format() not in out
479
+
480
+
481
+ def test_devices_human_output_unchanged_when_empty(tmp_path: Path) -> None:
482
+ # An empty PATH makes adb/xcrun unresolvable, so every list_* call returns [].
483
+ env = {**os.environ, "PATH": str(tmp_path)}
484
+ result = run_pn(["devices"], str(tmp_path), env=env)
485
+
486
+ assert result.returncode == 1
487
+ assert result.stdout == _NO_DEVICES_HINTS
488
+ assert result.stderr == ""
489
+
490
+
491
+ def test_devices_human_output_unchanged_when_populated(
492
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
493
+ ) -> None:
494
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
495
+
496
+ pn_cli.devices_command(argparse.Namespace(platform=None, json=False))
497
+
498
+ captured = capsys.readouterr()
499
+ assert captured.out == _DEVICE_TABLE
500
+ assert captured.err == ""
501
+
502
+
503
+ def test_devices_json_flag_is_wired_through_argparse(tmp_path: Path) -> None:
504
+ # The other JSON tests call devices_command() directly, so only this one
505
+ # proves the subparser actually accepts --json and routes it through.
506
+ env = {**os.environ, "PATH": str(tmp_path)}
507
+ result = run_pn(["devices", "--json"], str(tmp_path), env=env)
508
+
509
+ assert result.returncode == 0
510
+ assert json.loads(result.stdout) == []
511
+ assert result.stdout == "[]\n"
512
+ assert result.stderr == _NO_DEVICES_HINTS
513
+
514
+
515
+ def test_devices_json_serializes_awkward_field_values(
516
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
517
+ ) -> None:
518
+ # Quotes, a newline, non-ASCII, and the two empty-string defaults all survive
519
+ # the round trip today; nothing pinned them before.
520
+ awkward = Device("android", "device", "SER!@#123", 'M\u00e1laga "Pixel"\nLab', "", "")
521
+ monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices([awkward]))
522
+
523
+ pn_cli.devices_command(argparse.Namespace(platform=None, json=True))
524
+
525
+ (entry,) = json.loads(capsys.readouterr().out)
526
+ assert entry == {
527
+ "platform": "android",
528
+ "kind": "device",
529
+ "identifier": "SER!@#123",
530
+ "name": 'M\u00e1laga "Pixel"\nLab',
531
+ "os_version": "",
532
+ "state": "",
533
+ "is_ready": False,
534
+ }
535
+
536
+
342
537
  def test_hot_reload_manifest_payload_maps_files_to_modules(tmp_path: Path) -> None:
343
538
  app_dir = tmp_path / "app"
344
539
  app_dir.mkdir()
File without changes
File without changes
File without changes