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

MNT: make enums use python Enum class and updated Q_ENUM (no 's') #1146

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
12 changes: 9 additions & 3 deletions pydm/tests/widgets/test_enum_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ...widgets.enum_button import PyDMEnumButton, WidgetType, class_for_type
from ... import data_plugins
from ...utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes


def test_construct(qtbot):
Expand Down Expand Up @@ -43,11 +44,15 @@ def test_widget_type(qtbot, widget_type):
qtbot.addWidget(widget)

assert widget.widgetType == WidgetType.PushButton
assert isinstance(widget._widgets[0], class_for_type[WidgetType.PushButton])
# Support both pyqt enums (inherit from 'object') and pyside6 enums (inherit from python 'Enum' and
# therefore require '.value')
index = WidgetType.PushButton if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else WidgetType.PushButton.value
assert isinstance(widget._widgets[0], class_for_type[index])

widget.widgetType = widget_type
assert widget.widgetType == widget_type
assert isinstance(widget._widgets[0], class_for_type[widget_type])
index = widget_type if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else widget_type.value
assert isinstance(widget._widgets[0], class_for_type[index])


@pytest.mark.parametrize("orientation", [Qt.Horizontal, Qt.Vertical])
Expand Down Expand Up @@ -82,7 +87,8 @@ def test_widget_orientation(qtbot, orientation):
w = item.widget()
qtbot.addWidget(w)
assert w is not None
assert isinstance(w, class_for_type[widget.widgetType])
index = widget.widgetType if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else widget.widgetType.value
assert isinstance(w, class_for_type[index])


