pycontrol-gui 3.0.0a1__py3-none-any.whl
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.
- pycontrol_gui/__init__.py +15 -0
- pycontrol_gui/__main__.py +5 -0
- pycontrol_gui/app.py +102 -0
- pycontrol_gui/bus_adapter.py +54 -0
- pycontrol_gui/error_logging.py +134 -0
- pycontrol_gui/experiment_extensions.py +39 -0
- pycontrol_gui/main_window.py +894 -0
- pycontrol_gui/persistent_variables.py +55 -0
- pycontrol_gui/plotting/__init__.py +17 -0
- pycontrol_gui/plotting/base.py +38 -0
- pycontrol_gui/plotting/default_plotter.py +123 -0
- pycontrol_gui/plotting/host.py +364 -0
- pycontrol_gui/plotting/loader.py +48 -0
- pycontrol_gui/plotting/profiling.py +81 -0
- pycontrol_gui/plotting/widgets/__init__.py +7 -0
- pycontrol_gui/plotting/widgets/analog_plot.py +167 -0
- pycontrol_gui/plotting/widgets/events_plot.py +164 -0
- pycontrol_gui/plotting/widgets/states_plot.py +132 -0
- pycontrol_gui/resources/__init__.py +38 -0
- pycontrol_gui/resources/icons/add.svg +3 -0
- pycontrol_gui/resources/icons/bar-graph.svg +1 -0
- pycontrol_gui/resources/icons/book.svg +3 -0
- pycontrol_gui/resources/icons/circle-arrow-up.svg +3 -0
- pycontrol_gui/resources/icons/circle-arrow-up_white.svg +3 -0
- pycontrol_gui/resources/icons/close.svg +3 -0
- pycontrol_gui/resources/icons/close_white.svg +3 -0
- pycontrol_gui/resources/icons/collapse.svg +1 -0
- pycontrol_gui/resources/icons/comment.svg +3 -0
- pycontrol_gui/resources/icons/connect.svg +12 -0
- pycontrol_gui/resources/icons/copy.svg +1 -0
- pycontrol_gui/resources/icons/copy_evil.svg +1 -0
- pycontrol_gui/resources/icons/delete.svg +3 -0
- pycontrol_gui/resources/icons/detach.svg +1 -0
- pycontrol_gui/resources/icons/disable.svg +5 -0
- pycontrol_gui/resources/icons/disconnect.svg +13 -0
- pycontrol_gui/resources/icons/down.svg +3 -0
- pycontrol_gui/resources/icons/edit.svg +3 -0
- pycontrol_gui/resources/icons/enable.svg +4 -0
- pycontrol_gui/resources/icons/expand.svg +1 -0
- pycontrol_gui/resources/icons/filter.svg +3 -0
- pycontrol_gui/resources/icons/folder.svg +3 -0
- pycontrol_gui/resources/icons/funnel.svg +1 -0
- pycontrol_gui/resources/icons/github.svg +1 -0
- pycontrol_gui/resources/icons/help.svg +8 -0
- pycontrol_gui/resources/icons/info.svg +1 -0
- pycontrol_gui/resources/icons/keyboard.svg +1 -0
- pycontrol_gui/resources/icons/left.svg +3 -0
- pycontrol_gui/resources/icons/logo.svg +68 -0
- pycontrol_gui/resources/icons/mount.svg +1 -0
- pycontrol_gui/resources/icons/notion.svg +1 -0
- pycontrol_gui/resources/icons/pause.svg +3 -0
- pycontrol_gui/resources/icons/play.svg +3 -0
- pycontrol_gui/resources/icons/play_white.svg +3 -0
- pycontrol_gui/resources/icons/record.svg +10 -0
- pycontrol_gui/resources/icons/record_white.svg +8 -0
- pycontrol_gui/resources/icons/refresh.svg +3 -0
- pycontrol_gui/resources/icons/remove.svg +3 -0
- pycontrol_gui/resources/icons/right.svg +3 -0
- pycontrol_gui/resources/icons/run.svg +3 -0
- pycontrol_gui/resources/icons/run_white.svg +3 -0
- pycontrol_gui/resources/icons/save.svg +3 -0
- pycontrol_gui/resources/icons/save_as.svg +6 -0
- pycontrol_gui/resources/icons/settings.svg +4 -0
- pycontrol_gui/resources/icons/share.svg +1 -0
- pycontrol_gui/resources/icons/stop.svg +3 -0
- pycontrol_gui/resources/icons/stop_white.svg +3 -0
- pycontrol_gui/resources/icons/telegram.svg +1 -0
- pycontrol_gui/resources/icons/trash.svg +3 -0
- pycontrol_gui/resources/icons/up.svg +3 -0
- pycontrol_gui/resources/icons/upload.svg +6 -0
- pycontrol_gui/resources/icons/wrench.svg +4 -0
- pycontrol_gui/sandbox_session.py +83 -0
- pycontrol_gui/session_controller.py +605 -0
- pycontrol_gui/session_helpers.py +183 -0
- pycontrol_gui/session_runtime.py +194 -0
- pycontrol_gui/setup_maintenance.py +139 -0
- pycontrol_gui/task_variables.py +121 -0
- pycontrol_gui/update_check.py +283 -0
- pycontrol_gui/widgets/__init__.py +34 -0
- pycontrol_gui/widgets/about_dialog.py +103 -0
- pycontrol_gui/widgets/controls_dialog.py +828 -0
- pycontrol_gui/widgets/controls_dialog_editor.py +885 -0
- pycontrol_gui/widgets/controls_spec_io.py +36 -0
- pycontrol_gui/widgets/error_log_dialog.py +115 -0
- pycontrol_gui/widgets/experiment_editor_rows.py +70 -0
- pycontrol_gui/widgets/experiment_editor_table.py +137 -0
- pycontrol_gui/widgets/experiment_run_control.py +144 -0
- pycontrol_gui/widgets/experiment_run_models.py +48 -0
- pycontrol_gui/widgets/experiment_run_plots.py +306 -0
- pycontrol_gui/widgets/experiment_run_summary.py +113 -0
- pycontrol_gui/widgets/experiment_run_view.py +1288 -0
- pycontrol_gui/widgets/experiments_tab.py +1859 -0
- pycontrol_gui/widgets/file_tree.py +46 -0
- pycontrol_gui/widgets/hardware_variables_dialog.py +206 -0
- pycontrol_gui/widgets/keyboard_shortcuts_dialog.py +68 -0
- pycontrol_gui/widgets/log_filter_controller.py +218 -0
- pycontrol_gui/widgets/log_view.py +221 -0
- pycontrol_gui/widgets/run_task_tab.py +1598 -0
- pycontrol_gui/widgets/sandbox_dialog.py +422 -0
- pycontrol_gui/widgets/session_controls_shell.py +42 -0
- pycontrol_gui/widgets/session_event_ui.py +177 -0
- pycontrol_gui/widgets/session_panel.py +218 -0
- pycontrol_gui/widgets/session_shutdown_ui.py +42 -0
- pycontrol_gui/widgets/settings_dialog.py +728 -0
- pycontrol_gui/widgets/setups_tab.py +837 -0
- pycontrol_gui/widgets/share_dialog.py +162 -0
- pycontrol_gui/widgets/style.py +177 -0
- pycontrol_gui/widgets/subject_run_panel.py +1282 -0
- pycontrol_gui/widgets/task_controls_host.py +144 -0
- pycontrol_gui/widgets/task_selector_widgets.py +161 -0
- pycontrol_gui/widgets/task_status.py +177 -0
- pycontrol_gui/workspace_model.py +699 -0
- pycontrol_gui-3.0.0a1.dist-info/METADATA +228 -0
- pycontrol_gui-3.0.0a1.dist-info/RECORD +116 -0
- pycontrol_gui-3.0.0a1.dist-info/WHEEL +4 -0
- pycontrol_gui-3.0.0a1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Qt frontend package for pycontrol-core.
|
|
2
|
+
|
|
3
|
+
The stable package-level surface is intentionally small. GUI widgets,
|
|
4
|
+
controllers, and workspace models are application internals unless documented
|
|
5
|
+
in the public integration guides.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = version("pycontrol-gui")
|
|
12
|
+
except PackageNotFoundError: # pragma: no cover - editable tree before install.
|
|
13
|
+
__version__ = "0.0.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__"]
|
pycontrol_gui/app.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""QApplication bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import QObject, Qt, Signal
|
|
13
|
+
from PySide6.QtGui import QFont
|
|
14
|
+
from PySide6.QtWidgets import QApplication
|
|
15
|
+
|
|
16
|
+
from pycontrol.workspace import Workspace
|
|
17
|
+
from pycontrol_gui.error_logging import configure_error_logging
|
|
18
|
+
from pycontrol_gui.main_window import MainWindow
|
|
19
|
+
from pycontrol_gui.resources import icon
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _ExceptionNotifier(QObject):
|
|
25
|
+
message = Signal(str)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def launch_gui(start_path: str | Path | None = None) -> int:
|
|
29
|
+
resolved_start_path = _resolve_start_path(start_path)
|
|
30
|
+
configure_error_logging(resolved_start_path)
|
|
31
|
+
app = QApplication.instance()
|
|
32
|
+
created_here = app is None
|
|
33
|
+
if app is None:
|
|
34
|
+
app = QApplication(sys.argv)
|
|
35
|
+
|
|
36
|
+
app.setStyle("Fusion")
|
|
37
|
+
app.setWindowIcon(icon("logo"))
|
|
38
|
+
app.setApplicationDisplayName("pyControl GUI")
|
|
39
|
+
app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, False)
|
|
40
|
+
|
|
41
|
+
font = QFont()
|
|
42
|
+
font.setPixelSize(13)
|
|
43
|
+
app.setFont(font)
|
|
44
|
+
|
|
45
|
+
window = MainWindow(start_path=resolved_start_path)
|
|
46
|
+
_install_excepthook(window)
|
|
47
|
+
window.show()
|
|
48
|
+
if created_here:
|
|
49
|
+
return app.exec()
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_start_path(start_path: str | Path | None = None) -> Path | None:
|
|
54
|
+
if start_path is not None:
|
|
55
|
+
return Path(start_path).expanduser()
|
|
56
|
+
cli_path = _workspace_path_from_argv(sys.argv[1:])
|
|
57
|
+
if cli_path is not None:
|
|
58
|
+
return cli_path
|
|
59
|
+
try:
|
|
60
|
+
if Workspace.discover(Path.cwd()) is not None:
|
|
61
|
+
return Path.cwd()
|
|
62
|
+
except OSError:
|
|
63
|
+
return None
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _workspace_path_from_argv(args: list[str]) -> Path | None:
|
|
68
|
+
for arg in args:
|
|
69
|
+
if not arg or arg.startswith("-"):
|
|
70
|
+
continue
|
|
71
|
+
return Path(arg).expanduser()
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _install_excepthook(window: MainWindow) -> None:
|
|
76
|
+
notifier = _ExceptionNotifier(window)
|
|
77
|
+
notifier.message.connect(window.run_task_tab.on_error_message, Qt.ConnectionType.QueuedConnection)
|
|
78
|
+
window._exception_notifier = notifier # type: ignore[attr-defined]
|
|
79
|
+
|
|
80
|
+
def _forward_exception_to_ui(prefix: str, message: str) -> None:
|
|
81
|
+
try:
|
|
82
|
+
notifier.message.emit(f"{prefix}\n{message}")
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("Failed to forward exception text to Run task tab.")
|
|
85
|
+
|
|
86
|
+
def excepthook(ex_type: type[BaseException], ex_value: BaseException, ex_traceback: TracebackType | None) -> None:
|
|
87
|
+
logger.error("Uncaught exception in GUI thread.", exc_info=(ex_type, ex_value, ex_traceback))
|
|
88
|
+
message = "".join(traceback.format_exception(ex_type, ex_value, ex_traceback))
|
|
89
|
+
_forward_exception_to_ui("Uncaught exception:", message)
|
|
90
|
+
|
|
91
|
+
def thread_excepthook(args: threading.ExceptHookArgs) -> None:
|
|
92
|
+
thread_name = args.thread.name if args.thread is not None else "<unknown>"
|
|
93
|
+
logger.error(
|
|
94
|
+
"Uncaught exception in thread %s.",
|
|
95
|
+
thread_name,
|
|
96
|
+
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
|
|
97
|
+
)
|
|
98
|
+
message = "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback))
|
|
99
|
+
_forward_exception_to_ui(f"Uncaught thread exception ({thread_name}):", message)
|
|
100
|
+
|
|
101
|
+
sys.excepthook = excepthook
|
|
102
|
+
threading.excepthook = thread_excepthook
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Qt signal bridge for pycontrol bus events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QObject, Signal
|
|
6
|
+
|
|
7
|
+
from pycontrol.bus.events import (
|
|
8
|
+
Error,
|
|
9
|
+
EventsBatch,
|
|
10
|
+
SessionEnded,
|
|
11
|
+
SessionStarted,
|
|
12
|
+
TaskStatusUpdated,
|
|
13
|
+
VariableChanged,
|
|
14
|
+
Warning,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QtBusAdapter(QObject):
|
|
19
|
+
"""Subscriber implementation that re-emits typed events as Qt signals.
|
|
20
|
+
|
|
21
|
+
Core calls these methods on the board pump thread. The signal emissions also
|
|
22
|
+
originate from that thread; GUI safety depends on connecting them to
|
|
23
|
+
GUI-thread ``QObject`` receivers so Qt delivers the calls as queued
|
|
24
|
+
cross-thread signals.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
session_started = Signal(object) # SessionStarted
|
|
28
|
+
session_ended = Signal(object) # SessionEnded
|
|
29
|
+
events_batch = Signal(object) # EventsBatch
|
|
30
|
+
variable_changed = Signal(object) # VariableChanged
|
|
31
|
+
task_status_updated = Signal(object) # TaskStatusUpdated
|
|
32
|
+
warning = Signal(object) # Warning
|
|
33
|
+
error = Signal(object) # Error
|
|
34
|
+
|
|
35
|
+
def on_session_start(self, ev: SessionStarted) -> None:
|
|
36
|
+
self.session_started.emit(ev)
|
|
37
|
+
|
|
38
|
+
def on_data(self, batch: EventsBatch) -> None:
|
|
39
|
+
self.events_batch.emit(batch)
|
|
40
|
+
|
|
41
|
+
def on_variable_changed(self, ev: VariableChanged) -> None:
|
|
42
|
+
self.variable_changed.emit(ev)
|
|
43
|
+
|
|
44
|
+
def on_task_status_updated(self, ev: TaskStatusUpdated) -> None:
|
|
45
|
+
self.task_status_updated.emit(ev)
|
|
46
|
+
|
|
47
|
+
def on_warning(self, ev: Warning) -> None:
|
|
48
|
+
self.warning.emit(ev)
|
|
49
|
+
|
|
50
|
+
def on_error(self, ev: Error) -> None:
|
|
51
|
+
self.error.emit(ev)
|
|
52
|
+
|
|
53
|
+
def on_session_end(self, ev: SessionEnded) -> None:
|
|
54
|
+
self.session_ended.emit(ev)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Diagnostic logging bootstrap for the Qt frontend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from logging.handlers import RotatingFileHandler
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pycontrol.workspace import Workspace
|
|
10
|
+
|
|
11
|
+
ERROR_LOG_FILENAME = "ErrorLog.txt"
|
|
12
|
+
_FILE_HANDLER_NAME = "pycontrol_gui.error_file"
|
|
13
|
+
_STREAM_HANDLER_NAME = "pycontrol_gui.stderr"
|
|
14
|
+
_MAX_BYTES = 1_000_000
|
|
15
|
+
_BACKUP_COUNT = 5
|
|
16
|
+
|
|
17
|
+
_ACTIVE_ERROR_LOG_PATH: Path | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _workspace_root(start_path: str | Path | None) -> Path | None:
|
|
21
|
+
try:
|
|
22
|
+
workspace = Workspace.discover(start_path)
|
|
23
|
+
except Exception:
|
|
24
|
+
return None
|
|
25
|
+
if workspace is None:
|
|
26
|
+
return None
|
|
27
|
+
return workspace.root
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _candidate_log_dirs(start_path: str | Path | None) -> tuple[Path, ...]:
|
|
31
|
+
candidates: list[Path] = []
|
|
32
|
+
workspace_root = _workspace_root(start_path)
|
|
33
|
+
if workspace_root is not None:
|
|
34
|
+
candidates.append(workspace_root)
|
|
35
|
+
candidates.append(Path.home() / ".pycontrol-gui" / "logs")
|
|
36
|
+
candidates.append(Path.cwd())
|
|
37
|
+
return tuple(candidates)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _choose_log_path(start_path: str | Path | None) -> Path:
|
|
41
|
+
for directory in _candidate_log_dirs(start_path):
|
|
42
|
+
try:
|
|
43
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
return directory / ERROR_LOG_FILENAME
|
|
45
|
+
except OSError:
|
|
46
|
+
continue
|
|
47
|
+
return Path.cwd() / ERROR_LOG_FILENAME
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _upsert_file_handler(root_logger: logging.Logger, target_path: Path, formatter: logging.Formatter) -> None:
|
|
51
|
+
existing = next((h for h in root_logger.handlers if getattr(h, "name", "") == _FILE_HANDLER_NAME), None)
|
|
52
|
+
if isinstance(existing, RotatingFileHandler):
|
|
53
|
+
if Path(existing.baseFilename) == target_path:
|
|
54
|
+
existing.setLevel(logging.ERROR)
|
|
55
|
+
existing.setFormatter(formatter)
|
|
56
|
+
return
|
|
57
|
+
root_logger.removeHandler(existing)
|
|
58
|
+
existing.close()
|
|
59
|
+
elif existing is not None:
|
|
60
|
+
root_logger.removeHandler(existing)
|
|
61
|
+
existing.close()
|
|
62
|
+
|
|
63
|
+
file_handler = RotatingFileHandler(
|
|
64
|
+
target_path, maxBytes=_MAX_BYTES, backupCount=_BACKUP_COUNT, encoding="utf-8", delay=True
|
|
65
|
+
)
|
|
66
|
+
file_handler.set_name(_FILE_HANDLER_NAME)
|
|
67
|
+
file_handler.setLevel(logging.ERROR)
|
|
68
|
+
file_handler.setFormatter(formatter)
|
|
69
|
+
root_logger.addHandler(file_handler)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _upsert_stream_handler(root_logger: logging.Logger, formatter: logging.Formatter) -> None:
|
|
73
|
+
existing = next((h for h in root_logger.handlers if getattr(h, "name", "") == _STREAM_HANDLER_NAME), None)
|
|
74
|
+
if isinstance(existing, logging.StreamHandler):
|
|
75
|
+
existing.setLevel(logging.WARNING)
|
|
76
|
+
existing.setFormatter(formatter)
|
|
77
|
+
return
|
|
78
|
+
if existing is not None:
|
|
79
|
+
root_logger.removeHandler(existing)
|
|
80
|
+
existing.close()
|
|
81
|
+
|
|
82
|
+
stream_handler = logging.StreamHandler()
|
|
83
|
+
stream_handler.set_name(_STREAM_HANDLER_NAME)
|
|
84
|
+
stream_handler.setLevel(logging.WARNING)
|
|
85
|
+
stream_handler.setFormatter(formatter)
|
|
86
|
+
root_logger.addHandler(stream_handler)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def configure_error_logging(start_path: str | Path | None = None) -> Path:
|
|
90
|
+
"""Configure process-wide GUI diagnostics and return active log path.
|
|
91
|
+
|
|
92
|
+
This mutates the root logger so uncaught errors from worker threads, plugin
|
|
93
|
+
imports, and Qt-facing callbacks land in the same rotating diagnostics file.
|
|
94
|
+
Repeated calls update the handlers installed by pycontrol-gui rather than
|
|
95
|
+
stacking duplicate handlers.
|
|
96
|
+
"""
|
|
97
|
+
global _ACTIVE_ERROR_LOG_PATH
|
|
98
|
+
|
|
99
|
+
target_path = _choose_log_path(start_path)
|
|
100
|
+
formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
101
|
+
root_logger = logging.getLogger()
|
|
102
|
+
# Keep INFO+ records available to stderr while persisting only ERROR+ to file.
|
|
103
|
+
root_logger.setLevel(logging.INFO)
|
|
104
|
+
_upsert_file_handler(root_logger, target_path, formatter)
|
|
105
|
+
_upsert_stream_handler(root_logger, formatter)
|
|
106
|
+
_ACTIVE_ERROR_LOG_PATH = target_path
|
|
107
|
+
return target_path
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_error_log_path() -> Path:
|
|
111
|
+
"""Return the currently configured error-log path."""
|
|
112
|
+
global _ACTIVE_ERROR_LOG_PATH
|
|
113
|
+
if _ACTIVE_ERROR_LOG_PATH is None:
|
|
114
|
+
_ACTIVE_ERROR_LOG_PATH = _choose_log_path(None)
|
|
115
|
+
return _ACTIVE_ERROR_LOG_PATH
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def clear_error_log(path: Path | None = None) -> Path:
|
|
119
|
+
"""Truncate the active diagnostics file and keep handlers alive."""
|
|
120
|
+
target = path if path is not None else get_error_log_path()
|
|
121
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
for handler in logging.getLogger().handlers:
|
|
123
|
+
if not isinstance(handler, logging.FileHandler):
|
|
124
|
+
continue
|
|
125
|
+
base = getattr(handler, "baseFilename", None)
|
|
126
|
+
if base is None:
|
|
127
|
+
continue
|
|
128
|
+
if Path(base) == target:
|
|
129
|
+
handler.flush()
|
|
130
|
+
target.write_text("", encoding="utf-8")
|
|
131
|
+
return target
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = ["ERROR_LOG_FILENAME", "clear_error_log", "configure_error_logging", "get_error_log_path"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Shared helpers for GUI experiment extension loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pycontrol.config import Settings
|
|
8
|
+
from pycontrol.extension import ExperimentExtension
|
|
9
|
+
from pycontrol.extension.loader import instantiate_experiment_extension
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_experiment_extension_path(directory: str | Path | None, stem: str) -> Path | None:
|
|
13
|
+
if directory in (None, ""):
|
|
14
|
+
return None
|
|
15
|
+
candidate = Path(directory) / f"{stem}.py"
|
|
16
|
+
return candidate if candidate.exists() else None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_experiment_extension(
|
|
20
|
+
path: Path, *, settings: Settings | None = None, run_control: object | None = None
|
|
21
|
+
) -> ExperimentExtension:
|
|
22
|
+
extension = instantiate_experiment_extension(path)
|
|
23
|
+
if settings is not None:
|
|
24
|
+
extension_settings = dict(settings.extensions.experiment_extensions.get(path.stem, {}))
|
|
25
|
+
extension.configure_settings(extension_settings, settings)
|
|
26
|
+
if run_control is not None:
|
|
27
|
+
extension.attach_run_control(run_control)
|
|
28
|
+
return extension
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_experiment_extension(path: Path) -> str | None:
|
|
32
|
+
try:
|
|
33
|
+
load_experiment_extension(path)
|
|
34
|
+
except Exception as exc: # noqa: BLE001 - user plugin diagnostics.
|
|
35
|
+
return str(exc)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["load_experiment_extension", "resolve_experiment_extension_path", "validate_experiment_extension"]
|