Skip to content

Commit

Permalink
Merge pull request #1011 from jbellister-slac/disp
Browse files Browse the repository at this point in the history
ENH: Monitor the DISP field of records connected to by PyDMWritableWidget
  • Loading branch information
YektaY authored Jul 19, 2023
2 parents 30ce4be + ef9aba6 commit 29d6b5c
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 25 deletions.
2 changes: 2 additions & 0 deletions pydm/connection_inspector/connection_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def fetch_data(self):
return [connection
for p in plugins.values()
for connection in p.connections.values()
# DISP field is connected to separately for writable channels, including it on this list is redundant
if not connection.address.endswith('.DISP')
]

@Slot()
Expand Down
59 changes: 39 additions & 20 deletions pydm/tests/widgets/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,13 @@ def test_pydmwidget_tooltip(qtbot):
assert tool_tip == str(pydm_label.value)


def test_pydmwritablewidget_channels(qtbot):
@pytest.mark.parametrize('channel_address, monitor_disp', [
('tst://this', True),
('tst://this.VAL', True),
('tst://this.[1:2]', True),
('tst://this', False)
])
def test_pydmwritablewidget_channels(qtbot, channel_address, monitor_disp):
"""
Test the channels population for the widget whose base class PyDMWritableWidget
Expand All @@ -547,7 +553,8 @@ def test_pydmwritablewidget_channels(qtbot):
assert pydm_lineedit._channel is None
assert pydm_lineedit.channels() is None

pydm_lineedit.channel = 'tst://this'
pydm_lineedit.monitorDisp = monitor_disp
pydm_lineedit.channel = channel_address
pydm_channels = pydm_lineedit.channels()[0]

default_pydm_channels = PyDMChannel(address=pydm_lineedit.channel,
Expand All @@ -567,34 +574,41 @@ def test_pydmwritablewidget_channels(qtbot):
write_access_slot=pydm_lineedit.writeAccessChanged,
timestamp_slot=pydm_lineedit.timestamp_changed)
assert pydm_channels == default_pydm_channels

if monitor_disp:
assert pydm_lineedit._disp_channel.address == 'tst://this.DISP'
else:
assert pydm_lineedit._disp_channel is None

@pytest.mark.parametrize(
"channel_address, connected, write_access, is_app_read_only", [
("CA://MA_TEST", True, True, True),
("CA://MA_TEST", True, False, True),
("CA://MA_TEST", True, True, False),
("CA://MA_TEST", True, False, False),
("CA://MA_TEST", False, True, True),
("CA://MA_TEST", False, False, True),
("CA://MA_TEST", False, True, False),
("CA://MA_TEST", False, False, False),
("", False, False, False),
(None, False, False, False),
"channel_address, connected, write_access, is_app_read_only, disable_put", [
("CA://MA_TEST", True, True, True, 0),
("CA://MA_TEST", True, False, True, 0),
("CA://MA_TEST", True, True, False, 0),
("CA://MA_TEST", True, False, False, 0),
("CA://MA_TEST", False, True, True, 0),
("CA://MA_TEST", False, False, True, 0),
("CA://MA_TEST", False, True, False, 0),
("CA://MA_TEST", False, False, False, 0),
("CA://MA_TEST", False, False, False, 1),
("CA://MA_TEST", True, False, False, 1),
("CA://MA_TEST", True, True, False, 1),
("", False, False, False, 0),
(None, False, False, False, 0),
])
def test_pydmwritable_check_enable_state(qtbot, channel_address,
connected, write_access,
is_app_read_only):
is_app_read_only, disable_put):
"""
Test the tooltip generated depending on the channel address validation, connection, write access, and whether the
app is read-only. This test is for a widget whose base class is PyDMWritableWidget.
Test the tooltip generated depending on the channel address validation, connection, write access,
DISP field, and whether the app is read-only. This test is for a widget whose base class is PyDMWritableWidget.
Expectations:
1. The widget's tooltip will update only if the channel address is valid.
2. If the data channel is disconnected, the widget's tooltip will "PV is disconnected"
3. If the data channel is connected, but it has no write access:
a. If the app is read-only, the tooltip will read "Running PyDM on Read-Only mode."
b. If the app is not read-only, the tooltip will read "Access denied by Channel Access Security."
b. If the app is not read-only, the tooltip will read "Access denied by Channel Access Security." or
"Access denied by DISP field" depending on which is preventing it. Access Security takes precedence.
Parameters
----------
Expand All @@ -608,13 +622,16 @@ def test_pydmwritable_check_enable_state(qtbot, channel_address,
True if the widget has write access to the channel; False otherwise
is_app_read_only : bool
True if the PyDM app is read-only; False otherwise
disable_put : int
1 if puts should be disabled based on the DISP field, 0 otherwise
"""
pydm_lineedit = PyDMLineEdit()
qtbot.addWidget(pydm_lineedit)

pydm_lineedit.channel = channel_address
pydm_lineedit._connected = connected
pydm_lineedit._write_access = write_access
pydm_lineedit._disable_put = disable_put

data_plugins.set_read_only(is_app_read_only)

Expand All @@ -626,11 +643,13 @@ def test_pydmwritable_check_enable_state(qtbot, channel_address,
if is_channel_valid(channel_address):
if not pydm_lineedit._connected:
assert "PV is disconnected." in actual_tooltip
elif not write_access:
elif not write_access or disable_put:
if data_plugins.is_read_only():
assert "Running PyDM on Read-Only mode." in actual_tooltip
else:
elif not pydm_lineedit._write_access:
assert "Access denied by Channel Access Security." in actual_tooltip
else:
assert "Access denied by DISP field" in actual_tooltip
else:
assert actual_tooltip == original_tooltip

Expand Down
78 changes: 73 additions & 5 deletions pydm/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ..display import Display
from .rules import RulesDispatcher
from datetime import datetime
from typing import Optional

try:
from json.decoder import JSONDecodeError
Expand Down Expand Up @@ -1206,6 +1207,10 @@ def channel(self, value):
value : str
Channel address
"""
self.set_channel(value)

def set_channel(self, value):
""" A setter method without a pyqt decorator so subclasses can use this functionality """
if self._channel != value:
# Remove old connections
for channel in [c for c in self._channels if
Expand Down Expand Up @@ -1363,6 +1368,9 @@ class PyDMWritableWidget(PyDMWidget):

def __init__(self, init_channel=None):
self._write_access = False
self._disp_channel = None
self._disable_put = False
self._monitor_disp = False
super(PyDMWritableWidget, self).__init__(init_channel=init_channel)

def init_for_designer(self):
Expand Down Expand Up @@ -1403,6 +1411,62 @@ def eventFilter(self, obj, event):

return PyDMWidget.eventFilter(self, obj, event)

@Property(bool)
def monitorDisp(self) -> bool:
"""
Whether to monitor the DISP field for this widget's channel
"""
return self._monitor_disp

@monitorDisp.setter
def monitorDisp(self, monitor_disp: bool) -> None:
"""
Whether to monitor the DISP field for this widget's channel
"""
if self._monitor_disp != monitor_disp:
self._monitor_disp = monitor_disp
if self._disp_channel is not None:
if monitor_disp:
self._disp_channel.connect()
else:
self._disp_channel.disconnect()

@Property(str)
def channel(self) -> Optional[str]:
"""
The channel address in use for this widget.
Returns
-------
channel : str
Channel address
"""
if self._channel:
return str(self._channel)
return None

@channel.setter
def channel(self, value: str) -> None:
"""
The channel address in use for this widget. Also sets up a monitor on the DISP field.
"""
if self._channel != value:
self.set_channel(value)
if not self._monitor_disp or self._channel is None:
return

base_channel = self._channel.split(".", 1)[0] if "." in self._channel else self._channel
if self._disp_channel is None or self._disp_channel.address != f'{base_channel}.DISP':
if self._disp_channel is not None:
self._disp_channel.disconnect()
self._disp_channel = PyDMChannel(address=f'{base_channel}.DISP', value_slot=self.disp_value_changed)
self._disp_channel.connect()

def disp_value_changed(self, new_disp_value: int) -> None:
""" Callback function to receive changes to the DISP field of the monitored channel """
self._disable_put = new_disp_value
self.check_enable_state()

def write_access_changed(self, new_write_access):
"""
Callback invoked when the Channel has new write access value.
Expand Down Expand Up @@ -1430,13 +1494,12 @@ def writeAccessChanged(self, write_access):
self.write_access_changed(write_access)

@only_if_channel_set
def check_enable_state(self):
def check_enable_state(self) -> None:
"""
Checks whether or not the widget should be disable.
This method also disables the widget and add a Tool Tip
with the reason why it is disabled.
Checks whether or not the widget should be disabled.
This method also disables the widget and adds a tool tip with the reason why it is disabled.
"""
status = self._write_access and self._connected
status = self._write_access and self._connected and not self._disable_put
tooltip = self.restore_original_tooltip()
if not self._connected:
if tooltip != '':
Expand All @@ -1451,5 +1514,10 @@ def check_enable_state(self):
tooltip += "Running PyDM on Read-Only mode."
else:
tooltip += "Access denied by Channel Access Security."
elif self._disable_put:
if tooltip != '':
tooltip += '\n'
tooltip += 'Access denied by DISP field'

self.setToolTip(tooltip)
self.setEnabled(status)

0 comments on commit 29d6b5c

Please sign in to comment.