From 1e2cf008902f7ea33ddf16028503ce88c4b62367 Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 9 Mar 2023 13:25:07 -0800 Subject: [PATCH 01/37] added NTTable widget from Hugo's pydm_pva_widgets repositorty --- pydm/widgets/__init__.py | 1 + pydm/widgets/nt_table.py | 201 ++++++++++++++++++++++++++++++++++++++ pydm/widgets/qtplugins.py | 6 ++ 3 files changed, 208 insertions(+) create mode 100644 pydm/widgets/nt_table.py diff --git a/pydm/widgets/__init__.py b/pydm/widgets/__init__.py index a56a90454..7b537e231 100644 --- a/pydm/widgets/__init__.py +++ b/pydm/widgets/__init__.py @@ -27,3 +27,4 @@ from .eventplot import PyDMEventPlot from .tab_bar import PyDMTabWidget from .template_repeater import PyDMTemplateRepeater +from .nt_table import PyDMNTTable \ No newline at end of file diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py new file mode 100644 index 000000000..7c3735f3e --- /dev/null +++ b/pydm/widgets/nt_table.py @@ -0,0 +1,201 @@ +import logging + +from operator import itemgetter + +from pydm.widgets.base import PyDMWidget +from qtpy import QtCore, QtWidgets + +logger = logging.getLogger(__name__) + + +class PythonTableModel(QtCore.QAbstractTableModel): + def __init__(self, column_names, initial_list=[], parent=None, + edit_method=None, can_edit_method=None): + super(PythonTableModel, self).__init__(parent=parent) + self._list = [] + self._column_names = column_names + self.edit_method = edit_method + self.can_edit_method = can_edit_method + self.list = initial_list + + @property + def list(self): + return self._list + + @list.setter + def list(self, new_list): + self.beginResetModel() + self._list = new_list + self.endResetModel() + + # QAbstractItemModel Implementation + def clear(self): + self.list = [] + + def flags(self, index): + f = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + if self.edit_method is not None: + editable = True + if self.can_edit_method is not None: + editable = self.can_edit_method( + self._list[index.row()][index.column()]) + if editable: + f = f | QtCore.Qt.ItemIsEditable + return f + + def rowCount(self, parent=None): + if parent is not None and parent.isValid(): + return 0 + return len(self._list) + + def columnCount(self, parent=None): + return len(self._column_names) + + def data(self, index, role=QtCore.Qt.DisplayRole): + if not index.isValid(): + return QtCore.QVariant() + if index.row() >= self.rowCount(): + return QtCore.QVariant() + if index.column() >= self.columnCount(): + return QtCore.QVariant() + if role == QtCore.Qt.DisplayRole: + try: + item = str(self._list[index.row()][index.column()]) + except IndexError: + item = "" + return item + else: + return QtCore.QVariant() + + def setData(self, index, value, role=QtCore.Qt.EditRole): + if self.edit_method is None: + return False + if role != QtCore.Qt.EditRole: + return False + if not index.isValid(): + return False + if index.row() >= self.rowCount(): + return False + if index.column() >= self.columnCount(): + return False + success = self.edit_method(self._list[index.row()][index.column()], + value.toPyObject()) + if success: + self.dataChanged.emit(index, index) + return success + + def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): + if role != QtCore.Qt.DisplayRole: + return super(PythonTableModel, self).headerData(section, + orientation, role) + if orientation == QtCore.Qt.Horizontal \ + and section < self.columnCount(): + return str(self._column_names[section]) + elif orientation == QtCore.Qt.Vertical and section < self.rowCount(): + return section + + def sort(self, col, order=QtCore.Qt.AscendingOrder): + self.layoutAboutToBeChanged.emit() + sort_reversed = (order == QtCore.Qt.AscendingOrder) + self._list.sort(key=itemgetter(col), reverse=sort_reversed) + self.layoutChanged.emit() + + # End QAbstractItemModel implementation. + + # Python collection implementation. + def __len__(self): + return len(self._list) + + def __iter__(self): + return iter(self._list) + + def __contains__(self, value): + return value in self._list + + def __getitem__(self, index): + return self._list[index] + + def __setitem__(self, index, value): + if len(value) != self.columnCount(): + msg = "Items must have the same length as the column count ({})" + raise ValueError(msg.format(self.columnCount())) + self._list[index] = value + self.dataChanged.emit(index, index) + + def __delitem__(self, index): + if (index + 1) > len(self): + raise IndexError("list assignment index out of range") + self.beginRemoveRows(QtCore.QModelIndex(), index, index) + del self._list[index] + self.endRemoveRows() + + def append(self, value): + self.beginInsertRows(QtCore.QModelIndex(), len(self._list), + len(self._list)) + self._list.append(value) + self.endInsertRows() + + def extend(self, values): + self.beginInsertRows(QtCore.QModelIndex(), len(self._list), + len(self._list) + len(values) - 1) + self._list.extend(values) + self.endInsertRows() + + def remove(self, item): + index = None + try: + index = self._list.index(item) + except ValueError: + raise ValueError("list.remove(x): x not in list") + del self[index] + + def pop(self, index=None): + if len(self._list) < 1: + raise IndexError("pop from empty list") + if index is None: + index = len(self._list) - 1 + del self[index] + + def count(self, item): + return self._list.count(item) + + def reverse(self): + self.layoutAboutToBeChanged.emit() + self._list.reverse() + self.layoutChanged.emit() + + +class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): + def __init__(self, parent=None, init_channel=None): + super(PyDMNTTable, self).__init__(parent=parent, init_channel=init_channel) + self.setLayout(QtWidgets.QVBoxLayout()) + self._table = QtWidgets.QTableView(self) + self.layout().addWidget(self._table) + self._model = None + self._table_labels = None + self._table_values = [] + + def _receive_data(self, data=None, introspection=None, *args, **kwargs): + super(PyDMNTTable, self)._receive_data(data, introspection, *args, + **kwargs) + if data is None: + return + labels = data.get('labels', None) + values = data.get('value', {}) + + if labels is None or len(labels) == 0: + labels = values.keys() + + try: + values = list(zip(*[v for k, v in values.items()])) + except TypeError: + logger.exception("NTTable value items must be iterables.") + + self._table_values = values + + if labels != self._table_labels: + self._table_labels = labels + self._model = PythonTableModel(labels, initial_list=values) + self._table.setModel(self._model) + else: + self._model.list = values \ No newline at end of file diff --git a/pydm/widgets/qtplugins.py b/pydm/widgets/qtplugins.py index 6cc2fd6d3..8be76bdbb 100644 --- a/pydm/widgets/qtplugins.py +++ b/pydm/widgets/qtplugins.py @@ -50,6 +50,7 @@ from .timeplot import PyDMTimePlot from .waveformplot import PyDMWaveformPlot from .waveformtable import PyDMWaveformTable +from .nt_table import PyDMNTTable logger = logging.getLogger(__name__) @@ -272,6 +273,11 @@ group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("table")) +# NTTable plugin +PyDMNTTable = qtplugin_factory(PyDMNTTable, + group=WidgetCategory.INPUT, + extensions=BASE_EXTENSIONS, + icon=ifont.icon("table")) # Tab Widget plugin PyDMTabWidgetPlugin = TabWidgetPlugin(extensions=BASE_EXTENSIONS) From e1e992a059a53b1c7664f36230675747cf8e62c5 Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 10 Mar 2023 10:15:06 -0800 Subject: [PATCH 02/37] working on implementing nttable --- .../epics_plugins/p4p_plugin_component.py | 25 ++++++++++++++++--- pydm/data_plugins/plugin.py | 16 +++++++++--- pydm/widgets/base.py | 8 +++--- pydm/widgets/nt_table.py | 22 +++++++++------- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index 492915ca3..23e083f04 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -1,3 +1,4 @@ + import logging import numpy as np @@ -19,7 +20,6 @@ def __init__(self, channel: PyDMChannel, address: str, protocol: Optional[str] = None, parent: Optional[QObject] = None): """ Manages the connection to a channel using the P4P library. A given channel can have multiple listeners. - Parameters ---------- channel : PyDMChannel @@ -79,9 +79,20 @@ def send_new_value(self, value: Value) -> None: self.write_access_signal.emit(True) self._value = value + has_value_changed_yet = False for changed_value in value.changedSet(): - if changed_value == 'value': - new_value = value.value + if changed_value == 'value' or changed_value.split('.')[0] == 'value': + # NTTable has a changedSet item for each column that has changed + # Since we want to send an update on any table change, let's track + # if the value item has been updated yet + if has_value_changed_yet: + continue + else: + has_value_changed_yet = True + if 'NTTable' in value.getID(): + new_value = value.value.todict() + else: + new_value = value.value if new_value is not None: if isinstance(new_value, np.ndarray): if 'NTNDArray' in value.getID(): @@ -95,6 +106,9 @@ def send_new_value(self, value: Value) -> None: self.new_value_signal[int].emit(new_value) elif isinstance(new_value, str): self.new_value_signal[str].emit(new_value) + elif isinstance(new_value, dict): + # for some reason, pyqt struggles to emit on a dict type signal, and wants this to be a list + self.new_value_signal[dict].emit(np.array(new_value)) else: raise ValueError(f'No matching signal for value: {new_value} with type: {type(new_value)}') # Sometimes unchanged control variables appear to be returned with value changes, so checking against @@ -149,7 +163,6 @@ def put_value(self, value): def add_listener(self, channel: PyDMChannel): """ Adds a listener to this connection, connecting the appropriate signals/slots to the input PyDMChannel. - Parameters ---------- channel : PyDMChannel @@ -183,6 +196,10 @@ def add_listener(self, channel: PyDMChannel): channel.value_signal[np.ndarray].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass + try: + channel.value_signal[dict].connect(self.put_value, Qt.QueuedConnection) + except KeyError: + pass def close(self): """ Closes out this connection. """ diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 2c3798fde..6b354b895 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -9,10 +9,10 @@ from qtpy.QtCore import Signal, QObject, Qt from qtpy.QtWidgets import QApplication from .. import config - +import re class PyDMConnection(QObject): - new_value_signal = Signal([float], [int], [str], [ndarray], [bool]) + new_value_signal = Signal([float], [int], [str], [object], [bool]) connection_state_signal = Signal(bool) new_severity_signal = Signal(int) write_access_signal = Signal(bool) @@ -62,7 +62,11 @@ def add_listener(self, channel): self.new_value_signal[bool].connect(channel.value_slot, Qt.QueuedConnection) except TypeError: pass - + try: + self.new_value_signal[dict].connect(channel.value_slot, Qt.QueuedConnection) + except TypeError: + pass + if channel.severity_slot is not None: self.new_severity_signal.connect(channel.severity_slot, Qt.QueuedConnection) @@ -141,7 +145,11 @@ def remove_listener(self, channel, destroying: Optional[bool] = False) -> None: self.new_value_signal[bool].disconnect(channel.value_slot) except TypeError: pass - + try: + self.new_value_signal[dict].disconnect(channel.value_slot) + except TypeError: + pass + if self._should_disconnect(channel.severity_slot, destroying): try: self.new_severity_signal.disconnect(channel.severity_slot) diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index 26de51404..68348ef93 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -873,7 +873,8 @@ def connectionStateChanged(self, connected): @Slot(float) @Slot(str) @Slot(bool) - @Slot(np.ndarray) + #@Slot(np.ndarray) + @Slot(object) def channelValueChanged(self, new_val): """ PyQT Slot for changes on the Value of the Channel @@ -1348,10 +1349,11 @@ class PyDMWritableWidget(PyDMWidget): Emitted when the user changes the value """ - __Signals__ = ("send_value_signal([int], [float], [str], [bool], [np.ndarray])") + __Signals__ = ("send_value_signal([int], [float], [str], [bool], [object])") # Emitted when the user changes the value. - send_value_signal = Signal([int], [float], [str], [bool], [np.ndarray]) + + send_value_signal = Signal([int], [float], [str], [bool], [object]) def __init__(self, init_channel=None): self._write_access = False diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 7c3735f3e..02a2de57c 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -168,6 +168,7 @@ def reverse(self): class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): def __init__(self, parent=None, init_channel=None): super(PyDMNTTable, self).__init__(parent=parent, init_channel=init_channel) + PyDMWidget.__init__(self, init_channel=init_channel) self.setLayout(QtWidgets.QVBoxLayout()) self._table = QtWidgets.QTableView(self) self.layout().addWidget(self._table) @@ -175,27 +176,30 @@ def __init__(self, parent=None, init_channel=None): self._table_labels = None self._table_values = [] - def _receive_data(self, data=None, introspection=None, *args, **kwargs): - super(PyDMNTTable, self)._receive_data(data, introspection, *args, - **kwargs) + def value_changed(self, data=None): if data is None: return - labels = data.get('labels', None) - values = data.get('value', {}) - + print(data, type(data), "test") + super(PyDMNTTable, self).value_changed(data) + + labels = data.dtype.names + values = data.tolist() + + print(values, type(values), "test") + if labels is None or len(labels) == 0: labels = values.keys() - + try: values = list(zip(*[v for k, v in values.items()])) except TypeError: logger.exception("NTTable value items must be iterables.") self._table_values = values - + if labels != self._table_labels: self._table_labels = labels self._model = PythonTableModel(labels, initial_list=values) self._table.setModel(self._model) else: - self._model.list = values \ No newline at end of file + self._model.list = values From e06d030bb54beef325a1e797b37581f3a2c84ada Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 16 Mar 2023 11:49:31 -0700 Subject: [PATCH 03/37] Adds a working version of PyDMNTTable --- .../epics_plugins/p4p_plugin_component.py | 5 ++- pydm/data_plugins/plugin.py | 14 ++------- pydm/widgets/base.py | 1 + pydm/widgets/nt_table.py | 31 +++++++++++++------ 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index 23e083f04..d5123bc1b 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -107,8 +107,7 @@ def send_new_value(self, value: Value) -> None: elif isinstance(new_value, str): self.new_value_signal[str].emit(new_value) elif isinstance(new_value, dict): - # for some reason, pyqt struggles to emit on a dict type signal, and wants this to be a list - self.new_value_signal[dict].emit(np.array(new_value)) + self.new_value_signal[dict].emit(new_value) else: raise ValueError(f'No matching signal for value: {new_value} with type: {type(new_value)}') # Sometimes unchanged control variables appear to be returned with value changes, so checking against @@ -193,7 +192,7 @@ def add_listener(self, channel: PyDMChannel): except KeyError: pass try: - channel.value_signal[np.ndarray].connect(self.put_value, Qt.QueuedConnection) + channel.value_signal[object].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass try: diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 6b354b895..193195a2e 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -12,7 +12,7 @@ import re class PyDMConnection(QObject): - new_value_signal = Signal([float], [int], [str], [object], [bool]) + new_value_signal = Signal([float], [int], [str], [bool], [object]) connection_state_signal = Signal(bool) new_severity_signal = Signal(int) write_access_signal = Signal(bool) @@ -54,16 +54,12 @@ def add_listener(self, channel): self.new_value_signal[str].connect(channel.value_slot, Qt.QueuedConnection) except TypeError: pass - try: - self.new_value_signal[ndarray].connect(channel.value_slot, Qt.QueuedConnection) - except TypeError: - pass try: self.new_value_signal[bool].connect(channel.value_slot, Qt.QueuedConnection) except TypeError: pass try: - self.new_value_signal[dict].connect(channel.value_slot, Qt.QueuedConnection) + self.new_value_signal[object].connect(channel.value_slot, Qt.QueuedConnection) except TypeError: pass @@ -137,16 +133,12 @@ def remove_listener(self, channel, destroying: Optional[bool] = False) -> None: self.new_value_signal[str].disconnect(channel.value_slot) except TypeError: pass - try: - self.new_value_signal[ndarray].disconnect(channel.value_slot) - except TypeError: - pass try: self.new_value_signal[bool].disconnect(channel.value_slot) except TypeError: pass try: - self.new_value_signal[dict].disconnect(channel.value_slot) + self.new_value_signal[object].disconnect(channel.value_slot) except TypeError: pass diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index 68348ef93..29ff24b8a 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -741,6 +741,7 @@ def value_changed(self, new_val): self.value = new_val self.channeltype = type(self.value) if self.channeltype == np.ndarray: + print("cold") self.subtype = self.value.dtype.type else: try: diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 02a2de57c..b2ef4192c 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -166,6 +166,9 @@ def reverse(self): class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): + """ + PyDMNTTable + """ def __init__(self, parent=None, init_channel=None): super(PyDMNTTable, self).__init__(parent=parent, init_channel=init_channel) PyDMWidget.__init__(self, init_channel=init_channel) @@ -177,26 +180,36 @@ def __init__(self, parent=None, init_channel=None): self._table_values = [] def value_changed(self, data=None): + """ + Callback invoked when the Channel value is changed. + + Parameters + ---------- + data : dict + The new value from the channel. + """ if data is None: return - print(data, type(data), "test") - super(PyDMNTTable, self).value_changed(data) - labels = data.dtype.names - values = data.tolist() + super(PyDMNTTable, self).value_changed(data) - print(values, type(values), "test") + labels = data.get('labels', None) + values = data.get('value', {}) + + if not values: + values = data.values() if labels is None or len(labels) == 0: - labels = values.keys() - + labels = data.keys() + labels = list(labels) + try: - values = list(zip(*[v for k, v in values.items()])) + values = list(zip(*[v for k, v in data.items()])) except TypeError: logger.exception("NTTable value items must be iterables.") self._table_values = values - + if labels != self._table_labels: self._table_labels = labels self._model = PythonTableModel(labels, initial_list=values) From 027b58aa6f7d3fdd91d2c68f5c974045448e03cb Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 16 Mar 2023 12:03:02 -0700 Subject: [PATCH 04/37] small changes to revert necessary changes --- pydm/data_plugins/epics_plugins/p4p_plugin_component.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index d5123bc1b..a327718be 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -1,4 +1,3 @@ - import logging import numpy as np @@ -192,7 +191,7 @@ def add_listener(self, channel: PyDMChannel): except KeyError: pass try: - channel.value_signal[object].connect(self.put_value, Qt.QueuedConnection) + channel.value_signal[np.ndarray].connect(self.put_value, Qt.QueuedConnection) except KeyError: pass try: From 697afa28d2851522ee29aebc7b1461ec75e9ebd7 Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 16 Mar 2023 12:06:02 -0700 Subject: [PATCH 05/37] more small changes to revert unnecessary changes --- pydm/widgets/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index 29ff24b8a..fe5aebcb0 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -741,7 +741,6 @@ def value_changed(self, new_val): self.value = new_val self.channeltype = type(self.value) if self.channeltype == np.ndarray: - print("cold") self.subtype = self.value.dtype.type else: try: @@ -874,7 +873,7 @@ def connectionStateChanged(self, connected): @Slot(float) @Slot(str) @Slot(bool) - #@Slot(np.ndarray) + @Slot(np.ndarray) @Slot(object) def channelValueChanged(self, new_val): """ @@ -1350,11 +1349,11 @@ class PyDMWritableWidget(PyDMWidget): Emitted when the user changes the value """ - __Signals__ = ("send_value_signal([int], [float], [str], [bool], [object])") + __Signals__ = ("send_value_signal([int], [float], [str], [bool], [np.ndarray])") # Emitted when the user changes the value. - send_value_signal = Signal([int], [float], [str], [bool], [object]) + send_value_signal = Signal([int], [float], [str], [bool], [np.ndarray]) def __init__(self, init_channel=None): self._write_access = False From 4172d3c527cd3fee5f66e824d170f34b57cff21a Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 16 Mar 2023 13:07:50 -0700 Subject: [PATCH 06/37] a couple more small changes to revert unnecessary changes --- pydm/widgets/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index fe5aebcb0..26de51404 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -874,7 +874,6 @@ def connectionStateChanged(self, connected): @Slot(str) @Slot(bool) @Slot(np.ndarray) - @Slot(object) def channelValueChanged(self, new_val): """ PyQT Slot for changes on the Value of the Channel @@ -1352,7 +1351,6 @@ class PyDMWritableWidget(PyDMWidget): __Signals__ = ("send_value_signal([int], [float], [str], [bool], [np.ndarray])") # Emitted when the user changes the value. - send_value_signal = Signal([int], [float], [str], [bool], [np.ndarray]) def __init__(self, init_channel=None): From fd78b08d2694a3fde2e22727943b8408318ec05c Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 17 Mar 2023 16:17:01 -0700 Subject: [PATCH 07/37] beginning to implement a solution that will work for all widgets. two major issues outstanding with this solution --- pydm/data_plugins/__init__.py | 2 +- .../epics_plugins/p4p_plugin_component.py | 16 +++++++++++++++- pydm/data_plugins/plugin.py | 4 ++++ pydm/utilities/remove_protocol.py | 13 ++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py index 0621d9847..0116901b0 100644 --- a/pydm/data_plugins/__init__.py +++ b/pydm/data_plugins/__init__.py @@ -77,7 +77,7 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: Find the correct PyDMPlugin for a channel """ # Check for a configured protocol - protocol, addr = protocol_and_address(address) + protocol, addr, subfield = protocol_and_address(address) # Use default protocol if protocol is None and config.DEFAULT_PROTOCOL is not None: logger.debug("Using default protocol %s for %s", diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index a327718be..458056a74 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -32,7 +32,7 @@ def __init__(self, channel: PyDMChannel, address: str, """ super().__init__(channel, address, protocol, parent) self._connected = False - self.monitor = P4PPlugin.context.monitor(name=address, + self.monitor = P4PPlugin.context.monitor(name=self.address, cb=self.send_new_value, notify_disconnect=True) self.add_listener(channel) @@ -49,6 +49,8 @@ def __init__(self, channel: PyDMChannel, address: str, self._lower_warning_limit = None self._timestamp = None + self.nttable_data_location = PyDMPlugin.get_subfield(channel) + def clear_cache(self) -> None: """ Clear out all the stored values of this connection. """ self._value = None @@ -64,6 +66,8 @@ def clear_cache(self) -> None: self._lower_warning_limit = None self._timestamp = None + self.nttable_data_location = None + def send_new_value(self, value: Value) -> None: """ Callback invoked whenever a new value is received by our monitor. Emits signals based on values changed. """ if isinstance(value, Disconnected): @@ -88,10 +92,20 @@ def send_new_value(self, value: Value) -> None: continue else: has_value_changed_yet = True + if 'NTTable' in value.getID(): new_value = value.value.todict() else: new_value = value.value + + #temp code for testing + if self.nttable_data_location: + for value in self.nttable_data_location: + try: + new_value = new_value[value] + except IndexError: + new_value = new_value[int(value)] + if new_value is not None: if isinstance(new_value, np.ndarray): if 'NTNDArray' in value.getID(): diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 193195a2e..fbbf91475 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -253,6 +253,10 @@ def __init__(self): @staticmethod def get_address(channel): return protocol_and_address(channel.address)[1] + + @staticmethod + def get_subfield(channel): + return protocol_and_address(channel.address)[2] @staticmethod def get_connection_id(channel): diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 462a6a909..724f47aa6 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -37,8 +37,19 @@ def protocol_and_address(address): match = re.match('.*?://', address) protocol = None addr = address + subfield = None + if match: protocol = match.group(0)[:-3] addr = address.replace(match.group(0), '') + resulting_string = addr.split('/', 1) + + if len(resulting_string) < 2: + resulting_string.append(None) + + addr, subfield = resulting_string + + if subfield: + subfield = subfield.split('/') - return protocol, addr + return protocol, addr, subfield From 2a55563f114dd18ba4622344caa03a63b5defd4e Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 21 Mar 2023 14:41:16 -0700 Subject: [PATCH 08/37] attempts to address issue of appending a path the a PV_NAME to pass only a section of the NTTable data onto a widget which may not accept a dict --- .../epics_plugins/p4p_plugin_component.py | 32 ++++++++++++++----- pydm/utilities/remove_protocol.py | 27 +++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index 458056a74..f231cccee 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -1,6 +1,6 @@ import logging import numpy as np - +import collections from p4p.client.thread import Context, Disconnected from p4p.wrapper import Value from .pva_codec import decompress @@ -48,7 +48,6 @@ def __init__(self, channel: PyDMChannel, address: str, self._upper_warning_limit = None self._lower_warning_limit = None self._timestamp = None - self.nttable_data_location = PyDMPlugin.get_subfield(channel) def clear_cache(self) -> None: @@ -98,14 +97,31 @@ def send_new_value(self, value: Value) -> None: else: new_value = value.value - #temp code for testing if self.nttable_data_location: for value in self.nttable_data_location: - try: - new_value = new_value[value] - except IndexError: - new_value = new_value[int(value)] - + if isinstance(new_value, collections.Container) and type(new_value) != str: + if type(value) == str: + try: + new_value = new_value[value] + continue + except TypeError: + logger.debug('Type Error when attempting to use the given key, code will next attempt to convert the key to an int') + except KeyError: + msg = "Invalid channel address path for NTTable given. %s" + logger.exception(msg, self.nttable_data_location, exc_info=True) + raise KeyError("error in channel address") + + try: + new_value = new_value[int(value)] + except ValueError: + msg = "Invalid channel address path for NTTable given. %s" + logger.exception(msg, self.nttable_data_location, exc_info=True) + raise ValueError("error in channel address") + else: + msg = "Invalid channel address path for NTTable given. %s" + logger.exception(msg, self.nttable_data_location, exc_info=True) + raise ValueError("error in channel address") + if new_value is not None: if isinstance(new_value, np.ndarray): if 'NTNDArray' in value.getID(): diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 724f47aa6..8dfff144f 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -1,5 +1,5 @@ import re - +import urllib def remove_protocol(addr): """ @@ -20,7 +20,7 @@ def remove_protocol(addr): def protocol_and_address(address): """ - Returns the Protocol and Address pieces of a Channel Address + Returns the Protocol, Address and optional subfield pieces of a Channel Address Parameters ---------- @@ -33,23 +33,20 @@ def protocol_and_address(address): The protocol used. None in case the protocol is not specified. addr : str The piece of the address without the protocol. + subfield : list, str """ match = re.match('.*?://', address) - protocol = None + protocol = None addr = address subfield = None if match: - protocol = match.group(0)[:-3] - addr = address.replace(match.group(0), '') - resulting_string = addr.split('/', 1) - - if len(resulting_string) < 2: - resulting_string.append(None) - - addr, subfield = resulting_string - - if subfield: - subfield = subfield.split('/') - + parsed_address = urllib.parse.urlparse(address) + protocol = parsed_address.scheme + addr = parsed_address.netloc + subfield = parsed_address.path + + if subfield != '': + subfield = subfield[1:].split('/') + return protocol, addr, subfield From 747648ad3b7d28e42809f7ec7e2e6fdb7a33b470 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 21 Mar 2023 15:01:26 -0700 Subject: [PATCH 09/37] addresses an issue with two widgets looking at one NTTable channel, but one wants the whole dictionary while the other only wants a sub-field --- pydm/data_plugins/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index fbbf91475..eb112580d 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -267,6 +267,7 @@ def add_connection(self, channel): with self.lock: connection_id = self.get_connection_id(channel) address = self.get_address(channel) + # If this channel is already connected to this plugin lets ignore if channel in self.channels: return @@ -276,7 +277,7 @@ def add_connection(self, channel): return self.channels.add(channel) - if connection_id in self.connections: + if connection_id in self.connections and not PyDMPlugin.get_subfield(channel): self.connections[connection_id].add_listener(channel) else: self.connections[connection_id] = self.connection_class( From 3842540a451032110cb4db60bb957849498ab043 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 21 Mar 2023 15:33:57 -0700 Subject: [PATCH 10/37] last fix for multiple widgets looking at the same but diffrent parts of the NTTable data failed, this one seeems to fix the issue --- pydm/data_plugins/__init__.py | 2 +- pydm/data_plugins/plugin.py | 8 ++++++-- pydm/utilities/remove_protocol.py | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py index 0116901b0..3f6f2857b 100644 --- a/pydm/data_plugins/__init__.py +++ b/pydm/data_plugins/__init__.py @@ -77,7 +77,7 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: Find the correct PyDMPlugin for a channel """ # Check for a configured protocol - protocol, addr, subfield = protocol_and_address(address) + protocol, addr, subfield, full_addr = protocol_and_address(address) # Use default protocol if protocol is None and config.DEFAULT_PROTOCOL is not None: logger.debug("Using default protocol %s for %s", diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index eb112580d..4e05d6abf 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -250,6 +250,10 @@ def __init__(self): self.channels = weakref.WeakSet() self.lock = threading.Lock() + @staticmethod + def get_full_address(channel): + return protocol_and_address(channel.address)[3] + @staticmethod def get_address(channel): return protocol_and_address(channel.address)[1] @@ -260,7 +264,7 @@ def get_subfield(channel): @staticmethod def get_connection_id(channel): - return PyDMPlugin.get_address(channel) + return PyDMPlugin.get_full_address(channel) def add_connection(self, channel): from pydm.utilities import is_qt_designer @@ -277,7 +281,7 @@ def add_connection(self, channel): return self.channels.add(channel) - if connection_id in self.connections and not PyDMPlugin.get_subfield(channel): + if connection_id in self.connections: self.connections[connection_id].add_listener(channel) else: self.connections[connection_id] = self.connection_class( diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 8dfff144f..a5eb8d25f 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -45,8 +45,9 @@ def protocol_and_address(address): protocol = parsed_address.scheme addr = parsed_address.netloc subfield = parsed_address.path + full_addr = parsed_address.netloc + parsed_address.path if subfield != '': subfield = subfield[1:].split('/') - return protocol, addr, subfield + return protocol, addr, subfield, full_addr From 604bcd6e2b8430797dd5fa818c597bb80b43e957 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 21 Mar 2023 15:39:06 -0700 Subject: [PATCH 11/37] small fix --- pydm/data_plugins/__init__.py | 2 +- pydm/utilities/remove_protocol.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py index 3f6f2857b..945765e14 100644 --- a/pydm/data_plugins/__init__.py +++ b/pydm/data_plugins/__init__.py @@ -77,7 +77,7 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: Find the correct PyDMPlugin for a channel """ # Check for a configured protocol - protocol, addr, subfield, full_addr = protocol_and_address(address) + protocol, *_ = protocol_and_address(address) # Use default protocol if protocol is None and config.DEFAULT_PROTOCOL is not None: logger.debug("Using default protocol %s for %s", diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index a5eb8d25f..f39d5d438 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -14,7 +14,7 @@ def remove_protocol(addr): ------- str """ - _, addr = protocol_and_address(addr) + _, addr, *_ = protocol_and_address(addr) return addr From 0320821e4b8be0acb456a6f24ca35faf96e4610a Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 23 Mar 2023 10:47:35 -0700 Subject: [PATCH 12/37] small fix to address some failed tests, more fixes incoming --- pydm/utilities/remove_protocol.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index f39d5d438..bfe89e07a 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -39,7 +39,8 @@ def protocol_and_address(address): protocol = None addr = address subfield = None - + full_addr = None + if match: parsed_address = urllib.parse.urlparse(address) protocol = parsed_address.scheme From ea0816678636267a88d70c4d9a3ffc7e2f641a4a Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 23 Mar 2023 15:06:17 -0700 Subject: [PATCH 13/37] made changes to address breaking the calc plugin --- pydm/data_plugins/plugin.py | 22 ++++++++++++++++++++-- pydm/utilities/remove_protocol.py | 15 ++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 4e05d6abf..453faee08 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -252,7 +252,14 @@ def __init__(self): @staticmethod def get_full_address(channel): - return protocol_and_address(channel.address)[3] + parsed_address = protocol_and_address(channel.address)[2] + + if parsed_address: + full_addr = parsed_address.netloc + parsed_address.path + else: + full_addr = None + + return full_addr @staticmethod def get_address(channel): @@ -260,7 +267,18 @@ def get_address(channel): @staticmethod def get_subfield(channel): - return protocol_and_address(channel.address)[2] + + parsed_address = protocol_and_address(channel.address)[2] + + if parsed_address: + subfield = parsed_address.path + + if subfield != '': + subfield = subfield[1:].split('/') + else: + subfield = None + + return subfield @staticmethod def get_connection_id(channel): diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index bfe89e07a..eafc52248 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -14,13 +14,13 @@ def remove_protocol(addr): ------- str """ - _, addr, *_ = protocol_and_address(addr) + _, addr, _ = protocol_and_address(addr) return addr def protocol_and_address(address): """ - Returns the Protocol, Address and optional subfield pieces of a Channel Address + Returns the protocol, address and parsed address Parameters ---------- @@ -38,17 +38,10 @@ def protocol_and_address(address): match = re.match('.*?://', address) protocol = None addr = address - subfield = None - full_addr = None - + parsed_address = None if match: parsed_address = urllib.parse.urlparse(address) protocol = parsed_address.scheme addr = parsed_address.netloc - subfield = parsed_address.path - full_addr = parsed_address.netloc + parsed_address.path - - if subfield != '': - subfield = subfield[1:].split('/') - return protocol, addr, subfield, full_addr + return protocol, addr, parsed_address From cb794963a9776da3ead04422d2b986970562a5f6 Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 23 Mar 2023 19:58:58 -0700 Subject: [PATCH 14/37] last fix for calc plugin was incomplete. hopfully this does the trick --- pydm/utilities/remove_protocol.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index eafc52248..41b65144e 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -39,9 +39,15 @@ def protocol_and_address(address): protocol = None addr = address parsed_address = None + if match: parsed_address = urllib.parse.urlparse(address) protocol = parsed_address.scheme - addr = parsed_address.netloc + + if protocol == 'calc' or protocol == 'loc': + addr = parsed_address.netloc + parsed_address.query + else: + addr = parsed_address.netloc + return protocol, addr, parsed_address From 219d3a1550d62a5ded4166a4df70942f01f0787f Mon Sep 17 00:00:00 2001 From: YektaY Date: Thu, 23 Mar 2023 21:00:03 -0700 Subject: [PATCH 15/37] small additional fix for calc plugin issue --- pydm/utilities/remove_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 41b65144e..f35fcd852 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -45,7 +45,7 @@ def protocol_and_address(address): protocol = parsed_address.scheme if protocol == 'calc' or protocol == 'loc': - addr = parsed_address.netloc + parsed_address.query + addr = parsed_address.netloc + '?' + parsed_address.query else: addr = parsed_address.netloc From 562eebafe9a01fa1bf71d5f9e3e5dd54d932165c Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 24 Mar 2023 11:16:53 -0700 Subject: [PATCH 16/37] created a new method to handle the parsing of the address and reverted the old protocol_and_address method to it's original state --- pydm/data_plugins/__init__.py | 5 ++-- pydm/data_plugins/plugin.py | 23 ++++++++++++------- pydm/utilities/__init__.py | 2 +- pydm/utilities/remove_protocol.py | 38 +++++++++++++++++++------------ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py index 945765e14..16fbd3db5 100644 --- a/pydm/data_plugins/__init__.py +++ b/pydm/data_plugins/__init__.py @@ -14,8 +14,7 @@ from qtpy.QtWidgets import QApplication from .. import config -from ..utilities import (import_module_by_filename, log_failures, - protocol_and_address) +from ..utilities import (import_module_by_filename, log_failures, parsed_address) from .plugin import PyDMPlugin logger = logging.getLogger(__name__) @@ -77,7 +76,7 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: Find the correct PyDMPlugin for a channel """ # Check for a configured protocol - protocol, *_ = protocol_and_address(address) + protocol = parsed_address(address).scheme # Use default protocol if protocol is None and config.DEFAULT_PROTOCOL is not None: logger.debug("Using default protocol %s for %s", diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index 453faee08..cf10eb8d2 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -5,7 +5,7 @@ from numpy import ndarray from typing import Optional, Callable -from ..utilities.remove_protocol import protocol_and_address +from ..utilities.remove_protocol import protocol_and_address, parsed_address from qtpy.QtCore import Signal, QObject, Qt from qtpy.QtWidgets import QApplication from .. import config @@ -252,10 +252,10 @@ def __init__(self): @staticmethod def get_full_address(channel): - parsed_address = protocol_and_address(channel.address)[2] + parsed_addr = parsed_address(channel.address) - if parsed_address: - full_addr = parsed_address.netloc + parsed_address.path + if parsed_addr: + full_addr = parsed_addr.netloc + parsed_addr.path else: full_addr = None @@ -263,15 +263,22 @@ def get_full_address(channel): @staticmethod def get_address(channel): - return protocol_and_address(channel.address)[1] + parsed_addr = parsed_address(channel.address) + addr = parsed_addr.netloc + protocol = parsed_addr.scheme + + if protocol == 'calc' or protocol == 'loc': + addr = parsed_addr.netloc + '?' + parsed_addr.query + + return addr @staticmethod def get_subfield(channel): - parsed_address = protocol_and_address(channel.address)[2] + parsed_addr = parsed_address(channel.address) - if parsed_address: - subfield = parsed_address.path + if parsed_addr: + subfield = parsed_addr.path if subfield != '': subfield = subfield[1:].split('/') diff --git a/pydm/utilities/__init__.py b/pydm/utilities/__init__.py index 36f9f757e..4f252b340 100644 --- a/pydm/utilities/__init__.py +++ b/pydm/utilities/__init__.py @@ -17,7 +17,7 @@ from . import colors, macro, shortcuts from .connection import close_widget_connections, establish_widget_connections from .iconfont import IconFont -from .remove_protocol import protocol_and_address, remove_protocol +from .remove_protocol import protocol_and_address, remove_protocol, parsed_address from .units import convert, find_unit_options, find_unittype logger = logging.getLogger(__name__) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index f35fcd852..70f7cb2b2 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -14,40 +14,50 @@ def remove_protocol(addr): ------- str """ - _, addr, _ = protocol_and_address(addr) + _, addr = protocol_and_address(addr) return addr def protocol_and_address(address): """ - Returns the protocol, address and parsed address - + Returns the Protocol and Address pieces of a Channel Address Parameters ---------- address : str The address from which to remove the address prefix. - Returns ------- protocol : str The protocol used. None in case the protocol is not specified. addr : str The piece of the address without the protocol. - subfield : list, str """ match = re.match('.*?://', address) - protocol = None + protocol = None addr = address + if match: + protocol = match.group(0)[:-3] + addr = address.replace(match.group(0), '') + + return protocol, addr + +def parsed_address(address): + """ + Returns the given address parsed into a 6-tuple. The parsing is done by urllib.parse.urlparse + + Parameters + ---------- + address : str + The address from which to remove the address prefix. + + Returns + ------- + parsed_address : tuple + """ + match = re.match('.*?://', address) parsed_address = None if match: parsed_address = urllib.parse.urlparse(address) - protocol = parsed_address.scheme - - if protocol == 'calc' or protocol == 'loc': - addr = parsed_address.netloc + '?' + parsed_address.query - else: - addr = parsed_address.netloc - - return protocol, addr, parsed_address + return parsed_address From 0662a307c296c496d51dca6816a139bac93a8d90 Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 24 Mar 2023 13:44:34 -0700 Subject: [PATCH 17/37] woking on addressing some broken tests --- pydm/data_plugins/__init__.py | 10 ++++++++-- pydm/utilities/remove_protocol.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py index 16fbd3db5..16c2ba9e4 100644 --- a/pydm/data_plugins/__init__.py +++ b/pydm/data_plugins/__init__.py @@ -76,7 +76,11 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: Find the correct PyDMPlugin for a channel """ # Check for a configured protocol - protocol = parsed_address(address).scheme + try: + protocol = parsed_address(address).scheme + except AttributeError: + protocol = None + # Use default protocol if protocol is None and config.DEFAULT_PROTOCOL is not None: logger.debug("Using default protocol %s for %s", @@ -84,7 +88,8 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: # If no protocol was specified, and the default protocol # environment variable is specified, try to use that instead. protocol = config.DEFAULT_PROTOCOL - # Load proper plugin module + + # Load proper plugin module if protocol: initialize_plugins_if_needed() try: @@ -97,6 +102,7 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]: "will receive no data. To specify a default protocol, " "set the PYDM_DEFAULT_PROTOCOL environment variable." "".format(addr=address)) + return None diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 70f7cb2b2..bb76f5c47 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -21,10 +21,12 @@ def remove_protocol(addr): def protocol_and_address(address): """ Returns the Protocol and Address pieces of a Channel Address + Parameters ---------- address : str The address from which to remove the address prefix. + Returns ------- protocol : str @@ -54,6 +56,9 @@ def parsed_address(address): ------- parsed_address : tuple """ + if type(address) != str: + return None + match = re.match('.*?://', address) parsed_address = None From 7a71ecc20cc63a777113e97ef0a79f77e1ed487c Mon Sep 17 00:00:00 2001 From: YektaY Date: Mon, 27 Mar 2023 10:59:34 -0700 Subject: [PATCH 18/37] added some documentation --- docs/source/data_plugins/p4p_plugin.rst | 21 ++++++++++++++++++++- pydm/data_plugins/plugin.py | 3 +-- pydm/widgets/nt_table.py | 19 ++++++++++++++++--- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/docs/source/data_plugins/p4p_plugin.rst b/docs/source/data_plugins/p4p_plugin.rst index b007290b8..d6c970aa6 100644 --- a/docs/source/data_plugins/p4p_plugin.rst +++ b/docs/source/data_plugins/p4p_plugin.rst @@ -28,7 +28,7 @@ P4P is the only option (and will be chosen automatically if this variable is not in the future. Supported Types ---------------- +=============== Currently this data plugin supports all `normative types`_. The values and control variables are pulled out of the data received and sent through the existing PyDM signals to be read by widgets via the channels they are @@ -39,6 +39,25 @@ currently possible in this version of the plugin. For example, defining a group result in the named fields being sent to the widgets. Full support for structured data is planned to be supported as part of a future release. +NTTables +-------- + +The plugin accepts NTTables. It will convert NTTables in python dictionaries which are then passed to the pydm widgets. +Not all widgets will accept a dictionary (or the whole NTTable) as an input. +A specified section of the NTTable can be passed to a those pydm widgets which do not accept dictionaries. +If the PV is passing an NTTable and the user wants to pass only a specific subfield of the NTTable. +This can be achieved via appending a ``/`` followed by the key or name of the column header of the subfield of the NTTable. +For example:: + + pva://MTEST/subfield + +multiple layers of subfields also works:: + + pva://MTEST/sub-field/subfield_of_a_subfield + +Image decompression +------------------- + Image decompression is performed when image data is specified using an ``NTNDArray`` with the ``codec`` field set. The decompression algorithm to apply will be determined by what the ``codec`` field is set to. In order for decompression to happen, the python package for the associated codec must be installed in the environment diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py index cf10eb8d2..6a4b37d4e 100644 --- a/pydm/data_plugins/plugin.py +++ b/pydm/data_plugins/plugin.py @@ -5,7 +5,7 @@ from numpy import ndarray from typing import Optional, Callable -from ..utilities.remove_protocol import protocol_and_address, parsed_address +from ..utilities.remove_protocol import parsed_address from qtpy.QtCore import Signal, QObject, Qt from qtpy.QtWidgets import QApplication from .. import config @@ -274,7 +274,6 @@ def get_address(channel): @staticmethod def get_subfield(channel): - parsed_addr = parsed_address(channel.address) if parsed_addr: diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index b2ef4192c..30a82bf59 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -1,7 +1,5 @@ import logging - from operator import itemgetter - from pydm.widgets.base import PyDMWidget from qtpy import QtCore, QtWidgets @@ -167,7 +165,22 @@ def reverse(self): class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): """ - PyDMNTTable + The PyDMNTTable is a table widget used to display PVA NTTable data. + + The PyDMNTTable has two ways of filling the table from the data. + If the incoming data dictionary has a 'labels' and/or a 'value' key. + Then the list of labels will be set with the data from the 'labels' key. + While the data from the 'value' key will be used to set the values in the table. + if neither 'labels' or 'value' key are present in the incoming 'data' dictionary, + then the keys of the data dictionary are set as the labels + and all the values stored by the keys will make up the values of the table. + + Parameters + ---------- + parent : QWidget, optional + The parent widget for the PyDMNTTable + init_channel : str, optional + The channel to be used by the widget. """ def __init__(self, parent=None, init_channel=None): super(PyDMNTTable, self).__init__(parent=parent, init_channel=init_channel) From 24d15dcbcceca3b3a738711eefcd82c58ef1c26e Mon Sep 17 00:00:00 2001 From: YektaY Date: Mon, 27 Mar 2023 11:03:35 -0700 Subject: [PATCH 19/37] added documentation for the nttable widget --- docs/source/widgets/nt_table.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/source/widgets/nt_table.rst diff --git a/docs/source/widgets/nt_table.rst b/docs/source/widgets/nt_table.rst new file mode 100644 index 000000000..4fc2fd9c6 --- /dev/null +++ b/docs/source/widgets/nt_table.rst @@ -0,0 +1,13 @@ +####################### +PyDMNTTable +####################### + +.. autoclass:: pydm.widgets.nt_table.PythonTableModel + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: pydm.widgets.nt_table.PyDMNTTable + :members: + :inherited-members: + :show-inheritance: \ No newline at end of file From 2ae203c8ab31c5c48c473c4447212c79d5083449 Mon Sep 17 00:00:00 2001 From: YektaY Date: Mon, 27 Mar 2023 11:14:03 -0700 Subject: [PATCH 20/37] small syntax fix in doc --- docs/source/data_plugins/p4p_plugin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/data_plugins/p4p_plugin.rst b/docs/source/data_plugins/p4p_plugin.rst index d6c970aa6..53a8b7975 100644 --- a/docs/source/data_plugins/p4p_plugin.rst +++ b/docs/source/data_plugins/p4p_plugin.rst @@ -45,8 +45,8 @@ NTTables The plugin accepts NTTables. It will convert NTTables in python dictionaries which are then passed to the pydm widgets. Not all widgets will accept a dictionary (or the whole NTTable) as an input. A specified section of the NTTable can be passed to a those pydm widgets which do not accept dictionaries. -If the PV is passing an NTTable and the user wants to pass only a specific subfield of the NTTable. -This can be achieved via appending a ``/`` followed by the key or name of the column header of the subfield of the NTTable. +If the PV is passing an NTTable and the user wants to pass only a specific subfield of the NTTable this can be achieved via appending a ``/`` +followed by the key or name of the column header of the subfield of the NTTable. For example:: pva://MTEST/subfield From ab6db0ea7188200b5935ca860ac98bd23e5bb76a Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 14:36:03 -0700 Subject: [PATCH 21/37] added tests for two methods in remove_protocol.py --- pydm/tests/utilities/test_remove_protocol.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pydm/tests/utilities/test_remove_protocol.py b/pydm/tests/utilities/test_remove_protocol.py index 11b24f619..4a03b8deb 100644 --- a/pydm/tests/utilities/test_remove_protocol.py +++ b/pydm/tests/utilities/test_remove_protocol.py @@ -1,5 +1,6 @@ from ...utilities.remove_protocol import remove_protocol - +from ...utilities.remove_protocol import protocol_and_address +from ...utilities.remove_protocol import parsed_address def test_remove_protocol(): out = remove_protocol('foo://bar') @@ -10,3 +11,22 @@ def test_remove_protocol(): out = remove_protocol('foo://bar://foo2') assert (out == 'bar://foo2') + +def test_protocol_and_address(): + out = protocol_and_address('foo://bar') + assert (out == ('foo', 'bar')) + + out = protocol_and_address('foo:/bar') + assert (out == (None, 'foo:/bar')) + +def test_parsed_address(): + out = parsed_address(1) + assert (out == None) + + out = parsed_address('foo:/bar') + assert (out == None) + + out = parsed_address('foo://bar') + assert (out == ('foo', 'bar', '', '', '', '')) + + From 23c324ee34951845bd3baa352a2dae5a96271cd6 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 14:58:57 -0700 Subject: [PATCH 22/37] Update pydm/data_plugins/epics_plugins/p4p_plugin_component.py Co-authored-by: Ken Lauer --- pydm/data_plugins/epics_plugins/p4p_plugin_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index f231cccee..c17d367ae 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -108,7 +108,7 @@ def send_new_value(self, value: Value) -> None: logger.debug('Type Error when attempting to use the given key, code will next attempt to convert the key to an int') except KeyError: msg = "Invalid channel address path for NTTable given. %s" - logger.exception(msg, self.nttable_data_location, exc_info=True) + logger.exception(msg, self.nttable_data_location) raise KeyError("error in channel address") try: From bf3e8a7dade356347f1e15e4255cf7b328479858 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 15:00:08 -0700 Subject: [PATCH 23/37] Update pydm/widgets/nt_table.py Co-authored-by: Ken Lauer --- pydm/widgets/nt_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 30a82bf59..670e343b0 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -183,7 +183,7 @@ class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): The channel to be used by the widget. """ def __init__(self, parent=None, init_channel=None): - super(PyDMNTTable, self).__init__(parent=parent, init_channel=init_channel) + super().__init__(parent=parent, init_channel=init_channel) PyDMWidget.__init__(self, init_channel=init_channel) self.setLayout(QtWidgets.QVBoxLayout()) self._table = QtWidgets.QTableView(self) From a1e86027ae9fa620961bc89b3261376de5a135f7 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 15:00:32 -0700 Subject: [PATCH 24/37] Update pydm/widgets/nt_table.py Co-authored-by: Ken Lauer --- pydm/widgets/nt_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 670e343b0..25d9c0b6b 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -9,7 +9,7 @@ class PythonTableModel(QtCore.QAbstractTableModel): def __init__(self, column_names, initial_list=[], parent=None, edit_method=None, can_edit_method=None): - super(PythonTableModel, self).__init__(parent=parent) + super().__init__(parent=parent) self._list = [] self._column_names = column_names self.edit_method = edit_method From 9cc987c2f336e310714a59e8e0454bc724bad557 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 15:59:35 -0700 Subject: [PATCH 25/37] Update pydm/widgets/nt_table.py Co-authored-by: Ken Lauer --- pydm/widgets/nt_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 25d9c0b6b..056be62b1 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -23,7 +23,7 @@ def list(self): @list.setter def list(self, new_list): self.beginResetModel() - self._list = new_list + self._list = list(new_list) self.endResetModel() # QAbstractItemModel Implementation From 0fd19c7c4d388413d0e8b97993ae40a0a2e50004 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 28 Mar 2023 16:30:21 -0700 Subject: [PATCH 26/37] addesses a mutable default argument --- pydm/widgets/nt_table.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 056be62b1..b330180c9 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -7,10 +7,10 @@ class PythonTableModel(QtCore.QAbstractTableModel): - def __init__(self, column_names, initial_list=[], parent=None, + def __init__(self, column_names, initial_list=None, parent=None, edit_method=None, can_edit_method=None): super().__init__(parent=parent) - self._list = [] + self._list = None self._column_names = column_names self.edit_method = edit_method self.can_edit_method = can_edit_method @@ -22,6 +22,8 @@ def list(self): @list.setter def list(self, new_list): + if new_list is None: + new_list = [] self.beginResetModel() self._list = list(new_list) self.endResetModel() From bb65f6ac9c4921d13ec4d7833e6b42f2c282623a Mon Sep 17 00:00:00 2001 From: YektaY Date: Wed, 29 Mar 2023 13:35:12 -0700 Subject: [PATCH 27/37] changed how error message is handled and changed away from type check to isInstance --- .../epics_plugins/p4p_plugin_component.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py index c17d367ae..7414cb122 100644 --- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py +++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py @@ -98,8 +98,11 @@ def send_new_value(self, value: Value) -> None: new_value = value.value if self.nttable_data_location: + msg = f"Invalid channel... {self.nttable_data_location}" + for value in self.nttable_data_location: - if isinstance(new_value, collections.Container) and type(new_value) != str: + if isinstance(new_value, collections.Container) and not isinstance(new_value, str): + if type(value) == str: try: new_value = new_value[value] @@ -107,20 +110,15 @@ def send_new_value(self, value: Value) -> None: except TypeError: logger.debug('Type Error when attempting to use the given key, code will next attempt to convert the key to an int') except KeyError: - msg = "Invalid channel address path for NTTable given. %s" - logger.exception(msg, self.nttable_data_location) - raise KeyError("error in channel address") + logger.exception(msg) try: new_value = new_value[int(value)] except ValueError: - msg = "Invalid channel address path for NTTable given. %s" - logger.exception(msg, self.nttable_data_location, exc_info=True) - raise ValueError("error in channel address") + logger.exception(msg, exc_info=True) else: - msg = "Invalid channel address path for NTTable given. %s" - logger.exception(msg, self.nttable_data_location, exc_info=True) - raise ValueError("error in channel address") + logger.exception(msg, exc_info=True) + raise ValueError(msg) if new_value is not None: if isinstance(new_value, np.ndarray): From 900e6ea27df62916ecd6650136065f35042f448d Mon Sep 17 00:00:00 2001 From: YektaY Date: Wed, 29 Mar 2023 13:36:36 -0700 Subject: [PATCH 28/37] Update pydm/widgets/nt_table.py Co-authored-by: jbellister-slac <89539359+jbellister-slac@users.noreply.github.com> --- pydm/widgets/nt_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index b330180c9..730b22abf 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -185,7 +185,7 @@ class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): The channel to be used by the widget. """ def __init__(self, parent=None, init_channel=None): - super().__init__(parent=parent, init_channel=init_channel) + super().__init__(parent=parent) PyDMWidget.__init__(self, init_channel=init_channel) self.setLayout(QtWidgets.QVBoxLayout()) self._table = QtWidgets.QTableView(self) From 4f6f33447a5eb7ce959b178be6521ee198da5b9b Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 31 Mar 2023 10:28:31 -0700 Subject: [PATCH 29/37] added the option to write to an NTTable --- pydm/widgets/base.py | 4 +-- pydm/widgets/nt_table.py | 56 +++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py index 26de51404..bacd67b21 100644 --- a/pydm/widgets/base.py +++ b/pydm/widgets/base.py @@ -1348,10 +1348,10 @@ class PyDMWritableWidget(PyDMWidget): Emitted when the user changes the value """ - __Signals__ = ("send_value_signal([int], [float], [str], [bool], [np.ndarray])") + __Signals__ = ("send_value_signal([int], [float], [str], [bool], [object])") # Emitted when the user changes the value. - send_value_signal = Signal([int], [float], [str], [bool], [np.ndarray]) + send_value_signal = Signal([int], [float], [str], [bool], [object]) def __init__(self, init_channel=None): self._write_access = False diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 730b22abf..6e758b1bb 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -1,6 +1,6 @@ import logging from operator import itemgetter -from pydm.widgets.base import PyDMWidget +from pydm.widgets.base import PyDMWidget, PyDMWritableWidget from qtpy import QtCore, QtWidgets logger = logging.getLogger(__name__) @@ -10,6 +10,7 @@ class PythonTableModel(QtCore.QAbstractTableModel): def __init__(self, column_names, initial_list=None, parent=None, edit_method=None, can_edit_method=None): super().__init__(parent=parent) + self.parent = parent self._list = None self._column_names = column_names self.edit_method = edit_method @@ -78,8 +79,9 @@ def setData(self, index, value, role=QtCore.Qt.EditRole): return False if index.column() >= self.columnCount(): return False - success = self.edit_method(self._list[index.row()][index.column()], - value.toPyObject()) + + success = self.edit_method(self.parent, index.row(), index.column(), value) + if success: self.dataChanged.emit(index, index) return success @@ -165,7 +167,7 @@ def reverse(self): self.layoutChanged.emit() -class PyDMNTTable(QtWidgets.QWidget, PyDMWidget): +class PyDMNTTable(QtWidgets.QWidget, PyDMWritableWidget): """ The PyDMNTTable is a table widget used to display PVA NTTable data. @@ -193,6 +195,17 @@ def __init__(self, parent=None, init_channel=None): self._model = None self._table_labels = None self._table_values = [] + self._can_edit = False + self.edit_method = None + + @QtCore.Property(bool) + def set_edit(self): + return self._can_edit + + @set_edit.setter + def set_edit(self, value): + if self._can_edit != value: + self._can_edit = value def value_changed(self, data=None): """ @@ -207,7 +220,7 @@ def value_changed(self, data=None): return super(PyDMNTTable, self).value_changed(data) - + labels = data.get('labels', None) values = data.get('value', {}) @@ -226,8 +239,39 @@ def value_changed(self, data=None): self._table_values = values if labels != self._table_labels: + + if self.set_edit: + self.edit_method = PyDMNTTable.send_table + else: + self.edit_method = None + self._table_labels = labels - self._model = PythonTableModel(labels, initial_list=values) + self._model = PythonTableModel(labels, + initial_list=values, + parent=self, + edit_method=self.edit_method) self._table.setModel(self._model) else: self._model.list = values + + def send_table(self, row, column, value): + """ + Update Channel value when cell value is changed. + + Parameters + ---------- + row : int + index of row + column : int + index of column + value : str + new value of cell + """ + self.value[self._table_labels[column]][row] = value + + # dictionary needs to be wrapped in another dictionary with a key 'value' + # to be passed back to the p4p plugin. + emit_dict = {'value': self.value} + + self.send_value_signal[object].emit(emit_dict) + return True \ No newline at end of file From 8beccec04b5b68d0ed793ccfdc8e0832d9f76dff Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 31 Mar 2023 13:52:37 -0700 Subject: [PATCH 30/37] fixed issue with writting to an array --- pydm/widgets/nt_table.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 6e758b1bb..11101a238 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -1,4 +1,5 @@ import logging +import numpy as np from operator import itemgetter from pydm.widgets.base import PyDMWidget, PyDMWritableWidget from qtpy import QtCore, QtWidgets @@ -199,11 +200,11 @@ def __init__(self, parent=None, init_channel=None): self.edit_method = None @QtCore.Property(bool) - def set_edit(self): + def readOnly(self): return self._can_edit - @set_edit.setter - def set_edit(self, value): + @readOnly.setter + def readOnly(self, value): if self._can_edit != value: self._can_edit = value @@ -240,7 +241,7 @@ def value_changed(self, data=None): if labels != self._table_labels: - if self.set_edit: + if self.readOnly: self.edit_method = PyDMNTTable.send_table else: self.edit_method = None @@ -267,6 +268,10 @@ def send_table(self, row, column, value): value : str new value of cell """ + if isinstance(self.value[self._table_labels[column]], np.ndarray): + self.value[self._table_labels[column]] = self.value[self._table_labels[column]].copy() + self.value[self._table_labels[column]].setflags(write=True) + self.value[self._table_labels[column]][row] = value # dictionary needs to be wrapped in another dictionary with a key 'value' From 1e22476b47693261a5e2abd9f31be885b5ceb9b5 Mon Sep 17 00:00:00 2001 From: YektaY Date: Fri, 31 Mar 2023 13:55:11 -0700 Subject: [PATCH 31/37] small change from code review suggestion --- pydm/widgets/nt_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 11101a238..228882684 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -278,5 +278,5 @@ def send_table(self, row, column, value): # to be passed back to the p4p plugin. emit_dict = {'value': self.value} - self.send_value_signal[object].emit(emit_dict) + self.send_value_signal[dict].emit(emit_dict) return True \ No newline at end of file From a4ac7cdc032775be800ae4b9a56a47abdd0edc25 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 4 Apr 2023 14:38:21 -0700 Subject: [PATCH 32/37] added a note to documentation about not writing to subfields and made changes noted during code review --- docs/source/data_plugins/p4p_plugin.rst | 4 ++++ pydm/widgets/nt_table.py | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/source/data_plugins/p4p_plugin.rst b/docs/source/data_plugins/p4p_plugin.rst index 53a8b7975..43f9c2e11 100644 --- a/docs/source/data_plugins/p4p_plugin.rst +++ b/docs/source/data_plugins/p4p_plugin.rst @@ -55,6 +55,10 @@ multiple layers of subfields also works:: pva://MTEST/sub-field/subfield_of_a_subfield +Note: currenty subfields can only be used to read a subset of data from an NTTables +and can't be used to write to the subfield. A follow up release will add an ability to +write to the subfield. + Image decompression ------------------- diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 228882684..4ecca2244 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -196,17 +196,25 @@ def __init__(self, parent=None, init_channel=None): self._model = None self._table_labels = None self._table_values = [] - self._can_edit = False + self._read_only = True self.edit_method = None @QtCore.Property(bool) def readOnly(self): - return self._can_edit + return self._read_only @readOnly.setter def readOnly(self, value): - if self._can_edit != value: - self._can_edit = value + if self._read_only != value: + self._read_only = value + + def check_enable_state(self): + """ + Checks whether or not the widget should be disable. + + """ + PyDMWritableWidget.check_enable_state(self) + self.setEnabled(True) def value_changed(self, data=None): """ @@ -241,7 +249,7 @@ def value_changed(self, data=None): if labels != self._table_labels: - if self.readOnly: + if not self.readOnly: self.edit_method = PyDMNTTable.send_table else: self.edit_method = None @@ -270,7 +278,6 @@ def send_table(self, row, column, value): """ if isinstance(self.value[self._table_labels[column]], np.ndarray): self.value[self._table_labels[column]] = self.value[self._table_labels[column]].copy() - self.value[self._table_labels[column]].setflags(write=True) self.value[self._table_labels[column]][row] = value From ec38688e196f5dda5254e9cda8859f43eaa96432 Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 4 Apr 2023 15:00:32 -0700 Subject: [PATCH 33/37] added tooltip when running in read only mode: --- pydm/widgets/nt_table.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py index 4ecca2244..52e9db3ef 100644 --- a/pydm/widgets/nt_table.py +++ b/pydm/widgets/nt_table.py @@ -188,6 +188,8 @@ class PyDMNTTable(QtWidgets.QWidget, PyDMWritableWidget): The channel to be used by the widget. """ def __init__(self, parent=None, init_channel=None): + self._read_only = True + super().__init__(parent=parent) PyDMWidget.__init__(self, init_channel=init_channel) self.setLayout(QtWidgets.QVBoxLayout()) @@ -196,7 +198,6 @@ def __init__(self, parent=None, init_channel=None): self._model = None self._table_labels = None self._table_values = [] - self._read_only = True self.edit_method = None @QtCore.Property(bool) @@ -215,6 +216,14 @@ def check_enable_state(self): """ PyDMWritableWidget.check_enable_state(self) self.setEnabled(True) + tooltip = self.toolTip() + + if self.readOnly: + if tooltip != '': + tooltip += '\n' + tooltip += "Running PyDMNTTable on Read-Only mode." + + self.setToolTip(tooltip) def value_changed(self, data=None): """ @@ -278,7 +287,7 @@ def send_table(self, row, column, value): """ if isinstance(self.value[self._table_labels[column]], np.ndarray): self.value[self._table_labels[column]] = self.value[self._table_labels[column]].copy() - + self.value[self._table_labels[column]][row] = value # dictionary needs to be wrapped in another dictionary with a key 'value' From 97f2854f6c5b03dfddd2dee5db0296e929aa72ca Mon Sep 17 00:00:00 2001 From: YektaY Date: Tue, 4 Apr 2023 17:34:35 -0700 Subject: [PATCH 34/37] added a fix to an issue when the protocal is not listed. A good catch from Jesse --- pydm/utilities/remove_protocol.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index bb76f5c47..6c7af4f85 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -1,5 +1,6 @@ import re import urllib +from .. import config def remove_protocol(addr): """ @@ -64,5 +65,8 @@ def parsed_address(address): if match: parsed_address = urllib.parse.urlparse(address) + else: + parsed_address = urllib.parse.urlparse(config.DEFAULT_PROTOCOL + '://' + address) + return parsed_address From a50e87a4261425095adba301471854eef3e978f7 Mon Sep 17 00:00:00 2001 From: YektaY Date: Wed, 5 Apr 2023 09:40:13 -0700 Subject: [PATCH 35/37] added a check to see if config.DEFAULT_PROTOCOL is not None --- pydm/utilities/remove_protocol.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pydm/utilities/remove_protocol.py b/pydm/utilities/remove_protocol.py index 6c7af4f85..a620403e8 100644 --- a/pydm/utilities/remove_protocol.py +++ b/pydm/utilities/remove_protocol.py @@ -62,11 +62,10 @@ def parsed_address(address): match = re.match('.*?://', address) parsed_address = None - + if match: parsed_address = urllib.parse.urlparse(address) - else: + elif config.DEFAULT_PROTOCOL: parsed_address = urllib.parse.urlparse(config.DEFAULT_PROTOCOL + '://' + address) - return parsed_address From 521e0f21bac75477d7a40fc6d22708134ebc4a7a Mon Sep 17 00:00:00 2001 From: YektaY Date: Wed, 5 Apr 2023 11:13:00 -0700 Subject: [PATCH 36/37] made change to parsed_address test --- pydm/tests/utilities/test_remove_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydm/tests/utilities/test_remove_protocol.py b/pydm/tests/utilities/test_remove_protocol.py index 4a03b8deb..919128555 100644 --- a/pydm/tests/utilities/test_remove_protocol.py +++ b/pydm/tests/utilities/test_remove_protocol.py @@ -24,7 +24,7 @@ def test_parsed_address(): assert (out == None) out = parsed_address('foo:/bar') - assert (out == None) + assert (out == ('foo', '/bar', '', '', '', '')) out = parsed_address('foo://bar') assert (out == ('foo', 'bar', '', '', '', '')) From ebc7a6ead0295869e60e5d7451fcc7a9bae7efd4 Mon Sep 17 00:00:00 2001 From: YektaY Date: Wed, 5 Apr 2023 12:03:14 -0700 Subject: [PATCH 37/37] changed the test_plugin_for_address test to not ahve teh default protocol effect other tests. reverted test_parsed_address test --- pydm/tests/test_data_plugins_import.py | 4 ++-- pydm/tests/utilities/test_remove_protocol.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pydm/tests/test_data_plugins_import.py b/pydm/tests/test_data_plugins_import.py index 3e31ddaf9..9fce26263 100644 --- a/pydm/tests/test_data_plugins_import.py +++ b/pydm/tests/test_data_plugins_import.py @@ -39,13 +39,13 @@ def test_plugin_directory_loading(qapp, caplog): os.remove(os.path.join(cur_dir, 'plugin_foo.py')) -def test_plugin_for_address(test_plugin): +def test_plugin_for_address(test_plugin, monkeypatch): # Get by protocol assert isinstance(plugin_for_address('tst://tst:this'), test_plugin) assert plugin_for_address('tst:this') is None # Default protocol - config.DEFAULT_PROTOCOL = 'tst' + monkeypatch.setattr(config, 'DEFAULT_PROTOCOL', 'tst') assert isinstance(plugin_for_address('tst:this'), test_plugin) diff --git a/pydm/tests/utilities/test_remove_protocol.py b/pydm/tests/utilities/test_remove_protocol.py index 919128555..4a03b8deb 100644 --- a/pydm/tests/utilities/test_remove_protocol.py +++ b/pydm/tests/utilities/test_remove_protocol.py @@ -24,7 +24,7 @@ def test_parsed_address(): assert (out == None) out = parsed_address('foo:/bar') - assert (out == ('foo', '/bar', '', '', '', '')) + assert (out == None) out = parsed_address('foo://bar') assert (out == ('foo', 'bar', '', '', '', ''))