Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TST: Attempted test suite fixups #214

Merged
merged 16 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions atef/procedure.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,15 @@ class Target:
def to_signal(
self,
signal_cache: Optional[_SignalCache] = None
) -> Optional[ophyd.EpicsSignal]:
) -> Optional[ophyd.Signal]:
"""
Return the signal described by this Target. First attempts to use the
device + attr information to look up the signal in happi, falling back
to the raw PV.

Returns
-------
ophyd.EpicsSignal
ophyd.Signal
the signal described by this Target
"""
try:
Expand Down
4 changes: 2 additions & 2 deletions atef/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,13 @@ def test_config_window_basic(qtbot: QtBot):
qtbot.addWidget(window)


# @pytest.mark.parametrize('config', [0, 1, 2], indirect=True)
# @pytest.mark.skip()
def test_config_window_save_load(qtbot: QtBot, tmp_path: pathlib.Path,
all_config_path: os.PathLike):
"""
Pass if the config gui can open a file and save the same file back
"""
window = Window(show_welcome=False)
qtbot.addWidget(window)
config = pathlib.Path(all_config_path)
filename = config.name
source = str(config)
Expand All @@ -219,6 +218,7 @@ def test_config_window_save_load(qtbot: QtBot, tmp_path: pathlib.Path,
with open(dest, 'r') as fd:
dest_lines = fd.readlines()
assert source_lines == dest_lines
qtbot.addWidget(window)


# # Test encountered frequent failures due to C++ objects being deleted before
Expand Down
62 changes: 34 additions & 28 deletions atef/widgets/config/data_active.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import typhos
import typhos.cli
import typhos.display
from ophyd.signal import EpicsSignalBase
from ophyd.signal import Signal
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QDialogButtonBox
Expand Down Expand Up @@ -901,6 +901,10 @@ def __init__(self, data: Optional[ValueToTarget] = None, **kwargs):
self.edit_widget = None
if data is None:
data = ValueToTarget()
self._sig: Optional[Signal] = None
self._curr_value = None
self._dtype = None
self._enum_strs = None
super().__init__(data=data, **kwargs)

def setup_ui(self) -> None:
Expand All @@ -927,13 +931,37 @@ def update_target(self) -> None:

self.update_input_placeholder()

def get_curr_value(self):
if self._sig is None:
return
self._sig.wait_for_connection()
self._curr_value = self.bridge.value.get() or self._sig.get()
self._dtype = type(self._curr_value)
self._enum_strs = getattr(self._sig, 'enum_strs', None)

def fail_get_value(self, ex: Exception):
logger.debug(f'failed to get signal data for input widget: {ex}')
self._curr_value = 'no data'
# fall back to type in dataclass if available
stored_value = self.bridge.value.get()
if stored_value is not None:
self._dtype = type(stored_value)
else:
self._dtype = float

self.run_setup_input_widget()

def run_setup_input_widget(self):
self.setup_input_widget(self._curr_value, self._dtype,
enum_strs=self._enum_strs)

def update_input_placeholder(self) -> None:
"""
Updates value input widget with a QLineEdit with the approriate validator
given the target's datatype
"""
sig: EpicsSignalBase = self.data.to_signal()
if sig is None:
self._sig = self.data.to_signal()
if self._sig is None:
self.edit_widget = QtWidgets.QLabel('(no target set)')
insert_widget(self.edit_widget, self.value_input_placeholder)
self.value_button_box.hide()
Expand All @@ -943,35 +971,13 @@ def update_input_placeholder(self) -> None:
self._dtype = None
self._enum_strs = None

def get_curr_value():
sig.wait_for_connection()
self._curr_value = self.bridge.value.get() or sig.get()
self._dtype = type(self._curr_value)
self._enum_strs = getattr(sig, 'enum_strs', None)

def fail_get_value(ex: Exception):
logger.debug(f'failed to get signal data for input widget: {ex}')
self._curr_value = 'no data'
# fall back to type in dataclass if available
stored_value = self.bridge.value.get()
if stored_value is not None:
self._dtype = type(stored_value)
else:
self._dtype = float

run_setup_input_widget()

def run_setup_input_widget():
self.setup_input_widget(self._curr_value, self._dtype,
enum_strs=self._enum_strs)

if self.curr_val_thread and self.curr_val_thread.isRunning():
logger.debug('thread is still running. Ignore..')
return

self.curr_val_thread = BusyCursorThread(func=get_curr_value)
self.curr_val_thread.raised_exception.connect(fail_get_value)
self.curr_val_thread.task_finished.connect(run_setup_input_widget)
self.curr_val_thread = BusyCursorThread(parent=self, func=self.get_curr_value)
self.curr_val_thread.raised_exception.connect(self.fail_get_value)
self.curr_val_thread.task_finished.connect(self.run_setup_input_widget)
self.curr_val_thread.start()

def setup_input_widget(
Expand Down
43 changes: 31 additions & 12 deletions atef/widgets/config/find_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import happi
import qtawesome as qta
from apischema import ValidationError, serialize
from pcdsutils.qt.callbacks import WeakPartialMethodSlot
from qtpy import QtCore, QtWidgets

from atef.cache import DataCache, get_signal_cache
Expand Down Expand Up @@ -434,6 +435,7 @@ def __init__(
self._window = window
self.match_paths: Iterable[List[Any]] = []
self.orig_file = None
self._partial_slots: list[WeakPartialMethodSlot] = []

if not filepath:
self.open_converted_button.hide()
Expand Down Expand Up @@ -558,6 +560,13 @@ def update_match_fn(self, *args, **kwargs) -> None:
match_fn = get_default_match_fn(self._search_regex)
self._match_fn = match_fn

def _remove_item_from_change_list(self, list_item, *args, **kwargs):
self.change_list.takeItem(self.change_list.row(list_item))

def accept_change(self, list_item, *args, **kwargs):
# make sure this only runs if action was successful
self._remove_item_from_change_list(list_item)

def preview_changes(self, *args, **kwargs) -> None:
"""
Update the change list according to the provided find and replace settings
Expand All @@ -570,13 +579,6 @@ def preview_changes(self, *args, **kwargs) -> None:
self.change_list.clear()
self.match_paths = list(walk_find_match(self.orig_file, self._match_fn))

def remove_item(list_item):
self.change_list.takeItem(self.change_list.row(list_item))

def accept_change(list_item):
# make sure this only runs if action was successful
remove_item(list_item)

# generator can be unstable if dataclass changes during walk
# this is only ok because we consume generator entirely
for path in self.match_paths:
Expand All @@ -590,8 +592,16 @@ def accept_change(list_item):
self.change_list.addItem(l_item)
self.change_list.setItemWidget(l_item, row_widget)

row_widget.button_box.accepted.connect(partial(accept_change, l_item))
row_widget.button_box.rejected.connect(partial(remove_item, l_item))
accept_slot = WeakPartialMethodSlot(
row_widget.button_box, row_widget.button_box.accepted,
self.accept_change, l_item
)
self._partial_slots.append(accept_slot)
reject_slot = WeakPartialMethodSlot(
row_widget.button_box, row_widget.button_box.rejected,
self._remove_item_from_change_list, l_item
)
self._partial_slots.append(reject_slot)

def open_converted(self, *args, **kwargs) -> None:
""" open new file in new tab """
Expand Down Expand Up @@ -724,7 +734,7 @@ def __init__(
self._signals: List[str] = []
self._devices: List[str] = []
self.busy_thread = None

self._partial_slots: list[WeakPartialMethodSlot] = []
if filepath:
self.open_file(filename=filepath)
self.setup_ui()
Expand Down Expand Up @@ -978,7 +988,11 @@ def show_changes_from_edit(self, *args, **kwargs) -> None:
self.details_list.addItem(l_item)
self.details_list.setItemWidget(l_item, row_widget)

row_widget.remove_item.connect(partial(self.remove_item_from_details, l_item))
remove_slot = WeakPartialMethodSlot(
row_widget, row_widget.remove_item,
self.remove_item_from_details, l_item
)
self._partial_slots.append(remove_slot)

def remove_item_from_details(self, item: QtWidgets.QListWidgetItem) -> None:
""" remove an item from the details list """
Expand Down Expand Up @@ -1018,6 +1032,7 @@ def __init__(
self.actions: List[FindReplaceAction] = []
self._match_fn: MatchFunction = lambda x: False
self._replace_fn: ReplaceFunction = lambda x: x
self._partial_slots: list[WeakPartialMethodSlot] = []
self.setup_ui()

def setup_ui(self):
Expand Down Expand Up @@ -1139,7 +1154,11 @@ def get_details_rows(self) -> List[FindReplaceRow]:
for action in self.actions:
row_widget = FindReplaceRow(data=action)

row_widget.remove_item.connect(partial(self.remove_from_action_list, action=action))
remove_slot = WeakPartialMethodSlot(
row_widget, row_widget.remove_item,
self.remove_from_action_list, action=action
)
self._partial_slots.append(remove_slot)
details_row_widgets.append(row_widget)

return details_row_widgets
Expand Down
33 changes: 24 additions & 9 deletions atef/widgets/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import time
from dataclasses import fields
from enum import IntEnum
from functools import partial
from itertools import zip_longest
from typing import (Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type,
Union)

import numpy as np
import qtawesome as qta
from ophyd import EpicsSignal, EpicsSignalRO
from pcdsutils.qt.callbacks import WeakPartialMethodSlot
from qtpy import QtCore, QtWidgets
from qtpy.QtCore import (QPoint, QPointF, QRect, QRectF, QRegularExpression,
QSize, Qt, QTimer)
Expand Down Expand Up @@ -1560,6 +1560,7 @@ def __init__(
self._is_number = False
self._show_tol = False
self._prep_dynamic_thread = None
self._partial_slots: list[WeakPartialMethodSlot] = []
self.setup_widgets()
self.set_mode_from_data()
self.setSizePolicy(
Expand Down Expand Up @@ -1609,20 +1610,34 @@ def setup_widgets(self):
menu = QMenu()
if not self._is_number:
use_bool = menu.addAction("&Bool")
use_bool.triggered.connect(partial(self.set_mode, EditMode.BOOL)),
bool_slot = WeakPartialMethodSlot(use_bool, use_bool.triggered,
self.set_mode, EditMode.BOOL)
self._partial_slots.append(bool_slot)
use_enum = menu.addAction("&Enum")
use_enum.triggered.connect(partial(self.set_mode, EditMode.ENUM))
enum_slot = WeakPartialMethodSlot(use_enum, use_enum.triggered,
self.set_mode, EditMode.ENUM)
self._partial_slots.append(enum_slot)
use_float = menu.addAction("&Float")
use_float.triggered.connect(partial(self.set_mode, EditMode.FLOAT))
float_slot = WeakPartialMethodSlot(use_float, use_float.triggered,
self.set_mode, EditMode.FLOAT)
self._partial_slots.append(float_slot)
use_int = menu.addAction("&Int")
use_int.triggered.connect(partial(self.set_mode, EditMode.INT))
int_slot = WeakPartialMethodSlot(use_int, use_int.triggered,
self.set_mode, EditMode.INT)
self._partial_slots.append(int_slot)
if not self._is_number:
use_str = menu.addAction("&String")
use_str.triggered.connect(partial(self.set_mode, EditMode.STR))
str_slot = WeakPartialMethodSlot(use_str, use_str.triggered,
self.set_mode, EditMode.STR)
self._partial_slots.append(str_slot)
use_epics = menu.addAction("EPI&CS")
use_epics.triggered.connect(partial(self.set_mode, EditMode.EPICS))
epics_slot = WeakPartialMethodSlot(use_epics, use_epics.triggered,
self.set_mode, EditMode.EPICS)
self._partial_slots.append(epics_slot)
use_happi = menu.addAction("&Happi")
use_happi.triggered.connect(partial(self.set_mode, EditMode.HAPPI))
happi_slot = WeakPartialMethodSlot(use_happi, use_happi.triggered,
self.set_mode, EditMode.HAPPI)
self._partial_slots.append(happi_slot)
self.select_mode_button.setMenu(menu)
self.select_mode_button.setPopupMode(
self.select_mode_button.InstantPopup
Expand Down Expand Up @@ -1939,7 +1954,7 @@ def fill_enums():
self.thread_worker.task_finished.connect(fill_enums)
self.thread_worker.start()

def set_mode(self, mode: EditMode) -> None:
def set_mode(self, mode: EditMode, *args, **kwargs) -> None:
"""
Change the mode of the edit widget.
This adjusts the dynamic data classes as needed and
Expand Down
29 changes: 20 additions & 9 deletions atef/widgets/config/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
import traceback
import webbrowser
from copy import deepcopy
from functools import partial
from pathlib import Path
from pprint import pprint
from typing import ClassVar, Dict, Optional, Union

import qtawesome
from apischema import ValidationError, deserialize, serialize
from pcdsutils.qt.callbacks import WeakPartialMethodSlot
from qtpy import QtWidgets
from qtpy.QtCore import Qt, QTimer
from qtpy.QtCore import Signal as QSignal
Expand Down Expand Up @@ -72,6 +72,7 @@ class Window(DesignerDisplay, QMainWindow):

def __init__(self, *args, show_welcome: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self._partial_slots: list[WeakPartialMethodSlot] = []
self.setWindowTitle('atef config')
self.action_welcome_tab.triggered.connect(self.welcome_user)
self.action_new_file.triggered.connect(self.new_file)
Expand Down Expand Up @@ -114,22 +115,32 @@ def welcome_user(self):
curr_idx = self.tab_widget.count() - 1
self.tab_widget.setCurrentIndex(curr_idx)

widget.new_passive_button.clicked.connect(
partial(self.new_file, checkout_type='passive')
new_passive_slot = WeakPartialMethodSlot(
widget.new_passive_button, widget.new_passive_button.clicked,
self.new_file, checkout_type="passive"
)
widget.new_active_button.clicked.connect(
partial(self.new_file, checkout_type='active')
self._partial_slots.append(new_passive_slot)
new_active_slot = WeakPartialMethodSlot(
widget.new_active_button, widget.new_active_button.clicked,
self.new_file, checkout_type="active"
)
self._partial_slots.append(new_active_slot)

widget.fill_template_button.clicked.connect(
self.open_fill_template
)
widget.sample_active_button.clicked.connect(partial(

sample_active_slot = WeakPartialMethodSlot(
widget.sample_active_button, widget.sample_active_button.clicked,
self.open_file, filename=str(TEST_CONFIG_PATH / 'active_test.json')
))
)
self._partial_slots.append(sample_active_slot)

widget.sample_passive_button.clicked.connect(partial(
sample_passive_slot = WeakPartialMethodSlot(
widget.sample_passive_button, widget.sample_passive_button.clicked,
self.open_file, filename=str(TEST_CONFIG_PATH / 'all_fields.json')
))
)
self._partial_slots.append(sample_passive_slot)

widget.open_button.clicked.connect(self.open_file)
widget.exit_button.clicked.connect(self.close_all)
Expand Down
1 change: 1 addition & 0 deletions conda-recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ requirements:
- ipython
- numpy
- ophyd
- pcdsutils >=0.14.0
- pydm
- pyyaml
- qtpy
Expand Down
Loading