@pytest.mark.parametrize(
Expand Down
30 changes: 29 additions & 1 deletion pydm/utilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import errno

from typing import List, Optional

from enum import IntEnum
from qtpy import QtCore, QtGui, QtWidgets

from . import colors, macro, shortcuts
Expand All @@ -39,6 +39,34 @@
logger = logging.getLogger(__name__)


# The qtpy abstraction layer decides which qt python wrapper to use by the QT_API.
# Atm for pydm we only intend to support PyQt5 (legacy) and PySide6.
# ACTIVE_QT_WRAPPER is implemented basically to have easier access to the QT_API env variable,
# and we need to know the current wrapper being used to support both pyqt5 and pyside6.
class QtWrapperTypes(IntEnum):
UNSUPPORTED = 0
PYSIDE6 = 1
PYQT5 = 2


ACTIVE_QT_WRAPPER = QtWrapperTypes.UNSUPPORTED

# QT_API should be set according to the qtpy docs: https://github.com/spyder-ide/qtpy?tab=readme-ov-file#requirements
qt_api = os.getenv("QT_API", "").lower()
if qt_api == "pyside6":
ACTIVE_QT_WRAPPER = QtWrapperTypes.PYSIDE6
elif qt_api == "pyqt5":
ACTIVE_QT_WRAPPER = QtWrapperTypes.PYQT5

if ACTIVE_QT_WRAPPER == QtWrapperTypes.UNSUPPORTED:
error_message = (
"The QT_API variable is not set to a supported Qt Python wrapper "
"(PySide6 or PyQt5). Please set QT_API to 'pyside6' or 'pyqt5'."
)
logger.error(error_message)
raise RuntimeError(error_message)


def is_ssh_session():
"""
Whether or not this is a SSH session.
Expand Down
61 changes: 53 additions & 8 deletions pydm/widgets/datetime.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import logging
from qtpy import QtWidgets, QtCore
from enum import Enum
from PyQt5.QtCore import Q_ENUM

from .base import PyDMWritableWidget, PyDMWidget
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

# if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# from PyQt5.QtCore import Q_ENUM

logger = logging.getLogger(__name__)


class TimeBase(object):
# Inheriting from 'Enum' class is requried for enums in pyside6
class TimeBase(Enum):
Milliseconds = 0
Seconds = 1


class PyDMDateTimeEdit(QtWidgets.QDateTimeEdit, PyDMWritableWidget, TimeBase):
QtCore.Q_ENUMS(TimeBase)
if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# But inheriting from object class is requried for enums in pyqt5, since we need widgets to
# inherit from the enum class so the enums work in QtDesigner.
# This definition of the enum will override the previous definition.
# Doing this is pretty messy, but it seems like only way to support pyqt5 and pyside6 enums at same time,
# can be removed latter if we eventually drop pyqt5 support.
class TimeBase(object): # noqa F811
Milliseconds = 0
Seconds = 1


class PyDMDateTimeEditBase(QtWidgets.QDateTimeEdit, PyDMWritableWidget):
returnPressed = QtCore.Signal()
"""
A QDateTimeEdit with support for setting the text via a PyDM Channel, or
Expand Down Expand Up @@ -72,7 +89,7 @@ def blockPastDate(self, block):
self._block_past_date = block

def keyPressEvent(self, key_event):
ret = super(PyDMDateTimeEdit, self).keyPressEvent(key_event)
ret = super(PyDMDateTimeEditBase, self).keyPressEvent(key_event)
if key_event.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.returnPressed.emit()
return ret
Expand All @@ -94,7 +111,7 @@ def send_value(self):
self.send_value_signal.emit(new_value)

def value_changed(self, new_val):
super(PyDMDateTimeEdit, self).value_changed(new_val)
super(PyDMDateTimeEditBase, self).value_changed(new_val)

if self.timeBase == TimeBase.Seconds:
new_val *= 1000
Expand All @@ -107,8 +124,19 @@ def value_changed(self, new_val):
self.setDateTime(val)


class PyDMDateTimeLabel(QtWidgets.QLabel, PyDMWidget, TimeBase):
QtCore.Q_ENUMS(TimeBase)
# works with pyside6
class PyDMDateTimeEdit(PyDMDateTimeEditBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Overrides the previous class defintion
class PyDMDateTimeEdit(PyDMDateTimeEditBase, TimeBase): # noqa F811
pass


class PyDMDateTimeLabelBase(QtWidgets.QLabel, PyDMWidget):
Q_ENUM(TimeBase)
"""
A QLabel with support for setting the text via a PyDM Channel, or
through the PyDM Rules system.
Expand Down Expand Up @@ -166,7 +194,7 @@ def relative(self, checked):
self._relative = checked

def value_changed(self, new_val):
super(PyDMDateTimeLabel, self).value_changed(new_val)
super(PyDMDateTimeLabelBase, self).value_changed(new_val)

if self.timeBase == TimeBase.Seconds:
new_val *= 1000
Expand All @@ -177,3 +205,20 @@ def value_changed(self, new_val):
else:
val.setMSecsSinceEpoch(new_val)
self.setText(val.toString(self.textFormat))


# We define a '...Base' of the class so here we can 'conditionally define' the class as either inheriting
# from an enum or not, in order to support enums in QtDesigner for both pyqt5 and pyside6.
# We basically need to support the two ways of defining enums as explained here: https://stackoverflow.com/a/46919848.
class PyDMDateTimeLabel(PyDMDateTimeLabelBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Having the widget inherit from the enum class is requried in pyqt5 for the enums to load in QtDesigner.
# This definition of the enum will override the previous definition.
class PyDMDateTimeLabel(PyDMDateTimeLabelBase, TimeBase): # noqa F811
Q_ENUM(TimeBase)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
16 changes: 15 additions & 1 deletion pydm/widgets/display_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

import logging
import warnings
from enum import Enum
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes

logger = logging.getLogger(__name__)


class DisplayFormat(object):
# works with pyside6
class DisplayFormat(Enum):
"""Display format for showing data in a PyDM widget."""

#: The default display format.
Expand All @@ -25,6 +28,17 @@ class DisplayFormat(object):
Binary = 5


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class DisplayFormat(object): # noqa F811
Default = 0
String = 1
Decimal = 2
Exponential = 3
Hex = 4
Binary = 5


def parse_value_for_display(
value: Any,
precision: int,
Expand Down
44 changes: 35 additions & 9 deletions pydm/widgets/enum_button.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import logging
from enum import Enum

from qtpy.QtCore import Qt, QSize, Property, Slot, Q_ENUMS, QMargins
from qtpy.QtCore import Qt, QSize, Property, Slot, QMargins
from PyQt5.QtCore import Q_ENUM
from qtpy.QtGui import QPainter
from qtpy.QtWidgets import QWidget, QButtonGroup, QGridLayout, QPushButton, QRadioButton, QStyleOption, QStyle

from .base import PyDMWritableWidget
from .. import data_plugins
from ..utilities import ACTIVE_QT_WRAPPER, QtWrapperTypes


class WidgetType(object):
# works with pyside6
class WidgetType(Enum):
PushButton = 0
RadioButton = 1


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:

class WidgetType(object): # noqa F811
PushButton = 0
RadioButton = 1


class_for_type = [QPushButton, QRadioButton]

logger = logging.getLogger(__name__)


class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType):
class PyDMEnumButtonBase(QWidget, PyDMWritableWidget):
"""
A QWidget that renders buttons for every option of Enum Items.
For now, two types of buttons can be rendered:
Expand All @@ -38,9 +49,6 @@ class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType):
Emitted when the user changes the value.
"""

Q_ENUMS(WidgetType)
WidgetType = WidgetType

def __init__(self, parent=None, init_channel=None):
QWidget.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
Expand Down Expand Up @@ -399,7 +407,10 @@ def generate_widgets(items):
w.deleteLater()

for idx, entry in enumerate(items):
w = class_for_type[self._widget_type](parent=self)
# Support both pyqt enums (inherit from 'object') and pyside6 enums (inherit from python 'Enum' and
# therefore require '.value')
index = self._widget_type if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5 else self._widget_type.value
w = class_for_type[index](parent=self)
w.setCheckable(self.checkable)
w.setText(entry)
w.setVisible(False)
Expand Down Expand Up @@ -479,7 +490,7 @@ def value_changed(self, new_val):
The new value from the channel.
"""
if new_val is not None and new_val != self.value:
super(PyDMEnumButton, self).value_changed(new_val)
super(PyDMEnumButtonBase, self).value_changed(new_val)
btn = self._btn_group.button(new_val)
if btn:
btn.setChecked(True)
Expand All @@ -496,7 +507,7 @@ def enum_strings_changed(self, new_enum_strings):
The new list of values
"""
if new_enum_strings is not None and new_enum_strings != self.enum_strings:
super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings)
super(PyDMEnumButtonBase, self).enum_strings_changed(new_enum_strings)
self._has_enums = True
self.check_enable_state()
self.rebuild_widgets()
Expand All @@ -520,3 +531,18 @@ def paintEvent(self, _):
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
painter.setRenderHint(QPainter.Antialiasing)


# works with pyside6
class PyDMEnumButton(PyDMEnumButtonBase):
pass


if ACTIVE_QT_WRAPPER == QtWrapperTypes.PYQT5:
# Overrides the previous class defintion
class PyDMEnumButton(PyDMEnumButtonBase, WidgetType): # noqa F811
WidgetType = WidgetType
Q_ENUM(WidgetType)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Loading
Loading