pyobs-gui 2.0.0.dev5__tar.gz → 2.0.0.dev7__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 (147) hide show
  1. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.claude/skills/verify/SKILL.md +1 -1
  2. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.gitignore +5 -0
  3. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/PKG-INFO +1 -1
  4. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/__init__.py +4 -0
  5. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/__main__.py +4 -4
  6. pyobs_gui-2.0.0.dev7/pyobs_gui/_nuitka_astropy_patch.py +453 -0
  7. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/mainwindow.py +3 -3
  8. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/weatherwidget.py +51 -1
  9. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyproject.toml +10 -1
  10. pyobs_gui-2.0.0.dev7/pysidedeploy.spec +97 -0
  11. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/specs/README.md +14 -0
  12. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/uv.lock +1 -1
  13. pyobs_gui-2.0.0.dev5/DEVELOPMENT.md +0 -155
  14. pyobs_gui-2.0.0.dev5/DEV_IAcquisition.md +0 -95
  15. pyobs_gui-2.0.0.dev5/DEV_IAutoFocus.md +0 -162
  16. pyobs_gui-2.0.0.dev5/DEV_IAutoGuiding.md +0 -156
  17. pyobs_gui-2.0.0.dev5/DEV_NavbarShortcuts.md +0 -286
  18. pyobs_gui-2.0.0.dev5/DEV_qml_evaluation.md +0 -218
  19. pyobs_gui-2.0.0.dev5/DEV_telescopewidget_layout.md +0 -199
  20. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.github/dependabot.yml +0 -0
  21. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.github/workflows/pypi.yml +0 -0
  22. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.github/workflows/pyrefly.yml +0 -0
  23. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.github/workflows/ruff.yml +0 -0
  24. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.pre-commit-config.yaml +0 -0
  25. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/.readthedocs.yml +0 -0
  26. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/CHANGELOG.rst +0 -0
  27. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/LICENSE +0 -0
  28. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/README.md +0 -0
  29. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/bin/mypyuic5 +0 -0
  30. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/Makefile +0 -0
  31. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/make.bat +0 -0
  32. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/requirements.txt +0 -0
  33. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/source/_static/pyobs.gif +0 -0
  34. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/source/conf.py +0 -0
  35. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/docs/source/index.rst +0 -0
  36. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/mypy.ini +0 -0
  37. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/accounts.py +0 -0
  38. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/acquisitionwidget.py +0 -0
  39. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/autofocuswidget.py +0 -0
  40. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/autoguidingwidget.py +0 -0
  41. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/base.py +0 -0
  42. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/camerawidget.py +0 -0
  43. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/commandinputwidget.py +0 -0
  44. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/compassmovewidget.py +0 -0
  45. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/coolingwidget.py +0 -0
  46. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/datadisplaywidget.py +0 -0
  47. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/eventswidget.py +0 -0
  48. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/filterwidget.py +0 -0
  49. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/fitsheaderswidget.py +0 -0
  50. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/focuswidget.py +0 -0
  51. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/gui.py +0 -0
  52. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/login.py +0 -0
  53. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/loginwindow.py +0 -0
  54. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/logmodel.py +0 -0
  55. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/modewidget.py +0 -0
  56. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/modifiedmixin.py +0 -0
  57. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/modulegui.py +0 -0
  58. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/__init__.py +0 -0
  59. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  60. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  61. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  62. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  63. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  64. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  65. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/camerawidget.ui +0 -0
  66. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  67. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  68. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/compassmovewidget.py +0 -0
  69. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  70. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  71. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/compile.sh +0 -0
  72. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/coolingwidget.ui +0 -0
  73. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  74. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  75. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  76. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  77. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/designer.sh +0 -0
  78. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/eventswidget.ui +0 -0
  79. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  80. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/filterwidget.ui +0 -0
  81. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  82. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  83. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  84. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/focuswidget.ui +0 -0
  85. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  86. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/mainwindow.ui +0 -0
  87. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  88. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/modewidget.ui +0 -0
  89. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/modewidget_ui.py +0 -0
  90. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  91. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  92. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  93. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  94. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  95. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  96. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  97. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  98. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  99. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  100. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  101. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  102. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources.qrc +0 -0
  103. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/resources_rc.py +0 -0
  104. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/roofwidget.ui +0 -0
  105. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  106. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/shellwidget.ui +0 -0
  107. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  108. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  109. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  110. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/telescopewidget.ui +0 -0
  111. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  112. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  113. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  114. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  115. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  116. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/videowidget.ui +0 -0
  117. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/videowidget_ui.py +0 -0
  118. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/weatherwidget.ui +0 -0
  119. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  120. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/roofwidget.py +0 -0
  121. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/shellwidget.py +0 -0
  122. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/spectrographwidget.py +0 -0
  123. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/statuswidget.py +0 -0
  124. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/telescopewidget.py +0 -0
  125. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/temperaturesplotwidget.py +0 -0
  126. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/temperatureswidget.py +0 -0
  127. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/utils.py +0 -0
  128. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/videowidget.py +0 -0
  129. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/pyobs_gui/watchedlabel.py +0 -0
  130. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/spike_standalone/main.py +0 -0
  131. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/spike_standalone/pysidedeploy.spec +0 -0
  132. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/acquisition.yaml +0 -0
  133. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/autofocus.yaml +0 -0
  134. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/camera.yaml +0 -0
  135. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/full.yaml +0 -0
  136. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/guiding.yaml +0 -0
  137. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/mode.yaml +0 -0
  138. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/roof.yaml +0 -0
  139. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/spectrograph.yaml +0 -0
  140. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/telescope.yaml +0 -0
  141. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/telescope_acl.yaml +0 -0
  142. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/telescope_acl_denied.yaml +0 -0
  143. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/test/video.yaml +0 -0
  144. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/tests/__init__.py +0 -0
  145. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/tests/conftest.py +0 -0
  146. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/tests/test_accounts.py +0 -0
  147. {pyobs_gui-2.0.0.dev5 → pyobs_gui-2.0.0.dev7}/tests/test_loginwindow.py +0 -0
