Commit 142d1a08 authored by Philipp Middendorf's avatar Philipp Middendorf
Browse files

Merge branch 'widget' into 'master'

deviceMethodWidget.py: implement MethodEnabledByAttributeWidget

See merge request !33
parents 46260b05 3d567b89
Pipeline #28300 passed with stages
in 12 minutes and 41 seconds
......@@ -45,17 +45,23 @@ mypy:
- python -m pip install mypy
- PYTHONPATH=".:$PYTHONPATH" mypy --conf mypy.ini kamzik3
mypy-windows:
image: python:3.9-windowsservercore
tags:
- docker-windows
stage: lint
allow_failure: true
script:
- python -m pip install --upgrade pip
- python -m pip install -r requirements.txt
- python -m pip install mypy
- PYTHONPATH=".:$PYTHONPATH" mypy --conf mypy.ini kamzik3
# Below is commented out because the Windows runner is not yet available
# see rt #1140128 (Philipp also cc'ed in the ticket)
# answer from Elena (gitlab.service@desy.de) from 4 May 2022: "The docker-windows runner
# was once registered in GitLab, but it has never worked so far, we are still on it. We
# will assign your project as soon as the runner is available."
#mypy-windows:
# image: python:3.9-windowsservercore
# tags:
# - docker-windows
# stage: lint
# allow_failure: true
# script:
# - python -m pip install --upgrade pip
# - python -m pip install -r requirements.txt
# - python -m pip install mypy
# - PYTHONPATH=".:$PYTHONPATH" mypy --conf mypy.ini kamzik3
unit-tests:
image: python:3.9
......@@ -66,15 +72,6 @@ unit-tests:
- python -m pip install -r requirements.txt
- PYTHONPATH=".:$PYTHONPATH" pytest
unit-tests-37:
image: python:3.7
stage: test
script:
- python -m pip install --upgrade pip
- python -m pip install pytest pytest-cov pytest-lazy-fixture pytest-mock
- python -m pip install -r requirements.txt
- PYTHONPATH=".:$PYTHONPATH" pytest
unit-tests-38:
image: python:3.8
stage: test
......
......@@ -21,7 +21,7 @@ The documentation is available at: https://cfel-sc-public.pages.desy.de/kamzik3/
Requirements
============
* Python: 3.7 and higher
* Python: 3.8 or 3.9
**Python Modules: Backend**
......
......@@ -340,11 +340,13 @@ class DeviceSession(Device):
"""
raise DeviceError("Device does not accept any commands.")
def get_device(self, device_id: str, search_master: bool = True):
def get_device(self, device_id: str, search_master: bool = True) -> Device:
"""
Get Device object from the session.
If master_server key is defined in config, Session will try to obtain Device from it.
If Device is not found, raise DeviceUnknownError exception.
If master_server key is defined in config, the Session will try to obtain the
Device from it. If the Device is not found, raise DeviceUnknownError exception.
:param str device_id: Device ID
:param bool search_master: Ask master server for Device
:raises DeviceUnknownError
......
from typing import Optional
import numpy as np
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWidgets import QLabel, QWidget, QHBoxLayout
from kamzik3 import GuiTypeError
from kamzik3 import units
from kamzik3.constants import *
from kamzik3.gui.attributeWidgets.attributeBoolWidget import AttributeBoolWidget
......@@ -27,6 +28,7 @@ class AttributeDisplayWidget(QWidget):
self.attribute = attribute
self.config = config
self.title = title
self.name: Optional[str] = None
QWidget.__init__(self, parent=parent)
self.setupUi()
......
from PyQt5.QtCore import pyqtSlot, Qt
from typing import Callable, List, Optional, Tuple, Union
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
from PyQt5.QtWidgets import (
QLabel,
QHBoxLayout,
......@@ -11,26 +13,39 @@ from kamzik3.gui.attributeDeviceDisplayWidget import AttributeDeviceDisplayWidge
from kamzik3 import DeviceError, units
from kamzik3.constants import *
from kamzik3.devices.device import Device
from kamzik3.gui.attributeDisplayWidget import AttributeDisplayWidget
from kamzik3.gui.deviceWidget import DeviceWidget
from kamzik3.gui.general.stackableWidget import StackableWidget
from kamzik3.gui.templates.deviceMethodTemplate import Ui_Form
from kamzik3.snippets.snippetsWidgets import show_error_message
from kamzik3.snippets.snippetsWidgets import (
QSpinBox,
QDoubleSpinBox,
show_error_message,
)
class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
attribute = None
input_value = None
input_value: Optional[QWidget] = None
# Probably super should be called
# pylint: disable=super-init-not-called
def __init__(self, device, method, model_image=None, config=None, parent=None):
def __init__(
self,
device: Union[Device, str],
method: Callable,
model_image=None,
config=None,
parent=None,
):
self.method = method
self.value_controls = ()
self.value_controls: Tuple[QWidget, ...] = ()
DeviceWidget.__init__(
self, device, model_image=model_image, config=config, parent=parent
)
self.set_status_label(self.device.get_value(ATTR_STATUS))
if self.device is not None:
self.set_status_label(self.device.get_value(ATTR_STATUS))
self.label_device_name.setText(self.device_id)
@pyqtSlot()
......@@ -51,7 +66,9 @@ class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
self.set_status_label(self.device.get_value(ATTR_STATUS))
self.value_controls = (self.input_value,)
def method_widget(self):
def method_widget(self) -> QWidget:
if self.device is None:
raise DeviceError("Cannot generate a method widget, the device is None")
device_methods = self.device.exposed_methods
method_widget = QWidget()
......@@ -77,14 +94,16 @@ class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
for (method_name, method_attributes) in device_methods:
if method_name == self.method:
method_label = QLabel(method_name + " (")
method_label.setAlignment(Qt.AlignCenter | Qt.AlignRight)
method_label.setAlignment(
Qt.AlignCenter | Qt.AlignRight # type: ignore
)
method_label.setStyleSheet("QLabel {font-weight:bold}")
layout.addWidget(method_label)
method_execute_button = QPushButton("< Call >")
method_execute_button.setSizePolicy(
QSizePolicy.Fixed, QSizePolicy.Fixed
)
argument_inputs = []
argument_inputs: List[Union[AttributeDisplayWidget, QLineEdit]] = []
if method_attributes:
inputs_widget = QWidget()
inputs_layout = QHBoxLayout()
......@@ -97,7 +116,7 @@ class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
)
input_value = QLineEdit()
input_value.setPlaceholderText(attribute_type)
input_value.name = attribute_title
input_value.name = attribute_title # type: ignore
inputs_layout.addWidget(input_value)
argument_inputs.append(input_value)
else:
......@@ -105,9 +124,12 @@ class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
attribute_title, device_attribute
)
attribute_widget.name = attribute_title
inputs_layout.addWidget(attribute_widget.label_widget)
inputs_layout.addWidget(attribute_widget.input_widget)
inputs_layout.addWidget(attribute_widget.unit_widget)
if attribute_widget.label_widget is not None:
inputs_layout.addWidget(attribute_widget.label_widget)
if attribute_widget.input_widget is not None:
inputs_layout.addWidget(attribute_widget.input_widget)
if attribute_widget.unit_widget is not None:
inputs_layout.addWidget(attribute_widget.unit_widget)
argument_inputs.append(attribute_widget)
layout.addWidget(inputs_widget)
else:
......@@ -157,7 +179,8 @@ class DeviceMethodWidget(Ui_Form, DeviceWidget, StackableWidget):
self.device.stop()
def close(self):
self.input_value.close()
if self.input_value is not None:
self.input_value.close()
self.input_value = None
super().close()
......@@ -187,6 +210,105 @@ class DeviceMethodEnabledWidget(DeviceMethodWidget):
self.set_status_label(value)
class MethodEnabledByAttributeWidget(DeviceMethodEnabledWidget):
"""
The input fields of this method widget will be disabled when the attribute is True,
enabled when it is False.
This widget is used for the `start_run` method of the TapeDrive Runner, and the
attribute used for enabling/disabling is the external trigger (chopper).
"""
sig_control_changed = pyqtSignal("PyQt_PyObject") # Python object type unknown
def __init__(
self,
device: Union[Device, str],
method: Callable,
external_device_attribute: Tuple[Device, str],
model_image=None,
config=None,
parent=None,
) -> None:
self.external_device_attribute = external_device_attribute
super().__init__(
device=device,
method=method,
model_image=model_image,
config=config,
parent=parent,
)
def control_changed(self, value: bool) -> None:
"""
Slot executed when a signal is emitted from `sig_control_changed`.
"""
editable_widgets = self._find_editable_widgets()
if self.input_value is not None:
for index in editable_widgets:
if value:
self.input_value.children()[index].setEnabled(False) # type: ignore
else:
self.input_value.children()[index].setEnabled(True) # type: ignore
def _find_editable_widgets(self) -> List[int]:
"""Find the indices of the input fields that should be enabled/disabled"""
editable_widgets: List[int] = []
if self.input_value is not None:
for idx, child in enumerate(self.input_value.children()):
if type(child) == QWidget: # pylint: disable=unidiomatic-typecheck
# Here the exact type is needed, or it will pick up all widgets
# inherited from it
for grandchild in child.children():
if isinstance(grandchild, (QSpinBox, QDoubleSpinBox)):
editable_widgets.append(idx)
break
return editable_widgets
def init_signals(self) -> None:
"""
Connect the signals to the slots.
This method is called in DeviceWidget.__init__
"""
super().init_signals()
self.sig_control_changed.connect(self.control_changed)
@pyqtSlot()
def slot_handle_configuration(self) -> None:
DeviceWidget.slot_handle_configuration(self)
self.input_not_ready_placeholder.setParent(None)
layout = self.widget_holder.layout()
self.input_value = self.method_widget()
layout.insertWidget(2, self.input_value)
# find the index of the widgets to be enabled/disabled
self._find_editable_widgets()
# emit a signal each time the external trigger value is changed
# callback(attribute_object[key_filter]) is called
if self.device is not None:
self.device.attach_attribute_callback(
attribute=ATTR_EXTERNAL_TRIGGER,
callback=self.sig_control_changed.emit,
key_filter=VALUE,
)
# define the status of the input field
# using the current value of the attribute
self.control_changed(self.device.get_value(ATTR_EXTERNAL_TRIGGER))
# create the checkbox for the attribute
control_widget = AttributeDeviceDisplayWidget(
device=self.external_device_attribute[0],
attribute=self.external_device_attribute[1],
)
self.widget_holder.layout().insertWidget(3, control_widget)
self.set_status_label(self.device.get_value(ATTR_STATUS))
self.value_controls = (self.input_value,)
class DeviceSimpleMethodsListWidget(DeviceMethodWidget):
# Probably should be called
# pylint: disable=super-init-not-called
......@@ -212,13 +334,13 @@ class DeviceSimpleMethodsListWidget(DeviceMethodWidget):
layout = self.widget_holder.layout()
layout.insertStretch(2)
for required_method in reversed(self.methods_list):
input_value = self.method_widget(required_method)
input_value = self.required_method_widget(required_method)
layout.insertWidget(2, input_value)
self.set_status_label(self.device.get_value(ATTR_STATUS))
self.value_controls.append(input_value)
self.input_values.append(input_value)
def method_widget(self, required_method_name):
def required_method_widget(self, required_method_name):
device_methods = self.device.exposed_methods
method_widget = QWidget()
......@@ -261,7 +383,7 @@ class DeviceSimpleMethodsListWidget(DeviceMethodWidget):
for input in self.input_values:
input.close()
self.input_values = None
super(DeviceMethodWidget, self).close()
super().close()
class DeviceSimpleAttributeMethodsListWidget(DeviceSimpleMethodsListWidget):
......@@ -287,7 +409,7 @@ class DeviceSimpleAttributeMethodsListWidget(DeviceSimpleMethodsListWidget):
layout = self.widget_holder.layout()
layout.insertStretch(2)
for required_method in reversed(self.methods_list):
input_value = self.method_widget(required_method)
input_value = self.required_method_widget(required_method)
layout.insertWidget(2, input_value)
self.set_status_label(self.device.get_value(ATTR_STATUS))
self.value_controls.append(input_value)
......
import logging
from typing import Union
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget
import kamzik3
from kamzik3 import DeviceError
from kamzik3.constants import *
from kamzik3.devices.device import Device
from kamzik3.devices.observer import Observer
......@@ -34,7 +36,9 @@ class DeviceWidget(Observer, QWidget, YamlSerializable):
config = None
configured = False
def __init__(self, device, model_image=None, config=None, parent=None):
def __init__(
self, device: Union[Device, str], model_image=None, config=None, parent=None
):
QWidget.__init__(self)
Observer.__init__(parent)
if isinstance(device, Device):
......@@ -42,14 +46,17 @@ class DeviceWidget(Observer, QWidget, YamlSerializable):
self.device = device
else:
self.device_id = device
self.device = kamzik3.session.get_device(device)
if kamzik3.session is not None:
self.device = kamzik3.session.get_device(device)
else:
raise DeviceError("kamzik3 session is None")
if model_image is not None:
self.model_image = QIcon(model_image)
self.config = config
if self.config is None:
self.config = {}
self.logger = logging.getLogger("Gui.Device.{}".format(self.device_id))
self.setupUi(self)
self.setupUi(self) # type: ignore
self.init_signals()
self.attach_to_subject(self.device)
......
aiohttp==3.8.1
aiosignal==1.2.0
alabaster==0.7.12
astroid==2.9.3
async-timeout==4.0.2
atomicwrites==1.4.0
attrs==21.4.0
Babel==2.9.1
bidict==0.21.4
black==22.3.0
bleach==4.1.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12
click==8.0.3
colorama==0.4.4
coverage==6.3.2
cycler==0.11.0
docutils==0.17.1
fonttools==4.29.1
frozenlist==1.3.0
h5py==3.6.0
idna==3.3
imagesize==1.3.0
importlib-metadata==4.11.2
iniconfig==1.1.1
isort==5.10.1
Jinja2==3.0.3
keyring==23.5.0
kiwisolver==1.3.2
lazy-object-proxy==1.7.1
MarkupSafe==2.1.0
matplotlib==3.5.1
mccabe==0.6.1
multidict==6.0.2
mypy==0.931
mypy==0.950
mypy-extensions==0.4.3
natsort==8.0.0
numexpr==2.7.3
......@@ -30,21 +42,24 @@ opencv-python==4.5.5.62
oyaml==1.0
packaging==21.2
pandas==1.3.4
pandas-stubs==1.2.0.47
pandas-stubs==1.2.0.58
pathspec==0.9.0
Pillow==8.4.0
Pint==0.18
Pint==0.19.2
pkginfo==1.8.2
platformdirs==2.5.0
pluggy==1.0.0
psutil==5.8.0
py==1.11.0
pycparser==2.21
PyDAQmx==1.4.6
Pygments==2.11.2
pylint==2.12.2
pyparsing==2.4.7
PyQt5==5.15.6
PyQt5-Qt5==5.15.2
PyQt5-sip==12.9.0
PyQt5-stubs==5.15.2.0
PyQt5-stubs==5.15.6.0
pyqtgraph==0.12.3
pyserial==3.5
pytest==7.0.0
......@@ -52,17 +67,35 @@ pytest-lazy-fixture==0.6.3
pytest-stub==1.1.0
python-dateutil==2.8.2
pytz==2021.3
pywin32-ctypes==0.2.0
PyYAML==6.0
pyzmq==22.3.0
readme-renderer==33.0
reportlab==3.6.2
requests==2.27.1
requests-toolbelt==0.9.1
rfc3986==2.0.0
six==1.16.0
snowballstemmer==2.2.0
Sphinx==4.4.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tables==3.6.1
toml==0.10.2
tomli==2.0.1
types-psutil==5.8.20
types-PyYAML==6.0.4
types-requests==2.27.9
types-urllib3==1.26.9
typing_extensions==4.0.1
tqdm==4.63.0
twine==3.8.0
types-psutil==5.8.22
types-PyYAML==6.0.7
types-requests==2.27.25
types-urllib3==1.26.14
typing_extensions==4.2.0
urllib3==1.26.8
webencodings==0.5.1
wrapt==1.13.3
yarl==1.7.2
zipp==3.7.0
......@@ -15,8 +15,10 @@ setuptools.setup(
packages=setuptools.find_packages(),
package_data={"kamzik3": ["*.yml"], "kamzik3.example": ["*.yml", "*.att"]},
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
"Topic :: Software Development :: Libraries :: Python Modules",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
......@@ -52,5 +54,5 @@ setuptools.setup(
"pytest-lazy-fixture": [],
"pytest-mock": [],
},
python_requires=">=3.7",
python_requires=">=3.8, <3.10",
)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment