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.
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/PKG-INFO +2 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/index.rst +48 -3
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/__init__.py +4 -1
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/_nuitka_astropy_patch.py +59 -5
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/base.py +12 -3
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/camerawidget.py +12 -14
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/mainwindow.py +392 -75
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modulegui.py +21 -9
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/fitsheaderswidget.ui +12 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +1 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/mainwindow.ui +6 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/mainwindow_ui.py +2 -1
- pyobs_gui-2.1.0/pyobs_gui/qt/roboticwidget.ui +210 -0
- pyobs_gui-2.1.0/pyobs_gui/qt/roboticwidget_ui.py +191 -0
- pyobs_gui-2.1.0/pyobs_gui/qt/schedulewidget.ui +107 -0
- pyobs_gui-2.1.0/pyobs_gui/qt/schedulewidget_ui.py +105 -0
- pyobs_gui-2.1.0/pyobs_gui/roboticwidget.py +115 -0
- pyobs_gui-2.1.0/pyobs_gui/schedulewidget.py +129 -0
- pyobs_gui-2.1.0/pyobs_gui/structuredconfigwidget.py +247 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/telescopewidget.py +8 -13
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyproject.toml +2 -2
- pyobs_gui-2.1.0/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +506 -0
- pyobs_gui-2.1.0/specs/2026-08-28-structuredconfig-widget.md +118 -0
- pyobs_gui-2.1.0/specs/2026-08-31-irobotic-widgets.md +143 -0
- pyobs_gui-2.1.0/specs/2026-09-01-gui-video-widget-split.md +107 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/index.md +20 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/acquisition.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/autofocus.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/camera.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/full.yaml +0 -9
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/guiding.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/mode.yaml +0 -2
- pyobs_gui-2.1.0/test/robotic.yaml +33 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/roof.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/spectrograph.yaml +0 -2
- pyobs_gui-2.1.0/test/structuredconfig.yaml +16 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope_acl.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/telescope_acl_denied.yaml +0 -2
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/test/video.yaml +0 -2
- pyobs_gui-2.1.0/tests/test_multiwidget_pages.py +593 -0
- pyobs_gui-2.1.0/tests/test_roboticwidget.py +133 -0
- pyobs_gui-2.1.0/tests/test_schedulewidget.py +129 -0
- pyobs_gui-2.1.0/tests/test_structuredconfigwidget.py +253 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/uv.lock +74 -74
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/dependabot-automerge.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.gitignore +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/.readthedocs.yml +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/LICENSE +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/README.md +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/Makefile +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/make.bat +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/requirements.txt +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/docs/source/conf.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/mypy.ini +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/__main__.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/accounts.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/gui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/login.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/loginwindow.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/logmodel.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/camerawidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/shellwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/statuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/utils.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/videowidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pyobs_gui/weatherwidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/pysidedeploy.spec +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/spike_standalone/main.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/spike_standalone/pysidedeploy.spec +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/conftest.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_accounts.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_camerawidget.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_loginwindow.py +0 -0
- {pyobs_gui-2.0.0.dev22 → pyobs_gui-2.1.0}/tests/test_mainwindow_startup.py +0 -0
- {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.
|
|
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.
|
|
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,
|
|
58
|
-
(``
|
|
59
|
-
yet — this section was written from source, not
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
original frame walk always
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
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
|