@@ -6,7 +6,7 @@ description: Drive the real pyobs-gui app headlessly against a real pyobs module
6
6
  # Verifying pyobs-gui changes
7
7
 
8
8
  pyobs-gui is a PySide6 app that talks to pyobs modules over `Comm`. There's no
9
- pytest suite (see `DEVELOPMENT.md`) — verification means running the real
9
+ pytest suite — verification means running the real
10
10
  `GUI` module against a real target module and observing widget state /
11
11
  screenshots. This works headlessly, no Xvfb needed.
12
12
 
@@ -6,3 +6,8 @@ spike_standalone/deployment/
6
6
  spike_standalone/*.dist/
7
7
  spike_standalone/build*.log
8
8
  spike_standalone/spike_results.txt
9
+
10
+ # pyside6-deploy build output for the real app -- generated, not source
11
+ deployment/
12
+ dist/
13
+ build*.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-gui
3
- Version: 2.0.0.dev5
3
+ Version: 2.0.0.dev7
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -4,6 +4,10 @@ TODO: write doc
4
4
 
5
5
  __title__ = "GUI"
6
6
 
7
+ from ._nuitka_astropy_patch import patch_generic_unit_parser
8
+
9
+ patch_generic_unit_parser()
10
+
7
11
  from .gui import GUI
8
12
  from .camerawidget import CameraWidget
9
13
  from .datadisplaywidget import DataDisplayWidget
@@ -6,13 +6,13 @@ unless the user opts into "store password" in the login window.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from pyobs.application import Application
10
9
 
11
- from .gui import GUI
12
- from .login import login_and_build_gui
10
+ def main() -> None:
11
+ from pyobs.application import Application
13
12
 
13
+ from .gui import GUI
14
+ from .login import login_and_build_gui
14
15
 
15
- def main() -> None:
16
16
  app = Application(module_factory=login_and_build_gui, loop_module_class=GUI)
17
17
  app.run()
18
18
 
@@ -0,0 +1,453 @@
1
+ """Work around a Nuitka/astropy incompatibility that breaks unit-string parsing in the
2
+ compiled standalone binary (see specs/plans/gui-widget-plugins-and-packaging.md).
3
+
4
+ astropy.units.format.generic builds its PLY grammar by having ``astropy.extern.ply.yacc``/
5
+ ``lex`` walk the call stack with ``sys._getframe()`` to auto-discover the ``tokens``/``p_*``/
6
+ ``t_*`` local variables defined inside ``Generic._parser``/``Generic._lexer``. Nuitka-compiled
7
+ function frames don't populate ``f_locals`` the way CPython's interpreted frames do, so that
8
+ walk finds no grammar and the very first ``import astropy.units`` (pulled in unconditionally by
9
+ pyobs-core's ``pyobs.utils.enums``) crashes with ``YaccError: Unable to build parser``. This is a
10
+ real, unresolved upstream limitation as of 2026-07, not a pyobs bug:
11
+ https://github.com/astropy/astropy/issues/15069
12
+ https://github.com/Nuitka/Nuitka/issues/2313
13
+
14
+ Fix: patch ``astropy.extern.ply.yacc.get_caller_module_dict``/``...lex.get_caller_module_dict``
15
+ (the two functions actually doing the broken frame walk) so that whenever the walk comes back
16
+ without a ``tokens`` entry, they fall back to a namespace we rebuild here from the same grammar
17
+ (copied verbatim from astropy.units.format.generic), handed to PLY in a way that doesn't need
18
+ frame introspection at all.
19
+
20
+ This has to patch at the ``astropy.extern.ply`` level, not by importing
21
+ ``astropy.units.format.generic`` and monkeypatching ``Generic`` directly, because of a
22
+ bootstrapping deadlock: merely importing ``astropy.units.core``/``astropy.units.format.generic``
23
+ runs ``astropy/units/__init__.py``, which (via ``astropy.units.astrophys``, defining physical
24
+ constants) *already* triggers the broken parser during that very import -- before a class-level
25
+ patch could ever be installed. Patching ``get_caller_module_dict`` needs only
26
+ ``astropy.extern.ply``, which has no dependency on ``astropy.units`` at all, so it's safe to
27
+ install before anything touches ``astropy.units``. The vendored grammar's own reference to
28
+ ``astropy.units.core.Unit``/``CompositeUnit``/``Generic`` is resolved lazily, only when the
29
+ fallback actually fires -- by then we're already being called from inside
30
+ ``Generic._parser``/``_lexer``'s own call chain, so those names are guaranteed importable.
31
+
32
+ Scoped to the generic unit format only -- the one format pyobs_gui is known to hit
33
+ unconditionally at startup. astropy's cds/ogip/vounit formats and astropy.coordinates.angles use
34
+ the same PLY pattern and would need the identical treatment if/when they're actually exercised in
35
+ the compiled binary.
36
+
37
+ Must run before ``astropy.units`` is imported anywhere, i.e. as the first thing in
38
+ ``pyobs_gui/__init__.py``.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import re
44
+ import tempfile
45
+ import types
46
+ from fractions import Fraction
47
+ from pathlib import Path
48
+ from typing import Any
49
+
50
+ _generic_ns_cache: dict[str, types.ModuleType] = {}
51
+
52
+
53
+ def _build_generic_grammar_namespace() -> types.ModuleType:
54
+ """Rebuild astropy.units.format.generic's PLY grammar as a plain namespace object.
55
+
56
+ Only called lazily, from inside the ``get_caller_module_dict`` fallback below -- by that
57
+ point ``astropy.units.core``/``astropy.units.format.generic`` are already far enough along
58
+ to import from (see module docstring).
59
+ """
60
+ from astropy.units.core import CompositeUnit, Unit
61
+ from astropy.units.format.generic import Generic
62
+
63
+ cls = Generic
64
+ tokens = cls._tokens
65
+
66
+ # astropy.utils.parsing wants to write a generated tab-file cache next to __file__'s
67
+ # directory. In a frozen binary, astropy/units/format/ isn't a real directory on disk (it's
68
+ # compiled into the binary), so point this at a writable scratch directory instead --
69
+ # writing there is harmless best-effort caching, not required for parsing to work.
70
+ stub_dir = Path(tempfile.gettempdir()) / "pyobs_gui_astropy_ply_stub"
71
+ stub_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ ns = types.ModuleType("_pyobs_gui_generic_grammar")
74
+ ns.__file__ = str(stub_dir / "generic.py")
75
+ ns.tokens = tokens
76
+
77
+ ns.t_COMMA = r"\,"
78
+ ns.t_PRODUCT = "[*.]"
79
+ ns.t_DIVISION = "/"
80
+ ns.t_POWER = r"\^|(\*\*)"
81
+ ns.t_OPEN_PAREN = r"\("
82
+ ns.t_CLOSE_PAREN = r"\)"
83
+
84
+ def t_UFLOAT(t):
85
+ r"((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+)?"
86
+ if not re.search(r"[eE\.]", t.value):
87
+ t.type = "UINT"
88
+ t.value = int(t.value)
89
+ elif t.value.endswith("."):
90
+ t.type = "UINT"
91
+ t.value = int(t.value[:-1])
92
+ else:
93
+ t.value = float(t.value)
94
+ return t
95
+
96
+ def t_UINT(t):
97
+ r"\d+"
98
+ t.value = int(t.value)
99
+ return t
100
+
101
+ def t_SIGN(t):
102
+ r"[+-](?=\d)"
103
+ t.value = int(t.value + "1")
104
+ return t
105
+
106
+ def t_FUNCNAME(t):
107
+ r"((sqrt)|(ln)|(exp)|(log)|(mag)|(dB)|(dex))(?=\ *\()"
108
+ return t
109
+
110
+ def t_UNIT(t):
111
+ r"[^\s\d+\-\./\*\^\(\)\,]+"
112
+ t.value = cls._get_unit(t)
113
+ return t
114
+
115
+ ns.t_ignore = " "
116
+
117
+ def t_error(t):
118
+ raise ValueError(f"Invalid character at col {t.lexpos}")
119
+
120
+ ns.t_UFLOAT = t_UFLOAT
121
+ ns.t_UINT = t_UINT
122
+ ns.t_SIGN = t_SIGN
123
+ ns.t_FUNCNAME = t_FUNCNAME
124
+ ns.t_UNIT = t_UNIT
125
+ ns.t_error = t_error
126
+
127
+ def p_main(p):
128
+ """
129
+ main : unit
130
+ | structured_unit
131
+ | structured_subunit
132
+ """
133
+ if isinstance(p[1], tuple):
134
+ p[0] = p[1][0]
135
+ else:
136
+ p[0] = p[1]
137
+
138
+ def p_structured_subunit(p):
139
+ """
140
+ structured_subunit : OPEN_PAREN structured_unit CLOSE_PAREN
141
+ """
142
+ p[0] = (p[2],)
143
+
144
+ def p_structured_unit(p):
145
+ """
146
+ structured_unit : subunit COMMA
147
+ | subunit COMMA subunit
148
+ """
149
+ from astropy.units.structured import StructuredUnit
150
+
151
+ inputs = (p[1],) if len(p) == 3 else (p[1], p[3])
152
+ units = ()
153
+ for subunit in inputs:
154
+ if isinstance(subunit, tuple):
155
+ units += subunit
156
+ elif isinstance(subunit, StructuredUnit):
157
+ units += subunit.values()
158
+ else:
159
+ units += (subunit,)
160
+
161
+ p[0] = StructuredUnit(units)
162
+
163
+ def p_subunit(p):
164
+ """
165
+ subunit : unit
166
+ | structured_unit
167
+ | structured_subunit
168
+ """
169
+ p[0] = p[1]
170
+
171
+ def p_unit(p):
172
+ """
173
+ unit : product_of_units
174
+ | factor product_of_units
175
+ | factor PRODUCT product_of_units
176
+ | division_product_of_units
177
+ | factor division_product_of_units
178
+ | factor PRODUCT division_product_of_units
179
+ | inverse_unit
180
+ | factor inverse_unit
181
+ | factor PRODUCT inverse_unit
182
+ | factor
183
+ """
184
+ if len(p) == 2:
185
+ p[0] = Unit(p[1])
186
+ elif len(p) == 3:
187
+ p[0] = CompositeUnit(p[1] * p[2].scale, p[2].bases, p[2].powers)
188
+ elif len(p) == 4:
189
+ p[0] = CompositeUnit(p[1] * p[3].scale, p[3].bases, p[3].powers)
190
+
191
+ def p_division_product_of_units(p):
192
+ """
193
+ division_product_of_units : division_product_of_units DIVISION product_of_units
194
+ | product_of_units
195
+ """
196
+ if len(p) == 4:
197
+ p[0] = Unit(p[1] / p[3])
198
+ else:
199
+ p[0] = p[1]
200
+
201
+ def p_inverse_unit(p):
202
+ """
203
+ inverse_unit : DIVISION unit_expression
204
+ """
205
+ p[0] = p[2] ** -1
206
+
207
+ def p_factor(p):
208
+ """
209
+ factor : factor_fits
210
+ | factor_float
211
+ | factor_int
212
+ """
213
+ p[0] = p[1]
214
+
215
+ def p_factor_float(p):
216
+ """
217
+ factor_float : signed_float
218
+ | signed_float UINT signed_int
219
+ | signed_float UINT POWER numeric_power
220
+ """
221
+ if cls.name == "fits":
222
+ raise ValueError("Numeric factor not supported by FITS")
223
+ if len(p) == 4:
224
+ p[0] = p[1] * p[2] ** float(p[3])
225
+ elif len(p) == 5:
226
+ p[0] = p[1] * p[2] ** float(p[4])
227
+ elif len(p) == 2:
228
+ p[0] = p[1]
229
+
230
+ def p_factor_int(p):
231
+ """
232
+ factor_int : UINT
233
+ | UINT signed_int
234
+ | UINT POWER numeric_power
235
+ | UINT UINT signed_int
236
+ | UINT UINT POWER numeric_power
237
+ """
238
+ if cls.name == "fits":
239
+ raise ValueError("Numeric factor not supported by FITS")
240
+ if len(p) == 2:
241
+ p[0] = p[1]
242
+ elif len(p) == 3:
243
+ p[0] = p[1] ** float(p[2])
244
+ elif len(p) == 4:
245
+ if isinstance(p[2], int):
246
+ p[0] = p[1] * p[2] ** float(p[3])
247
+ else:
248
+ p[0] = p[1] ** float(p[3])
249
+ elif len(p) == 5:
250
+ p[0] = p[1] * p[2] ** p[4]
251
+
252
+ def p_factor_fits(p):
253
+ """
254
+ factor_fits : UINT POWER OPEN_PAREN signed_int CLOSE_PAREN
255
+ | UINT POWER OPEN_PAREN UINT CLOSE_PAREN
256
+ | UINT POWER signed_int
257
+ | UINT POWER UINT
258
+ | UINT SIGN UINT
259
+ | UINT OPEN_PAREN signed_int CLOSE_PAREN
260
+ """
261
+ if p[1] != 10:
262
+ if cls.name == "fits":
263
+ raise ValueError("Base must be 10")
264
+ else:
265
+ return
266
+ if len(p) == 4:
267
+ if p[2] in ("**", "^"):
268
+ p[0] = 10 ** p[3]
269
+ else:
270
+ p[0] = 10 ** (p[2] * p[3])
271
+ elif len(p) == 5:
272
+ p[0] = 10 ** p[3]
273
+ elif len(p) == 6:
274
+ p[0] = 10 ** p[4]
275
+
276
+ def p_product_of_units(p):
277
+ """
278
+ product_of_units : unit_expression PRODUCT product_of_units
279
+ | unit_expression product_of_units
280
+ | unit_expression
281
+ """
282
+ if len(p) == 2:
283
+ p[0] = p[1]
284
+ elif len(p) == 3:
285
+ p[0] = p[1] * p[2]
286
+ else:
287
+ p[0] = p[1] * p[3]
288
+
289
+ def p_unit_expression(p):
290
+ """
291
+ unit_expression : function
292
+ | unit_with_power
293
+ | OPEN_PAREN product_of_units CLOSE_PAREN
294
+ """
295
+ if len(p) == 2:
296
+ p[0] = p[1]
297
+ else:
298
+ p[0] = p[2]
299
+
300
+ def p_unit_with_power(p):
301
+ """
302
+ unit_with_power : UNIT POWER numeric_power
303
+ | UNIT numeric_power
304
+ | UNIT
305
+ """
306
+ if len(p) == 2:
307
+ p[0] = p[1]
308
+ elif len(p) == 3:
309
+ p[0] = p[1] ** p[2]
310
+ else:
311
+ p[0] = p[1] ** p[3]
312
+
313
+ def p_numeric_power(p):
314
+ """
315
+ numeric_power : sign UINT
316
+ | OPEN_PAREN paren_expr CLOSE_PAREN
317
+ """
318
+ if len(p) == 3:
319
+ p[0] = p[1] * p[2]
320
+ elif len(p) == 4:
321
+ p[0] = p[2]
322
+
323
+ def p_paren_expr(p):
324
+ """
325
+ paren_expr : sign UINT
326
+ | signed_float
327
+ | frac
328
+ """
329
+ if len(p) == 3:
330
+ p[0] = p[1] * p[2]
331
+ else:
332
+ p[0] = p[1]
333
+
334
+ def p_frac(p):
335
+ """
336
+ frac : sign UINT DIVISION sign UINT
337
+ """
338
+ p[0] = Fraction(p[1] * p[2], p[4] * p[5])
339
+
340
+ def p_sign(p):
341
+ """
342
+ sign : SIGN
343
+ |
344
+ """
345
+ if len(p) == 2:
346
+ p[0] = p[1]
347
+ else:
348
+ p[0] = 1
349
+
350
+ def p_signed_int(p):
351
+ """
352
+ signed_int : SIGN UINT
353
+ """
354
+ p[0] = p[1] * p[2]
355
+
356
+ def p_signed_float(p):
357
+ """
358
+ signed_float : sign UINT
359
+ | sign UFLOAT
360
+ """
361
+ p[0] = p[1] * p[2]
362
+
363
+ def p_function(p):
364
+ """
365
+ function : FUNCNAME OPEN_PAREN main CLOSE_PAREN
366
+ """
367
+ if p[1] == "sqrt":
368
+ p[0] = p[3] ** 0.5
369
+ return
370
+ elif p[1] in ("mag", "dB", "dex"):
371
+ function_unit = cls._validate_unit(p[1])
372
+ if callable(function_unit):
373
+ p[0] = function_unit(p[3])
374
+ return
375
+
376
+ raise ValueError(f"'{p[1]}' is not a recognized function")
377
+
378
+ def p_error(p):
379
+ raise ValueError()
380
+
381
+ ns.p_main = p_main
382
+ ns.p_structured_subunit = p_structured_subunit
383
+ ns.p_structured_unit = p_structured_unit
384
+ ns.p_subunit = p_subunit
385
+ ns.p_unit = p_unit
386
+ ns.p_division_product_of_units = p_division_product_of_units
387
+ ns.p_inverse_unit = p_inverse_unit
388
+ ns.p_factor = p_factor
389
+ ns.p_factor_float = p_factor_float
390
+ ns.p_factor_int = p_factor_int
391
+ ns.p_factor_fits = p_factor_fits
392
+ ns.p_product_of_units = p_product_of_units
393
+ ns.p_unit_expression = p_unit_expression
394
+ ns.p_unit_with_power = p_unit_with_power
395
+ ns.p_numeric_power = p_numeric_power
396
+ ns.p_paren_expr = p_paren_expr
397
+ ns.p_frac = p_frac
398
+ ns.p_sign = p_sign
399
+ ns.p_signed_int = p_signed_int
400
+ ns.p_signed_float = p_signed_float
401
+ ns.p_function = p_function
402
+ ns.p_error = p_error
403
+
404
+ return ns
405
+
406
+
407
+ def _namespace_pdict(ns: types.ModuleType) -> dict[str, Any]:
408
+ return {k: getattr(ns, k) for k in dir(ns)}
409
+
410
+
411
+ def patch_generic_unit_parser() -> None:
412
+ """Patch astropy.extern.ply's frame-walking helpers to fall back to our vendored grammar.
413
+
414
+ Safe to call unconditionally (frozen or not) -- under a normal, unfrozen interpreter the
415
+ original frame walk always succeeds, so the fallback path never triggers.
416
+ """
417
+ from astropy.extern.ply import lex as ply_lex
418
+ from astropy.extern.ply import yacc as ply_yacc
419
+
420
+ original_yacc_dict = ply_yacc.get_caller_module_dict
421
+ original_lex_dict = ply_lex.get_caller_module_dict
422
+
423
+ def _fallback_pdict(pdict: dict[str, Any] | None) -> dict[str, Any]:
424
+ # Keep whatever the real (possibly broken-locals) pdict already has -- __file__/
425
+ # __package__/__name__ are module globals and come through fine even on a compiled
426
+ # frame -- and only supplement the missing tokens/p_*/t_* grammar entries. Replacing
427
+ # __file__ wholesale would point astropy's tab-file bookkeeping at the wrong directory.
428
+ if "ns" not in _generic_ns_cache:
429
+ _generic_ns_cache["ns"] = _build_generic_grammar_namespace()
430
+ merged = dict(pdict) if pdict else {}
431
+ merged.update(_namespace_pdict(_generic_ns_cache["ns"]))
432
+ return merged
433
+
434
+ def patched_yacc_dict(levels: int) -> dict[str, Any]:
435
+ try:
436
+ pdict = original_yacc_dict(levels)
437
+ except Exception:
438
+ return _fallback_pdict(None)
439
+ if "tokens" not in pdict:
440
+ return _fallback_pdict(pdict)
441
+ return pdict
442
+
443
+ def patched_lex_dict(levels: int) -> dict[str, Any]:
444
+ try:
445
+ pdict = original_lex_dict(levels)
446
+ except Exception:
447
+ return _fallback_pdict(None)
448
+ if "tokens" not in pdict:
449
+ return _fallback_pdict(pdict)
450
+ return pdict
451
+
452
+ ply_yacc.get_caller_module_dict = patched_yacc_dict
453
+ ply_lex.get_caller_module_dict = patched_lex_dict
@@ -288,7 +288,7 @@ class MainWindow(QtWidgets.QMainWindow, BaseWindow, Ui_MainWindow): # type: ign
288
288
  self._modules_header_added = False
289
289
 
290
290
  # navbar keyboard shortcuts: slot -> currently bound page name, session-only (see
291
- # DEV_NavbarShortcuts.md)
291
+ # pyobs-core/specs/plans/gui-navbar-shortcuts.md)
292
292
  self._slot_bindings: Dict[str, str] = {}
293
293
  self.listPages.setItemDelegate(NavPageItemDelegate(self._slot_bindings, self))
294
294
  self._setup_shortcuts()
@@ -469,8 +469,8 @@ class MainWindow(QtWidgets.QMainWindow, BaseWindow, Ui_MainWindow): # type: ign
469
469
  self._widgets / self._slot_bindings at press-time, since module pages come and go but the
470
470
  shortcut objects themselves live for the app's lifetime. Every shortcut requires Ctrl (or
471
471
  Ctrl+Alt) so no text/numeric-entry widget can ever mistake one for ordinary input -- see
472
- DEV_NavbarShortcuts.md for why that's a structural guarantee rather than one that needs
473
- per-widget-type verification.
472
+ pyobs-core/specs/plans/gui-navbar-shortcuts.md for why that's a structural guarantee
473
+ rather than one that needs per-widget-type verification.
474
474
  """
475
475
  self._shortcuts: List[QtGui.QShortcut] = []
476
476
 
@@ -1,7 +1,14 @@
1
- from typing import Any, Dict, TYPE_CHECKING
1
+ import os
2
+ from collections import deque
3
+ from typing import Any, Deque, Dict, Tuple, TYPE_CHECKING
2
4
  from PySide6 import QtWidgets, QtGui, QtCore # type: ignore
3
5
  import logging
4
6
 
7
+ os.environ["QT_API"] = "PySide6"
8
+ from matplotlib import pyplot as plt
9
+ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
10
+ from matplotlib.dates import DateFormatter
11
+
5
12
  from pyobs.interfaces import IWeather, WeatherState, WeatherSensorReading
6
13
  from pyobs.utils.enums import WeatherSensors
7
14
  from .qt.weatherwidget_ui import Ui_WeatherWidget
@@ -13,6 +20,9 @@ if TYPE_CHECKING:
13
20
 
14
21
  log = logging.getLogger(__name__)
15
22
 
23
+ # how many samples to keep per sensor for the history plot
24
+ _HISTORY_LENGTH = 200
25
+
16
26
 
17
27
  # label and display order for known sensors; anything else reported by the module is ignored
18
28
  SENSOR_LABELS: Dict[WeatherSensors, str] = {
@@ -92,6 +102,14 @@ class WeatherWidget(BaseWidget, Ui_WeatherWidget):
92
102
  self._good: bool | None = None
93
103
  self._readings: Dict[WeatherSensors, WeatherSensorReading] = {}
94
104
  self._current_widgets: Dict[str, WidgetCurrentSensor] = {}
105
+ self._history: Dict[WeatherSensors, "Deque[Tuple[Time, float]]"] = {}
106
+
107
+ # add plot
108
+ self.figure = plt.figure()
109
+ layout = QtWidgets.QVBoxLayout(self.framePlot)
110
+ self.canvas = FigureCanvas(self.figure)
111
+ layout.addWidget(self.canvas)
112
+ self.framePlot.setLayout(layout)
95
113
 
96
114
  # before first update, disable myself
97
115
  self.setEnabled(False)
@@ -106,6 +124,11 @@ class WeatherWidget(BaseWidget, Ui_WeatherWidget):
106
124
  self._time = state.time
107
125
  self._good = state.good
108
126
  self._readings = {r.sensor: r for r in state.readings}
127
+ for reading in state.readings:
128
+ if reading.value is not None:
129
+ self._history.setdefault(reading.sensor, deque(maxlen=_HISTORY_LENGTH)).append(
130
+ (reading.time, reading.value)
131
+ )
109
132
  self.signal_update_gui.emit()
110
133
 
111
134
  def update_gui(self) -> None:
@@ -150,5 +173,32 @@ class WeatherWidget(BaseWidget, Ui_WeatherWidget):
150
173
  widget.set_value(s)
151
174
  widget.set_good(self._good)
152
175
 
176
+ self._plot_history()
177
+
178
+ def _plot_history(self) -> None:
179
+ # one stacked subplot per sensor that has history, in the same order as the current-value
180
+ # tiles; the sensor set (and therefore the subplot count) can change at runtime, so the
181
+ # figure is rebuilt from scratch rather than reusing fixed axes
182
+ sensors = [s for s in SENSOR_LABELS if self._history.get(s)]
183
+
184
+ self.figure.clear()
185
+ if not sensors:
186
+ self.canvas.draw()
187
+ return
188
+
189
+ axes = self.figure.subplots(len(sensors), 1, sharex=True)
190
+ if len(sensors) == 1:
191
+ axes = [axes]
192
+ for ax, sensor in zip(axes, sensors, strict=True):
193
+ times, values = zip(*self._history[sensor], strict=True)
194
+ ax.plot([t.to_datetime() for t in times], values, color="tab:blue")
195
+ ax.set_ylabel(SENSOR_LABELS[sensor], fontsize="small")
196
+ ax.grid(linestyle=":", alpha=0.5)
197
+ ax.set_axisbelow(True)
198
+ axes[-1].xaxis.set_major_formatter(DateFormatter("%H:%M:%S"))
199
+ self.figure.autofmt_xdate()
200
+ self.figure.tight_layout()
201
+ self.canvas.draw()
202
+
153
203
 
154
204
  __all__ = ["WeatherWidget"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.0.0.dev5"
3
+ version = "2.0.0.dev7"
4
4
  description = "A remote GUI for pyobs"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11,<3.14"
@@ -20,6 +20,9 @@ dependencies = [
20
20
  "secretstorage>=3.5.0,<4",
21
21
  ]
22
22
 
23
+ [project.scripts]
24
+ pyobs-gui = "pyobs_gui.__main__:main"
25
+
23
26
  [dependency-groups]
24
27
  dev = [
25
28
  "black>=25.1.0,<27",
@@ -56,6 +59,12 @@ ignore = ["E203", "E266", "E501", "F403", "E722"]
56
59
 
57
60
  [tool.ruff.lint.per-file-ignores]
58
61
  "__init__.py" = ["F401"]
62
+ # The astropy/Nuitka patch must run before any other pyobs_gui import (see its own docstring).
63
+ "pyobs_gui/__init__.py" = ["E402"]
64
+ # _build_generic_grammar_namespace is a verbatim transcription of astropy's own PLY grammar,
65
+ # deliberately mirroring astropy's structure for easy diffing -- splitting it up to satisfy
66
+ # mccabe would work against that goal.
67
+ "pyobs_gui/_nuitka_astropy_patch.py" = ["C901"]
59
68
 
60
69
  [tool.ruff.lint.mccabe]
61
70
  max-complexity = 30