pyobs-gui 2.0.0.dev22__tar.gz → 2.1.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 (163) hide show
  1. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/PKG-INFO +2 -2
  2. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/index.rst +48 -3
  3. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/__init__.py +4 -1
  4. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/_nuitka_astropy_patch.py +59 -5
  5. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/base.py +12 -3
  6. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/camerawidget.py +12 -14
  7. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/mainwindow.py +392 -75
  8. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modulegui.py +21 -9
  9. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/fitsheaderswidget.ui +12 -0
  10. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +1 -0
  11. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/mainwindow.ui +6 -0
  12. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/mainwindow_ui.py +2 -1
  13. pyobs_gui-2.1.0/pyobs_gui/qt/roboticwidget.ui +210 -0
  14. pyobs_gui-2.1.0/pyobs_gui/qt/roboticwidget_ui.py +191 -0
  15. pyobs_gui-2.1.0/pyobs_gui/qt/schedulewidget.ui +107 -0
  16. pyobs_gui-2.1.0/pyobs_gui/qt/schedulewidget_ui.py +105 -0
  17. pyobs_gui-2.1.0/pyobs_gui/roboticwidget.py +115 -0
  18. pyobs_gui-2.1.0/pyobs_gui/schedulewidget.py +129 -0
  19. pyobs_gui-2.1.0/pyobs_gui/structuredconfigwidget.py +247 -0
  20. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/telescopewidget.py +8 -13
  21. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyproject.toml +2 -2
  22. pyobs_gui-2.1.0/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +506 -0
  23. pyobs_gui-2.1.0/specs/2026-08-28-structuredconfig-widget.md +118 -0
  24. pyobs_gui-2.1.0/specs/2026-08-31-irobotic-widgets.md +143 -0
  25. pyobs_gui-2.1.0/specs/2026-09-01-gui-video-widget-split.md +107 -0
  26. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/index.md +20 -0
  27. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/acquisition.yaml +0 -2
  28. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/autofocus.yaml +0 -2
  29. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/camera.yaml +0 -2
  30. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/full.yaml +0 -9
  31. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/guiding.yaml +0 -2
  32. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/mode.yaml +0 -2
  33. pyobs_gui-2.1.0/test/robotic.yaml +33 -0
  34. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/roof.yaml +0 -2
  35. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/spectrograph.yaml +0 -2
  36. pyobs_gui-2.1.0/test/structuredconfig.yaml +16 -0
  37. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope.yaml +0 -2
  38. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope_acl.yaml +0 -2
  39. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope_acl_denied.yaml +0 -2
  40. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/video.yaml +0 -2
  41. pyobs_gui-2.1.0/tests/test_multiwidget_pages.py +593 -0
  42. pyobs_gui-2.1.0/tests/test_roboticwidget.py +133 -0
  43. pyobs_gui-2.1.0/tests/test_schedulewidget.py +129 -0
  44. pyobs_gui-2.1.0/tests/test_structuredconfigwidget.py +253 -0
  45. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/uv.lock +74 -74
  46. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.claude/skills/verify/SKILL.md +0 -0
  47. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/dependabot.yml +0 -0
  48. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/dependabot-automerge.yml +0 -0
  49. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/pypi.yml +0 -0
  50. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/pyrefly.yml +0 -0
  51. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/ruff.yml +0 -0
  52. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.gitignore +0 -0
  53. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.pre-commit-config.yaml +0 -0
  54. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.readthedocs.yml +0 -0
  55. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/CHANGELOG.rst +0 -0
  56. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/LICENSE +0 -0
  57. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/README.md +0 -0
  58. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/bin/mypyuic5 +0 -0
  59. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/Makefile +0 -0
  60. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/make.bat +0 -0
  61. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/requirements.txt +0 -0
  62. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/_static/pyobs.gif +0 -0
  63. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/conf.py +0 -0
  64. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/mypy.ini +0 -0
  65. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/__main__.py +0 -0
  66. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/accounts.py +0 -0
  67. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/acquisitionwidget.py +0 -0
  68. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/autofocuswidget.py +0 -0
  69. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/autoguidingwidget.py +0 -0
  70. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/commandinputwidget.py +0 -0
  71. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/compassmovewidget.py +0 -0
  72. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/coolingwidget.py +0 -0
  73. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/datadisplaywidget.py +0 -0
  74. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/eventswidget.py +0 -0
  75. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/filterwidget.py +0 -0
  76. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/fitsheaderswidget.py +0 -0
  77. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/focuswidget.py +0 -0
  78. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/gui.py +0 -0
  79. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/login.py +0 -0
  80. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/loginwindow.py +0 -0
  81. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/logmodel.py +0 -0
  82. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modewidget.py +0 -0
  83. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modifiedmixin.py +0 -0
  84. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/__init__.py +0 -0
  85. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  86. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  87. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  88. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  89. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  90. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  91. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/camerawidget.ui +0 -0
  92. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  93. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  94. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
  95. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  96. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  97. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compile.sh +0 -0
  98. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
  99. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  100. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  101. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  102. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  103. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/designer.sh +0 -0
  104. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/eventswidget.ui +0 -0
  105. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  106. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/filterwidget.ui +0 -0
  107. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  108. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/focuswidget.ui +0 -0
  109. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  110. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/modewidget.ui +0 -0
  111. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
  112. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  113. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  114. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  115. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  116. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  117. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  118. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  119. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  120. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  121. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  122. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  123. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  124. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources.qrc +0 -0
  125. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources_rc.py +0 -0
  126. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/roofwidget.ui +0 -0
  127. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  128. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/shellwidget.ui +0 -0
  129. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  130. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  131. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  132. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
  133. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  134. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  135. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  136. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  137. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  138. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/videowidget.ui +0 -0
  139. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
  140. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
  141. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  142. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/roofwidget.py +0 -0
  143. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/shellwidget.py +0 -0
  144. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/spectrographwidget.py +0 -0
  145. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/statuswidget.py +0 -0
  146. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
  147. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/temperatureswidget.py +0 -0
  148. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/utils.py +0 -0
  149. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/videowidget.py +0 -0
  150. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/watchedlabel.py +0 -0
  151. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/weatherwidget.py +0 -0
  152. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pysidedeploy.spec +0 -0
  153. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  154. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  155. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/spike_standalone/main.py +0 -0
  156. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/spike_standalone/pysidedeploy.spec +0 -0
  157. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/__init__.py +0 -0
  158. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/conftest.py +0 -0
  159. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_accounts.py +0 -0
  160. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_camerawidget.py +0 -0
  161. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_loginwindow.py +0 -0
  162. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_mainwindow_startup.py +0 -0
  163. {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_videowidget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.0.0.dev22
3
+ Version: 2.1.0
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Requires-Dist: astropy<8,>=7.0.1
11
11
  Requires-Dist: colour<0.2,>=0.1.5
12
12
  Requires-Dist: keyring<26,>=25.7.0
13
13
  Requires-Dist: matplotlib<4,>=3.10.1
14
- Requires-Dist: pyobs-core<3,>=2.0.0.dev93
14
+ Requires-Dist: pyobs-core<3,>=2.2.0
15
15
  Requires-Dist: pyside6>=6.10.1
16
16
  Requires-Dist: qasync<0.29,>=0.27.1
17
17
  Requires-Dist: qfitswidget>=0.13.1
@@ -18,6 +18,11 @@ This is an example configuration for a GUI that defines one custom widget for th
18
18
  widget:
19
19
  class: mypackage.GuidingWidget
20
20
 
21
+ sidebar:
22
+ - module: guiding
23
+ widget:
24
+ class: mypackage.GuidingExtraSidebarWidget
25
+
21
26
  comm:
22
27
  jid: test@example.com
23
28
  password: ***
@@ -54,9 +59,39 @@ GUI
54
59
  Widgets
55
60
  *******
56
61
 
57
- The GUI shows one page per connected module, picking a widget by the module's interfaces
58
- (``DEFAULT_WIDGETS`` in ``mainwindow.py``), plus three always-present "Tools" pages. No screenshots
59
- yet — this section was written from source, not from a running GUI; add screenshots as a follow-up.
62
+ The GUI shows one page per connected module, matching its interfaces against an ordered registry
63
+ of **main widgets** (``MAIN_WIDGETS`` in ``mainwindow.py``) see below for the current list — plus
64
+ three always-present "Tools" pages. No screenshots yet — this section was written from source, not
65
+ from a running GUI; add screenshots as a follow-up.
66
+
67
+ Main widgets vs. sidebar widgets
68
+ =================================
69
+ Every entry in the registry drives either a **main widget** (fills the page) or is declared as a
70
+ **sidebar widget** for one or more main widgets (lives only in the page's sidebar, e.g. filter
71
+ wheel, focuser, temperature, cooling and FITS-header controls). A module matching several main
72
+ widgets gets one nav entry whose page is a tab widget, one tab per main widget (e.g. a camera
73
+ module that is also a filter wheel shows a "Camera" tab, with filter controls in the shared
74
+ sidebar rather than a separate tab); one match keeps a plain, tab-bar-less page; the sidebar itself
75
+ is shared across every tab, so e.g. FITS-header info stays visible while switching tabs. Some
76
+ interfaces (``IFilters``, ``IFocuser``, ``ITemperatures``, ``ICooling``) are *sidebar-preferred*:
77
+ they fill a sidebar slot on another matched main widget by default, but still get their own page if
78
+ a module implements one of them standalone (e.g. a bare filter-wheel-only module).
79
+
80
+ Custom ``widgets:``/``sidebar:`` config
81
+ ==========================================
82
+ Each ``widgets:`` entry (``module``, ``widget``, optional ``label``, ``icon``, ``interface``,
83
+ ``overwrite``) targets one connected module:
84
+
85
+ - ``interface: ICamera`` replaces/merges only the ``ICamera``-derived tab for that module (same
86
+ registry slot, so tab order is stable); ignored with a log message if the module doesn't
87
+ implement it.
88
+ - No ``interface`` and no ``overwrite`` — the entry is appended as an **extra tab** alongside
89
+ whatever the registry already matched.
90
+ - No ``interface`` and ``overwrite: true`` — the page becomes **exactly** the custom entries (the
91
+ ``guiding`` example above).
92
+
93
+ ``sidebar:`` entries (``module``, ``widget``) are always appended to the page's sidebar, visible
94
+ regardless of the module's matched interfaces.
60
95
 
61
96
  Shell
62
97
  =====
@@ -118,6 +153,16 @@ Filter wheel — drives ``IFilters``
118
153
  =====================================
119
154
  Set the active filter.
120
155
 
156
+ Temperatures — drives ``ITemperatures``
157
+ ==========================================
158
+ Live sensor readout. Normally a sidebar block on a camera/telescope page; a temperature-sensor-only
159
+ module gets its own page.
160
+
161
+ Cooling — drives ``ICooling``
162
+ ================================
163
+ Set/get the cooling setpoint. Normally a sidebar block on a camera page; a cooling-only module
164
+ gets its own page.
165
+
121
166
 
122
167
  Keyboard shortcuts
123
168
  *******************
@@ -6,7 +6,10 @@ __title__ = "GUI"
6
6
 
7
7
  from ._nuitka_astropy_patch import patch_generic_unit_parser
8
8
 
9
- patch_generic_unit_parser()
9
+ # Only relevant in the Nuitka-compiled standalone binary: under a regular interpreter the
10
+ # patch must not install, or it breaks astropy's unit/coordinate parsing (issue #151). The
11
+ # marker is evaluated here because pyobs_gui/__init__.py is guaranteed compiled in the binary.
12
+ patch_generic_unit_parser(compiled="__compiled__" in globals())
10
13
 
11
14
  from .gui import GUI
12
15
  from .camerawidget import CameraWidget
@@ -34,6 +34,23 @@ unconditionally at startup. astropy's cds/ogip/vounit formats and astropy.coordi
34
34
  the same PLY pattern and would need the identical treatment if/when they're actually exercised in
35
35
  the compiled binary.
36
36
 
37
+ Two caveats learned the hard way (pyobs-gui issue #151):
38
+
39
+ - **The patch must be a strict no-op under a regular interpreter.** The wrapper functions below
40
+ add one stack frame, which shifts astropy's frame-level bookkeeping: ``astropy.utils.parsing``
41
+ calls ``get_caller_module_dict(2)`` directly, and its own ``_patch_ply_module`` wrapper adds two
42
+ more levels, so the extra frame makes the walk land on astropy's parsing wrapper instead of the
43
+ grammar-defining frame -- the fallback fired on plain CPython and corrupted unit/coordinate
44
+ parsing when running from source ("Invalid coordinates" in the Telescope widget, astropy units
45
+ failing to import). Therefore the patched functions step one level deeper (``levels + 1``) to
46
+ restore the original frame targeting, and ``patch_generic_unit_parser`` only installs at all
47
+ when running under Nuitka (``"__compiled__" in globals()``); otherwise it returns immediately.
48
+ - **The vendored grammar must be a *registered* module.** PLY validates p-functions with
49
+ ``inspect.getmodule()``/``getsourcefile()``, which resolve the function's ``__module__`` through
50
+ ``sys.modules`` and want a real source file. ``_build_generic_grammar_namespace`` therefore
51
+ registers the namespace under its name and materializes its ``__file__`` stub; without that the
52
+ parser rebuild dies with "Unable to build parser" (``TypeError ... got NoneType``).
53
+
37
54
  Must run before ``astropy.units`` is imported anywhere, i.e. as the first thing in
38
55
  ``pyobs_gui/__init__.py``.
39
56
  """
@@ -41,6 +58,7 @@ Must run before ``astropy.units`` is imported anywhere, i.e. as the first thing
41
58
  from __future__ import annotations
42
59
 
43
60
  import re
61
+ import sys
44
62
  import tempfile
45
63
  import types
46
64
  from fractions import Fraction
@@ -76,6 +94,18 @@ def _build_generic_grammar_namespace() -> types.ModuleType:
76
94
  ns.__file__ = str(stub_dir / "generic.py")
77
95
  ns.tokens = tokens
78
96
 
97
+ # Register the namespace under its own name and materialize its __file__ so PLY's
98
+ # grammar validation can resolve it: ParserReflect.get_pfunctions() calls
99
+ # inspect.getmodule() on every p_* function (which looks the function's __module__ up
100
+ # in sys.modules), and validate_pfunctions() calls inspect.getsourcefile() on the
101
+ # result -- both need a real, registered module or the rebuild dies with
102
+ # "Unable to build parser" / "TypeError: ... got NoneType".
103
+ sys.modules.setdefault(ns.__name__, ns)
104
+ (stub_dir / "generic.py").write_text(
105
+ "# pyobs-gui: vendored astropy.units.format.generic PLY grammar (Nuitka workaround).\n",
106
+ encoding="utf-8",
107
+ )
108
+
79
109
  ns.t_COMMA = r"\,"
80
110
  ns.t_PRODUCT = "[*.]"
81
111
  ns.t_DIVISION = "/"
@@ -410,12 +440,23 @@ def _namespace_pdict(ns: types.ModuleType) -> dict[str, Any]:
410
440
  return {k: getattr(ns, k) for k in dir(ns)}
411
441
 
412
442
 
413
- def patch_generic_unit_parser() -> None:
443
+ def patch_generic_unit_parser(*, compiled: bool | None = None) -> None:
414
444
  """Patch astropy.extern.ply's frame-walking helpers to fall back to our vendored grammar.
415
445
 
416
- Safe to call unconditionally (frozen or not) -- under a normal, unfrozen interpreter the
417
- original frame walk always succeeds, so the fallback path never triggers.
446
+ Only intended for the Nuitka-compiled standalone binary. Under a regular interpreter the
447
+ original frame walk always finds the grammar, and installing the wrapper would *shift* the
448
+ walk by one stack frame and break astropy's parsing (see module docstring / pyobs-gui issue
449
+ #151), so unless ``compiled`` is true this is a strict no-op.
450
+
451
+ Pass ``compiled`` as ``"__compiled__" in globals()`` evaluated in the entry package
452
+ (``pyobs_gui/__init__.py``), which is guaranteed to be compiled in the binary. When left as
453
+ None the marker is checked in this module's own globals instead.
418
454
  """
455
+ if compiled is None:
456
+ compiled = "__compiled__" in globals()
457
+ if not compiled:
458
+ return
459
+
419
460
  from astropy.extern.ply import lex as ply_lex
420
461
  from astropy.extern.ply import yacc as ply_yacc
421
462
 
@@ -423,6 +464,14 @@ def patch_generic_unit_parser() -> None:
423
464
  original_lex_dict = ply_lex.get_caller_module_dict
424
465
 
425
466
  def _fallback_pdict(pdict: dict[str, Any] | None) -> dict[str, Any]:
467
+ # Only the *generic unit* format's grammar is vendored here. astropy's other PLY
468
+ # grammars (angle/cds/ogip/vounit) would need the identical treatment, and feeding
469
+ # them the generic grammar would silently parse with the wrong productions (that is
470
+ # exactly how the binary's coordinate parsing broke: the angle parser got the unit
471
+ # grammar). For any other grammar return the broken-locals pdict unchanged so the
472
+ # build fails loudly instead of corrupting results.
473
+ if pdict is not None and pdict.get("__name__") != "astropy.units.format.generic":
474
+ return dict(pdict)
426
475
  # Keep whatever the real (possibly broken-locals) pdict already has -- __file__/
427
476
  # __package__/__name__ are module globals and come through fine even on a compiled
428
477
  # frame -- and only supplement the missing tokens/p_*/t_* grammar entries. Replacing
@@ -435,7 +484,12 @@ def patch_generic_unit_parser() -> None:
435
484
 
436
485
  def patched_yacc_dict(levels: int) -> dict[str, Any]:
437
486
  try:
438
- pdict = original_yacc_dict(levels)
487
+ # Our wrapper adds one frame compared with the unpatched call chain (the original
488
+ # walk is now called from here instead of directly from astropy's parsing.yacc /
489
+ # _patch_ply_module wrapper), so step one level deeper to land on the same frame
490
+ # the original walk would have inspected. Without this the fallback fires under
491
+ # plain CPython and astropy's parsing breaks (pyobs-gui issue #151).
492
+ pdict = original_yacc_dict(levels + 1)
439
493
  except Exception:
440
494
  return _fallback_pdict(None)
441
495
  if "tokens" not in pdict:
@@ -444,7 +498,7 @@ def patch_generic_unit_parser() -> None:
444
498
 
445
499
  def patched_lex_dict(levels: int) -> dict[str, Any]:
446
500
  try:
447
- pdict = original_lex_dict(levels)
501
+ pdict = original_lex_dict(levels + 1)
448
502
  except Exception:
449
503
  return _fallback_pdict(None)
450
504
  if "tokens" not in pdict:
@@ -241,8 +241,17 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
241
241
  if hasattr(self, "widgetSidebar"):
242
242
  self.widgetSidebar.setLayout(self.sidebar_layout)
243
243
 
244
- # open it
245
- await self._open_child(widget)
244
+ # open it -- a failing sidebar widget is logged and dropped rather than left to propagate:
245
+ # otherwise one flaky sidebar widget (e.g. a filter wheel briefly unreachable) would kill
246
+ # the whole page it's attached to, including every already-open main widget/tab
247
+ try:
248
+ await self._open_child(widget)
249
+ except asyncio.CancelledError:
250
+ raise
251
+ except Exception:
252
+ log.exception("Failed to open sidebar widget %s; dropping it.", type(widget).__name__)
253
+ await widget.discard()
254
+ return
246
255
 
247
256
  # append widget
248
257
  self.sidebar_widgets.append(widget)
@@ -251,7 +260,7 @@ class BaseWidget(BaseWindow, QtWidgets.QWidget): # type: ignore
251
260
  async def register_event(self, event_class: type[Event], handler: Callable[[Event, str], Any]) -> None:
252
261
  """Register an event handler through comm, tracked so discard() can unregister it later.
253
262
 
254
- Widgets that are created/destroyed per connected client (see DEFAULT_WIDGETS in
263
+ Widgets that are created/destroyed per connected client (see MAIN_WIDGETS in
255
264
  mainwindow.py) must use this instead of calling self.comm.register_event() directly,
256
265
  so their handler stops firing once the widget is discarded.
257
266
  """
@@ -17,9 +17,6 @@ from pyobs.interfaces import (
17
17
  IBinning,
18
18
  BinningState,
19
19
  IWindow,
20
- IFilters,
21
- ICooling,
22
- ITemperatures,
23
20
  IGain,
24
21
  GainState,
25
22
  IExposure,
@@ -30,9 +27,6 @@ from pyobs.interfaces import (
30
27
  from pyobs.utils.enums import ImageType, ImageFormat, ExposureStatus
31
28
  from pyobs.vfs import VirtualFileSystem
32
29
  from .base import BaseWidget
33
- from .coolingwidget import CoolingWidget
34
- from .filterwidget import FilterWidget
35
- from .temperatureswidget import TemperaturesWidget
36
30
  from .fitsheaderswidget import FitsHeadersWidget
37
31
  from .qt.camerawidget_ui import Ui_CameraWidget
38
32
 
@@ -50,6 +44,15 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
50
44
  signal_update_gui = QtCore.Signal()
51
45
  signal_new_image = QtCore.Signal(NewImageEvent, str)
52
46
 
47
+ # declared sidebar fills (mainwindow.py, MAIN_WIDGETS/collect_main_widgets): consulted via
48
+ # getattr(widget_class, "sidebar_fills", ...) so a site that wires CameraWidget in via
49
+ # custom widgets: config still gets these. IFilters/ICooling/ITemperatures are NOT listed
50
+ # here -- they're sidebar_preferred MAIN_WIDGETS entries, already demoted into the sidebar by
51
+ # the promotion rule; declaring them here too would add each one twice (PR #157 review).
52
+ sidebar_fills = [
53
+ (None, FitsHeadersWidget),
54
+ ]
55
+
53
56
  def __init__(self, **kwargs: Any):
54
57
  BaseWidget.__init__(self, **kwargs)
55
58
  self.setupUi(self) # type: ignore
@@ -106,14 +109,9 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
106
109
  self.butExpose.clicked.connect(self.expose)
107
110
  self.butAbort.clicked.connect(self.abort)
108
111
 
109
- # fill sidebar
110
- await self.add_to_sidebar(self.create_widget(FitsHeadersWidget, module=self.module))
111
- if await self.comm.has_proxy(self.module, IFilters):
112
- await self.add_to_sidebar(self.create_widget(FilterWidget, module=self.module))
113
- if await self.comm.has_proxy(self.module, ICooling):
114
- await self.add_to_sidebar(self.create_widget(CoolingWidget, module=self.module))
115
- if await self.comm.has_proxy(self.module, ITemperatures):
116
- await self.add_to_sidebar(self.create_widget(TemperaturesWidget, module=self.module))
112
+ # sidebar fills are applied by the page assembler (mainwindow.py: open_module_page),
113
+ # driven by this class's sidebar_fills attribute -- see specs/2026-08-28-gui-main-vs-
114
+ # sidebar-widgets.md, D2
117
115
 
118
116
  async def _init(self) -> None:
119
117
  # every interface is initialized independently (caps -> state -> subscribe, in that