diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 0000000000..c2f7e71fcf
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,14 @@
+name: pre-commit
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v3
+ - uses: pre-commit/action@v3.0.0
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index a8edfda9f9..d55d93aff6 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -1,4 +1,4 @@
-# This workflow will install pydm dependencies, lint with flake8, and run the test suite, for all combinations
+# This workflow will install pydm dependencies and run the test suite for all combinations
# of operating systems and version numbers specified in the matrix
name: Build Status
@@ -20,8 +20,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: [3.7, 3.8, 3.9]
- pyqt-version: [5.12.3, 5.15.7]
+ python-version: ['3.8', '3.9', '3.10']
+ pyqt-version: [5.12.3, 5.15.9]
env:
DISPLAY: ':99.0'
QT_MAC_WANTS_LAYER: 1 # PyQT gui tests involving qtbot interaction on macOS will fail without this
@@ -38,30 +38,22 @@ jobs:
- name: Install python packages
shell: bash -el {0}
run: |
- # Note the pinning of zipp here is because versions 3.16.0+ do not support python 3.7. Specifying zipp can be entirely removed (will be pulled in by flake8) if pydm drops support for 3.7
if [ "$RUNNER_OS" == "Windows" ]; then
- mamba install flake8 zipp=3.15.0 pyqt=${{ matrix.pyqt-version }}
+ mamba install pyqt=${{ matrix.pyqt-version }}
mamba install --file requirements.txt --file windows-dev-requirements.txt
else
- mamba install flake8 zipp=3.15.0 pyqt=${{ matrix.pyqt-version }} $(cat requirements.txt dev-requirements.txt)
+ mamba install pyqt=${{ matrix.pyqt-version }} $(cat requirements.txt dev-requirements.txt)
fi
- name: Install packages for testing a pyqt app on linux
shell: bash -el {0}
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
- sudo apt install xvfb herbstluftwm libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
+ sudo apt install xvfb herbstluftwm libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
sudo /sbin/start-stop-daemon --start --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset
sleep 3
sudo /sbin/start-stop-daemon --start --pidfile /tmp/custom_herbstluftwm_99.pid --make-pidfile --background --exec /usr/bin/herbstluftwm
sleep 1
fi
- - name: Lint with flake8
- shell: bash -el {0}
- run: |
- # stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings.
- flake8 . --count --exit-zero --max-line-length=120 --statistics
- name: Test with pytest
shell: bash -el {0}
run: |
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000..3e7e4a4f58
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,18 @@
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: end-of-file-fixer
+ files: '\.(py|txt)$' # Only run on .py and .txt files
+ - id: trailing-whitespace
+ files: '\.(py|txt)$' # Only run on .py and .txt files
+- repo: https://github.com/psf/black
+ rev: 23.7.0
+ hooks:
+ - id: black
+ args: [--line-length, '120']
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.287
+ hooks:
+ - id: ruff
+ args: [--line-length, '120', --fix, --exit-non-zero-on-fix]
diff --git a/README.md b/README.md
index 96b4dce10b..1a71f6a163 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,30 @@
[](https://github.com/slaclab/pydm/actions/workflows/run-tests.yml)
+
+
+
+
+
PyDM: Python Display Manager
+
+
+
+ PyDM is a PyQt-based framework for building user interfaces for control systems.
+ The goal is to provide a no-code, drag-and-drop system to make simple screens,
+ as well as a straightforward Python framework to build complex applications.
+
+
+
-
PyDM: Python Display Manager
-
-
- PyDM is a PyQt-based framework for building user interfaces for control systems.
- The goal is to provide a no-code, drag-and-drop system to make simple screens,
- as well as a straightforward Python framework to build complex applications.
-
-
- « Explore PyDM docs and tutorials »
-
-
- Report bug
- ·
- Request feature
- ·
- How to Contribute
- ·
- Support
-
+ « Explore PyDM docs and tutorials »
+
+
+ Report bug
+ ·
+ Request feature
+ ·
+ How to Contribute
+ ·
+ Support
@@ -71,7 +76,7 @@ python scripts/pydm examples/home.ui
```
# Building the Documentation Locally
-In order to build the documentation you will need to instll some dependencies
+In order to build the documentation you will need to install some dependencies
that are not part of the runtime dependencies of PyDM.
Assuming that you have cloned this repository do:
@@ -111,27 +116,5 @@ pip install .[all]
When using Anaconda to install PyDM at a Linux Environment it will automatically
define the PYQTDESIGNERPATH environment variable pointing to /etc/pydm which
will have a file named designer_plugin.py which will make all the PyDM widgets
-available to the Qt Designer.
-
-### Most Recent Development Build
-
-[](https://conda.anaconda.org/pydm-dev)
-[](https://anaconda.org/pydm-dev/pydm)
-[](https://anaconda.org/pydm-dev/pydm)
-[](https://anaconda.org/pydm-dev/pydm)
-
-
-```sh
-conda install -c pydm-dev -c conda-forge pydm
-```
-### Most Recent Tagged Build
-
-[](https://conda.anaconda.org/pydm-tag)
-[](https://anaconda.org/pydm-tag/pydm)
-[](https://anaconda.org/pydm-tag/pydm)
-[](https://anaconda.org/pydm-tag/pydm)
-
-
-```sh
-conda install -c pydm-tag -c conda-forge pydm
-```
+available to the Qt Designer. For more information please see
+our installation guide.
\ No newline at end of file
diff --git a/dev-requirements.txt b/dev-requirements.txt
index cf67e910fa..8acd707df3 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -5,3 +5,4 @@ pytest-cov
pytest-timeout
p4p
pyca
+pre-commit
diff --git a/docs/source/_static/tutorials/action/all_motors1.png b/docs/source/_static/tutorials/action/all_motors1.png
new file mode 100644
index 0000000000..a890f74c1d
Binary files /dev/null and b/docs/source/_static/tutorials/action/all_motors1.png differ
diff --git a/docs/source/_static/tutorials/action/application.png b/docs/source/_static/tutorials/action/application.png
new file mode 100644
index 0000000000..706c3994aa
Binary files /dev/null and b/docs/source/_static/tutorials/action/application.png differ
diff --git a/docs/source/_static/tutorials/action/components.png b/docs/source/_static/tutorials/action/components.png
new file mode 100644
index 0000000000..fe7eb400ba
Binary files /dev/null and b/docs/source/_static/tutorials/action/components.png differ
diff --git a/docs/source/_static/tutorials/action/designer.png b/docs/source/_static/tutorials/action/designer.png
new file mode 100644
index 0000000000..fedef10eae
Binary files /dev/null and b/docs/source/_static/tutorials/action/designer.png differ
diff --git a/docs/source/_static/tutorials/action/expert/expert.png b/docs/source/_static/tutorials/action/expert/expert.png
new file mode 100644
index 0000000000..42f90c4ca4
Binary files /dev/null and b/docs/source/_static/tutorials/action/expert/expert.png differ
diff --git a/docs/source/_static/tutorials/action/expert/expert_all_widgets_ok.png b/docs/source/_static/tutorials/action/expert/expert_all_widgets_ok.png
new file mode 100644
index 0000000000..11ed5a53cf
Binary files /dev/null and b/docs/source/_static/tutorials/action/expert/expert_all_widgets_ok.png differ
diff --git a/docs/source/_static/tutorials/action/expert/expert_layout.gif b/docs/source/_static/tutorials/action/expert/expert_layout.gif
new file mode 100644
index 0000000000..ea7d891dbc
Binary files /dev/null and b/docs/source/_static/tutorials/action/expert/expert_layout.gif differ
diff --git a/docs/source/_static/tutorials/action/expert/widgets.png b/docs/source/_static/tutorials/action/expert/widgets.png
new file mode 100644
index 0000000000..9133a10263
Binary files /dev/null and b/docs/source/_static/tutorials/action/expert/widgets.png differ
diff --git a/docs/source/_static/tutorials/action/inline/inline.png b/docs/source/_static/tutorials/action/inline/inline.png
new file mode 100644
index 0000000000..482851fe68
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline.png differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_2.gif b/docs/source/_static/tutorials/action/inline/inline_3_2.gif
new file mode 100644
index 0000000000..6272c495a6
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_2.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_3.gif b/docs/source/_static/tutorials/action/inline/inline_3_3.gif
new file mode 100644
index 0000000000..4746a199f6
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_3.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_4.gif b/docs/source/_static/tutorials/action/inline/inline_3_4.gif
new file mode 100644
index 0000000000..8878ebfdd7
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_4.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_5.gif b/docs/source/_static/tutorials/action/inline/inline_3_5.gif
new file mode 100644
index 0000000000..0a4df9acde
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_5.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_6.gif b/docs/source/_static/tutorials/action/inline/inline_3_6.gif
new file mode 100644
index 0000000000..aa6964f6b2
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_6.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_8.gif b/docs/source/_static/tutorials/action/inline/inline_3_8.gif
new file mode 100644
index 0000000000..09046d8353
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_8.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_3_9.gif b/docs/source/_static/tutorials/action/inline/inline_3_9.gif
new file mode 100644
index 0000000000..60b94a9d64
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_3_9.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_all_widgets.png b/docs/source/_static/tutorials/action/inline/inline_all_widgets.png
new file mode 100644
index 0000000000..c0655912d2
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_all_widgets.png differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_all_widgets_ok.png b/docs/source/_static/tutorials/action/inline/inline_all_widgets_ok.png
new file mode 100644
index 0000000000..fad6e394f8
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_all_widgets_ok.png differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_desc.gif b/docs/source/_static/tutorials/action/inline/inline_desc.gif
new file mode 100644
index 0000000000..74eff54177
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_desc.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/inline_layout.gif b/docs/source/_static/tutorials/action/inline/inline_layout.gif
new file mode 100644
index 0000000000..ea7d891dbc
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/inline_layout.gif differ
diff --git a/docs/source/_static/tutorials/action/inline/widgets.png b/docs/source/_static/tutorials/action/inline/widgets.png
new file mode 100644
index 0000000000..33ad488d7c
Binary files /dev/null and b/docs/source/_static/tutorials/action/inline/widgets.png differ
diff --git a/docs/source/_static/tutorials/action/little_code/main.gif b/docs/source/_static/tutorials/action/little_code/main.gif
new file mode 100644
index 0000000000..a87523ee44
Binary files /dev/null and b/docs/source/_static/tutorials/action/little_code/main.gif differ
diff --git a/docs/source/_static/tutorials/action/main/main.png b/docs/source/_static/tutorials/action/main/main.png
new file mode 100644
index 0000000000..706c3994aa
Binary files /dev/null and b/docs/source/_static/tutorials/action/main/main.png differ
diff --git a/docs/source/_static/tutorials/action/main/main_all_widgets_ok.png b/docs/source/_static/tutorials/action/main/main_all_widgets_ok.png
new file mode 100644
index 0000000000..1839891a3c
Binary files /dev/null and b/docs/source/_static/tutorials/action/main/main_all_widgets_ok.png differ
diff --git a/docs/source/_static/tutorials/action/main/widgets.png b/docs/source/_static/tutorials/action/main/widgets.png
new file mode 100644
index 0000000000..a36023a445
Binary files /dev/null and b/docs/source/_static/tutorials/action/main/widgets.png differ
diff --git a/docs/source/_static/tutorials/action/new_widget.gif b/docs/source/_static/tutorials/action/new_widget.gif
new file mode 100644
index 0000000000..e0cf71bbc9
Binary files /dev/null and b/docs/source/_static/tutorials/action/new_widget.gif differ
diff --git a/docs/source/_static/tutorials/action/new_widget.png b/docs/source/_static/tutorials/action/new_widget.png
new file mode 100644
index 0000000000..6fadcf6b3d
Binary files /dev/null and b/docs/source/_static/tutorials/action/new_widget.png differ
diff --git a/docs/source/_static/tutorials/action/pydm_properties.png b/docs/source/_static/tutorials/action/pydm_properties.png
new file mode 100644
index 0000000000..8be669eb63
Binary files /dev/null and b/docs/source/_static/tutorials/action/pydm_properties.png differ
diff --git a/docs/source/_static/tutorials/action/python/all_motors.gif b/docs/source/_static/tutorials/action/python/all_motors.gif
new file mode 100644
index 0000000000..c2b7d97f9d
Binary files /dev/null and b/docs/source/_static/tutorials/action/python/all_motors.gif differ
diff --git a/docs/source/_static/tutorials/action/python/all_motors.png b/docs/source/_static/tutorials/action/python/all_motors.png
new file mode 100644
index 0000000000..aa952415bd
Binary files /dev/null and b/docs/source/_static/tutorials/action/python/all_motors.png differ
diff --git a/docs/source/_static/tutorials/code/all_motors.py b/docs/source/_static/tutorials/code/all_motors.py
new file mode 100644
index 0000000000..56ebc4a30b
--- /dev/null
+++ b/docs/source/_static/tutorials/code/all_motors.py
@@ -0,0 +1,146 @@
+import os
+import json
+from pydm import Display
+from qtpy.QtWidgets import (
+ QVBoxLayout,
+ QHBoxLayout,
+ QGroupBox,
+ QLabel,
+ QLineEdit,
+ QPushButton,
+ QScrollArea,
+ QFrame,
+ QApplication,
+ QWidget,
+)
+from qtpy import QtCore
+from pydm.widgets import PyDMEmbeddedDisplay
+
+
+class AllMotorsDisplay(Display):
+ def __init__(self, parent=None, args=[], macros=None):
+ super(AllMotorsDisplay, self).__init__(parent=parent, args=args, macros=None)
+ # Placeholder for data to filter
+ self.data = []
+ # Reference to the PyDMApplication
+ self.app = QApplication.instance()
+ # Assemble the Widgets
+ self.setup_ui()
+ # Load data from file
+ self.load_data()
+
+ def minimumSizeHint(self):
+ # This is the default recommended size
+ # for this screen
+ return QtCore.QSize(750, 120)
+
+ def ui_filepath(self):
+ # No UI file is being used
+ return None
+
+ def setup_ui(self):
+ # Create the main layout
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ # Create a Label to be the title
+ lbl_title = QLabel("Motors Diagnostic")
+ # Add some StyleSheet to it
+ lbl_title.setStyleSheet(
+ "\
+ QLabel {\
+ qproperty-alignment: AlignCenter;\
+ border: 1px solid #FF17365D;\
+ border-top-left-radius: 15px;\
+ border-top-right-radius: 15px;\
+ background-color: #FF17365D;\
+ padding: 5px 0px;\
+ color: rgb(255, 255, 255);\
+ max-height: 25px;\
+ font-size: 14px;\
+ }"
+ )
+
+ # Add the title label to the main layout
+ main_layout.addWidget(lbl_title)
+
+ # Create the Search Panel layout
+ search_layout = QHBoxLayout()
+
+ # Create a GroupBox with "Filtering" as Title
+ gb_search = QGroupBox(parent=self)
+ gb_search.setTitle("Filtering")
+ gb_search.setLayout(search_layout)
+
+ # Create a label, line edit and button for filtering
+ lbl_search = QLabel(text="Filter: ")
+ self.txt_filter = QLineEdit()
+ self.txt_filter.returnPressed.connect(self.do_search)
+ btn_search = QPushButton()
+ btn_search.setText("Search")
+ btn_search.clicked.connect(self.do_search)
+
+ # Add the created widgets to the layout
+ search_layout.addWidget(lbl_search)
+ search_layout.addWidget(self.txt_filter)
+ search_layout.addWidget(btn_search)
+
+ # Add the Groupbox to the main layout
+ main_layout.addWidget(gb_search)
+
+ # Create the Results Layout
+ self.results_layout = QVBoxLayout()
+ self.results_layout.setContentsMargins(0, 0, 0, 0)
+
+ # Create a Frame to host the results of search
+ self.frm_result = QFrame(parent=self)
+ self.frm_result.setLayout(self.results_layout)
+
+ # Create a ScrollArea so we can properly handle
+ # many entries
+ scroll_area = QScrollArea(parent=self)
+ scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+ scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ scroll_area.setWidgetResizable(True)
+
+ # Add the Frame to the scroll area
+ scroll_area.setWidget(self.frm_result)
+
+ # Add the scroll area to the main layout
+ main_layout.addWidget(scroll_area)
+
+ def load_data(self):
+ # Extract the directory of this file...
+ base_dir = os.path.dirname(os.path.realpath(__file__))
+ # Concatenate the directory with the file name...
+ data_file = os.path.join(base_dir, "motor_db.txt")
+ # Open the file so we can read the data...
+ with open(data_file, "r") as f:
+ # For each line in the file...
+ for entry in f.readlines():
+ # Append to the list of data...
+ self.data.append(entry[:-1])
+
+ def do_search(self):
+ # For each widget inside the results frame, lets destroy them
+ for widget in self.frm_result.findChildren(QWidget):
+ widget.setParent(None)
+ widget.deleteLater()
+
+ # Grab the filter text
+ filter_text = self.txt_filter.text()
+
+ # For every entry in the dataset...
+ for entry in self.data:
+ # Check if they match our filter
+ if filter_text.upper() not in entry.upper():
+ continue
+ # Create a PyDMEmbeddedDisplay for this entry
+ disp = PyDMEmbeddedDisplay(parent=self)
+ disp.macros = json.dumps({"MOTOR": entry})
+ disp.filename = "inline_motor.ui"
+ disp.setMinimumWidth(700)
+ disp.setMinimumHeight(40)
+ disp.setMaximumHeight(100)
+ # Add the Embedded Display to the Results Layout
+ self.results_layout.addWidget(disp)
diff --git a/docs/source/_static/tutorials/code/expert_motor.ui b/docs/source/_static/tutorials/code/expert_motor.ui
new file mode 100644
index 0000000000..b1ade292cd
--- /dev/null
+++ b/docs/source/_static/tutorials/code/expert_motor.ui
@@ -0,0 +1,289 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 447
+ 217
+
+
+
+ Engineer Screen: ${MOTOR}
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 0
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Configuring Motor: ${MOTOR}
+
+
+
+
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ 10
+
+
+ 10
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+
+
+ Description:
+
+
+
+
+
+
+
+
+
+
+
+
+ ca://${MOTOR}.DESC
+
+
+ PyDMLineEdit::String
+
+
+
+
+
+
+ Position:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ false
+
+
+ true
+
+
+ ca://${MOTOR}.VAL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+ Readback:
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ ca://${MOTOR}.RBV
+
+
+ PyDMLabel::Decimal
+
+
+
+
+
+
+ Velocity:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ false
+
+
+ true
+
+
+ ca://${MOTOR}.VELO
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+ Acceleration:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ false
+
+
+ true
+
+
+ ca://${MOTOR}.ACCL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+ pydm.widgets.label
+
+
+ PyDMLineEdit
+ QLineEdit
+ pydm.widgets.line_edit
+
+
+
+
+
diff --git a/docs/source/_static/tutorials/code/inline_motor.ui b/docs/source/_static/tutorials/code/inline_motor.ui
new file mode 100755
index 0000000000..42b320a44c
--- /dev/null
+++ b/docs/source/_static/tutorials/code/inline_motor.ui
@@ -0,0 +1,500 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 700
+ 32
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 32
+
+
+
+
+ 16777215
+ 38
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 5
+
+
+ 5
+
+
+ 10
+
+
+ 5
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ ca://${MOTOR}.RBV
+
+
+ PyDMLabel::Decimal
+
+
+
+
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw +10
+
+
+ ca://${MOTOR}
+
+
+ 10
+
+
+ true
+
+
+
+
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw -10
+
+
+ ca://${MOTOR}
+
+
+ -10
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ background-color: red;
+
+
+ Stop
+
+
+ ca://${MOTOR}.STOP
+
+
+ 1
+
+
+
+
+
+
+
+ 125
+ 24
+
+
+
+
+ 125
+ 24
+
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ Engineer...
+
+
+ true
+
+
+ expert_motor.ui
+
+
+ {"MOTOR":"${MOTOR}"}
+
+
+ true
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ ca://${MOTOR}
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+
+ 100
+ 0
+
+
+
+
+ Arial
+ 75
+ true
+
+
+
+
+
+
+
+
+
+ ca://${MOTOR}.DESC
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ Widget for graphical representation of bits from an integer number
+ with support for Channels and more from PyDM
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ false
+
+
+ false
+
+
+ ca://${MOTOR}.MOVN
+
+
+
+ 0
+ 255
+ 0
+
+
+
+
+ 100
+ 100
+ 100
+
+
+
+ Qt::Vertical
+
+
+ false
+
+
+ true
+
+
+ QTabWidget::East
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+ pydm.widgets.label
+
+
+ PyDMLineEdit
+ QLineEdit
+ pydm.widgets.line_edit
+
+
+ PyDMPushButton
+ QPushButton
+ pydm.widgets.pushbutton
+
+
+ PyDMByteIndicator
+ QWidget
+ pydm.widgets.byte
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/docs/source/_static/tutorials/code/main.py b/docs/source/_static/tutorials/code/main.py
new file mode 100644
index 0000000000..5188a3205d
--- /dev/null
+++ b/docs/source/_static/tutorials/code/main.py
@@ -0,0 +1,41 @@
+from os import path
+from pydm import Display
+from scipy.ndimage.measurements import maximum_position
+
+
+class BeamPositioning(Display):
+ def __init__(self, parent=None, args=None):
+ super(BeamPositioning, self).__init__(parent=parent, args=args)
+ # Attach our custom process_image method
+ self.ui.imageView.process_image = self.process_image
+ # Hook up to the newImageSignal so we can update
+ # our widgets after the new image is done
+ self.ui.imageView.newImageSignal.connect(self.show_blob)
+ # Store blob coordinate
+ self.blob = (0, 0)
+
+ def ui_filename(self):
+ # Point to our UI file
+ return "main.ui"
+
+ def ui_filepath(self):
+ # Return the full path to the UI file
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+ def show_blob(self, *args, **kwargs):
+ # If we have a blob, present the coordinates at label
+ if self.blob != (0, 0):
+ blob_txt = "Blob Found:"
+ blob_txt += " ({}, {})".format(self.blob[1], self.blob[0])
+ else:
+ # If no blob was found, present the "Not Found" message
+ blob_txt = "Blob Not Found"
+ # Update the label text
+ self.ui.lbl_blobs.setText(blob_txt)
+
+ def process_image(self, new_image):
+ # Consider the maximum as the Blob since we have only
+ # one blob.
+ self.blob = maximum_position(new_image)
+ # Send the original image data to the image widget
+ return new_image
diff --git a/docs/source/_static/tutorials/code/main.ui b/docs/source/_static/tutorials/code/main.ui
new file mode 100755
index 0000000000..cfa42eb9e9
--- /dev/null
+++ b/docs/source/_static/tutorials/code/main.ui
@@ -0,0 +1,321 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 724
+ 695
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ Beam Positioning
+
+
+
+
+
+ 0
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Beam Alignment
+
+
+
+
+
+
+
+ 600
+ 480
+
+
+
+
+
+
+
+ A PyQtGraph ImageView with support for Channels and more from PyDM.
+
+ If there is no :attr:`channelWidth` it is possible to define the width of
+ the image with the :attr:`width` property.
+
+ The :attr:`normalizeData` property defines if the colors of the images are
+ relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
+ the minimum and maximum values of the image.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ image_channel : str, optional
+ The channel to be used by the widget for the image data.
+ width_channel : str, optional
+ The channel to be used by the widget to receive the image width
+ information
+
+
+
+ 255.000000000000000
+
+
+ true
+
+
+ PyDMImageView::Clike
+
+
+ ca://13SIM1:image1:ArrayData
+
+
+ ca://13SIM1:image1:ArraySize0_RBV
+
+
+ 30
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Controls
+
+
+
+
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m1"}
+
+
+ inline_motor.ui
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m2"}
+
+
+ inline_motor.ui
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ View All Motors
+
+
+ all_motors.py
+
+
+ false
+
+
+
+
+
+
+
+ PyDMEmbeddedDisplay
+ QFrame
+ pydm.widgets.embedded_display
+
+
+ PyDMImageView
+ QWidget
+ pydm.widgets.image
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/docs/source/_static/tutorials/code/motor_db.txt b/docs/source/_static/tutorials/code/motor_db.txt
new file mode 100644
index 0000000000..8cc77371f2
--- /dev/null
+++ b/docs/source/_static/tutorials/code/motor_db.txt
@@ -0,0 +1,8 @@
+IOC:m1
+IOC:m2
+IOC:m3
+IOC:m4
+IOC:m5
+IOC:m6
+IOC:m7
+IOC:m8
diff --git a/docs/source/_static/tutorials/intro/architecture.png b/docs/source/_static/tutorials/intro/architecture.png
new file mode 100644
index 0000000000..550f2aa9e2
Binary files /dev/null and b/docs/source/_static/tutorials/intro/architecture.png differ
diff --git a/docs/source/_static/tutorials/intro/main_window.png b/docs/source/_static/tutorials/intro/main_window.png
new file mode 100644
index 0000000000..f28d0c7706
Binary files /dev/null and b/docs/source/_static/tutorials/intro/main_window.png differ
diff --git a/docs/source/_static/tutorials/new_vm.png b/docs/source/_static/tutorials/new_vm.png
new file mode 100644
index 0000000000..bb55978171
Binary files /dev/null and b/docs/source/_static/tutorials/new_vm.png differ
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 86b3860c6f..46eb3c4e22 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -14,53 +14,50 @@
import sys
import os
-import shlex
-import sphinx_rtd_theme
+import pydm
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),'../../')
-sys.path.insert(0,module_path)
-sys.path.insert(0,os.path.join(module_path,'pydm/widgets'))
-
-import pydm
+# needs_sphinx = '1.0'
+module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
+sys.path.insert(0, module_path)
+sys.path.insert(0, os.path.join(module_path, "pydm/widgets"))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx.ext.viewcode',
- 'sphinx.ext.githubpages',
- 'sphinx.ext.napoleon',
+ "sphinx.ext.autodoc",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.githubpages",
+ "sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
+source_suffix = ".rst"
# The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = u'PyDM'
-copyright = u'2016, mgibbs, hhslepicka, trendahl, zlentz'
-author = u'mgibbs, hhslepicka, trendahl, zlentz'
+project = "PyDM"
+copyright = "2023, hhslepicka, trendahl, zlentz, yektay, nstelter"
+author = "mgibbs, hhslepicka, trendahl, zlentz, yektay, nstelter"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -76,13 +73,13 @@
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
-#today = ''
+# today = ''
# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -90,27 +87,27 @@
# The reST default role (used for this markup: `text`) to use for all
# documents.
-#default_role = None
+# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
-#show_authors = False
+# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
@@ -120,156 +117,149 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
-#html_theme_options = {}
+# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
-#html_title = None
+# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-#html_logo = None
+# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-#html_favicon = None
+# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
# If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
# If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
# If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
-#html_search_language = 'en'
+# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
-#html_search_options = {'type': 'default'}
+# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
-#html_search_scorer = 'scorer.js'
+# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
-htmlhelp_basename = 'PyDMdoc'
+htmlhelp_basename = "PyDMdoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
+ # Latex figure (float) alignment
+ #'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'PyDM.tex', u'PyDM Documentation',
- u'mgibbs, hhslepicka, trendahl, zlentz', 'manual'),
+ (master_doc, "PyDM.tex", "PyDM Documentation", "mgibbs, hhslepicka, trendahl, zlentz", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
-#latex_logo = None
+# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
# If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
# If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
# Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
# If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [
- (master_doc, 'pydm', u'PyDM Documentation',
- [author], 1)
-]
+man_pages = [(master_doc, "pydm", "PyDM Documentation", [author], 1)]
# If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@@ -278,19 +268,17 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'PyDM', u'PyDM Documentation',
- author, 'PyDM', 'One line description of project.',
- 'Miscellaneous'),
+ (master_doc, "PyDM", "PyDM Documentation", author, "PyDM", "One line description of project.", "Miscellaneous"),
]
# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
# If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 67af6f283f..10a0b35d0f 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -23,7 +23,7 @@ as well as a straightforward python framework to build complex applications.
:maxdepth: 1
:caption: Tutorials
- Explore Tutorials
+ tutorials/index.rst
.. toctree::
:maxdepth: 2
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index e084fe8f00..847a272a64 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -1,3 +1,5 @@
+.. _Install:
+
=========================
Installation
=========================
diff --git a/docs/source/tutorials/action/designer.rst b/docs/source/tutorials/action/designer.rst
new file mode 100644
index 0000000000..dd0408fec1
--- /dev/null
+++ b/docs/source/tutorials/action/designer.rst
@@ -0,0 +1,17 @@
+Building Your First Display with Qt Designer
+============================================
+
+This section will guide you through the work necessary to create PyDM screens
+using Qt Designer.
+
+To make it more interesting, we will develop three pieces of the
+:ref:`components ` described in our
+:ref:`tutorial application `.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Components
+
+ designer_inline.rst
+ designer_expert.rst
+ designer_main.rst
diff --git a/docs/source/tutorials/action/designer_expert.rst b/docs/source/tutorials/action/designer_expert.rst
new file mode 100644
index 0000000000..d20e34eab5
--- /dev/null
+++ b/docs/source/tutorials/action/designer_expert.rst
@@ -0,0 +1,309 @@
+.. _Expert:
+
+Expert Motor Screen
+===================
+
+.. important::
+
+ * Make sure the PCASpy tutorial server is :ref:`running `
+
+For this screen we will present detailed information to the user for the motors.
+Also, to ensure that we can re-use this screen in other displays, it will be
+necessary to use :ref:`Macros` that will later be replaced by the proper information
+for each motor.
+
+The finished result will look like this:
+
+.. figure:: /_static/tutorials/action/expert/expert.png
+ :scale: 75 %
+ :align: center
+ :alt: Expert Motor Screen
+
+ Expert Motor Screen
+
+
+* **Step 1.**
+
+ Let's start by opening the :ref:`Qt Designer `
+ and creating a new ``Widget``.
+
+ .. figure:: /_static/tutorials/action/new_widget.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 2.**
+
+ With the new form available, let's add a ``Vertical Layout`` widget and make
+ it fill the whole form. Let's select ``Layout Vertically`` for the Form.
+
+ .. figure:: /_static/tutorials/action/inline/inline_layout.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 3.**
+
+ Now that we have a layout, let's take a look at the widgets we'll use on this
+ screen:
+
+ .. figure:: /_static/tutorials/action/expert/widgets.png
+ :scale: 70 %
+ :align: center
+
+
+ * **Step 3.1.**
+
+ The first ``Label`` will be the title of our screen:
+
+ #. Drag and drop a ``Label`` into the previously added ``Vertical Layout``.
+ #. Set the ``text`` property of this label to: ``Configuring Motor: ${MOTOR}``.
+
+ .. note::
+
+ :ref:`Macros` are not something exclusive to PyDM Widgets, they can be
+ used with any kind of widget (even the basic Qt widgets) and in any
+ property. In this case we are using it to add the motor ``PV`` name
+ to the title. However, due to limitations in Qt Designer, you cannot
+ specify a macro for a variable that is numeric-only inside Designer
+ itself. An (unfortunate) work-around is to edit the .ui file in a
+ text editor, and insert your macro variable into the XML that defines
+ the display.
+
+ #. In order to make the label look better as a title, add the following to
+ the ``stylesheet`` property:
+
+ .. code-block:: css
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+ }
+
+ * **Step 3.2.**
+
+ The second widget that we will add is a ``Frame``, which will be the container
+ of the fields in our form:
+
+ #. Drag and drop a ``Frame`` into the previously added ``Vertical Layout`` under
+ the ``Label`` that was added at **Step 3.1**.
+ #. Set the ``frameShape`` property to ``StyledPanel``.
+ #. Set the ``frameShadow`` property to ``Raised``.
+ #. In order to add some nice rounded corners to this frame, add the following
+ to the ``stylesheet`` property:
+
+ .. code-block:: css
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+ }
+
+ * **Step 3.3.**
+
+ Now to ensure the alignment and positioning of the form content, let's add a
+ ``Form Layout``:
+
+ #. Drag and drop a ``Form Layout`` into the previously added ``Frame``.
+ #. Right-click the ``Frame`` and select ``Layout > Layout Vertically``.
+
+ - This will make the ``Form Layout`` fill the whole space of the ``Frame``
+ and make our form behave better when resizing.
+
+ #. Set the ``frameShadow`` property to ``Raised``.
+ #. In order to add some nice rounded corners to this frame, add the following
+ to the ``stylesheet`` property:
+
+ .. code-block:: css
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+ }
+
+ * **Step 3.4.**
+
+ Now that we have our ``Form Layout`` ready, it is time to start adding the form
+ widgets. Let's start with the first pair of ``Label`` and ``PyDMLineEdit`` that
+ will be used to edit the **Description** of the Motor:
+
+ #. Drag and drop a ``Label`` into the the previously added ``Form Layout``.
+ #. Set the ``text`` property to ``Description:``.
+ #. Drag and drop a ``PyDMLineEdit`` into the ``Form Layout`` paying attention to
+ add it on the right side of the previously added ``Label``.
+
+ .. note::
+
+ The area that will match the ``Label`` will be highlighted with red
+ borders. When that happens you will know that the widget will be placed
+ at the expected place.
+
+ #. Set the ``channel`` property to ``ca://${MOTOR}.DESC``.
+ #. Set the ``displayFormat`` property to ``String``.
+
+ * **Step 3.5.**
+
+ Let's now add the second pair of ``Label`` and ``PyDMLineEdit`` that
+ will be used to edit the **Position** of the Motor:
+
+ #. Drag and drop a ``Label`` into the the previously added ``Form Layout`` right
+ under the previously added components.
+
+ .. note::
+
+ The area will be highlighted with blue line. When that happens you will
+ know that the widget will be placed at the expected place.
+
+ #. Set the ``text`` property to ``Position:``.
+ #. Drag and drop a ``PyDMLineEdit`` into the ``Form Layout`` paying attention to
+ add it on the side of the previously added ``Label``.
+ #. Set the ``channel`` property to ``ca://${MOTOR}.VAL``.
+ #. Set the ``displayFormat`` property to ``Decimal``.
+ #. Select the ``showUnits`` property.
+ #. Expand the ``maximumSize`` property and set the ``Width`` property to ``150``.
+
+ * **Step 3.6.**
+
+ Let's now add a ``Label``, and this time, a ``PyDMLabel`` that
+ will be used to read the **Readback Position** of the Motor:
+
+ #. Drag and drop a ``Label`` into the the previously added ``Form Layout`` right
+ under the previously added components.
+ #. Set the ``text`` property to ``Readback:``.
+ #. Drag and drop a ``PyDMLabel`` to the ``Form Layout`` paying attention to
+ add it on the right side of the previously added ``Label``.
+ #. Set the ``channel`` property to ``ca://${MOTOR}.RBV``.
+ #. Set the ``displayFormat`` property to ``Decimal``.
+ #. Select the ``showUnits`` property.
+
+ * **Step 3.7.**
+
+ Let's add another ``Label`` and ``PyDMLineEdit`` pair that will be used
+ to edit the **Velocity** of the Motor:
+
+ #. Drag and drop a ``Label`` into the the previously added ``Form Layout`` right
+ under the previously added components.
+ #. Set the ``text`` property to ``Velocity:``.
+ #. Drag and drop a ``PyDMLineEdit`` to the ``Form Layout`` paying attention to
+ add it on the side of the previously added ``Label``.
+ #. Set the ``channel`` property to ``ca://${MOTOR}.VELO``.
+ #. Set the ``displayFormat`` property to ``Decimal``.
+ #. Select the ``showUnits`` property.
+ #. Expand the ``maximumSize`` property and set the ``Width`` property to ``150``.
+
+
+ * **Step 3.8.**
+
+ And now to the last ``Label`` and ``PyDMLineEdit`` pair that will be used
+ to edit the **Acceleration** of the Motor:
+
+ #. Drag and drop a ``Label`` into the the previously added ``Form Layout`` right
+ under the previously added components.
+ #. Set the ``text`` property to ``Acceleration:``.
+ #. Drag and drop a ``PyDMLineEdit`` to the ``Form Layout`` paying attention to
+ add it on the side of the previously added ``Label``.
+ #. Set the ``channel`` property to ``ca://${MOTOR}.ACCL``.
+ #. Set the ``displayFormat`` property to ``Decimal``.
+ #. Select the ``showUnits`` property.
+ #. Expand the ``maximumSize`` property and set the ``Width`` property to ``150``.
+
+
+ * **Step 3.9.**
+
+ Once all the widgets are added to the form, it is now time to adjust the layouts
+ and make sure that everything is well-positioned and behaving nicely.
+
+ #. Using the ``Object Inspector`` at the top-right corner of the Qt Designer
+ window, select the ``formLayout`` object and set the properties according
+ to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutTopMargin 6
+ layoutRightMargin 6
+ layoutBottomMargin 6
+ layoutHorizontalSpacing 10
+ layoutVerticalSpacing 10
+ layoutLabelAlignment > Horizontal AlignRight
+ layoutLabelAlignment > Vertical AlignVCenter
+ layoutFormAlignment > Horizontal AlignLeft
+ layoutFormAlignment > Vertical AlignVCenter
+ ================================== ==================
+
+ #. Continuing with the ``Object Inspector``, select the ``frame`` object,
+ scroll down the ``Property Editor`` until the end and set the properties
+ according to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutLeftMargin 0
+ layoutTopMargin 0
+ layoutRightMargin 0
+ layoutBottomMargin 0
+ layoutSpacing 0
+ ================================== ==================
+
+ #. Still with the ``Object Inspector``, now select the ``verticalLayout`` object
+ that is right under the ``Form`` object and set the properties according
+ to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutSpacing 0
+ ================================== ==================
+
+ #. Finally, with the ``Object Inspector`` select the ``Form`` object
+ set the properties according to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ geometry > Width 450
+ geometry > Height 217
+ layoutLeftMargin 0
+ layoutTopMargin 0
+ layoutRightMargin 0
+ layoutBottomMargin 0
+ layoutSpacing 0
+ ================================== ==================
+
+ The end result will be something like this:
+
+ .. figure:: /_static/tutorials/action/expert/expert_all_widgets_ok.png
+ :scale: 100 %
+ :align: center
+
+* **Step 4.**
+
+ Save this file as ``expert_motor.ui``.
+
+ .. warning::
+ For this tutorial it is important to use this file name, as it will be referenced
+ at the other sections. If you change it please remember to also change it in the
+ next steps when referenced.
+
+* **Step 5.**
+
+ Test the Expert Motor Screen:
+
+ .. code-block:: bash
+
+ pydm -m '{"MOTOR":"IOC:m1"}' expert_motor.ui
+
+ .. figure:: /_static/tutorials/action/expert/expert.png
+ :scale: 75 %
+ :align: center
+ :alt: Expert Motor Screen
+
+.. note::
+ You can download this file using :download:`this link <../../../../examples/tutorial/expert_motor.ui>`.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/designer_inline.rst b/docs/source/tutorials/action/designer_inline.rst
new file mode 100644
index 0000000000..858cfe04c7
--- /dev/null
+++ b/docs/source/tutorials/action/designer_inline.rst
@@ -0,0 +1,282 @@
+.. _Inline:
+
+Inline Motor Screen
+===================
+
+.. important::
+
+ * Make sure the PCASpy tutorial server is :ref:`running `
+
+For this screen, we want to present useful information to the user to operate
+the motors, and also provide a way for them to access other less-commonly-used parameters via an "Expert" screen. To make this screen re-usable in other displays, it will be necessary
+to use :ref:`Macros` that will later be replaced by the proper information for
+each motor.
+
+The finished result will look like this:
+
+.. figure:: /_static/tutorials/action/inline/inline.png
+ :scale: 75 %
+ :align: center
+ :alt: Inline Motor Screen
+
+ Inline Motor Screen
+
+* **Step 1.**
+
+ Let's start by opening :ref:`Qt Designer `
+ and creating a new ``Widget``.
+
+ .. figure:: /_static/tutorials/action/new_widget.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 2.**
+
+ With the new form available, let's add a GridLayout widget to it. To make it
+ fill the whole form let's select ``Layout Vertically`` for the form.
+
+ .. figure:: /_static/tutorials/action/inline/inline_layout.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 3.**
+
+ Now that we have a layout, let's take a look at the widgets on this screen:
+
+ .. figure:: /_static/tutorials/action/inline/widgets.png
+ :scale: 70 %
+ :align: center
+
+ * **Step 3.1.**
+
+ The first ``PyDMLabel`` will display the description of the motor:
+
+ #. Drag and drop a ``PyDMLabel`` at the previously added ``GridLayout``.
+ #. Set the ``Channel`` property of this label to: ``ca://${MOTOR}.DESC``.
+
+ * ${MOTOR} is a macro that will later be replaced by a value sent by screens
+ using this widget in embedded displays, related displays, or when launching
+ using the command line.
+
+ #. Set the ``displayFormat`` property of this label to: ``String``.
+ #. Expand the ``Font`` property and mark the checkbox for ``Bold``.
+
+ .. figure:: /_static/tutorials/action/inline/inline_desc.gif
+ :scale: 100 %
+ :align: center
+
+
+ * **Step 3.2.**
+
+ The second widget is a ``PyDMLineEdit`` which will be used to set the desired
+ position for the motor:
+
+ #. Drag and drop a ``PyDMLineEdit`` into the ``GridLayout`` on the side of the
+ previously added ``PyDMLabel`` (**Note**: The border will become blue showing that
+ the widget will be placed on the side and not on top or under the other widget).
+ #. Set the ``Channel`` property of this line edit to: ``ca://${MOTOR}.VAL``.
+ #. Change its ``displayFormat`` property to ``Decimal``.
+ #. Expand the ``sizePolicy`` property and set ``Horizontal Policy`` and
+ ``Vertical Policy`` to ``Fixed``.
+
+ * This will make the widget respect the size on screen.
+
+ #. Expand the ``minimumSize`` property and set ``Width`` to ``75``.
+
+ * This property will indicate the minimum size constrains for the widget and
+ avoid this widget from being hidden or reduced to an unusable size on window resizing.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_2.gif
+ :scale: 100 %
+ :align: center
+
+ * **Step 3.3.**
+
+ The third widget is a ``PyDMLabel`` which will be used to monitor the readback
+ value for the motor:
+
+ #. Drag and drop a ``PyDMLabel`` at the ``GridLayout`` on the side of the
+ previously added ``PyDMLineEdit``.
+ #. Set the ``Channel`` property of this line edit to: ``ca://${MOTOR}.RBV``.
+ #. Change its ``displayFormat`` property to ``Decimal``.
+ #. Expand the ``sizePolicy`` property and set ``Horizontal Policy`` and
+ ``Vertical Policy`` to ``Fixed``.
+
+ * This will make the widget respect the size on screen.
+
+ #. Expand the ``minimumSize`` property and set ``Width`` to ``75``.
+
+ * This property will indicate the minimum size constraints for the widget and
+ avoid this widget becoming hidden or reduced to an unusable size when a user
+ resizes the window.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_3.gif
+ :scale: 100 %
+ :align: center
+
+ * **Step 3.4.**
+
+ The fourth widget is a ``PyDMByteIndicator`` which will be used for visual
+ feedback that the motor is moving:
+
+ #. Drag and drop a ``PyDMByteIndicator`` into the ``GridLayout`` on the side of the
+ previously added ``PyDMLabel``.
+ #. Set the ``Channel`` property of this line edit to: ``ca://${MOTOR}.MOVN``.
+ #. Turn off the ``showLabels`` property since we are only interested on the
+ color for this widget.
+ #. Set the ``circles`` property so we have a circle instead of a square.
+ #. Expand the ``sizePolicy`` property and set ``Horizontal Policy`` and
+ ``Vertical Policy`` to ``Fixed``.
+
+ #. Expand the ``minimumSize`` property and set ``Width`` and ``Height`` to
+ ``32``.
+ #. Repeat the same previous step for the ``maximumSize`` property.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_4.gif
+ :scale: 100 %
+ :align: center
+
+
+ * **Step 3.5.**
+
+ The fifth widget is a ``PyDMPushButton`` which will be used to stop the motor:
+
+ #. Drag and drop a ``PyDMPushButton`` at the ``GridLayout`` on the side of the
+ previously added ``PyDMByteIndicator``.
+ #. Set the ``channel`` property of this line edit to: ``ca://${MOTOR}.STOP``.
+ #. Set the ``pressValue`` property to ``1``.
+
+ * This is the value that will be written to the channel once the button is
+ pressed.
+
+ #. Set the ``text`` property to ``Stop``.
+ #. Expand the ``sizePolicy`` property and set ``Horizontal Policy`` to ``Minimum``
+ and the ``Vertical Policy`` to ``Fixed``.
+ #. Set the ``styleSheet`` property to ``background-color: red;`` in order to
+ give the button a nice look and feel and bring the attention to it in case
+ of emergency.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_5.gif
+ :scale: 100 %
+ :align: center
+
+ * **Step 3.6.**
+
+ The sixth widget is also a ``PyDMPushButton`` which will be used to tweak the
+ motor a certain distance in the positive direction:
+
+ #. Drag and drop a ``PyDMPushButton`` into the ``GridLayout`` on the side of the
+ previously added ``PyDMPushButton``.
+ #. Set the ``channel`` property of this line edit to: ``ca://${MOTOR}.VAL``.
+ #. Set the ``pressValue`` property to ``10``.
+ #. Set the ``relativeChange`` property so the new value written to the
+ channel will be relative to the channel's current value.
+ #. Set the ``text`` property to ``Tw +10``.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_6.gif
+ :scale: 100 %
+ :align: center
+
+ * **Step 3.7.**
+
+ The seventh widget is also a ``PyDMPushButton`` which will be used to tweak the
+ motor a certain distance in the negative direction:
+
+ #. Drag and drop a ``PyDMPushButton`` into the ``GridLayout`` on the side of the
+ previously added ``PyDMPushButton``.
+ #. Set the ``channel`` property of this line edit to: ``ca://${MOTOR}.VAL``.
+ #. Set the ``pressValue`` property to ``-10``.
+ #. Set the ``relativeChange`` property so the new value written to the
+ channel will be relative to the channel's current value.
+ #. Set the ``text`` property to ``Tw -10``.
+
+ * **Step 3.8.**
+
+ The final widget is a ``PyDMRelatedDisplayButton`` which will be used to launch
+ the **engineer** screen so users can configure advanced parameters and troubleshoot
+ possible issues with the motor:
+
+ #. Drag and drop a ``PyDMRelatedDisplayButton`` at the ``GridLayout`` on the
+ side of the previously added ``PyDMPushButton``.
+ #. Set the ``text`` property to ``Engineer...``.
+ #. Add the string ``exper_motor.ui`` to the ``filenames`` property.
+
+ .. note::
+
+ We will create the ``expert_motor.ui`` file in the next section.
+
+ #. Set the ``openInNewWindow`` property so the screen will show up in a standalone
+ window.
+ #. Expand the ``minimumSize`` property and set ``Width`` to ``125`` and
+ ``Height`` to ``24``.
+ #. Repeat the same previous step for the ``maximumSize`` property.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_8.gif
+ :scale: 100 %
+ :align: center
+
+ * **Step 3.9.**
+
+ After adding all the widgets to the layout, it will look like this:
+
+ .. figure:: /_static/tutorials/action/inline/inline_all_widgets.png
+ :scale: 50 %
+ :align: center
+
+ Let's adjust the sizes and reduce the top and bottom margins on the layout.
+
+ #. Using the Object Inspector on the top-right corner, select the ``gridLayout``
+ object and:
+
+ * Set the property ``layoutRightMargin`` to ``5``.
+ * Set the property ``layoutBottomMargin`` to ``5``.
+ * Set the property ``layoutHorizontalSpacing`` to ``10``.
+ * Set the property ``layoutVerticalSpacing`` to ``5``.
+
+ #. Using the Object Inspector on the top-right corner, select the ``Form``
+ object and:
+
+ * Expand the ``geometry`` property and set ``Width`` to ``700`` and ``Height`` to
+ ``32``.
+ * Expand the ``sizePolicy`` property and set ``Vertical Policy`` to ``Fixed``.
+ * Expand the ``minimumSize`` property and set ``Width`` to ``700`` and ``Height`` to
+ ``32``.
+ * Scroll all the way down on the property editor and set ``layoutLeftMargin``,
+ ``layoutTopMargin``, ``layoutRightMargin``, ``layoutBottomMargin`` and
+ ``layoutSpacing`` to ``0`` so the form is very tight.
+ * Expand the ``maximumSize`` property and set ``Height`` to ``38``.
+
+ .. figure:: /_static/tutorials/action/inline/inline_3_9.gif
+ :scale: 100 %
+ :align: center
+
+ The end result will be something like this:
+
+ .. figure:: /_static/tutorials/action/inline/inline_all_widgets_ok.png
+ :scale: 75 %
+ :align: center
+
+* **Step 4.**
+
+ Save this file as ``inline_motor.ui``.
+
+ .. warning::
+ For this tutorial it is important to use this file name as it will be referenced
+ at the other sections. If you change it, please remember to also change it in the
+ next steps when referenced.
+
+* **Step 5.**
+
+ Test the Inline Motor Screen:
+
+ .. code-block:: bash
+
+ pydm -m '{"MOTOR":"IOC:m1"}' inline_motor.ui
+
+ .. figure:: /_static/tutorials/action/inline/inline.png
+ :scale: 75 %
+ :align: center
+ :alt: Inline Motor Screen
+
+.. note::
+ You can download this file using :download:`this link <../../../../examples/tutorial/inline_motor.ui>`.
diff --git a/docs/source/tutorials/action/designer_main.rst b/docs/source/tutorials/action/designer_main.rst
new file mode 100644
index 0000000000..ea97deb949
--- /dev/null
+++ b/docs/source/tutorials/action/designer_main.rst
@@ -0,0 +1,259 @@
+.. _Main:
+
+Main Screen
+===========
+
+.. important::
+
+ * Make sure the PCASpy tutorial server is :ref:`running `
+
+This will be the main piece of our Beam Positioning application and will group the other
+components of this tutorial.
+
+The finished result will look like this:
+
+.. figure:: /_static/tutorials/action/main/main.png
+ :scale: 75 %
+ :align: center
+ :alt: Expert Motor Screen
+
+ Main Screen for Beam Positioning Application
+
+
+* **Step 1.**
+
+ Let's start by opening the :ref:`Qt Designer `
+ and creating a new ``Widget``.
+
+ .. figure:: /_static/tutorials/action/new_widget.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 2.**
+
+ With the new form available, let's add a ``Vertical Layout`` widget and make
+ it fill the whole form. Let's select ``Layout Vertically`` for the Form.
+
+ .. figure:: /_static/tutorials/action/inline/inline_layout.gif
+ :scale: 100 %
+ :align: center
+
+* **Step 3.**
+
+ Now that we have a layout, let's take a look at the widgets on this screen:
+
+ .. figure:: /_static/tutorials/action/main/widgets.png
+ :scale: 70 %
+ :align: center
+
+ * **Step 3.1.**
+
+ The first ``Label`` will be the title of our screen:
+
+ #. Drag and drop a ``Label`` into the previously added ``Vertical Layout``.
+ #. Set the ``text`` property of this label to: ``Beam Alignment``.
+ #. In order to make the label look better as a title, add the following to
+ the ``stylesheet`` property:
+
+ .. code-block:: css
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+ }
+
+
+ * **Step 3.2.**
+
+ The second widget that we will add is a ``PyDMImageView``, which will display
+ the image coming from our camera:
+
+ #. Drag and drop a ``PyDMImageView`` into the previously added ``Vertical Layout`` under
+ the ``Label`` that was added at **Step 3.1**.
+ #. Set the ``objectName`` property to ``imageView``.
+ #. Set the ``imageChannel`` property to ``ca://IOC:Image``.
+ #. Set the ``widthChannel`` property to ``ca://IOC:ImageWidth``.
+ #. Set the ``readingOrder`` property to ``Clike``.
+ #. Set the ``maxRedrawRate`` property to ``30`` so we can update the image at
+ 30 Hz.
+
+ * **Step 3.3.**
+
+ The third widget that we will add is a ``Vertical Layout``, which will be the
+ placeholder for the controls area of the screen:
+
+ #. Drag and drop a ``Vertical Layout`` into the previously added ``Vertical Layout`` under
+ the ``PyDMImageView`` that was added at **Step 3.2**.
+
+ * **Step 3.4.**
+
+ The fourth widget that we will add is a ``Label``, which will be updated with
+ the result of the calculation of beam position in the next section (:ref:`LittleCode`):
+
+ #. Drag and drop a ``Label`` into the ``Vertical Layout`` that was added in
+ **Step 3.3**.
+ #. Set the ``objectName`` property of this widget to ``lbl_blobs``.
+
+ .. important::
+
+ It is very important to set the ``objectName`` property of widgets in
+ the designer if you intend to access them using code, otherwise the
+ names will be automatically assigned, and will not make much sense later
+ on.
+
+ #. Set the ``text`` property to empty so this label will only show information
+ when we write to it using the code later on.
+
+ * **Step 3.5.**
+
+ The fifth widget that we will add is another ``Label``, which will show the title
+ of our controls area:
+
+ #. Drag and drop a ``Label`` into the ``Vertical Layout`` that was added in
+ **Step 3.3** right under the ``Label`` added in **Step 3.5**.
+ #. Set the ``text`` property of this label to: ``Controls``.
+ #. In order to make the label look better as a title, add the following to
+ the ``stylesheet`` property:
+
+ .. code-block:: css
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+ }
+
+ * **Step 3.6.**
+
+ The sixth widget that we will add is a ``Frame``, which will be the container
+ for our two motors' ``Embedded Displays``:
+
+ #. Drag and drop a ``Frame`` under the ``Label`` added in **Step 3.6**.
+ #. Set the ``frameShape`` property to ``StyledPanel``.
+ #. Set the ``frameShadow`` property to ``Raised``
+ #. Set the ``stylesheet`` property to:
+
+ .. code-block:: css
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+ }
+
+ * **Step 3.7.**
+
+ The seventh widget that we will add is a ``PyDMEmbeddedDisplay``, which will
+ display the ``inline_motor.ui`` with information for our first motor axis:
+
+ #. Drag and drop a ``PyDMEmbeddedDisplay`` into the ``Frame`` added in **Step 3.7**.
+ #. Right-click the ``Frame`` from **Step 3.7** and select ``Layout >> Layout Vertically``.
+ #. Set the ``macros`` property to ``{"MOTOR":"IOC:m1"}``.
+ #. Set the ``filename`` property to ``inline_motor.ui``.
+
+ * **Step 3.8.**
+
+ The eigth widget that we will add is a ``PyDMEmbeddedDisplay``, which will
+ display the ``inline_motor.ui`` with information for our second motor axis:
+
+ #. Drag and drop a ``PyDMEmbeddedDisplay`` into the ``Frame`` added in **Step 3.7**.
+ #. Set the ``macros`` property to ``{"MOTOR":"IOC:m2"}``.
+ #. Set the ``filename`` property to ``inline_motor.ui``.
+
+ * **Step 3.9.**
+
+ Finally, the ninth widget that we will add is a ``PyDMRelatedDisplayButton``, which will
+ open the ``All Motors`` screen that will be developed :ref:`later `:
+
+ #. Drag and drop a ``PyDMRelatedDisplayButton`` into the ``Vertical Layout`` added in **Step 2**.
+ #. Add the string ``all_motors.py`` to the ``filenames`` property.
+ #. Uncheck the ``openInNewWindow`` property.
+ #. Set the ``text`` property to: ``View All Motors``
+
+ * **Step 3.10.**
+
+ Once all the widgets are added to the form, it is now time to adjust the layouts
+ and make sure that all is well positioned and behaving nicely.
+
+ #. Using the ``Object Inspector`` at the top-right corner of the Qt Designer
+ window, select the ``frame`` object and set the properties according
+ to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutLeftMargin 0
+ layoutTopMargin 0
+ layoutRightMargin 0
+ layoutBottomMargin 0
+ layoutSpacing 0
+ ================================== ==================
+
+ #. Continuing with the ``Object Inspector``, select the ``vertical layout``
+ object right before the ``frame`` and set the properties according to the
+ table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutSpacing 0
+ ================================== ==================
+
+ #. Still with the ``Object Inspector``, now select the top most ``verticalLayout``
+ object set the properties according to the table below:
+
+ ================================== ==================
+ Property Value
+ ================================== ==================
+ layoutSpacing 0
+ ================================== ==================
+
+ The end result will be something like this:
+
+ .. figure:: /_static/tutorials/action/main/main_all_widgets_ok.png
+ :scale: 100 %
+ :align: center
+
+* **Step 4.**
+
+ Save this file as ``main.ui``.
+
+ .. warning::
+ For this tutorial it is important to use this file name, as it will be referenced
+ at the other sections. If you change it please remember to also change at the
+ next steps when referenced.
+
+* **Step 5.**
+
+ Test the Expert Motor Screen:
+
+ .. code-block:: bash
+
+ pydm main.ui
+
+ .. figure:: /_static/tutorials/action/main/main.png
+ :scale: 75 %
+ :align: center
+ :alt: Main Application Screen
+
+.. note::
+ Purple borders will appear around any widgets that have "Alarm Sensitive Border" enabled.
+ These can be removed by simply unchecking the setting. (for the purposes of this tutorial,
+ these borders are not significant and can be in either the on or off state)
+
+.. note::
+ You can download this file using :download:`this link <../../../../examples/tutorial/main.ui>`.
+
diff --git a/docs/source/tutorials/action/intro_designer.rst b/docs/source/tutorials/action/intro_designer.rst
new file mode 100644
index 0000000000..1616a400fc
--- /dev/null
+++ b/docs/source/tutorials/action/intro_designer.rst
@@ -0,0 +1,74 @@
+.. _Designer:
+
+Introduction to Qt Designer
+===========================
+
+Once you have PyDM installed, you can start making displays. The easiest way
+to make a display is to use Qt Designer, which is Qt's drag-and-drop tool for
+building user interfaces.
+
+.. note::
+ To launch the Qt Designer do:
+
+ .. code-block:: bash
+
+ #########
+ # Linux #
+ #########
+ $ designer
+
+ #########
+ # macOS #
+ #########
+ $ /Designer.app/Contents/MacOS/Designer
+
+ ###########
+ # Windows #
+ ###########
+ C:\> designer
+
+Once you open Designer, you'll be greeted by a mostly
+blank screen, with a list of widgets on the left, and a property inspector on the
+right.
+
+.. figure:: /_static/tutorials/action/designer.png
+ :scale: 33 %
+ :align: center
+ :alt: Screenshot of newly-opened Qt Designer.
+
+ A newly-opened Qt Designer. Notice the PyDM widgets at the bottom of
+ the widget list on the left.
+
+To make a new PyDM display, go to File->New..., then choose to build a new Widget.
+
+.. note::
+ All PyDM displays must have a Widget for the base - if you try to make
+ one using a MainWindow, your display will not work properly.
+
+.. figure:: /_static/tutorials/action/new_widget.png
+ :scale: 100 %
+ :align: center
+ :alt: Screenshot of new file screen
+
+ New File Dialog. Notice the Widget as the selected option.
+
+Now you should see a blank form on which you can drag widgets.
+If you drag a PyDMLabel (in the 'PyDM Display Widgets' section) onto the form,
+on the right side of the screen you can see all the properties for this widget.
+At the bottom of the properties list are the PyDM-specific properties.
+
+.. figure:: /_static/tutorials/action/pydm_properties.png
+ :scale: 33 %
+ :align: center
+ :alt: Screenshot showing the PyDMLabel's properties.
+
+ The PyDMLabel's properties are highlighted in red.
+
+Once you are done with the screen design, go to File->Save... and save the .ui
+file somewhere.
+
+We can launch this screen with the following command::
+
+ $ pydm
+
+for the .ui file you just saved. This will open your display in PyDM.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/intro_python.rst b/docs/source/tutorials/action/intro_python.rst
new file mode 100644
index 0000000000..fffabe94aa
--- /dev/null
+++ b/docs/source/tutorials/action/intro_python.rst
@@ -0,0 +1,140 @@
+.. _Python:
+
+A Word About Python Displays
+============================
+
+PyDM supports making displays that are powered by Python scripts. This is quite
+powerful - you can do anything from generating a simple display at run time
+(from a file or database, for example), up to building entire applications that
+utilize the PyDM widget set. In additon to this guide, you can look at some of
+PyDM's bundled examples to see how a script-based display works. In particular,
+the 'image_processing' example is a good place to start.
+
+Building Your UI In Designer
+----------------------------
+
+When you make a Python-based display, you can still use Qt Designer to lay out
+your user interface. In addition to the PyDM widget set, you may want to consider
+using some of Qt's base widgets like Line Edit, Push Button, etc, if your display
+will have some internal functionality that does not depend on a data source.
+
+For example, the 'positioner' example uses normal (non-PyDM) Line Edits to take
+user input, performs some math on the input, and outputs the result to PVs, which
+are displayed with PyDMLabels. If you want to dynamically generate your display
+when it launches, you should still build a UI file, but it might only have an empty
+container (like a Vertical Layout, or a Scroll Area), which you will fill with
+widgets later in your code.
+
+Writing The Code For Your Display
+---------------------------------
+
+Python-based displays in PyDM are mostly just PyQt widgets with a few extra features
+on top. This guide expects that you have a basic familiarity with PyQt and Qt itself.
+Good resources for these topics are available online. The Qt documentation, especially,
+is very thorough, and will come in handy as you build your display.
+
+.. _Display:
+
+Subclassing Display
+^^^^^^^^^^^^^^^^^^^
+
+Python-based displays are just PyQt widgets, based on PyDM's 'Display' class.
+Your display must subclass Display, and implement a few required methods::
+
+ from os import path
+ from pydm import Display
+ class MyDisplay(Display):
+ def __init__(self, parent=None, args=None, macros=None):
+ super(MyDisplay, self).__init__(parent=parent, args=args, macros=macros)
+
+ def ui_filename(self):
+ return 'my_display.ui'
+
+ def ui_filepath(self):
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+Lets look at this in detail::
+
+ from os import path
+ from pydm import Display
+
+First, we import the modules we need. You will probably want to import more of
+your own modules here as well.
+
+Next, we define our Display subclass, and its initializer::
+
+ class MyDisplay(Display):
+ def __init__(self, parent=None, args=None):
+ super(MyDisplay, self).__init__(parent=parent)
+
+It is important to remember that you must always call the superclass' initializer
+in your own, and pass it the 'parent' argument from your initializer. Otherwise,
+your display might get garbage collected by Python and crash.
+
+Now we must implement two methods that tell PyDM where the .ui file for this display
+lives::
+
+ def ui_filename(self):
+ return 'my_display.ui'
+
+lets PyDM know what the .ui file to load is called, and::
+
+ def ui_filepath(self):
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+lets PyDM know where to find that file. The implementation of ui_filepath used
+here can probably be copied and pasted into your display verbatim: it just joins
+the path of the display's .py file to the filename of the .ui file. Unfortunately,
+at the time of writing you must include this yourself, it cannot be done automatically
+by PyDM.
+
+PyDM will expose all the widgets from the .ui file as a variable called 'ui'
+in your display class. To access a widget in your code, call
+`self.ui.widgetName`
+
+Handling Command Line Arguments
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Displays can accept command line arguments supplied at launch. Your display's
+initializer has a named argument called 'args'::
+
+ def __init__(self, parent=None, args=None, macros=None):
+
+It is recommended to use python's `argparse` module to parse your arguments.
+For example, you could write a method like this in your display to do this::
+
+ def parse_args(self, args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--list', dest='magnet_list', help='File containing a list of magnet names to use.')
+ parsed_args, _unknown_args = parser.parse_known_args(args)
+ return parsed_args
+
+Using command line arguments can be a good way to make displays that generate
+themselves dynamically: you could accept a filename argument, and read the contents
+of that file to add widgets to your display.
+
+Handling Macros
+^^^^^^^^^^^^^^^
+You can also use PyDM's macro system as a way to get user data into your display.
+All macros passed into your display are available as a dictionary in the initializer.
+In addition, macro substitution will always be performed on the .ui file for
+your display.
+
+Building Your Interface Dynamically
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A common reason to build a Python-based display is to generate your UI dynamically,
+from some other source of data, like a file or database. As mentioned in
+`Handling Command Line Arguments`_, you can read in command line arguments to help
+get data into your display.
+
+Once you have a source of data, you can use PyQt to make new widgets, and add them
+to your display. For example, if you get a list of devices from somewhere, you can
+make widgets for each device, and add them to a layout you defined in the .ui file::
+
+ for device_name in device_list:
+ device_label = PyDMLabel(parent=self, init_channel=device_name)
+ self.ui.deviceListLayout.addWidget(device_label)
+
+You usually want to put code like this in your display's initializer, so that it
+happens when the display launches.
diff --git a/docs/source/tutorials/action/little_code.rst b/docs/source/tutorials/action/little_code.rst
new file mode 100644
index 0000000000..474ade0683
--- /dev/null
+++ b/docs/source/tutorials/action/little_code.rst
@@ -0,0 +1,143 @@
+.. _LittleCode:
+
+Adding Code into the Main Display
+=================================
+
+.. important::
+
+ * Make sure the PCASpy tutorial server is :ref:`running `
+
+For this particular application it would be of interest to not only see the beam
+image on the screen, but to also calculate the maximum point on the image and display
+the coordinates in a label.
+
+To do so, we will need to add some Python code to our main screen developed at
+the :ref:`Main` section.
+
+Since we already have the screen designed in the UI file, we can re-use it in
+our Python-based display, and hook up code to interact with widgets.
+
+This is accomplished by subclassing `pydm.Display` (See :ref:`Display` for more details).
+
+* **Step 1.**
+
+ Open a new text file. The first thing that we will do is add the imports
+ needed for the code that will follow.
+
+ .. code-block:: python
+
+ import time
+ from os import path
+ from pydm import Display
+ from scipy.ndimage.measurements import maximum_position
+
+* **Step 2.**
+
+ Let's create our Python class that will inherit from ``Display`` (See :ref:`Display`).
+
+ .. code-block:: python
+
+ class BeamPositioning(Display):
+
+ def __init__(self, parent=None, args=None, macros=None):
+ super(BeamPositioning, self).__init__(parent=parent, args=args, macros=None)
+ # Attach our custom process_image method
+ self.ui.imageView.process_image = self.process_image
+ # Hook up to the newImageSignal so we can update
+ # our widgets after the new image is done
+ self.ui.imageView.newImageSignal.connect(self.show_blob)
+ # Store blob coordinate
+ self.blob = (0, 0)
+
+ def ui_filename(self):
+ # Point to our UI file
+ return 'main.ui'
+
+ def ui_filepath(self):
+ # Return the full path to the UI file
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+ Breaking the class constructor code into pieces, we have:
+
+ #. Replaced the default ``PyDMImageView`` method ``process_image`` with our
+ own custom method.
+ #. Hooked up our ``show_blob`` method to the ``newImageSignal`` that is emitted
+ by the ``PyDMImageView`` every time a new image is displayed.
+ #. Initialized the ``self.blob`` variable with `(0, 0)`.
+ #. Implemented a ``ui_filename`` method returning the name of the ``UI`` file to be used and
+ compose the screen.
+ #. Implemented a ``ui_filepath`` method returning the full path to the ``ui_filename`` so PyDM
+ can properly load it.
+
+ * **Step 2.1.**
+
+ Add code to the ``process_image`` callback method so we can calculate the
+ blob position.
+
+ .. important::
+
+ The ``process_image`` method is defined in the ``PyDMImageView`` widget
+ and more information about it can be found at the
+ `PyDMImage widget documentation page `_.
+
+ Since this method runs in a separated ``QThread``, we shouldn't
+ manipulate widgets in this method, since this code runs outside of the
+ **Qt Main Thread**.
+
+ .. code-block:: python
+
+ def process_image(self, new_image):
+ # Consider the maximum as the Blob since we have only
+ # one.
+ self.blob = maximum_position(new_image)
+ # Send the original image data to the image widget
+ return new_image
+
+ In ``process_image`` we call the scipy method `maximum_position `_
+ to calculate the coordinates for the maximum spot and save it to ``self.blob``.
+ At the end, this method returns the unmodified image, which the ImageView
+ will display. If you'd like to manipulate the image before displaying it,
+ you can do so in this method, and return the manipulated version.
+
+ * **Step 2.2.**
+
+ Add code to the ``show_blob`` method so we update the ``QLabel`` with the
+ new blob position calculated in ``process_image``.
+
+ .. code-block:: python
+
+ def show_blob(self, *args, **kwargs):
+ # If we have a blob, present the coordinates in label
+ if self.blob != (0, 0):
+ blob_txt = "Blob Found:"
+ blob_txt += " ({}, {})".format(self.blob[1], self.blob[0])
+ else:
+ # If no blob was found, present the "Not Found" message
+ blob_txt = "Blob Not Found"
+ # Update the label text
+ self.ui.lbl_blobs.setText(blob_txt)
+
+
+* **Step 3.**
+
+ Save this file as ``main.py``.
+
+ .. warning::
+ For this tutorial it is important to use this file name as it will be referenced
+ at the other sections. If you change it please remember to also change in the
+ other steps when referenced.
+
+* **Step 4.**
+
+ Test the Main Screen:
+
+ .. code-block:: bash
+
+ pydm main.py
+
+ .. figure:: /_static/tutorials/action/little_code/main.gif
+ :scale: 75 %
+ :align: center
+
+.. note::
+ You can download this file using :download:`this link `.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/python.rst b/docs/source/tutorials/action/python.rst
new file mode 100644
index 0000000000..6d654b8449
--- /dev/null
+++ b/docs/source/tutorials/action/python.rst
@@ -0,0 +1,257 @@
+.. _PurePython:
+
+Making Pure Python Displays
+===========================
+
+.. important::
+
+ * Make sure the PCASpy tutorial server is :ref:`running `
+
+As we saw in the :ref:`A Word About Python Display ` section, it is
+possible to make displays using Python code and a .ui file from Qt Designer.
+It is also possible to make displays without using the Qt Designer at all,
+and write the user interface entirely in code.
+
+To demonstrate this capability we will describe the steps to create the "All Motors"
+screen described at the :ref:`Components Section `.
+
+This screen will have a ``QLineEdit`` and a ``QPushButton`` that will invoke a
+method to filter our list of motors and present a list of
+``PyDMEmbeddedDisplays`` in the frame below pointing to the
+``inline_motor.ui`` file that was created in the
+:ref:`Inline Motor Screen ` section of this tutorial.
+
+Here is how it will look once we are done:
+
+.. figure:: /_static/tutorials/action/python/all_motors.png
+ :scale: 75 %
+ :align: center
+ :alt: All Motors Screen
+
+.. important::
+
+ In order to simplify this tutorial, instead of using a database or other type
+ of service, the data to populate the list of motors will come from a simple text file
+ named ``motor_db.txt`` that can be downloaded :download:`here `.
+
+* **Step 1.**
+
+ The first thing that we will do is add the imports needed for the code that
+ will follow.
+
+ .. code-block:: python
+
+ import os
+ import json
+ from qtpy import QtCore
+ from pydm import Display
+ from qtpy.QtWidgets import (QVBoxLayout, QHBoxLayout, QGroupBox,
+ QLabel, QLineEdit, QPushButton, QScrollArea, QFrame,
+ QApplication, QWidget)
+
+ from pydm.widgets import PyDMEmbeddedDisplay
+ from pydm.utilities import connection
+
+
+* **Step 2.**
+
+ Let's create our Python Class that will inherit from ``Display`` (See :ref:`Display`).
+
+ .. code-block:: python
+
+ class AllMotorsDisplay(Display):
+ def __init__(self, parent=None, args=[], macros=None):
+ super(AllMotorsDisplay, self).__init__(parent=parent, args=args, macros=None)
+ # Placeholder for data to filter
+ self.data = []
+ # Reference to the PyDMApplication
+ self.app = QApplication.instance()
+ # Load data from file
+ self.load_data()
+ # Assemble the Widgets
+ self.setup_ui()
+
+ def minimumSizeHint(self):
+ # This is the default recommended size
+ # for this screen
+ return QtCore.QSize(750, 120)
+
+ def ui_filepath(self):
+ # No UI file is being used
+ return None
+
+ Breaking it down into pieces:
+
+ #. The constructor of the class will call the ``load_data`` method that is
+ responsible for opening our database and adding the information to our
+ placeholder, ``self.data``, for later filtering, as well as the ``setup_ui``
+ method in which the widgets be constructed and configuered.
+ #. ``minimumSizeHint`` returns the suggested minimum dimensions for the display.
+ #. ``ui_filepath`` will return ``None``, as no ``ui`` file is being used in this
+ case.
+
+ * **Step 2.1.**
+
+ Add the code to the ``load_data`` method.
+
+ .. note::
+
+ Look at the comments over the lines for explanation on what they do.
+
+ .. code-block:: python
+
+ def load_data(self):
+ # Extract the directory of this file...
+ base_dir = os.path.dirname(os.path.realpath(__file__))
+ # Concatenate the directory with the file name...
+ data_file = os.path.join(base_dir, "motor_db.txt")
+ # Open the file so we can read the data...
+ with open(data_file, 'r') as f:
+ # For each line in the file...
+ for entry in f.readlines():
+ # Append to the list of data...
+ self.data.append(entry[:-1])
+
+ * **Step 2.2.**
+
+ Add the code to the ``setup_ui`` method.
+
+ .. note::
+
+ Look at the comments over the lines for explanation on what they do.
+
+ .. code-block:: python
+
+ def setup_ui(self):
+ # Create the main layout
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ # Create a Label to be the title
+ lbl_title = QLabel("Motors Diagnostic")
+ # Add some StyleSheet to it
+ lbl_title.setStyleSheet("\
+ QLabel {\
+ qproperty-alignment: AlignCenter;\
+ border: 1px solid #FF17365D;\
+ border-top-left-radius: 15px;\
+ border-top-right-radius: 15px;\
+ background-color: #FF17365D;\
+ padding: 5px 0px;\
+ color: rgb(255, 255, 255);\
+ max-height: 25px;\
+ font-size: 14px;\
+ }")
+
+ # Add the title label to the main layout
+ main_layout.addWidget(lbl_title)
+
+ # Create the Search Panel layout
+ search_layout = QHBoxLayout()
+
+ # Create a GroupBox with "Filtering" as Title
+ gb_search = QGroupBox(parent=self)
+ gb_search.setTitle("Filtering")
+ gb_search.setLayout(search_layout)
+
+ # Create a label, line edit and button for filtering
+ lbl_search = QLabel(text="Filter: ")
+ self.txt_filter = QLineEdit()
+ self.txt_filter.returnPressed.connect(self.do_search)
+ btn_search = QPushButton()
+ btn_search.setText("Search")
+ btn_search.clicked.connect(self.do_search)
+
+ # Add the created widgets to the layout
+ search_layout.addWidget(lbl_search)
+ search_layout.addWidget(self.txt_filter)
+ search_layout.addWidget(btn_search)
+
+ # Add the Groupbox to the main layout
+ main_layout.addWidget(gb_search)
+
+ # Create the Results Layout
+ self.results_layout = QVBoxLayout()
+ self.results_layout.setContentsMargins(0, 0, 0, 0)
+
+ # Create a Frame to host the results of search
+ self.frm_result = QFrame(parent=self)
+ self.frm_result.setLayout(self.results_layout)
+
+ # Create a ScrollArea so we can properly handle
+ # many entries
+ scroll_area = QScrollArea(parent=self)
+ scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+ scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ scroll_area.setWidgetResizable(True)
+
+ # Add the Frame to the scroll area
+ scroll_area.setWidget(self.frm_result)
+
+ # Add the scroll area to the main layout
+ main_layout.addWidget(scroll_area)
+
+ * **Step 2.3.**
+
+ Add the code to connect the ``QPushButton`` click and perform the search
+ on our data.
+
+ .. note::
+
+ Look at the comments over the lines for explanation on what they do.
+
+ .. code-block:: python
+
+ def do_search(self):
+ # For each widget inside the results frame, lets destroy them
+ for widget in self.frm_result.findChildren(QWidget):
+ widget.setParent(None)
+ widget.deleteLater()
+
+ # Grab the filter text
+ filter_text = self.txt_filter.text()
+
+ # For every entry in the dataset...
+ for entry in self.data:
+ # Check if they match our filter
+ if filter_text.upper() not in entry.upper():
+ continue
+ # Create a PyDMEmbeddedDisplay for this entry
+ disp = PyDMEmbeddedDisplay(parent=self)
+ disp.macros = json.dumps({"MOTOR":entry})
+ disp.filename = 'inline_motor.ui'
+ disp.setMinimumWidth(700)
+ disp.setMinimumHeight(40)
+ disp.setMaximumHeight(100)
+ # Add the Embedded Display to the Results Layout
+ self.results_layout.addWidget(disp)
+
+
+ .. important::
+
+ Since `PyDM v1.6.0 `_ it is no longer required to call ``pydm.utilities.connection.establish_widget_connections``
+ and ``pydm.utilities.connection.close_widget_connections``.
+
+* **Step 3.**
+
+ Save this file as ``all_motors.py``.
+
+ .. warning::
+ For this tutorial it is important to use this file name as it will be referenced
+ at the other sections. If you change it please remember to also change at the
+ other steps when referenced.
+
+* **Step 4.**
+
+ Test the All Motors Screen:
+
+ .. code-block:: bash
+
+ pydm all_motors.py
+
+ .. figure:: /_static/tutorials/action/python/all_motors.gif
+ :scale: 75 %
+ :align: center
+
+.. note::
+ You can download this file using :download:`this link `.
\ No newline at end of file
diff --git a/docs/source/tutorials/action/tutorial.rst b/docs/source/tutorials/action/tutorial.rst
new file mode 100644
index 0000000000..47fa852294
--- /dev/null
+++ b/docs/source/tutorials/action/tutorial.rst
@@ -0,0 +1,54 @@
+.. _Application:
+
+About the Application
+=====================
+
+
+To demonstrate the concepts and capabilities of PyDM, let's develop a real
+application composed of PyDM widgets for beam positioning and alignment.
+
+
+PyDM allows users to create screens in three ways:
+
+#. Using only the Qt Designer application (.ui file)
+#. Using Qt Designer and Python Code (.ui and .py files)
+#. Using only Python code (.py file)
+
+In most of the cases users will choose between numbers 1 and 2 and in rare cases
+go with number 3.
+
+This tutorial will also cover the three scenarios above while building the proposed
+application.
+
+The application is a simulated x-ray beam positioning/alignment application
+in which the data from a camera will be presented along with two mirror motor
+axes to position the beam in X and Y.
+
+.. figure:: /_static/tutorials/action/application.png
+ :scale: 100 %
+ :align: center
+ :alt: Tutorial Application
+
+ Proposed Application Main Screen
+
+.. _App Components:
+
+Macro Components
+----------------
+
+.. figure:: /_static/tutorials/action/components.png
+ :scale: 100 %
+ :align: center
+ :alt: Tutorial Application
+
+- The ``main.ui`` file (Highlighted in Red) uses an embedded display
+ (Highlighted in Green) two times, which points to ``inline_motor.ui`` for **Motor X**
+ and **Motor Y**.
+
+- Inside of this embedded display there is a related display button (Highlighted
+ in Orange) which launches the ``expert_motor.ui`` for configuration of motor
+ parameters.
+
+- Finally, the **View All Motors** related display button (Highlighted in Blue)
+ launches the ``all_motors.py`` screen in which we can list all motor axes
+ available.
diff --git a/docs/source/tutorials/contrib/help.rst b/docs/source/tutorials/contrib/help.rst
new file mode 100644
index 0000000000..ec491e7f36
--- /dev/null
+++ b/docs/source/tutorials/contrib/help.rst
@@ -0,0 +1,12 @@
+Getting Help With PyDM
+======================
+
+If you have questions of comments in general about PyDM we want to know.
+
+You can choose between the channels open for communication the one that best fit you:
+
+- `Chat channel using Gitter `_ (Public)
+- `Stanford & SLAC Slack channel `_ (Limited for users with a Stanford Account)
+- `PyDM Mailing List `_ (Limited for SLAC users for now)
+
+or you can `file a bug `_ and let us know where our documentation could be improved.
diff --git a/docs/source/tutorials/contrib/requests.rst b/docs/source/tutorials/contrib/requests.rst
new file mode 100644
index 0000000000..549a8c6d30
--- /dev/null
+++ b/docs/source/tutorials/contrib/requests.rst
@@ -0,0 +1,76 @@
+Bug and Requests
+================
+
+If you spot a problem with PyDM, please let us know.
+Follow the steps below to increase the chances of a quick resolution.
+
+1. Is it a problem, a question or a request?
+--------------------------------------------
+For questions about how to use PyDM, please refer to the :doc:`help` page.
+
+If you don't have a GitHub account we strongly encourage you to create one by following the 3 easy steps described on this `page `_.
+
+Problems (not questions!) with the software or website can be reported as GitHub issues (`See here how to create one `_).
+
+**Problems include:**
+
+- Bugs (or probable bugs) with the PyDM software
+- Typos or other mistakes in the Reference or elsewhere on the documentation
+
+If you create a new issue that’s really just a clarifying question about how to use PyDM, we’ll close it and nicely ask
+you to visit the :doc:`help` channels in the future.
+
+Ideas or suggestions for enhancements should be posted as GitHub issues with the 'REQUEST: ' prefix to your issue title.
+When in doubt, start by reaching us through one of the :doc:`help` channels.
+
+
+2. Write helpfully
+-------------------
+
+To write an effective issue report:
+
+- Be precise
+- Be clear
+- Explain the steps required to reproduce the bug
+- Cite your OS and version, and which version of PyDM you’re using
+- If reporting an issue on the website, include the URL where the problem is
+- Screenshots are super helpful! They let us see what you see. You can drag images directly onto the issue text to upload them.
+- Errors are super helpful! (Sometimes.) Copy and paste any errors into the issue.
+- Include only one bug per report
+- Separate fact from speculation
+- No bug is too trivial to report, as small bugs may hide big bugs.
+
+
+Template for Bugs and Requests
+------------------------------
+
+We strongly recommend that you follow the template below but feel free to add more information (More is always better in this case):
+
+
+**Expected Behavior**
+ - Provide a general summary of the issue in the Title above
+ - If you are describing a bug, tell us what should happen
+ - If you are suggesting a change/improvement, tell us how it should work
+
+**Current Behavior**
+ - If describing a bug, tell us what happens instead of the expected behavior
+ - If suggesting a change/improvement, explain the difference from current behavior
+
+**Possible Solution**
+ - Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
+
+**Steps to Reproduce (for bugs)**
+ - Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant
+
+ 1. Step 1
+ 2. Step 2
+ 3. Step 3
+
+**Context**
+ - How has this issue affected you? What are you trying to accomplish?
+ - Providing context helps us come up with a solution that is most useful in the real world
+
+**Your Environment**
+ - Include as many relevant details about the environment you experienced the bug in
+ - One good start point is the **File > About** screen at PyDM.
+
diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst
new file mode 100644
index 0000000000..b230c4f101
--- /dev/null
+++ b/docs/source/tutorials/index.rst
@@ -0,0 +1,54 @@
+Tutorial
+========================================
+
+PyDM (Python Display Manager) is a new framework for building control system
+graphical user interfaces using Python and Qt.
+
+It provides a system for the drag-and-drop creation of user interfaces using
+Qt Designer, and also allows for the creation of displays driven by Python code.
+
+PyDM is intended to span the range from simple displays without any dynamic
+behavior to complex high level applications, with the same set of widgets.
+
+Developers can extend the framework with custom widgets for site-specific
+tasks, and data plugins for multiple control systems.
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Before You Start
+
+ intro.rst
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Introduction
+
+ intro/launcher.rst
+ intro/channels.rst
+ intro/data_arch.rst
+ intro/macros.rst
+ intro/widgets.rst
+ intro/datasource.rst
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Hands-on
+
+ action/tutorial.rst
+ action/intro_designer.rst
+ action/designer.rst
+ action/intro_python.rst
+ action/little_code.rst
+ action/python.rst
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Contributing
+
+ contrib/help.rst
+ contrib/requests.rst
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Links
+ :hidden:
diff --git a/docs/source/tutorials/intro.rst b/docs/source/tutorials/intro.rst
new file mode 100644
index 0000000000..287e4ca616
--- /dev/null
+++ b/docs/source/tutorials/intro.rst
@@ -0,0 +1,42 @@
+.. _Setup:
+
+Tutorial Setup
+==========================
+
+PCASpy Server
+---------------
+
+A `PCASpy `_ server provides PVs for the tutorial files to read/write.
+
+The server mimics some PVs of a motor and camera, and is located as follows:
+ .. code-block:: bash
+
+ examples/testing_ioc/pydm-tutorial-ioc
+
+Installing PCASpy from the documentation above and following the :ref:`pydm installation instructions` provides all needed prerequisites for this tutorial.
+
+Using the PCASpy Server
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. note::
+ You will need to export the following variable in each terminal that will run either the PCASpy server or pydm:
+ .. code-block:: bash
+
+ export EPICS_CA_MAX_ARRAY_BYTES=300000
+
+Run the server as follows:
+ .. code-block:: bash
+
+ ./examples/testing_ioc/pydm-tutorial-ioc
+
+In another terminal window, enable the sever's running state:
+ .. code-block:: bash
+
+ caput IOC:Run 1
+
+The server will now be running and the tutorial files can access the necessary PV's.
+
+In another (third) terminal window, the completed tutorial files can be ran as follows:
+ .. code-block:: bash
+
+ pydm .ui|.py
\ No newline at end of file
diff --git a/docs/source/tutorials/intro/channels.rst b/docs/source/tutorials/intro/channels.rst
new file mode 100644
index 0000000000..ed35b5a567
--- /dev/null
+++ b/docs/source/tutorials/intro/channels.rst
@@ -0,0 +1,35 @@
+.. _Channel:
+
+Channels
+========
+
+**Channels** are the bridge between PyDM Widgets and the different Data Plugins
+provided.
+
+Usually channels are specified in the following format:
+
+.. code-block:: python
+
+ ://
+
+Where ``protocol`` is a unique identifier for a given Data Plugin used with PyDM
+and ``channel address`` will vary depending on the data plugin. Every plugin
+should document the expected address format for users.
+
+Here are some examples:
+
+.. code-block:: python
+
+ ca://MTEST:Float
+
+Where ``ca`` means the Channel Access plugin and ``MTEST:Float`` is the PV name in this case.
+
+Another example is the Archiver Appliance plugin in which channels are specified as:
+
+.. code-block:: python
+
+ archiver://pv=test:pv:123&donotchunk
+
+In which everything in the ``channel address`` section is the same as what is sent
+to the ``retrieval`` part of the Archiver as specified at the **Retrieving data**
+**using other tools** section of the `Archiver Appliance User Guide `_
\ No newline at end of file
diff --git a/docs/source/tutorials/intro/data_arch.rst b/docs/source/tutorials/intro/data_arch.rst
new file mode 100644
index 0000000000..98879d6d63
--- /dev/null
+++ b/docs/source/tutorials/intro/data_arch.rst
@@ -0,0 +1,17 @@
+.. _DataArchitecture:
+
+Data Architecture
+=================
+
+* PyDM widgets are data-source agnostic, and communicate with the PyDM Application through Qt signals and slots.
+
+* The PyDM application routes data between the widgets and data source plugins.
+
+* A data source plugin speaks to a particular source of data (EPICS, HTTP, modbus, databases, etc).
+
+* This system makes it possible to mix-and-match different data sources within the same display, using the same widget set.
+
+.. figure:: /_static/tutorials/intro/architecture.png
+ :scale: 25 %
+ :align: center
+ :alt: PyDM Data Plugin Architecture
\ No newline at end of file
diff --git a/docs/source/tutorials/intro/datasource.rst b/docs/source/tutorials/intro/datasource.rst
new file mode 100644
index 0000000000..60b158ca14
--- /dev/null
+++ b/docs/source/tutorials/intro/datasource.rst
@@ -0,0 +1,60 @@
+Data Sources
+============
+
+PyDM is shipped with a couple of data plugins:
+
+- EPICS
+- Archiver
+
+EPICS
+-----
+
+This plugin is used to allow PyDM Widgets to access to Channel Access process variables and interact with them.
+
+======== ============== ================
+Protocol Address Format Example
+======== ============== ================
+ca ca:// ca://MTEST:Float
+======== ============== ================
+
+
+For this particular data plugin, three options are available for the Python interface to Channel Access.
+
+- PyEpics: http://pyepics.github.io/pyepics/
+
+- pyca: https://github.com/slaclab/pyca
+
+- caproto: https://nsls-ii.github.io/caproto/
+
+.. warning::
+ By default, PyEpics is selected unless configured through the ``PYDM_EPICS_LIB`` environment variable.
+
+ Possible values are ``PYEPICS``, ``PYCA``, and ``CAPROTO``.
+
+Archiver
+--------
+
+This plugin is used to allow PyDM Widgets to read data from Archiver Appliance.
+
+While still crude, this data plugin works and has been tested against a real instance of the Archiver.
+
+Basically a HTTP request is sent to the archiver appliance and the address is used to form the parameters for the data
+retrieval. Warning: The request is sent synchronously, meaning it will block your application until the request is
+complete. This limitation will be removed in a future version.
+
+======== =================== ====================================
+Protocol Address Format Example
+======== =================== ====================================
+archiver archiver:// archiver://pv=test:pv:123&donotchunk
+======== =================== ====================================
+
+.. warning::
+ Different facilities must set the ``PYDM_ARCHIVER_URL`` environment variable to point to their Archiver Appliance
+ location or the retrieval application. If not set, this variable will point to SLAC's Archiver Appliance URL, which
+ is almost certainly not what you want.
+
+ If your Archiver Retrieval is hosted under the following address ``http://lcls-archapp.slac.stanford.edu/retrieval/...``
+ set the variable to ``http://lcls-archapp.slac.stanford.edu``.
+
+ The remaining parts of the URL will be dealt with at the Data Plugin level and should not be a concern.
+
diff --git a/docs/source/tutorials/intro/launcher.rst b/docs/source/tutorials/intro/launcher.rst
new file mode 100644
index 0000000000..6d726d6d74
--- /dev/null
+++ b/docs/source/tutorials/intro/launcher.rst
@@ -0,0 +1,58 @@
+PyDM Launcher
+=============
+
+PyDM provides a command-line launcher that makes it easier for users to quickly run UI files as well as code based screens.
+The launcher is responsible for setting up the Python logging module but it mainly just parses the command line parameters
+and sends them to the instantiated PyDMApplication.
+
+The Launcher is available for Linux, OSX and Windows and it can be called using the command line:
+
+.. code-block:: bash
+
+ pydm
+
+This will result in the PyDM Main Window being displayed.
+
+.. figure:: /_static/tutorials/intro/main_window.png
+ :scale: 75 %
+ :align: center
+ :alt: PyDM Main Window
+
+Command Line Arguments
+----------------------
+
+The PyDM Launcher accepts many command line arguments, here they are:
+
+.. code-block:: bash
+
+ pydm [-h] [--perfmon] [--hide-nav-bar] [--hide-menu-bar]
+ [--hide-status-bar] [--fullscreen] [--read-only]
+ [--log_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--version]
+ [-m MACRO] [--stylesheet STYLESHEET]
+ [displayfile] ...
+
+
+Where:
+
+========================= =============================================================================
+Argument Description
+========================= =============================================================================
+-h, --help Show the help message and exit
+--perfmon Enable performance monitoring, and print CPU usage to the terminal
+--hide-nav-bar Start PyDM with the navigation bar hidden
+--hide-menu-bar Start PyDM with the menu bar hidden
+--hide-status-bar Start PyDM with the status bar hidden
+--fullscreen Start PyDM in full screen mode.
+--read-only Start PyDM in a Read-Only mode
+--log_level Configure the level of the display logger.
+--version Show PyDM's version number and exit
+-m MACRO, --macro MACRO Specify macro replacements to use, in JSON object format
+--stylesheet STYLESHEET Provide the full path to a CSS stylesheet file, which contains styles to be applied to specific Qt/PyDM widgets.
+displayfile (positional) A PyDM file to display. Can be either a Qt (.ui) file or a Python (.py) file
+display_args (positional) Arguments to be passed to the PyDM client application and displays.
+========================= =============================================================================
+
+.. note::
+ It is not mandatory to use the PyDM Launcher to run your screen, but keep in
+ mind that without it you will need to handle command line arguments, logging
+ setup, and the instantiation of the PyDMApplication in your own code.
\ No newline at end of file
diff --git a/docs/source/tutorials/intro/macros.rst b/docs/source/tutorials/intro/macros.rst
new file mode 100644
index 0000000000..806a656559
--- /dev/null
+++ b/docs/source/tutorials/intro/macros.rst
@@ -0,0 +1,45 @@
+.. _Macros:
+
+Macro Substitution
+==================
+
+PyDM has support for macro substitution, which is a way to make a .ui template
+for a display, and fill in variables in the template when the display is opened.
+
+The macro system is also a good way to supply data to python-based displays when
+launching them from the command line, related display button, or as an embedded
+display.
+
+Inserting Macro Variables
+-------------------------
+
+Anywhere in a .ui file, you can insert a macro of the following form: ``${variable}``.
+Note that Qt Designer will only let you use macros in string properties, but you
+can insert macros anywhere in a .ui file using a text editor.
+
+Replacing Macro Variables at Launch Time
+----------------------------------------
+
+When launching a .ui file which contains macro variables, specify values for each
+variable using the '-m' flag on the command line:
+
+.. code-block:: bash
+
+ python pydm.py -m 'variable1=value, variable2=another_value' my_file.ui
+
+Macros in Python-based Displays
+-------------------------------
+If you open a python file and specify macros (via the command line, related display
+button, or embedded display widget), the macros will be passed as a dictionary to
+the Display class initializer, where they can be accessed and used to generate the
+display.
+
+In addition, if the Display class specifies a .ui file to generate its user
+interface, macro substitution will occur inside the .ui file.
+
+Macro Behavior at Run Time
+--------------------------
+PyDM will remember the macros used to launch a display, and re-use them when
+navigating with the forward, back, and home buttons. When a new display is opened,
+any macros defined on the current window are also passed to the new display.
+This lets you cascade macros to child displays.
\ No newline at end of file
diff --git a/docs/source/tutorials/intro/widgets.rst b/docs/source/tutorials/intro/widgets.rst
new file mode 100644
index 0000000000..2b11c999c6
--- /dev/null
+++ b/docs/source/tutorials/intro/widgets.rst
@@ -0,0 +1,60 @@
+Widgets
+=======
+
+Organization
+------------
+
+PyDM Widgets are divided into 5 main categories:
+
+- Display Widgets
+ Widgets used for visualization of channels such as Label, Byte Indicator, Image Viewer, Symbol and others.
+
+- Input Widgets
+ Widgets used to interact with a channel and write to it, such as Line Edit, Push Button, Combo Box,
+ Checkbox, among others.
+
+- Plot Widgets
+ Widgets used for data visualization and plotting.
+
+ Currently we offer three plot types:
+
+ - Time Plot
+ Plot scalar values versus time.
+
+ - Waveform Plot
+ Plot waveform (array) values versus either array index, or another waveform (array) of the same length.
+
+ - Scatter Plot
+ Plot one scalar channel versus a different scalar channel, adding each new data point to a ring buffer.
+
+- Container Widgets
+ Widgets that group or wrap other widgets (like Frame, Tab Widget and Embedded Display) are part of this category.
+
+- Drawing Widgets
+ Widgets used to display static shapes such as Rectangle, Triangle, Circle, Image, among others.
+
+
+Common Properties
+-----------------
+
+All PyDM Widgets will have the same set of base properties. Not every widget uses all of them.
+
+As an example, the PyDMEnumComboBox makes no use of the ``precision`` part of the base properties.
+
+
+===================== ==== ========================================================================
+Property Type Description
+===================== ==== ========================================================================
+alarmSensitiveContent bool Whether or not the content will be affected by an alarm state
+alarmSensitiveBorder bool Whether or not the border will be affected by an alarm state
+precisionFromPV bool Whether or not to use the precision information from the PV
+precision int Precision to be used if precisionFromPV is ``False``
+showUnits bool Whether or not to display the engineering units information
+channel str The :ref:`Channel ` value for this widget
+===================== ==== ========================================================================
+
+Widget Set
+----------
+
+For a complete list of widgets as well as their user-level and API documentation please refer to the
+`PyDM Widgets `_ section of PyDM documentation.
diff --git a/docs/source/utilities/index.rst b/docs/source/utilities/index.rst
index a0e23566d6..cd7555fa76 100644
--- a/docs/source/utilities/index.rst
+++ b/docs/source/utilities/index.rst
@@ -10,9 +10,6 @@ General
.. automodule:: pydm.utilities
:members:
-.. automodule:: pydm.utilities.remove_protocol
- :members:
-
--------
Icons
--------
diff --git a/docs/source/widgets/PyDMWidget.rst b/docs/source/widgets/PyDMWidget.rst
index 3b5676b4bb..7c2122655d 100644
--- a/docs/source/widgets/PyDMWidget.rst
+++ b/docs/source/widgets/PyDMWidget.rst
@@ -7,7 +7,6 @@ API Documentation
.. autoclass:: pydm.widgets.base.PyDMWidget
:members:
- :inherited-members:
:show-inheritance:
PyDMToolTip Instructions
diff --git a/docs/source/widgets/analog_indicator.rst b/docs/source/widgets/analog_indicator.rst
index b70890e9e5..7e93b5f705 100644
--- a/docs/source/widgets/analog_indicator.rst
+++ b/docs/source/widgets/analog_indicator.rst
@@ -25,6 +25,7 @@ backgroundSizeRate = 0.4.
:align: center
* Suggested Orientations
+
Horizontal with value displayed on the right.
Vertical with value displayed on bottom.
@@ -39,6 +40,7 @@ There are a few methods of not drawing alarm regions.
1. Set the alarm to the corresponding limit. Do not set the alarm to outside of the limits, this will cause drawing errors.
2. Set userUpperMajorAlarm = userLowerMajorAlarm = 0. Or set userUpperMinorAlarm = userLowerMinorAlarm = 0.
3. If any alarm value is set to nan (not a number), those regions won't draw. Setting an alarm value to nan is not possible in designer.
+
.. figure:: /_static/widgets/analog_indicator/no_upper_minor.png
:scale: 100%
:align: center
diff --git a/docs/source/widgets/archiver_timeplot.rst b/docs/source/widgets/archiver_timeplot.rst
index f0de58f1d7..2a761fda81 100644
--- a/docs/source/widgets/archiver_timeplot.rst
+++ b/docs/source/widgets/archiver_timeplot.rst
@@ -46,7 +46,7 @@ be plotted as bars to show the full range of data represented by each point. As
of 365, a request for a year of data for a PV that updates every second would return roughly
365 points each of which will contain the min and max of that day's data to plot the full range represented.
-.. figure:: /_static/widgets/archiver_time_plot/archiver_time_plot.gif
+.. figure:: /_static/widgets/archiver_time_plot/archiver_plot.gif
:scale: 100 %
:align: center
:alt: Requesting additional data from a live plot
@@ -58,7 +58,6 @@ PyDMArchiverTimePlot
.. autoclass:: pydm.widgets.archiver_time_plot.PyDMArchiverTimePlot
:members:
- :inherited-members:
:show-inheritance:
#######################
@@ -67,5 +66,4 @@ ArchivePlotCurveItem
.. autoclass:: pydm.widgets.archiver_time_plot.ArchivePlotCurveItem
:members:
- :inherited-members:
:show-inheritance:
diff --git a/docs/source/widgets/byte.rst b/docs/source/widgets/byte.rst
index 708682f8ff..3ebc774741 100644
--- a/docs/source/widgets/byte.rst
+++ b/docs/source/widgets/byte.rst
@@ -4,5 +4,8 @@ PyDMByteIndicator
.. autoclass:: pydm.widgets.byte.PyDMByteIndicator
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QWidget Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/checkbox.rst b/docs/source/widgets/checkbox.rst
index 9249798754..154c963065 100644
--- a/docs/source/widgets/checkbox.rst
+++ b/docs/source/widgets/checkbox.rst
@@ -4,5 +4,7 @@ PyDMCheckbox
.. autoclass:: pydm.widgets.checkbox.PyDMCheckbox
:members:
- :inherited-members:
:show-inheritance:
+
+.. note::
+ See `QCheckBox Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/datetime_edit.rst b/docs/source/widgets/datetime_edit.rst
index 4fb3897034..c9a37958a9 100644
--- a/docs/source/widgets/datetime_edit.rst
+++ b/docs/source/widgets/datetime_edit.rst
@@ -4,5 +4,7 @@ PyDMDateTimeEdit
.. autoclass:: pydm.widgets.datetime.PyDMDateTimeEdit
:members:
- :inherited-members:
:show-inheritance:
+
+.. note::
+ See `QDateTimeEdit Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/datetime_label.rst b/docs/source/widgets/datetime_label.rst
index 6569f4daad..5b107e68bc 100644
--- a/docs/source/widgets/datetime_label.rst
+++ b/docs/source/widgets/datetime_label.rst
@@ -4,5 +4,8 @@ PyDMDateTimeLabel
.. autoclass:: pydm.widgets.datetime.PyDMDateTimeLabel
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QLabel Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/drawing.rst b/docs/source/widgets/drawing.rst
index 9d1fc17180..5cc4049771 100644
--- a/docs/source/widgets/drawing.rst
+++ b/docs/source/widgets/drawing.rst
@@ -4,61 +4,49 @@ PyDMDrawing Widgets
.. autoclass:: pydm.widgets.drawing.PyDMDrawingLine
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingImage
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingRectangle
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingTriangle
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingEllipse
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingCircle
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingArc
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingPie
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingChord
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingPolygon
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingPolyline
:members:
- :inherited-members:
:show-inheritance:
.. autoclass:: pydm.widgets.drawing.PyDMDrawingIrregularPolygon
:members:
- :inherited-members:
:show-inheritance:
diff --git a/docs/source/widgets/embedded_display.rst b/docs/source/widgets/embedded_display.rst
index c2f8df9500..7ab06390bc 100644
--- a/docs/source/widgets/embedded_display.rst
+++ b/docs/source/widgets/embedded_display.rst
@@ -4,5 +4,8 @@ PyDMEmbeddedDisplay
.. autoclass:: pydm.widgets.embedded_display.PyDMEmbeddedDisplay
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QFrame Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/enum_button.rst b/docs/source/widgets/enum_button.rst
index 9bda01f78f..251b9789ad 100644
--- a/docs/source/widgets/enum_button.rst
+++ b/docs/source/widgets/enum_button.rst
@@ -4,5 +4,7 @@ PyDMEnumButton
.. autoclass:: pydm.widgets.enum_button.PyDMEnumButton
:members:
- :inherited-members:
:show-inheritance:
+
+.. note::
+ See `QWidget Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/enum_combo_box.rst b/docs/source/widgets/enum_combo_box.rst
index 09fd4a1078..3afda73578 100644
--- a/docs/source/widgets/enum_combo_box.rst
+++ b/docs/source/widgets/enum_combo_box.rst
@@ -4,5 +4,8 @@ PyDMEnumComboBox
.. autoclass:: pydm.widgets.enum_combo_box.PyDMEnumComboBox
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QComboBox Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/event_plot.rst b/docs/source/widgets/event_plot.rst
new file mode 100644
index 0000000000..865853d923
--- /dev/null
+++ b/docs/source/widgets/event_plot.rst
@@ -0,0 +1,7 @@
+#######################
+PyDMEventPlot
+#######################
+
+.. autoclass:: pydm.widgets.eventplot.PyDMEventPlot
+ :members:
+ :show-inheritance:
diff --git a/docs/source/widgets/frame.rst b/docs/source/widgets/frame.rst
index 8a86b60952..7ca61cc067 100644
--- a/docs/source/widgets/frame.rst
+++ b/docs/source/widgets/frame.rst
@@ -4,5 +4,8 @@ PyDMFrame
.. autoclass:: pydm.widgets.frame.PyDMFrame
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QFrame Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/image.rst b/docs/source/widgets/image.rst
index 5a3f8d7b88..9cb07eee34 100644
--- a/docs/source/widgets/image.rst
+++ b/docs/source/widgets/image.rst
@@ -4,5 +4,8 @@ PyDMImageView
.. autoclass:: pydm.widgets.image.PyDMImageView
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `ImageView Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/index.rst b/docs/source/widgets/index.rst
index 1c1ccdd694..66e4abe488 100644
--- a/docs/source/widgets/index.rst
+++ b/docs/source/widgets/index.rst
@@ -17,6 +17,8 @@ Display Widgets
related_display_button.rst
scale.rst
symbol.rst
+ analog_indicator.rst
+ nt_table.rst
Input Widgets
-------------
@@ -42,6 +44,7 @@ Plot Widgets
curve_editor.rst
scatterplot.rst
timeplot.rst
+ event_plot.rst
archiver_timeplot.rst
waveformplot.rst
@@ -62,6 +65,13 @@ Drawing Widgets
drawing.rst
+Base Widgets
+------------
+.. toctree::
+ :maxdepth: 1
+
+ PyDMWidget.rst
+
Utilities
---------
diff --git a/docs/source/widgets/label.rst b/docs/source/widgets/label.rst
index d9024eb47c..68a9462010 100644
--- a/docs/source/widgets/label.rst
+++ b/docs/source/widgets/label.rst
@@ -4,5 +4,9 @@ PyDMLabel
.. autoclass:: pydm.widgets.label.PyDMLabel
:members:
- :inherited-members:
:show-inheritance:
+ :exclude-members: DisplayFormat
+
+
+.. note::
+ See `QLabel Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/line_edit.rst b/docs/source/widgets/line_edit.rst
index 64a7a742b6..e62190f7b2 100644
--- a/docs/source/widgets/line_edit.rst
+++ b/docs/source/widgets/line_edit.rst
@@ -4,5 +4,9 @@ PyDMLineEdit
.. autoclass:: pydm.widgets.line_edit.PyDMLineEdit
:members:
- :inherited-members:
:show-inheritance:
+ :exclude-members: DisplayFormat
+
+
+.. note::
+ See `QLineEdit Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/log.rst b/docs/source/widgets/log.rst
index a28b2e1243..fda6ae4bbc 100644
--- a/docs/source/widgets/log.rst
+++ b/docs/source/widgets/log.rst
@@ -4,5 +4,8 @@ PyDMLogDisplay
.. autoclass:: pydm.widgets.logdisplay.PyDMLogDisplay
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QWidget Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/nt_table.rst b/docs/source/widgets/nt_table.rst
index 4fc2fd9c6f..13831d330f 100644
--- a/docs/source/widgets/nt_table.rst
+++ b/docs/source/widgets/nt_table.rst
@@ -4,10 +4,8 @@ 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
diff --git a/docs/source/widgets/pushbutton.rst b/docs/source/widgets/pushbutton.rst
index 08c73ddf3d..1017ad92a8 100644
--- a/docs/source/widgets/pushbutton.rst
+++ b/docs/source/widgets/pushbutton.rst
@@ -4,6 +4,8 @@ PyDMPushButton
.. autoclass:: pydm.widgets.pushbutton.PyDMPushButton
:members:
- :inherited-members:
:show-inheritance:
+
+.. note::
+ See `QPushButton Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/related_display_button.rst b/docs/source/widgets/related_display_button.rst
index 02e057aa19..38dbb92b19 100644
--- a/docs/source/widgets/related_display_button.rst
+++ b/docs/source/widgets/related_display_button.rst
@@ -4,5 +4,8 @@ PyDMRelatedDisplayButton
.. autoclass:: pydm.widgets.related_display_button.PyDMRelatedDisplayButton
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QPushButton Documentation `_ for all inherited properties and methods.
diff --git a/docs/source/widgets/scale.rst b/docs/source/widgets/scale.rst
index 85cbc6e0e4..16a38029e1 100644
--- a/docs/source/widgets/scale.rst
+++ b/docs/source/widgets/scale.rst
@@ -4,5 +4,8 @@ PyDMScaleIndicator
.. autoclass:: pydm.widgets.scale.PyDMScaleIndicator
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QFrame Documentation `_ for all inherited properties and methods.
diff --git a/docs/source/widgets/scatterplot.rst b/docs/source/widgets/scatterplot.rst
index 0bef5f219c..a615d96e9b 100644
--- a/docs/source/widgets/scatterplot.rst
+++ b/docs/source/widgets/scatterplot.rst
@@ -4,7 +4,6 @@ PyDMScatterPlot
.. autoclass:: pydm.widgets.scatterplot.PyDMScatterPlot
:members:
- :inherited-members:
:show-inheritance:
#####################
@@ -13,5 +12,4 @@ ScatterPlotCurveItem
.. autoclass:: pydm.widgets.scatterplot.ScatterPlotCurveItem
:members:
- :inherited-members:
:show-inheritance:
diff --git a/docs/source/widgets/shell_command.rst b/docs/source/widgets/shell_command.rst
index 5c8cf41d8f..a2a6b12646 100644
--- a/docs/source/widgets/shell_command.rst
+++ b/docs/source/widgets/shell_command.rst
@@ -4,5 +4,8 @@ PyDMShellCommand
.. autoclass:: pydm.widgets.shell_command.PyDMShellCommand
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QPushButton Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/slider.rst b/docs/source/widgets/slider.rst
index 8f7b1c305d..3894e0e2bc 100644
--- a/docs/source/widgets/slider.rst
+++ b/docs/source/widgets/slider.rst
@@ -4,5 +4,8 @@ PyDMSlider
.. autoclass:: pydm.widgets.slider.PyDMSlider
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QFrame Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/spinbox.rst b/docs/source/widgets/spinbox.rst
index 6e929ee78a..9feae8efc3 100644
--- a/docs/source/widgets/spinbox.rst
+++ b/docs/source/widgets/spinbox.rst
@@ -4,5 +4,8 @@ PyDMSpinbox
.. autoclass:: pydm.widgets.spinbox.PyDMSpinbox
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QDoubleSpinBox Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/symbol.rst b/docs/source/widgets/symbol.rst
index ba6ac80c21..b7f08e9224 100644
--- a/docs/source/widgets/symbol.rst
+++ b/docs/source/widgets/symbol.rst
@@ -4,5 +4,8 @@ PyDMSymbol
.. autoclass:: pydm.widgets.symbol.PyDMSymbol
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QWidget Documentation `_ for all inherited properties and methods.
diff --git a/docs/source/widgets/tab_widget.rst b/docs/source/widgets/tab_widget.rst
index da585ca3fd..9c189a5a3a 100644
--- a/docs/source/widgets/tab_widget.rst
+++ b/docs/source/widgets/tab_widget.rst
@@ -33,5 +33,8 @@ API Documentation
.. autoclass:: pydm.widgets.tab_bar.PyDMTabWidget
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QTabWidget Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/template_repeater.rst b/docs/source/widgets/template_repeater.rst
index 6b2705a345..efdba7dfcb 100644
--- a/docs/source/widgets/template_repeater.rst
+++ b/docs/source/widgets/template_repeater.rst
@@ -4,5 +4,8 @@ PyDMTemplateRepeater
.. autoclass:: pydm.widgets.template_repeater.PyDMTemplateRepeater
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QFrame Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/docs/source/widgets/timeplot.rst b/docs/source/widgets/timeplot.rst
index db76e6253a..a26acb111b 100644
--- a/docs/source/widgets/timeplot.rst
+++ b/docs/source/widgets/timeplot.rst
@@ -4,7 +4,6 @@ PyDMTimePlot
.. autoclass:: pydm.widgets.timeplot.PyDMTimePlot
:members:
- :inherited-members:
:show-inheritance:
#######################
@@ -13,5 +12,4 @@ TimePlotCurveItem
.. autoclass:: pydm.widgets.timeplot.TimePlotCurveItem
:members:
- :inherited-members:
:show-inheritance:
diff --git a/docs/source/widgets/waveformplot.rst b/docs/source/widgets/waveformplot.rst
index 30469ed5e4..cb2fec83a5 100644
--- a/docs/source/widgets/waveformplot.rst
+++ b/docs/source/widgets/waveformplot.rst
@@ -4,7 +4,6 @@ PyDMWaveformPlot
.. autoclass:: pydm.widgets.waveformplot.PyDMWaveformPlot
:members:
- :inherited-members:
:show-inheritance:
#######################
@@ -13,5 +12,4 @@ WaveformCurveItem
.. autoclass:: pydm.widgets.waveformplot.WaveformCurveItem
:members:
- :inherited-members:
:show-inheritance:
diff --git a/docs/source/widgets/waveformtable.rst b/docs/source/widgets/waveformtable.rst
index e5f42d9ac7..07bdb96e97 100644
--- a/docs/source/widgets/waveformtable.rst
+++ b/docs/source/widgets/waveformtable.rst
@@ -4,5 +4,8 @@ PyDMWaveformTable
.. autoclass:: pydm.widgets.waveformtable.PyDMWaveformTable
:members:
- :inherited-members:
:show-inheritance:
+
+
+.. note::
+ See `QTableWidget Documentation `_ for all inherited properties and methods.
\ No newline at end of file
diff --git a/examples/accessory_window/accessory_window.py b/examples/accessory_window/accessory_window.py
index 1bfed42176..a8a00c866f 100644
--- a/examples/accessory_window/accessory_window.py
+++ b/examples/accessory_window/accessory_window.py
@@ -1,17 +1,30 @@
import os
import sys
from pydm import Display
-from qtpy.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QRadioButton
+from qtpy.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QPushButton
from qtpy.QtCore import Slot
+
class MyDisplay(Display):
def __init__(self, parent=None, args=[]):
super(MyDisplay, self).__init__(parent=parent, args=args)
- self.tracks = ["Humming", "Cowboys", "All Mine", "Mysterons", "Only You", "Half Day Closing", "Over", "Glory Box", "Sour Times", "Roads", "Strangers"]
+ self.tracks = [
+ "Humming",
+ "Cowboys",
+ "All Mine",
+ "Mysterons",
+ "Only You",
+ "Half Day Closing",
+ "Over",
+ "Glory Box",
+ "Sour Times",
+ "Roads",
+ "Strangers",
+ ]
self.numbers = range(len(self.tracks))
self.labels = [QLabel(str(i), parent=self) for i in self.numbers]
self.setup_ui()
-
+
def setup_ui(self):
self.setWindowTitle("Accessory Window Example")
main = QHBoxLayout()
@@ -25,24 +38,25 @@ def setup_ui(self):
main.addLayout(sub)
self.setLayout(main)
path_to_class = os.path.dirname(sys.modules[self.__module__].__file__)
- accessory_ui_path = os.path.join(path_to_class, 'settings_window.ui')
- self.accessory_window = Display(ui_filename=accessory_ui_path) #Note: purposefully not setting 'parent' on this - that way it shows up as its own window.
+ accessory_ui_path = os.path.join(path_to_class, "settings_window.ui")
+ self.accessory_window = Display(
+ ui_filename=accessory_ui_path
+ ) # Note: purposefully not setting 'parent' on this - that way it shows up as its own window.
self.accessory_window.ui.numbersButton.toggled.connect(self.show_numbers)
self.accessory_window.ui.tracksButton.toggled.connect(self.show_tracks)
-
-
+
@Slot()
def open_settings_window(self):
self.accessory_window.show()
-
+
@Slot(bool)
def show_numbers(self, toggled):
if toggled:
for number, label in zip(self.numbers, self.labels):
label.setText(str(number))
-
+
@Slot(bool)
def show_tracks(self, toggled):
if toggled:
for track, label in zip(self.tracks, self.labels):
- label.setText(track)
\ No newline at end of file
+ label.setText(track)
diff --git a/examples/archiver_time_plot/archiver_time_plot_example.py b/examples/archiver_time_plot/archiver_time_plot_example.py
new file mode 100644
index 0000000000..8a794a681c
--- /dev/null
+++ b/examples/archiver_time_plot/archiver_time_plot_example.py
@@ -0,0 +1,45 @@
+from pydm import Display
+
+from qtpy import QtCore
+from qtpy.QtWidgets import QHBoxLayout, QApplication
+from pydm.widgets import PyDMArchiverTimePlot
+
+
+class archiver_time_plot_example(Display):
+ def __init__(self, parent=None, args=None, macros=None):
+ super(archiver_time_plot_example, self).__init__(parent=parent, args=args, macros=None)
+ self.app = QApplication.instance()
+ self.setup_ui()
+
+ def minimumSizeHint(self):
+ return QtCore.QSize(100, 100)
+
+ def ui_filepath(self):
+ return None
+
+ def setup_ui(self):
+ self.main_layout = QHBoxLayout()
+ self.setLayout(self.main_layout)
+ self.plot_live = PyDMArchiverTimePlot(background=[255, 255, 255, 255])
+ self.plot_archived = PyDMArchiverTimePlot(background=[255, 255, 255, 255])
+ self.main_layout.addWidget(self.plot_live)
+ self.main_layout.addWidget(self.plot_archived)
+
+ self.plot_live.addYChannel(
+ y_channel="CA://XCOR:LI29:302:IACT",
+ name="name",
+ color="red",
+ yAxisName="Axis",
+ useArchiveData=True,
+ liveData=True
+ )
+
+ self.plot_archived.addYChannel(
+ y_channel="CA://XCOR:LI29:302:IACT",
+ name="name",
+ color="blue",
+ yAxisName="Axis",
+ useArchiveData=True,
+ liveData=False
+ )
+
\ No newline at end of file
diff --git a/examples/camviewer/camviewer.py b/examples/camviewer/camviewer.py
index d4c28f04d1..70213fa0cf 100644
--- a/examples/camviewer/camviewer.py
+++ b/examples/camviewer/camviewer.py
@@ -4,7 +4,6 @@
from qtpy.QtCore import Slot, Signal, QPointF, QRectF
from qtpy.QtGui import QPen
from qtpy.QtWidgets import QSizePolicy
-from os import path
from pydm import Display
from pydm.widgets.channel import PyDMChannel
from pydm.widgets.base import widget_destroyed
@@ -27,9 +26,17 @@ def __init__(self, parent=None, args=None):
super(CamViewer, self).__init__(parent=parent, args=args)
# Set up the list of cameras, and all the PVs
- test_dict = { "image": "ca://MTEST:Image", "max_width": "ca://MTEST:ImageWidth", "max_height": "ca://MTEST:ImageWidth", "roi_x": None, "roi_y": None, "roi_width": None, "roi_height": None }
+ test_dict = {
+ "image": "ca://MTEST:Image",
+ "max_width": "ca://MTEST:ImageWidth",
+ "max_height": "ca://MTEST:ImageWidth",
+ "roi_x": None,
+ "roi_y": None,
+ "roi_width": None,
+ "roi_height": None,
+ }
# self.cameras = { "VCC": vcc_dict, "C-Iris": c_iris_dict, "Test": test_dict }
- self.cameras = {"Testing IOC Image": test_dict }
+ self.cameras = {"Testing IOC Image": test_dict}
self._channels = []
self.imageChannel = None
@@ -67,7 +74,7 @@ def __init__(self, parent=None, args=None):
self.yLineoutPlot.setMaximumWidth(80)
self.yLineoutPlot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
self.yLineoutPlot.getPlotItem().invertY()
- self.yLineoutPlot.hideAxis('bottom')
+ self.yLineoutPlot.hideAxis("bottom")
# self.yLineoutPlot.setYLink(self.ui.imageView.getView())
self.ui.imageGridLayout.addWidget(self.yLineoutPlot, 0, 0)
self.yLineoutPlot.hide()
@@ -79,7 +86,7 @@ def __init__(self, parent=None, args=None):
self.xLineoutPlot = PlotWidget()
self.xLineoutPlot.setMaximumHeight(80)
self.xLineoutPlot.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
- self.xLineoutPlot.hideAxis('left')
+ self.xLineoutPlot.hideAxis("left")
# self.xLineoutPlot.setXLink(self.ui.imageView.getView())
self.ui.imageGridLayout.addWidget(self.xLineoutPlot, 1, 1)
self.xLineoutPlot.hide()
@@ -88,42 +95,42 @@ def __init__(self, parent=None, args=None):
self.ui.imageView.getView().sigRangeChanged.connect(self.updateLineoutRange)
# Instantiate markers.
- self.marker_dict = {1:{}, 2:{}, 3:{}, 4:{}}
- marker_size = QPointF(20., 20.)
- self.marker_dict[1]['marker'] = ImageMarker((0, 0), size=marker_size, pen=mkPen((100, 100, 255), width=5))
- self.marker_dict[1]['button'] = self.ui.marker1Button
- self.marker_dict[1]['xlineedit'] = self.ui.marker1XPosLineEdit
- self.marker_dict[1]['ylineedit'] = self.ui.marker1YPosLineEdit
-
- self.marker_dict[2]['marker'] = ImageMarker((0, 0), size=marker_size, pen=mkPen((255, 100, 100), width=5))
- self.marker_dict[2]['button'] = self.ui.marker2Button
- self.marker_dict[2]['xlineedit'] = self.ui.marker2XPosLineEdit
- self.marker_dict[2]['ylineedit'] = self.ui.marker2YPosLineEdit
-
- self.marker_dict[3]['marker'] = ImageMarker((0, 0), size=marker_size, pen=mkPen((60, 255, 60), width=5))
- self.marker_dict[3]['button'] = self.ui.marker3Button
- self.marker_dict[3]['xlineedit'] = self.ui.marker3XPosLineEdit
- self.marker_dict[3]['ylineedit'] = self.ui.marker3YPosLineEdit
-
- self.marker_dict[4]['marker'] = ImageMarker((0, 0), size=marker_size, pen=mkPen((255, 60, 255), width=5))
- self.marker_dict[4]['button'] = self.ui.marker4Button
- self.marker_dict[4]['xlineedit'] = self.ui.marker4XPosLineEdit
- self.marker_dict[4]['ylineedit'] = self.ui.marker4YPosLineEdit
+ self.marker_dict = {1: {}, 2: {}, 3: {}, 4: {}}
+ marker_size = QPointF(20.0, 20.0)
+ self.marker_dict[1]["marker"] = ImageMarker((0, 0), size=marker_size, pen=mkPen((100, 100, 255), width=5))
+ self.marker_dict[1]["button"] = self.ui.marker1Button
+ self.marker_dict[1]["xlineedit"] = self.ui.marker1XPosLineEdit
+ self.marker_dict[1]["ylineedit"] = self.ui.marker1YPosLineEdit
+
+ self.marker_dict[2]["marker"] = ImageMarker((0, 0), size=marker_size, pen=mkPen((255, 100, 100), width=5))
+ self.marker_dict[2]["button"] = self.ui.marker2Button
+ self.marker_dict[2]["xlineedit"] = self.ui.marker2XPosLineEdit
+ self.marker_dict[2]["ylineedit"] = self.ui.marker2YPosLineEdit
+
+ self.marker_dict[3]["marker"] = ImageMarker((0, 0), size=marker_size, pen=mkPen((60, 255, 60), width=5))
+ self.marker_dict[3]["button"] = self.ui.marker3Button
+ self.marker_dict[3]["xlineedit"] = self.ui.marker3XPosLineEdit
+ self.marker_dict[3]["ylineedit"] = self.ui.marker3YPosLineEdit
+
+ self.marker_dict[4]["marker"] = ImageMarker((0, 0), size=marker_size, pen=mkPen((255, 60, 255), width=5))
+ self.marker_dict[4]["button"] = self.ui.marker4Button
+ self.marker_dict[4]["xlineedit"] = self.ui.marker4XPosLineEdit
+ self.marker_dict[4]["ylineedit"] = self.ui.marker4YPosLineEdit
# Disable auto-ranging the image (it feels strange when the zoom changes as you move markers around.)
self.ui.imageView.getView().getViewBox().disableAutoRange()
for d in self.marker_dict:
- marker = self.marker_dict[d]['marker']
+ marker = self.marker_dict[d]["marker"]
marker.setZValue(20)
marker.hide()
marker.sigRegionChanged.connect(self.markerMoved)
self.ui.imageView.getView().getViewBox().addItem(marker)
- self.marker_dict[d]['button'].toggled.connect(self.enableMarker)
+ self.marker_dict[d]["button"].toggled.connect(self.enableMarker)
curvepen = QPen(marker.pen)
curvepen.setWidth(1)
- self.marker_dict[d]['xcurve'] = self.xLineoutPlot.plot(pen=curvepen)
- self.marker_dict[d]['ycurve'] = self.yLineoutPlot.plot(pen=curvepen)
- self.marker_dict[d]['xlineedit'].returnPressed.connect(self.markerPositionLineEditChanged)
- self.marker_dict[d]['ylineedit'].returnPressed.connect(self.markerPositionLineEditChanged)
+ self.marker_dict[d]["xcurve"] = self.xLineoutPlot.plot(pen=curvepen)
+ self.marker_dict[d]["ycurve"] = self.yLineoutPlot.plot(pen=curvepen)
+ self.marker_dict[d]["xlineedit"].returnPressed.connect(self.markerPositionLineEditChanged)
+ self.marker_dict[d]["ylineedit"].returnPressed.connect(self.markerPositionLineEditChanged)
# Set up zoom buttons
self.ui.zoomInButton.clicked.connect(self.zoomIn)
@@ -136,7 +143,6 @@ def __init__(self, parent=None, args=None):
self.destroyed.connect(functools.partial(widget_destroyed, self.channels))
-
@Slot()
def zoomIn(self):
self.ui.imageView.getView().getViewBox().scaleBy((0.5, 0.5))
@@ -149,26 +155,28 @@ def zoomOut(self):
def zoomToActualSize(self):
if len(self.image_data) == 0:
return
- self.ui.imageView.getView().setRange(xRange=(0, self.image_data.shape[0]), yRange=(0, self.image_data.shape[1]), padding=0.0)
+ self.ui.imageView.getView().setRange(
+ xRange=(0, self.image_data.shape[0]), yRange=(0, self.image_data.shape[1]), padding=0.0
+ )
def disable_all_markers(self):
for d in self.marker_dict:
- self.marker_dict[d]['button'].setChecked(False)
- self.marker_dict[d]['marker'].setPos((0, 0))
+ self.marker_dict[d]["button"].setChecked(False)
+ self.marker_dict[d]["marker"].setPos((0, 0))
@Slot(bool)
def enableMarker(self, checked):
any_markers_visible = False
for d in self.marker_dict:
- marker = self.marker_dict[d]['marker']
- button = self.marker_dict[d]['button']
+ marker = self.marker_dict[d]["marker"]
+ button = self.marker_dict[d]["button"]
any_markers_visible = any_markers_visible or button.isChecked()
marker.setVisible(button.isChecked())
self.markerMoved(d)
- self.marker_dict[d]['xcurve'].setVisible(button.isChecked())
- self.marker_dict[d]['ycurve'].setVisible(button.isChecked())
- self.marker_dict[d]['xlineedit'].setEnabled(button.isChecked())
- self.marker_dict[d]['ylineedit'].setEnabled(button.isChecked())
+ self.marker_dict[d]["xcurve"].setVisible(button.isChecked())
+ self.marker_dict[d]["ycurve"].setVisible(button.isChecked())
+ self.marker_dict[d]["xlineedit"].setEnabled(button.isChecked())
+ self.marker_dict[d]["ylineedit"].setEnabled(button.isChecked())
if any_markers_visible:
self.xLineoutPlot.show()
self.yLineoutPlot.show()
@@ -179,15 +187,15 @@ def enableMarker(self, checked):
@Slot()
def markerPositionLineEditChanged(self):
for d in self.marker_dict:
- marker = self.marker_dict[d]['marker']
- x_line_edit = self.marker_dict[d]['xlineedit']
- y_line_edit = self.marker_dict[d]['ylineedit']
+ marker = self.marker_dict[d]["marker"]
+ x_line_edit = self.marker_dict[d]["xlineedit"]
+ y_line_edit = self.marker_dict[d]["ylineedit"]
try:
new_x = int(x_line_edit.text())
new_y = int(y_line_edit.text())
if new_x <= marker.maxBounds.width() and new_y <= marker.maxBounds.height():
marker.setPos((new_x, new_y))
- except:
+ except Exception:
pass
coords = marker.getPixelCoords()
x_line_edit.setText(str(coords[0]))
@@ -197,9 +205,9 @@ def markerPositionLineEditChanged(self):
def markerMoved(self, marker):
self.updateLineouts()
for marker_index in self.marker_dict:
- marker = self.marker_dict[marker_index]['marker']
- x_line_edit = self.marker_dict[marker_index]['xlineedit']
- y_line_edit = self.marker_dict[marker_index]['ylineedit']
+ marker = self.marker_dict[marker_index]["marker"]
+ x_line_edit = self.marker_dict[marker_index]["xlineedit"]
+ y_line_edit = self.marker_dict[marker_index]["ylineedit"]
coords = marker.getPixelCoords()
x_line_edit.setText(str(coords[0]))
y_line_edit.setText(str(coords[1]))
@@ -211,9 +219,9 @@ def updateLineoutRange(self, view, new_ranges):
def updateLineouts(self):
for marker_index in self.marker_dict:
- marker = self.marker_dict[marker_index]['marker']
- xcurve = self.marker_dict[marker_index]['xcurve']
- ycurve = self.marker_dict[marker_index]['ycurve']
+ marker = self.marker_dict[marker_index]["marker"]
+ xcurve = self.marker_dict[marker_index]["xcurve"]
+ ycurve = self.marker_dict[marker_index]["ycurve"]
if marker.isVisible():
result, coords = marker.getArrayRegion(self.image_data, self.ui.imageView.getImageItem())
xcurve.setData(y=result[0], x=np.arange(len(result[0])))
@@ -258,15 +266,35 @@ def initializeCamera(self, new_camera):
self.roiWidthChannel = self.cameras[new_camera]["roi_width"]
self.roiHeightChannel = self.cameras[new_camera]["roi_height"]
- self._channels = [PyDMChannel(address=self.imageChannel, connection_slot=self.connectionStateChanged, value_slot=self.receiveImageWaveform, severity_slot=self.alarmSeverityChanged),
- PyDMChannel(address=self.widthChannel, value_slot=self.receiveImageWidth),
- PyDMChannel(address=self.maxWidthChannel, value_slot=self.receiveMaxWidth),
- PyDMChannel(address=self.maxHeightChannel, value_slot=self.receiveMaxHeight)]
+ self._channels = [
+ PyDMChannel(
+ address=self.imageChannel,
+ connection_slot=self.connectionStateChanged,
+ value_slot=self.receiveImageWaveform,
+ severity_slot=self.alarmSeverityChanged,
+ ),
+ PyDMChannel(address=self.widthChannel, value_slot=self.receiveImageWidth),
+ PyDMChannel(address=self.maxWidthChannel, value_slot=self.receiveMaxWidth),
+ PyDMChannel(address=self.maxHeightChannel, value_slot=self.receiveMaxHeight),
+ ]
if self.roiXChannel and self.roiYChannel and self.roiWidthChannel and self.roiHeightChannel:
- self._channels.extend([PyDMChannel(address=self.roiXChannel, value_slot=self.receiveRoiX, value_signal=self.roi_x_signal, write_access_slot=self.roiWriteAccessChanged),
- PyDMChannel(address=self.roiYChannel, value_slot=self.receiveRoiY, value_signal=self.roi_y_signal),
- PyDMChannel(address=self.roiWidthChannel, value_slot=self.receiveRoiWidth, value_signal=self.roi_w_signal),
- PyDMChannel(address=self.roiHeightChannel, value_slot=self.receiveRoiHeight, value_signal=self.roi_h_signal)])
+ self._channels.extend(
+ [
+ PyDMChannel(
+ address=self.roiXChannel,
+ value_slot=self.receiveRoiX,
+ value_signal=self.roi_x_signal,
+ write_access_slot=self.roiWriteAccessChanged,
+ ),
+ PyDMChannel(address=self.roiYChannel, value_slot=self.receiveRoiY, value_signal=self.roi_y_signal),
+ PyDMChannel(
+ address=self.roiWidthChannel, value_slot=self.receiveRoiWidth, value_signal=self.roi_w_signal
+ ),
+ PyDMChannel(
+ address=self.roiHeightChannel, value_slot=self.receiveRoiHeight, value_signal=self.roi_h_signal
+ ),
+ ]
+ )
self.ui.roiXLineEdit.setEnabled(True)
self.ui.roiYLineEdit.setEnabled(True)
self.ui.roiWLineEdit.setEnabled(True)
@@ -313,7 +341,7 @@ def configureColorMapLimitSliders(self, max_int):
def colorMapMinLineEditChanged(self):
try:
new_min = int(self.ui.colorMapMinLineEdit.text())
- except:
+ except Exception:
self.ui.colorMapMinLineEdit.setText(str(self.ui.colorMapMinSlider.value()))
return
if new_min < 0:
@@ -334,7 +362,7 @@ def setColorMapMin(self, new_min):
def colorMapMaxLineEditChanged(self):
try:
new_max = int(self.ui.colorMapMaxLineEdit.text())
- except:
+ except Exception:
self.ui.colorMapMaxLineEdit.setText(str(self.ui.colorMapMaxSlider.value()))
return
if new_max < 0:
@@ -355,7 +383,7 @@ def createAverageBuffer(self, size, type, initial_val=[]):
num_shots = 1
try:
num_shots = int(self.ui.numShotsLineEdit.text())
- except:
+ except Exception:
self.ui.numShotsLineEdit.setText(str(num_shots))
if num_shots < 1:
num_shots = 1
@@ -402,16 +430,16 @@ def receiveImageWaveform(self, new_waveform):
# self._average_buffer = np.roll(self._average_buffer, 1, axis=0)
self._average_buffer[self._average_counter] = new_waveform
mean = np.mean(self._average_buffer, axis=0).astype(new_waveform.dtype)
- self.image_data = mean.reshape((int(self.image_width), -1), order='F')
+ self.image_data = mean.reshape((int(self.image_width), -1), order="F")
else:
- self.image_data = new_waveform.reshape((int(self.image_width), -1), order='F')
+ self.image_data = new_waveform.reshape((int(self.image_width), -1), order="F")
self.setMarkerBounds()
self.updateLineouts()
self.ui.imageView.image_value_changed(self.image_data)
self.calculateStats()
if self._needs_auto_range:
self.ui.imageView.getView().autoRange(padding=0.0)
- current_range = self.ui.imageView.getView().viewRange()
+ self.ui.imageView.getView().viewRange()
self._needs_auto_range = False
def calculateStats(self):
@@ -422,7 +450,11 @@ def calculateStats(self):
height = self.image_data.shape[1]
min_val = np.min(self.image_data)
max_val = np.max(self.image_data)
- self.ui.imageStatsLabel.setText("Mean: {0:.2f}, Std: {1:.2f}, Min: {2}, Max: {3}, Width: {4}, Height: {5}".format(mean, std, min_val, max_val, width, height))
+ self.ui.imageStatsLabel.setText(
+ "Mean: {0:.2f}, Std: {1:.2f}, Min: {2}, Max: {3}, Width: {4}, Height: {5}".format(
+ mean, std, min_val, max_val, width, height
+ )
+ )
# Current view stats
current_range = self.ui.imageView.getView().viewRange()
view_x_min = int(max(0, current_range[0][0]))
@@ -436,12 +468,18 @@ def calculateStats(self):
height = view_slice.shape[1]
min_val = np.min(view_slice)
max_val = np.max(view_slice)
- self.ui.viewStatsLabel.setText("Mean: {0:.2f}, Std: {1:.2f}, Min: {2}, Max: {3}, Width: {4}, Height: {5}".format(mean, std, min_val, max_val, width, height))
+ self.ui.viewStatsLabel.setText(
+ "Mean: {0:.2f}, Std: {1:.2f}, Min: {2}, Max: {3}, Width: {4}, Height: {5}".format(
+ mean, std, min_val, max_val, width, height
+ )
+ )
def setMarkerBounds(self):
for marker_index in self.marker_dict:
- marker = self.marker_dict[marker_index]['marker']
- marker.maxBounds = QRectF(0, 0, self.image_data.shape[0] + marker.size()[0] - 1, self.image_data.shape[1] + marker.size()[1] - 1)
+ marker = self.marker_dict[marker_index]["marker"]
+ marker.maxBounds = QRectF(
+ 0, 0, self.image_data.shape[0] + marker.size()[0] - 1, self.image_data.shape[1] + marker.size()[1] - 1
+ )
@Slot(int)
def receiveImageWidth(self, new_width):
@@ -501,7 +539,7 @@ def connectionStateChanged(self, connected):
self.ui.connectedLabel.setText({True: "Yes", False: "No"}[connected])
def ui_filename(self):
- return 'camviewer.ui'
+ return "camviewer.ui"
def channels(self):
return self._channels
diff --git a/examples/camviewer/marker.py b/examples/camviewer/marker.py
index 9ce9878595..753bbf4887 100644
--- a/examples/camviewer/marker.py
+++ b/examples/camviewer/marker.py
@@ -6,10 +6,10 @@ class ImageMarker(ROI):
"""A crosshair ROI. Returns the full image line-out for X and Y at the position of the crosshair."""
def __init__(self, pos=None, size=None, **kargs):
- if size == None:
+ if size is None:
# size = [100e-6,100e-6]
size = [20, 20]
- if pos == None:
+ if pos is None:
pos = [0, 0]
self._shape = None
ROI.__init__(self, pos, size, **kargs)
@@ -37,7 +37,7 @@ def getPixelCoords(self):
def shape(self):
if self._shape is None:
- radius = self.getState()['size'][1]
+ radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
@@ -51,7 +51,7 @@ def shape(self):
return self._shape
def paint(self, p, *args):
- radius = self.getState()['size'][1]
+ radius = self.getState()["size"][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
p.drawLine(Point(0, -radius), Point(0, radius))
diff --git a/examples/code_only/code_only.py b/examples/code_only/code_only.py
index 914f8a2563..409d61bf99 100644
--- a/examples/code_only/code_only.py
+++ b/examples/code_only/code_only.py
@@ -1,11 +1,12 @@
from pydm import Display
from qtpy.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout
+
class MyDisplay(Display):
def __init__(self, parent=None, args=[]):
super(MyDisplay, self).__init__(parent=parent, args=args)
self.setup_ui()
-
+
def setup_ui(self):
main = QHBoxLayout()
sub = QVBoxLayout()
@@ -13,9 +14,9 @@ def setup_ui(self):
sub.addWidget(QLabel(str(i)))
main.addLayout(sub)
self.setLayout(main)
-
+
def ui_filename(self):
return None
-
+
def ui_filepath(self):
return None
diff --git a/examples/drawing/drawing_demo.ui b/examples/drawing/drawing_demo.ui
index 3812705c0a..c7d145ae41 100644
--- a/examples/drawing/drawing_demo.ui
+++ b/examples/drawing/drawing_demo.ui
@@ -516,6 +516,41 @@
5
+
+
+
+ 290
+ 530
+ 101
+ 131
+
+
+
+
+
+
+
+ 60.0, 5.0
+ 5.0, 50.0
+ 25.0, 50.0
+ 25.0, 100.0
+ 50.0, 100.0
+
+
+
+
+
+
+ 280
+ 510
+ 151
+ 16
+
+
+
+ PyDMDrawingPolyline
+
+
@@ -568,6 +603,11 @@
QWidgetpydm.widgets.drawing
+
+ PyDMDrawingPolyline
+ QWidget
+ pydm.widgets.drawing
+
diff --git a/examples/exception/demo.py b/examples/exception/demo.py
index 34f9db09b7..7d6b63fcc4 100644
--- a/examples/exception/demo.py
+++ b/examples/exception/demo.py
@@ -1,6 +1,6 @@
import sys
import functools
-from qtpy import QtCore, QtGui, QtWidgets
+from qtpy import QtWidgets
from pydm import exception
@@ -36,11 +36,13 @@ def setup_handler(self, install=True):
def raise_exception(self):
raise Exception("This is an exception being raised...")
+
def main():
app = QtWidgets.QApplication(sys.argv)
screen = Screen()
screen.show()
sys.exit(app.exec_())
-if __name__ == '__main__':
- main()
\ No newline at end of file
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/external_tool/dummy_tool.py b/examples/external_tool/dummy_tool.py
index 8fecbcc037..3007226394 100644
--- a/examples/external_tool/dummy_tool.py
+++ b/examples/external_tool/dummy_tool.py
@@ -3,18 +3,12 @@
class DummyTool(ExternalTool):
-
def __init__(self):
icon = IconFont().icon("cogs")
name = "Dummy Tool"
group = "Example"
use_with_widgets = False
- super().__init__(
- icon=icon,
- name=name,
- group=group,
- use_with_widgets=use_with_widgets
- )
+ super().__init__(icon=icon, name=name, group=group, use_with_widgets=use_with_widgets)
def call(self, channels, sender):
print("Called Dummy Tool from: {} with:".format(sender))
@@ -29,23 +23,17 @@ def from_json(self, content):
def get_info(self):
ret = ExternalTool.get_info(self)
- ret.update({'file': __file__})
+ ret.update({"file": __file__})
return ret
class DummyTool3(ExternalTool):
-
def __init__(self):
icon = None
name = "Dummy Tool 3"
group = ""
use_with_widgets = False
- super().__init__(
- icon=icon,
- name=name,
- group=group,
- use_with_widgets=use_with_widgets
- )
+ super().__init__(icon=icon, name=name, group=group, use_with_widgets=use_with_widgets)
def call(self, channels, sender):
print("Called Dummy Tool 3 from: {} with:".format(sender))
@@ -60,5 +48,5 @@ def from_json(self, content):
def get_info(self):
ret = ExternalTool.get_info(self)
- ret.update({'file': __file__})
+ ret.update({"file": __file__})
return ret
diff --git a/examples/external_tool/lookup_path/new_tool.py b/examples/external_tool/lookup_path/new_tool.py
index 18d4d0f0f1..6c62fe9c96 100644
--- a/examples/external_tool/lookup_path/new_tool.py
+++ b/examples/external_tool/lookup_path/new_tool.py
@@ -3,7 +3,6 @@
class DummyTool2(ExternalTool):
-
def __init__(self):
icon = IconFont().icon("code")
name = "Dummy Tool 2"
@@ -31,5 +30,5 @@ def from_json(self, content):
def get_info(self):
ret = ExternalTool.get_info(self)
- ret.update({'file': __file__})
+ ret.update({"file": __file__})
return ret
diff --git a/examples/external_tool/lookup_path/root_tool.py b/examples/external_tool/lookup_path/root_tool.py
index 341d8779f3..961e02faa0 100644
--- a/examples/external_tool/lookup_path/root_tool.py
+++ b/examples/external_tool/lookup_path/root_tool.py
@@ -3,18 +3,12 @@
class RootTool(ExternalTool):
-
def __init__(self):
icon = IconFont().icon("code")
name = "Root Tool"
group = ""
use_with_widgets = True
- super().__init__(
- icon=icon,
- name=name,
- group=group,
- use_with_widgets=use_with_widgets
- )
+ super().__init__(icon=icon, name=name, group=group, use_with_widgets=use_with_widgets)
def call(self, channels, sender):
print("Called Root Tool from: {} with:".format(sender))
@@ -29,5 +23,5 @@ def from_json(self, content):
def get_info(self):
ret = ExternalTool.get_info(self)
- ret.update({'file': __file__})
+ ret.update({"file": __file__})
return ret
diff --git a/examples/icons/icons.ui b/examples/icons/icons.ui
new file mode 100644
index 0000000000..ee9c038ea6
--- /dev/null
+++ b/examples/icons/icons.ui
@@ -0,0 +1,403 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 510
+ 400
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 500
+ 400
+
+
+
+ Form
+
+
+
+
+
+ <html><head/><body><p>The PyDMIcon property allows for specifying icons in Designer from provided cross-platform icon sets.</p><p>Icons can be used from both Qt's standard icon set (https://doc.qt.io/qt-6/qstyle.html#StandardPixmap-enum) and the "Font Awesome" icon set (https://fontawesome.com/icons?d=gallery).</p></body></html>
+
+
+ Qt::RichText
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+ QFrame with support for alarms
+ This class inherits from QFrame and PyDMWidget.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ true
+
+
+ ca://MTEST:Run
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+
+
+ This button uses an icon from Qt's standard icon set.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This button uses a standard icon.
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://MTEST:Run
+
+
+ SP_ArrowUp
+
+
+
+ 170
+ 0
+ 127
+
+
+
+ false
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ 1
+
+
+ None
+
+
+ false
+
+
+ false
+
+
+ eye-slash
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QFrame with support for alarms
+ This class inherits from QFrame and PyDMWidget.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ false
+
+
+ true
+
+
+ ca://MTEST:Run
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+
+
+ This button uses an icon from the "Font Awesome" icon set.
+
+
+
+
+
+
+
+
+
+
+
+
+ This button uses a "Font Awesome" icon.
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://MTEST:Run
+
+
+ arrow-up
+
+
+ false
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ 0
+
+
+ None
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QFrame with support for alarms
+ This class inherits from QFrame and PyDMWidget.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ true
+
+
+ ca://MTEST:Run
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+
+
+ The color of icons from the "Font Awesome" set is configurable. (this can't be done with Qt standard icons since they are already multi-colored)
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ This button uses a colored "Font Awesome" icon.
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://MTEST:Run
+
+
+ arrow-up
+
+
+
+ 255
+ 0
+ 0
+
+
+
+ false
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ 0
+
+
+ None
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+ PyDMFrame
+ QFrame
+ pydm.widgets.frame
+ 1
+
+
+ PyDMPushButton
+ QPushButton
+ pydm.widgets.pushbutton
+
+
+
+
+
diff --git a/examples/image_processing/image_view.py b/examples/image_processing/image_view.py
index 1a7d916721..3a23e7bfeb 100644
--- a/examples/image_processing/image_view.py
+++ b/examples/image_processing/image_view.py
@@ -1,13 +1,11 @@
from pydm import Display
import threading
-from os import path
from skimage.feature import blob_doh
from marker import ImageMarker
-from pyqtgraph import mkPen, PlotItem
+from pyqtgraph import mkPen
class ImageViewer(Display):
-
def __init__(self, parent=None, args=None):
super(ImageViewer, self).__init__(parent=parent, args=args)
self.markers_lock = threading.Lock()
@@ -17,7 +15,7 @@ def __init__(self, parent=None, args=None):
self.blobs = list()
def ui_filename(self):
- return 'image_view.ui'
+ return "image_view.ui"
def draw_markers(self, *args, **kwargs):
with self.markers_lock:
@@ -37,7 +35,7 @@ def draw_markers(self, *args, **kwargs):
def process_image(self, new_image):
# Find blobs in the image with scikit-image
- self.blobs = blob_doh(new_image, max_sigma=512, min_sigma=64, threshold=.02)
+ self.blobs = blob_doh(new_image, max_sigma=512, min_sigma=64, threshold=0.02)
# Send the original image data to the image widget
return new_image
diff --git a/examples/image_processing/marker.py b/examples/image_processing/marker.py
index 797efaa82f..753bbf4887 100644
--- a/examples/image_processing/marker.py
+++ b/examples/image_processing/marker.py
@@ -1,44 +1,43 @@
-from qtpy import QtCore, QtGui
+from qtpy import QtGui
from pyqtgraph import Point, ROI
-from pyqtgraph import functions as fn
-import numpy as np
+
class ImageMarker(ROI):
"""A crosshair ROI. Returns the full image line-out for X and Y at the position of the crosshair."""
def __init__(self, pos=None, size=None, **kargs):
- if size == None:
- #size = [100e-6,100e-6]
- size=[20,20]
- if pos == None:
- pos = [0,0]
+ if size is None:
+ # size = [100e-6,100e-6]
+ size = [20, 20]
+ if pos is None:
+ pos = [0, 0]
self._shape = None
ROI.__init__(self, pos, size, **kargs)
-
+
self.sigRegionChanged.connect(self.invalidate)
self.aspectLocked = True
def invalidate(self):
self._shape = None
self.prepareGeometryChange()
-
+
def boundingRect(self):
return self.shape().boundingRect()
-
- def getArrayRegion(self, data, img, axes=(0,1)):
- #img_point = self.mapToItem(img, self.pos())
+
+ def getArrayRegion(self, data, img, axes=(0, 1)):
+ # img_point = self.mapToItem(img, self.pos())
coords = self.getPixelCoords()
- ystrip = data[coords[0],:]
- xstrip = data[:,coords[1]]
- return ([xstrip,ystrip], coords)
-
+ ystrip = data[coords[0], :]
+ xstrip = data[:, coords[1]]
+ return ([xstrip, ystrip], coords)
+
def getPixelCoords(self):
img_point = self.pos()
return (int(img_point.x()), int(img_point.y()))
-
+
def shape(self):
if self._shape is None:
- radius = self.getState()['size'][1]
+ radius = self.getState()["size"][1]
p = QtGui.QPainterPath()
p.moveTo(Point(0, -radius))
p.lineTo(Point(0, radius))
@@ -50,10 +49,10 @@ def shape(self):
outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline)
return self._shape
-
+
def paint(self, p, *args):
- radius = self.getState()['size'][1]
+ radius = self.getState()["size"][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
- p.setPen(self.currentPen)
+ p.setPen(self.currentPen)
p.drawLine(Point(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0))
diff --git a/examples/macros/basics/readme.txt b/examples/macros/basics/readme.txt
index ea4da51485..2f4b4e3ae5 100644
--- a/examples/macros/basics/readme.txt
+++ b/examples/macros/basics/readme.txt
@@ -1,4 +1,4 @@
-This example illustrates the basics of using macro variables with .ui files.
+This example illustrates the basics of using macro variables with .ui files.
Load either the 'related_display_with_macros.ui' file or the 'embedded_display_with_macro.ui'
file to see examples of using the related display button or embedded display widget to pass
- macro variables to a .ui file used as a template (called macro_pv.ui, in these examples).
\ No newline at end of file
+ macro variables to a .ui file used as a template (called macro_pv.ui, in these examples).
diff --git a/examples/macros/macros_and_python/macro_addition.py b/examples/macros/macros_and_python/macro_addition.py
index e4b5e99d46..3a3653af41 100644
--- a/examples/macros/macros_and_python/macro_addition.py
+++ b/examples/macros/macros_and_python/macro_addition.py
@@ -1,10 +1,10 @@
-from os import path
from pydm import Display
+
class MacroAddition(Display):
def __init__(self, parent=None, args=None, macros=None):
super(MacroAddition, self).__init__(parent=parent, macros=macros)
- self.ui.resultLabel.setText("{}".format(float(macros['a']) + float(macros['b'])))
-
+ self.ui.resultLabel.setText("{}".format(float(macros["a"]) + float(macros["b"])))
+
def ui_filename(self):
- return 'macro_addition.ui'
+ return "macro_addition.ui"
diff --git a/examples/macros/macros_and_python/readme.txt b/examples/macros/macros_and_python/readme.txt
index 7ae8a988fd..6ae9d1505b 100644
--- a/examples/macros/macros_and_python/readme.txt
+++ b/examples/macros/macros_and_python/readme.txt
@@ -14,4 +14,4 @@ cascades macros from one display to the next when a related display button
is clicked.
To run the .ui file, supply one macro variable, "a":
-python pydm.py -m '{"a": 3}' examples/macros/macros_and_python/macros_to_python_displays.ui
\ No newline at end of file
+python pydm.py -m '{"a": 3}' examples/macros/macros_and_python/macros_to_python_displays.ui
diff --git a/examples/macros/nested_embedded_windows/macro_addition.py b/examples/macros/nested_embedded_windows/macro_addition.py
index 22fcd42e9b..3a3653af41 100644
--- a/examples/macros/nested_embedded_windows/macro_addition.py
+++ b/examples/macros/nested_embedded_windows/macro_addition.py
@@ -1,10 +1,10 @@
-from os import path
from pydm import Display
+
class MacroAddition(Display):
def __init__(self, parent=None, args=None, macros=None):
super(MacroAddition, self).__init__(parent=parent, macros=macros)
- self.ui.resultLabel.setText("{}".format(float(macros['a']) + float(macros['b'])))
-
+ self.ui.resultLabel.setText("{}".format(float(macros["a"]) + float(macros["b"])))
+
def ui_filename(self):
- return 'macro_addition.ui'
\ No newline at end of file
+ return "macro_addition.ui"
diff --git a/examples/macros/nested_embedded_windows/readme.txt b/examples/macros/nested_embedded_windows/readme.txt
index 2c811de892..091fd97f7b 100644
--- a/examples/macros/nested_embedded_windows/readme.txt
+++ b/examples/macros/nested_embedded_windows/readme.txt
@@ -1,7 +1,7 @@
This example demonstrates how macro variables are cascaded through
-nested embedded displays. The first embedded display defines a
+nested embedded displays. The first embedded display defines a
variable "a", which is sent to a second embedded display, which
defines another variable "b". The second display embeds a python
based display which shows the sum of the macro variables "a" and "b".
-To run this example, open 'nested_embedded_windows.ui'.
\ No newline at end of file
+To run this example, open 'nested_embedded_windows.ui'.
diff --git a/examples/positioner/cams.py b/examples/positioner/cams.py
index 5708422a22..757c2ca974 100644
--- a/examples/positioner/cams.py
+++ b/examples/positioner/cams.py
@@ -1,35 +1,35 @@
import numpy as np
-#Cam Geometry
+# Cam Geometry
R = 31
L = 1.5875
S1 = 34.925
S2 = 290.5
c = 282.0416
-b = c + S1 - np.sqrt(2)*R
+b = c + S1 - np.sqrt(2) * R
a = 139.6238
dx = 74.55
dy = 245.47
-#Silence invalid arcsin arguments, we deal with that ourselves.
-np.seterr(invalid='ignore')
+# Silence invalid arcsin arguments, we deal with that ourselves.
+np.seterr(invalid="ignore")
+
def real2cams(coords):
- (x,y,theta) = coords
- theta = theta * 1.e-3 #Convert to rad from mrad
-
- x1 = x + (a*np.cos(theta)) + (b*np.sin(theta)) - a
- y1 = y - (b*np.cos(theta)) + (a*np.sin(theta)) + c
- bp = (np.cos(theta) + np.sin(theta)) / np.sqrt(2.)
- bm = (np.cos(theta) - np.sin(theta)) / np.sqrt(2.)
-
- p1 = theta - np.arcsin((1/L)*(((x1+S2)*np.sin(theta))-(y1*np.cos(theta))+(c-b)))
- p2 = theta - np.arcsin((1/L)*(((x1+S1)*bm)+(y1*bp)-R))
- p3 = theta - np.arcsin((1/L)*(((x1-S1)*bp)-(y1*bm)+R))
-
- valid = False
- if np.all(np.isreal([p1,p2,p3])) and not np.any(np.isnan([p1,p2,p3])):
- valid = True
-
- return (p1*180./np.pi, p2*180./np.pi, p3*180./np.pi, valid)
-
+ (x, y, theta) = coords
+ theta = theta * 1.0e-3 # Convert to rad from mrad
+
+ x1 = x + (a * np.cos(theta)) + (b * np.sin(theta)) - a
+ y1 = y - (b * np.cos(theta)) + (a * np.sin(theta)) + c
+ bp = (np.cos(theta) + np.sin(theta)) / np.sqrt(2.0)
+ bm = (np.cos(theta) - np.sin(theta)) / np.sqrt(2.0)
+
+ p1 = theta - np.arcsin((1 / L) * (((x1 + S2) * np.sin(theta)) - (y1 * np.cos(theta)) + (c - b)))
+ p2 = theta - np.arcsin((1 / L) * (((x1 + S1) * bm) + (y1 * bp) - R))
+ p3 = theta - np.arcsin((1 / L) * (((x1 - S1) * bp) - (y1 * bm) + R))
+
+ valid = False
+ if np.all(np.isreal([p1, p2, p3])) and not np.any(np.isnan([p1, p2, p3])):
+ valid = True
+
+ return (p1 * 180.0 / np.pi, p2 * 180.0 / np.pi, p3 * 180.0 / np.pi, valid)
diff --git a/examples/positioner/positioner_ioc.py b/examples/positioner/positioner_ioc.py
index 906039e942..21eed0f1ed 100644
--- a/examples/positioner/positioner_ioc.py
+++ b/examples/positioner/positioner_ioc.py
@@ -1,70 +1,61 @@
from pcaspy import SimpleServer, Driver, Severity
-import time
import numpy
-prefix = 'MOTOR:'
+prefix = "MOTOR:"
pvdb = {
- '1:VAL': {
- 'value': 0.0,
- 'prec': 2,
- 'hilim' : 180,
- 'hihi' : 140,
- 'high' : 100,
- 'low' : -100,
- 'lolo' : -140,
- 'lolim' : -180,
- 'unit' : 'deg'
- },
- '1:STR': {
- 'type': 'string',
- 'value': 'Hello'
- },
- '1:INT': {
- 'type': 'int',
- 'value': 5
- },
- '2:VAL': {
- 'value': 0.0,
- 'prec': 2,
- 'hilim' : 180,
- 'hihi' : 140,
- 'high' : 100,
- 'low' : -100,
- 'lolo' : -140,
- 'lolim' : -180,
- 'unit' : 'deg'
- },
- '3:VAL': {
- 'value': 0.0,
- 'prec': 2,
- 'hilim' : 180,
- 'hihi' : 140,
- 'high' : 100,
- 'low' : -100,
- 'lolo' : -140,
- 'lolim' : -180,
- 'unit' : 'deg'
- },
- 'STATUS': {
- 'type': 'enum',
- 'enums': ['Off', 'On'],
- 'states': [Severity.MINOR_ALARM, Severity.NO_ALARM],
- },
- 'WAVE': {
- 'count': 16,
- 'prec': 2,
- 'value': numpy.arange(16, dtype=float)
- }
+ "1:VAL": {
+ "value": 0.0,
+ "prec": 2,
+ "hilim": 180,
+ "hihi": 140,
+ "high": 100,
+ "low": -100,
+ "lolo": -140,
+ "lolim": -180,
+ "unit": "deg",
+ },
+ "1:STR": {"type": "string", "value": "Hello"},
+ "1:INT": {"type": "int", "value": 5},
+ "2:VAL": {
+ "value": 0.0,
+ "prec": 2,
+ "hilim": 180,
+ "hihi": 140,
+ "high": 100,
+ "low": -100,
+ "lolo": -140,
+ "lolim": -180,
+ "unit": "deg",
+ },
+ "3:VAL": {
+ "value": 0.0,
+ "prec": 2,
+ "hilim": 180,
+ "hihi": 140,
+ "high": 100,
+ "low": -100,
+ "lolo": -140,
+ "lolim": -180,
+ "unit": "deg",
+ },
+ "STATUS": {
+ "type": "enum",
+ "enums": ["Off", "On"],
+ "states": [Severity.MINOR_ALARM, Severity.NO_ALARM],
+ },
+ "WAVE": {"count": 16, "prec": 2, "value": numpy.arange(16, dtype=float)},
}
+
class myDriver(Driver):
- def __init__(self):
- super(myDriver, self).__init__()
-
-if __name__ == '__main__':
- server = SimpleServer()
- server.createPV(prefix, pvdb)
- driver = myDriver()
- print("Server is running... (ctrl+c to close)")
- while True:
- server.process(0.1)
+ def __init__(self):
+ super(myDriver, self).__init__()
+
+
+if __name__ == "__main__":
+ server = SimpleServer()
+ server.createPV(prefix, pvdb)
+ driver = myDriver()
+ print("Server is running... (ctrl+c to close)")
+ while True:
+ server.process(0.1)
diff --git a/examples/positioner/positioner_module.py b/examples/positioner/positioner_module.py
index e99a04b5ff..dc7dfa3aa8 100644
--- a/examples/positioner/positioner_module.py
+++ b/examples/positioner/positioner_module.py
@@ -1,12 +1,11 @@
import epics
import cams
import time
-from qtpy import uic
-from qtpy.QtCore import QObject, Slot, Signal
-from qtpy.QtWidgets import QWidget
+from qtpy.QtCore import Slot
from os import path
from pydm import Display
+
class Positioner(Display):
def __init__(self, parent=None, args=None):
super(Positioner, self).__init__(parent=parent, args=args)
@@ -19,7 +18,7 @@ def __init__(self, parent=None, args=None):
self.ui.xPosTextEntry.textChanged.connect(self.desired_position_changed)
self.ui.yPosTextEntry.textChanged.connect(self.desired_position_changed)
self.ui.zPosTextEntry.textChanged.connect(self.desired_position_changed)
-
+
@property
def display_manager_window(self):
return self.window()
@@ -28,14 +27,14 @@ def display_manager_window(self):
def move_motors(self):
if self.moving:
return
-
+
self.moving = True
self.ui.pushButton.setEnabled(False)
self.display_manager_window.statusBar().showMessage("Moving motors...")
self.motor1pv.put(self.m1des)
self.motor2pv.put(self.m2des)
self.motor3pv.put(self.m3des)
-
+
waiting = True
while waiting:
time.sleep(0.001)
@@ -43,7 +42,7 @@ def move_motors(self):
self.display_manager_window.statusBar().showMessage("Motor move complete.", 2000)
self.ui.pushButton.setEnabled(True)
self.moving = False
-
+
@Slot(str)
def desired_position_changed(self):
x = self.ui.xPosTextEntry.text()
@@ -55,20 +54,23 @@ def desired_position_changed(self):
theta = float(theta)
except ValueError:
self.ui.pushButton.setEnabled(False)
- self.display_manager_window.statusBar().showMessage("Cannot calculate new position, desired position is invalid.")
+ self.display_manager_window.statusBar().showMessage(
+ "Cannot calculate new position, desired position is invalid."
+ )
return
-
+
self.display_manager_window.statusBar().showMessage("Calculating new position...", 1000)
- (self.m1des, self.m2des, self.m3des, valid) = cams.real2cams((x,y,theta))
- self.ui.motor1DesLabel.setText('%.3f' % self.m1des)
- self.ui.motor2DesLabel.setText('%.3f' % self.m2des)
- self.ui.motor3DesLabel.setText('%.3f' % self.m3des)
+ (self.m1des, self.m2des, self.m3des, valid) = cams.real2cams((x, y, theta))
+ self.ui.motor1DesLabel.setText("%.3f" % self.m1des)
+ self.ui.motor2DesLabel.setText("%.3f" % self.m2des)
+ self.ui.motor3DesLabel.setText("%.3f" % self.m3des)
self.ui.pushButton.setEnabled(valid)
-
+
def ui_filename(self):
- return 'positioner-widget.ui'
-
+ return "positioner-widget.ui"
+
def ui_filepath(self):
return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
intelclass = Positioner
diff --git a/examples/shell_command/example_cmd.sh b/examples/shell_command/example_cmd.sh
new file mode 100755
index 0000000000..ff250de465
--- /dev/null
+++ b/examples/shell_command/example_cmd.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# This script can be called by a PyDMShellCommand widget,
+# allowing it to make use of command chaining and other shell features.
+echo "Hello World!" && echo "Hello Again!"
diff --git a/examples/shell_command/shell_command_full_shell.ui b/examples/shell_command/shell_command_full_shell.ui
new file mode 100644
index 0000000000..b5e6b2442a
--- /dev/null
+++ b/examples/shell_command/shell_command_full_shell.ui
@@ -0,0 +1,256 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 458
+ 386
+
+
+
+ Form
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p>The PyDMShellCommand button with run your command through a full shell if you enable the 'runCommandsInFullShell' option. This allows you to use some additional features such as shell syntax ('|', '&', ';', etc), environment variables ($VAR), glob expansion ('*', '?', etc), and some other features.</p></body></html>
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ runCmdsInFullShell Option Enabled
+
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+ false
+
+
+ true
+
+
+ Are you sure you want to proceed?
+
+
+
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+ echo First; echo Second
+
+
+
+ false
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+ You can run through a Bash shell (without needing to enable any options) by specifying "bash -c" at the start of your command.
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ Using "-c bash"
+
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+ false
+
+
+ Are you sure you want to proceed?
+
+
+
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+ bash -c "echo 'Hello One'; echo 'Hello two'"
+
+
+
+ false
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ You can also call a shell script. For this button to work correctly, run pydm from dir 'examples/shell_command' so it can find the script file.
+
+
+ true
+
+
+
+
+
+
+
+
+
+ Calling external script
+
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+ false
+
+
+ Are you sure you want to proceed?
+
+
+
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+ Print "Hello, World!" to terminal
+ Print current working directory to terminal
+
+
+
+
+ ./example_cmd.sh
+
+
+
+ false
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+ PyDMShellCommand
+ QPushButton
+ pydm.widgets.shell_command
+
+
+
+
+
diff --git a/examples/template_repeater/magnet_ioc.py b/examples/template_repeater/magnet_ioc.py
index 9c0951a79f..37e9613038 100644
--- a/examples/template_repeater/magnet_ioc.py
+++ b/examples/template_repeater/magnet_ioc.py
@@ -1,25 +1,58 @@
-import os
import asyncio
import json
from caproto.server import ioc_arg_parser, run, pvproperty, PVGroup
from caproto import ChannelType
+
class MagnetPV(PVGroup):
- bcon = pvproperty(value=0.0, name=':BCON', upper_ctrl_limit=0.006, lower_ctrl_limit=-0.006,
- upper_alarm_limit=0.004, lower_alarm_limit=-0.004, precision=4)
- bdes = pvproperty(value=0.0, name=':BDES', upper_ctrl_limit=0.006, lower_ctrl_limit=-0.006,
- upper_alarm_limit=0.004, lower_alarm_limit=-0.004, precision=4)
- bact = pvproperty(value=0.0, name=':BACT', read_only=True, upper_ctrl_limit=0.006, lower_ctrl_limit=-0.006,
- upper_alarm_limit=0.004, lower_alarm_limit=-0.004, precision=4)
- ctrl_strings = ("Ready", "TRIM", "PERTURB", "BCON_TO_BDES", "SAVE_BDES",
- "LOAD_BDES", "UNDO_BDES", "DAC_ZERO", "CALB", "STDZ",
- "RESET", "TURN_ON", "TURN_OFF")
- ctrl = pvproperty(value=0, name=':CTRL', dtype=ChannelType.ENUM,
- enum_strings=ctrl_strings)
- abort = pvproperty(value=0, name=':ABORT', dtype=ChannelType.ENUM,
- enum_strings=("Ready", "Abort"))
- madname = pvproperty(value="", name=':MADNAME', read_only=True, dtype=ChannelType.STRING)
- statmsg = pvproperty(value="", name=':STATMSG', read_only=True, dtype=ChannelType.STRING)
+ bcon = pvproperty(
+ value=0.0,
+ name=":BCON",
+ upper_ctrl_limit=0.006,
+ lower_ctrl_limit=-0.006,
+ upper_alarm_limit=0.004,
+ lower_alarm_limit=-0.004,
+ precision=4,
+ )
+ bdes = pvproperty(
+ value=0.0,
+ name=":BDES",
+ upper_ctrl_limit=0.006,
+ lower_ctrl_limit=-0.006,
+ upper_alarm_limit=0.004,
+ lower_alarm_limit=-0.004,
+ precision=4,
+ )
+ bact = pvproperty(
+ value=0.0,
+ name=":BACT",
+ read_only=True,
+ upper_ctrl_limit=0.006,
+ lower_ctrl_limit=-0.006,
+ upper_alarm_limit=0.004,
+ lower_alarm_limit=-0.004,
+ precision=4,
+ )
+ ctrl_strings = (
+ "Ready",
+ "TRIM",
+ "PERTURB",
+ "BCON_TO_BDES",
+ "SAVE_BDES",
+ "LOAD_BDES",
+ "UNDO_BDES",
+ "DAC_ZERO",
+ "CALB",
+ "STDZ",
+ "RESET",
+ "TURN_ON",
+ "TURN_OFF",
+ )
+ ctrl = pvproperty(value=0, name=":CTRL", dtype=ChannelType.ENUM, enum_strings=ctrl_strings)
+ abort = pvproperty(value=0, name=":ABORT", dtype=ChannelType.ENUM, enum_strings=("Ready", "Abort"))
+ madname = pvproperty(value="", name=":MADNAME", read_only=True, dtype=ChannelType.STRING)
+ statmsg = pvproperty(value="", name=":STATMSG", read_only=True, dtype=ChannelType.STRING)
+
def __init__(self, device_name, element_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.device_name = device_name
@@ -27,11 +60,11 @@ def __init__(self, device_name, element_name, *args, **kwargs):
self.saved_bdes = None
self.bdes_for_undo = None
initial_value = 0.0
- self.bcon._data['value'] = initial_value
- self.bdes._data['value'] = initial_value
- self.bact._data['value'] = initial_value
- self.madname._data['value'] = element_name.upper()
-
+ self.bcon._data["value"] = initial_value
+ self.bdes._data["value"] = initial_value
+ self.bact._data["value"] = initial_value
+ self.madname._data["value"] = element_name.upper()
+
@ctrl.putter
async def ctrl(self, instance, value):
ioc = instance.group
@@ -53,24 +86,31 @@ async def ctrl(self, instance, value):
else:
print("Warning, using a non-implemented magnet control function.")
return 0
-
- @pvproperty(value=0.0, name=":BCTRL", upper_ctrl_limit=0.006, lower_ctrl_limit=-0.006,
- upper_alarm_limit=0.004, lower_alarm_limit=-0.004, precision=4)
+
+ @pvproperty(
+ value=0.0,
+ name=":BCTRL",
+ upper_ctrl_limit=0.006,
+ lower_ctrl_limit=-0.006,
+ upper_alarm_limit=0.004,
+ lower_alarm_limit=-0.004,
+ precision=4,
+ )
async def bctrl(self, instance):
# We have to do some hacky stuff with caproto private data
# because otherwise, the putter method gets called any time
# we read.
ioc = instance.group
- instance._data['value'] = ioc.bact.value
+ instance._data["value"] = ioc.bact.value
return None
-
+
@bctrl.putter
async def bctrl(self, instance, value):
ioc = instance.group
await ioc.bdes.write(value)
await ioc.ctrl.write("PERTURB")
return value
-
+
@bdes.putter
async def bdes(self, instance, value):
ioc = instance.group
@@ -82,16 +122,16 @@ def main():
pvdb = {}
with open("xcor_list.json") as f:
mags = json.load(f)
- pvs = [MagnetPV(mag["devname"], "XCOR{}".format(mag["devname"].split(":")[-1]), prefix=mag["devname"]) for mag in mags]
+ pvs = [
+ MagnetPV(mag["devname"], "XCOR{}".format(mag["devname"].split(":")[-1]), prefix=mag["devname"])
+ for mag in mags
+ ]
for pv in pvs:
pvdb.update(**pv.pvdb)
-
- _, run_options = ioc_arg_parser(
- default_prefix='',
- desc="Simulated Corrector Magnet IOC")
+
+ _, run_options = ioc_arg_parser(default_prefix="", desc="Simulated Corrector Magnet IOC")
run(pvdb, **run_options)
-
-if __name__ == '__main__':
+
+
+if __name__ == "__main__":
main()
-
-
\ No newline at end of file
diff --git a/examples/testing_ioc/pva_testing_ioc.py b/examples/testing_ioc/pva_testing_ioc.py
index c5bbd5a465..c36e501691 100644
--- a/examples/testing_ioc/pva_testing_ioc.py
+++ b/examples/testing_ioc/pva_testing_ioc.py
@@ -8,87 +8,98 @@
class Handler(object):
- """ A handler for dealing with put requests to our test PVs """
+ """A handler for dealing with put requests to our test PVs"""
def put(self, pv: SharedPV, op: ServerOperation) -> None:
- """ Called each time a client issues a put operation on the channel using this handler """
+ """Called each time a client issues a put operation on the channel using this handler"""
pv.post(op.value())
op.done()
class PVServer(object):
- """ A simple server created by p4p for making some PVs to read and write test data to """
+ """A simple server created by p4p for making some PVs to read and write test data to"""
+
def __init__(self):
self.create_pvs()
- self.context = Context('pva', nt=False) # Disable automatic value unwrapping
+ self.context = Context("pva", nt=False) # Disable automatic value unwrapping
self.event = threading.Event()
- print('Creating testing server...')
+ print("Creating testing server...")
self.server_thread = threading.Thread(target=self.run_server)
self.server_thread.start()
self.update_thread = threading.Thread(target=self.update_pvs, daemon=True)
self.update_thread.start()
def create_pvs(self) -> None:
- """ Create a few PVs for interacting with """
+ """Create a few PVs for interacting with"""
handler = Handler() # A simple default handler that will just post PV updates is sufficient here
# A scalar int value with some alarm limits set
nt = NTScalar("i", display=True, control=True, valueAlarm=True)
- initial = nt.wrap({'value': 5, 'valueAlarm': {'lowAlarmLimit': 2, 'lowWarningLimit': 3,
- 'highAlarmLimit': 9, 'highWarningLimit': 8}})
-
- self.int_value = SharedPV(handler=handler,
- nt=NTScalar('i', display=True, control=True, valueAlarm=True),
- initial=initial)
- self.float_value = SharedPV(handler=handler, nt=NTScalar('f'), initial=0.0)
- self.bool_value = SharedPV(handler=handler, nt=NTScalar('?'), initial=False)
- self.byte_value = SharedPV(handler=handler, nt=NTScalar('b'), initial=12)
- self.short_value = SharedPV(handler=handler, nt=NTScalar('h'), initial=1)
- self.string_value = SharedPV(handler=handler, nt=NTScalar('s'), initial='PyDM!')
-
- self.int_array = SharedPV(handler=handler, nt=NTScalar('ai'), initial=[1, 2, 3, 4, 5])
- self.short_array = SharedPV(handler=handler, nt=NTScalar('ah'), initial=[0, 1, 1, 0, 0, 0, 1, 1])
- self.float_array = SharedPV(handler=handler, nt=NTScalar('af'), initial=[1.5, 2.5, 3.5, 4.5, 5.5])
- self.wave_form = SharedPV(handler=handler, nt=NTScalar('af'), initial=[1.0, 2.0, 3.0, 4.0])
- self.bool_array = SharedPV(handler=handler, nt=NTScalar('a?'), initial=[True, False, True, False])
- self.string_array = SharedPV(handler=handler, nt=NTScalar('as'), initial=['One', 'Two', 'Three'])
+ initial = nt.wrap(
+ {
+ "value": 5,
+ "valueAlarm": {"lowAlarmLimit": 2, "lowWarningLimit": 3, "highAlarmLimit": 9, "highWarningLimit": 8},
+ }
+ )
+
+ self.int_value = SharedPV(
+ handler=handler, nt=NTScalar("i", display=True, control=True, valueAlarm=True), initial=initial
+ )
+ self.float_value = SharedPV(handler=handler, nt=NTScalar("f"), initial=0.0)
+ self.bool_value = SharedPV(handler=handler, nt=NTScalar("?"), initial=False)
+ self.byte_value = SharedPV(handler=handler, nt=NTScalar("b"), initial=12)
+ self.short_value = SharedPV(handler=handler, nt=NTScalar("h"), initial=1)
+ self.string_value = SharedPV(handler=handler, nt=NTScalar("s"), initial="PyDM!")
+
+ self.int_array = SharedPV(handler=handler, nt=NTScalar("ai"), initial=[1, 2, 3, 4, 5])
+ self.short_array = SharedPV(handler=handler, nt=NTScalar("ah"), initial=[0, 1, 1, 0, 0, 0, 1, 1])
+ self.float_array = SharedPV(handler=handler, nt=NTScalar("af"), initial=[1.5, 2.5, 3.5, 4.5, 5.5])
+ self.wave_form = SharedPV(handler=handler, nt=NTScalar("af"), initial=[1.0, 2.0, 3.0, 4.0])
+ self.bool_array = SharedPV(handler=handler, nt=NTScalar("a?"), initial=[True, False, True, False])
+ self.string_array = SharedPV(handler=handler, nt=NTScalar("as"), initial=["One", "Two", "Three"])
# An NTNDArray that will be used to hold image data
self.image_pv = SharedPV(handler=handler, nt=NTNDArray(), initial=np.zeros(1))
# An NTEnum that supports read/write from PyDM widgets
- self.enum_pv = SharedPV(handler=handler, nt=NTEnum(), initial={'index': 0, 'choices': ['YES', 'NO', 'MAYBE']})
+ self.enum_pv = SharedPV(handler=handler, nt=NTEnum(), initial={"index": 0, "choices": ["YES", "NO", "MAYBE"]})
# An NTTable that can be displayed via the PyDMNTTable widget
- table_structure = NTTable([('names', 's'), ('floats', 'd'), ('booleans', '?')])
- table_strings = ['This', 'Is', 'A', 'PyDM', 'Table']
+ table_structure = NTTable([("names", "s"), ("floats", "d"), ("booleans", "?")])
+ table_strings = ["This", "Is", "A", "PyDM", "Table"]
table_rows = []
for i in range(5):
- table_rows.append({'names': table_strings[i], 'floats': 0.35 * i, 'booleans': i % 2 == 0})
+ table_rows.append({"names": table_strings[i], "floats": 0.35 * i, "booleans": i % 2 == 0})
self.nt_table_pv = SharedPV(handler=handler, nt=table_structure, initial=table_structure.wrap(table_rows))
def run_server(self) -> None:
- """ Run the server that will provide the PVs until keyboard interrupt """
- Server.forever(providers=[{'PyDM:PVA:IntValue': self.int_value,
- 'PyDM:PVA:FloatValue': self.float_value,
- 'PyDM:PVA:BoolValue': self.bool_value,
- 'PyDM:PVA:ByteValue': self.byte_value,
- 'PyDM:PVA:ShortValue': self.short_value,
- 'PyDM:PVA:StringValue': self.string_value,
- 'PyDM:PVA:IntArray': self.int_array,
- 'PyDM:PVA:ShortArray': self.short_array,
- 'PyDM:PVA:FloatArray': self.float_array,
- 'PyDM:PVA:Waveform': self.wave_form,
- 'PyDM:PVA:BoolArray': self.bool_array,
- 'PyDM:PVA:StringArray': self.string_array,
- 'PyDM:PVA:Image': self.image_pv,
- 'PyDM:PVA:Enum': self.enum_pv,
- 'PyDM:PVA:Table': self.nt_table_pv}])
+ """Run the server that will provide the PVs until keyboard interrupt"""
+ Server.forever(
+ providers=[
+ {
+ "PyDM:PVA:IntValue": self.int_value,
+ "PyDM:PVA:FloatValue": self.float_value,
+ "PyDM:PVA:BoolValue": self.bool_value,
+ "PyDM:PVA:ByteValue": self.byte_value,
+ "PyDM:PVA:ShortValue": self.short_value,
+ "PyDM:PVA:StringValue": self.string_value,
+ "PyDM:PVA:IntArray": self.int_array,
+ "PyDM:PVA:ShortArray": self.short_array,
+ "PyDM:PVA:FloatArray": self.float_array,
+ "PyDM:PVA:Waveform": self.wave_form,
+ "PyDM:PVA:BoolArray": self.bool_array,
+ "PyDM:PVA:StringArray": self.string_array,
+ "PyDM:PVA:Image": self.image_pv,
+ "PyDM:PVA:Enum": self.enum_pv,
+ "PyDM:PVA:Table": self.nt_table_pv,
+ }
+ ]
+ )
def gaussian_2d(self, x: float, y: float, x0: float, y0: float, xsig: float, ysig: float) -> np.ndarray:
return np.exp(-0.5 * (((x - x0) / xsig) ** 2 + ((y - y0) / ysig) ** 2))
def update_pvs(self) -> None:
- """ Continually update the value of some PVs """
+ """Continually update the value of some PVs"""
while True:
self.event.wait(0.7)
x = np.linspace(-5.0, 5.0, 512)
@@ -99,20 +110,20 @@ def update_pvs(self) -> None:
ysig = 0.8 - 0.2 * np.random.rand()
xgrid, ygrid = np.meshgrid(x, y)
z = self.gaussian_2d(xgrid, ygrid, x0, y0, xsig, ysig)
- image_data = np.abs(256.0 * (z)).flatten(order='C').astype(np.uint8, copy=False)
- self.context.put('PyDM:PVA:Image',
- {'value': image_data,
- 'dimension': [{'size': 512, 'offset': 0}, {'size': 512, 'offset': 0}]
- })
- self.context.put('PyDM:PVA:IntValue', random.randint(1, 100))
- self.context.put('PyDM:PVA:FloatValue', random.uniform(1, 100))
- self.context.put('PyDM:PVA:BoolValue', random.choice([True, False]))
- self.context.put('PyDM:PVA:FloatArray', (np.random.rand(5) * 10).tolist())
-
-
-if __name__ == '__main__':
+ image_data = np.abs(256.0 * (z)).flatten(order="C").astype(np.uint8, copy=False)
+ self.context.put(
+ "PyDM:PVA:Image",
+ {"value": image_data, "dimension": [{"size": 512, "offset": 0}, {"size": 512, "offset": 0}]},
+ )
+ self.context.put("PyDM:PVA:IntValue", random.randint(1, 100))
+ self.context.put("PyDM:PVA:FloatValue", random.uniform(1, 100))
+ self.context.put("PyDM:PVA:BoolValue", random.choice([True, False]))
+ self.context.put("PyDM:PVA:FloatArray", (np.random.rand(5) * 10).tolist())
+
+
+if __name__ == "__main__":
try:
- print('Starting PVA testing ioc...')
+ print("Starting PVA testing ioc...")
server = PVServer()
except KeyboardInterrupt:
- print('\nInterrupted... finishing PVA testing ioc')
+ print("\nInterrupted... finishing PVA testing ioc")
diff --git a/examples/testing_ioc/pydm-testing-ioc b/examples/testing_ioc/pydm-testing-ioc
index 5401cd137e..fe0469dc01 100755
--- a/examples/testing_ioc/pydm-testing-ioc
+++ b/examples/testing_ioc/pydm-testing-ioc
@@ -14,51 +14,71 @@ MIN_UPDATE_TIME = 0.001
IMAGE_SIZE = 512
MESSAGE = "PyDM Rocks!"
-prefix = 'MTEST:'
+prefix = "MTEST:"
pvdb = {
- 'Run' : { 'type' : 'enum',
- 'enums': ['STOP', 'RUN'], 'asg' : 'default'},
- 'ReadOnly' : { 'type' : 'enum',
- 'enums': ['FALSE', 'TRUE'], 'value': 0 },
- 'UpdateTime' : { 'prec' : 3, 'unit' : 's', 'value' : 0.03, 'asg' : 'default' },
- 'TimePerDivision' : { 'prec' : 5, 'unit' : 's', 'value' : 0.001, 'asg' : 'default' },
- 'TriggerDelay' : { 'prec' : 5, 'unit' : 's', 'value' : 0.0005, 'asg' : 'default'},
- 'VoltsPerDivision' : { 'prec' : 3, 'unit' : 'V', 'value' : 0.2, 'asg' : 'default' },
- 'VoltOffset' : { 'prec' : 3, 'unit' : 'V', 'asg' : 'default' },
- 'NoiseAmplitude' : { 'prec' : 3, 'value' : 0.2, 'asg' : 'default' },
- 'Waveform' : { 'count': MAX_POINTS,
- 'prec' : 5, 'asg' : 'default' },
- 'Cosine' : { 'count': MAX_POINTS,
- 'prec' : 5, 'asg' : 'default' },
- 'TimeBase' : { 'count': MAX_POINTS,
- 'prec' : 5,
- 'value': numpy.arange(MAX_POINTS, dtype=float)
- * NUM_DIVISIONS / (MAX_POINTS - 1), 'asg' : 'default'},
- 'MinValue' : { 'prec' : 4, 'asg' : 'default' },
- 'MaxValue' : { 'prec' : 4, 'asg' : 'default' },
- 'MeanValue' : { 'prec' : 4, 'asg' : 'default' },
- 'XPos' : { 'prec' : 2, 'value' : 0.0, 'asg' : 'default' },
- 'YPos' : { 'prec' : 2, 'value' : 0.0, 'asg' : 'default' },
- 'Image' : { 'type' : 'char', 'count': IMAGE_SIZE ** 2, 'value': numpy.zeros(IMAGE_SIZE ** 2, dtype=numpy.uint8), 'asg' : 'default' },
- 'TwoSpotImage' : { 'type' : 'char', 'count': IMAGE_SIZE ** 2, 'value': numpy.zeros(IMAGE_SIZE ** 2, dtype=numpy.uint8), 'asg' : 'default' },
- 'ImageWidth' : { 'type' : 'int', 'value' : IMAGE_SIZE, 'asg' : 'default' },
- 'String' : { 'type' : 'string', 'value': "Test String", 'asg' : 'default'},
- 'Message' : { 'type' : 'char', 'count': 100, 'value': MESSAGE},
- 'Float' : { 'type' : 'float', 'value': 0.0, 'lolim':-1.2, 'lolo':-1.0, 'low':-0.8, 'high': 0.8, 'hihi': 1.0, 'hilim': 1.2, 'unit': 'mJ', 'prec': 3, 'asg' : 'default' },
- 'StatusBits' : { 'type' : 'int', 'value': 0b101010, 'lolim': 0, 'hilim': 32, 'asg' : 'default' },
- 'SinVal' : { 'type' : 'float', 'value': 0.0, 'asg': 'default' },
- 'CosVal' : { 'type' : 'float', 'value': 0.0, 'asg': 'default' },
- 'Normal' : { 'type' : 'float', 'value': 0.0, 'asg': 'default' },
- 'Infinity' : { 'count': MAX_POINTS,
- 'prec' : 5, 'asg' : 'default', 'value': numpy.array([numpy.inf]*MAX_POINTS)},
- 'CurrentTimeSec' : {'type': 'float', 'value': 0},
- 'ConfigTime' : { 'type': 'float', 'value': 0, 'asg': 'default'}
-
+ "Run": {"type": "enum", "enums": ["STOP", "RUN"], "asg": "default"},
+ "ReadOnly": {"type": "enum", "enums": ["FALSE", "TRUE"], "value": 0},
+ "UpdateTime": {"prec": 3, "unit": "s", "value": 0.03, "asg": "default"},
+ "TimePerDivision": {"prec": 5, "unit": "s", "value": 0.001, "asg": "default"},
+ "TriggerDelay": {"prec": 5, "unit": "s", "value": 0.0005, "asg": "default"},
+ "VoltsPerDivision": {"prec": 3, "unit": "V", "value": 0.2, "asg": "default"},
+ "VoltOffset": {"prec": 3, "unit": "V", "asg": "default"},
+ "NoiseAmplitude": {"prec": 3, "value": 0.2, "asg": "default"},
+ "Waveform": {"count": MAX_POINTS, "prec": 5, "asg": "default"},
+ "Cosine": {"count": MAX_POINTS, "prec": 5, "asg": "default"},
+ "TimeBase": {
+ "count": MAX_POINTS,
+ "prec": 5,
+ "value": numpy.arange(MAX_POINTS, dtype=float) * NUM_DIVISIONS / (MAX_POINTS - 1),
+ "asg": "default",
+ },
+ "MinValue": {"prec": 4, "asg": "default"},
+ "MaxValue": {"prec": 4, "asg": "default"},
+ "MeanValue": {"prec": 4, "asg": "default"},
+ "XPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "YPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "Image": {
+ "type": "char",
+ "count": IMAGE_SIZE**2,
+ "value": numpy.zeros(IMAGE_SIZE**2, dtype=numpy.uint8),
+ "asg": "default",
+ },
+ "TwoSpotImage": {
+ "type": "char",
+ "count": IMAGE_SIZE**2,
+ "value": numpy.zeros(IMAGE_SIZE**2, dtype=numpy.uint8),
+ "asg": "default",
+ },
+ "ImageWidth": {"type": "int", "value": IMAGE_SIZE, "asg": "default"},
+ "String": {"type": "string", "value": "Test String", "asg": "default"},
+ "Message": {"type": "char", "count": 100, "value": MESSAGE},
+ "Float": {
+ "type": "float",
+ "value": 0.0,
+ "lolim": -1.2,
+ "lolo": -1.0,
+ "low": -0.8,
+ "high": 0.8,
+ "hihi": 1.0,
+ "hilim": 1.2,
+ "unit": "mJ",
+ "prec": 3,
+ "asg": "default",
+ },
+ "StatusBits": {"type": "int", "value": 0b101010, "lolim": 0, "hilim": 32, "asg": "default"},
+ "SinVal": {"type": "float", "value": 0.0, "asg": "default"},
+ "CosVal": {"type": "float", "value": 0.0, "asg": "default"},
+ "Normal": {"type": "float", "value": 0.0, "asg": "default"},
+ "Infinity": {"count": MAX_POINTS, "prec": 5, "asg": "default", "value": numpy.array([numpy.inf] * MAX_POINTS)},
+ "CurrentTimeSec": {"type": "float", "value": 0},
+ "ConfigTime": {"type": "float", "value": 0, "asg": "default"},
}
def double_gaussian_2d(x, y, x0, y0, xsig, ysig):
- return numpy.exp(-0.5 * (((x - x0) / xsig) ** 2 + ((y - y0) / ysig) ** 2)) + numpy.exp(-0.5 * (((x + 3) / xsig) ** 2 + ((y + 3) / ysig) ** 2))
+ return numpy.exp(-0.5 * (((x - x0) / xsig) ** 2 + ((y - y0) / ysig) ** 2)) + numpy.exp(
+ -0.5 * (((x + 3) / xsig) ** 2 + ((y + 3) / ysig) ** 2)
+ )
def gaussian_2d(x, y, x0, y0, xsig, ysig):
@@ -66,7 +86,6 @@ def gaussian_2d(x, y, x0, y0, xsig, ysig):
class myDriver(Driver):
-
def __init__(self):
Driver.__init__(self)
self.eid = threading.Event()
@@ -77,10 +96,10 @@ class myDriver(Driver):
def write(self, reason, value):
status = True
# take proper actions
- if reason == 'UpdateTime':
+ if reason == "UpdateTime":
value = max(MIN_UPDATE_TIME, value)
- elif reason == 'Run':
- if not self.getParam('Run') and value == 1:
+ elif reason == "Run":
+ if not self.getParam("Run") and value == 1:
self.eid.set()
self.eid.clear()
# store the values
@@ -96,74 +115,80 @@ class myDriver(Driver):
i = 0
while True:
- run = self.getParam('Run')
- updateTime = self.getParam('UpdateTime')
+ run = self.getParam("Run")
+ updateTime = self.getParam("UpdateTime")
if run:
self.eid.wait(updateTime)
else:
self.eid.wait()
- run = self.getParam('Run')
- self.setParam('CurrentTimeSec', time.time())
+ run = self.getParam("Run")
+ self.setParam("CurrentTimeSec", time.time())
if not run:
- self.updatePV('CurrentTimeSec')
+ self.updatePV("CurrentTimeSec")
continue
# retrieve parameters
- noiseAmplitude = self.getParam('NoiseAmplitude')
- timePerDivision = self.getParam('TimePerDivision')
- voltsPerDivision = self.getParam('VoltsPerDivision')
- triggerDelay = self.getParam('TriggerDelay')
- voltOffset = self.getParam('VoltOffset')
+ noiseAmplitude = self.getParam("NoiseAmplitude")
+ timePerDivision = self.getParam("TimePerDivision")
+ voltsPerDivision = self.getParam("VoltsPerDivision")
+ triggerDelay = self.getParam("TriggerDelay")
+ voltOffset = self.getParam("VoltOffset")
# calculate the data wave based on timeWave scale
timeStart = triggerDelay
timeStep = timePerDivision * NUM_DIVISIONS / MAX_POINTS
timeWave = timeStart + numpy.arange(MAX_POINTS) * timeStep
# noise = noiseAmplitude * numpy.random.random(MAX_POINTS)
- data = AMPLITUDE * numpy.sin(timeWave * FREQUENCY * 2 * numpy.pi) + (noiseAmplitude * numpy.random.random(MAX_POINTS))
- cos_data = AMPLITUDE * numpy.cos(timeWave * FREQUENCY * 2 * numpy.pi) + (noiseAmplitude * numpy.random.random(MAX_POINTS))
+ data = AMPLITUDE * numpy.sin(timeWave * FREQUENCY * 2 * numpy.pi) + (
+ noiseAmplitude * numpy.random.random(MAX_POINTS)
+ )
+ cos_data = AMPLITUDE * numpy.cos(timeWave * FREQUENCY * 2 * numpy.pi) + (
+ noiseAmplitude * numpy.random.random(MAX_POINTS)
+ )
# calculate statistics
- self.setParam('MinValue', data.min())
- self.setParam('MaxValue', data.max())
- self.setParam('MeanValue', data.mean())
- self.setParam('Normal', numpy.random.normal())
+ self.setParam("MinValue", data.min())
+ self.setParam("MaxValue", data.max())
+ self.setParam("MeanValue", data.mean())
+ self.setParam("Normal", numpy.random.normal())
# scale/offset
yScale = 1.0 / voltsPerDivision
data = NUM_DIVISIONS / 2.0 + yScale * (data + voltOffset)
cos_data = NUM_DIVISIONS / 2.0 + yScale * (cos_data + voltOffset)
- self.setParam('Waveform', data)
- self.setParam('Cosine', cos_data)
+ self.setParam("Waveform", data)
+ self.setParam("Cosine", cos_data)
i = (i + 1) % MAX_POINTS
- self.setParam('SinVal', data[i])
- self.setParam('CosVal', cos_data[i])
+ self.setParam("SinVal", data[i])
+ self.setParam("CosVal", cos_data[i])
# Generate the image data
- x0 = 0.5 * (numpy.random.rand() - 0.5) + self.getParam('XPos')
- y0 = 0.5 * (numpy.random.rand() - 0.5) - self.getParam('YPos')
+ x0 = 0.5 * (numpy.random.rand() - 0.5) + self.getParam("XPos")
+ y0 = 0.5 * (numpy.random.rand() - 0.5) - self.getParam("YPos")
xsig = 0.8 - 0.2 * numpy.random.rand()
ysig = 0.8 - 0.2 * numpy.random.rand()
z = gaussian_2d(xgrid, ygrid, x0, y0, xsig, ysig)
- image_data = numpy.abs(256.0 * (z)).flatten(order='C').astype(numpy.uint8, copy=False)
- self.setParam('Image', image_data)
+ image_data = numpy.abs(256.0 * (z)).flatten(order="C").astype(numpy.uint8, copy=False)
+ self.setParam("Image", image_data)
two_spots = double_gaussian_2d(xgrid, ygrid, x0, y0, xsig, ysig)
- two_spot_image = numpy.abs(256.0 * (two_spots)).flatten(order='C').astype(numpy.uint8, copy=False)
- self.setParam('TwoSpotImage', two_spot_image)
+ two_spot_image = numpy.abs(256.0 * (two_spots)).flatten(order="C").astype(numpy.uint8, copy=False)
+ self.setParam("TwoSpotImage", two_spot_image)
# do updates so clients see the changes
self.updatePVs()
-if __name__ == '__main__':
+if __name__ == "__main__":
try:
- print('Starting testing-ioc')
- print('To start processing records do: caput ' + prefix + 'Run 1')
+ print("Starting testing-ioc")
+ print("To start processing records do: caput " + prefix + "Run 1")
server = SimpleServer()
- server.initAccessSecurityFile(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'access_rules.as'), P=prefix)
+ server.initAccessSecurityFile(
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "access_rules.as"), P=prefix
+ )
server.createPV(prefix, pvdb)
driver = myDriver()
# Manually set the ReadOnly PV to force access rule calculation.
# You can set ReadOnly to 1 to disable write access on all PVs.
- driver.setParam('ReadOnly', 0)
+ driver.setParam("ReadOnly", 0)
# process CA transactions
while True:
server.process(0.03)
except KeyboardInterrupt:
- print('\nInterrupted... finishing testing-ioc')
+ print("\nInterrupted... finishing testing-ioc")
diff --git a/examples/testing_ioc/pydm-tutorial-ioc b/examples/testing_ioc/pydm-tutorial-ioc
new file mode 100755
index 0000000000..0f83d50a9e
--- /dev/null
+++ b/examples/testing_ioc/pydm-tutorial-ioc
@@ -0,0 +1,261 @@
+#!/usr/bin/env python
+import os
+import threading
+import numpy
+import time
+
+from pcaspy import Driver, SimpleServer
+
+""" This file provides the server needed for for providing PVs to run the tutorial.
+ You can follow along the tutorial here: https://slaclab.github.io/pydm/tutorials/index.html
+ The server mimics variables used by the tutorial, which mimic a simulated camera and motor.
+ Mimicking this behavior is done (over running simulator programs) in order to minimize the overhead
+ to get the tutorial up-and-running, and since the simulated data is not necessarily needed to learn
+ how to make a PyDM user-interface.
+"""
+
+MAX_POINTS = 1000
+FREQUENCY = 1000
+AMPLITUDE = 1.0
+NUM_DIVISIONS = 10
+MIN_UPDATE_TIME = 0.001
+IMAGE_SIZE = 512
+MESSAGE = "PyDM Rocks!"
+
+prefix = "IOC:"
+pvdb = {
+ "Run": {"type": "enum", "enums": ["STOP", "RUN"], "asg": "default"},
+ "ReadOnly": {"type": "enum", "enums": ["FALSE", "TRUE"], "value": 0},
+ "XPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "YPos": {"prec": 2, "value": 0.0, "asg": "default"},
+ "Image": {
+ "type": "char",
+ "count": IMAGE_SIZE**2,
+ "value": numpy.zeros(IMAGE_SIZE**2, dtype=numpy.uint8),
+ "asg": "default",
+ },
+ "ImageWidth": {"type": "int", "value": IMAGE_SIZE, "asg": "default"},
+ # m1
+ "m1.DESC": {"type": "string", "value": "Motor X", "asg": "default"},
+ "m1": {"type": "float", "unit": "degrees", "prec": 1, "value": 500.0, "asg": "default"},
+ "m1.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m1.RBV": {"type": "float", "prec": 5, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m1.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m1.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m1.ACCL": {"type": "float", "prec": 3, "unit": "sec", "value": 0.002, "asg": "default"},
+ "m1.VELO": {"type": "float", "prec": 1, "unit": "degrees", "value": 100.0, "asg": "default"},
+ # m2
+ "m2.DESC": {"type": "string", "value": "Motor Y", "asg": "default"},
+ "m2": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m2.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m2.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m2.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m2.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m2.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m2.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m3
+ "m3.DESC": {"type": "string", "value": "Motor 3", "asg": "default"},
+ "m3": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m3.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m3.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m3.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m3.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m3.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m3.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m4
+ "m4.DESC": {"type": "string", "value": "Motor 4", "asg": "default"},
+ "m4": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m4.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m4.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m4.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m4.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m4.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m4.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m5
+ "m5.DESC": {"type": "string", "value": "Motor 5", "asg": "default"},
+ "m5": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m5.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m5.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m5.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m5.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m5.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m5.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m6
+ "m6.DESC": {"type": "string", "value": "Motor 6", "asg": "default"},
+ "m6": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m6.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m6.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m6.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m6.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m6.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m6.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m7
+ "m7.DESC": {"type": "string", "value": "Motor 7", "asg": "default"},
+ "m7": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m7.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m7.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m7.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m7.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m7.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m7.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+ # m8
+ "m8.DESC": {"type": "string", "value": "Motor 8", "asg": "default"},
+ "m8": {"type": "float", "prec": 1, "unit": "degrees", "value": 500.0, "asg": "default"},
+ "m8.VAL": {"type": "float", "unit": "degrees", "prec": 5, "value": 500.0, "asg": "default"},
+ "m8.RBV": {"type": "float", "prec": 5, "value": 500.0, "unit": "degrees", "asg": "default"},
+ "m8.MOVN": {"type": "int", "value": 0, "asg": "default"},
+ "m8.STOP": {"type": "int", "value": 0, "asg": "default"},
+ "m8.ACCL": {"type": "float", "prec": 3, "value": 0.002, "unit": "sec", "asg": "default"},
+ "m8.VELO": {"type": "float", "prec": 1, "value": 100.0, "unit": "degrees", "asg": "default"},
+}
+
+
+def gaussian_2d(x, y, x0, y0, xsig, ysig):
+ return numpy.exp(-0.5 * (((x - x0) / xsig) ** 2 + ((y - y0) / ysig) ** 2))
+
+
+class myDriver(Driver):
+ def __init__(self):
+ Driver.__init__(self)
+ self.eid = threading.Event()
+ self.tid = threading.Thread(target=self.runSimScope)
+ self.tid.setDaemon(True)
+ self.tid.start()
+
+ self.motorXThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ "m1",
+ "XPos",
+ ),
+ )
+ self.motorXThread.setDaemon(True)
+ self.motorXThread.start()
+
+ self.motorYThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ "m2",
+ "YPos",
+ ),
+ )
+ self.motorYThread.setDaemon(True)
+ self.motorYThread.start()
+
+ for i in range(3, 9):
+ currMotorName = "m" + str(i)
+ motorCurrThread = threading.Thread(
+ target=self.updateMotor,
+ args=(
+ currMotorName,
+ "NA",
+ ),
+ )
+ motorCurrThread.setDaemon(True)
+ motorCurrThread.start()
+
+ def updateMotor(self, motorVarName, axisVarName):
+ # mimic the functionality of fully-simulated motor and camera
+
+ sleepTime = 0.2 # set arbitrarily to match timing when fully-simulated
+ motorRbvName = motorVarName + ".RBV"
+ motorMovingVarName = motorVarName + ".MOVN"
+ # set to avoid button having purple "uninit" color
+ self.setParam(motorMovingVarName, 0)
+ motorStopVarName = motorVarName + ".STOP"
+ motorValVarName = motorVarName + ".VAL"
+
+ motorRbv = self.getParam(motorRbvName)
+ while True:
+ time.sleep(sleepTime)
+ motorParam = self.getParam(motorVarName)
+
+ if motorParam != motorRbv: # need to move motor
+ self.setParam(motorMovingVarName, 1)
+
+ # for Motor X: 'Tw +10' = move right, 'Tw -10' = move left
+ # for Motor Y: 'Tw -10' = move up, 'Tw +10' = move down left
+ motorMoveAmount = 10 if motorRbv < motorParam else -10
+ # so Motor Y buttons move as expected
+ if axisVarName == "YPos":
+ imageMoveAmount = -0.1 if motorRbv < motorParam else 0.1
+ else:
+ imageMoveAmount = 0.1 if motorRbv < motorParam else -0.1
+
+ # do movement in increments, updating displayed value and sleeping inbetween
+ while motorRbv != motorParam:
+ currStopVal = self.getParam(motorStopVarName)
+ if currStopVal:
+ break
+
+ # update displayed values
+
+ # allow for setting values not divisible by 10
+ if motorParam > 0:
+ motorRbv = (
+ motorParam if (motorRbv + motorMoveAmount > motorParam) else motorRbv + motorMoveAmount
+ )
+ else:
+ motorRbv = (
+ motorParam if (motorRbv + motorMoveAmount < motorParam) else motorRbv + motorMoveAmount
+ )
+ self.setParam(motorRbvName, motorRbv)
+ self.setParam(motorValVarName, motorRbv)
+
+ # only update axis for m1(XPos) and m2(YPos)
+ if axisVarName != "NA":
+ axis_pos = self.getParam(axisVarName)
+ self.setParam(axisVarName, axis_pos + imageMoveAmount)
+
+ self.updatePVs()
+ time.sleep(sleepTime)
+
+ self.setParam(motorStopVarName, 0)
+ # if stopped, set motorVar line-edit to the motorRBV text-field value
+ # (motor was stopped before reaching the originally entered value)
+ if motorParam != motorRbv:
+ self.setParam(motorVarName, motorRbv)
+
+ self.setParam(motorMovingVarName, 0)
+ self.updatePVs()
+
+ def runSimScope(self):
+ # simulate scope waveform
+ x = numpy.linspace(-5.0, 5.0, IMAGE_SIZE)
+ y = numpy.linspace(-5.0, 5.0, IMAGE_SIZE)
+ xgrid, ygrid = numpy.meshgrid(x, y)
+
+ while True:
+ # Generate the image data
+ x0 = 0.1 * (numpy.random.rand() - 0.5) + self.getParam("XPos")
+ y0 = 0.1 * (numpy.random.rand() - 0.5) - self.getParam("YPos")
+ xsig = 0.6
+ ysig = 0.2
+ z = gaussian_2d(xgrid, ygrid, x0, y0, xsig, ysig)
+ image_data = numpy.abs(256.0 * (z)).flatten(order="C").astype(numpy.uint8, copy=False)
+ self.setParam("Image", image_data)
+
+ # do updates so clients see the changes
+ self.updatePVs()
+
+
+if __name__ == "__main__":
+ try:
+ print("Starting testing-ioc")
+ print("To start processing records do: caput " + prefix + "Run 1")
+ server = SimpleServer()
+ server.initAccessSecurityFile(
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), "access_rules.as"), P=prefix
+ )
+ server.createPV(prefix, pvdb)
+ driver = myDriver()
+
+ # Manually set the ReadOnly PV to force access rule calculation.
+ # You can set ReadOnly to 1 to disable write access on all PVs.
+ driver.setParam("ReadOnly", 0)
+
+ # process CA transactions
+ while True:
+ server.process(0.03)
+ except KeyboardInterrupt:
+ print("\nInterrupted... finishing testing-ioc")
diff --git a/examples/tutorial/all_motors.py b/examples/tutorial/all_motors.py
new file mode 100644
index 0000000000..56ebc4a30b
--- /dev/null
+++ b/examples/tutorial/all_motors.py
@@ -0,0 +1,146 @@
+import os
+import json
+from pydm import Display
+from qtpy.QtWidgets import (
+ QVBoxLayout,
+ QHBoxLayout,
+ QGroupBox,
+ QLabel,
+ QLineEdit,
+ QPushButton,
+ QScrollArea,
+ QFrame,
+ QApplication,
+ QWidget,
+)
+from qtpy import QtCore
+from pydm.widgets import PyDMEmbeddedDisplay
+
+
+class AllMotorsDisplay(Display):
+ def __init__(self, parent=None, args=[], macros=None):
+ super(AllMotorsDisplay, self).__init__(parent=parent, args=args, macros=None)
+ # Placeholder for data to filter
+ self.data = []
+ # Reference to the PyDMApplication
+ self.app = QApplication.instance()
+ # Assemble the Widgets
+ self.setup_ui()
+ # Load data from file
+ self.load_data()
+
+ def minimumSizeHint(self):
+ # This is the default recommended size
+ # for this screen
+ return QtCore.QSize(750, 120)
+
+ def ui_filepath(self):
+ # No UI file is being used
+ return None
+
+ def setup_ui(self):
+ # Create the main layout
+ main_layout = QVBoxLayout()
+ self.setLayout(main_layout)
+
+ # Create a Label to be the title
+ lbl_title = QLabel("Motors Diagnostic")
+ # Add some StyleSheet to it
+ lbl_title.setStyleSheet(
+ "\
+ QLabel {\
+ qproperty-alignment: AlignCenter;\
+ border: 1px solid #FF17365D;\
+ border-top-left-radius: 15px;\
+ border-top-right-radius: 15px;\
+ background-color: #FF17365D;\
+ padding: 5px 0px;\
+ color: rgb(255, 255, 255);\
+ max-height: 25px;\
+ font-size: 14px;\
+ }"
+ )
+
+ # Add the title label to the main layout
+ main_layout.addWidget(lbl_title)
+
+ # Create the Search Panel layout
+ search_layout = QHBoxLayout()
+
+ # Create a GroupBox with "Filtering" as Title
+ gb_search = QGroupBox(parent=self)
+ gb_search.setTitle("Filtering")
+ gb_search.setLayout(search_layout)
+
+ # Create a label, line edit and button for filtering
+ lbl_search = QLabel(text="Filter: ")
+ self.txt_filter = QLineEdit()
+ self.txt_filter.returnPressed.connect(self.do_search)
+ btn_search = QPushButton()
+ btn_search.setText("Search")
+ btn_search.clicked.connect(self.do_search)
+
+ # Add the created widgets to the layout
+ search_layout.addWidget(lbl_search)
+ search_layout.addWidget(self.txt_filter)
+ search_layout.addWidget(btn_search)
+
+ # Add the Groupbox to the main layout
+ main_layout.addWidget(gb_search)
+
+ # Create the Results Layout
+ self.results_layout = QVBoxLayout()
+ self.results_layout.setContentsMargins(0, 0, 0, 0)
+
+ # Create a Frame to host the results of search
+ self.frm_result = QFrame(parent=self)
+ self.frm_result.setLayout(self.results_layout)
+
+ # Create a ScrollArea so we can properly handle
+ # many entries
+ scroll_area = QScrollArea(parent=self)
+ scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+ scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ scroll_area.setWidgetResizable(True)
+
+ # Add the Frame to the scroll area
+ scroll_area.setWidget(self.frm_result)
+
+ # Add the scroll area to the main layout
+ main_layout.addWidget(scroll_area)
+
+ def load_data(self):
+ # Extract the directory of this file...
+ base_dir = os.path.dirname(os.path.realpath(__file__))
+ # Concatenate the directory with the file name...
+ data_file = os.path.join(base_dir, "motor_db.txt")
+ # Open the file so we can read the data...
+ with open(data_file, "r") as f:
+ # For each line in the file...
+ for entry in f.readlines():
+ # Append to the list of data...
+ self.data.append(entry[:-1])
+
+ def do_search(self):
+ # For each widget inside the results frame, lets destroy them
+ for widget in self.frm_result.findChildren(QWidget):
+ widget.setParent(None)
+ widget.deleteLater()
+
+ # Grab the filter text
+ filter_text = self.txt_filter.text()
+
+ # For every entry in the dataset...
+ for entry in self.data:
+ # Check if they match our filter
+ if filter_text.upper() not in entry.upper():
+ continue
+ # Create a PyDMEmbeddedDisplay for this entry
+ disp = PyDMEmbeddedDisplay(parent=self)
+ disp.macros = json.dumps({"MOTOR": entry})
+ disp.filename = "inline_motor.ui"
+ disp.setMinimumWidth(700)
+ disp.setMinimumHeight(40)
+ disp.setMaximumHeight(100)
+ # Add the Embedded Display to the Results Layout
+ self.results_layout.addWidget(disp)
diff --git a/examples/tutorial/expert_motor.ui b/examples/tutorial/expert_motor.ui
new file mode 100644
index 0000000000..fafcf5374f
--- /dev/null
+++ b/examples/tutorial/expert_motor.ui
@@ -0,0 +1,373 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 447
+ 217
+
+
+
+ Engineer Screen: ${MOTOR}
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 0
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Configuring Motor: ${MOTOR}
+
+
+
+
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+ 10
+
+
+ 10
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+
+
+ Description:
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.DESC
+
+
+ PyDMLineEdit::String
+
+
+
+
+
+
+ Position:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.VAL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+ Readback:
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.RBV
+
+
+ false
+
+
+ PyDMLabel::Decimal
+
+
+
+
+
+
+ Velocity:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.VELO
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+ Acceleration:
+
+
+
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}.ACCL
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+ pydm.widgets.label
+
+
+ PyDMLineEdit
+ QLineEdit
+ pydm.widgets.line_edit
+
+
+
+
+
diff --git a/examples/tutorial/inline_motor.ui b/examples/tutorial/inline_motor.ui
new file mode 100644
index 0000000000..e7c4aad980
--- /dev/null
+++ b/examples/tutorial/inline_motor.ui
@@ -0,0 +1,565 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 700
+ 32
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 32
+
+
+
+
+ 16777215
+ 38
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 5
+
+
+ 5
+
+
+ 10
+
+
+ 5
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.RBV
+
+
+ false
+
+
+ PyDMLabel::Decimal
+
+
+
+
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw +10
+
+
+ ca://${MOTOR}
+
+
+ 10
+
+
+ true
+
+
+
+
+
+
+
+ 50
+ 0
+
+
+
+
+ 50
+ 16777215
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ Tw -10
+
+
+ ca://${MOTOR}
+
+
+ -10
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+ Basic PushButton to send a fixed value.
+
+ The PyDMPushButton is meant to hold a specific value, and send that value
+ to a channel when it is clicked, much like the MessageButton does in EDM.
+ The PyDMPushButton works in two different modes of operation, first, a
+ fixed value can be given to the :attr:`.pressValue` attribute, whenever the
+ button is clicked a signal containing this value will be sent to the
+ connected channel. This is the default behavior of the button. However, if
+ the :attr:`.relativeChange` is set to True, the fixed value will be added
+ to the current value of the channel. This means that the button will
+ increment a channel by a fixed amount with every click, a consistent
+ relative move
+
+ Parameters
+ ----------
+ parent : QObject, optional
+ Parent of PyDMPushButton
+
+ label : str, optional
+ String to place on button
+
+ icon : QIcon, optional
+ An Icon to display on the PyDMPushButton
+
+ pressValue : int, float, str
+ Value to be sent when the button is clicked
+
+ relative : bool, optional
+ Choice to have the button perform a relative put, instead of always
+ setting to an absolute value
+
+ init_channel : str, optional
+ ID of channel to manipulate
+
+
+
+
+ background-color: red;
+
+
+ Stop
+
+
+ ca://${MOTOR}.STOP
+
+
+ 1
+
+
+
+
+
+
+
+ 125
+ 24
+
+
+
+
+ 125
+ 24
+
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ Engineer...
+
+
+ true
+
+
+ expert_motor.ui
+
+
+
+ {"MOTOR":"${MOTOR}"}
+
+
+
+ true
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ 0
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ false
+
+
+ ca://${MOTOR}
+
+
+ PyDMLineEdit::Decimal
+
+
+
+
+
+
+
+ 100
+ 0
+
+
+
+
+ Arial
+ 75
+ true
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+ ca://${MOTOR}.DESC
+
+
+ false
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 32
+
+
+
+
+ 32
+ 32
+
+
+
+
+
+
+
+ Widget for graphical representation of bits from an integer number
+ with support for Channels and more from PyDM
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+
+
+ false
+
+
+ ca://${MOTOR}.MOVN
+
+
+
+ 0
+ 255
+ 0
+
+
+
+
+ 100
+ 100
+ 100
+
+
+
+ Qt::Vertical
+
+
+ false
+
+
+ true
+
+
+ QTabWidget::East
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+ PyDMLabel
+ QLabel
+ pydm.widgets.label
+
+
+ PyDMByteIndicator
+ QWidget
+ pydm.widgets.byte
+
+
+ PyDMLineEdit
+ QLineEdit
+ pydm.widgets.line_edit
+
+
+ PyDMPushButton
+ QPushButton
+ pydm.widgets.pushbutton
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/examples/tutorial/main.py b/examples/tutorial/main.py
new file mode 100644
index 0000000000..5188a3205d
--- /dev/null
+++ b/examples/tutorial/main.py
@@ -0,0 +1,41 @@
+from os import path
+from pydm import Display
+from scipy.ndimage.measurements import maximum_position
+
+
+class BeamPositioning(Display):
+ def __init__(self, parent=None, args=None):
+ super(BeamPositioning, self).__init__(parent=parent, args=args)
+ # Attach our custom process_image method
+ self.ui.imageView.process_image = self.process_image
+ # Hook up to the newImageSignal so we can update
+ # our widgets after the new image is done
+ self.ui.imageView.newImageSignal.connect(self.show_blob)
+ # Store blob coordinate
+ self.blob = (0, 0)
+
+ def ui_filename(self):
+ # Point to our UI file
+ return "main.ui"
+
+ def ui_filepath(self):
+ # Return the full path to the UI file
+ return path.join(path.dirname(path.realpath(__file__)), self.ui_filename())
+
+ def show_blob(self, *args, **kwargs):
+ # If we have a blob, present the coordinates at label
+ if self.blob != (0, 0):
+ blob_txt = "Blob Found:"
+ blob_txt += " ({}, {})".format(self.blob[1], self.blob[0])
+ else:
+ # If no blob was found, present the "Not Found" message
+ blob_txt = "Blob Not Found"
+ # Update the label text
+ self.ui.lbl_blobs.setText(blob_txt)
+
+ def process_image(self, new_image):
+ # Consider the maximum as the Blob since we have only
+ # one blob.
+ self.blob = maximum_position(new_image)
+ # Send the original image data to the image widget
+ return new_image
diff --git a/examples/tutorial/main.ui b/examples/tutorial/main.ui
new file mode 100644
index 0000000000..f34f423097
--- /dev/null
+++ b/examples/tutorial/main.ui
@@ -0,0 +1,321 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 724
+ 700
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ Beam Positioning
+
+
+
+
+
+ 0
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Beam Alignment
+
+
+
+
+
+
+
+ 600
+ 480
+
+
+
+
+
+
+
+ A PyQtGraph ImageView with support for Channels and more from PyDM.
+
+ If there is no :attr:`channelWidth` it is possible to define the width of
+ the image with the :attr:`width` property.
+
+ The :attr:`normalizeData` property defines if the colors of the images are
+ relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
+ the minimum and maximum values of the image.
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+ image_channel : str, optional
+ The channel to be used by the widget for the image data.
+ width_channel : str, optional
+ The channel to be used by the widget to receive the image width
+ information
+
+
+
+ 255.000000000000000
+
+
+ true
+
+
+ PyDMImageView::Clike
+
+
+ ca://IOC:Image
+
+
+ ca://IOC:ImageWidth
+
+
+ 30
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ QLabel {
+ qproperty-alignment: AlignCenter;
+ border: 1px solid #FF17365D;
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ background-color: #FF17365D;
+ padding: 5px 0px;
+ color: rgb(255, 255, 255);
+ max-height: 25px;
+ font-size: 14px;
+}
+
+
+
+ Controls
+
+
+
+
+
+
+ QFrame#frame{
+ border: 1px solid #FF17365D;
+ border-bottom-left-radius: 15px;
+ border-bottom-right-radius: 15px;
+}
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m1"}
+
+
+ inline_motor.ui
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 700
+ 42
+
+
+
+
+ 16777215
+ 100
+
+
+
+
+
+
+
+ A QFrame capable of rendering a PyDM Display
+
+ Parameters
+ ----------
+ parent : QWidget
+ The parent widget for the Label
+
+
+
+
+ {"MOTOR":"IOC:m2"}
+
+
+ inline_motor.ui
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+ A QPushButton capable of opening a new Display at the same of at a
+ new window.
+
+ Parameters
+ ----------
+ init_channel : str, optional
+ The channel to be used by the widget.
+
+ filename : str, optional
+ The file to be opened
+
+
+
+ View All Motors
+
+
+ all_motors.py
+
+
+ false
+
+
+
+
+
+
+
+ PyDMEmbeddedDisplay
+ QFrame
+ pydm.widgets.embedded_display
+
+
+ PyDMImageView
+ QWidget
+ pydm.widgets.image
+
+
+ PyDMRelatedDisplayButton
+ QPushButton
+ pydm.widgets.related_display_button
+
+
+
+
+
diff --git a/examples/tutorial/motor_db.txt b/examples/tutorial/motor_db.txt
new file mode 100644
index 0000000000..8cc77371f2
--- /dev/null
+++ b/examples/tutorial/motor_db.txt
@@ -0,0 +1,8 @@
+IOC:m1
+IOC:m2
+IOC:m3
+IOC:m4
+IOC:m5
+IOC:m6
+IOC:m7
+IOC:m8
diff --git a/pydm/PyQt/Qt.py b/pydm/PyQt/Qt.py
index 818b1f7fee..c32aa23227 100644
--- a/pydm/PyQt/Qt.py
+++ b/pydm/PyQt/Qt.py
@@ -1 +1 @@
-from qtpy.Qt import *
+from qtpy.Qt import * # noqa: F403
diff --git a/pydm/PyQt/QtCore.py b/pydm/PyQt/QtCore.py
index d3d51d5b6f..db16a6b8f2 100644
--- a/pydm/PyQt/QtCore.py
+++ b/pydm/PyQt/QtCore.py
@@ -1 +1 @@
-from qtpy.QtCore import *
\ No newline at end of file
+from qtpy.QtCore import * # noqa: F403
diff --git a/pydm/PyQt/QtDesigner.py b/pydm/PyQt/QtDesigner.py
index 94f8af5d00..57ce90d4b6 100644
--- a/pydm/PyQt/QtDesigner.py
+++ b/pydm/PyQt/QtDesigner.py
@@ -1 +1 @@
-from qtpy.QtDesigner import *
+from qtpy.QtDesigner import * # noqa: F403
diff --git a/pydm/PyQt/QtGui.py b/pydm/PyQt/QtGui.py
index d9899b8abf..c6688c0c51 100644
--- a/pydm/PyQt/QtGui.py
+++ b/pydm/PyQt/QtGui.py
@@ -1,3 +1,2 @@
-from qtpy.QtGui import *
-from qtpy.QtWidgets import *
-from qtpy.QtCore import QItemSelection
+from qtpy.QtGui import * # noqa: F403
+from qtpy.QtWidgets import * # noqa: F403
diff --git a/pydm/PyQt/QtSvg.py b/pydm/PyQt/QtSvg.py
index 32f4807fde..ca2719c4f8 100644
--- a/pydm/PyQt/QtSvg.py
+++ b/pydm/PyQt/QtSvg.py
@@ -1 +1 @@
-from qtpy.QtSvg import *
+from qtpy.QtSvg import * # noqa: F403
diff --git a/pydm/PyQt/QtWidgets.py b/pydm/PyQt/QtWidgets.py
index 48874d777f..311f102a3e 100644
--- a/pydm/PyQt/QtWidgets.py
+++ b/pydm/PyQt/QtWidgets.py
@@ -1 +1 @@
-from qtpy.QtWidgets import *
+from qtpy.QtWidgets import * # noqa: F403
diff --git a/pydm/PyQt/__init__.py b/pydm/PyQt/__init__.py
index 96975715f4..f4f114a181 100644
--- a/pydm/PyQt/__init__.py
+++ b/pydm/PyQt/__init__.py
@@ -1,5 +1,7 @@
import warnings
+
warnings.warn(
"'pydm.PyQt' is deprecated, "
"please directly import from the 'qtpy' module. "
- "pydm.PyQt will be removed in PyDM v1.8")
\ No newline at end of file
+ "pydm.PyQt will be removed in PyDM v1.8"
+)
diff --git a/pydm/PyQt/uic.py b/pydm/PyQt/uic.py
index c1a59ddb58..cbaf1b1a9b 100644
--- a/pydm/PyQt/uic.py
+++ b/pydm/PyQt/uic.py
@@ -1 +1 @@
-from qtpy.uic import *
+from qtpy.uic import * # noqa: F403
diff --git a/pydm/__init__.py b/pydm/__init__.py
index aa065b32cd..95ba8bb95f 100644
--- a/pydm/__init__.py
+++ b/pydm/__init__.py
@@ -3,5 +3,13 @@
from .data_plugins import set_read_only
from .widgets import PyDMChannel
from ._version import get_versions
-__version__ = get_versions()['version']
+
+__all__ = [
+ "PyDMApplication",
+ "Display",
+ "set_read_only",
+ "PyDMChannel",
+]
+
+__version__ = get_versions()["version"]
del get_versions
diff --git a/pydm/_version.py b/pydm/_version.py
index 624d801f4d..5be4aa114d 100644
--- a/pydm/_version.py
+++ b/pydm/_version.py
@@ -1,4 +1,3 @@
-
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (built by setup.py sdist) and build
@@ -58,17 +57,18 @@ class NotThisMethod(Exception):
def register_vcs_handler(vcs, method): # decorator
"""Decorator to mark a method as the handler for a particular VCS."""
+
def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f
+
return decorate
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
- env=None):
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
p = None
@@ -76,10 +76,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
try:
dispcmd = str([c] + args)
# remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen([c] + args, cwd=cwd, env=env,
- stdout=subprocess.PIPE,
- stderr=(subprocess.PIPE if hide_stderr
- else None))
+ p = subprocess.Popen(
+ [c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)
+ )
break
except EnvironmentError:
e = sys.exc_info()[1]
@@ -116,16 +115,19 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
for i in range(3):
dirname = os.path.basename(root)
if dirname.startswith(parentdir_prefix):
- return {"version": dirname[len(parentdir_prefix):],
- "full-revisionid": None,
- "dirty": False, "error": None, "date": None}
+ return {
+ "version": dirname[len(parentdir_prefix) :],
+ "full-revisionid": None,
+ "dirty": False,
+ "error": None,
+ "date": None,
+ }
else:
rootdirs.append(root)
root = os.path.dirname(root) # up a level
if verbose:
- print("Tried directories %s but none started with prefix %s" %
- (str(rootdirs), parentdir_prefix))
+ print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@@ -181,7 +183,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
TAG = "tag: "
- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+ tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)])
if not tags:
# Either we're using git < 1.8.3, or there really are no tags. We use
# a heuristic: assume all version tags have a digit. The old git %d
@@ -190,7 +192,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# between branches and tags. By ignoring refnames without digits, we
# filter out many common branch names like "release" and
# "stabilization", as well as "HEAD" and "master".
- tags = set([r for r in refs if re.search(r'\d', r)])
+ tags = set([r for r in refs if re.search(r"\d", r)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
@@ -198,19 +200,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
- r = ref[len(tag_prefix):]
+ r = ref[len(tag_prefix) :]
if verbose:
print("picking %s" % r)
- return {"version": r,
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": None,
- "date": date}
+ return {
+ "version": r,
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False,
+ "error": None,
+ "date": date,
+ }
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
- return {"version": "0+unknown",
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": "no suitable tags", "date": None}
+ return {
+ "version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False,
+ "error": "no suitable tags",
+ "date": None,
+ }
@register_vcs_handler("git", "pieces_from_vcs")
@@ -225,8 +234,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
- hide_stderr=True)
+ out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
@@ -234,10 +242,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
# if there isn't one, this yields HEX[-dirty] (no NUM)
- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
- "--always", "--long",
- "--match", "%s*" % tag_prefix],
- cwd=root)
+ describe_out, rc = run_command(
+ GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root
+ )
# --long was added in git-1.5.5
if describe_out is None:
raise NotThisMethod("'git describe' failed")
@@ -260,17 +267,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
- git_describe = git_describe[:git_describe.rindex("-dirty")]
+ git_describe = git_describe[: git_describe.rindex("-dirty")]
# now we have TAG-NUM-gHEX or HEX
if "-" in git_describe:
# TAG-NUM-gHEX
- mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
- pieces["error"] = ("unable to parse git-describe output: '%s'"
- % describe_out)
+ pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
return pieces
# tag
@@ -279,10 +285,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
- pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
- % (full_tag, tag_prefix))
+ pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)
return pieces
- pieces["closest-tag"] = full_tag[len(tag_prefix):]
+ pieces["closest-tag"] = full_tag[len(tag_prefix) :]
# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
@@ -293,13 +298,11 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
else:
# HEX: no tags
pieces["closest-tag"] = None
- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
- cwd=root)
+ count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
pieces["distance"] = int(count_out) # total number of commits
# commit date: see ISO-8601 comment in git_versions_from_keywords()
- date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
- cwd=root)[0].strip()
+ date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
return pieces
@@ -330,8 +333,7 @@ def render_pep440(pieces):
rendered += ".dirty"
else:
# exception #1
- rendered = "0+untagged.%d.g%s" % (pieces["distance"],
- pieces["short"])
+ rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
@@ -445,11 +447,13 @@ def render_git_describe_long(pieces):
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
- return {"version": "unknown",
- "full-revisionid": pieces.get("long"),
- "dirty": None,
- "error": pieces["error"],
- "date": None}
+ return {
+ "version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"],
+ "date": None,
+ }
if not style or style == "default":
style = "pep440" # the default
@@ -469,9 +473,13 @@ def render(pieces, style):
else:
raise ValueError("unknown style '%s'" % style)
- return {"version": rendered, "full-revisionid": pieces["long"],
- "dirty": pieces["dirty"], "error": None,
- "date": pieces.get("date")}
+ return {
+ "version": rendered,
+ "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"],
+ "error": None,
+ "date": pieces.get("date"),
+ }
def get_versions():
@@ -485,8 +493,7 @@ def get_versions():
verbose = cfg.verbose
try:
- return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
- verbose)
+ return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose)
except NotThisMethod:
pass
@@ -495,13 +502,16 @@ def get_versions():
# versionfile_source is the relative path from the top of the source
# tree (where the .git directory might live) to this file. Invert
# this to find the root from __file__.
- for i in cfg.versionfile_source.split('/'):
+ for i in cfg.versionfile_source.split("/"):
root = os.path.dirname(root)
except NameError:
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None,
- "error": "unable to find root of source tree",
- "date": None}
+ return {
+ "version": "0+unknown",
+ "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to find root of source tree",
+ "date": None,
+ }
try:
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
@@ -515,6 +525,10 @@ def get_versions():
except NotThisMethod:
pass
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None,
- "error": "unable to compute version", "date": None}
+ return {
+ "version": "0+unknown",
+ "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to compute version",
+ "date": None,
+ }
diff --git a/pydm/about_pydm/__init__.py b/pydm/about_pydm/__init__.py
index 2980dc534a..034a88ef00 100644
--- a/pydm/about_pydm/__init__.py
+++ b/pydm/about_pydm/__init__.py
@@ -1 +1,5 @@
-from .about import AboutWindow
\ No newline at end of file
+from .about import AboutWindow
+
+__all__ = [
+ "AboutWindow",
+]
diff --git a/pydm/about_pydm/about.py b/pydm/about_pydm/about.py
index 283ebb2102..292aff1a82 100644
--- a/pydm/about_pydm/about.py
+++ b/pydm/about_pydm/about.py
@@ -1,5 +1,4 @@
-from qtpy import uic
-from qtpy.QtWidgets import QWidget, QApplication, QTableWidgetItem
+from qtpy.QtWidgets import QWidget, QTableWidgetItem
from qtpy.QtCore import Qt, PYQT_VERSION_STR, qVersion
from .about_ui import Ui_Form
from numpy import __version__ as numpyver
@@ -17,11 +16,11 @@ def __init__(self, parent=None):
self.ui.setupUi(self)
self.ui.pydmVersionLabel.setText(str(self.ui.pydmVersionLabel.text()).format(version=pydm.__version__))
pyver = ".".join([str(v) for v in sys.version_info[0:3]])
- self.ui.modulesVersionLabel.setText(str(self.ui.modulesVersionLabel.text()).format(pyver=pyver,
- numpyver=numpyver,
- pyqtgraphver=pyqtgraphver,
- pyqtver=PYQT_VERSION_STR,
- qtver=qVersion()))
+ self.ui.modulesVersionLabel.setText(
+ str(self.ui.modulesVersionLabel.text()).format(
+ pyver=pyver, numpyver=numpyver, pyqtgraphver=pyqtgraphver, pyqtver=PYQT_VERSION_STR, qtver=qVersion()
+ )
+ )
self.populate_external_tools_list()
self.populate_plugin_list()
self.populate_contributor_list()
@@ -35,15 +34,15 @@ def populate_external_tools_list(self):
self.add_tools_to_list(pydm.tools.ext_tools)
def add_tools_to_list(self, tools):
- for (name, tool) in tools.items():
+ for name, tool in tools.items():
if isinstance(tool, dict):
self.add_tools_to_list(tool)
else:
tool_info = tool.get_info()
- name_item = QTableWidgetItem(tool_info.get("name","None"))
- group_item = QTableWidgetItem(tool_info.get("group","None"))
- author_item = QTableWidgetItem(tool_info.get("author","None"))
- file_item = QTableWidgetItem(tool_info.get("file","None"))
+ name_item = QTableWidgetItem(tool_info.get("name", "None"))
+ group_item = QTableWidgetItem(tool_info.get("group", "None"))
+ author_item = QTableWidgetItem(tool_info.get("author", "None"))
+ file_item = QTableWidgetItem(tool_info.get("file", "None"))
new_row = self.ui.externalToolsTableWidget.rowCount()
self.ui.externalToolsTableWidget.insertRow(new_row)
self.ui.externalToolsTableWidget.setItem(new_row, 0, name_item)
@@ -58,7 +57,7 @@ def populate_plugin_list(self):
self.ui.dataPluginsTableWidget.horizontalHeader().setStretchLastSection(True)
self.ui.dataPluginsTableWidget.verticalHeader().setVisible(False)
pydm.data_plugins.initialize_plugins_if_needed()
- for (protocol, plugin) in pydm.data_plugins.plugin_modules.items():
+ for protocol, plugin in pydm.data_plugins.plugin_modules.items():
protocol_item = QTableWidgetItem(protocol)
file_item = QTableWidgetItem(inspect.getfile(plugin.__class__))
new_row = self.ui.dataPluginsTableWidget.rowCount()
diff --git a/pydm/about_pydm/about.ui b/pydm/about_pydm/about.ui
index 52fbd03b1f..53569921dc 100644
--- a/pydm/about_pydm/about.ui
+++ b/pydm/about_pydm/about.ui
@@ -94,7 +94,7 @@
- Using Python v{pyver}, Numpy v{numpyver}, PyQt v{pyqtver}, Qt v{qtver}, and PyQtGraph v{pyqtgraphver}.
+ Using Python v{pyver}, Numpy v{numpyver}, PyQt v{pyqtver}, Qt v{qtver},
and PyQtGraph v{pyqtgraphver}.Qt::AlignCenter
diff --git a/pydm/about_pydm/about_ui.py b/pydm/about_pydm/about_ui.py
index 92a1cca4eb..ad11be6548 100644
--- a/pydm/about_pydm/about_ui.py
+++ b/pydm/about_pydm/about_ui.py
@@ -8,6 +8,7 @@
from qtpy import QtCore, QtWidgets, QtGui
+
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
@@ -95,9 +96,14 @@ def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "About PyDM"))
self.pydmLabel.setText(_translate("Form", "PyDM"))
self.pydmVersionLabel.setText(_translate("Form", "Version {version}"))
- self.modulesVersionLabel.setText(_translate("Form", "Using Python v{pyver}, Numpy v{numpyver}, PyQt v{pyqtver}, Qt v{qtver}, and PyQtGraph v{pyqtgraphver}."))
+ self.modulesVersionLabel.setText(
+ _translate(
+ "Form",
+ """Using Python v{pyver}, Numpy v{numpyver}, PyQt v{pyqtver}, Qt v{qtver},
+ and PyQtGraph v{pyqtgraphver}.""",
+ )
+ )
self.tabWidget.setTabText(self.tabWidget.indexOf(self.versionTab), _translate("Form", "Version"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.externalToolsTab), _translate("Form", "External Tools"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.dataPluginsTab), _translate("Form", "Data Plugins"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.contributorsTab), _translate("Form", "Contributors"))
-
diff --git a/pydm/about_pydm/contributors.txt b/pydm/about_pydm/contributors.txt
index 93ca85aa16..f64406fb0f 100644
--- a/pydm/about_pydm/contributors.txt
+++ b/pydm/about_pydm/contributors.txt
@@ -1,22 +1,41 @@
+Development Team
+Jesse Bellister (@jbellister-slac, jesseb@slac.stanford.edu)
+Nolan Stelter (@nstelter-slac, nstelter@slac.stanford.edu)
+Yekta Yazar (@YektaY, yazar@slac.stanford.edu)
Matt Gibbs (@mattgibbs, mgibbs@slac.stanford.edu)
-Hugo Slepicka (@hhslepicka, slepicka@slac.stanford.edu)
-@ZLLentz
+
+Contributors
@teddyrendahl
-@gabrielfedel
+@hhslepicka
+@hmbui
+@ZLLentz
+@klauer
+@cristinasewell
@fernandohds564
+@Ryan-McClanahan
@laispc
-@ninenein
-@xresende
+@mcb64
+@prjemian
+@tacaswell
+@colbychang
+@jacquelinegarrahan
@tcope
-@gu1lermelp
-@scalverson
-@hmbui
-@klauer
@anacso17
-@tacaswell
-@prjemian
-@KurtJacobson
+@flowln
+@ninenein
+@lvhuihui1988
+@herodotus77
+@gabrielfedel
+@justincslac
+@c-tsoi
+@anleslac
+@kleleux
+@vespos
@ivany4
-@cristinasewell
-@jacquelinegarrahan
-@YektaY
\ No newline at end of file
+@scalverson
+@KurtJacobson
+@xresende
+@vosarah
+@tangkong
+@ronpandolfi
+@gu1lermelp
diff --git a/pydm/application.py b/pydm/application.py
index e3afcaf60d..74671d6d50 100644
--- a/pydm/application.py
+++ b/pydm/application.py
@@ -12,7 +12,7 @@
import warnings
from qtpy.QtCore import Qt, QTimer, Slot
-from qtpy.QtWidgets import QApplication, QWidget
+from qtpy.QtWidgets import QApplication
from .main_window import PyDMMainWindow
from .utilities import which, path_info
@@ -69,16 +69,29 @@ class PyDMApplication(QApplication):
homefile : str, optional
The path to a PyDM file to return to whenever the home button is clicked in the navigation bar.
"""
+
# Instantiate our plugins.
plugins = data_plugins.plugin_modules
- def __init__(self, ui_file=None, command_line_args=[], display_args=[],
- perfmon=False, hide_nav_bar=False, hide_menu_bar=False,
- hide_status_bar=False, read_only=False, macros=None,
- use_main_window=True, stylesheet_path=None, fullscreen=False, home_file=None):
+ def __init__(
+ self,
+ ui_file=None,
+ command_line_args=[],
+ display_args=[],
+ perfmon=False,
+ hide_nav_bar=False,
+ hide_menu_bar=False,
+ hide_status_bar=False,
+ read_only=False,
+ macros=None,
+ use_main_window=True,
+ stylesheet_path=None,
+ fullscreen=False,
+ home_file=None,
+ ):
super(PyDMApplication, self).__init__(command_line_args)
# Enable High DPI display, if available.
- if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
+ if hasattr(Qt, "AA_UseHighDpiPixmaps"):
self.setAttribute(Qt.AA_UseHighDpiPixmaps)
data_plugins.set_read_only(read_only)
@@ -100,12 +113,20 @@ def __init__(self, ui_file=None, command_line_args=[], display_args=[],
# Open a window if required.
if ui_file is not None:
- self.make_main_window(stylesheet_path=stylesheet_path, home_file=self.home_file,
- macros=macros, command_line_args=command_line_args)
+ self.make_main_window(
+ stylesheet_path=stylesheet_path,
+ home_file=self.home_file,
+ macros=macros,
+ command_line_args=command_line_args,
+ )
self.main_window.open(ui_file, macros, command_line_args)
elif use_main_window:
- self.make_main_window(stylesheet_path=stylesheet_path, home_file=self.home_file,
- macros=macros, command_line_args=command_line_args)
+ self.make_main_window(
+ stylesheet_path=stylesheet_path,
+ home_file=self.home_file,
+ macros=macros,
+ command_line_args=command_line_args,
+ )
self.had_file = ui_file is not None
# Re-enable sigint (usually blocked by pyqt)
@@ -114,6 +135,7 @@ def __init__(self, ui_file=None, command_line_args=[], display_args=[],
# Performance monitoring
if perfmon:
import psutil
+
self.perf = psutil.Process()
self.perf_timer = QTimer()
self.perf_timer.setInterval(2000)
@@ -130,8 +152,7 @@ def exec_(self):
return super(PyDMApplication, self).exec_()
def is_read_only(self):
- warnings.warn("'PyDMApplication.is_read_only' is deprecated, "
- "use 'pydm.data_plugins.is_read_only' instead.")
+ warnings.warn("'PyDMApplication.is_read_only' is deprecated, " "use 'pydm.data_plugins.is_read_only' instead.")
return data_plugins.is_read_only()
@Slot()
@@ -178,7 +199,9 @@ def new_pydm_process(self, ui_file, macros=None, command_line_args=None):
else:
# Not in the PATH and no ENV VAR pointing to it...
# Let's try the script folder...
- pydm_display_app_path = os.path.join(os.path.split(os.path.realpath(__file__))[0], "..", "pydm_launcher", "main.py")
+ pydm_display_app_path = os.path.join(
+ os.path.split(os.path.realpath(__file__))[0], "..", "pydm_launcher", "main.py"
+ )
args = [pydm_display_app_path]
if self.hide_nav_bar:
@@ -214,12 +237,14 @@ def make_main_window(self, stylesheet_path=None, home_file=None, macros=None, co
of starting up a new process, because PyDMApplications only have
one window per process.
"""
- main_window = PyDMMainWindow(hide_nav_bar=self.hide_nav_bar,
- hide_menu_bar=self.hide_menu_bar,
- hide_status_bar=self.hide_status_bar,
- home_file=home_file,
- macros=macros,
- command_line_args=command_line_args)
+ main_window = PyDMMainWindow(
+ hide_nav_bar=self.hide_nav_bar,
+ hide_menu_bar=self.hide_menu_bar,
+ hide_status_bar=self.hide_status_bar,
+ home_file=home_file,
+ macros=macros,
+ command_line_args=command_line_args,
+ )
self.main_window = main_window
apply_stylesheet(stylesheet_path, widget=self.main_window)
@@ -242,8 +267,9 @@ def plugin_for_channel(self, channel):
-------
PyDMPlugin
"""
- warnings.warn("'PyDMApplication.plugin_for_channel' is deprecated, "
- "use 'pydm.data_plugins.plugin_for_address' instead.")
+ warnings.warn(
+ "'PyDMApplication.plugin_for_channel' is deprecated, " "use 'pydm.data_plugins.plugin_for_address' instead."
+ )
if channel.address is None or channel.address == "":
return None
return data_plugins.plugin_for_address(channel.address)
@@ -256,8 +282,7 @@ def add_connection(self, channel):
----------
channel : PyDMChannel
"""
- warnings.warn("'PyDMApplication.add_connection' is deprecated, "
- "use PyDMConnection.connect()")
+ warnings.warn("'PyDMApplication.add_connection' is deprecated, " "use PyDMConnection.connect()")
channel.connect()
def remove_connection(self, channel):
@@ -268,18 +293,17 @@ def remove_connection(self, channel):
----------
channel : PyDMChannel
"""
- warnings.warn("'PyDMApplication.remove_connection' is deprecated, "
- "use PyDMConnection.disconnect()")
+ warnings.warn("'PyDMApplication.remove_connection' is deprecated, " "use PyDMConnection.disconnect()")
channel.disconnect()
def eventFilter(self, obj, event):
- warnings.warn("'PyDMApplication.eventFilter' is deprecated, "
- " this function is now found on PyDMWidget")
+ warnings.warn("'PyDMApplication.eventFilter' is deprecated, " " this function is now found on PyDMWidget")
obj.eventFilter(obj, event)
def show_address_tooltip(self, obj, event):
- warnings.warn("'PyDMApplication.show_address_tooltip' is deprecated, "
- " this function is now found on PyDMWidget")
+ warnings.warn(
+ "'PyDMApplication.show_address_tooltip' is deprecated, " " this function is now found on PyDMWidget"
+ )
obj.show_address_tooltip(event)
def establish_widget_connections(self, widget):
@@ -294,8 +318,10 @@ def establish_widget_connections(self, widget):
----------
widget : QWidget
"""
- warnings.warn("'PyDMApplication.establish_widget_connections' is deprecated, "
- "this function is now found on `utilities.establish_widget_connections`.")
+ warnings.warn(
+ "'PyDMApplication.establish_widget_connections' is deprecated, "
+ "this function is now found on `utilities.establish_widget_connections`."
+ )
connection.establish_widget_connections(widget)
def close_widget_connections(self, widget):
@@ -309,5 +335,6 @@ def close_widget_connections(self, widget):
"""
warnings.warn(
"'PyDMApplication.close_widget_connections' is deprecated, "
- "this function is now found on `utilities.close_widget_connections`.")
+ "this function is now found on `utilities.close_widget_connections`."
+ )
connection.close_widget_connections(widget)
diff --git a/pydm/config.py b/pydm/config.py
index 32b5628f16..75a052a914 100644
--- a/pydm/config.py
+++ b/pydm/config.py
@@ -1,11 +1,6 @@
import os
-__all__ = ['DEFAULT_PROTOCOL',
- 'DESIGNER_ONLINE',
- 'STYLESHEET',
- 'STYLESHEET_INCLUDE_DEFAULT',
- 'CONFIRM_QUIT'
- ]
+__all__ = ["DEFAULT_PROTOCOL", "DESIGNER_ONLINE", "STYLESHEET", "STYLESHEET_INCLUDE_DEFAULT", "CONFIRM_QUIT"]
DEFAULT_PROTOCOL = os.getenv("PYDM_DEFAULT_PROTOCOL")
diff --git a/pydm/connection_inspector/__init__.py b/pydm/connection_inspector/__init__.py
index 4405c8aa5f..d1519453ee 100644
--- a/pydm/connection_inspector/__init__.py
+++ b/pydm/connection_inspector/__init__.py
@@ -1 +1,5 @@
-from .connection_inspector import ConnectionInspector
\ No newline at end of file
+from .connection_inspector import ConnectionInspector
+
+__all__ = [
+ "ConnectionInspector",
+]
diff --git a/pydm/connection_inspector/connection_inspector.py b/pydm/connection_inspector/connection_inspector.py
index b950f90801..0412772669 100644
--- a/pydm/connection_inspector/connection_inspector.py
+++ b/pydm/connection_inspector/connection_inspector.py
@@ -1,7 +1,17 @@
import platform
-from qtpy.QtWidgets import (QWidget, QTableView, QAbstractItemView, QHBoxLayout,
- QVBoxLayout, QAbstractScrollArea, QPushButton,
- QApplication, QFileDialog, QMessageBox, QLabel)
+from qtpy.QtWidgets import (
+ QWidget,
+ QTableView,
+ QAbstractItemView,
+ QHBoxLayout,
+ QVBoxLayout,
+ QAbstractScrollArea,
+ QPushButton,
+ QApplication,
+ QFileDialog,
+ QMessageBox,
+ QLabel,
+)
from qtpy.QtCore import Qt, Slot, QTimer
from .connection_table_model import ConnectionTableModel
from .. import data_plugins
@@ -38,24 +48,21 @@ def update_data(self):
def fetch_data(self):
plugins = data_plugins.plugin_modules
- return [connection
- for p in plugins.values()
- for connection in p.connections.values()
- # DISP field is connected to separately for writable channels, including it on this list is redundant
- if not connection.address.endswith('.DISP')
- ]
+ return [
+ connection
+ for p in plugins.values()
+ for connection in p.connections.values()
+ # DISP field is connected to separately for writable channels, including it on this list is redundant
+ if not connection.address.endswith(".DISP")
+ ]
@Slot()
def save_list_to_file(self):
- filename, filters = QFileDialog.getSaveFileName(self,
- "Save connection list",
- "",
- "Text Files (*.txt)")
+ filename, filters = QFileDialog.getSaveFileName(self, "Save connection list", "", "Text Files (*.txt)")
try:
with open(filename, "w") as f:
for conn in self.table_view.model().connections:
- f.write(
- "{p}://{a}\n".format(p=conn.protocol, a=conn.address))
+ f.write("{p}://{a}\n".format(p=conn.protocol, a=conn.address))
self.save_status_label.setText("File saved to {}".format(filename))
except Exception as e:
msgBox = QMessageBox()
@@ -66,14 +73,14 @@ def save_list_to_file(self):
@Slot()
def copy_pv_list_to_clipboard(self):
- """ Copy the list of PVs from the table to the clipboard """
+ """Copy the list of PVs from the table to the clipboard"""
pv_list = [connection.address for connection in self.table_view.model().connections]
if len(pv_list) == 0:
return
pvs_to_copy = " ".join(pv_list)
clipboard = QApplication.clipboard()
- if platform.system() == 'Linux':
+ if platform.system() == "Linux":
# Mode Selection is only valid for X11.
clipboard.setText(pvs_to_copy, clipboard.Selection)
clipboard.setText(pvs_to_copy, clipboard.Clipboard)
@@ -82,8 +89,7 @@ def copy_pv_list_to_clipboard(self):
class ConnectionTableView(QTableView):
def __init__(self, connections=[], parent=None):
super(ConnectionTableView, self).__init__(parent)
- self.setSizeAdjustPolicy(
- QAbstractScrollArea.AdjustToContentsOnFirstShow)
+ self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContentsOnFirstShow)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.horizontalHeader().setStretchLastSection(True)
self.setSelectionMode(QAbstractItemView.SingleSelection)
diff --git a/pydm/connection_inspector/connection_table_model.py b/pydm/connection_inspector/connection_table_model.py
index 91d7e61ee2..322d25ebc2 100644
--- a/pydm/connection_inspector/connection_table_model.py
+++ b/pydm/connection_inspector/connection_table_model.py
@@ -1,7 +1,7 @@
-from qtpy.QtCore import QAbstractTableModel, Qt, QModelIndex, QVariant, QTimer, Slot
-from qtpy.QtGui import QBrush
+from qtpy.QtCore import QAbstractTableModel, Qt, QVariant, QTimer, Slot
from operator import attrgetter
+
class ConnectionTableModel(QAbstractTableModel):
def __init__(self, connections=[], parent=None):
super(ConnectionTableModel, self).__init__(parent=parent)
@@ -10,12 +10,12 @@ def __init__(self, connections=[], parent=None):
self.update_timer.setInterval(1000)
self.update_timer.timeout.connect(self.update_values)
self.connections = connections
-
+
def sort(self, col, order=Qt.AscendingOrder):
if self._column_names[col] == "value":
return
self.layoutAboutToBeChanged.emit()
- sort_reversed = (order == Qt.AscendingOrder)
+ sort_reversed = order == Qt.AscendingOrder
self._connections.sort(key=attrgetter(self._column_names[col]), reverse=sort_reversed)
self.layoutChanged.emit()
@@ -61,14 +61,14 @@ def data(self, index, role=Qt.DisplayRole):
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
- return super(ConnectionTableModel, self).headerData(
- section, orientation, role)
+ return super(ConnectionTableModel, self).headerData(section, orientation, role)
if orientation == Qt.Horizontal and section < self.columnCount():
return str(self._column_names[section]).capitalize()
elif orientation == Qt.Vertical and section < self.rowCount():
return section
+
# End QAbstractItemModel implementation.
@Slot()
def update_values(self):
- self.dataChanged.emit(self.index(0,2), self.index(self.rowCount(),2))
\ No newline at end of file
+ self.dataChanged.emit(self.index(0, 2), self.index(self.rowCount(), 2))
diff --git a/pydm/data_plugins/__init__.py b/pydm/data_plugins/__init__.py
index 16c2ba9e41..9f931153db 100644
--- a/pydm/data_plugins/__init__.py
+++ b/pydm/data_plugins/__init__.py
@@ -14,7 +14,7 @@
from qtpy.QtWidgets import QApplication
from .. import config
-from ..utilities import (import_module_by_filename, log_failures, parsed_address)
+from ..utilities import import_module_by_filename, log_failures, parsed_address
from .plugin import PyDMPlugin
logger = logging.getLogger(__name__)
@@ -46,8 +46,7 @@ def establish_queued_connections():
if __CONNECTION_QUEUE__ is None:
return
try:
- while (__CONNECTION_QUEUE__ is not None and
- len(__CONNECTION_QUEUE__) > 0):
+ while __CONNECTION_QUEUE__ is not None and len(__CONNECTION_QUEUE__) > 0:
channel = __CONNECTION_QUEUE__.popleft()
establish_connection_immediately(channel)
QApplication.instance().processEvents()
@@ -79,17 +78,16 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]:
try:
protocol = parsed_address(address).scheme
except AttributeError:
- protocol = None
-
+ protocol = None
+
# Use default protocol
if protocol is None and config.DEFAULT_PROTOCOL is not None:
- logger.debug("Using default protocol %s for %s",
- config.DEFAULT_PROTOCOL, address)
+ logger.debug("Using default protocol %s for %s", config.DEFAULT_PROTOCOL, address)
# 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,12 +95,14 @@ def plugin_for_address(address: str) -> Optional[PyDMPlugin]:
except KeyError:
logger.exception("Could not find protocol for %r", address)
# Catch all in case of improper plugin specification
- logger.error("Channel {addr} did not specify a valid protocol "
- "and no default protocol is defined. This channel "
- "will receive no data. To specify a default protocol, "
- "set the PYDM_DEFAULT_PROTOCOL environment variable."
- "".format(addr=address))
-
+ logger.error(
+ "Channel {addr} did not specify a valid protocol "
+ "and no default protocol is defined. This channel "
+ "will receive no data. To specify a default protocol, "
+ "set the PYDM_DEFAULT_PROTOCOL environment variable."
+ "".format(addr=address)
+ )
+
return None
@@ -140,10 +140,7 @@ def add_plugin(plugin: Type[PyDMPlugin]) -> Optional[PyDMPlugin]:
@log_failures(
logger,
- explanation=(
- "Unable to import plugin file: {args[0]}. "
- "This plugin will be skipped."
- ),
+ explanation=("Unable to import plugin file: {args[0]}. " "This plugin will be skipped."),
include_traceback=True,
)
def _get_plugins_from_source(source_filename: str) -> List[Type[PyDMPlugin]]:
@@ -161,13 +158,7 @@ def _get_plugins_from_source(source_filename: str) -> List[Type[PyDMPlugin]]:
The plugin classes.
"""
module = import_module_by_filename(source_filename)
- return list(
- set(
- obj
- for _, obj in inspect.getmembers(module)
- if _is_valid_plugin_class(obj)
- )
- )
+ return list(set(obj for _, obj in inspect.getmembers(module) if _is_valid_plugin_class(obj)))
def find_plugins_from_path(
@@ -213,18 +204,11 @@ def find_plugins_from_entrypoints(
try:
plugin_cls = entry.load()
except Exception as ex:
- logger.exception(
- "Failed to load %s entry %s: %s",
- key, entry.name, ex
- )
+ logger.exception("Failed to load %s entry %s: %s", key, entry.name, ex)
continue
if not _is_valid_plugin_class(plugin_cls):
- logger.warning(
- "Invalid plugin class specified in entrypoint "
- "%s: %s",
- entry.name, plugin_cls
- )
+ logger.warning("Invalid plugin class specified in entrypoint " "%s: %s", entry.name, plugin_cls)
continue
yield plugin_cls
@@ -232,16 +216,10 @@ def find_plugins_from_entrypoints(
def _is_valid_plugin_class(obj: Any) -> bool:
"""Is the object a data plugin class?"""
- return (
- inspect.isclass(obj)
- and issubclass(obj, PyDMPlugin)
- and obj is not PyDMPlugin
- )
+ return inspect.isclass(obj) and issubclass(obj, PyDMPlugin) and obj is not PyDMPlugin
-def load_plugins_from_entrypoints(
- key: str = config.ENTRYPOINT_DATA_PLUGIN
-) -> Dict[str, PyDMPlugin]:
+def load_plugins_from_entrypoints(key: str = config.ENTRYPOINT_DATA_PLUGIN) -> Dict[str, PyDMPlugin]:
"""
Load plugins from file locations that match a specific token
@@ -270,10 +248,7 @@ def load_plugins_from_entrypoints(
return added_plugins
-def load_plugins_from_path(
- locations: List[str],
- token: str = config.DATA_PLUGIN_SUFFIX
-) -> Dict[str, PyDMPlugin]:
+def load_plugins_from_path(locations: List[str], token: str = config.DATA_PLUGIN_SUFFIX) -> Dict[str, PyDMPlugin]:
"""
Load plugins from file locations that match a specific token
@@ -344,9 +319,9 @@ def initialize_plugins_if_needed():
__plugins_initialized = True
# Load the data plugins from PYDM_DATA_PLUGINS_PATH
- logger.debug("*"*80)
+ logger.debug("*" * 80)
logger.debug("* Loading PyDM Data Plugins")
- logger.debug("*"*80)
+ logger.debug("*" * 80)
path = os.getenv("PYDM_DATA_PLUGINS_PATH", None)
if path is None:
diff --git a/pydm/data_plugins/archiver_plugin.py b/pydm/data_plugins/archiver_plugin.py
index 8aea8a4fa4..3eb43c69ed 100644
--- a/pydm/data_plugins/archiver_plugin.py
+++ b/pydm/data_plugins/archiver_plugin.py
@@ -19,8 +19,9 @@ class Connection(PyDMConnection):
Manages the requests between the archiver data plugin and the archiver appliance itself.
"""
- def __init__(self, channel: PyDMChannel, address: str, protocol: Optional[str] = None,
- parent: Optional[QObject] = None):
+ def __init__(
+ self, channel: PyDMChannel, address: str, protocol: Optional[str] = None, parent: Optional[QObject] = None
+ ):
super().__init__(channel, address, protocol, parent)
self.add_listener(channel)
self.address = address
@@ -73,8 +74,10 @@ def fetch_data(self, from_date: float, to_date: float, processing_command: Optio
base_url = os.getenv("PYDM_ARCHIVER_URL")
if base_url is None:
- logger.error("Environment variable: PYDM_ARCHIVER_URL must be defined to use the archiver plugin, for "
- "example: http://lcls-archapp.slac.stanford.edu")
+ logger.error(
+ "Environment variable: PYDM_ARCHIVER_URL must be defined to use the archiver plugin, for "
+ "example: http://lcls-archapp.slac.stanford.edu"
+ )
return
url_string = f"{base_url}/retrieval/data/getData.json?{self.address}&from={from_date_str}&to={to_date_str}"
@@ -97,17 +100,22 @@ def data_request_finished(self, reply: QNetworkReply) -> None:
----------
reply: The response from the archiver appliance
"""
- if reply.error() == QNetworkReply.NoError and reply.header(QNetworkRequest.ContentTypeHeader) == "application/json":
+ if (
+ reply.error() == QNetworkReply.NoError
+ and reply.header(QNetworkRequest.ContentTypeHeader) == "application/json"
+ ):
bytes_str = reply.readAll()
- data_dict = json.loads(str(bytes_str, 'utf-8'))
+ data_dict = json.loads(str(bytes_str, "utf-8"))
if "pv=optimized" in reply.url().url(): # From a url object to a string
self._send_optimized_data(data_dict)
else:
self._send_raw_data(data_dict)
else:
- logger.debug(f"Request for data from archiver failed, request url: {reply.url()} retrieved header: "
- f"{reply.header(QNetworkRequest.ContentTypeHeader)} error: {reply.error()}")
+ logger.debug(
+ f"Request for data from archiver failed, request url: {reply.url()} retrieved header: "
+ f"{reply.header(QNetworkRequest.ContentTypeHeader)} error: {reply.error()}"
+ )
reply.deleteLater()
def _send_raw_data(self, data_dict: dict) -> None:
@@ -115,8 +123,9 @@ def _send_raw_data(self, data_dict: dict) -> None:
Sends a numpy array of shape (2, data_length) containing the x-values (timestamps) and y-values (PV data)
via the new value signal
"""
- data = np.array(([point["secs"] for point in data_dict[0]["data"]],
- [point["val"] for point in data_dict[0]["data"]]))
+ data = np.array(
+ ([point["secs"] for point in data_dict[0]["data"]], [point["val"] for point in data_dict[0]["data"]])
+ )
self.new_value_signal[np.ndarray].emit(data)
def _send_optimized_data(self, data_dict: dict) -> None:
@@ -126,11 +135,15 @@ def _send_optimized_data(self, data_dict: dict) -> None:
"""
pv_data = [point["val"] for point in data_dict[0]["data"]]
try:
- data = np.array(([point["secs"] for point in data_dict[0]["data"]],
- [point[0] for point in pv_data],
- [point[1] for point in pv_data],
- [point[2] for point in pv_data],
- [point[3] for point in pv_data]))
+ data = np.array(
+ (
+ [point["secs"] for point in data_dict[0]["data"]],
+ [point[0] for point in pv_data],
+ [point[1] for point in pv_data],
+ [point[2] for point in pv_data],
+ [point[3] for point in pv_data],
+ )
+ )
except TypeError:
# The archiver will fall back to sending raw data if the optimized request is for more data points
# than are in the bin
diff --git a/pydm/data_plugins/calc_plugin.py b/pydm/data_plugins/calc_plugin.py
index 6018397460..6d6cf2c8fe 100644
--- a/pydm/data_plugins/calc_plugin.py
+++ b/pydm/data_plugins/calc_plugin.py
@@ -27,7 +27,7 @@ def epics_string(value: np.ndarray, string_encoding: str = "utf-8") -> str:
# Assume the ndarray is one-dimensional
value = value.tobytes()
try:
- value = value[:value.index(0)]
+ value = value[: value.index(0)]
except (IndexError, ValueError):
pass
return value.decode(string_encoding, "replace") # <-- ignore decoding errors, just in case
@@ -48,13 +48,9 @@ def epics_unsigned(value: int, bits: int = 32) -> int:
class CalcThread(QThread):
- eval_env = {'math': math,
- 'np': np,
- 'numpy': np,
- 'epics_string': epics_string,
- 'epics_unsigned': epics_unsigned}
+ eval_env = {"math": math, "np": np, "numpy": np, "epics_string": epics_string, "epics_unsigned": epics_unsigned}
- eval_env.update({k: v for k, v in math.__dict__.items() if k[0] != '_'})
+ eval_env.update({k: v for k, v in math.__dict__.items() if k[0] != "_"})
new_data_signal = Signal(dict)
RESERVED_FIELD = ["update", "expr", "name"]
@@ -72,24 +68,23 @@ def __init__(self, config, *args, **kwargs):
self._value = None
self._values = collections.defaultdict(lambda: None)
self._connections = collections.defaultdict(lambda: False)
- self._expression = self.config.get('expr', '')[0]
+ self._expression = self.config.get("expr", "")[0]
channels = {}
for key, channel in self.config.items():
if key not in CalcThread.RESERVED_FIELD:
channels[key] = channel[0]
- update = self.config.get('update', None)
+ update = self.config.get("update", None)
if update is not None:
- self.listen_for_update = update[0].split(',')
+ self.listen_for_update = update[0].split(",")
self.listen_for_update = list(map(str.strip, self.listen_for_update))
for name, channel in channels.items():
conn_cb = functools.partial(self.callback_conn, name)
value_cb = functools.partial(self.callback_value, name)
- c = pydm.PyDMChannel(channel, connection_slot=conn_cb,
- value_slot=value_cb)
+ c = pydm.PyDMChannel(channel, connection_slot=conn_cb, value_slot=value_cb)
self._channels.append(c)
self._names.append(name)
@@ -106,8 +101,7 @@ def _disconnect(self):
ch.disconnect()
def _send_update(self, conn, value):
- self.new_data_signal.emit({"connection": conn,
- "value": value})
+ self.new_data_signal.emit({"connection": conn, "value": value})
def run(self):
self._connect()
@@ -137,9 +131,7 @@ def callback_value(self, name, value):
"""
self._values[name] = value
if not self.connected:
- logger.debug(
- "Calculation '%s': Not all channels are connected, skipping execution.",
- self.objectName())
+ logger.debug("Calculation '%s': Not all channels are connected, skipping execution.", self.objectName())
return
if self.listen_for_update is None or name in self.listen_for_update:
@@ -167,20 +159,19 @@ def calculate_expression(self):
"""
vals = self._values.copy()
if any([vals.get(n) is None for n in self._names]):
- logger.debug('Skipping execution as not all values are set.')
+ logger.debug("Skipping execution as not all values are set.")
return
env = dict(CalcThread.eval_env)
env.update(**vals)
- env.update({'prev_res': self._value})
+ env.update({"prev_res": self._value})
try:
ret = eval(self._expression, env)
self._value = ret
self._send_update(self.connected, ret)
- except Exception as e:
- logger.exception("Error while evaluating CalcPlugin connection %s",
- self.objectName())
+ except Exception:
+ logger.exception("Error while evaluating CalcPlugin connection %s", self.objectName())
class Connection(PyDMConnection):
@@ -209,23 +200,22 @@ def broadcast_value(self):
def _setup_calc(self, channel):
if not self._waiting_config:
- logger.debug('CalcPlugin connection already configured.')
+ logger.debug("CalcPlugin connection already configured.")
return
try:
url_data = UrlToPython(channel)
except ValueError("Not enough information"):
- logger.debug('Invalid configuration for Calc Plugin connection', exc_info=True)
+ logger.debug("Invalid configuration for Calc Plugin connection", exc_info=True)
return
- self._configuration['name'] = url_data.name
+ self._configuration["name"] = url_data.name
self._configuration.update(url_data.config)
self._waiting_config = False
self._calc_thread = CalcThread(self._configuration)
self._calc_thread.setObjectName("calc_{}".format(url_data.name))
- self._calc_thread.new_data_signal.connect(self.receive_new_data,
- Qt.QueuedConnection)
+ self._calc_thread.new_data_signal.connect(self.receive_new_data, Qt.QueuedConnection)
self._calc_thread.start()
return True
@@ -234,18 +224,18 @@ def receive_new_data(self, data):
if not data:
return
try:
- conn = data.get('connection')
+ conn = data.get("connection")
self.connected = conn
self.connection_state_signal.emit(conn)
except KeyError:
- logger.debug('Connection was not available yet for calc.')
+ logger.debug("Connection was not available yet for calc.")
try:
- val = data.get('value')
+ val = data.get("value")
self.value = val
if val is not None:
self.new_value_signal[type(val)].emit(val)
except KeyError:
- logger.debug('Value was not available yet for calc.')
+ logger.debug("Value was not available yet for calc.")
def close(self):
self._calc_thread.requestInterruption()
@@ -288,7 +278,7 @@ def get_info(self):
try:
if not self.name:
raise
- logger.debug('Calc Plugin connection %s got new listener.', self.parsed_address)
+ logger.debug("Calc Plugin connection %s got new listener.", self.parsed_address)
return None, self.name, self.parsed_address
except Exception:
msg = "Invalid configuration for Calc Plugin connection. %s"
diff --git a/pydm/data_plugins/epics_plugin.py b/pydm/data_plugins/epics_plugin.py
index 4de98c0b63..bd8ea5df5d 100644
--- a/pydm/data_plugins/epics_plugin.py
+++ b/pydm/data_plugins/epics_plugin.py
@@ -3,21 +3,27 @@
# To force a particular library, set the PYDM_EPICS_LIB environment
# variable to either pyepics or pyca.
import os
+
EPICS_LIB = os.getenv("PYDM_EPICS_LIB", "").upper()
if EPICS_LIB == "PYEPICS":
from pydm.data_plugins.epics_plugins.pyepics_plugin_component import PyEPICSPlugin
+
EPICSPlugin = PyEPICSPlugin
elif EPICS_LIB == "PYCA":
from pydm.data_plugins.epics_plugins.psp_plugin_component import PSPPlugin
+
EPICSPlugin = PSPPlugin
elif EPICS_LIB == "CAPROTO":
from pydm.data_plugins.epics_plugins.caproto_plugin_component import CaprotoPlugin
+
EPICSPlugin = CaprotoPlugin
else:
try:
from pydm.data_plugins.epics_plugins.pyepics_plugin_component import PyEPICSPlugin
+
EPICSPlugin = PyEPICSPlugin
except ImportError:
from pydm.data_plugins.epics_plugins.psp_plugin_component import PSPPlugin
+
EPICSPlugin = PSPPlugin
EPICSPlugin.protocol = "ca"
diff --git a/pydm/data_plugins/epics_plugins/caproto_plugin_component.py b/pydm/data_plugins/epics_plugins/caproto_plugin_component.py
index a363129305..3ae864240b 100644
--- a/pydm/data_plugins/epics_plugins/caproto_plugin_component.py
+++ b/pydm/data_plugins/epics_plugins/caproto_plugin_component.py
@@ -9,17 +9,36 @@
logger = logging.getLogger(__name__)
-int_types = set((epics.ChannelType.INT, epics.ChannelType.CTRL_INT, epics.ChannelType.TIME_INT,
- epics.ChannelType.ENUM, epics.ChannelType.CTRL_ENUM, epics.ChannelType.TIME_ENUM,
- epics.ChannelType.TIME_LONG, epics.ChannelType.LONG, epics.ChannelType.CTRL_LONG,
- epics.ChannelType.CHAR, epics.ChannelType.TIME_CHAR, epics.ChannelType.CTRL_CHAR))
-
-float_types = set((epics.ChannelType.CTRL_FLOAT, epics.ChannelType.FLOAT, epics.ChannelType.TIME_FLOAT,
- epics.ChannelType.CTRL_DOUBLE, epics.ChannelType.DOUBLE, epics.ChannelType.TIME_DOUBLE))
+int_types = set(
+ (
+ epics.ChannelType.INT,
+ epics.ChannelType.CTRL_INT,
+ epics.ChannelType.TIME_INT,
+ epics.ChannelType.ENUM,
+ epics.ChannelType.CTRL_ENUM,
+ epics.ChannelType.TIME_ENUM,
+ epics.ChannelType.TIME_LONG,
+ epics.ChannelType.LONG,
+ epics.ChannelType.CTRL_LONG,
+ epics.ChannelType.CHAR,
+ epics.ChannelType.TIME_CHAR,
+ epics.ChannelType.CTRL_CHAR,
+ )
+)
+
+float_types = set(
+ (
+ epics.ChannelType.CTRL_FLOAT,
+ epics.ChannelType.FLOAT,
+ epics.ChannelType.TIME_FLOAT,
+ epics.ChannelType.CTRL_DOUBLE,
+ epics.ChannelType.DOUBLE,
+ epics.ChannelType.TIME_DOUBLE,
+ )
+)
class Connection(PyDMConnection):
-
def __init__(self, channel, pv, protocol=None, parent=None):
super(Connection, self).__init__(channel, pv, protocol, parent)
self.app = QApplication.instance()
@@ -37,8 +56,13 @@ def __init__(self, channel, pv, protocol=None, parent=None):
self._timestamp = None
monitor_mask = SubscriptionType.DBE_VALUE | SubscriptionType.DBE_ALARM | SubscriptionType.DBE_PROPERTY
- self.pv = epics.get_pv(pv, connection_callback=self.send_connection_state, form='ctrl',
- auto_monitor=monitor_mask, access_callback=self.send_access_state)
+ self.pv = epics.get_pv(
+ pv,
+ connection_callback=self.send_connection_state,
+ form="ctrl",
+ auto_monitor=monitor_mask,
+ access_callback=self.send_access_state,
+ )
self.pv.add_callback(self.send_new_value, with_ctrlvars=True)
self.add_listener(channel)
@@ -77,9 +101,22 @@ def send_new_value(self, value=None, char_value=None, count=None, typefull=None,
else:
self.new_value_signal[str].emit(char_value)
- def update_ctrl_vars(self, units=None, enum_strs=None, severity=None, upper_ctrl_limit=None, lower_ctrl_limit=None,
- upper_alarm_limit=None, lower_alarm_limit=None, upper_warning_limit=None,
- lower_warning_limit=None, precision=None, timestamp=None, *args, **kws):
+ def update_ctrl_vars(
+ self,
+ units=None,
+ enum_strs=None,
+ severity=None,
+ upper_ctrl_limit=None,
+ lower_ctrl_limit=None,
+ upper_alarm_limit=None,
+ lower_alarm_limit=None,
+ upper_warning_limit=None,
+ lower_warning_limit=None,
+ precision=None,
+ timestamp=None,
+ *args,
+ **kws
+ ):
if severity is not None and self._severity != severity:
self._severity = severity
self.new_severity_signal.emit(int(severity))
@@ -89,12 +126,12 @@ def update_ctrl_vars(self, units=None, enum_strs=None, severity=None, upper_ctrl
if enum_strs is not None and self._enum_strs != enum_strs:
self._enum_strs = enum_strs
try:
- enum_strs = tuple(b.decode(encoding='ascii') for b in enum_strs)
+ enum_strs = tuple(b.decode(encoding="ascii") for b in enum_strs)
except AttributeError:
pass
self.enum_strings_signal.emit(enum_strs)
if units is not None and len(units) > 0 and self._unit != units:
- if type(units) == bytes:
+ if isinstance(units, bytes):
units = units.decode()
self._unit = units
self.unit_signal.emit(units)
@@ -138,7 +175,7 @@ def send_connection_state(self, conn=None, *args, **kws):
self.connection_state_signal.emit(conn)
if conn:
self.clear_cache()
- if hasattr(self, 'pv'):
+ if hasattr(self, "pv"):
self.reload_access_state()
self.pv.run_callbacks()
@@ -154,8 +191,7 @@ def put_value(self, new_val):
try:
self.pv.put(new_val)
except Exception as e:
- logger.exception("Unable to put %s to %s. Exception: %s",
- new_val, self.pv.pvname, str(e))
+ logger.exception("Unable to put %s to %s. Exception: %s", new_val, self.pv.pvname, str(e))
def add_listener(self, channel):
super(Connection, self).add_listener(channel)
diff --git a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py
index 092bc020aa..2407a2a5d7 100644
--- a/pydm/data_plugins/epics_plugins/p4p_plugin_component.py
+++ b/pydm/data_plugins/epics_plugins/p4p_plugin_component.py
@@ -15,9 +15,9 @@
class Connection(PyDMConnection):
-
- def __init__(self, channel: PyDMChannel, address: str,
- protocol: Optional[str] = None, parent: Optional[QObject] = None):
+ 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
@@ -34,9 +34,7 @@ def __init__(self, channel: PyDMChannel, address: str,
super().__init__(channel, address, protocol, parent)
self._connected = False
self.nttable_data_location = PyDMPlugin.get_subfield(channel)
- self.monitor = P4PPlugin.context.monitor(name=self.address,
- cb=self.send_new_value,
- notify_disconnect=True)
+ self.monitor = P4PPlugin.context.monitor(name=self.address, cb=self.send_new_value, notify_disconnect=True)
self.add_listener(channel)
self._value = None
self._severity = None
@@ -52,7 +50,7 @@ def __init__(self, channel: PyDMChannel, address: str,
self._timestamp = None
def clear_cache(self) -> None:
- """ Clear out all the stored values of this connection. """
+ """Clear out all the stored values of this connection."""
self._value = None
self._severity = None
self._precision = None
@@ -67,7 +65,7 @@ def clear_cache(self) -> None:
self._timestamp = 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. """
+ """Callback invoked whenever a new value is received by our monitor. Emits signals based on values changed."""
if isinstance(value, Disconnected):
self._connected = False
self.clear_cache()
@@ -82,7 +80,7 @@ def send_new_value(self, value: Value) -> None:
self._value = value
has_value_changed_yet = False
for changed_value in value.changedSet():
- if changed_value == 'value' or changed_value.split('.')[0] == '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
@@ -90,33 +88,36 @@ def send_new_value(self, value: Value) -> None:
continue
else:
has_value_changed_yet = True
-
- if 'NTTable' in value.getID():
+
+ if "NTTable" in value.getID():
new_value = value.value.todict()
- if hasattr(value, 'labels') and 'labels' not in new_value:
+ if hasattr(value, "labels") and "labels" not in new_value:
# Labels are the column headers for the table
- new_value['labels'] = value.labels
- elif 'NTEnum' in value.getID():
+ new_value["labels"] = value.labels
+ elif "NTEnum" in value.getID():
new_value = value.value.index
self.enum_strings_signal.emit(tuple(value.value.choices))
else:
new_value = value.value
-
+
if self.nttable_data_location:
msg = f"Invalid channel... {self.nttable_data_location}"
for subfield in self.nttable_data_location:
- if isinstance(new_value, collections.Container) and not isinstance(new_value, str):
- if type(subfield) == str:
+ if isinstance(new_value, collections.Container) and not isinstance(new_value, str):
+ if isinstance(subfield, str):
try:
- new_value = new_value[subfield]
+ new_value = new_value[subfield]
continue
except (TypeError, IndexError):
- logger.debug('Type Error when attempting to use the given key, code will next attempt to convert the key to an int')
+ logger.debug(
+ """Type Error when attempting to use the given key, code will next attempt
+ to convert the key to an int"""
+ )
except KeyError:
logger.exception(msg)
-
+
try:
- new_value = new_value[int(subfield)]
+ new_value = new_value[int(subfield)]
except ValueError:
logger.exception(msg, exc_info=True)
else:
@@ -125,7 +126,7 @@ def send_new_value(self, value: Value) -> None:
if new_value is not None:
if isinstance(new_value, np.ndarray):
- if 'NTNDArray' in value.getID():
+ if "NTNDArray" in value.getID():
new_value = decompress(value)
self.new_value_signal[np.ndarray].emit(new_value)
elif isinstance(new_value, np.bool_):
@@ -141,57 +142,67 @@ def send_new_value(self, value: Value) -> None:
elif isinstance(new_value, dict):
self.new_value_signal[dict].emit(new_value)
else:
- raise ValueError(f'No matching signal for value: {new_value} with type: {type(new_value)}')
+ 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
# stored values to avoid sending misleading signals. Will revisit on data plugin changes.
- elif changed_value == 'alarm.severity' and value.alarm.severity != self._severity:
+ elif changed_value == "alarm.severity" and value.alarm.severity != self._severity:
self._severity = value.alarm.severity
self.new_severity_signal.emit(value.alarm.severity)
- elif changed_value == 'display.precision' and value.display.precision != self._precision:
+ elif changed_value == "display.precision" and value.display.precision != self._precision:
self._precision = value.display.precision
self.prec_signal.emit(value.display.precision)
- elif changed_value == 'display.units' and value.display.units != self._units:
+ elif changed_value == "display.units" and value.display.units != self._units:
self._units = value.display.units
self.unit_signal.emit(value.display.units)
- elif changed_value == 'control.limitLow' and value.control.limitLow != self._lower_ctrl_limit:
+ elif changed_value == "control.limitLow" and value.control.limitLow != self._lower_ctrl_limit:
self._lower_ctrl_limit = value.control.limitLow
self.lower_ctrl_limit_signal.emit(value.control.limitLow)
- elif changed_value == 'control.limitHigh' and value.control.limitHigh != self._upper_ctrl_limit:
+ elif changed_value == "control.limitHigh" and value.control.limitHigh != self._upper_ctrl_limit:
self._upper_ctrl_limit = value.control.limitHigh
self.upper_ctrl_limit_signal.emit(value.control.limitHigh)
- elif changed_value == 'valueAlarm.highAlarmLimit' and \
- value.valueAlarm.highAlarmLimit != self._upper_alarm_limit:
+ elif (
+ changed_value == "valueAlarm.highAlarmLimit"
+ and value.valueAlarm.highAlarmLimit != self._upper_alarm_limit
+ ):
self._upper_alarm_limit = value.valueAlarm.highAlarmLimit
self.upper_alarm_limit_signal.emit(value.valueAlarm.highAlarmLimit)
- elif changed_value == 'valueAlarm.lowAlarmLimit' and \
- value.valueAlarm.lowAlarmLimit != self._lower_alarm_limit:
+ elif (
+ changed_value == "valueAlarm.lowAlarmLimit"
+ and value.valueAlarm.lowAlarmLimit != self._lower_alarm_limit
+ ):
self._lower_alarm_limit = value.valueAlarm.lowAlarmLimit
self.lower_alarm_limit_signal.emit(value.valueAlarm.lowAlarmLimit)
- elif changed_value == 'valueAlarm.highWarningLimit' and \
- value.valueAlarm.highWarningLimit != self._upper_warning_limit:
+ elif (
+ changed_value == "valueAlarm.highWarningLimit"
+ and value.valueAlarm.highWarningLimit != self._upper_warning_limit
+ ):
self._upper_warning_limit = value.valueAlarm.highWarningLimit
self.upper_warning_limit_signal.emit(value.valueAlarm.highWarningLimit)
- elif changed_value == 'valueAlarm.lowWarningLimit' and \
- value.valueAlarm.lowWarningLimit != self._lower_warning_limit:
+ elif (
+ changed_value == "valueAlarm.lowWarningLimit"
+ and value.valueAlarm.lowWarningLimit != self._lower_warning_limit
+ ):
self._lower_warning_limit = value.valueAlarm.lowWarningLimit
self.lower_warning_limit_signal.emit(value.valueAlarm.lowWarningLimit)
- elif changed_value == 'timeStamp.secondsPastEpoch' and \
- value.timeStamp.secondsPastEpoch != self._timestamp:
+ elif (
+ changed_value == "timeStamp.secondsPastEpoch"
+ and value.timeStamp.secondsPastEpoch != self._timestamp
+ ):
self._timestamp = value.timeStamp.secondsPastEpoch
self.timestamp_signal.emit(value.timeStamp.secondsPastEpoch)
@staticmethod
def convert_epics_nttable(epics_struct):
- """
- Converts an epics nttable (passed as a class object p4p.wrapper.Value) to a python dictionary.
-
+ """
+ Converts an epics nttable (passed as a class object p4p.wrapper.Value) to a python dictionary.
+
Parameters
----------
epics_struct: 'p4p.wrapper.Value'
Return
------
- result: dict
+ result: dict
"""
result = {}
for field in epics_struct.keys():
@@ -205,8 +216,8 @@ def convert_epics_nttable(epics_struct):
@staticmethod
def set_value_by_keys(table, keys, new_value):
- """
- Saves the passed new_value into the appropriate spot in the given table
+ """
+ Saves the passed new_value into the appropriate spot in the given table
using the given keys.
Parameters
@@ -217,7 +228,7 @@ def set_value_by_keys(table, keys, new_value):
"""
if len(keys) == 1:
key = keys[0]
- try:
+ try:
table[key] = new_value
except TypeError:
table[int(key)] = new_value
@@ -227,16 +238,16 @@ def set_value_by_keys(table, keys, new_value):
Connection.set_value_by_keys(table[key], keys[1:], new_value)
def put_value(self, value):
- """ Write a value to the PV """
+ """Write a value to the PV"""
if self.nttable_data_location:
nttable = Connection.convert_epics_nttable(self._value)
nttable = nttable["value"]
Connection.set_value_by_keys(nttable, self.nttable_data_location, value)
- value = {'value': nttable}
+ value = {"value": nttable}
if is_read_only():
- logger.warning(f'PyDM read-only mode is enabled, could not write value: {value} to {self.address}')
+ logger.warning(f"PyDM read-only mode is enabled, could not write value: {value} to {self.address}")
return
try:
@@ -286,7 +297,7 @@ def add_listener(self, channel: PyDMChannel):
pass
def close(self):
- """ Closes out this connection. """
+ """Closes out this connection."""
self.monitor.close()
super().close()
@@ -303,5 +314,5 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if P4PPlugin.context is None:
# Create the p4p pva context for all connections to use
- context = Context('pva', nt=False) # Disable automatic value unwrapping
+ context = Context("pva", nt=False) # Disable automatic value unwrapping
P4PPlugin.context = context
diff --git a/pydm/data_plugins/epics_plugins/psp_plugin_component.py b/pydm/data_plugins/epics_plugins/psp_plugin_component.py
index 61ae6f4536..b0a1ab837b 100644
--- a/pydm/data_plugins/epics_plugins/psp_plugin_component.py
+++ b/pydm/data_plugins/epics_plugins/psp_plugin_component.py
@@ -111,7 +111,10 @@ def setup_pv(pvname, con_cb=None, mon_cb=None, rwaccess_cb=None, signal=None, mo
pv = Pv(pvname, use_numpy=True, control=control)
if signal is None:
- default_mon_cb = lambda e: None
+
+ def default_mon_cb(e):
+ return None
+
else:
default_mon_cb = generic_mon_cb(pv, signal)
@@ -142,11 +145,9 @@ def __init__(self, channel, pv, protocol=None, parent=None):
"""
super(Connection, self).__init__(channel, pv, protocol, parent)
self.python_type = None
- self.pv = setup_pv(pv,
- con_cb=self.connected_cb,
- mon_cb=self.monitor_cb,
- rwaccess_cb=self.rwaccess_cb,
- control=True)
+ self.pv = setup_pv(
+ pv, con_cb=self.connected_cb, mon_cb=self.monitor_cb, rwaccess_cb=self.rwaccess_cb, control=True
+ )
self.enums = None
self.sevr = None
self.ctrl_llim = None
@@ -164,8 +165,7 @@ def __init__(self, channel, pv, protocol=None, parent=None):
self.read_access = False
self.write_access = False
# Auxilliary info to help with throttling
- self.scan_pv = setup_pv(pv + ".SCAN", mon_cb=self.scan_pv_cb,
- mon_cb_once=True)
+ self.scan_pv = setup_pv(pv + ".SCAN", mon_cb=self.scan_pv_cb, mon_cb_once=True)
self.throttle = QTimer(self)
self.throttle.timeout.connect(self.throttle_cb)
@@ -193,8 +193,7 @@ def connected_cb(self, isconnected):
self.pv.monitor()
self.python_type = type_map.get(self.epics_type)
if self.python_type is None:
- raise Exception("Unsupported EPICS type {0} for pv {1}".format(
- self.epics_type, self.pv.name))
+ raise Exception("Unsupported EPICS type {0} for pv {1}".format(self.epics_type, self.pv.name))
def monitor_cb(self, e=None):
"""
@@ -251,7 +250,7 @@ def send_new_value(self, value=None):
self.new_severity_signal.emit(self.sevr)
try:
- prec = self.pv.data['precision']
+ prec = self.pv.data["precision"]
except KeyError:
pass
else:
@@ -260,13 +259,15 @@ def send_new_value(self, value=None):
self.prec_signal.emit(int(self.prec))
try:
- units = self.pv.data['units']
+ units = self.pv.data["units"]
except KeyError:
pass
else:
if self.units != units:
self.units = units
- self.unit_signal.emit(self.units.decode(encoding='ascii') if isinstance(self.units, bytes) else self.units)
+ self.unit_signal.emit(
+ self.units.decode(encoding="ascii") if isinstance(self.units, bytes) else self.units
+ )
time = self.timestamp()
@@ -275,7 +276,7 @@ def send_new_value(self, value=None):
self.timestamp_signal.emit(self.time)
try:
- ctrl_llim = self.pv.data['ctrl_llim']
+ ctrl_llim = self.pv.data["ctrl_llim"]
except KeyError:
pass
else:
@@ -284,7 +285,7 @@ def send_new_value(self, value=None):
self.lower_ctrl_limit_signal.emit(self.ctrl_llim)
try:
- ctrl_hlim = self.pv.data['ctrl_hlim']
+ ctrl_hlim = self.pv.data["ctrl_hlim"]
except KeyError:
pass
else:
@@ -293,7 +294,7 @@ def send_new_value(self, value=None):
self.upper_ctrl_limit_signal.emit(self.ctrl_hlim)
try:
- alarm_hlim = self.pv.data['alarm_hlim']
+ alarm_hlim = self.pv.data["alarm_hlim"]
except KeyError:
pass
else:
@@ -302,7 +303,7 @@ def send_new_value(self, value=None):
self.upper_alarm_limit_signal.emit(self.alarm_hlim)
try:
- alarm_llim = self.pv.data['alarm_llim']
+ alarm_llim = self.pv.data["alarm_llim"]
except KeyError:
pass
else:
@@ -311,7 +312,7 @@ def send_new_value(self, value=None):
self.lower_alarm_limit_signal.emit(self.alarm_llim)
try:
- warn_hlim = self.pv.data['warn_hlim']
+ warn_hlim = self.pv.data["warn_hlim"]
except KeyError:
pass
else:
@@ -320,7 +321,7 @@ def send_new_value(self, value=None):
self.upper_warning_limit_signal.emit(self.warn_hlim)
try:
- warn_llim = self.pv.data['warn_llim']
+ warn_llim = self.pv.data["warn_llim"]
except KeyError:
pass
else:
@@ -348,7 +349,7 @@ def send_ctrl_vars(self):
if self.prec is None:
try:
- self.prec = self.pv.data['precision']
+ self.prec = self.pv.data["precision"]
except KeyError:
pass
if self.prec is not None:
@@ -362,17 +363,17 @@ def send_ctrl_vars(self):
if self.units is None:
try:
- self.units = self.pv.data['units']
+ self.units = self.pv.data["units"]
except KeyError:
pass
if self.units:
if isinstance(self.units, bytes):
- self.units = self.units.decode(encoding='ascii')
+ self.units = self.units.decode(encoding="ascii")
self.unit_signal.emit(self.units)
if self.ctrl_llim is None:
try:
- self.ctrl_llim = self.pv.data['ctrl_llim']
+ self.ctrl_llim = self.pv.data["ctrl_llim"]
except KeyError:
pass
if self.ctrl_llim is not None:
@@ -380,7 +381,7 @@ def send_ctrl_vars(self):
if self.ctrl_hlim is None:
try:
- self.ctrl_hlim = self.pv.data['ctrl_hlim']
+ self.ctrl_hlim = self.pv.data["ctrl_hlim"]
except KeyError:
pass
if self.ctrl_hlim is not None:
@@ -388,7 +389,7 @@ def send_ctrl_vars(self):
if self.alarm_hlim is None:
try:
- self.alarm_hlim = self.pv.data['alarm_hlim']
+ self.alarm_hlim = self.pv.data["alarm_hlim"]
except KeyError:
pass
if self.alarm_hlim is not None:
@@ -396,7 +397,7 @@ def send_ctrl_vars(self):
if self.alarm_llim is None:
try:
- self.alarm_llim = self.pv.data['alarm_llim']
+ self.alarm_llim = self.pv.data["alarm_llim"]
except KeyError:
pass
if self.alarm_llim is not None:
@@ -404,7 +405,7 @@ def send_ctrl_vars(self):
if self.warn_hlim is None:
try:
- self.warn_hlim = self.pv.data['warn_hlim']
+ self.warn_hlim = self.pv.data["warn_hlim"]
except KeyError:
pass
if self.warn_hlim is not None:
@@ -412,7 +413,7 @@ def send_ctrl_vars(self):
if self.warn_llim is None:
try:
- self.warn_llim = self.pv.data['warn_llim']
+ self.warn_llim = self.pv.data["warn_llim"]
except KeyError:
pass
if self.warn_llim is not None:
@@ -440,7 +441,9 @@ def update_enums(self):
"""
if self.epics_type == "DBF_ENUM":
if self.enums is None:
- self.enums = tuple(b.decode(encoding='ascii') if isinstance(b, bytes) else b for b in self.pv.data["enum_set"])
+ self.enums = tuple(
+ b.decode(encoding="ascii") if isinstance(b, bytes) else b for b in self.pv.data["enum_set"]
+ )
self.enum_strings_signal.emit(self.enums)
@Slot(int)
@@ -485,7 +488,7 @@ def scan_pv_cb(self, e=None):
self.pv.wait_ready()
count = self.pv.count or 1
if count > 1:
- max_data_rate = 1000000. # bytes/s
+ max_data_rate = 1000000.0 # bytes/s
bytes = self.pv.value.itemsize # bytes
throttle = max_data_rate / (bytes * count) # Hz
if throttle < 120:
@@ -567,6 +570,7 @@ class PSPPlugin(PyDMPlugin):
"""
Class to define our protocol and point to our :class:`Connection` Class
"""
+
# NOTE: protocol is intentionally "None" to keep this plugin from getting directly imported.
# If this plugin is chosen as the One True EPICS Plugin in epics_plugin.py, the protocol will
# be properly set before it is used.
diff --git a/pydm/data_plugins/epics_plugins/pva_codec.py b/pydm/data_plugins/epics_plugins/pva_codec.py
index df4d6d1adc..0859b86bc0 100644
--- a/pydm/data_plugins/epics_plugins/pva_codec.py
+++ b/pydm/data_plugins/epics_plugins/pva_codec.py
@@ -7,8 +7,19 @@
logger = logging.getLogger(__name__)
-ScalarType = (bool, np.int8, np.int16, np.int32, np.compat.long, np.uint8,
- np.uint16, np.uint32, np.uint64, float, np.double)
+ScalarType = (
+ bool,
+ np.int8,
+ np.int16,
+ np.int32,
+ np.compat.long,
+ np.uint8,
+ np.uint16,
+ np.uint32,
+ np.uint64,
+ float,
+ np.double,
+)
codecs = {}
@@ -27,22 +38,22 @@ def decompress(structure: Value):
if structure is None:
return
- data = structure.get('value')
+ data = structure.get("value")
if data is None:
return
shape = []
- for dim in structure.get('dimension', []):
- shape.append(dim.get('size'))
- codec = structure.get('codec', {})
- data_type = codec.get('parameters')
+ for dim in structure.get("dimension", []):
+ shape.append(dim.get("size"))
+ codec = structure.get("codec", {})
+ data_type = codec.get("parameters")
if data_type is None:
# Assuming data type from data
dtype = data.dtype
else:
dtype = ScalarType[data_type]
- codec_name = codec.get('name')
- uncompressed_size = structure.get('uncompressedSize')
+ codec_name = codec.get("name")
+ uncompressed_size = structure.get("uncompressedSize")
if not codec_name:
return none_decompress(data, shape, dtype)
@@ -50,34 +61,34 @@ def decompress(structure: Value):
try:
return codecs[codec_name](data, shape, dtype, uncompressed_size)
except Exception:
- logging.exception('Could not run codec decompress for %s', codec_name)
+ logging.exception("Could not run codec decompress for %s", codec_name)
return data
def none_decompress(data, shape, dtype):
- """ Perform a reshape based on the dimensions that were set """
+ """Perform a reshape based on the dimensions that were set"""
return np.frombuffer(data, dtype=dtype).reshape(shape)
def jpeg_decompress(data, shape, dtype, uncompressed_size):
- """ Decompress using Pillow's jpeg decompression """
+ """Decompress using Pillow's jpeg decompression"""
return np.array(Image.open(io.BytesIO(data.tobytes())))
def blosc_decompress(data, shape, dtype, uncompressed_size):
- """ Decompress using blosc """
+ """Decompress using blosc"""
dec_data = blosc.decompress(data)
return np.frombuffer(dec_data, dtype=dtype).reshape(shape)
def lz4_decompress(data, shape, dtype, uncompressed_size):
- """ Decompress using lz4 """
+ """Decompress using lz4"""
dec_data = block.decompress(data, uncompressed_size)
return np.frombuffer(dec_data, dtype=dtype).reshape(shape)
def bslz4_decompress(data, shape, dtype, uncompressed_size):
- """ Decompress using bslz4 """
+ """Decompress using bslz4"""
nelems = reduce((lambda x, y: x * y), shape)
dec_data = bitshuffle.decompress_lz4(data, (nelems,), np.dtype(dtype))
return dec_data.reshape(shape)
@@ -85,24 +96,28 @@ def bslz4_decompress(data, shape, dtype, uncompressed_size):
try:
import blosc
- codecs['blosc'] = blosc_decompress
+
+ codecs["blosc"] = blosc_decompress
except ImportError:
- logger.debug('Blosc codec not available for PVAccess data decompression')
+ logger.debug("Blosc codec not available for PVAccess data decompression")
try:
from lz4 import block
- codecs['lz4'] = lz4_decompress
+
+ codecs["lz4"] = lz4_decompress
except ImportError:
- logger.debug('LZ4 codec not available for PVAccess data decompression')
+ logger.debug("LZ4 codec not available for PVAccess data decompression")
try:
import bitshuffle
- codecs['bslz4'] = bslz4_decompress
+
+ codecs["bslz4"] = bslz4_decompress
except ImportError:
- logger.debug('BSLZ4 codec not available for PVAccess data decompression')
+ logger.debug("BSLZ4 codec not available for PVAccess data decompression")
try:
from PIL import Image
- codecs['jpeg'] = jpeg_decompress
+
+ codecs["jpeg"] = jpeg_decompress
except ImportError:
- logger.debug('JPEG codec not available for PVAccess data decompression')
+ logger.debug("JPEG codec not available for PVAccess data decompression")
diff --git a/pydm/data_plugins/epics_plugins/pyepics_plugin_component.py b/pydm/data_plugins/epics_plugins/pyepics_plugin_component.py
index 03de9c455e..9df69761d4 100644
--- a/pydm/data_plugins/epics_plugins/pyepics_plugin_component.py
+++ b/pydm/data_plugins/epics_plugins/pyepics_plugin_component.py
@@ -13,30 +13,55 @@
try:
from epics import utils3
+
utils3.EPICS_STR_ENCODING = "latin-1"
-except:
+except Exception:
pass
logger = logging.getLogger(__name__)
-int_types = set((epics.dbr.INT, epics.dbr.CTRL_INT, epics.dbr.TIME_INT,
- epics.dbr.ENUM, epics.dbr.CTRL_ENUM, epics.dbr.TIME_ENUM,
- epics.dbr.TIME_LONG, epics.dbr.LONG, epics.dbr.CTRL_LONG,
- epics.dbr.CHAR, epics.dbr.TIME_CHAR, epics.dbr.CTRL_CHAR,
- epics.dbr.TIME_SHORT, epics.dbr.CTRL_SHORT))
+int_types = set(
+ (
+ epics.dbr.INT,
+ epics.dbr.CTRL_INT,
+ epics.dbr.TIME_INT,
+ epics.dbr.ENUM,
+ epics.dbr.CTRL_ENUM,
+ epics.dbr.TIME_ENUM,
+ epics.dbr.TIME_LONG,
+ epics.dbr.LONG,
+ epics.dbr.CTRL_LONG,
+ epics.dbr.CHAR,
+ epics.dbr.TIME_CHAR,
+ epics.dbr.CTRL_CHAR,
+ epics.dbr.TIME_SHORT,
+ epics.dbr.CTRL_SHORT,
+ )
+)
-float_types = set((epics.dbr.CTRL_FLOAT, epics.dbr.FLOAT, epics.dbr.TIME_FLOAT,
- epics.dbr.CTRL_DOUBLE, epics.dbr.DOUBLE, epics.dbr.TIME_DOUBLE))
+float_types = set(
+ (
+ epics.dbr.CTRL_FLOAT,
+ epics.dbr.FLOAT,
+ epics.dbr.TIME_FLOAT,
+ epics.dbr.CTRL_DOUBLE,
+ epics.dbr.DOUBLE,
+ epics.dbr.TIME_DOUBLE,
+ )
+)
class Connection(PyDMConnection):
-
def __init__(self, channel, pv, protocol=None, parent=None):
super(Connection, self).__init__(channel, pv, protocol, parent)
self.app = QApplication.instance()
- self.pv = epics.PV(pv, connection_callback=self.send_connection_state,
- form='ctrl', auto_monitor=epics.dbr.DBE_VALUE|epics.dbr.DBE_ALARM|epics.dbr.DBE_PROPERTY,
- access_callback=self.send_access_state)
+ self.pv = epics.PV(
+ pv,
+ connection_callback=self.send_connection_state,
+ form="ctrl",
+ auto_monitor=epics.dbr.DBE_VALUE | epics.dbr.DBE_ALARM | epics.dbr.DBE_PROPERTY,
+ access_callback=self.send_access_state,
+ )
self._value = None
self._severity = None
self._precision = None
@@ -92,11 +117,24 @@ def send_new_value(self, value=None, char_value=None, count=None, ftype=None, *a
else:
self.new_value_signal[str].emit(char_value)
- def update_ctrl_vars(self, units=None, enum_strs=None, severity=None, upper_ctrl_limit=None, lower_ctrl_limit=None,
- precision=None, upper_alarm_limit=None, lower_alarm_limit=None, upper_warning_limit=None,
- lower_warning_limit=None, timestamp=None, *args, **kws):
- """ Callback invoked when there is a change any of these variables. For a full description see:
- https://cars9.uchicago.edu/software/python/pyepics3/pv.html#user-supplied-callback-functions
+ def update_ctrl_vars(
+ self,
+ units=None,
+ enum_strs=None,
+ severity=None,
+ upper_ctrl_limit=None,
+ lower_ctrl_limit=None,
+ precision=None,
+ upper_alarm_limit=None,
+ lower_alarm_limit=None,
+ upper_warning_limit=None,
+ lower_warning_limit=None,
+ timestamp=None,
+ *args,
+ **kws
+ ):
+ """Callback invoked when there is a change any of these variables. For a full description see:
+ https://cars9.uchicago.edu/software/python/pyepics3/pv.html#user-supplied-callback-functions
"""
if severity is not None and self._severity != severity:
self._severity = severity
@@ -107,12 +145,12 @@ def update_ctrl_vars(self, units=None, enum_strs=None, severity=None, upper_ctrl
if enum_strs is not None and self._enum_strs != enum_strs:
self._enum_strs = enum_strs
try:
- enum_strs = tuple(b.decode(encoding='ascii') for b in enum_strs)
+ enum_strs = tuple(b.decode(encoding="ascii") for b in enum_strs)
except AttributeError:
pass
self.enum_strings_signal.emit(enum_strs)
if units is not None and len(units) > 0 and self._unit != units:
- if type(units) == bytes:
+ if isinstance(units, bytes):
units = units.decode()
self._unit = units
self.unit_signal.emit(units)
@@ -156,7 +194,7 @@ def send_connection_state(self, conn=None, *args, **kws):
self.connection_state_signal.emit(conn)
if conn:
self.clear_cache()
- if hasattr(self, 'pv'):
+ if hasattr(self, "pv"):
self.reload_access_state()
self.pv.run_callbacks()
@@ -172,8 +210,7 @@ def put_value(self, new_val):
try:
self.pv.put(new_val)
except Exception as e:
- logger.exception("Unable to put %s to %s. Exception: %s",
- new_val, self.pv.pvname, str(e))
+ logger.exception("Unable to put %s to %s. Exception: %s", new_val, self.pv.pvname, str(e))
def add_listener(self, channel):
super(Connection, self).add_listener(channel)
diff --git a/pydm/data_plugins/fake_plugin.py b/pydm/data_plugins/fake_plugin.py
index 1b4a9bfb29..15a1afc57e 100644
--- a/pydm/data_plugins/fake_plugin.py
+++ b/pydm/data_plugins/fake_plugin.py
@@ -4,7 +4,6 @@
class Connection(PyDMConnection):
-
def __init__(self, widget, address, protocol=None, parent=None):
super(Connection, self).__init__(widget, address, protocol, parent)
self.add_listener(widget)
@@ -30,4 +29,3 @@ def add_listener(self, widget):
class FakePlugin(PyDMPlugin):
protocol = "fake"
connection_class = Connection
-
diff --git a/pydm/data_plugins/local_plugin.py b/pydm/data_plugins/local_plugin.py
index 08ec6debe6..c5576b417f 100644
--- a/pydm/data_plugins/local_plugin.py
+++ b/pydm/data_plugins/local_plugin.py
@@ -22,35 +22,13 @@ def __init__(self, channel, address, protocol=None, parent=None):
self._precision_set = None
self._type_kwargs = {}
- self._required_config_keys = [
- 'name',
- 'type',
- 'init'
- ]
-
- self._extra_config_keys = [
- "precision",
- "unit",
- "upper_limit",
- "lower_limit",
- "enum_string"
- ]
-
- self._extra_numpy_config_keys = [
- "dtype",
- "copy",
- "order",
- "subok",
- "ndmin"
- ]
-
- self._data_types = {
- 'int': int,
- 'float': float,
- 'str': str,
- 'bool': bool,
- 'array': np.array
- }
+ self._required_config_keys = ["name", "type", "init"]
+
+ self._extra_config_keys = ["precision", "unit", "upper_limit", "lower_limit", "enum_string"]
+
+ self._extra_numpy_config_keys = ["dtype", "copy", "order", "subok", "ndmin"]
+
+ self._data_types = {"int": int, "float": float, "str": str, "bool": bool, "array": np.array}
self._precision = None
self._unit = None
@@ -67,16 +45,16 @@ def __init__(self, channel, address, protocol=None, parent=None):
def _configure_local_plugin(self, channel):
if self._is_connection_configured:
- logger.debug('LocalPlugin connection already configured.')
+ logger.debug("LocalPlugin connection already configured.")
return
try:
url_data = UrlToPython(channel).get_info()
except ValueError("Not enough information"):
- logger.debug('Invalid configuration for LocalPlugin connection', exc_info=True)
+ logger.debug("Invalid configuration for LocalPlugin connection", exc_info=True)
return
- self._configuration['name'] = url_data[1]
+ self._configuration["name"] = url_data[1]
address = url_data[2]
if url_data[0] is None:
@@ -86,9 +64,9 @@ def _configure_local_plugin(self, channel):
self.address = address
# set the object's attributes
- init_value = self._configuration.get('init')[0]
- self._value_type = self._configuration.get('type')[0]
- self.name = self._configuration.get('name')
+ init_value = self._configuration.get("init")[0]
+ self._value_type = self._configuration.get("type")[0]
+ self.name = self._configuration.get("name")
# get the extra info if any
self.parse_channel_extras(self._configuration)
@@ -121,36 +99,38 @@ def parse_channel_extras(self, extras):
"""
- precision = extras.get('precision')
+ precision = extras.get("precision")
if precision is not None:
try:
self._precision_set = int(precision[0])
self.prec_signal.emit(self._precision_set)
except ValueError:
- logger.debug('Cannot convert precision value=%r', precision)
- unit = extras.get('unit')
+ logger.debug("Cannot convert precision value=%r", precision)
+ unit = extras.get("unit")
if unit is not None:
self.unit_signal.emit(str(unit[0]))
- upper_limit = extras.get('upper_limit')
- if upper_limit is not None and (self._value_type == 'float' or self._value_type == 'int'):
+ upper_limit = extras.get("upper_limit")
+ if upper_limit is not None and (self._value_type == "float" or self._value_type == "int"):
self.send_upper_limit(upper_limit[0])
- lower_limit = extras.get('lower_limit')
- if lower_limit is not None and (self._value_type == 'float' or self._value_type == 'int'):
+ lower_limit = extras.get("lower_limit")
+ if lower_limit is not None and (self._value_type == "float" or self._value_type == "int"):
self.send_lower_limit(lower_limit[0])
- enum_string = extras.get('enum_string')
+ enum_string = extras.get("enum_string")
if enum_string is not None:
self.send_enum_string(enum_string[0])
- type_kwargs = {k: v for k, v in extras.items()
- if k in self._extra_numpy_config_keys}
+ type_kwargs = {k: v for k, v in extras.items() if k in self._extra_numpy_config_keys}
if type_kwargs:
self.format_type_params(type_kwargs)
- unused = {k: v for k, v in extras.items()
- if k not in self._extra_config_keys
- and k not in self._required_config_keys
- and k not in self._extra_numpy_config_keys}
+ unused = {
+ k: v
+ for k, v in extras.items()
+ if k not in self._extra_config_keys
+ and k not in self._required_config_keys
+ and k not in self._extra_numpy_config_keys
+ }
if len(unused) == 0:
return
@@ -287,22 +267,22 @@ def format_type_params(self, type_kwargs):
"""
- dtype = type_kwargs.get('dtype')
+ dtype = type_kwargs.get("dtype")
if dtype is not None:
dtype = dtype[0]
try:
- self._type_kwargs['dtype'] = np.dtype(dtype)
+ self._type_kwargs["dtype"] = np.dtype(dtype)
return self._type_kwargs
except TypeError:
- logger.debug('Cannot convert dtype value=%r', dtype)
+ logger.debug("Cannot convert dtype value=%r", dtype)
else:
- dtype = 'object'
+ dtype = "object"
try:
- self._type_kwargs['dtype'] = np.dtype(dtype)
+ self._type_kwargs["dtype"] = np.dtype(dtype)
return self._type_kwargs
except TypeError:
- logger.debug('Cannot convert dtype value=%r', dtype)
+ logger.debug("Cannot convert dtype value=%r", dtype)
return self._type_kwargs
@@ -341,7 +321,7 @@ def convert_value(self, value, value_type):
value = ast.literal_eval(value)
return _type(value, **self._type_kwargs)
except ValueError:
- logger.debug('Cannot convert value_type')
+ logger.debug("Cannot convert value_type")
else:
return None
@@ -370,28 +350,23 @@ def add_listener(self, channel):
# which captures the values sent through the plugin
if channel.value_signal is not None:
try:
- channel.value_signal[int].connect(
- self.put_value, Qt.QueuedConnection)
+ channel.value_signal[int].connect(self.put_value, Qt.QueuedConnection)
except KeyError:
pass
try:
- channel.value_signal[float].connect(
- self.put_value, Qt.QueuedConnection)
+ channel.value_signal[float].connect(self.put_value, Qt.QueuedConnection)
except KeyError:
pass
try:
- channel.value_signal[str].connect(
- self.put_value, Qt.QueuedConnection)
+ channel.value_signal[str].connect(self.put_value, Qt.QueuedConnection)
except KeyError:
pass
try:
- channel.value_signal[bool].connect(
- self.put_value, Qt.QueuedConnection)
+ channel.value_signal[bool].connect(self.put_value, Qt.QueuedConnection)
except KeyError:
pass
try:
- channel.value_signal[np.ndarray].connect(
- self.put_value, Qt.QueuedConnection)
+ channel.value_signal[np.ndarray].connect(self.put_value, Qt.QueuedConnection)
except KeyError:
pass
@@ -421,7 +396,7 @@ def put_value(self, new_value):
@staticmethod
def precision_for_value(value, max_precision=8):
dec = decimal.Decimal(str(value))
- solution = min((len(str(dec).split('.')[1]), max_precision))
+ solution = min((len(str(dec).split(".")[1]), max_precision))
return solution
@@ -461,13 +436,13 @@ def get_info(self):
if not name or not config:
raise
- elif config['init'] is None or config['type'] is None:
+ elif config["init"] is None or config["type"] is None:
raise
except Exception:
try:
if not name:
raise
- logger.debug('LocalPlugin connection %s got new listener.', address)
+ logger.debug("LocalPlugin connection %s got new listener.", address)
return None, name, address
except Exception:
msg = "Invalid configuration for LocalPlugin connection. %s"
diff --git a/pydm/data_plugins/plugin.py b/pydm/data_plugins/plugin.py
index 93659536cc..7191462706 100644
--- a/pydm/data_plugins/plugin.py
+++ b/pydm/data_plugins/plugin.py
@@ -2,14 +2,13 @@
import weakref
import threading
-from numpy import ndarray
from typing import Optional, Callable
from ..utilities.remove_protocol import parsed_address
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], [bool], [object])
@@ -62,7 +61,7 @@ def add_listener(self, channel):
self.new_value_signal[object].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 +140,7 @@ def remove_listener(self, channel, destroying: Optional[bool] = False) -> None:
self.new_value_signal[object].disconnect(channel.value_slot)
except TypeError:
pass
-
+
if self._should_disconnect(channel.severity_slot, destroying):
try:
self.new_severity_signal.disconnect(channel.severity_slot)
@@ -220,7 +219,7 @@ def remove_listener(self, channel, destroying: Optional[bool] = False) -> None:
@staticmethod
def _should_disconnect(slot: Callable, destroying: bool):
- """ Return True if the signal/slot should be disconnected, False otherwise """
+ """Return True if the signal/slot should be disconnected, False otherwise"""
if slot is None:
# Nothing to do if the slot does not exist
return False
@@ -261,7 +260,7 @@ def get_full_address(channel):
if parsed_addr:
full_addr = parsed_addr.netloc + parsed_addr.path
- else:
+ else:
full_addr = None
return full_addr
@@ -270,9 +269,9 @@ def get_full_address(channel):
def get_address(channel):
parsed_addr = parsed_address(channel.address)
addr = parsed_addr.netloc
-
+
return addr
-
+
@staticmethod
def get_subfield(channel):
parsed_addr = parsed_address(channel.address)
@@ -280,11 +279,11 @@ def get_subfield(channel):
if parsed_addr:
subfield = parsed_addr.path
- if subfield != '':
- subfield = subfield[1:].split('/')
+ if subfield != "":
+ subfield = subfield[1:].split("/")
else:
subfield = None
-
+
return subfield
@staticmethod
@@ -293,6 +292,7 @@ def get_connection_id(channel):
def add_connection(self, channel):
from pydm.utilities import is_qt_designer
+
with self.lock:
connection_id = self.get_connection_id(channel)
address = self.get_address(channel)
@@ -301,25 +301,21 @@ def add_connection(self, channel):
if channel in self.channels:
return
- if (is_qt_designer() and not config.DESIGNER_ONLINE and
- not self.designer_online_by_default):
+ if is_qt_designer() and not config.DESIGNER_ONLINE and not self.designer_online_by_default:
return
self.channels.add(channel)
if connection_id in self.connections:
self.connections[connection_id].add_listener(channel)
else:
- self.connections[connection_id] = self.connection_class(
- channel, address, self.protocol)
+ self.connections[connection_id] = self.connection_class(channel, address, self.protocol)
def remove_connection(self, channel, destroying=False):
with self.lock:
connection_id = self.get_connection_id(channel)
if connection_id in self.connections and channel in self.channels:
- self.connections[connection_id].remove_listener(
- channel, destroying=destroying)
+ self.connections[connection_id].remove_listener(channel, destroying=destroying)
self.channels.remove(channel)
if self.connections[connection_id].listener_count < 1:
self.connections[connection_id].deleteLater()
del self.connections[connection_id]
-
diff --git a/pydm/data_plugins/pva_plugin.py b/pydm/data_plugins/pva_plugin.py
index ab37d3356a..835f1e8d6c 100644
--- a/pydm/data_plugins/pva_plugin.py
+++ b/pydm/data_plugins/pva_plugin.py
@@ -8,6 +8,7 @@
try:
if PVA_LIB == "P4P" or not PVA_LIB:
from pydm.data_plugins.epics_plugins.p4p_plugin_component import P4PPlugin
+
PVAPlugin = P4PPlugin
elif PVA_LIB == "PVAPY":
logger.error("PVAPY is not currently supported by PyDM")
diff --git a/pydm/display.py b/pydm/display.py
index 91dcd35caf..8427929f14 100644
--- a/pydm/display.py
+++ b/pydm/display.py
@@ -54,8 +54,7 @@ def load_file(file, macros=None, args=None, target=ScreenTarget.NEW_PROCESS):
pydm.Display
"""
if not is_pydm_app() and target == ScreenTarget.NEW_PROCESS:
- logger.warning('New Process is only valid with PyDM Application. ' +
- 'Falling back to ScreenTarget.DIALOG.')
+ logger.warning("New Process is only valid with PyDM Application. " + "Falling back to ScreenTarget.DIALOG.")
target = ScreenTarget.DIALOG
if target == ScreenTarget.NEW_PROCESS:
@@ -69,10 +68,10 @@ def load_file(file, macros=None, args=None, target=ScreenTarget.NEW_PROCESS):
logger.debug("Loading %s file by way of %s...", file, loader.__name__)
loaded_display = loader(file, args=args, macros=macros)
- if os.path.exists(base + '.txt'):
- loaded_display.load_help_file(base + '.txt')
- elif os.path.exists(base + '.html'):
- loaded_display.load_help_file(base + '.html')
+ if os.path.exists(base + ".txt"):
+ loaded_display.load_help_file(base + ".txt")
+ elif os.path.exists(base + ".html"):
+ loaded_display.load_help_file(base + ".html")
if target == ScreenTarget.DIALOG:
loaded_display.show()
@@ -98,7 +97,7 @@ def _compile_ui_file(uifile: str) -> Tuple[str, str]:
code_string = StringIO()
uic.compileUi(uifile, code_string)
# Grabs non-whitespace characters between class and the opening parenthesis
- class_name = re.search(r'^class\s*(\S*)\(', code_string.getvalue(), re.MULTILINE).group(1)
+ class_name = re.search(r"^class\s*(\S*)\(", code_string.getvalue(), re.MULTILINE).group(1)
return code_string.getvalue(), class_name
@@ -117,10 +116,9 @@ def _load_ui_into_display(uifile, display):
display.ui = display
-def _load_compiled_ui_into_display(code_string: str,
- class_name: str,
- display: Display,
- macros: Optional[Dict[str, str]] = None) -> None:
+def _load_compiled_ui_into_display(
+ code_string: str, class_name: str, display: Display, macros: Optional[Dict[str, str]] = None
+) -> None:
"""
Takes a ui file which has already been compiled by uic and loads it into the input display.
Performs macro substitution within the input code_string if any macros supplied are
@@ -245,26 +243,32 @@ def load_py_file(pyfile, args=None, macros=None):
# as real python modules.
module = import_module_by_filename(os.path.abspath(pyfile))
- if hasattr(module, 'intelclass'):
+ if hasattr(module, "intelclass"):
cls = module.intelclass
if not issubclass(cls, Display):
raise ValueError(
"Invalid class definition at file {}. {} does not inherit from Display. "
- "Nothing to open at this time.".format(
- pyfile, cls.__name__))
+ "Nothing to open at this time.".format(pyfile, cls.__name__)
+ )
else:
- classes = [obj for name, obj in inspect.getmembers(module) if
- inspect.isclass(obj) and issubclass(obj,
- Display) and obj != Display]
+ classes = [
+ obj
+ for name, obj in inspect.getmembers(module)
+ if inspect.isclass(obj) and issubclass(obj, Display) and obj != Display
+ ]
if len(classes) == 0:
raise ValueError(
"Invalid File Format. {} has no class inheriting from Display. Nothing to open at this time.".format(
- pyfile))
+ pyfile
+ )
+ )
if len(classes) > 1:
warnings.warn(
"More than one Display class in file {}. "
- "The first occurrence (in alphabetical order) will be opened: {}".format(
- pyfile, classes[0].__name__), RuntimeWarning, stacklevel=2)
+ "The first occurrence (in alphabetical order) will be opened: {}".format(pyfile, classes[0].__name__),
+ RuntimeWarning,
+ stacklevel=2,
+ )
cls = classes[0]
module_params = inspect.signature(cls).parameters
@@ -272,10 +276,10 @@ def load_py_file(pyfile, args=None, macros=None):
# Because older versions of Display may not have the args parameter or the macros parameter, we check
# to see if it does before trying to use them.
kwargs = {}
- if 'args' in module_params:
- kwargs['args'] = args
- if 'macros' in module_params:
- kwargs['macros'] = macros
+ if "args" in module_params:
+ kwargs["args"] = args
+ if "macros" in module_params:
+ kwargs["macros"] = macros
instance = cls(**kwargs)
instance._loaded_file = pyfile
return instance
@@ -289,7 +293,6 @@ def load_py_file(pyfile, args=None, macros=None):
class Display(QWidget):
-
def __init__(self, parent=None, args=None, macros=None, ui_filename=None):
super(Display, self).__init__(parent=parent)
self.ui = None
@@ -324,7 +327,7 @@ def next_display(self, display):
self._next_display = display
def menu_items(self):
- """ Returns a dictionary where the keys are the names of the menu entries,
+ """Returns a dictionary where the keys are the names of the menu entries,
and the values are callables, where the callable is the action performed
when the menu item is selected.
@@ -344,7 +347,7 @@ def menu_items(self):
return {}
def file_menu_items(self):
- """ Returns a dictionary accepting a protected set of keys corresponding to one or more
+ """Returns a dictionary accepting a protected set of keys corresponding to one or more
possible default actions in the "File" menu, with the values as callables, where the callable
is the action performed when the menu item is selected.
@@ -364,7 +367,7 @@ def file_menu_items(self):
return {}
def show_help(self) -> None:
- """ Show the associated help file for this display """
+ """Show the associated help file for this display"""
if self.help_window is not None:
self.help_window.show()
@@ -383,16 +386,15 @@ def args(self):
return self._args
def ui_filepath(self):
- """ Returns the path to the ui file relative to the file of the class
+ """Returns the path to the ui file relative to the file of the class
calling this function."""
if not self.ui_filename():
return None
path_to_class = sys.modules[self.__module__].__file__
- return path.join(path.dirname(path.realpath(path_to_class)),
- self.ui_filename())
+ return path.join(path.dirname(path.realpath(path_to_class)), self.ui_filename())
def ui_filename(self):
- """ Returns the name of the ui file. In modern PyDM, it is preferable
+ """Returns the name of the ui file. In modern PyDM, it is preferable
specify this via the ui_filename argument in Display's constructor,
rather than reimplementing this in Display subclasses."""
if self._ui_filename is None:
@@ -401,7 +403,7 @@ def ui_filename(self):
return self._ui_filename
def load_ui(self, macros=None):
- """ Load and parse the ui file, and make the file's widgets available
+ """Load and parse the ui file, and make the file's widgets available
in self.ui. Called by the initializer."""
if self.ui:
return self.ui
@@ -409,13 +411,13 @@ def load_ui(self, macros=None):
self.load_ui_from_file(self.ui_filepath(), macros)
def load_ui_from_file(self, ui_file_path: str, macros: Optional[Dict[str, str]] = None):
- """ Load the ui file from the input path, and make the file's widgets available in self.ui """
+ """Load the ui file from the input path, and make the file's widgets available in self.ui"""
self._loaded_file = ui_file_path
code_string, class_name = _compile_ui_file(ui_file_path)
_load_compiled_ui_into_display(code_string, class_name, self, macros)
def load_help_file(self, file_path: str) -> None:
- """ Loads the input help file into a window for display """
+ """Loads the input help file into a window for display"""
self.help_window = HelpWindow(file_path)
def setStyleSheet(self, new_stylesheet):
@@ -429,7 +431,9 @@ def setStyleSheet(self, new_stylesheet):
stylesheet_filename = possible_stylesheet_filename
# Second, check if the css file is specified relative to the display file.
else:
- rel_path = os.path.join(os.path.dirname(os.path.abspath(self._loaded_file)), possible_stylesheet_filename)
+ rel_path = os.path.join(
+ os.path.dirname(os.path.abspath(self._loaded_file)), possible_stylesheet_filename
+ )
if os.path.isfile(rel_path):
stylesheet_filename = rel_path
except Exception as e:
diff --git a/pydm/display_module.py b/pydm/display_module.py
index e46151aa4f..426a923699 100644
--- a/pydm/display_module.py
+++ b/pydm/display_module.py
@@ -1,10 +1,10 @@
import warnings
-warnings.warn("The display_module was renamed to display."
- "Please modify your code to reflect this change.",
- DeprecationWarning, stacklevel=2)
+from pydm.display import Display, ScreenTarget, load_file, load_py_file, load_ui_file
-__all__ = ['Display', 'ScreenTarget',
- 'load_file', 'load_py_file', 'load_ui_file']
+warnings.warn(
+ "The display_module was renamed to display." "Please modify your code to reflect this change.",
+ DeprecationWarning,
+ stacklevel=2,
+)
-from pydm.display import (Display, ScreenTarget,
- load_file, load_py_file, load_ui_file)
+__all__ = ["Display", "ScreenTarget", "load_file", "load_py_file", "load_ui_file"]
diff --git a/pydm/exception.py b/pydm/exception.py
index ad42b1f193..eba1abaed4 100644
--- a/pydm/exception.py
+++ b/pydm/exception.py
@@ -9,7 +9,7 @@
from .utilities import only_main_thread
"""
-Utility functions which installs an exception hook and displays any global
+Utility functions which installs an exception hook and displays any global
uncaught exception to operators.
excepthook is based on https://fman.io/blog/pyqt-excepthook/
@@ -22,14 +22,14 @@ class ExceptionDispatcher(QtCore.QThread):
Singleton QTread class that receives and dispatch uncaught exceptions via
the `newException` signal.
"""
+
newException = QtCore.Signal(object)
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
- cls.__instance = QtCore.QThread.__new__(ExceptionDispatcher,
- *args, **kwargs)
+ cls.__instance = QtCore.QThread.__new__(ExceptionDispatcher, *args, **kwargs)
cls.__instance.__initialized = False
return cls.__instance
@@ -88,12 +88,12 @@ class DefaultExceptionNotifier(QtCore.QObject):
and also calls `raise_to_operator` to display a dialog with the exception
information.
"""
+
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
- cls.__instance = QtCore.QObject.__new__(DefaultExceptionNotifier,
- *args, **kwargs)
+ cls.__instance = QtCore.QObject.__new__(DefaultExceptionNotifier, *args, **kwargs)
cls.__instance.__initialized = False
return cls.__instance
@@ -113,9 +113,7 @@ def receiveException(self, exception_data):
_old_excepthook = None
-fake_tb = namedtuple(
- 'fake_tb', ('tb_frame', 'tb_lasti', 'tb_lineno', 'tb_next')
-)
+fake_tb = namedtuple("fake_tb", ("tb_frame", "tb_lasti", "tb_lineno", "tb_next"))
def excepthook(exc_type, exc_value, exc_tb):
@@ -139,12 +137,10 @@ def _add_missing_frames(tb):
traceback. Finally, the default sys.__excepthook__(...) does not work with
fake data, so we need to call traceback.print_exception(...) instead.
"""
- result = fake_tb(tb.tb_frame, tb.tb_lasti,
- tb.tb_lineno, tb.tb_next)
+ result = fake_tb(tb.tb_frame, tb.tb_lasti, tb.tb_lineno, tb.tb_next)
frame = tb.tb_frame.f_back
while frame:
- result = fake_tb(frame, frame.f_lasti,
- frame.f_lineno, result)
+ result = fake_tb(frame, frame.f_lasti, frame.f_lineno, result)
frame = frame.f_back
return result
@@ -169,7 +165,7 @@ def install(use_default_handler=True):
if dispatcher.isRunning():
return
if use_default_handler:
- handler = DefaultExceptionNotifier()
+ DefaultExceptionNotifier()
dispatcher.start()
sys.excepthook = excepthook
@@ -191,7 +187,7 @@ def uninstall():
def raise_to_operator(exc):
"""Utility function to show an Exception to the operator"""
err_msg = QtWidgets.QMessageBox()
- err_msg.setText('{}: {}'.format(exc.__class__.__name__, exc))
+ err_msg.setText("{}: {}".format(exc.__class__.__name__, exc))
err_msg.setWindowTitle(type(exc).__name__)
err_msg.setIcon(QtWidgets.QMessageBox.Critical)
handle = io.StringIO()
diff --git a/pydm/help_files/__init__.py b/pydm/help_files/__init__.py
index 43c6266650..f32a0f763e 100644
--- a/pydm/help_files/__init__.py
+++ b/pydm/help_files/__init__.py
@@ -1 +1,5 @@
from .help_window import HelpWindow
+
+__all__ = [
+ "HelpWindow",
+]
diff --git a/pydm/help_files/help_window.py b/pydm/help_files/help_window.py
index d5eb43c02d..801f516bc1 100644
--- a/pydm/help_files/help_window.py
+++ b/pydm/help_files/help_window.py
@@ -13,17 +13,18 @@ class HelpWindow(QWidget):
help_file_path : str
The path to the help file to be displayed
"""
+
def __init__(self, help_file_path: str, parent: Optional[QWidget] = None):
super().__init__(parent, Qt.Window)
self.resize(500, 400)
path = Path(help_file_path)
- self.setWindowTitle(f'Help for {path.stem}')
+ self.setWindowTitle(f"Help for {path.stem}")
self.display_content = QTextBrowser()
with open(help_file_path) as file:
- if path.suffix == '.txt':
+ if path.suffix == ".txt":
self.display_content.setText(file.read())
else:
self.display_content.setHtml(file.read())
diff --git a/pydm/main_window.py b/pydm/main_window.py
index 87dc9d65f7..40239733e5 100644
--- a/pydm/main_window.py
+++ b/pydm/main_window.py
@@ -1,12 +1,10 @@
import os
from os import path
-from qtpy.QtWidgets import (QApplication, QMainWindow, QFileDialog,
- QAction, QMessageBox)
+from qtpy.QtWidgets import QApplication, QMainWindow, QFileDialog, QAction, QMessageBox
from qtpy.QtCore import Qt, QTimer, Slot, QSize, QLibraryInfo
from qtpy.QtGui import QKeySequence
-from .utilities import (IconFont, find_file, establish_widget_connections,
- close_widget_connections)
+from .utilities import IconFont, find_file, establish_widget_connections, close_widget_connections
from .pydm_ui import Ui_MainWindow
from .display import Display, ScreenTarget, load_file
from .connection_inspector import ConnectionInspector
@@ -20,15 +18,21 @@
import platform
import logging
-from .utilities.stylesheet import apply_stylesheet, _get_style_data
logger = logging.getLogger(__name__)
class PyDMMainWindow(QMainWindow):
-
- def __init__(self, parent=None, hide_nav_bar=False, hide_menu_bar=False, hide_status_bar=False,
- home_file=None, macros=None, command_line_args=None):
+ def __init__(
+ self,
+ parent=None,
+ hide_nav_bar=False,
+ hide_menu_bar=False,
+ hide_status_bar=False,
+ home_file=None,
+ macros=None,
+ command_line_args=None,
+ ):
super(PyDMMainWindow, self).__init__(parent)
self.app = QApplication.instance()
self.font_factor = 1
@@ -100,14 +104,17 @@ def __init__(self, parent=None, hide_nav_bar=False, hide_menu_bar=False, hide_st
# Try to find the designer binary.
self.ui.actionEdit_in_Designer.setEnabled(False)
- possible_designer_bin_paths = (QLibraryInfo.location(QLibraryInfo.BinariesPath), QLibraryInfo.location(QLibraryInfo.LibraryExecutablesPath))
+ possible_designer_bin_paths = (
+ QLibraryInfo.location(QLibraryInfo.BinariesPath),
+ QLibraryInfo.location(QLibraryInfo.LibraryExecutablesPath),
+ )
for bin_path in possible_designer_bin_paths:
- if platform.system() == 'Darwin':
- designer_path = os.path.join(bin_path, 'Designer.app/Contents/MacOS/Designer')
- elif platform.system() == 'Linux':
- designer_path = os.path.join(bin_path, 'designer')
+ if platform.system() == "Darwin":
+ designer_path = os.path.join(bin_path, "Designer.app/Contents/MacOS/Designer")
+ elif platform.system() == "Linux":
+ designer_path = os.path.join(bin_path, "designer")
else:
- designer_path = os.path.join(bin_path, 'designer.exe')
+ designer_path = os.path.join(bin_path, "designer.exe")
if os.path.isfile(designer_path):
self.designer_path = designer_path
break
@@ -158,14 +165,16 @@ def back(self, open_in_new_process=False):
prev_display = curr_display.previous_display
if not prev_display:
- logger.error('No display history to execute back navigation.')
+ logger.error("No display history to execute back navigation.")
return
if open_in_new_process:
- load_file(prev_display.loaded_file(),
- macros=prev_display.macros,
- args=prev_display.args,
- target=ScreenTarget.NEW_PROCESS)
+ load_file(
+ prev_display.loaded_file(),
+ macros=prev_display.macros,
+ args=prev_display.args,
+ target=ScreenTarget.NEW_PROCESS,
+ )
else:
prev_display.next_display = curr_display
establish_widget_connections(prev_display)
@@ -183,14 +192,16 @@ def forward(self, open_in_new_process=False):
next_display = curr_display.next_display
if not next_display:
- logger.error('No display history to execute forward navigation.')
+ logger.error("No display history to execute forward navigation.")
return
if open_in_new_process:
- load_file(next_display.loaded_file(),
- macros=next_display.macros,
- args=next_display.args,
- target=ScreenTarget.NEW_PROCESS)
+ load_file(
+ next_display.loaded_file(),
+ macros=next_display.macros,
+ args=next_display.args,
+ target=ScreenTarget.NEW_PROCESS,
+ )
else:
establish_widget_connections(next_display)
register_widget_rules(next_display)
@@ -209,10 +220,7 @@ def home(self, open_in_new_process=False):
fname = self.home_widget.loaded_file()
macros = self.home_widget.macros()
args = self.home_widget.args()
- load_file(fname,
- macros=macros,
- args=args,
- target=ScreenTarget.NEW_PROCESS)
+ load_file(fname, macros=macros, args=args, target=ScreenTarget.NEW_PROCESS)
else:
if self.home_widget != self.display_widget():
self.home_widget.previous_display = self.display_widget()
@@ -226,8 +234,7 @@ def enable_disable_navigation(self):
# We can't do much if it is not a Display and we don't have the
# previous_display and next_display properties since we don't
# have the navigation stack set.
- nav_stack_methods = hasattr(w, 'previous_display') \
- and hasattr(w, 'next_display')
+ nav_stack_methods = hasattr(w, "previous_display") and hasattr(w, "next_display")
if not nav_stack_methods:
return
if not w:
@@ -295,7 +302,7 @@ def get_files_in_display(self):
return None, None
_, extension = path.splitext(curr_file)
- if extension == '.ui':
+ if extension == ".ui":
return self.current_file(), None
else:
central_widget = self.centralWidget() if isinstance(self.centralWidget(), Display) else None
@@ -305,7 +312,6 @@ def get_files_in_display(self):
@Slot(bool)
def edit_in_designer(self, checked):
-
def open_editor_ui(fname):
if self.designer_path is None or fname is None or fname == "":
return
@@ -314,9 +320,9 @@ def open_editor_ui(fname):
def open_editor_generic(fname):
if platform.system() == "Darwin":
- subprocess.call(('open', fname))
+ subprocess.call(("open", fname))
elif platform.system() == "Linux":
- subprocess.call(('xdg-open', fname))
+ subprocess.call(("xdg-open", fname))
elif platform.system() == "Windows":
os.startfile(fname)
@@ -335,7 +341,7 @@ def open_file_action(self, checked):
except Exception:
folder = os.getcwd()
- filename = QFileDialog.getOpenFileName(self, 'Open File...', folder, 'PyDM Display Files (*.ui *.py)')
+ filename = QFileDialog.getOpenFileName(self, "Open File...", folder, "PyDM Display Files (*.ui *.py)")
filename = filename[0] if isinstance(filename, (list, tuple)) else filename
if filename:
@@ -358,14 +364,15 @@ def change_stylesheet_action(self, checked):
except Exception:
folder = os.getcwd()
- ss_filename = QFileDialog.getOpenFileName(self, 'Change Stylesheet...',
- folder, 'PyDM Stylesheets (*.qss *.css)')
-
+ ss_filename = QFileDialog.getOpenFileName(
+ self, "Change Stylesheet...", folder, "PyDM Stylesheets (*.qss *.css)"
+ )
+
if ss_filename:
ss_filename = str(ss_filename)
self.ss_path = ss_filename.split("'")
- style= open(self.ss_path[1],"r")
- style= style.read()
+ style = open(self.ss_path[1], "r")
+ style = style.read()
self.setStyleSheet(style)
def open(self, filename, macros=None, args=None, target=None):
@@ -375,10 +382,7 @@ def open(self, filename, macros=None, args=None, target=None):
if curr_display:
base_path = os.path.dirname(curr_display.loaded_file())
filename = find_file(filename, base_path=base_path, raise_if_not_found=True)
- new_widget = load_file(filename,
- macros=macros,
- args=args,
- target=target)
+ new_widget = load_file(filename, macros=macros, args=args, target=target)
if new_widget:
if self.home_widget is None:
self.home_widget = new_widget
@@ -392,7 +396,7 @@ def open(self, filename, macros=None, args=None, target=None):
editors.append("Designer")
if py_file:
editors.append("Text Editor")
- edit_in_text = "Open in {}".format(' and '.join(editors))
+ edit_in_text = "Open in {}".format(" and ".join(editors))
self.ui.actionEdit_in_Designer.setText(edit_in_text)
if (self.designer_path and ui_file) or (py_file and not ui_file):
self.ui.actionEdit_in_Designer.setEnabled(True)
@@ -404,7 +408,7 @@ def load_tool(self, checked):
except Exception:
curr_dir = os.getcwd()
logger.error("The display manager does not have a display loaded. Suggesting current work directory.")
- filename = QFileDialog.getOpenFileName(self, 'Load tool...', curr_dir, 'PyDM External Tool Files (*_tool.py)')
+ filename = QFileDialog.getOpenFileName(self, "Load tool...", curr_dir, "PyDM External Tool Files (*_tool.py)")
filename = filename[0] if isinstance(filename, (list, tuple)) else filename
if filename:
@@ -416,10 +420,8 @@ def update_tools_menu(self):
"""
Update the Main Window Tools menu.
"""
- kwargs = {'channels': None, 'sender': self}
- tools.assemble_tools_menu(self.ui.menuTools,
- clear_menu=True,
- **kwargs)
+ kwargs = {"channels": None, "sender": self}
+ tools.assemble_tools_menu(self.ui.menuTools, clear_menu=True, **kwargs)
self.ui.menuTools.addSeparator()
self.ui.menuTools.addAction(self.ui.actionLoadTool)
@@ -439,8 +441,7 @@ def reload_display(self, checked):
args = curr_display.args()
loaded_file = curr_display.loaded_file()
- self.statusBar().showMessage(
- "Reloading '{0}'...".format(self.current_file()), 5000)
+ self.statusBar().showMessage("Reloading '{0}'...".format(self.current_file()), 5000)
new_widget = self.open(loaded_file, macros=macros, args=args)
new_widget.previous_display = prev_display
new_widget.next_display = next_display
@@ -465,12 +466,12 @@ def reset_font_size(self, checked):
def set_font_size(self, old, new):
current_font = self.app.font()
- current_font.setPointSizeF(current_font.pointSizeF()/old*new)
+ current_font.setPointSizeF(current_font.pointSizeF() / old * new)
QApplication.instance().setFont(current_font)
for w in self.app.allWidgets():
w_c_f = w.font()
- w_c_f.setPointSizeF(w_c_f.pointSizeF()/old*new)
+ w_c_f.setPointSizeF(w_c_f.pointSizeF() / old * new)
w.setFont(w_c_f)
QTimer.singleShot(0, self.resizeForNewDisplayWidget)
@@ -488,7 +489,7 @@ def show_connections(self, checked):
c.show()
def show_help(self):
- """ Show the associated help file for this window """
+ """Show the associated help file for this window"""
if self.display_widget() is not None:
self.display_widget().show_help()
@@ -523,8 +524,8 @@ def _confirm_quit(self, callback):
callback()
else:
quit_message = QMessageBox.question(
- self, 'Quitting Application', 'Exit Application?',
- QMessageBox.Yes | QMessageBox.No)
+ self, "Quitting Application", "Exit Application?", QMessageBox.Yes | QMessageBox.No
+ )
if quit_message == QMessageBox.Yes:
callback()
@@ -550,7 +551,7 @@ def add_menu_items(self):
# connect custom save, save as, and load functions
file_menu_items = self.display_widget().file_menu_items()
if len(file_menu_items) != 0:
- valid_keys = ('save', 'save_as', 'load')
+ valid_keys = ("save", "save_as", "load")
ui_actions = (self.ui.actionSave, self.ui.actionSave_As, self.ui.actionLoad)
action_dict = dict(zip(valid_keys, ui_actions))
# iterate through user given keys, which need to match the possible keys
@@ -580,7 +581,8 @@ def create_menu(menu, items):
# iterate through the items list and add to the menu
for key in items.keys():
val = items[key]
- # if there is a dictionary nested in the primary dictionary, create a new submenu, and call create_menu with that submenu as the parent menu
+ # if there is a dictionary nested in the primary dictionary, create a new submenu, and call create_menu
+ # with that submenu as the parent menu
if isinstance(val, dict):
new_menu = menu.addMenu(key)
PyDMMainWindow().create_menu(new_menu, val)
diff --git a/pydm/pydm_ui.py b/pydm/pydm_ui.py
index b3e554ecc0..d2baffdc05 100644
--- a/pydm/pydm_ui.py
+++ b/pydm/pydm_ui.py
@@ -7,7 +7,7 @@
# WARNING! All changes made in this file will be lost!
-from qtpy import QtCore, QtGui, QtWidgets
+from qtpy import QtCore, QtWidgets
class Ui_MainWindow(object):
@@ -106,13 +106,13 @@ def setupUi(self, MainWindow):
self.actionQuit.setEnabled(True)
self.actionQuit.setObjectName("actionQuit")
self.actionSave = QtWidgets.QAction(MainWindow)
- self.actionSave.setObjectName('actionSave')
+ self.actionSave.setObjectName("actionSave")
self.actionSave.setVisible(False)
self.actionSave_As = QtWidgets.QAction(MainWindow)
- self.actionSave_As.setObjectName('actionSave_As')
+ self.actionSave_As.setObjectName("actionSave_As")
self.actionSave_As.setVisible(False)
self.actionLoad = QtWidgets.QAction(MainWindow)
- self.actionLoad.setObjectName('actionLoad')
+ self.actionLoad.setObjectName("actionLoad")
self.actionLoad.setVisible(False)
self.actionChange_Stylesheet = QtWidgets.QAction(MainWindow)
self.actionChange_Stylesheet.setObjectName("actionChange_Stylesheet")
diff --git a/pydm/qtdesigner.py b/pydm/qtdesigner.py
index 8ea4bb631f..ed00194ab0 100644
--- a/pydm/qtdesigner.py
+++ b/pydm/qtdesigner.py
@@ -10,6 +10,7 @@ class DesignerHooks(object):
Class that handles the integration with PyDM and the Qt Designer
by hooking up slots to signals provided by FormEditor and other classes.
"""
+
__instance = None
def __init__(self):
@@ -48,9 +49,7 @@ def setup_hooks(self):
if self.form_editor:
fwman = self.form_editor.formWindowManager()
if fwman:
- fwman.formWindowAdded.connect(
- self.__new_form_added
- )
+ fwman.formWindowAdded.connect(self.__new_form_added)
def __new_form_added(self, form_window_interface):
style_data = stylesheet._get_style_data(None)
@@ -68,7 +67,7 @@ def __kick(self):
def __handle_exceptions(self, exc_type, value, trace):
print("Exception occurred while running Qt Designer.")
- msg = ''.join(traceback.format_exception(exc_type, value, trace))
+ msg = "".join(traceback.format_exception(exc_type, value, trace))
print(msg)
def __start_kicker(self):
diff --git a/pydm/show_macros/__init__.py b/pydm/show_macros/__init__.py
index c128e845be..51cdeb7f9d 100644
--- a/pydm/show_macros/__init__.py
+++ b/pydm/show_macros/__init__.py
@@ -1 +1,5 @@
from .show_macros import MacroWindow
+
+__all__ = [
+ "MacroWindow",
+]
diff --git a/pydm/show_macros/show_macros.py b/pydm/show_macros/show_macros.py
index 1f2b8ba18a..6a4cff9b04 100644
--- a/pydm/show_macros/show_macros.py
+++ b/pydm/show_macros/show_macros.py
@@ -1,12 +1,22 @@
import json
-from qtpy.QtWidgets import QWidget, QPlainTextEdit, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QApplication, QLabel
+from qtpy.QtWidgets import (
+ QWidget,
+ QPlainTextEdit,
+ QVBoxLayout,
+ QHBoxLayout,
+ QPushButton,
+ QApplication,
+ QLabel,
+)
from qtpy.QtCore import Qt
from qtpy.QtGui import QFont
+
class MacroWindow(QWidget):
"""
A replica of EDM's "Show Macros" display
"""
+
def __init__(self, parent=None):
super().__init__(parent, Qt.Window)
self.setWindowTitle("Macros")
@@ -25,7 +35,7 @@ def __init__(self, parent=None):
self.populate_macros()
self.label = QLabel()
- self.label.setText('')
+ self.label.setText("")
self.label.setMinimumSize(100, 25)
self.copy_button = QPushButton()
@@ -59,4 +69,4 @@ def populate_macros(self):
def copy_macros(self):
self.clipboard.setText(json.dumps(self.macros))
- self.label.setText('Macros copied to clipboard!')
+ self.label.setText("Macros copied to clipboard!")
diff --git a/pydm/tests/conftest.py b/pydm/tests/conftest.py
index 51686cd0cb..01960ade98 100644
--- a/pydm/tests/conftest.py
+++ b/pydm/tests/conftest.py
@@ -28,6 +28,7 @@ class ConnectionSignals(QObject):
"""
An assortment of signals, to which a unit test can choose from and bind an appropriate slot
"""
+
new_value_signal = Signal([float], [int], [str], [np.ndarray])
connection_state_signal = Signal(bool)
new_severity_signal = Signal(int)
@@ -112,7 +113,7 @@ def signals():
return ConnectionSignals()
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
def qapp(qapp_args):
"""
Fixture for a PyDMApplication app instance.
@@ -128,10 +129,10 @@ def qapp(qapp_args):
yield PyDMApplication(use_main_window=False, *qapp_args)
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
def test_plugin():
# Create test PyDMPlugin with mock protocol
test_plug = PyDMPlugin
- test_plug.protocol = 'tst'
+ test_plug.protocol = "tst"
add_plugin(test_plug)
return test_plug
diff --git a/pydm/tests/data_plugins/test_archiver_plugin.py b/pydm/tests/data_plugins/test_archiver_plugin.py
index 01bffdb4f3..8ef351a82c 100644
--- a/pydm/tests/data_plugins/test_archiver_plugin.py
+++ b/pydm/tests/data_plugins/test_archiver_plugin.py
@@ -8,39 +8,45 @@
from pydm.widgets.channel import PyDMChannel
import logging
+
logger = logging.getLogger(__name__)
class MockNetworkManager:
- """ A mock of the Qt NetworkManager. Does not actually make any requests, but allows the
- inspection of what those requests would have been.
+ """A mock of the Qt NetworkManager. Does not actually make any requests, but allows the
+ inspection of what those requests would have been.
"""
+
def __init__(self):
self.request_url = None
def get(self, request: QNetworkRequest):
- """ Simply set the request_url to the call that would have been made to the archiver """
+ """Simply set the request_url to the call that would have been made to the archiver"""
self.request_url = request.url().url()
class MockNetworkReply:
- """ A mock of a reply made from the archiver. Setup here rather than in a unit test to keep the
- test clean as the response is rather long-winded. """
+ """A mock of a reply made from the archiver. Setup here rather than in a unit test to keep the
+ test clean as the response is rather long-winded."""
def __init__(self, is_optimized: bool):
self.data = None
if is_optimized:
- self.response = b'[ \n{ "meta": { "name": "ROOM:TEMP" , "EGU": "DegF" , "PREC": "1" },\n"data": ' \
- b'[ \n{ "secs": 100, "val": [53, 0.2, 52, 54, 10], "nanos": 10, "severity":0, ' \
- b'"status":0 } ,\n{ "secs": 101, "val": [54.1, 0.3, 54, 55, 10], "nanos": 47, ' \
- b'"severity":0, "status":0 } ,\n{ "secs": 102, "val": [53.9, 0.1, 53.8, 54, 10], ' \
- b'"nanos": 22, "severity":0, "status":0 }\n] }\n ]\n'
+ self.response = (
+ b'[ \n{ "meta": { "name": "ROOM:TEMP" , "EGU": "DegF" , "PREC": "1" },\n"data": '
+ b'[ \n{ "secs": 100, "val": [53, 0.2, 52, 54, 10], "nanos": 10, "severity":0, '
+ b'"status":0 } ,\n{ "secs": 101, "val": [54.1, 0.3, 54, 55, 10], "nanos": 47, '
+ b'"severity":0, "status":0 } ,\n{ "secs": 102, "val": [53.9, 0.1, 53.8, 54, 10], '
+ b'"nanos": 22, "severity":0, "status":0 }\n] }\n ]\n'
+ )
self.url_obj = QUrl("http://mock-pydm-url.com/response&pv=optimized")
else:
- self.response = b'[ \n{ "meta": { "name": "ROOM:TEMP" , "EGU": "DegF" , "PREC": "1" },\n"data": ' \
- b'[ \n{ "secs": 100, "val": 53, "nanos": 10, "severity":0, "status":0 },\n{ "secs": 101,' \
- b' "val": 54.1, "nanos": 47, "severity":0, "status":0 },\n{ "secs": 102, "val": 53.9, ' \
- b'"nanos": 22, "severity":0, "status":0 }\n] }\n ]\n'
+ self.response = (
+ b'[ \n{ "meta": { "name": "ROOM:TEMP" , "EGU": "DegF" , "PREC": "1" },\n"data": '
+ b'[ \n{ "secs": 100, "val": 53, "nanos": 10, "severity":0, "status":0 },\n{ "secs": 101,'
+ b' "val": 54.1, "nanos": 47, "severity":0, "status":0 },\n{ "secs": 102, "val": 53.9, '
+ b'"nanos": 22, "severity":0, "status":0 }\n] }\n ]\n'
+ )
self.url_obj = QUrl("http://mock-pydm-url")
def readAll(self):
@@ -61,7 +67,7 @@ def deleteLater(self):
@mock.patch.dict(os.environ, {"PYDM_ARCHIVER_URL": "http://mock-pydm-url"})
def test_fetch_data():
- """ Ensure that the url request is built correctly based on the input parameters received """
+ """Ensure that the url request is built correctly based on the input parameters received"""
mock_channel = PyDMChannel()
archiver_connection = Connection(mock_channel, "pv=mock_pv_address")
archiver_connection.network_manager = MockNetworkManager()
@@ -73,21 +79,25 @@ def test_fetch_data():
# This is requesting archive data between December 14th at 8AM and December 15th at 9:30 AM.
archiver_connection.fetch_data(1639468800, 1639560600)
- expected_url = "http://mock-pydm-url/retrieval/data/getData.json?pv=mock_pv_address" \
- "&from=2021-12-14T08:00:00.000Z&to=2021-12-15T09:30:00.000Z"
+ expected_url = (
+ "http://mock-pydm-url/retrieval/data/getData.json?pv=mock_pv_address"
+ "&from=2021-12-14T08:00:00.000Z&to=2021-12-15T09:30:00.000Z"
+ )
assert archiver_connection.network_manager.request_url == expected_url
# Finally try one that includes a processing command for the archiver appliance
archiver_connection.fetch_data(1639468800, 1639560600, "optimized_1000")
- expected_url = "http://mock-pydm-url/retrieval/data/getData.json?pv=optimized_1000(mock_pv_address)" \
- "&from=2021-12-14T08:00:00.000Z&to=2021-12-15T09:30:00.000Z"
+ expected_url = (
+ "http://mock-pydm-url/retrieval/data/getData.json?pv=optimized_1000(mock_pv_address)"
+ "&from=2021-12-14T08:00:00.000Z&to=2021-12-15T09:30:00.000Z"
+ )
assert archiver_connection.network_manager.request_url == expected_url
def test_data_request_finished(signals: ConnectionSignals):
- """ Verify that an archiver response is parsed correctly and sends the data out in the right format, using
- both the raw data and optimized data formats """
+ """Verify that an archiver response is parsed correctly and sends the data out in the right format, using
+ both the raw data and optimized data formats"""
mock_channel = PyDMChannel()
archiver_connection = Connection(mock_channel, "pv=mock_pv_address")
@@ -99,8 +109,7 @@ def test_data_request_finished(signals: ConnectionSignals):
archiver_connection.data_request_finished(mock_reply) # type: ignore
# Verify the data was sent in the expected format
- expected_data_sent = np.array([[100, 101, 102],
- [53, 54.1, 53.9]])
+ expected_data_sent = np.array([[100, 101, 102], [53, 54.1, 53.9]])
assert np.array_equal(signals.value, expected_data_sent)
# Now repeat the process, except this time as if we requested optimized data
@@ -108,9 +117,5 @@ def test_data_request_finished(signals: ConnectionSignals):
archiver_connection.data_request_finished(mock_reply) # type: ignore
# Verify the data was sent as expected (timestamps, values, standard deviations, minimums, maximums)
- expected_data_sent = np.array([[100, 101, 102],
- [53, 54.1, 53.9],
- [0.2, 0.3, 0.1],
- [52, 54, 53.8],
- [54, 55, 54]])
+ expected_data_sent = np.array([[100, 101, 102], [53, 54.1, 53.9], [0.2, 0.3, 0.1], [52, 54, 53.8], [54, 55, 54]])
assert np.array_equal(signals._value, expected_data_sent)
diff --git a/pydm/tests/data_plugins/test_calc_plugin.py b/pydm/tests/data_plugins/test_calc_plugin.py
index ffb3145f5e..f585498bd8 100644
--- a/pydm/tests/data_plugins/test_calc_plugin.py
+++ b/pydm/tests/data_plugins/test_calc_plugin.py
@@ -13,9 +13,9 @@
@pytest.mark.parametrize(
"input_string,expected",
[
- (np.array((0x6f, 0x6b, 0x61, 0x79, 0, 42), dtype=np.int8), "okay"),
- (np.array((0x6f, 0x6b, 0x61, 0x79), dtype=np.int8), "okay"),
- (np.array((0, 0x6f, 0x6b, 0x61, 0x79, 0, 42, 42), dtype=np.int8), ""),
+ (np.array((0x6F, 0x6B, 0x61, 0x79, 0, 42), dtype=np.int8), "okay"),
+ (np.array((0x6F, 0x6B, 0x61, 0x79), dtype=np.int8), "okay"),
+ (np.array((0, 0x6F, 0x6B, 0x61, 0x79, 0, 42, 42), dtype=np.int8), ""),
],
)
def test_epics_string(input_string: str, expected: str):
@@ -37,14 +37,12 @@ def test_epics_unsigned(input_int: int, bits: int, expected: int):
@pytest.mark.parametrize(
"calc,input1,expected1,input2,expected2",
[
- ('val + 3', 0, 3, 1, 4),
- ('int(np.abs(val))', -5, 5, -10, 10),
- ('math.floor(val)', 3.4, 3, 5.7, 5),
- ('epics_string(val)',
- np.array((0x61, 0), dtype=np.int8), 'a',
- np.array((0x62, 0), dtype=np.int8), 'b'),
- ('epics_unsigned(val, 8)', -1, 255, -2, 254),
- ]
+ ("val + 3", 0, 3, 1, 4),
+ ("int(np.abs(val))", -5, 5, -10, 10),
+ ("math.floor(val)", 3.4, 3, 5.7, 5),
+ ("epics_string(val)", np.array((0x61, 0), dtype=np.int8), "a", np.array((0x62, 0), dtype=np.int8), "b"),
+ ("epics_unsigned(val, 8)", -1, 255, -2, 254),
+ ],
)
def test_calc_plugin(
qapp: PyDMApplication,
@@ -60,9 +58,9 @@ class SigHolder(QObject):
sig_holder = SigHolder()
type_str = str(type(input1))
- local_addr = f'loc://test_calc_plugin_local_{calc}'
+ local_addr = f"loc://test_calc_plugin_local_{calc}"
local_ch = PyDMChannel(
- address=f'{local_addr}?type={type_str}&init={input1}',
+ address=f"{local_addr}?type={type_str}&init={input1}",
value_signal=sig_holder.sig,
)
local_ch.connect()
@@ -71,9 +69,9 @@ class SigHolder(QObject):
def new_calc_value(val: Any):
calc_values.append(val)
- calc_addr = f'calc://test_calc_plugin_calc_{calc}'
+ calc_addr = f"calc://test_calc_plugin_calc_{calc}"
calc_ch = PyDMChannel(
- address=f'{calc_addr}?val={local_addr}&expr={calc}',
+ address=f"{calc_addr}?val={local_addr}&expr={calc}",
value_slot=new_calc_value,
)
calc_ch.connect()
diff --git a/pydm/tests/data_plugins/test_p4p_plugin_component.py b/pydm/tests/data_plugins/test_p4p_plugin_component.py
index 7bfb05609d..c3e98384e5 100644
--- a/pydm/tests/data_plugins/test_p4p_plugin_component.py
+++ b/pydm/tests/data_plugins/test_p4p_plugin_component.py
@@ -6,93 +6,110 @@
from pydm.tests.conftest import ConnectionSignals
from pydm.widgets.channel import PyDMChannel
from pytest import MonkeyPatch
-from p4p.wrapper import Value
+from p4p.wrapper import Value
from p4p import Type
class MockContext:
- """ A do-nothing mock of a p4p context object """
+ """A do-nothing mock of a p4p context object"""
+
def __init__(self):
self.monitor = None
def generate_control_variables(value):
- """ Generate some set values for control variables to test against """
- return {'value': value,
- 'valueAlarm': {'lowAlarmLimit': 2, 'lowWarningLimit': 3, 'highAlarmLimit': 10, 'highWarningLimit': 8},
- 'alarm': {'severity': 0},
- 'display': {'units': 'mV'},
- 'control': {'limitLow': 1, 'limitHigh': 11}
- }
-
-
-@pytest.mark.parametrize('value_to_send, has_ctrl_vars, expected_value_to_receive, expected_signal_count', [
- (NTScalar("i", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(5)), True, 5, 9),
- (NTScalar("b", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(1)), True, 1, 9),
- (NTScalar("h", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(2)), True, 2, 9),
- (NTScalar("f", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(7.0)), True, 7.0, 9),
- (NTScalar("s").wrap({'value': 'PyDM:TEST'}), False, 'PyDM:TEST', 1),
- (NTScalar("ai").wrap({'value': [1, 2, 4]}), False, [1, 2, 4], 1),
- (NTScalar("ab").wrap({'value': [6, 7, 8]}), False, [6, 7, 8], 1),
- (NTScalar("ah").wrap({'value': [10, 11, 12]}), False, [10, 11, 12], 1),
- (NTScalar("af").wrap({'value': [5.0, 6.0, 7.0, 8.0]}), False, [5.0, 6.0, 7.0, 8.0], 1),
- (NTScalar("as").wrap({'value': ['PyDM', 'PVA', 'Test', 'Strings']}), False, ['PyDM', 'PVA', 'Test', 'Strings'], 1),
- (NTEnum().wrap({'index': 0, 'choices': ['YES', 'NO', 'MAYBE']}), False, 0, 2)
-])
-def test_send_new_value(monkeypatch: MonkeyPatch,
- signals: ConnectionSignals,
- value_to_send: Value,
- has_ctrl_vars: bool,
- expected_value_to_receive: object,
- expected_signal_count: int):
- """ Ensure that all our signals are emitted as expected based on the structured data we received from p4p """
+ """Generate some set values for control variables to test against"""
+ return {
+ "value": value,
+ "valueAlarm": {"lowAlarmLimit": 2, "lowWarningLimit": 3, "highAlarmLimit": 10, "highWarningLimit": 8},
+ "alarm": {"severity": 0},
+ "display": {"units": "mV"},
+ "control": {"limitLow": 1, "limitHigh": 11},
+ }
+
+
+@pytest.mark.parametrize(
+ "value_to_send, has_ctrl_vars, expected_value_to_receive, expected_signal_count",
+ [
+ (NTScalar("i", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(5)), True, 5, 9),
+ (NTScalar("b", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(1)), True, 1, 9),
+ (NTScalar("h", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(2)), True, 2, 9),
+ (
+ NTScalar("f", display=True, control=True, valueAlarm=True).wrap(generate_control_variables(7.0)),
+ True,
+ 7.0,
+ 9,
+ ),
+ (NTScalar("s").wrap({"value": "PyDM:TEST"}), False, "PyDM:TEST", 1),
+ (NTScalar("ai").wrap({"value": [1, 2, 4]}), False, [1, 2, 4], 1),
+ (NTScalar("ab").wrap({"value": [6, 7, 8]}), False, [6, 7, 8], 1),
+ (NTScalar("ah").wrap({"value": [10, 11, 12]}), False, [10, 11, 12], 1),
+ (NTScalar("af").wrap({"value": [5.0, 6.0, 7.0, 8.0]}), False, [5.0, 6.0, 7.0, 8.0], 1),
+ (
+ NTScalar("as").wrap({"value": ["PyDM", "PVA", "Test", "Strings"]}),
+ False,
+ ["PyDM", "PVA", "Test", "Strings"],
+ 1,
+ ),
+ (NTEnum().wrap({"index": 0, "choices": ["YES", "NO", "MAYBE"]}), False, 0, 2),
+ ],
+)
+def test_send_new_value(
+ monkeypatch: MonkeyPatch,
+ signals: ConnectionSignals,
+ value_to_send: Value,
+ has_ctrl_vars: bool,
+ expected_value_to_receive: object,
+ expected_signal_count: int,
+):
+ """Ensure that all our signals are emitted as expected based on the structured data we received from p4p"""
# Set up a mock p4p client
mock_channel = PyDMChannel()
- monkeypatch.setattr(P4PPlugin, 'context', MockContext())
- monkeypatch.setattr(P4PPlugin.context, 'monitor', lambda **args: None) # Don't want to actually setup a monitor
- p4p_connection = Connection(mock_channel, 'pva://TEST:ADDRESS')
+ monkeypatch.setattr(P4PPlugin, "context", MockContext())
+ monkeypatch.setattr(P4PPlugin.context, "monitor", lambda **args: None) # Don't want to actually setup a monitor
+ p4p_connection = Connection(mock_channel, "pva://TEST:ADDRESS")
received_values = {}
signals_received = 0
def receive_signal(value_name: str, value_received: object):
- """ A simple slot for receiving all our test signals and storing the values to ensure they are as expected """
+ """A simple slot for receiving all our test signals and storing the values to ensure they are as expected"""
received_values[value_name] = value_received
nonlocal signals_received
signals_received += 1
expected_value_type = type(value_to_send.value)
- if type(value_to_send.value) == list:
+ if isinstance(value_to_send.value, list):
expected_value_type = np.ndarray
- elif 'NTEnum' in value_to_send.getID():
+ elif "NTEnum" in value_to_send.getID():
expected_value_type = int
- p4p_connection.new_value_signal[expected_value_type].connect(functools.partial(receive_signal, 'value'))
- p4p_connection.lower_alarm_limit_signal.connect(functools.partial(receive_signal, 'low_alarm_limit'))
- p4p_connection.lower_warning_limit_signal.connect(functools.partial(receive_signal, 'low_warning_limit'))
- p4p_connection.upper_alarm_limit_signal.connect(functools.partial(receive_signal, 'high_alarm_limit'))
- p4p_connection.upper_warning_limit_signal.connect(functools.partial(receive_signal, 'high_warning_limit'))
- p4p_connection.new_severity_signal.connect(functools.partial(receive_signal, 'severity'))
- p4p_connection.unit_signal.connect(functools.partial(receive_signal, 'units'))
- p4p_connection.lower_ctrl_limit_signal.connect(functools.partial(receive_signal, 'lower_ctrl_limit'))
- p4p_connection.upper_ctrl_limit_signal.connect(functools.partial(receive_signal, 'upper_ctrl_limit'))
- p4p_connection.enum_strings_signal.connect(functools.partial(receive_signal, 'enum_strings'))
+ p4p_connection.new_value_signal[expected_value_type].connect(functools.partial(receive_signal, "value"))
+ p4p_connection.lower_alarm_limit_signal.connect(functools.partial(receive_signal, "low_alarm_limit"))
+ p4p_connection.lower_warning_limit_signal.connect(functools.partial(receive_signal, "low_warning_limit"))
+ p4p_connection.upper_alarm_limit_signal.connect(functools.partial(receive_signal, "high_alarm_limit"))
+ p4p_connection.upper_warning_limit_signal.connect(functools.partial(receive_signal, "high_warning_limit"))
+ p4p_connection.new_severity_signal.connect(functools.partial(receive_signal, "severity"))
+ p4p_connection.unit_signal.connect(functools.partial(receive_signal, "units"))
+ p4p_connection.lower_ctrl_limit_signal.connect(functools.partial(receive_signal, "lower_ctrl_limit"))
+ p4p_connection.upper_ctrl_limit_signal.connect(functools.partial(receive_signal, "upper_ctrl_limit"))
+ p4p_connection.enum_strings_signal.connect(functools.partial(receive_signal, "enum_strings"))
# Send out the initial value for our test PV
p4p_connection.send_new_value(value_to_send)
# Confirm all the signals were fired off as expected, and that each value is what we specified
- assert np.array_equal(received_values['value'], expected_value_to_receive)
+ assert np.array_equal(received_values["value"], expected_value_to_receive)
if has_ctrl_vars:
- assert received_values['low_alarm_limit'] == 2
- assert received_values['low_warning_limit'] == 3
- assert received_values['high_warning_limit'] == 8
- assert received_values['high_alarm_limit'] == 10
- assert received_values['severity'] == 0
- assert received_values['units'] == 'mV'
- assert received_values['lower_ctrl_limit'] == 1
- assert received_values['upper_ctrl_limit'] == 11
+ assert received_values["low_alarm_limit"] == 2
+ assert received_values["low_warning_limit"] == 3
+ assert received_values["high_warning_limit"] == 8
+ assert received_values["high_alarm_limit"] == 10
+ assert received_values["severity"] == 0
+ assert received_values["units"] == "mV"
+ assert received_values["lower_ctrl_limit"] == 1
+ assert received_values["upper_ctrl_limit"] == 11
assert signals_received == expected_signal_count
else:
assert signals_received == expected_signal_count
@@ -104,8 +121,8 @@ def receive_signal(value_name: str, value_received: object):
p4p_connection.send_new_value(value_to_send)
# Verify that the two values did update, and that the other 7 signals did not send
- assert received_values['value'] == 9
- assert received_values['severity'] == 1
+ assert received_values["value"] == 9
+ assert received_values["severity"] == 1
assert signals_received == 11
@@ -124,18 +141,17 @@ def test_set_value_by_keys():
def test_convert_epics_nttable():
- my_type = Type([
- ("secondsPastEpoch", 'l'),
- ("nanoseconds", 'i'),
- ("userTag", 'i'),
- ])
-
- epics_struct = Value(my_type, {"secondsPastEpoch": 0,
- "nanoseconds": 0,
- "userTag": 0
- })
-
- solution = {'secondsPastEpoch': 0, 'nanoseconds': 0, 'userTag': 0}
+ my_type = Type(
+ [
+ ("secondsPastEpoch", "l"),
+ ("nanoseconds", "i"),
+ ("userTag", "i"),
+ ]
+ )
+
+ epics_struct = Value(my_type, {"secondsPastEpoch": 0, "nanoseconds": 0, "userTag": 0})
+
+ solution = {"secondsPastEpoch": 0, "nanoseconds": 0, "userTag": 0}
result = Connection.convert_epics_nttable(epics_struct)
- assert result == solution
+ assert result == solution
diff --git a/pydm/tests/data_plugins/test_psp_plugin_component.py b/pydm/tests/data_plugins/test_psp_plugin_component.py
index 9ec71a29e0..884515ccdf 100644
--- a/pydm/tests/data_plugins/test_psp_plugin_component.py
+++ b/pydm/tests/data_plugins/test_psp_plugin_component.py
@@ -8,8 +8,10 @@
# Note: This test cannot be run on any Windows OS as a build of PyCA is not available there
+
class MockPV:
- """ A simple mock of the psp Pv object """
+ """A simple mock of the psp Pv object"""
+
def __init__(self):
self.data = {}
@@ -19,49 +21,49 @@ def severity(self):
def test_update_ctrl_vars(monkeypatch: MonkeyPatch, signals: ConnectionSignals):
- """ Invoke our callback for updating the control values for a PV as if we had a monitor on it. Verify
- that the signals sent are received as expected.
+ """Invoke our callback for updating the control values for a PV as if we had a monitor on it. Verify
+ that the signals sent are received as expected.
"""
# Initialize a mock channel and connection for testing
mock_channel = PyDMChannel()
mock_pv = MockPV()
- monkeypatch.setattr(pydm.data_plugins.epics_plugins.psp_plugin_component,
- 'setup_pv',
- lambda *args, **kwargs: mock_pv)
- monkeypatch.setattr(Connection, 'add_listener', lambda *args, **kwargs: None)
- psp_connection = Connection(mock_channel, 'Test:PV:1')
+ monkeypatch.setattr(
+ pydm.data_plugins.epics_plugins.psp_plugin_component, "setup_pv", lambda *args, **kwargs: mock_pv
+ )
+ monkeypatch.setattr(Connection, "add_listener", lambda *args, **kwargs: None)
+ psp_connection = Connection(mock_channel, "Test:PV:1")
psp_connection.count = 1
# Create some control values for our PV as if we had received them using psp
- mock_pv.data['precision'] = 3
- mock_pv.data['units'] = 'mV'
- mock_pv.data['ctrl_llim'] = 5.5
- mock_pv.data['ctrl_hlim'] = 30.5
- mock_pv.data['alarm_hlim'] = 28
- mock_pv.data['alarm_llim'] = 8
- mock_pv.data['warn_hlim'] = 22.5
- mock_pv.data['warn_llim'] = 10.25
+ mock_pv.data["precision"] = 3
+ mock_pv.data["units"] = "mV"
+ mock_pv.data["ctrl_llim"] = 5.5
+ mock_pv.data["ctrl_hlim"] = 30.5
+ mock_pv.data["alarm_hlim"] = 28
+ mock_pv.data["alarm_llim"] = 8
+ mock_pv.data["warn_hlim"] = 22.5
+ mock_pv.data["warn_llim"] = 10.25
# Connect the signals to a slot that allows us to inspect the value received
psp_connection.new_value_signal[float].connect(lambda: None) # Only testing control variables here
- psp_connection.prec_signal.connect(functools.partial(signals.receive_value, 'precision'))
- psp_connection.unit_signal.connect(functools.partial(signals.receive_value, 'units'))
- psp_connection.lower_ctrl_limit_signal.connect(functools.partial(signals.receive_value, 'ctrl_llim'))
- psp_connection.upper_ctrl_limit_signal.connect(functools.partial(signals.receive_value, 'ctrl_hlim'))
- psp_connection.upper_alarm_limit_signal.connect(functools.partial(signals.receive_value, 'alarm_hlim'))
- psp_connection.lower_alarm_limit_signal.connect(functools.partial(signals.receive_value, 'alarm_llim'))
- psp_connection.upper_warning_limit_signal.connect(functools.partial(signals.receive_value, 'warn_hlim'))
- psp_connection.lower_warning_limit_signal.connect(functools.partial(signals.receive_value, 'warn_llim'))
+ psp_connection.prec_signal.connect(functools.partial(signals.receive_value, "precision"))
+ psp_connection.unit_signal.connect(functools.partial(signals.receive_value, "units"))
+ psp_connection.lower_ctrl_limit_signal.connect(functools.partial(signals.receive_value, "ctrl_llim"))
+ psp_connection.upper_ctrl_limit_signal.connect(functools.partial(signals.receive_value, "ctrl_hlim"))
+ psp_connection.upper_alarm_limit_signal.connect(functools.partial(signals.receive_value, "alarm_hlim"))
+ psp_connection.lower_alarm_limit_signal.connect(functools.partial(signals.receive_value, "alarm_llim"))
+ psp_connection.upper_warning_limit_signal.connect(functools.partial(signals.receive_value, "warn_hlim"))
+ psp_connection.lower_warning_limit_signal.connect(functools.partial(signals.receive_value, "warn_llim"))
psp_connection.python_type = float
psp_connection.send_new_value(10.0)
# Verify all the signals were emitted as expected
- assert signals['precision'] == 3
- assert signals['units'] == 'mV'
- assert signals['ctrl_llim'] == 5.5
- assert signals['ctrl_hlim'] == 30.5
- assert signals['alarm_hlim'] == 28
- assert signals['alarm_llim'] == 8
- assert signals['warn_hlim'] == 22.5
- assert signals['warn_llim'] == 10.25
+ assert signals["precision"] == 3
+ assert signals["units"] == "mV"
+ assert signals["ctrl_llim"] == 5.5
+ assert signals["ctrl_hlim"] == 30.5
+ assert signals["alarm_hlim"] == 28
+ assert signals["alarm_llim"] == 8
+ assert signals["warn_hlim"] == 22.5
+ assert signals["warn_llim"] == 10.25
diff --git a/pydm/tests/data_plugins/test_pyepics_plugin_component.py b/pydm/tests/data_plugins/test_pyepics_plugin_component.py
index f050fed8fc..dd65ecd800 100644
--- a/pydm/tests/data_plugins/test_pyepics_plugin_component.py
+++ b/pydm/tests/data_plugins/test_pyepics_plugin_component.py
@@ -1,16 +1,15 @@
-from pydm.data_plugins.archiver_plugin import Connection
from pydm.data_plugins.epics_plugins.pyepics_plugin_component import Connection
from pydm.tests.conftest import ConnectionSignals
from pydm.widgets.channel import PyDMChannel
def test_update_ctrl_vars(signals: ConnectionSignals):
- """ Invoke our callback for updating the control values for a PV as if we had a monitor on it. Verify
- that the signals sent are received as expected.
+ """Invoke our callback for updating the control values for a PV as if we had a monitor on it. Verify
+ that the signals sent are received as expected.
"""
values_received = []
mock_channel = PyDMChannel()
- mock_pyepics_connection = Connection(mock_channel, 'Test:PV:1')
+ mock_pyepics_connection = Connection(mock_channel, "Test:PV:1")
mock_pyepics_connection.upper_alarm_limit_signal.connect(lambda x: values_received.append(x))
mock_pyepics_connection.lower_alarm_limit_signal.connect(lambda x: values_received.append(x))
mock_pyepics_connection.lower_warning_limit_signal.connect(lambda x: values_received.append(x))
@@ -18,8 +17,14 @@ def test_update_ctrl_vars(signals: ConnectionSignals):
mock_pyepics_connection.upper_ctrl_limit_signal.connect(lambda x: values_received.append(x))
mock_pyepics_connection.lower_ctrl_limit_signal.connect(lambda x: values_received.append(x))
- mock_pyepics_connection.update_ctrl_vars(upper_ctrl_limit=70, lower_ctrl_limit=20, upper_alarm_limit=100,
- lower_alarm_limit=2, upper_warning_limit=90, lower_warning_limit=10)
+ mock_pyepics_connection.update_ctrl_vars(
+ upper_ctrl_limit=70,
+ lower_ctrl_limit=20,
+ upper_alarm_limit=100,
+ lower_alarm_limit=2,
+ upper_warning_limit=90,
+ lower_warning_limit=10,
+ )
expected_values = [70, 20, 100, 2, 90, 10]
assert values_received == expected_values
diff --git a/pydm/tests/test_about_screen.py b/pydm/tests/test_about_screen.py
index a3d8c8e36e..9303f58408 100644
--- a/pydm/tests/test_about_screen.py
+++ b/pydm/tests/test_about_screen.py
@@ -1,6 +1,8 @@
from pydm.about_pydm import AboutWindow
+
+
def test_about_window_launches(qtbot):
"""Make sure the About window doesn't crash."""
a = AboutWindow(parent=None)
qtbot.addWidget(a)
- a.show()
\ No newline at end of file
+ a.show()
diff --git a/pydm/tests/test_data/no_display_test_file.py b/pydm/tests/test_data/no_display_test_file.py
index 08948ed3b1..eccff8e664 100644
--- a/pydm/tests/test_data/no_display_test_file.py
+++ b/pydm/tests/test_data/no_display_test_file.py
@@ -3,6 +3,7 @@
class InvalidDisplayExample(QObject):
- """ A simple class that inherits from QObject only """
+ """A simple class that inherits from QObject only"""
+
def __init__(self, parent=None):
super().__init__(parent=parent)
diff --git a/pydm/tests/test_data/valid_display_test_file.py b/pydm/tests/test_data/valid_display_test_file.py
index 3557ce47dd..6713feae68 100644
--- a/pydm/tests/test_data/valid_display_test_file.py
+++ b/pydm/tests/test_data/valid_display_test_file.py
@@ -2,27 +2,29 @@
import os
from pydm import Display
from pydm.widgets import PyDMPushButton, PyDMLabel
+
# Ensure loading of modules in the same directory works as expected when this file is loaded as a PyDM Display
import no_display_test_file
class DisplayExample(Display):
- """ An example of a simple display that can be loaded by `load_py_file` in `display.py` """
+ """An example of a simple display that can be loaded by `load_py_file` in `display.py`"""
+
def __init__(self, parent=None, args=None, macros=None):
super().__init__(parent=parent, args=args, macros=macros)
self.button = PyDMPushButton()
self.button.clicked.connect(self.delete_widget)
- self.label = PyDMLabel(init_channel='TST:Val1')
+ self.label = PyDMLabel(init_channel="TST:Val1")
def print_file(self):
- print(f'{no_display_test_file}')
+ print(f"{no_display_test_file}")
def delete_widget(self):
self.label.deleteLater()
def ui_filename(self):
- return 'test.ui'
+ return "test.ui"
def ui_filepath(self):
return os.path.join(os.path.dirname(os.path.realpath(__file__)), self.ui_filename())
diff --git a/pydm/tests/test_data_plugins_import.py b/pydm/tests/test_data_plugins_import.py
index 9fce26263f..5c8144d160 100644
--- a/pydm/tests/test_data_plugins_import.py
+++ b/pydm/tests/test_data_plugins_import.py
@@ -3,51 +3,53 @@
import entrypoints
from pydm import config
-from pydm.data_plugins import (PyDMPlugin, initialize_plugins_if_needed,
- load_plugins_from_entrypoints,
- load_plugins_from_path, plugin_for_address,
- plugin_modules)
+from pydm.data_plugins import (
+ PyDMPlugin,
+ initialize_plugins_if_needed,
+ load_plugins_from_entrypoints,
+ load_plugins_from_path,
+ plugin_for_address,
+ plugin_modules,
+)
def test_data_plugin_add(qapp, test_plugin):
# Check that adding this after import will be reflected in PyDMApp
initialize_plugins_if_needed()
- assert isinstance(plugin_modules['tst'], test_plugin)
- assert isinstance(qapp.plugins['tst'], test_plugin)
+ assert isinstance(plugin_modules["tst"], test_plugin)
+ assert isinstance(qapp.plugins["tst"], test_plugin)
def test_plugin_directory_loading(qapp, caplog):
# Create a fake file
cur_dir = os.getcwd()
- with open(os.path.join(cur_dir, 'plugin_foo.py'), 'w+') as handle:
+ with open(os.path.join(cur_dir, "plugin_foo.py"), "w+") as handle:
handle.write(fake_file)
handle.flush()
try:
# Load plugins. One of them will raise an exception during initialization. This will
# prevent it from being added to the available plugins, but the others should still load.
- load_plugins_from_path([cur_dir], 'foo.py')
- assert 'tst1' in plugin_modules
- assert 'tst2' in plugin_modules
- assert 'fail' not in plugin_modules
+ load_plugins_from_path([cur_dir], "foo.py")
+ assert "tst1" in plugin_modules
+ assert "tst2" in plugin_modules
+ assert "fail" not in plugin_modules
# Check for the error logged when a plugin fails to load. Confirms that the plugin that failed to load
# was the one expected to fail, and that there was only one failure total.
assert "FailingTestPlugin'> failed to load and will not be available for use" in caplog.text
assert caplog.text.count("failed to load and will not be available for use") == 1
finally:
- os.remove(os.path.join(cur_dir, 'plugin_foo.py'))
+ os.remove(os.path.join(cur_dir, "plugin_foo.py"))
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
+ assert isinstance(plugin_for_address("tst://tst:this"), test_plugin)
+ assert plugin_for_address("tst:this") is None
# Default protocol
- monkeypatch.setattr(config, 'DEFAULT_PROTOCOL', 'tst')
- assert isinstance(plugin_for_address('tst:this'),
- test_plugin)
+ monkeypatch.setattr(config, "DEFAULT_PROTOCOL", "tst")
+ assert isinstance(plugin_for_address("tst:this"), test_plugin)
fake_file = """\
diff --git a/pydm/tests/test_display.py b/pydm/tests/test_display.py
index 139eb5f2f2..036776c78e 100644
--- a/pydm/tests/test_display.py
+++ b/pydm/tests/test_display.py
@@ -5,23 +5,18 @@
from qtpy.QtWidgets import QLabel
# The path to the .ui file used in these tests
-test_ui_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "test_data", "test.ui")
+test_ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data", "test.ui")
-test_ui_with_macros_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "test_data", "macro_test.ui"
-)
+test_ui_with_macros_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data", "macro_test.ui")
# The path to the .py files used in these tests
no_display_test_py_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "test_data", "no_display_test_file.py")
+ os.path.dirname(os.path.realpath(__file__)), "test_data", "no_display_test_file.py"
+)
valid_display_test_py_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "test_data", "valid_display_test_file.py")
+ os.path.dirname(os.path.realpath(__file__)), "test_data", "valid_display_test_file.py"
+)
def test_ui_filename_arg(qtbot):
@@ -33,59 +28,61 @@ def test_ui_filename_arg(qtbot):
def test_reimplemented_ui_filename(qtbot):
"""If you reimplement ui_filename and return a valid filename, you
shouldn't get any exceptions."""
+
class TestDisplay(Display):
def ui_filename(self):
return test_ui_path
+
my_display = TestDisplay(parent=None)
qtbot.addWidget(my_display)
def test_nonexistant_ui_file_raises(qtbot):
with pytest.raises(IOError):
- my_display = Display(parent=None, ui_filename="this_doesnt_exist.ui")
+ Display(parent=None, ui_filename="this_doesnt_exist.ui")
class TestDisplay(Display):
def ui_filename(self):
return "this_doesnt_exist.ui"
with pytest.raises(IOError):
- my_display = TestDisplay(parent=None)
+ TestDisplay(parent=None)
def test_nonexistent_py_file_raises():
- """ Load a python file that does not exist and confirm the error raised is as expected """
+ """Load a python file that does not exist and confirm the error raised is as expected"""
with pytest.raises(FileNotFoundError):
- load_py_file('this_doesnt_exist.py')
+ load_py_file("this_doesnt_exist.py")
def test_doesnt_inherit_display_raises():
- """ Load a python file that does not inherit from PyDM Display and confirm the error raised is as expected """
+ """Load a python file that does not inherit from PyDM Display and confirm the error raised is as expected"""
with pytest.raises(ValueError) as error_info:
load_py_file(no_display_test_py_path)
- assert 'no class inheriting from Display' in str(error_info.value)
+ assert "no class inheriting from Display" in str(error_info.value)
def test_load_valid_python_display_file(qtbot):
- """ Verify that loading a valid python only file inheriting from Display works as expected """
+ """Verify that loading a valid python only file inheriting from Display works as expected"""
display = load_py_file(valid_display_test_py_path)
qtbot.addWidget(display)
# Confirm that the file loaded everything as expected
assert display.loaded_file() == valid_display_test_py_path
- assert display.ui_filename() == 'test.ui'
+ assert display.ui_filename() == "test.ui"
assert display.macros() == {}
assert display.previous_display is None
assert display.next_display is None
def test_load_python_file_with_macros(qtbot):
- """ Attempt to add macros to the display while loading the file """
- macros = {'MACRO_1': 7, 'MACRO_2': 'test_string'}
+ """Attempt to add macros to the display while loading the file"""
+ macros = {"MACRO_1": 7, "MACRO_2": "test_string"}
display = load_py_file(valid_display_test_py_path, macros=macros)
qtbot.addWidget(display)
assert display.loaded_file() == valid_display_test_py_path
- assert display.ui_filename() == 'test.ui'
- assert display.macros() == {'MACRO_1': 7, 'MACRO_2': 'test_string'}
+ assert display.ui_filename() == "test.ui"
+ assert display.macros() == {"MACRO_1": 7, "MACRO_2": "test_string"}
def test_file_path_in_stylesheet_property(qtbot):
@@ -115,9 +112,9 @@ def test_compile_ui_file():
and the expected methods are added
"""
code_string, class_name = _compile_ui_file(test_ui_path)
- assert class_name == 'Ui_Form'
- assert 'setupUi(self' in code_string
- assert 'retranslateUi(self' in code_string
+ assert class_name == "Ui_Form"
+ assert "setupUi(self" in code_string
+ assert "retranslateUi(self" in code_string
def test_load_file_with_macros(qtbot):
@@ -131,18 +128,21 @@ def test_load_file_with_macros(qtbot):
commands_from_macro = []
def setCommands(self, commands):
- """ Store what the commands were after macro parsing """
+ """Store what the commands were after macro parsing"""
nonlocal commands_from_macro
commands_from_macro = commands
+
QLabel.setCommands = setCommands
# Compile the ui file into python code
- macros = {"test_label": "magnet_list",
- "test_command": "grep -i 'string with spaces'",
- "test_command_2": "echo hello",
- "channel_with_dec_option": '.{"dec":{"n": 25}}'}
+ macros = {
+ "test_label": "magnet_list",
+ "test_command": "grep -i 'string with spaces'",
+ "test_command_2": "echo hello",
+ "channel_with_dec_option": '.{"dec":{"n": 25}}',
+ }
code_string, class_name = _compile_ui_file(test_ui_with_macros_path)
- assert class_name == 'Ui_Form'
+ assert class_name == "Ui_Form"
# Parse and replace macros, then load into the display
test_display = Display(macros=macros)
@@ -165,4 +165,6 @@ def test_load_file_with_help_display(qtbot):
"""
test_display = load_file(test_ui_path, target=ScreenTarget.HOME)
assert test_display.help_window is not None
- assert test_display.help_window.display_content.toPlainText() == 'This is a test help file for the test.ui display\n'
+ assert (
+ test_display.help_window.display_content.toPlainText() == "This is a test help file for the test.ui display\n"
+ )
diff --git a/pydm/tests/test_plugins_import.py b/pydm/tests/test_plugins_import.py
index 45df3bcbc7..3dfaeaa105 100644
--- a/pydm/tests/test_plugins_import.py
+++ b/pydm/tests/test_plugins_import.py
@@ -1,136 +1,184 @@
from ..widgets.qtplugin_base import qtplugin_factory
+
def test_import_byte_plugin():
# Byte plugin
from ..widgets.byte import PyDMByteIndicator
- PyDMByteIndicatorPlugin = qtplugin_factory(PyDMByteIndicator)
+
+ qtplugin_factory(PyDMByteIndicator)
+
def test_import_checkbox_plugin():
# Checkbox plugin
from ..widgets.checkbox import PyDMCheckbox
- PyDMCheckboxPlugin = qtplugin_factory(PyDMCheckbox)
+
+ qtplugin_factory(PyDMCheckbox)
+
def test_import_drawing_plugins():
# Drawing plugins
- from ..widgets.drawing import (PyDMDrawingLine, PyDMDrawingRectangle, PyDMDrawingTriangle,
- PyDMDrawingEllipse, PyDMDrawingCircle, PyDMDrawingArc,
- PyDMDrawingPie, PyDMDrawingChord, PyDMDrawingImage,
- PyDMDrawingPolygon, PyDMDrawingPolyline,
- PyDMDrawingIrregularPolygon)
-
- PyDMDrawingImagePlugin = qtplugin_factory(PyDMDrawingImage)
- PyDMDrawingLinePlugin = qtplugin_factory(PyDMDrawingLine)
- PyDMDrawingRectanglePlugin = qtplugin_factory(PyDMDrawingRectangle)
- PyDMDrawingTrianglePlugin = qtplugin_factory(PyDMDrawingTriangle)
- PyDMDrawingEllipsePlugin = qtplugin_factory(PyDMDrawingEllipse)
- PyDMDrawingCirclePlugin = qtplugin_factory(PyDMDrawingCircle)
- PyDMDrawingArcPlugin = qtplugin_factory(PyDMDrawingArc)
- PyDMDrawingPiePlugin = qtplugin_factory(PyDMDrawingPie)
- PyDMDrawingChordPlugin = qtplugin_factory(PyDMDrawingChord)
- PyDMDrawingPolygonPlugin = qtplugin_factory(PyDMDrawingPolygon)
- PyDMDrawingPolylinePlugin = qtplugin_factory(PyDMDrawingPolyline)
- PyDMDrawingIrregularPolygonPlugin = qtplugin_factory(PyDMDrawingIrregularPolygon)
+ from ..widgets.drawing import (
+ PyDMDrawingLine,
+ PyDMDrawingRectangle,
+ PyDMDrawingTriangle,
+ PyDMDrawingEllipse,
+ PyDMDrawingCircle,
+ PyDMDrawingArc,
+ PyDMDrawingPie,
+ PyDMDrawingChord,
+ PyDMDrawingImage,
+ PyDMDrawingPolygon,
+ PyDMDrawingPolyline,
+ PyDMDrawingIrregularPolygon,
+ )
+
+ qtplugin_factory(PyDMDrawingImage)
+ qtplugin_factory(PyDMDrawingLine)
+ qtplugin_factory(PyDMDrawingRectangle)
+ qtplugin_factory(PyDMDrawingTriangle)
+ qtplugin_factory(PyDMDrawingEllipse)
+ qtplugin_factory(PyDMDrawingCircle)
+ qtplugin_factory(PyDMDrawingArc)
+ qtplugin_factory(PyDMDrawingPie)
+ qtplugin_factory(PyDMDrawingChord)
+ qtplugin_factory(PyDMDrawingPolygon)
+ qtplugin_factory(PyDMDrawingPolyline)
+ qtplugin_factory(PyDMDrawingIrregularPolygon)
+
def test_import_embedded_display_plugin():
# Embedded Display plugin
from ..widgets.embedded_display import PyDMEmbeddedDisplay
- PyDMEmbeddedDisplayPlugin = qtplugin_factory(PyDMEmbeddedDisplay)
+
+ qtplugin_factory(PyDMEmbeddedDisplay)
+
def test_import_frame_plugin():
# Frame plugin
from ..widgets.frame import PyDMFrame
- PyDMFramePlugin = qtplugin_factory(PyDMFrame, is_container=True)
+
+ qtplugin_factory(PyDMFrame, is_container=True)
+
def test_import_enum_button_plugin():
# Enum Button plugin
from ..widgets.enum_button import PyDMEnumButton
- PyDMEnumButtonPlugin = qtplugin_factory(PyDMEnumButton)
+
+ qtplugin_factory(PyDMEnumButton)
+
def test_import_combobox_plugin():
# Enum Combobox plugin
from ..widgets.enum_combo_box import PyDMEnumComboBox
- PyDMEnumComboBoxPlugin = qtplugin_factory(PyDMEnumComboBox)
+
+ qtplugin_factory(PyDMEnumComboBox)
+
def test_import_image_plugin():
# Image plugin
from ..widgets.image import PyDMImageView
- PyDMImageViewPlugin = qtplugin_factory(PyDMImageView)
+
+ qtplugin_factory(PyDMImageView)
+
def test_import_label_plugin():
# Label plugin
from ..widgets.label import PyDMLabel
- PyDMLabelPlugin = qtplugin_factory(PyDMLabel)
+
+ qtplugin_factory(PyDMLabel)
+
def test_import_line_edit_plugin():
# Line Edit plugin
from ..widgets.line_edit import PyDMLineEdit
- PyDMLineEditPlugin = qtplugin_factory(PyDMLineEdit)
+
+ qtplugin_factory(PyDMLineEdit)
+
def test_import_pushbutton_plugin():
# Push Button plugin
from ..widgets.pushbutton import PyDMPushButton
- PyDMPushButtonPlugin = qtplugin_factory(PyDMPushButton)
+
+ qtplugin_factory(PyDMPushButton)
+
def test_import_related_display_plugin():
# Related Display Button plugin
from ..widgets.related_display_button import PyDMRelatedDisplayButton
- PyDMRelatedDisplayButtonPlugin = qtplugin_factory(PyDMRelatedDisplayButton)
+
+ qtplugin_factory(PyDMRelatedDisplayButton)
+
def test_import_scale_indicator_plugin():
# Scale Indicator plugin
from ..widgets.scale import PyDMScaleIndicator
- PyDMScaleIndicatorPlugin = qtplugin_factory(PyDMScaleIndicator)
+
+ qtplugin_factory(PyDMScaleIndicator)
+
def test_import_shellcmd_plugin():
# Shell Command plugin
from ..widgets.shell_command import PyDMShellCommand
- PyDMShellCommandPlugin = qtplugin_factory(PyDMShellCommand)
+
+ qtplugin_factory(PyDMShellCommand)
+
def test_import_slider_plugin():
# Slider plugin
from ..widgets.slider import PyDMSlider
- PyDMSliderPlugin = qtplugin_factory(PyDMSlider)
+
+ qtplugin_factory(PyDMSlider)
+
def test_import_spinbox_plugin():
# Spinbox plugin
from ..widgets.spinbox import PyDMSpinbox
- PyDMSpinboxplugin = qtplugin_factory(PyDMSpinbox)
+
+ qtplugin_factory(PyDMSpinbox)
+
def test_import_symbol_plugin():
# Symbol plugin
from ..widgets.symbol import PyDMSymbol
- PyDMSymbolPlugin = qtplugin_factory(PyDMSymbol)
+
+ qtplugin_factory(PyDMSymbol)
+
def test_import_waveform_table_plugin():
# Waveform Table plugin
from ..widgets.waveformtable import PyDMWaveformTable
- PyDMWaveformTablePlugin = qtplugin_factory(PyDMWaveformTable)
+
+ qtplugin_factory(PyDMWaveformTable)
+
def test_import_timeplot_plugin():
# Time Plot plugin
from ..widgets.timeplot import PyDMTimePlot
from ..widgets.timeplot_curve_editor import TimePlotCurveEditorDialog
+
# Time Plot plugin
- PyDMTimePlotPlugin = qtplugin_factory(
- PyDMTimePlot, TimePlotCurveEditorDialog)
+ qtplugin_factory(PyDMTimePlot, TimePlotCurveEditorDialog)
+
def test_import_waveformplot_plugin():
# Time Plot plugin
from ..widgets.waveformplot import PyDMWaveformPlot
from ..widgets.waveformplot_curve_editor import WaveformPlotCurveEditorDialog
+
# Waveform Plot plugin
- PyDMWaveformPlotPlugin = qtplugin_factory(
- PyDMWaveformPlot, WaveformPlotCurveEditorDialog)
+ qtplugin_factory(PyDMWaveformPlot, WaveformPlotCurveEditorDialog)
+
def test_import_scatterplot_plugin():
from ..widgets.scatterplot import PyDMScatterPlot
from ..widgets.scatterplot_curve_editor import ScatterPlotCurveEditorDialog
+
# Scatter Plot plugin
- PyDMScatterPlotPlugin = qtplugin_factory(
- PyDMScatterPlot, ScatterPlotCurveEditorDialog)
-
+ qtplugin_factory(PyDMScatterPlot, ScatterPlotCurveEditorDialog)
+
+
def test_import_tab_widget_plugin():
# Symbol plugin
- from ..widgets.tab_bar import PyDMTabWidget
from ..widgets.tab_bar_qtplugin import TabWidgetPlugin
- PyDMTabWidgetPlugin = TabWidgetPlugin()
+
+ TabWidgetPlugin()
diff --git a/pydm/tests/test_tools.py b/pydm/tests/test_tools.py
index 17e8c7e55e..e5d7f48283 100644
--- a/pydm/tests/test_tools.py
+++ b/pydm/tests/test_tools.py
@@ -14,8 +14,7 @@
EXAMPLE_EXT_TOOL_PATH = EXAMPLE_PATH / "external_tool"
examples_required = pytest.mark.skipif(
- not EXAMPLE_PATH.exists(),
- reason="Not a source checkout - no examples available"
+ not EXAMPLE_PATH.exists(), reason="Not a source checkout - no examples available"
)
@@ -33,7 +32,7 @@ class InvalidTool:
pytest.param(ValidTool, True, id="valid"),
pytest.param(InvalidTool, False, id="invalid"),
pytest.param(None, False, id="invalid-none"),
- ]
+ ],
)
def test_valid_external_tool(cls: Any, valid: bool):
assert tools._is_valid_external_tool_class(cls) is valid
@@ -58,18 +57,11 @@ def test_valid_external_tool(cls: Any, valid: bool):
{"RootTool"},
id="root_tool",
),
- ]
+ ],
)
-def test_tools_from_source(
- source_file: pathlib.Path,
- expected_tools: Set[str],
- qapp: PyDMApplication
-):
+def test_tools_from_source(source_file: pathlib.Path, expected_tools: Set[str], qapp: PyDMApplication):
assert source_file.exists()
- loaded_tools = [
- tool.__class__.__name__
- for tool in tools._get_tools_from_source(str(source_file))
- ]
+ loaded_tools = [tool.__class__.__name__ for tool in tools._get_tools_from_source(str(source_file))]
assert set(loaded_tools) == expected_tools
diff --git a/pydm/tests/utilities/test_colors.py b/pydm/tests/utilities/test_colors.py
index 45b288d876..d5fa83bfab 100644
--- a/pydm/tests/utilities/test_colors.py
+++ b/pydm/tests/utilities/test_colors.py
@@ -4,25 +4,25 @@
def test_read_file():
- assert(colors.svg_color_to_hex_map is not None)
- assert (colors.hex_to_svg_color_map is not None)
+ assert colors.svg_color_to_hex_map is not None
+ assert colors.hex_to_svg_color_map is not None
def test_svg_color_from_hex():
- svg = colors.svg_color_from_hex('#000000', hex_on_fail=False)
- assert (svg == 'black')
- svg = colors.svg_color_from_hex('#000000', hex_on_fail=True)
- assert (svg == 'black')
+ svg = colors.svg_color_from_hex("#000000", hex_on_fail=False)
+ assert svg == "black"
+ svg = colors.svg_color_from_hex("#000000", hex_on_fail=True)
+ assert svg == "black"
with pytest.raises(KeyError):
- colors.svg_color_from_hex('#XXXXXXX', hex_on_fail=False)
- svg = colors.svg_color_from_hex('#XXXXXXX', hex_on_fail=True)
- assert(svg == '#XXXXXXX')
+ colors.svg_color_from_hex("#XXXXXXX", hex_on_fail=False)
+ svg = colors.svg_color_from_hex("#XXXXXXX", hex_on_fail=True)
+ assert svg == "#XXXXXXX"
def test_hex_from_svg_color():
- hex = colors.hex_from_svg_color('black')
- assert (hex == '#000000')
+ hex = colors.hex_from_svg_color("black")
+ assert hex == "#000000"
with pytest.raises(KeyError):
- colors.hex_from_svg_color('invalid_color')
+ colors.hex_from_svg_color("invalid_color")
diff --git a/pydm/tests/utilities/test_iconfont.py b/pydm/tests/utilities/test_iconfont.py
index 46f1519cab..58289b3fda 100644
--- a/pydm/tests/utilities/test_iconfont.py
+++ b/pydm/tests/utilities/test_iconfont.py
@@ -6,46 +6,46 @@
def test_icon_font_constructor(qtbot):
icon_f = iconfont.IconFont()
icon_f2 = iconfont.IconFont()
- assert (icon_f is icon_f2)
+ assert icon_f is icon_f2
def test_icon_font_load_font(qtbot):
icon_f = iconfont.IconFont()
with pytest.raises(OSError):
icon_f.char_map = None
- icon_f.load_font('foo', icon_f.charmap_file)
+ icon_f.load_font("foo", icon_f.charmap_file)
with pytest.raises(OSError):
icon_f.char_map = None
- icon_f.load_font(icon_f.charmap_file, 'foo')
+ icon_f.load_font(icon_f.charmap_file, "foo")
icon_f.load_font(icon_f.font_file, icon_f.charmap_file)
- assert (icon_f.char_map is not None)
+ assert icon_f.char_map is not None
def test_icon_font_get_char_for_name(qtbot):
icon_f = iconfont.IconFont()
- c = icon_f.get_char_for_name('cogs')
- assert (c == u'\uf085')
+ c = icon_f.get_char_for_name("cogs")
+ assert c == "\uf085"
with pytest.raises(ValueError):
- icon_f.get_char_for_name('foo')
+ icon_f.get_char_for_name("foo")
def test_icon_font_font(qtbot):
icon_f = iconfont.IconFont()
f = icon_f.font(12)
- assert(f.family() == icon_f.font_name)
- assert(f.pixelSize() == 12)
+ assert f.family() == icon_f.font_name
+ assert f.pixelSize() == 12
def test_icon_font_icon(qtbot):
icon_f = iconfont.IconFont()
- ico = icon_f.icon('cogs', color=None)
- ico1 = icon_f.icon('cogs', color=QtGui.QColor(255, 0, 0))
+ icon_f.icon("cogs", color=None)
+ icon_f.icon("cogs", color=QtGui.QColor(255, 0, 0))
with pytest.raises(ValueError):
- ico_invalid = icon_f.icon('foo', color=None)
+ icon_f.icon("foo", color=None)
def test_char_icon_engine(qtbot):
- engine = iconfont.CharIconEngine(iconfont.IconFont(), 'cogs', color=None)
- pm = engine.pixmap(QtCore.QSize(32, 32), mode=QtGui.QIcon.Normal, state=QtGui.QIcon.On)
- pm = engine.pixmap(QtCore.QSize(32, 32), mode=QtGui.QIcon.Disabled, state=QtGui.QIcon.On)
\ No newline at end of file
+ engine = iconfont.CharIconEngine(iconfont.IconFont(), "cogs", color=None)
+ engine.pixmap(QtCore.QSize(32, 32), mode=QtGui.QIcon.Normal, state=QtGui.QIcon.On)
+ engine.pixmap(QtCore.QSize(32, 32), mode=QtGui.QIcon.Disabled, state=QtGui.QIcon.On)
diff --git a/pydm/tests/utilities/test_macro.py b/pydm/tests/utilities/test_macro.py
index f7c16e839c..627aefa441 100644
--- a/pydm/tests/utilities/test_macro.py
+++ b/pydm/tests/utilities/test_macro.py
@@ -1,55 +1,61 @@
import os
import tempfile
import pytest
-import json
from ...utilities.macro import substitute_in_file, parse_macro_string
-@pytest.mark.parametrize("text, macros, expected", [
- (
- 'This is a ${what} to ensure that macros work. ${A} ${B} ${C}',
- {'what': 'test', 'A': 'X', 'B': 'Y', 'C': 'Z'},
- 'This is a test to ensure that macros work. X Y Z'
- ),
- (
- 'This is a ${what} to ensure that macros work. ${A} ${B} ${C}',
- {'what': 'test', 'A': 'X', 'B': 'Y', 'C': 'Z', 'D': 'W'},
- 'This is a test to ensure that macros work. X Y Z'
- ),
- (
- 'This is a ${what} to ensure that macros work. ${A} ${B} ${C}',
- {'what': 'test', 'A': 'X', 'B': 'Y'},
- 'This is a test to ensure that macros work. X Y ${C}'
- )
-])
+@pytest.mark.parametrize(
+ "text, macros, expected",
+ [
+ (
+ "This is a ${what} to ensure that macros work. ${A} ${B} ${C}",
+ {"what": "test", "A": "X", "B": "Y", "C": "Z"},
+ "This is a test to ensure that macros work. X Y Z",
+ ),
+ (
+ "This is a ${what} to ensure that macros work. ${A} ${B} ${C}",
+ {"what": "test", "A": "X", "B": "Y", "C": "Z", "D": "W"},
+ "This is a test to ensure that macros work. X Y Z",
+ ),
+ (
+ "This is a ${what} to ensure that macros work. ${A} ${B} ${C}",
+ {"what": "test", "A": "X", "B": "Y"},
+ "This is a test to ensure that macros work. X Y ${C}",
+ ),
+ ],
+)
def test_substitute_in_file(text, macros, expected):
fd, name = tempfile.mkstemp(text=True)
# use a context manager to open the file at that path and close it again
- with open(name, 'w') as f:
+ with open(name, "w") as f:
f.write(text)
# close the file descriptor
os.close(fd)
nf = substitute_in_file(name, macros)
nt = nf.read()
- assert (nt == expected)
+ assert nt == expected
-@pytest.mark.parametrize("macro_string, expected_dict", [
- ('{"A": "1", "B": "2"}', {"A": "1", "B": "2"}),
- ("A=1,B=2", {"A": "1", "B": "2"}),
- ("A=$(other_macro),B=2,C=3", {"A": "$(other_macro)", "B": "2", "C": "3"}),
- ("A=$(other_macro=3)", {"A": "$(other_macro=3)"}),
- ("TITLE='1,2', B=2, C=3", {"TITLE": "1,2", "B": "2", "C": "3"}),
- ("TITLE=1\,2,B=2,C=3", {"TITLE": "1,2", "B": "2", "C": "3"}),
- ('TITLE="e=mc^2",B=2,C=3', {"TITLE": "e=mc^2", "B": "2", "C": "3"}),
- ('', {}),
- (None, {})
-])
+
+@pytest.mark.parametrize(
+ "macro_string, expected_dict",
+ [
+ ('{"A": "1", "B": "2"}', {"A": "1", "B": "2"}),
+ ("A=1,B=2", {"A": "1", "B": "2"}),
+ ("A=$(other_macro),B=2,C=3", {"A": "$(other_macro)", "B": "2", "C": "3"}),
+ ("A=$(other_macro=3)", {"A": "$(other_macro=3)"}),
+ ("TITLE='1,2', B=2, C=3", {"TITLE": "1,2", "B": "2", "C": "3"}),
+ ("TITLE=1\,2,B=2,C=3", {"TITLE": "1,2", "B": "2", "C": "3"}),
+ ('TITLE="e=mc^2",B=2,C=3', {"TITLE": "e=mc^2", "B": "2", "C": "3"}),
+ ("", {}),
+ (None, {}),
+ ],
+)
def test_macro_parser(macro_string, expected_dict):
"""
Test the parser, using a couple of normal cases, and a bunch of perverse
- macro strings that only some insane macro genius (or huge macro idiot)
+ macro strings that only some insane macro genius (or huge macro idiot)
would ever attempt.
"""
assert parse_macro_string(macro_string) == expected_dict
diff --git a/pydm/tests/utilities/test_remove_protocol.py b/pydm/tests/utilities/test_remove_protocol.py
index 4a03b8deb0..bd0c4003ef 100644
--- a/pydm/tests/utilities/test_remove_protocol.py
+++ b/pydm/tests/utilities/test_remove_protocol.py
@@ -2,31 +2,32 @@
from ...utilities.remove_protocol import protocol_and_address
from ...utilities.remove_protocol import parsed_address
+
def test_remove_protocol():
- out = remove_protocol('foo://bar')
- assert (out == 'bar')
+ out = remove_protocol("foo://bar")
+ assert out == "bar"
+
+ out = remove_protocol("bar")
+ assert out == "bar"
- out = remove_protocol('bar')
- assert (out == 'bar')
+ out = remove_protocol("foo://bar://foo2")
+ assert out == "bar://foo2"
- 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 == ("foo", "bar")
+
+ out = protocol_and_address("foo:/bar")
+ assert out == (None, "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', '', '', '', ''))
+ assert out is None
+ out = parsed_address("foo:/bar")
+ assert out is None
+ out = parsed_address("foo://bar")
+ assert out == ("foo", "bar", "", "", "", "")
diff --git a/pydm/tests/utilities/test_stylesheet.py b/pydm/tests/utilities/test_stylesheet.py
index 56fb196c7c..e877f77264 100644
--- a/pydm/tests/utilities/test_stylesheet.py
+++ b/pydm/tests/utilities/test_stylesheet.py
@@ -8,11 +8,11 @@
# The path to the stylesheet used in these unit tests
test_stylesheet_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "..", "test_data", "global_stylesheet.css")
+ os.path.dirname(os.path.realpath(__file__)), "..", "test_data", "global_stylesheet.css"
+)
-@pytest.fixture(scope='function')
+@pytest.fixture(scope="function")
def save_and_restore_pydm_stylesheet():
"""
A fixture for ensuring that modifications to the PYDM_STYLESHEET environment variable are restored
diff --git a/pydm/tests/utilities/test_units.py b/pydm/tests/utilities/test_units.py
index 4d618dbca4..a4c154cda5 100644
--- a/pydm/tests/utilities/test_units.py
+++ b/pydm/tests/utilities/test_units.py
@@ -3,38 +3,27 @@
from ...utilities import units
-@pytest.mark.parametrize("typ, expected", [
- ('cm', 'length'),
- ('non_existent', None)
-])
+@pytest.mark.parametrize("typ, expected", [("cm", "length"), ("non_existent", None)])
def test_find_unittype(typ, expected):
tp = units.find_unittype(typ)
- assert (tp == expected)
+ assert tp == expected
-@pytest.mark.parametrize("unit, expected", [
- ('cm', constants.centi),
- ('non_existent', None)
-])
+@pytest.mark.parametrize("unit, expected", [("cm", constants.centi), ("non_existent", None)])
def test_find_unit(unit, expected):
r = units.find_unit(unit)
- assert (r == expected)
+ assert r == expected
-@pytest.mark.parametrize("unit, desired, expected", [
- ('m', 'cm', 1/constants.centi),
- ('m', 'rad', None),
- ('non_existent', 'rad', None)
-])
+@pytest.mark.parametrize(
+ "unit, desired, expected", [("m", "cm", 1 / constants.centi), ("m", "rad", None), ("non_existent", "rad", None)]
+)
def test_convert(unit, desired, expected):
r = units.convert(unit, desired)
- assert (r == expected)
+ assert r == expected
-@pytest.mark.parametrize("unit, expected", [
- ('V', ['MV', 'kV', 'V', 'mV', 'uV']),
- ('foo', None)
-])
+@pytest.mark.parametrize("unit, expected", [("V", ["MV", "kV", "V", "mV", "uV"]), ("foo", None)])
def test_find_unit_options(unit, expected):
opts = units.find_unit_options(unit)
- assert (opts == expected)
+ assert opts == expected
diff --git a/pydm/tests/utilities/test_utilities.py b/pydm/tests/utilities/test_utilities.py
index 76b582e04d..ed91b628c4 100644
--- a/pydm/tests/utilities/test_utilities.py
+++ b/pydm/tests/utilities/test_utilities.py
@@ -5,8 +5,7 @@
from qtpy import QtWidgets
-from ...utilities import (find_display_in_path, is_pydm_app, is_qt_designer,
- log_failures, path_info, which)
+from ...utilities import find_display_in_path, is_pydm_app, is_qt_designer, log_failures, path_info, which
logger = logging.getLogger(__name__)
@@ -24,55 +23,55 @@ def test_is_qt_designer():
def test_path_info():
- dir_name, file_name, args = path_info('/bin/ls -l -a')
- assert (dir_name == '/bin')
- assert (file_name == 'ls')
- assert (args == ['-l', '-a'])
+ dir_name, file_name, args = path_info("/bin/ls -l -a")
+ assert dir_name == "/bin"
+ assert file_name == "ls"
+ assert args == ["-l", "-a"]
- dir_name, file_name, args = path_info('/bin/ls')
- assert (dir_name == '/bin')
- assert (file_name == 'ls')
- assert (args == [])
+ dir_name, file_name, args = path_info("/bin/ls")
+ assert dir_name == "/bin"
+ assert file_name == "ls"
+ assert args == []
def test_find_display_in_path():
temp, file_path = tempfile.mkstemp(suffix=".ui", prefix="display_")
direc, fname, _ = path_info(file_path)
# Try to find the file as is... is should not find it.
- assert(find_display_in_path(fname) is None)
+ assert find_display_in_path(fname) is None
# Try to find the file passing the path
disp_path = find_display_in_path(fname, mode=None, path=direc)
- assert(disp_path == file_path)
+ assert disp_path == file_path
# Try to find the file passing the path but relative name
rel_name = ".{}{}".format(os.sep, fname)
expected = "{}{}{}".format(direc, os.sep, rel_name)
disp_path = find_display_in_path(rel_name, mode=None, path=direc)
- assert (disp_path == expected)
+ assert disp_path == expected
def test_which():
- if platform.system() == 'Windows':
- out = which('ping')
- assert out.lower() == r'c:\windows\system32\ping.exe'
+ if platform.system() == "Windows":
+ out = which("ping")
+ assert out.lower() == r"c:\windows\system32\ping.exe"
- out = which(r'C:\Windows\System32\PING.EXE')
- assert out.lower() == r'c:\windows\system32\ping.exe'
+ out = which(r"C:\Windows\System32\PING.EXE")
+ assert out.lower() == r"c:\windows\system32\ping.exe"
else:
- out = which('ls')
- assert 'ls' in out
+ out = which("ls")
+ assert "ls" in out
- out = which('/bin/ls')
- assert 'ls' in out
+ out = which("/bin/ls")
+ assert "ls" in out
- out = which('non_existant_binary')
+ out = which("non_existant_binary")
assert out is None
- out = which('/bin/non_existant_binary')
+ out = which("/bin/non_existant_binary")
assert out is None
- out = which('ls', path='')
+ out = which("ls", path="")
assert out is None
diff --git a/pydm/tests/widgets/test_archiver_timeplot.py b/pydm/tests/widgets/test_archiver_timeplot.py
index f353340747..a5c29d38a9 100644
--- a/pydm/tests/widgets/test_archiver_timeplot.py
+++ b/pydm/tests/widgets/test_archiver_timeplot.py
@@ -6,24 +6,23 @@
from ...widgets.archiver_time_plot import ArchivePlotCurveItem, PyDMArchiverTimePlot
-@pytest.mark.parametrize('address', ['ca://LINAC:PV1', 'pva://LINAC:PV1', 'LINAC:PV1'])
+@pytest.mark.parametrize("address", ["ca://LINAC:PV1", "pva://LINAC:PV1", "LINAC:PV1"])
def test_set_archive_channel(address):
- """ Verify the address for the archiver data plugin is set correctly for all possible EPICS address prefixes """
+ """Verify the address for the archiver data plugin is set correctly for all possible EPICS address prefixes"""
curve_item = ArchivePlotCurveItem(channel_address=address)
- assert curve_item.archive_channel.address == 'archiver://pv=LINAC:PV1'
+ assert curve_item.archive_channel.address == "archiver://pv=LINAC:PV1"
def test_receive_archive_data(signals: ConnectionSignals):
- """ Ensure data from archiver appliance is inserted into the archive buffer correctly """
+ """Ensure data from archiver appliance is inserted into the archive buffer correctly"""
curve_item = ArchivePlotCurveItem()
curve_item.setBufferSize(20)
curve_item.setArchiveBufferSize(20)
# We start with a data buffer with some sample values like those generated by a running time plot
# The x-values are timestamps and the y-values are samples from a PV
- example_live_data = np.array([[100, 101, 102, 103, 104, 105],
- [2.15, 2.20, 2.25, 2.22, 2.20, 2.18]])
- starting_data = np.concatenate((np.zeros((2, curve_item.getBufferSize()-6)), example_live_data), axis=1)
+ example_live_data = np.array([[100, 101, 102, 103, 104, 105], [2.15, 2.20, 2.25, 2.22, 2.20, 2.18]])
+ starting_data = np.concatenate((np.zeros((2, curve_item.getBufferSize() - 6)), example_live_data), axis=1)
curve_item.points_accumulated += 6
curve_item.data_buffer = starting_data
@@ -33,8 +32,7 @@ def test_receive_archive_data(signals: ConnectionSignals):
signals.new_value_signal[np.ndarray].connect(curve_item.receiveArchiveData)
# First test the most basic case, where we've requested a bit of archive data right before the live data
- mock_archive_data = np.array([[70, 75, 80, 85, 90, 95],
- [2.05, 2.08, 2.07, 2.08, 2.12, 2.14]])
+ mock_archive_data = np.array([[70, 75, 80, 85, 90, 95], [2.05, 2.08, 2.07, 2.08, 2.12, 2.14]])
signals.new_value_signal[np.ndarray].emit(mock_archive_data)
expected_data = np.zeros((2, 14))
@@ -45,9 +43,9 @@ def test_receive_archive_data(signals: ConnectionSignals):
def test_insert_archive_data():
- """ When first receiving large amounts of data from the archiver appliance, it will be of the 'optimized' form
- in which it is sampled across a fixed number of bins. Drawing a zoom box in this data will get more detailed
- data which must be inserted into the archive data buffer. This tests that insertion is successful. """
+ """When first receiving large amounts of data from the archiver appliance, it will be of the 'optimized' form
+ in which it is sampled across a fixed number of bins. Drawing a zoom box in this data will get more detailed
+ data which must be inserted into the archive data buffer. This tests that insertion is successful."""
curve_item = ArchivePlotCurveItem()
curve_item.setBufferSize(10)
curve_item.setArchiveBufferSize(10)
@@ -57,29 +55,27 @@ def test_insert_archive_data():
curve_item.points_accumulated = 2
# Set up a sample archive buffer
- curve_item.archive_data_buffer = np.array([[0, 0, 0, 0, 100, 105, 110, 115, 120, 125],
- [0, 0, 0, 0, 2, 3, 4, 5, 6, 7]],
- dtype=float)
+ curve_item.archive_data_buffer = np.array(
+ [[0, 0, 0, 0, 100, 105, 110, 115, 120, 125], [0, 0, 0, 0, 2, 3, 4, 5, 6, 7]], dtype=float
+ )
curve_item.archive_points_accumulated = 6
curve_item.zoomed = True
# Receive raw data that is more detailed than the two values it will be replacing
- mock_archive_data = np.array([[104, 106, 108, 111],
- [2.8, 3.1, 3.7, 3.95]])
+ mock_archive_data = np.array([[104, 106, 108, 111], [2.8, 3.1, 3.7, 3.95]])
curve_item.insert_archive_data(mock_archive_data)
# The original average values for timestamps 105 and 106 should now be replace with the actual PV data
- expected_data = np.array([[0, 0, 100, 104, 106, 108, 111, 115, 120, 125],
- [0, 0, 2, 2.8, 3.1, 3.7, 3.95, 5, 6, 7]])
+ expected_data = np.array([[0, 0, 100, 104, 106, 108, 111, 115, 120, 125], [0, 0, 2, 2.8, 3.1, 3.7, 3.95, 5, 6, 7]])
assert np.array_equal(curve_item.archive_data_buffer, expected_data)
def test_archive_buffer_full():
- """ If we insert more data points than the archive buffer can hold, then the oldest points are
- removed in favor of the new ones until the user requests further backfill data again """
+ """If we insert more data points than the archive buffer can hold, then the oldest points are
+ removed in favor of the new ones until the user requests further backfill data again"""
curve_item = ArchivePlotCurveItem()
curve_item.setBufferSize(6)
curve_item.setArchiveBufferSize(6)
@@ -87,35 +83,31 @@ def test_archive_buffer_full():
curve_item.points_accumulated = 2
# Set up a sample archive buffer that is already full
- curve_item.archive_data_buffer = np.array([[100, 105, 110, 115, 120, 125],
- [2, 3, 4, 5, 6, 7]],
- dtype=float)
+ curve_item.archive_data_buffer = np.array([[100, 105, 110, 115, 120, 125], [2, 3, 4, 5, 6, 7]], dtype=float)
curve_item.archive_points_accumulated = 6
curve_item.zoomed = True
# Receive data that will cause that will not fit in the buffer without deleting other data points
- mock_archive_data = np.array([[104, 106, 108],
- [2.8, 3.1, 3.7]])
+ mock_archive_data = np.array([[104, 106, 108], [2.8, 3.1, 3.7]])
curve_item.insert_archive_data(mock_archive_data)
# This is what is left over after the oldest data points have been trimmed
- expected_data = np.array([[104, 106, 108, 115, 120, 125],
- [2.8, 3.1, 3.7, 5, 6, 7]])
+ expected_data = np.array([[104, 106, 108, 115, 120, 125], [2.8, 3.1, 3.7, 5, 6, 7]])
assert np.array_equal(curve_item.archive_data_buffer, expected_data)
@Slot(float, float, str)
def inspect_data_request(min_x: float, max_x: float, processing_command: str):
- """ Simple helper function to store the signal parameters it was invoked with """
+ """Simple helper function to store the signal parameters it was invoked with"""
inspect_data_request.min_x = min_x
inspect_data_request.max_x = max_x
inspect_data_request.processing_command = processing_command
def test_request_data_from_archiver(qtbot):
- """ Test that the signal requesting data from the archiver appliance is built correctly """
+ """Test that the signal requesting data from the archiver appliance is built correctly"""
# Create a plot and its associated curve item
plot = PyDMArchiverTimePlot(optimized_data_bins=10)
@@ -131,7 +123,7 @@ def test_request_data_from_archiver(qtbot):
# Verify that the data is requested for the time period specified, and since it is only 100 seconds, it is raw data
assert inspect_data_request.min_x == 100
assert inspect_data_request.max_x == 199
- assert inspect_data_request.processing_command == ''
+ assert inspect_data_request.processing_command == ""
# Now request over a day's worth of data at once. This will cause the request to be for optimized data
# returned in 10 bins as specified by the "optimized_data_bins" param above
@@ -139,7 +131,7 @@ def test_request_data_from_archiver(qtbot):
plot.requestDataFromArchiver(100, 100000)
assert inspect_data_request.min_x == 100
assert inspect_data_request.max_x == 99999
- assert inspect_data_request.processing_command == 'optimized_10'
+ assert inspect_data_request.processing_command == "optimized_10"
# Finally let's do a test without specifying min_x and max_x to test the plot's logic of determining
# these values itself
diff --git a/pydm/tests/widgets/test_base.py b/pydm/tests/widgets/test_base.py
index 73a1d74082..156a78fe3e 100644
--- a/pydm/tests/widgets/test_base.py
+++ b/pydm/tests/widgets/test_base.py
@@ -21,11 +21,14 @@
# --------------------
-@pytest.mark.parametrize("channel_address, expected", [
- ("CA://MA_TEST", True),
- ("", False),
- (None, False),
-])
+@pytest.mark.parametrize(
+ "channel_address, expected",
+ [
+ ("CA://MA_TEST", True),
+ ("", False),
+ (None, False),
+ ],
+)
def test_is_channel_valid(channel_address, expected):
"""
Test to ensure channel validation.
@@ -45,15 +48,22 @@ def test_is_channel_valid(channel_address, expected):
test_local_connection_status_color_map = {
False: QColor(0, 0, 0),
- True: QColor(0, 0, 0, )
+ True: QColor(
+ 0,
+ 0,
+ 0,
+ ),
}
-@pytest.mark.parametrize("init_channel", [
- "CA://MA_TEST",
- "",
- None,
-])
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MA_TEST",
+ "",
+ None,
+ ],
+)
def test_pydmwidget_construct(qtbot, init_channel):
"""
Test the construction of the widget.
@@ -112,11 +122,14 @@ def test_pydmwidget_construct(qtbot, init_channel):
assert pydm_label.opacity() == 1.0
-@pytest.mark.parametrize("init_channel", [
- "CA://MA_TEST",
- "",
- None,
-])
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MA_TEST",
+ "",
+ None,
+ ],
+)
def test_pydmwidget_widget_ctx_menu(qtbot, init_channel):
"""
Test the initial context menu creation.
@@ -183,16 +196,14 @@ def mock_exec_(*args):
monkeypatch.setattr(QMenu, "exec_", mock_exec_)
- mouse_event = QMouseEvent(QMouseEvent.MouseButtonRelease, pydm_label.rect().center(), Qt.RightButton,
- Qt.RightButton, Qt.ShiftModifier)
+ mouse_event = QMouseEvent(
+ QMouseEvent.MouseButtonRelease, pydm_label.rect().center(), Qt.RightButton, Qt.RightButton, Qt.ShiftModifier
+ )
pydm_label.open_context_menu(mouse_event)
assert "Context Menu displayed." in caplog.text
-@pytest.mark.parametrize("init_channel, expected_clipboard_text", [
- ("CA://MA_TEST", "MA_TEST"),
- (None, "")
-])
+@pytest.mark.parametrize("init_channel, expected_clipboard_text", [("CA://MA_TEST", "MA_TEST"), (None, "")])
def test_middle_click(qtbot, monkeypatch, init_channel, expected_clipboard_text):
"""
Verify that when a middle click happens on a PyDM widget, the PV name of the channel connected to will be
@@ -200,7 +211,7 @@ def test_middle_click(qtbot, monkeypatch, init_channel, expected_clipboard_text)
"""
pydm_label = PyDMLabel(init_channel=init_channel)
qtbot.addWidget(pydm_label)
- copied_text = ''
+ copied_text = ""
# Create a function that will store what would have been copied to the clipboard instead of
# doing the actual copy so that the user who is running the test does not have their actual clipboard modified
@@ -208,6 +219,7 @@ def test_middle_click(qtbot, monkeypatch, init_channel, expected_clipboard_text)
def mock_copy(self, text, mode=None):
nonlocal copied_text
copied_text = text
+
monkeypatch.setattr(QClipboard, "setText", mock_copy)
# Perform the middle click and verify the correct text (if any) was copied
@@ -216,11 +228,14 @@ def mock_copy(self, text, mode=None):
assert copied_text == expected_clipboard_text
-@pytest.mark.parametrize("init_channel", [
- "CA://MA_TEST",
- "",
- None,
-])
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MA_TEST",
+ "",
+ None,
+ ],
+)
def test_pydmwidget_init_for_designer(qtbot, init_channel):
"""
Test the initialization sequence of a PyDMWidget object.
@@ -252,11 +267,14 @@ def test_pydmwidget_alarm_severity_changed(qtbot):
assert pydm_label.alarmSeverity == PyDMWidget.ALARM_MAJOR
-@pytest.mark.parametrize("init_channel", [
- "CA://MA_TEST",
- "",
- None,
-])
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MA_TEST",
+ "",
+ None,
+ ],
+)
def test_pydmwritablewidget_init_for_designer(qtbot, init_channel):
"""
Test the initialization sequence of a PyDMWritableWidget object.
@@ -280,10 +298,13 @@ def test_pydmwritablewidget_init_for_designer(qtbot, init_channel):
assert pydm_lineedit._connected is True
-@pytest.mark.parametrize("which_limit, new_limit", [
- ("UPPER", 123.456),
- ("LOWER", 12.345),
-])
+@pytest.mark.parametrize(
+ "which_limit, new_limit",
+ [
+ ("UPPER", 123.456),
+ ("LOWER", 12.345),
+ ],
+)
def test_ctrl_limit_changed(qtbot, signals, which_limit, new_limit):
"""
Test the upper and lower limit settings.
@@ -307,27 +328,23 @@ def test_ctrl_limit_changed(qtbot, signals, which_limit, new_limit):
qtbot.addWidget(pydm_label)
if which_limit == "UPPER":
- signals.upper_ctrl_limit_signal[type(new_limit)].connect(
- pydm_label.upperCtrlLimitChanged)
+ signals.upper_ctrl_limit_signal[type(new_limit)].connect(pydm_label.upperCtrlLimitChanged)
signals.upper_ctrl_limit_signal[type(new_limit)].emit(new_limit)
assert pydm_label.get_ctrl_limits()[1] == new_limit
elif which_limit == "LOWER":
- signals.lower_ctrl_limit_signal[type(new_limit)].connect(
- pydm_label.lowerCtrlLimitChanged)
+ signals.lower_ctrl_limit_signal[type(new_limit)].connect(pydm_label.lowerCtrlLimitChanged)
signals.lower_ctrl_limit_signal[type(new_limit)].emit(new_limit)
assert pydm_label.get_ctrl_limits()[0] == new_limit
-@pytest.mark.parametrize("which_limit, new_limit", [
- (AlarmLimit.HIHI, 100.10),
- (AlarmLimit.HIGH, 90),
- (AlarmLimit.LOW, 20.5),
- (AlarmLimit.LOLO, 7)
-])
+@pytest.mark.parametrize(
+ "which_limit, new_limit",
+ [(AlarmLimit.HIHI, 100.10), (AlarmLimit.HIGH, 90), (AlarmLimit.LOW, 20.5), (AlarmLimit.LOLO, 7)],
+)
def test_alarm_limits_changed(qtbot, signals: ConnectionSignals, which_limit: str, new_limit: float):
- """ Ensure that changes to the alarm limits of a PV get sent to the right place """
+ """Ensure that changes to the alarm limits of a PV get sent to the right place"""
pydm_label = PyDMLabel(init_channel="CA://MA_TEST")
qtbot.addWidget(pydm_label)
@@ -429,11 +446,10 @@ def test_channels_for_tools(qtbot):
qtbot : fixture
Window for widget testing
"""
- pydm_label = PyDMLabel(init_channel='tst://This')
+ pydm_label = PyDMLabel(init_channel="tst://This")
qtbot.addWidget(pydm_label)
- assert all(x == y for x, y in
- zip(pydm_label.channels(), pydm_label.channels_for_tools()))
+ assert all(x == y for x, y in zip(pydm_label.channels(), pydm_label.channels_for_tools()))
def test_pydmwidget_channel_change(qtbot):
@@ -451,13 +467,13 @@ def test_pydmwidget_channel_change(qtbot):
assert pydm_label._channel is None
assert pydm_label.channels() is None
- pydm_label.channel = 'foo://bar'
- assert pydm_label._channel == 'foo://bar'
- assert pydm_label.channels()[0].address == 'foo://bar'
+ pydm_label.channel = "foo://bar"
+ assert pydm_label._channel == "foo://bar"
+ assert pydm_label.channels()[0].address == "foo://bar"
- pydm_label.channel = 'abc://def'
- assert pydm_label._channel == 'abc://def'
- assert pydm_label.channels()[0].address == 'abc://def'
+ pydm_label.channel = "abc://def"
+ assert pydm_label._channel == "abc://def"
+ assert pydm_label.channels()[0].address == "abc://def"
def test_pydmwidget_channels(qtbot):
@@ -479,39 +495,41 @@ def test_pydmwidget_channels(qtbot):
assert pydm_label._channel is None
assert pydm_label.channels() is None
- pydm_label.channel = 'test://this'
+ pydm_label.channel = "test://this"
pydm_channels = pydm_label.channels()[0]
- default_pydm_channels = PyDMChannel(address=pydm_label.channel,
- connection_slot=pydm_label.connectionStateChanged,
- value_slot=pydm_label.channelValueChanged,
- severity_slot=pydm_label.alarmSeverityChanged,
- enum_strings_slot=pydm_label.enumStringsChanged,
- unit_slot=pydm_label.unitChanged,
- prec_slot=pydm_label.precisionChanged,
- upper_ctrl_limit_slot=pydm_label.upperCtrlLimitChanged,
- lower_ctrl_limit_slot=pydm_label.lowerCtrlLimitChanged,
- upper_alarm_limit_slot=pydm_label.upper_alarm_limit_changed,
- upper_warning_limit_slot=pydm_label.upper_warning_limit_changed,
- lower_alarm_limit_slot=pydm_label.lower_alarm_limit_changed,
- lower_warning_limit_slot=pydm_label.lower_warning_limit_changed,
- value_signal=None,
- write_access_slot=None,
- timestamp_slot=pydm_label.timestamp_changed)
+ default_pydm_channels = PyDMChannel(
+ address=pydm_label.channel,
+ connection_slot=pydm_label.connectionStateChanged,
+ value_slot=pydm_label.channelValueChanged,
+ severity_slot=pydm_label.alarmSeverityChanged,
+ enum_strings_slot=pydm_label.enumStringsChanged,
+ unit_slot=pydm_label.unitChanged,
+ prec_slot=pydm_label.precisionChanged,
+ upper_ctrl_limit_slot=pydm_label.upperCtrlLimitChanged,
+ lower_ctrl_limit_slot=pydm_label.lowerCtrlLimitChanged,
+ upper_alarm_limit_slot=pydm_label.upper_alarm_limit_changed,
+ upper_warning_limit_slot=pydm_label.upper_warning_limit_changed,
+ lower_alarm_limit_slot=pydm_label.lower_alarm_limit_changed,
+ lower_warning_limit_slot=pydm_label.lower_warning_limit_changed,
+ value_signal=None,
+ write_access_slot=None,
+ timestamp_slot=pydm_label.timestamp_changed,
+ )
assert pydm_channels == default_pydm_channels
def test_pydmwidget_tooltip(qtbot):
"""
- Test the tooltip. This test is for a widget whose base class is PyDMWidget.
+ Test the tooltip. This test is for a widget whose base class is PyDMWidget.
- Expectations:
- 1. The widget's tooltip will update
+ Expectations:
+ 1. The widget's tooltip will update
- Parameters
- ----------
- qtbot : fixture
- Window for widget testing
+ Parameters
+ ----------
+ qtbot : fixture
+ Window for widget testing
"""
pydm_label = PyDMLabel()
qtbot.addWidget(pydm_label)
@@ -526,12 +544,10 @@ def test_pydmwidget_tooltip(qtbot):
assert tool_tip == str(pydm_label.value)
-@pytest.mark.parametrize('channel_address, monitor_disp', [
- ('tst://this', True),
- ('tst://this.VAL', True),
- ('tst://this.[1:2]', True),
- ('tst://this', False)
- ])
+@pytest.mark.parametrize(
+ "channel_address, monitor_disp",
+ [("tst://this", True), ("tst://this.VAL", True), ("tst://this.[1:2]", True), ("tst://this", False)],
+)
def test_pydmwritablewidget_channels(qtbot, channel_address, monitor_disp):
"""
Test the channels population for the widget whose base class PyDMWritableWidget
@@ -557,30 +573,34 @@ def test_pydmwritablewidget_channels(qtbot, channel_address, monitor_disp):
pydm_lineedit.channel = channel_address
pydm_channels = pydm_lineedit.channels()[0]
- default_pydm_channels = PyDMChannel(address=pydm_lineedit.channel,
- connection_slot=pydm_lineedit.connectionStateChanged,
- value_slot=pydm_lineedit.channelValueChanged,
- severity_slot=pydm_lineedit.alarmSeverityChanged,
- enum_strings_slot=pydm_lineedit.enumStringsChanged,
- unit_slot=pydm_lineedit.unitChanged,
- prec_slot=pydm_lineedit.precisionChanged,
- upper_ctrl_limit_slot=pydm_lineedit.upperCtrlLimitChanged,
- lower_ctrl_limit_slot=pydm_lineedit.lowerCtrlLimitChanged,
- upper_alarm_limit_slot=pydm_lineedit.upper_alarm_limit_changed,
- lower_alarm_limit_slot=pydm_lineedit.lower_alarm_limit_changed,
- upper_warning_limit_slot=pydm_lineedit.upper_warning_limit_changed,
- lower_warning_limit_slot=pydm_lineedit.lower_warning_limit_changed,
- value_signal=pydm_lineedit.send_value_signal,
- write_access_slot=pydm_lineedit.writeAccessChanged,
- timestamp_slot=pydm_lineedit.timestamp_changed)
+ default_pydm_channels = PyDMChannel(
+ address=pydm_lineedit.channel,
+ connection_slot=pydm_lineedit.connectionStateChanged,
+ value_slot=pydm_lineedit.channelValueChanged,
+ severity_slot=pydm_lineedit.alarmSeverityChanged,
+ enum_strings_slot=pydm_lineedit.enumStringsChanged,
+ unit_slot=pydm_lineedit.unitChanged,
+ prec_slot=pydm_lineedit.precisionChanged,
+ upper_ctrl_limit_slot=pydm_lineedit.upperCtrlLimitChanged,
+ lower_ctrl_limit_slot=pydm_lineedit.lowerCtrlLimitChanged,
+ upper_alarm_limit_slot=pydm_lineedit.upper_alarm_limit_changed,
+ lower_alarm_limit_slot=pydm_lineedit.lower_alarm_limit_changed,
+ upper_warning_limit_slot=pydm_lineedit.upper_warning_limit_changed,
+ lower_warning_limit_slot=pydm_lineedit.lower_warning_limit_changed,
+ value_signal=pydm_lineedit.send_value_signal,
+ write_access_slot=pydm_lineedit.writeAccessChanged,
+ timestamp_slot=pydm_lineedit.timestamp_changed,
+ )
assert pydm_channels == default_pydm_channels
if monitor_disp:
- assert pydm_lineedit._disp_channel.address == 'tst://this.DISP'
+ assert pydm_lineedit._disp_channel.address == "tst://this.DISP"
else:
assert pydm_lineedit._disp_channel is None
+
@pytest.mark.parametrize(
- "channel_address, connected, write_access, is_app_read_only, disable_put", [
+ "channel_address, connected, write_access, is_app_read_only, disable_put",
+ [
("CA://MA_TEST", True, True, True, 0),
("CA://MA_TEST", True, False, True, 0),
("CA://MA_TEST", True, True, False, 0),
@@ -594,10 +614,11 @@ def test_pydmwritablewidget_channels(qtbot, channel_address, monitor_disp):
("CA://MA_TEST", True, True, False, 1),
("", False, False, False, 0),
(None, False, False, False, 0),
- ])
-def test_pydmwritable_check_enable_state(qtbot, channel_address,
- connected, write_access,
- is_app_read_only, disable_put):
+ ],
+)
+def test_pydmwritable_check_enable_state(
+ qtbot, channel_address, connected, write_access, is_app_read_only, disable_put
+):
"""
Test the tooltip generated depending on the channel address validation, connection, write access,
DISP field, and whether the app is read-only. This test is for a widget whose base class is PyDMWritableWidget.
@@ -696,15 +717,20 @@ def test_pydmwidget_rules(qtbot, caplog):
assert record.levelno == logging.ERROR
assert "Invalid format for Rules" in caplog.text
- rules = [{'name': 'Rule #1', 'property': 'Enable',
- 'expression': 'ch[0] > 1',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Enable",
+ "expression": "ch[0] > 1",
+ "channels": [{"channel": "ca://MTEST:Float", "trigger": True}],
+ }
+ ]
rules_json = json.dumps(rules)
pydm_label.rules = rules_json
assert pydm_label.rules == rules_json
- rules[0]['name'] = 'Rule #2'
+ rules[0]["name"] = "Rule #2"
rules_json = json.dumps(rules)
pydm_label.rules = rules_json
@@ -724,22 +750,14 @@ def test_pydmwidget_rule_evaluated(qtbot, caplog):
qtbot.addWidget(widget)
widget.show()
- payload = {
- 'name': 'Test Rule 1',
- 'property': 'Invalid Property',
- 'value': 'foo'
- }
+ payload = {"name": "Test Rule 1", "property": "Invalid Property", "value": "foo"}
widget.rule_evaluated(payload)
for record in caplog.records:
assert record.levelno == logging.ERROR
assert "is not part of this widget properties" in caplog.text
- payload = {
- 'name': 'Test Rule 1',
- 'property': 'Visible',
- 'value': False
- }
+ payload = {"name": "Test Rule 1", "property": "Visible", "value": False}
assert widget.isVisible()
widget.rule_evaluated(payload)
diff --git a/pydm/tests/widgets/test_baseplot.py b/pydm/tests/widgets/test_baseplot.py
index 13c274b634..81cf98dce9 100644
--- a/pydm/tests/widgets/test_baseplot.py
+++ b/pydm/tests/widgets/test_baseplot.py
@@ -12,11 +12,10 @@
logger = logging.getLogger(__name__)
-@pytest.mark.parametrize("color, line_style, line_width, name", [
- (QColor("red"), Qt.SolidLine, 1, "test_name"),
- (None, Qt.DashLine, 10, ""),
- (None, None, None, None)
-])
+@pytest.mark.parametrize(
+ "color, line_style, line_width, name",
+ [(QColor("red"), Qt.SolidLine, 1, "test_name"), (None, Qt.DashLine, 10, ""), (None, None, None, None)],
+)
def test_baseplotcurveitem_construct(qtbot, color, line_style, line_width, name):
base_plotcurve_item = BasePlotCurveItem(color, line_style, line_width, name=name)
@@ -49,13 +48,13 @@ def test_baseplotcurveitem_properties_and_setters(qtbot):
base_plotcurve_item.lineWidth = 3
assert base_plotcurve_item.lineWidth == 3
- base_plotcurve_item.symbol = 'o'
- assert base_plotcurve_item.symbol == 'o'
+ base_plotcurve_item.symbol = "o"
+ assert base_plotcurve_item.symbol == "o"
assert base_plotcurve_item._pen.color() == base_plotcurve_item._color
# The invalid symbol 'A' should be ejected
base_plotcurve_item.symbol = "A"
- assert base_plotcurve_item.symbol == 'o'
+ assert base_plotcurve_item.symbol == "o"
base_plotcurve_item.symbolSize = 100
assert base_plotcurve_item.symbolSize == 100
@@ -99,8 +98,8 @@ def test_baseplot_add_curve(qtbot):
def test_baseplot_multiple_y_axes(qtbot):
- """ Test that when we add curves while specifying new y-axis names for them, those axes are created and
- added to the plot correctly. Also confirm that adding a curve to an existing axis works properly. """
+ """Test that when we add curves while specifying new y-axis names for them, those axes are created and
+ added to the plot correctly. Also confirm that adding a curve to an existing axis works properly."""
base_plot = BasePlot()
base_plot.clear()
base_plot.clearAxes()
@@ -112,34 +111,34 @@ def test_baseplot_multiple_y_axes(qtbot):
plot_curve_item_2 = WaveformCurveItem()
plot_curve_item_3 = WaveformCurveItem()
plot_curve_item_4 = WaveformCurveItem()
- base_plot.addCurve(plot_curve_item_1, y_axis_name='Test Axis 1')
- base_plot.addCurve(plot_curve_item_2, y_axis_name='Test Axis 2')
- base_plot.addCurve(plot_curve_item_3, y_axis_name='Test Axis 3')
- base_plot.addCurve(plot_curve_item_4, y_axis_name='Test Axis 1')
+ base_plot.addCurve(plot_curve_item_1, y_axis_name="Test Axis 1")
+ base_plot.addCurve(plot_curve_item_2, y_axis_name="Test Axis 2")
+ base_plot.addCurve(plot_curve_item_3, y_axis_name="Test Axis 3")
+ base_plot.addCurve(plot_curve_item_4, y_axis_name="Test Axis 1")
qtbot.addWidget(base_plot)
# There should be 4 axes (the x-axis, and the 3 new y-axes we have just created)
assert len(base_plot.plotItem.axes) == 4
# Verify the 4 axes are indeed the ones we expect
- assert 'bottom' in base_plot.plotItem.axes
- assert 'Test Axis 1' in base_plot.plotItem.axes
- assert 'Test Axis 2' in base_plot.plotItem.axes
- assert 'Test Axis 3' in base_plot.plotItem.axes
+ assert "bottom" in base_plot.plotItem.axes
+ assert "Test Axis 1" in base_plot.plotItem.axes
+ assert "Test Axis 2" in base_plot.plotItem.axes
+ assert "Test Axis 3" in base_plot.plotItem.axes
# Verify their orientations were set correctly
- assert base_plot.plotItem.axes['Test Axis 1']['item'].orientation == 'left'
- assert base_plot.plotItem.axes['Test Axis 2']['item'].orientation == 'left'
- assert base_plot.plotItem.axes['Test Axis 3']['item'].orientation == 'left'
+ assert base_plot.plotItem.axes["Test Axis 1"]["item"].orientation == "left"
+ assert base_plot.plotItem.axes["Test Axis 2"]["item"].orientation == "left"
+ assert base_plot.plotItem.axes["Test Axis 3"]["item"].orientation == "left"
# Verify the curves got assigned to the correct axes
- assert base_plot.plotItem.curvesPerAxis['Test Axis 1'] == 2
- assert base_plot.plotItem.curvesPerAxis['Test Axis 2'] == 1
- assert base_plot.plotItem.curvesPerAxis['Test Axis 3'] == 1
+ assert base_plot.plotItem.curvesPerAxis["Test Axis 1"] == 2
+ assert base_plot.plotItem.curvesPerAxis["Test Axis 2"] == 1
+ assert base_plot.plotItem.curvesPerAxis["Test Axis 3"] == 1
def test_baseplot_no_added_y_axes(qtbot):
- """ Confirm that if the user does not name or create any new y-axes, the plot will still work just fine """
+ """Confirm that if the user does not name or create any new y-axes, the plot will still work just fine"""
base_plot = BasePlot()
base_plot.clear()
@@ -157,30 +156,32 @@ def test_baseplot_no_added_y_axes(qtbot):
assert len(base_plot.plotItem.axes) == 4
# Verify their names were not changed in any way
- assert 'bottom' in base_plot.plotItem.axes
- assert 'left' in base_plot.plotItem.axes
- assert 'top' in base_plot.plotItem.axes
- assert 'right' in base_plot.plotItem.axes
+ assert "bottom" in base_plot.plotItem.axes
+ assert "left" in base_plot.plotItem.axes
+ assert "top" in base_plot.plotItem.axes
+ assert "right" in base_plot.plotItem.axes
# Verify their orientations were not changed in any way
- assert base_plot.plotItem.axes['bottom']['item'].orientation == 'bottom'
- assert base_plot.plotItem.axes['left']['item'].orientation == 'left'
- assert base_plot.plotItem.axes['top']['item'].orientation == 'top'
- assert base_plot.plotItem.axes['right']['item'].orientation == 'right'
+ assert base_plot.plotItem.axes["bottom"]["item"].orientation == "bottom"
+ assert base_plot.plotItem.axes["left"]["item"].orientation == "left"
+ assert base_plot.plotItem.axes["top"]["item"].orientation == "top"
+ assert base_plot.plotItem.axes["right"]["item"].orientation == "right"
def test_timeplot_add_multiple_axes(qtbot):
- """ Simliar to the multiple y axes test above, but this one creates the new axes first, and invokes the setters
- for the curves and axes directly, which is exactly what would happen in the flow initiated from designer.
- Since the base plot has no method for setting curves itself, we instead use a time plot. """
+ """Simliar to the multiple y axes test above, but this one creates the new axes first, and invokes the setters
+ for the curves and axes directly, which is exactly what would happen in the flow initiated from designer.
+ Since the base plot has no method for setting curves itself, we instead use a time plot."""
time_plot = PyDMTimePlot()
# A list of axes represented in json that would be auto-generated by a user creating a new plot
# We have 4 unique axes, 2 on the left side of the plot and 2 on the right
- json_axes = ['{"name": "Axis 1", "orientation": "left", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}',
- '{"name": "Axis 2", "orientation": "right", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}',
- '{"name": "Axis 3", "orientation": "right", "minRange": -0.5, "maxRange": 0.5, "autoRange": false}',
- '{"name": "Axis 4", "orientation": "left", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}']
+ json_axes = [
+ '{"name": "Axis 1", "orientation": "left", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}',
+ '{"name": "Axis 2", "orientation": "right", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}',
+ '{"name": "Axis 3", "orientation": "right", "minRange": -0.5, "maxRange": 0.5, "autoRange": false}',
+ '{"name": "Axis 4", "orientation": "left", "minRange": -1.0, "maxRange": 1.0, "autoRange": true}',
+ ]
time_plot.setYAxes(json_axes)
@@ -188,93 +189,95 @@ def test_timeplot_add_multiple_axes(qtbot):
assert len(time_plot.plotItem.axes) == 5
# Verify the 5 axes are indeed the ones we expect
- assert 'bottom' in time_plot.plotItem.axes
- assert 'Axis 1' in time_plot.plotItem.axes
- assert 'Axis 2' in time_plot.plotItem.axes
- assert 'Axis 3' in time_plot.plotItem.axes
- assert 'Axis 4' in time_plot.plotItem.axes
+ assert "bottom" in time_plot.plotItem.axes
+ assert "Axis 1" in time_plot.plotItem.axes
+ assert "Axis 2" in time_plot.plotItem.axes
+ assert "Axis 3" in time_plot.plotItem.axes
+ assert "Axis 4" in time_plot.plotItem.axes
# Verify their orientations were set correctly
- assert time_plot.plotItem.axes['bottom']['item'].orientation == 'bottom'
- assert time_plot.plotItem.axes['Axis 1']['item'].orientation == 'left'
- assert time_plot.plotItem.axes['Axis 2']['item'].orientation == 'right'
- assert time_plot.plotItem.axes['Axis 3']['item'].orientation == 'right'
- assert time_plot.plotItem.axes['Axis 4']['item'].orientation == 'left'
+ assert time_plot.plotItem.axes["bottom"]["item"].orientation == "bottom"
+ assert time_plot.plotItem.axes["Axis 1"]["item"].orientation == "left"
+ assert time_plot.plotItem.axes["Axis 2"]["item"].orientation == "right"
+ assert time_plot.plotItem.axes["Axis 3"]["item"].orientation == "right"
+ assert time_plot.plotItem.axes["Axis 4"]["item"].orientation == "left"
# Also confirm that setting a specific range on an axis works
- assert time_plot.plotItem.axes['Axis 3']['item'].min_range == -0.5
- assert time_plot.plotItem.axes['Axis 3']['item'].max_range == 0.5
- assert time_plot.plotItem.axes['Axis 3']['item'].auto_range is False
+ assert time_plot.plotItem.axes["Axis 3"]["item"].min_range == -0.5
+ assert time_plot.plotItem.axes["Axis 3"]["item"].max_range == 0.5
+ assert time_plot.plotItem.axes["Axis 3"]["item"].auto_range is False
# Now let's connect 5 curves with these axes. Sine and cosine will share an axis, the rest will get their own
- json_curves = ['{"channel": "ca://MTEST:SinVal", "name": "Sine", "color": "white", "lineStyle": 1, '
- '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 1"}',
- '{"channel": "ca://MTEST:CosVal", "name": "Cosine", "color": "red", "lineStyle": 1, '
- '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 1"}',
- '{"channel": "ca://MTEST:MeanValue", "name": "Orange Value", "color": "orange", "lineStyle": 1, '
- '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 2"}',
- '{"channel": "ca://MTEST:MaxValue", "name": "Green Value", "color": "forestgreen", "lineStyle": 1, '
- '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 3"}',
- '{"channel": "ca://MTEST:MinValue", "name": "Yellow Value", "color": "yellow", "lineStyle": 1, '
- '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 4"}']
+ json_curves = [
+ '{"channel": "ca://MTEST:SinVal", "name": "Sine", "color": "white", "lineStyle": 1, '
+ '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 1"}',
+ '{"channel": "ca://MTEST:CosVal", "name": "Cosine", "color": "red", "lineStyle": 1, '
+ '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 1"}',
+ '{"channel": "ca://MTEST:MeanValue", "name": "Orange Value", "color": "orange", "lineStyle": 1, '
+ '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 2"}',
+ '{"channel": "ca://MTEST:MaxValue", "name": "Green Value", "color": "forestgreen", "lineStyle": 1, '
+ '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 3"}',
+ '{"channel": "ca://MTEST:MinValue", "name": "Yellow Value", "color": "yellow", "lineStyle": 1, '
+ '"lineWidth": 1, "symbol": null, "symbolSize": 10, "yAxisName": "Axis 4"}',
+ ]
time_plot.setCurves(json_curves)
# Verify the curves got assigned to the correct axes
assert len(time_plot.curves) == 5
- assert time_plot.plotItem.curvesPerAxis['Axis 1'] == 2
- assert time_plot.plotItem.curvesPerAxis['Axis 2'] == 1
- assert time_plot.plotItem.curvesPerAxis['Axis 3'] == 1
- assert time_plot.plotItem.curvesPerAxis['Axis 4'] == 1
+ assert time_plot.plotItem.curvesPerAxis["Axis 1"] == 2
+ assert time_plot.plotItem.curvesPerAxis["Axis 2"] == 1
+ assert time_plot.plotItem.curvesPerAxis["Axis 3"] == 1
+ assert time_plot.plotItem.curvesPerAxis["Axis 4"] == 1
def test_multiaxis_plot_no_designer_flow(qtbot):
- """ Similar to the tests above except don't use the qt designer flow at all. Verify the correct axes get
- added to the plot in this case as well. """
+ """Similar to the tests above except don't use the qt designer flow at all. Verify the correct axes get
+ added to the plot in this case as well."""
# Setup a plot with three test data channels, 2 assigned to the same axis, the third to its own
plot = PyDMTimePlot()
- plot.addYChannel(y_channel='ca://test_channel', yAxisName='Axis 1')
- plot.addYChannel(y_channel='ca://test_channel_2', yAxisName='Axis 1')
- plot.addYChannel(y_channel='ca://test_channel_3', yAxisName='Axis 2')
+ plot.addYChannel(y_channel="ca://test_channel", yAxisName="Axis 1")
+ plot.addYChannel(y_channel="ca://test_channel_2", yAxisName="Axis 1")
+ plot.addYChannel(y_channel="ca://test_channel_3", yAxisName="Axis 2")
# There should be 5 axes, the 3 we definitely expect ('Axis 1' 'Axis 2', and the default 'bottom' x-axis). But
# pyqtgraph also adds a mirrored 'right' axis and a mirrored 'top' axis. These do not display by
# default so we'll keep them.
assert len(plot.plotItem.axes) == 5
- assert 'bottom' in plot.plotItem.axes
- assert 'Axis 1' in plot.plotItem.axes
- assert 'Axis 2' in plot.plotItem.axes
- assert 'right' in plot.plotItem.axes
- assert 'top' in plot.plotItem.axes
- assert plot.plotItem.curvesPerAxis['Axis 1'] == 2
- assert plot.plotItem.curvesPerAxis['Axis 2'] == 1
+ assert "bottom" in plot.plotItem.axes
+ assert "Axis 1" in plot.plotItem.axes
+ assert "Axis 2" in plot.plotItem.axes
+ assert "right" in plot.plotItem.axes
+ assert "top" in plot.plotItem.axes
+ assert plot.plotItem.curvesPerAxis["Axis 1"] == 2
+ assert plot.plotItem.curvesPerAxis["Axis 2"] == 1
# Now check the case where no new y-axis name is specified for any of the new channels.
plot = PyDMTimePlot()
- plot.addYChannel(y_channel='ca://test_channel')
- plot.addYChannel(y_channel='ca://test_channel_2')
- plot.addYChannel(y_channel='ca://test_channel_3')
+ plot.addYChannel(y_channel="ca://test_channel")
+ plot.addYChannel(y_channel="ca://test_channel_2")
+ plot.addYChannel(y_channel="ca://test_channel_3")
# Since no new axes were created, we just have the default ones provided by pyqtgraph
assert len(plot.plotItem.axes) == 4
- assert 'bottom' in plot.plotItem.axes
- assert 'left' in plot.plotItem.axes
- assert 'right' in plot.plotItem.axes
- assert 'top' in plot.plotItem.axes
+ assert "bottom" in plot.plotItem.axes
+ assert "left" in plot.plotItem.axes
+ assert "right" in plot.plotItem.axes
+ assert "top" in plot.plotItem.axes
def test_reset_autorange(qtbot):
- """ Verify that resetting the autorange properties of the plot works as expected """
+ """Verify that resetting the autorange properties of the plot works as expected"""
plot = PyDMWaveformPlot()
plot.setAutoRangeX(False)
plot.setAutoRangeY(False)
# Quick check to ensure autorange was turned off
for view in plot.getPlotItem().stackedViews:
- assert not any(view.state['autoRange'])
+ assert not any(view.state["autoRange"])
plot.resetAutoRangeX()
plot.resetAutoRangeY()
for view in plot.getPlotItem().stackedViews:
- assert all(view.state['autoRange'])
+ assert all(view.state["autoRange"])
diff --git a/pydm/tests/widgets/test_byte.py b/pydm/tests/widgets/test_byte.py
index 1bffdb0363..5027ec1c24 100644
--- a/pydm/tests/widgets/test_byte.py
+++ b/pydm/tests/widgets/test_byte.py
@@ -3,19 +3,20 @@
from ...widgets.byte import PyDMByteIndicator
-@pytest.mark.parametrize("shift, value, expected", [
- (0, 0, (False, False, False)),
- (0, 5, (True, False, True)),
- (0, -5, (False, False, False)),
-
- (1, 0, (False, False, False)),
- (1, 4, (False, True, False)),
- (1, -5, (False, False, False)),
-
- (-1, 0, (False, False, False)),
- (-1, 1, (False, True, False)),
- (-1, -5, (False, False, False)),
-])
+@pytest.mark.parametrize(
+ "shift, value, expected",
+ [
+ (0, 0, (False, False, False)),
+ (0, 5, (True, False, True)),
+ (0, -5, (False, False, False)),
+ (1, 0, (False, False, False)),
+ (1, 4, (False, True, False)),
+ (1, -5, (False, False, False)),
+ (-1, 0, (False, False, False)),
+ (-1, 1, (False, True, False)),
+ (-1, -5, (False, False, False)),
+ ],
+)
def test_value_shift(qtbot, signals, shift, value, expected):
"""
Test the widget's handling of the value changed event affected by predefined shift.
diff --git a/pydm/tests/widgets/test_channel.py b/pydm/tests/widgets/test_channel.py
index 3279184dd9..2f9a1460e5 100644
--- a/pydm/tests/widgets/test_channel.py
+++ b/pydm/tests/widgets/test_channel.py
@@ -7,9 +7,10 @@
from pydm.data_plugins import plugin_for_address
-class A():
+class A:
pass
+
def test_construct(qtbot):
"""
Test the construct of the widget.
@@ -27,46 +28,50 @@ def test_construct(qtbot):
"""
pydm_channel = PyDMChannel()
- assert pydm_channel.address is None and \
- pydm_channel.connection_slot is None and \
- pydm_channel.value_slot is None and \
- pydm_channel.severity_slot is None and \
- pydm_channel.enum_strings_slot is None and \
- pydm_channel.unit_slot is None and \
- pydm_channel.prec_slot is None and \
- pydm_channel.upper_ctrl_limit_slot is None and \
- pydm_channel.lower_ctrl_limit_slot is None and \
- pydm_channel.upper_alarm_limit_slot is None and \
- pydm_channel.lower_alarm_limit_slot is None and \
- pydm_channel.upper_warning_limit_slot is None and \
- pydm_channel.lower_warning_limit_slot is None and \
- pydm_channel.write_access_slot is None and \
- pydm_channel.value_signal is None and \
- pydm_channel.timestamp_slot is None
-
- pydm_label = PyDMLabel(init_channel='tst://this')
+ assert (
+ pydm_channel.address is None
+ and pydm_channel.connection_slot is None
+ and pydm_channel.value_slot is None
+ and pydm_channel.severity_slot is None
+ and pydm_channel.enum_strings_slot is None
+ and pydm_channel.unit_slot is None
+ and pydm_channel.prec_slot is None
+ and pydm_channel.upper_ctrl_limit_slot is None
+ and pydm_channel.lower_ctrl_limit_slot is None
+ and pydm_channel.upper_alarm_limit_slot is None
+ and pydm_channel.lower_alarm_limit_slot is None
+ and pydm_channel.upper_warning_limit_slot is None
+ and pydm_channel.lower_warning_limit_slot is None
+ and pydm_channel.write_access_slot is None
+ and pydm_channel.value_signal is None
+ and pydm_channel.timestamp_slot is None
+ )
+
+ pydm_label = PyDMLabel(init_channel="tst://this")
qtbot.addWidget(pydm_label)
pydm_label_channels = pydm_label.channels()[0]
- default_pydm_label_channels = PyDMChannel(address=pydm_label.channel,
- connection_slot=pydm_label.connectionStateChanged,
- value_slot=pydm_label.channelValueChanged,
- severity_slot=pydm_label.alarmSeverityChanged,
- enum_strings_slot=pydm_label.enumStringsChanged,
- unit_slot=pydm_label.unitChanged,
- prec_slot=pydm_label.precisionChanged,
- upper_ctrl_limit_slot=pydm_label.upperCtrlLimitChanged,
- lower_ctrl_limit_slot=pydm_label.lowerCtrlLimitChanged,
- upper_alarm_limit_slot=pydm_label.upper_alarm_limit_changed,
- lower_alarm_limit_slot=pydm_label.lower_alarm_limit_changed,
- upper_warning_limit_slot=pydm_label.upper_warning_limit_changed,
- lower_warning_limit_slot=pydm_label.lower_warning_limit_changed,
- value_signal=None,
- write_access_slot=None,
- timestamp_slot=pydm_label.timestamp_changed)
+ default_pydm_label_channels = PyDMChannel(
+ address=pydm_label.channel,
+ connection_slot=pydm_label.connectionStateChanged,
+ value_slot=pydm_label.channelValueChanged,
+ severity_slot=pydm_label.alarmSeverityChanged,
+ enum_strings_slot=pydm_label.enumStringsChanged,
+ unit_slot=pydm_label.unitChanged,
+ prec_slot=pydm_label.precisionChanged,
+ upper_ctrl_limit_slot=pydm_label.upperCtrlLimitChanged,
+ lower_ctrl_limit_slot=pydm_label.lowerCtrlLimitChanged,
+ upper_alarm_limit_slot=pydm_label.upper_alarm_limit_changed,
+ lower_alarm_limit_slot=pydm_label.lower_alarm_limit_changed,
+ upper_warning_limit_slot=pydm_label.upper_warning_limit_changed,
+ lower_warning_limit_slot=pydm_label.lower_warning_limit_changed,
+ value_signal=None,
+ write_access_slot=None,
+ timestamp_slot=pydm_label.timestamp_changed,
+ )
assert pydm_label_channels == default_pydm_label_channels
- pydm_lineedit = PyDMLineEdit(init_channel='tst://this2')
+ pydm_lineedit = PyDMLineEdit(init_channel="tst://this2")
qtbot.addWidget(pydm_lineedit)
# Test equal and not equal comparisons
@@ -81,20 +86,20 @@ def test_construct(qtbot):
def test_pydm_connection(test_plugin):
# Plugin, Channel and Registry
- chan = PyDMChannel('tst://Tst:this3')
+ chan = PyDMChannel("tst://Tst:this3")
plugin = plugin_for_address(chan.address)
plugin_no = len(plugin.connections)
- print('Connections Before:')
+ print("Connections Before:")
for k, v in plugin.connections.items():
- print('\t', k, ' - ', v)
+ print("\t", k, " - ", v)
# Make a connection
chan.connect()
- print('Connections After:')
+ print("Connections After:")
for k, v in plugin.connections.items():
- print('\t', k, ' - ', v)
+ print("\t", k, " - ", v)
assert len(plugin.connections) == plugin_no + 1
# Remove connections
@@ -102,9 +107,9 @@ def test_pydm_connection(test_plugin):
assert len(plugin.connections) == plugin_no
-
@pytest.mark.parametrize(
- "ch, ch_expected", [
+ "ch, ch_expected",
+ [
("ca://MTEST:Float", "ca://MTEST:Float"),
(" foo://bar", "foo://bar"),
(" foo://bar ", "foo://bar"),
@@ -113,7 +118,8 @@ def test_pydm_connection(test_plugin):
("\tfoo://bar", "foo://bar"),
("", ""),
(None, None),
- ])
+ ],
+)
def test_channel_address(ch, ch_expected):
channel = PyDMChannel()
channel.address = ch
diff --git a/pydm/tests/widgets/test_checkbox.py b/pydm/tests/widgets/test_checkbox.py
index 5e7d24730a..45d4e8a070 100644
--- a/pydm/tests/widgets/test_checkbox.py
+++ b/pydm/tests/widgets/test_checkbox.py
@@ -8,11 +8,15 @@
# POSITIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("init_channel", [
- "CA://MTEST",
- "",
- None,
-])
+
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MTEST",
+ "",
+ None,
+ ],
+)
def test_construct(qtbot, init_channel):
"""
Test the widget construct.
@@ -33,16 +37,19 @@ def test_construct(qtbot, init_channel):
assert not pydm_checkbox.isChecked()
-@pytest.mark.parametrize("init_checked_status, new_value", [
- (True, 0),
- (True, -1),
- (False, 0),
- (False, -66),
- (True, 1),
- (False, 1),
- (True, 999),
- (False, 999),
-])
+@pytest.mark.parametrize(
+ "init_checked_status, new_value",
+ [
+ (True, 0),
+ (True, -1),
+ (False, 0),
+ (False, -66),
+ (True, 1),
+ (False, 1),
+ (True, 999),
+ (False, 999),
+ ],
+)
def test_value_changed(qtbot, signals, init_checked_status, new_value):
"""
@@ -73,11 +80,14 @@ def test_value_changed(qtbot, signals, init_checked_status, new_value):
assert pydm_checkbox.isChecked() if new_value > 0 else not pydm_checkbox.isChecked()
-@pytest.mark.parametrize("is_checked", [
- True,
- False,
- None,
-])
+@pytest.mark.parametrize(
+ "is_checked",
+ [
+ True,
+ False,
+ None,
+ ],
+)
def test_send_value(qtbot, signals, is_checked):
"""
Test the data sent from the widget to the channel when the widget is checked or unchecked.
@@ -103,4 +113,3 @@ def test_send_value(qtbot, signals, is_checked):
pydm_checkbox.send_value(is_checked)
assert signals.value == 1 if is_checked else signals.value == 0
-
diff --git a/pydm/tests/widgets/test_colormaps.py b/pydm/tests/widgets/test_colormaps.py
index e0e6d36589..8ab21509eb 100644
--- a/pydm/tests/widgets/test_colormaps.py
+++ b/pydm/tests/widgets/test_colormaps.py
@@ -1,18 +1,27 @@
# Unit Tests for the Color Map
-import pytest
-
import numpy as np
-from ...widgets.colormaps import PyDMColorMap, _magma_data, _inferno_data, _plasma_data, _viridis_data, _jet_data, \
- _monochrome_data, _hot_data, cmaps, magma, inferno, plasma, viridis, jet, monochrome, hot, cmap_names
+from ...widgets.colormaps import (
+ PyDMColorMap,
+ cmaps,
+ magma,
+ inferno,
+ plasma,
+ viridis,
+ jet,
+ monochrome,
+ hot,
+ cmap_names,
+)
# --------------------
# POSITIVE TEST CASES
# --------------------
+
def test_construct():
"""
Test the construction of the ColorMaps, and the creations of auxiliary helper objects.
diff --git a/pydm/tests/widgets/test_curve_editor.py b/pydm/tests/widgets/test_curve_editor.py
index 09e8c09d2b..12d01a68e2 100644
--- a/pydm/tests/widgets/test_curve_editor.py
+++ b/pydm/tests/widgets/test_curve_editor.py
@@ -1,7 +1,13 @@
from qtpy.QtWidgets import QTableView
from ...widgets.baseplot import BasePlot
-from ...widgets.baseplot_curve_editor import (AxisColumnDelegate, ColorColumnDelegate, LineColumnDelegate,
- SymbolColumnDelegate, RedrawModeColumnDelegate, PlotStyleColumnDelegate)
+from ...widgets.baseplot_curve_editor import (
+ AxisColumnDelegate,
+ ColorColumnDelegate,
+ LineColumnDelegate,
+ SymbolColumnDelegate,
+ RedrawModeColumnDelegate,
+ PlotStyleColumnDelegate,
+)
from ...widgets.baseplot_table_model import BasePlotCurvesModel
from ...widgets.scatterplot_curve_editor import ScatterPlotCurveEditorDialog
from ...widgets.timeplot_curve_editor import TimePlotCurveEditorDialog
@@ -28,11 +34,11 @@ def test_waveform_curve_editor(qtbot):
# Verify that the drop downs for columns with non built-in types are all put in the correct place
# Note: We do need to check these on each individual type of curve editor (see below tests) and not just
# in the base plot editor since each plot type can have varying numbers of columns
- color_index = table_model.getColumnIndex('Color')
- line_style_index = table_model.getColumnIndex('Line Style')
- symbol_index = table_model.getColumnIndex('Symbol')
- redraw_mode_index = table_model.getColumnIndex('Redraw Mode')
- plot_style_index = table_model.getColumnIndex('Style')
+ color_index = table_model.getColumnIndex("Color")
+ line_style_index = table_model.getColumnIndex("Line Style")
+ symbol_index = table_model.getColumnIndex("Symbol")
+ redraw_mode_index = table_model.getColumnIndex("Redraw Mode")
+ plot_style_index = table_model.getColumnIndex("Style")
assert type(table_view.itemDelegateForColumn(color_index)) is ColorColumnDelegate
assert type(table_view.itemDelegateForColumn(line_style_index)) is LineColumnDelegate
@@ -58,10 +64,10 @@ def test_timeplot_curve_editor(qtbot):
table_view = curve_editor.table_view
# Verify that the drop downs for columns with non built-in types are all put in the correct place
- color_index = table_model.getColumnIndex('Color')
- line_style_index = table_model.getColumnIndex('Line Style')
- symbol_index = table_model.getColumnIndex('Symbol')
- plot_style_index = table_model.getColumnIndex('Style')
+ color_index = table_model.getColumnIndex("Color")
+ line_style_index = table_model.getColumnIndex("Line Style")
+ symbol_index = table_model.getColumnIndex("Symbol")
+ plot_style_index = table_model.getColumnIndex("Style")
assert type(table_view.itemDelegateForColumn(color_index)) is ColorColumnDelegate
assert type(table_view.itemDelegateForColumn(line_style_index)) is LineColumnDelegate
@@ -86,10 +92,10 @@ def test_scatterplot_editor(qtbot):
table_view = curve_editor.table_view
# Verify that the drop downs for columns with non built-in types are all put in the correct place
- color_index = table_model.getColumnIndex('Color')
- line_style_index = table_model.getColumnIndex('Line Style')
- symbol_index = table_model.getColumnIndex('Symbol')
- redraw_mode_index = table_model.getColumnIndex('Redraw Mode')
+ color_index = table_model.getColumnIndex("Color")
+ line_style_index = table_model.getColumnIndex("Line Style")
+ symbol_index = table_model.getColumnIndex("Symbol")
+ redraw_mode_index = table_model.getColumnIndex("Redraw Mode")
assert type(table_view.itemDelegateForColumn(color_index)) is ColorColumnDelegate
assert type(table_view.itemDelegateForColumn(line_style_index)) is LineColumnDelegate
@@ -110,25 +116,25 @@ def test_axis_editor(qtbot):
axis_view = curve_editor.axis_view
# Verify the column count is correct, and the axis column delegate is placed correctly
- axis_orientation_index = axis_model._column_names.index('Y-Axis Orientation')
+ axis_orientation_index = axis_model._column_names.index("Y-Axis Orientation")
assert type(axis_view.itemDelegateForColumn(axis_orientation_index)) is AxisColumnDelegate
def test_plot_style_column_delegate(qtbot):
- """ Verify the functionality of the show/hide column feature """
+ """Verify the functionality of the show/hide column feature"""
# Set up a plot with three data items. Two will be plotted as lines, and one as bars.
base_plot = BasePlot()
qtbot.addWidget(base_plot)
line_item_1 = WaveformCurveItem()
line_item_2 = WaveformCurveItem()
- bar_item = WaveformCurveItem(plot_style='Bar')
+ bar_item = WaveformCurveItem(plot_style="Bar")
plot_curves_model = BasePlotCurvesModel(plot=base_plot)
table_view = QTableView()
table_view.setModel(plot_curves_model)
- plot_style_column_delegate = PlotStyleColumnDelegate(parent=base_plot,
- table_model=plot_curves_model,
- table_view=table_view)
+ plot_style_column_delegate = PlotStyleColumnDelegate(
+ parent=base_plot, table_model=plot_curves_model, table_view=table_view
+ )
base_plot.addCurve(line_item_1)
plot_style_column_delegate.toggleColumnVisibility()
diff --git a/pydm/tests/widgets/test_display_format.py b/pydm/tests/widgets/test_display_format.py
index a2d3f01624..a2d2aed3c6 100644
--- a/pydm/tests/widgets/test_display_format.py
+++ b/pydm/tests/widgets/test_display_format.py
@@ -4,25 +4,29 @@
from qtpy.QtWidgets import QWidget
-from pydm.widgets.display_format import DisplayFormat, parse_value_for_display
+from pydm.widgets.display_format import DisplayFormat, parse_value_for_display
# --------------------
# POSITIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("value, precision, display_format, widget, expected", [
- (np.array([65, 66], dtype=np.uint8), 1, DisplayFormat.String, QWidget, "AB"),
- (np.array([65, 66, 0, 199], dtype=np.uint8), 1, DisplayFormat.String, QWidget, "AB"),
- ("abc", 0, DisplayFormat.Default, QWidget, "abc"),
- (123, 0, DisplayFormat.Default, QWidget, 123),
- (123.45, 0, DisplayFormat.Default, QWidget, 123.45),
- ("abc", 0, DisplayFormat.String, QWidget, "abc"),
- (123.45, 0, DisplayFormat.Decimal, QWidget, 123.45),
- (3.000e-02, 0, DisplayFormat.Exponential, QWidget, "3e-02"),
- (0x07FF, 0, DisplayFormat.Hex, QWidget, "0x7ff"),
- (0b1101, 0, DisplayFormat.Binary, QWidget, "0b1101"),
-])
+
+@pytest.mark.parametrize(
+ "value, precision, display_format, widget, expected",
+ [
+ (np.array([65, 66], dtype=np.uint8), 1, DisplayFormat.String, QWidget, "AB"),
+ (np.array([65, 66, 0, 199], dtype=np.uint8), 1, DisplayFormat.String, QWidget, "AB"),
+ ("abc", 0, DisplayFormat.Default, QWidget, "abc"),
+ (123, 0, DisplayFormat.Default, QWidget, 123),
+ (123.45, 0, DisplayFormat.Default, QWidget, 123.45),
+ ("abc", 0, DisplayFormat.String, QWidget, "abc"),
+ (123.45, 0, DisplayFormat.Decimal, QWidget, 123.45),
+ (3.000e-02, 0, DisplayFormat.Exponential, QWidget, "3e-02"),
+ (0x07FF, 0, DisplayFormat.Hex, QWidget, "0x7ff"),
+ (0b1101, 0, DisplayFormat.Binary, QWidget, "0b1101"),
+ ],
+)
def test_parse_value_for_display_format(value, precision, display_format, widget, expected):
"""
Test the correctness of the displayed value according to the specified value type.
@@ -44,21 +48,23 @@ def test_parse_value_for_display_format(value, precision, display_format, widget
expected : str
The expected formatted presentation of the provided value
"""
- parsed_value = parse_value_for_display(
- value, precision, display_format_type=display_format, widget=widget)
- assert(parsed_value == expected)
-
-
-@pytest.mark.parametrize("value, precision, display_format, widget, expected", [
- (123.45, 1, DisplayFormat.Default, QWidget, 123.45),
- (123.45, 1, DisplayFormat.Decimal, QWidget, 123.45),
- (3.000e-02, 2, DisplayFormat.Exponential, QWidget, "3.00e-02"),
- (3.000e-02, 3, DisplayFormat.Exponential, QWidget, "3.000e-02"),
- (1.234, 3, DisplayFormat.Hex, QWidget, "0x1"),
- (-1.234, 3, DisplayFormat.Hex, QWidget, "-0x2"),
- (1.234, 3, DisplayFormat.Binary, QWidget, "0b1"),
- (-1.234, 3, DisplayFormat.Binary, QWidget, "-0b10")
-])
+ parsed_value = parse_value_for_display(value, precision, display_format_type=display_format, widget=widget)
+ assert parsed_value == expected
+
+
+@pytest.mark.parametrize(
+ "value, precision, display_format, widget, expected",
+ [
+ (123.45, 1, DisplayFormat.Default, QWidget, 123.45),
+ (123.45, 1, DisplayFormat.Decimal, QWidget, 123.45),
+ (3.000e-02, 2, DisplayFormat.Exponential, QWidget, "3.00e-02"),
+ (3.000e-02, 3, DisplayFormat.Exponential, QWidget, "3.000e-02"),
+ (1.234, 3, DisplayFormat.Hex, QWidget, "0x1"),
+ (-1.234, 3, DisplayFormat.Hex, QWidget, "-0x2"),
+ (1.234, 3, DisplayFormat.Binary, QWidget, "0b1"),
+ (-1.234, 3, DisplayFormat.Binary, QWidget, "-0b10"),
+ ],
+)
def test_parse_value_for_display_precision(value, precision, display_format, widget, expected):
"""
Test the correctness of the displayed value according to the specified value precision.
@@ -80,23 +86,33 @@ def test_parse_value_for_display_precision(value, precision, display_format, wid
expected : str
The expected formatted presentation of the provided value
"""
- assert parse_value_for_display(
- value, precision, display_format_type=display_format, widget=widget) == expected
+ assert parse_value_for_display(value, precision, display_format_type=display_format, widget=widget) == expected
# --------------------
# NEGATIVE TEST CASES
# ---------------------
-@pytest.mark.parametrize("value, precision, display_format, widget, expected", [
- (np.array([-1, -2]), 1, DisplayFormat.String, QWidget, "Could not decode"),
- (np.array([0xfffe, 0xffff]), 1, DisplayFormat.String, QWidget, "Could not decode"),
- ("aaa", 1, DisplayFormat.Exponential, QWidget, "Could not display value 'aaa' using displayFormat 'Exponential'"),
- ("zzz", 2, DisplayFormat.Hex, QWidget, "Could not display value 'zzz' using displayFormat 'Hex'"),
- ("zzz", 3, DisplayFormat.Binary, QWidget, "Could not display value 'zzz' using displayFormat 'Binary'"),
-])
+
+@pytest.mark.parametrize(
+ "value, precision, display_format, widget, expected",
+ [
+ (np.array([-1, -2]), 1, DisplayFormat.String, QWidget, "Could not decode"),
+ (np.array([0xFFFE, 0xFFFF]), 1, DisplayFormat.String, QWidget, "Could not decode"),
+ (
+ "aaa",
+ 1,
+ DisplayFormat.Exponential,
+ QWidget,
+ "Could not display value 'aaa' using displayFormat 'Exponential'",
+ ),
+ ("zzz", 2, DisplayFormat.Hex, QWidget, "Could not display value 'zzz' using displayFormat 'Hex'"),
+ ("zzz", 3, DisplayFormat.Binary, QWidget, "Could not display value 'zzz' using displayFormat 'Binary'"),
+ ],
+)
def test_parse_value_for_display_precision_incorrect_display_format(
- caplog, value, precision, display_format, widget, expected):
+ caplog, value, precision, display_format, widget, expected
+):
"""
Test that errors will be output into stderr.
@@ -117,9 +133,9 @@ def test_parse_value_for_display_precision_incorrect_display_format(
"""
parsed_value = parse_value_for_display(value, precision, display_format_type=display_format, widget=widget)
if isinstance(value, np.ndarray):
- assert (np.array_equal(value, parsed_value))
+ assert np.array_equal(value, parsed_value)
else:
- assert(parsed_value == value)
+ assert parsed_value == value
# Make sure logging capture the error, and have the correct error message
for record in caplog.records:
diff --git a/pydm/tests/widgets/test_drawing.py b/pydm/tests/widgets/test_drawing.py
index dc03e08c5d..2b126414a7 100644
--- a/pydm/tests/widgets/test_drawing.py
+++ b/pydm/tests/widgets/test_drawing.py
@@ -6,34 +6,45 @@
from qtpy.QtGui import QColor, QBrush, QPixmap
from qtpy.QtWidgets import QApplication
-from qtpy.QtCore import Property, Qt, QPoint, QSize
+from qtpy.QtCore import Qt, QSize
from qtpy.QtDesigner import QDesignerFormWindowInterface
from ...widgets.base import PyDMWidget
-from ...widgets.drawing import (deg_to_qt, qt_to_deg, PyDMDrawing,
- PyDMDrawingLine, PyDMDrawingImage,
- PyDMDrawingRectangle, PyDMDrawingTriangle,
- PyDMDrawingEllipse,
- PyDMDrawingCircle, PyDMDrawingArc,
- PyDMDrawingPie, PyDMDrawingChord,
- PyDMDrawingPolygon, PyDMDrawingPolyline,
- PyDMDrawingIrregularPolygon)
-
-from ...utilities.stylesheet import apply_stylesheet
+from ...widgets.drawing import (
+ deg_to_qt,
+ qt_to_deg,
+ PyDMDrawing,
+ PyDMDrawingLine,
+ PyDMDrawingImage,
+ PyDMDrawingRectangle,
+ PyDMDrawingTriangle,
+ PyDMDrawingEllipse,
+ PyDMDrawingCircle,
+ PyDMDrawingArc,
+ PyDMDrawingPie,
+ PyDMDrawingChord,
+ PyDMDrawingPolygon,
+ PyDMDrawingPolyline,
+ PyDMDrawingIrregularPolygon,
+)
# --------------------
# POSITIVE TEST CASES
# --------------------
+
# # -------------
# # PyDMDrawing
# # -------------
-@pytest.mark.parametrize("deg, expected_qt_deg", [
- (0, 0),
- (1, 16),
- (-1, -16),
-])
+@pytest.mark.parametrize(
+ "deg, expected_qt_deg",
+ [
+ (0, 0),
+ (1, 16),
+ (-1, -16),
+ ],
+)
def test_deg_to_qt(deg, expected_qt_deg):
"""
Test the conversion from degrees to Qt degrees.
@@ -53,29 +64,23 @@ def test_deg_to_qt(deg, expected_qt_deg):
assert deg_to_qt(deg) == expected_qt_deg
-@pytest.mark.parametrize("qt_deg, expected_deg", [
- (0, 0),
- (16, 1),
- (-16, -1),
- (-32.0, -2),
- (16.16, 1.01)
-])
+@pytest.mark.parametrize("qt_deg, expected_deg", [(0, 0), (16, 1), (-16, -1), (-32.0, -2), (16.16, 1.01)])
def test_qt_to_deg(qt_deg, expected_deg):
"""
- Test the conversion from Qt degrees to degrees.
+ Test the conversion from Qt degrees to degrees.
- Expectations:
- The angle measurement in Qt degrees is converted correctly to degrees, which are 16 times less than the Qt degree
- value, i.e. 1 Qt degree = 1/16 degree
+ Expectations:
+ The angle measurement in Qt degrees is converted correctly to degrees, which are 16 times less than the Qt degree
+ value, i.e. 1 Qt degree = 1/16 degree
- Parameters
- ----------
- qt_deg : int, float
- The angle value in Qt degrees
+ Parameters
+ ----------
+ qt_deg : int, float
+ The angle value in Qt degrees
- expected_deg : int, floag
- The expected degrees after the conversion
- """
+ expected_deg : int, floag
+ The expected degrees after the conversion
+ """
assert qt_to_deg(qt_deg) == expected_deg
@@ -121,10 +126,13 @@ def test_pydmdrawing_sizeHint(qtbot):
assert pydm_drawing.sizeHint() == QSize(100, 100)
-@pytest.mark.parametrize("alarm_sensitive_content", [
- True,
- False,
-])
+@pytest.mark.parametrize(
+ "alarm_sensitive_content",
+ [
+ True,
+ False,
+ ],
+)
def test_pydmdrawing_paintEvent(qtbot, signals, alarm_sensitive_content):
"""
Test the paintEvent handling of the widget. This test method will also execute PyDMDrawing alarm_severity_changed
@@ -133,7 +141,8 @@ def test_pydmdrawing_paintEvent(qtbot, signals, alarm_sensitive_content):
Expectations:
The paintEvent will be triggered, and the widget's brush color is correctly set.
- NOTE: This test depends on the default stylesheet having different values for 'qproperty-brush' for different alarm states of PyDMDrawing.
+ NOTE: This test depends on the default stylesheet having different values for 'qproperty-brush' for different
+ alarm states of PyDMDrawing.
Parameters
----------
@@ -147,7 +156,7 @@ def test_pydmdrawing_paintEvent(qtbot, signals, alarm_sensitive_content):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
qtbot.addWidget(main_window)
- pydm_drawing = PyDMDrawing(parent=main_window, init_channel='fake://tst')
+ pydm_drawing = PyDMDrawing(parent=main_window, init_channel="fake://tst")
qtbot.addWidget(pydm_drawing)
pydm_drawing.alarmSensitiveContent = alarm_sensitive_content
brush_before = pydm_drawing.brush.color().name()
@@ -161,13 +170,10 @@ def test_pydmdrawing_paintEvent(qtbot, signals, alarm_sensitive_content):
assert brush_before == brush_after
-@pytest.mark.parametrize("widget_width, widget_height, expected_results", [
- (4.0, 4.0, (2.0, 2.0)),
- (1.0, 1.0, (0.5, 0.5)),
- (0, 0, (0, 0))
-])
-def test_pydmdrawing_get_center(qtbot, monkeypatch, widget_width, widget_height,
- expected_results):
+@pytest.mark.parametrize(
+ "widget_width, widget_height, expected_results", [(4.0, 4.0, (2.0, 2.0)), (1.0, 1.0, (0.5, 0.5)), (0, 0, (0, 0))]
+)
+def test_pydmdrawing_get_center(qtbot, monkeypatch, widget_width, widget_height, expected_results):
"""
Test the calculation of the widget's center from its width and height.
@@ -201,73 +207,43 @@ def test_pydmdrawing_get_center(qtbot, monkeypatch, widget_width, widget_height,
[
# Zero rotation, with typical width, height, pen_width, and variable max_size, has_border, and force_no_pen
# width > height
- (25.53, 10.35, 0.0, 2, True, True, True,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 0.0, 2, True, True, False,
- (-10.765, -3.175, 21.53, 6.35)),
- (25.53, 10.35, 0.0, 2, True, False, True,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 0.0, 2, True, False, False,
- (-10.765, -3.175, 21.53, 6.35)),
- (25.53, 10.35, 0.0, 2, False, True, True,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 0.0, 2, False, True, False,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 0.0, 2, False, False, True,
- (-12.765, -5.175, 25.53, 10.35)),
-
+ (25.53, 10.35, 0.0, 2, True, True, True, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 0.0, 2, True, True, False, (-10.765, -3.175, 21.53, 6.35)),
+ (25.53, 10.35, 0.0, 2, True, False, True, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 0.0, 2, True, False, False, (-10.765, -3.175, 21.53, 6.35)),
+ (25.53, 10.35, 0.0, 2, False, True, True, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 0.0, 2, False, True, False, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 0.0, 2, False, False, True, (-12.765, -5.175, 25.53, 10.35)),
# width < height
- (10.35, 25.53, 0.0, 2, True, True, True,
- (-5.175, -12.765, 10.35, 25.53)),
- (10.35, 25.53, 0.0, 2, True, True, False,
- (-3.175, -10.765, 6.35, 21.53)),
- (10.35, 25.53, 0.0, 2, True, False, True,
- (-5.175, -12.765, 10.35, 25.53)),
- (10.35, 25.53, 0.0, 2, True, False, False,
- (-3.175, -10.765, 6.35, 21.53)),
- (10.35, 25.53, 0.0, 2, False, True, True,
- (-5.175, -12.765, 10.35, 25.53)),
- (10.35, 25.53, 0.0, 2, False, True, False,
- (-5.175, -12.765, 10.35, 25.53)),
- (10.35, 25.53, 0.0, 2, False, False, True,
- (-5.175, -12.765, 10.35, 25.53)),
-
+ (10.35, 25.53, 0.0, 2, True, True, True, (-5.175, -12.765, 10.35, 25.53)),
+ (10.35, 25.53, 0.0, 2, True, True, False, (-3.175, -10.765, 6.35, 21.53)),
+ (10.35, 25.53, 0.0, 2, True, False, True, (-5.175, -12.765, 10.35, 25.53)),
+ (10.35, 25.53, 0.0, 2, True, False, False, (-3.175, -10.765, 6.35, 21.53)),
+ (10.35, 25.53, 0.0, 2, False, True, True, (-5.175, -12.765, 10.35, 25.53)),
+ (10.35, 25.53, 0.0, 2, False, True, False, (-5.175, -12.765, 10.35, 25.53)),
+ (10.35, 25.53, 0.0, 2, False, False, True, (-5.175, -12.765, 10.35, 25.53)),
# width == height
- (
- 10.35, 10.35, 0.0, 2, True, True, True, (-5.175, -5.175, 10.35, 10.35)),
+ (10.35, 10.35, 0.0, 2, True, True, True, (-5.175, -5.175, 10.35, 10.35)),
(10.35, 10.35, 0.0, 2, True, True, False, (-3.175, -3.175, 6.35, 6.35)),
- (10.35, 10.35, 0.0, 2, True, False, True,
- (-5.175, -5.175, 10.35, 10.35)),
- (
- 10.35, 10.35, 0.0, 2, True, False, False, (-3.175, -3.175, 6.35, 6.35)),
- (10.35, 10.35, 0.0, 2, False, True, True,
- (-5.175, -5.175, 10.35, 10.35)),
- (10.35, 10.35, 0.0, 2, False, True, False,
- (-5.175, -5.175, 10.35, 10.35)),
- (10.35, 10.35, 0.0, 2, False, False, True,
- (-5.175, -5.175, 10.35, 10.35)),
-
+ (10.35, 10.35, 0.0, 2, True, False, True, (-5.175, -5.175, 10.35, 10.35)),
+ (10.35, 10.35, 0.0, 2, True, False, False, (-3.175, -3.175, 6.35, 6.35)),
+ (10.35, 10.35, 0.0, 2, False, True, True, (-5.175, -5.175, 10.35, 10.35)),
+ (10.35, 10.35, 0.0, 2, False, True, False, (-5.175, -5.175, 10.35, 10.35)),
+ (10.35, 10.35, 0.0, 2, False, False, True, (-5.175, -5.175, 10.35, 10.35)),
# Variable rotation, max_size, and force_no_pen, has_border is True
- (25.53, 10.35, 45.0, 2, True, True, True,
- (-5.207, -2.111, 10.415, 4.222)),
- (25.53, 10.35, 145.0, 2, True, True, True,
- (-5.714, -2.316, 11.428, 4.633)),
- (25.53, 10.35, 90.0, 2, True, True, False,
- (-3.175, -0.098, 6.35, 0.196)),
- (25.53, 10.35, 180.0, 2, True, False, True,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 270.0, 2, True, False, False,
- (-10.765, -3.175, 21.53, 6.35)),
- (25.53, 10.35, 360.0, 2, False, True, True,
- (-12.765, -5.175, 25.53, 10.35)),
- (25.53, 10.35, 0.72, 2, False, True, False,
- (-12.382, -5.02, 24.764, 10.04)),
- (25.53, 10.35, 71.333, 2, False, False, True,
- (-12.765, -5.175, 25.53, 10.35)),
- ])
-def test_pydmdrawing_get_bounds(qtbot, monkeypatch, width, height, rotation_deg,
- pen_width, has_border, max_size,
- force_no_pen, expected):
+ (25.53, 10.35, 45.0, 2, True, True, True, (-5.207, -2.111, 10.415, 4.222)),
+ (25.53, 10.35, 145.0, 2, True, True, True, (-5.714, -2.316, 11.428, 4.633)),
+ (25.53, 10.35, 90.0, 2, True, True, False, (-3.175, -0.098, 6.35, 0.196)),
+ (25.53, 10.35, 180.0, 2, True, False, True, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 270.0, 2, True, False, False, (-10.765, -3.175, 21.53, 6.35)),
+ (25.53, 10.35, 360.0, 2, False, True, True, (-12.765, -5.175, 25.53, 10.35)),
+ (25.53, 10.35, 0.72, 2, False, True, False, (-12.382, -5.02, 24.764, 10.04)),
+ (25.53, 10.35, 71.333, 2, False, False, True, (-12.765, -5.175, 25.53, 10.35)),
+ ],
+)
+def test_pydmdrawing_get_bounds(
+ qtbot, monkeypatch, width, height, rotation_deg, pen_width, has_border, max_size, force_no_pen, expected
+):
"""
Test the useful area calculations and compare the resulted tuple to the expected one.
@@ -302,19 +278,21 @@ def test_pydmdrawing_get_bounds(qtbot, monkeypatch, width, height, rotation_deg,
monkeypatch.setattr(PyDMDrawing, "has_border", lambda *args: False)
calculated_bounds = pydm_drawing.get_bounds(max_size, force_no_pen)
- calculated_bounds = tuple(
- [round(x, 3) if isinstance(x, float) else x for x in calculated_bounds])
+ calculated_bounds = tuple([round(x, 3) if isinstance(x, float) else x for x in calculated_bounds])
assert calculated_bounds == expected
-@pytest.mark.parametrize("pen_style, pen_width, expected_result", [
- (Qt.NoPen, 0, False),
- (Qt.NoPen, 1, False),
- (Qt.SolidLine, 0, False),
- (Qt.DashLine, 0, False),
- (Qt.SolidLine, 1, True),
- (Qt.DashLine, 10, True)
-])
+@pytest.mark.parametrize(
+ "pen_style, pen_width, expected_result",
+ [
+ (Qt.NoPen, 0, False),
+ (Qt.NoPen, 1, False),
+ (Qt.SolidLine, 0, False),
+ (Qt.DashLine, 0, False),
+ (Qt.SolidLine, 1, True),
+ (Qt.DashLine, 10, True),
+ ],
+)
def test_pydmdrawing_has_border(qtbot, pen_style, pen_width, expected_result):
"""
Test the determination whether the widget will be drawn with a border, taking into account the pen style and width
@@ -342,16 +320,18 @@ def test_pydmdrawing_has_border(qtbot, pen_style, pen_width, expected_result):
assert pydm_drawing.has_border() == expected_result
-@pytest.mark.parametrize("width, height, expected_result", [
- (10, 15, False),
- (10.5, 22.333, False),
- (-10.333, -10.332, False),
- (10.333, 10.333, True),
- (-20.777, -20.777, True),
- (70, 70, True),
-])
-def test_pydmdrawing_is_square(qtbot, monkeypatch, width, height,
- expected_result):
+@pytest.mark.parametrize(
+ "width, height, expected_result",
+ [
+ (10, 15, False),
+ (10.5, 22.333, False),
+ (-10.333, -10.332, False),
+ (10.333, 10.333, True),
+ (-20.777, -20.777, True),
+ (70, 70, True),
+ ],
+)
+def test_pydmdrawing_is_square(qtbot, monkeypatch, width, height, expected_result):
"""
Check if the widget has the same width and height values.
@@ -380,18 +360,20 @@ def test_pydmdrawing_is_square(qtbot, monkeypatch, width, height,
assert pydm_drawing.is_square() == expected_result
-@pytest.mark.parametrize("width, height, rotation_deg, expected", [
- (25.53, 10.35, 0.0, (25.53, 10.35)),
- (10.35, 25.53, 0.0, (10.35, 25.53)),
- (25.53, 10.35, 45.0, (10.415, 4.222)),
- (10.35, 25.53, 45.0, (4.222, 10.415)),
- (10.35, 25.53, 360.0, (10.35, 25.53)),
- (10.35, 25.53, -45.0, (4.222, 10.415)),
- (10.35, 25.53, -270.0, (4.196, 10.35)),
- (10.35, 25.53, -360.0, (10.35, 25.53)),
-])
-def test_pydmdrawing_get_inner_max(qtbot, monkeypatch, width, height,
- rotation_deg, expected):
+@pytest.mark.parametrize(
+ "width, height, rotation_deg, expected",
+ [
+ (25.53, 10.35, 0.0, (25.53, 10.35)),
+ (10.35, 25.53, 0.0, (10.35, 25.53)),
+ (25.53, 10.35, 45.0, (10.415, 4.222)),
+ (10.35, 25.53, 45.0, (4.222, 10.415)),
+ (10.35, 25.53, 360.0, (10.35, 25.53)),
+ (10.35, 25.53, -45.0, (4.222, 10.415)),
+ (10.35, 25.53, -270.0, (4.196, 10.35)),
+ (10.35, 25.53, -360.0, (10.35, 25.53)),
+ ],
+)
+def test_pydmdrawing_get_inner_max(qtbot, monkeypatch, width, height, rotation_deg, expected):
"""
Test the calculation of the inner rectangle in a rotated rectangle.
@@ -421,9 +403,7 @@ def test_pydmdrawing_get_inner_max(qtbot, monkeypatch, width, height,
monkeypatch.setattr(PyDMDrawing, "height", lambda *args: height)
calculated_inner_max = pydm_drawing.get_inner_max()
- calculated_inner_max = tuple(
- [round(x, 3) if isinstance(x, float) else x for x in
- calculated_inner_max])
+ calculated_inner_max = tuple([round(x, 3) if isinstance(x, float) else x for x in calculated_inner_max])
assert calculated_inner_max == expected
@@ -477,10 +457,13 @@ def test_pydmdrawing_properties_and_setters(qtbot):
# # ----------------
# # PyDMDrawingLine
# # ----------------
-@pytest.mark.parametrize("alarm_sensitive_content", [
- True,
- False,
-])
+@pytest.mark.parametrize(
+ "alarm_sensitive_content",
+ [
+ True,
+ False,
+ ],
+)
def test_pydmdrawingline_draw_item(qtbot, signals, alarm_sensitive_content):
"""
Test PyDMDrawingLine base class drawing handling.
@@ -498,7 +481,7 @@ def test_pydmdrawingline_draw_item(qtbot, signals, alarm_sensitive_content):
alarm_sensitive_content : bool
True if the widget will be redraw with a different color if an alarm is triggered; False otherwise
"""
- pydm_drawingline = PyDMDrawingLine(init_channel='fake://tst')
+ pydm_drawingline = PyDMDrawingLine(init_channel="fake://tst")
qtbot.addWidget(pydm_drawingline)
pydm_drawingline.alarmSensitiveContent = alarm_sensitive_content
@@ -539,8 +522,7 @@ def test_pydmdrawingimage_construct(qtbot):
assert pydm_drawingimage.filename == ""
base_path = os.path.dirname(__file__)
- test_file = os.path.join(base_path, '..', '..', '..', 'examples', 'drawing',
- 'SLAC_logo.jpeg')
+ test_file = os.path.join(base_path, "..", "..", "..", "examples", "drawing", "SLAC_logo.jpeg")
pydm_drawingimage2 = PyDMDrawingImage(filename=test_file)
qtbot.addWidget(pydm_drawingimage2)
@@ -550,8 +532,7 @@ def test_pydmdrawingimage_construct(qtbot):
pydm_drawingimage4 = PyDMDrawingImage(filename="foo")
qtbot.addWidget(pydm_drawingimage4)
- test_gif = os.path.join(base_path, '..', '..', '..', 'examples', 'drawing',
- 'test.gif')
+ test_gif = os.path.join(base_path, "..", "..", "..", "examples", "drawing", "test.gif")
pydm_drawingimage5 = PyDMDrawingImage(filename=test_gif)
pydm_drawingimage5.movie_finished()
qtbot.addWidget(pydm_drawingimage5)
@@ -603,10 +584,13 @@ def test_pydmdrawingimage_test_properties_and_setters(qtbot):
assert pydm_drawingimage.aspectRatioMode == Qt.KeepAspectRatioByExpanding
-@pytest.mark.parametrize("is_pixmap_empty", [
- True,
- False,
-])
+@pytest.mark.parametrize(
+ "is_pixmap_empty",
+ [
+ True,
+ False,
+ ],
+)
def test_pydmdrawingimage_size_hint(qtbot, monkeypatch, is_pixmap_empty):
"""
Test the size hint of a PyDMDrawingImage object.
@@ -633,20 +617,21 @@ def test_pydmdrawingimage_size_hint(qtbot, monkeypatch, is_pixmap_empty):
monkeypatch.setattr(QPixmap, "size", lambda *args: QSize(125, 125))
size_hint = pydm_drawingimage.sizeHint()
- assert size_hint == QSize(100,
- 100) if is_pixmap_empty else size_hint == pydm_drawingimage._pixmap.size()
+ assert size_hint == QSize(100, 100) if is_pixmap_empty else size_hint == pydm_drawingimage._pixmap.size()
-@pytest.mark.parametrize("width, height, pen_width", [
- (7.7, 10.2, 0),
- (10.2, 7.7, 0),
- (5.0, 5.0, 0),
- (10.25, 10.25, 1.5),
- (10.25, 100.0, 5.125),
- (100.0, 10.25, 5.125),
-])
-def test_pydmdrawingimage_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width):
+@pytest.mark.parametrize(
+ "width, height, pen_width",
+ [
+ (7.7, 10.2, 0),
+ (10.2, 7.7, 0),
+ (5.0, 5.0, 0),
+ (10.25, 10.25, 1.5),
+ (10.25, 100.0, 5.125),
+ (100.0, 10.25, 5.125),
+ ],
+)
+def test_pydmdrawingimage_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width):
"""
Test the rendering of a PyDMDrawingImage object.
@@ -680,16 +665,18 @@ def test_pydmdrawingimage_draw_item(qapp, qtbot, monkeypatch, width, height,
# # ---------------------
# # PyDMDrawingRectangle
# # ---------------------
-@pytest.mark.parametrize("width, height, pen_width", [
- (7.7, 10.2, 0),
- (10.2, 7.7, 0),
- (5.0, 5.0, 0),
- (10.25, 10.25, 1.5),
- (10.25, 100.0, 5.125),
- (100.0, 10.25, 5.125),
-])
-def test_pydmdrawingrectangle_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width):
+@pytest.mark.parametrize(
+ "width, height, pen_width",
+ [
+ (7.7, 10.2, 0),
+ (10.2, 7.7, 0),
+ (5.0, 5.0, 0),
+ (10.25, 10.25, 1.5),
+ (10.25, 100.0, 5.125),
+ (100.0, 10.25, 5.125),
+ ],
+)
+def test_pydmdrawingrectangle_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width):
"""
Test the rendering of a PyDMDrawingRectangle object.
@@ -723,16 +710,18 @@ def test_pydmdrawingrectangle_draw_item(qapp, qtbot, monkeypatch, width, height,
# # ---------------------
# # PyDMDrawingTriangle
# # ---------------------
-@pytest.mark.parametrize("width, height, pen_width", [
- (7.7, 10.2, 0),
- (10.2, 7.7, 0),
- (5.0, 5.0, 0),
- (10.25, 10.25, 1.5),
- (10.25, 100.0, 5.125),
- (100.0, 10.25, 5.125),
-])
-def test_pydmdrawingtriangle_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width):
+@pytest.mark.parametrize(
+ "width, height, pen_width",
+ [
+ (7.7, 10.2, 0),
+ (10.2, 7.7, 0),
+ (5.0, 5.0, 0),
+ (10.25, 10.25, 1.5),
+ (10.25, 100.0, 5.125),
+ (100.0, 10.25, 5.125),
+ ],
+)
+def test_pydmdrawingtriangle_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width):
"""
Test the rendering of a PyDMDrawingTriangle object.
@@ -762,16 +751,19 @@ def test_pydmdrawingtriangle_draw_item(qapp, qtbot, monkeypatch, width, height,
pydm_drawingtriangle.show()
+
# # -------------------
# # PyDMDrawingEllipse
# # -------------------
-@pytest.mark.parametrize("width, height, pen_width", [
- (5.0, 5.0, 0),
- (10.25, 10.25, 1.5),
- (10.25, 100.0, 5.125),
-])
-def test_pydmdrawingellipse_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width):
+@pytest.mark.parametrize(
+ "width, height, pen_width",
+ [
+ (5.0, 5.0, 0),
+ (10.25, 10.25, 1.5),
+ (10.25, 100.0, 5.125),
+ ],
+)
+def test_pydmdrawingellipse_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width):
"""
Test the rendering of a PyDMDrawingEllipse object.
@@ -805,13 +797,15 @@ def test_pydmdrawingellipse_draw_item(qapp, qtbot, monkeypatch, width, height,
# # ------------------
# # PyDMDrawingCircle
# # ------------------
-@pytest.mark.parametrize("width, height, expected_radius", [
- (5.0, 5.0, 2.5),
- (10.25, 10.25, 5.125),
- (10.25, 100.0, 5.125),
-])
-def test_pydmdrawingcircle_calculate_radius(qtbot, width, height,
- expected_radius):
+@pytest.mark.parametrize(
+ "width, height, expected_radius",
+ [
+ (5.0, 5.0, 2.5),
+ (10.25, 10.25, 5.125),
+ (10.25, 100.0, 5.125),
+ ],
+)
+def test_pydmdrawingcircle_calculate_radius(qtbot, width, height, expected_radius):
"""
Test the calculation of a PyDMDrawingCircle's radius.
@@ -836,13 +830,15 @@ def test_pydmdrawingcircle_calculate_radius(qtbot, width, height,
assert calculated_radius == expected_radius
-@pytest.mark.parametrize("width, height, pen_width", [
- (5.0, 5.0, 0),
- (10.25, 10.25, 1.5),
- (10.25, 100.0, 5.125),
-])
-def test_pydmdrawingcircle_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width):
+@pytest.mark.parametrize(
+ "width, height, pen_width",
+ [
+ (5.0, 5.0, 0),
+ (10.25, 10.25, 1.5),
+ (10.25, 100.0, 5.125),
+ ],
+)
+def test_pydmdrawingcircle_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width):
"""
Test the rendering of a PyDMDrawingCircle object.
@@ -897,17 +893,19 @@ def test_pydmdrawingarc_construct(qtbot):
assert pydm_drawingarc._span_angle == deg_to_qt(90)
-@pytest.mark.parametrize("width, height, start_angle_deg, span_angle_deg", [
- (10.333, 11.777, 0, 0),
- (10.333, 10.333, 0, 0),
- (10.333, 10.333, 0, 45),
- (10.333, 11.777, 0, 45),
- (10.333, 11.777, 0, -35),
- (10.333, 11.777, 11, 45),
- (10.333, 11.777, -11, -25),
-])
-def test_pydmdrawingarc_draw_item(qapp, qtbot, monkeypatch, width, height,
- start_angle_deg, span_angle_deg):
+@pytest.mark.parametrize(
+ "width, height, start_angle_deg, span_angle_deg",
+ [
+ (10.333, 11.777, 0, 0),
+ (10.333, 10.333, 0, 0),
+ (10.333, 10.333, 0, 45),
+ (10.333, 11.777, 0, 45),
+ (10.333, 11.777, 0, -35),
+ (10.333, 11.777, 11, 45),
+ (10.333, 11.777, -11, -25),
+ ],
+)
+def test_pydmdrawingarc_draw_item(qapp, qtbot, monkeypatch, width, height, start_angle_deg, span_angle_deg):
"""
Test the rendering of a PyDMDrawingArc object.
@@ -948,7 +946,8 @@ def test_pydmdrawingarc_draw_item(qapp, qtbot, monkeypatch, width, height,
# # PyDMDrawingPie
# # ---------------
@pytest.mark.parametrize(
- "width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg", [
+ "width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg",
+ [
(10.333, 11.777, 0, 0, 0, 0),
(10.333, 10.333, 0, 0, 0, 0),
(10.333, 11.777, 0, 0, 0, 45),
@@ -958,10 +957,11 @@ def test_pydmdrawingarc_draw_item(qapp, qtbot, monkeypatch, width, height,
(10.333, 11.777, 3, 15.333, 0, -35),
(10.333, 11.777, 3, 15.333, 11, 45),
(10.333, 11.777, 3, 15.333, -11, -25),
- ])
-def test_pydmdrawingpie_draw_item(qapp, qtbot, monkeypatch, width, height, pen_width,
- rotation_deg, start_angle_deg,
- span_angle_deg):
+ ],
+)
+def test_pydmdrawingpie_draw_item(
+ qapp, qtbot, monkeypatch, width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg
+):
"""
Test the rendering of a PyDMDrawingPie object.
@@ -1005,7 +1005,8 @@ def test_pydmdrawingpie_draw_item(qapp, qtbot, monkeypatch, width, height, pen_w
# # PyDMDrawingChord
# # -----------------
@pytest.mark.parametrize(
- "width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg", [
+ "width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg",
+ [
(10.333, 11.777, 0, 0, 0, 0),
(10.333, 10.333, 0, 0, 0, 0),
(10.333, 11.777, 0, 0, 0, 45),
@@ -1015,10 +1016,11 @@ def test_pydmdrawingpie_draw_item(qapp, qtbot, monkeypatch, width, height, pen_w
(10.333, 11.777, 3, 15.333, 0, -35),
(10.333, 11.777, 3, 15.333, 11, 45),
(10.333, 11.777, 3, 15.333, -11, -25),
- ])
-def test_pydmdrawingchord_draw_item(qapp, qtbot, monkeypatch, width, height,
- pen_width, rotation_deg, start_angle_deg,
- span_angle_deg):
+ ],
+)
+def test_pydmdrawingchord_draw_item(
+ qapp, qtbot, monkeypatch, width, height, pen_width, rotation_deg, start_angle_deg, span_angle_deg
+):
"""
Test the rendering of a PyDMDrawingChord object.
@@ -1057,16 +1059,18 @@ def test_pydmdrawingchord_draw_item(qapp, qtbot, monkeypatch, width, height,
pydm_drawingchord.show()
+
# # ---------------------
# # PyDMDrawingPolygon
# # ---------------------
-@pytest.mark.parametrize("x, y, width, height, num_points, expected_points", [
- (0, 0, 100, 100, 3, [(50.0, 0),(-25, 43.3012),(-25, -43.3012)]),
- (0, 0, 100, 100, 4, [(50.0, 0), (0, 50.0), (-50.0, 0), (0, -50.0)])
-])
-def test_pydmdrawingpolygon_calculate_drawing_points(qapp, qtbot, x, y, width,
- height, num_points,
- expected_points):
+@pytest.mark.parametrize(
+ "x, y, width, height, num_points, expected_points",
+ [
+ (0, 0, 100, 100, 3, [(50.0, 0), (-25, 43.3012), (-25, -43.3012)]),
+ (0, 0, 100, 100, 4, [(50.0, 0), (0, 50.0), (-50.0, 0), (0, -50.0)]),
+ ],
+)
+def test_pydmdrawingpolygon_calculate_drawing_points(qapp, qtbot, x, y, width, height, num_points, expected_points):
"""
Test the calculations of the point coordinates of a PyDMDrawingTriangle widget.
@@ -1097,9 +1101,7 @@ def test_pydmdrawingpolygon_calculate_drawing_points(qapp, qtbot, x, y, width,
assert drawing.numberOfPoints == num_points
- calculated_points = drawing._calculate_drawing_points(x, y,
- width,
- height)
+ calculated_points = drawing._calculate_drawing_points(x, y, width, height)
for idx, p in enumerate(calculated_points):
assert p.x() == pytest.approx(expected_points[idx][0], 0.1)
@@ -1107,16 +1109,15 @@ def test_pydmdrawingpolygon_calculate_drawing_points(qapp, qtbot, x, y, width,
drawing.show()
+
# # ---------------------
# # PyDMDrawingPolyline
# # ---------------------
-@pytest.mark.parametrize("x, y, width, height, num_points, expected_points", [
- (-1, 27, 389, 3, 2, [(-2, -2),(384, -2)]),
- (301, 230, 99, 20, 3, [(-1, 18),(-1, -1),(97, -1)])
-])
-def test_pydmdrawingpolyline_getpoints(qapp, qtbot, x, y, width,
- height, num_points,
- expected_points):
+@pytest.mark.parametrize(
+ "x, y, width, height, num_points, expected_points",
+ [(-1, 27, 389, 3, 2, [(-2, -2), (384, -2)]), (301, 230, 99, 20, 3, [(-1, 18), (-1, -1), (97, -1)])],
+)
+def test_pydmdrawingpolyline_getpoints(qapp, qtbot, x, y, width, height, num_points, expected_points):
"""
Test the calculations of the point coordinates of a PyDMDrawingPolyline widget.
@@ -1156,18 +1157,28 @@ def test_pydmdrawingpolyline_getpoints(qapp, qtbot, x, y, width,
drawing.show()
-@pytest.mark.parametrize("width, height, points, num_points", [
- (99, 20, ["-1, 18", "-1, -1", "97, -1"], 3),
- (99, 20, ["-1, 18", "-1, -1", "97, -1", "", " ", "a b"], 3),
- (99, 20, [[-1, 18], (-1, -1), [97, -1]], 3),
- (99, 20, [[-1, 18], (-1, -1)], 2),
- (99, 20, [[-1, 18], (-1, -1.1)], 2),
- (99, 20, [[-1, 18], "-1, -1.1"], 2),
- (99, 20, [[-1, 18], "-1, -1.1", "5"], 2),
- (99, 20, [[-1, 18], "-1, -1.1", ""], 2),
- (99, 20, [[-1, 18], "-1, -1.1", " "], 2),
- (99, 20, [[-1, 18],], 0),
-])
+@pytest.mark.parametrize(
+ "width, height, points, num_points",
+ [
+ (99, 20, ["-1, 18", "-1, -1", "97, -1"], 3),
+ (99, 20, ["-1, 18", "-1, -1", "97, -1", "", " ", "a b"], 3),
+ (99, 20, [[-1, 18], (-1, -1), [97, -1]], 3),
+ (99, 20, [[-1, 18], (-1, -1)], 2),
+ (99, 20, [[-1, 18], (-1, -1.1)], 2),
+ (99, 20, [[-1, 18], "-1, -1.1"], 2),
+ (99, 20, [[-1, 18], "-1, -1.1", "5"], 2),
+ (99, 20, [[-1, 18], "-1, -1.1", ""], 2),
+ (99, 20, [[-1, 18], "-1, -1.1", " "], 2),
+ (
+ 99,
+ 20,
+ [
+ [-1, 18],
+ ],
+ 0,
+ ),
+ ],
+)
def test_pydmdrawingpolyline_setpoints(qapp, qtbot, monkeypatch, width, height, points, num_points):
"""
Test the rendering of a PyDMDrawingPolyline widget.
@@ -1202,22 +1213,64 @@ def test_pydmdrawingpolyline_setpoints(qapp, qtbot, monkeypatch, width, height,
drawing.show()
+@pytest.mark.parametrize("points, num_points", [([[-1, 18], [-1, -1], [97, -1]], 3), ([[-1, 18], [-1, -1]], 2)])
+def test_pydmdrawingpolyline_arrows(qapp, qtbot, points, num_points):
+ """
+ Test the rendering of a PyDMDrawingPolyline widget with arrow options enabled.
+
+ Expectations:
+ The drawing of the widget takes place without any problems.
+
+ Parameters
+ ----------
+ qtbot : fixture
+ Window for widget testing
+ points : [(float, float)]
+ Requested vertices of the polygon.
+ num_points :int
+ The actual number of vertices of the polygon.
+ """
+ drawing = PyDMDrawingPolyline()
+ qtbot.addWidget(drawing)
+
+ # make sure points seutp correctly before drawing arrows
+ drawing.setPoints(points)
+ assert len(drawing.getPoints()) == num_points
+
+ # enable all arrow options
+ drawing._arrow_end_point_selection = True
+ drawing._arrow_start_point_selection = True
+ drawing._arrow_mid_point_selection = True
+ drawing._arrow_mid_point_flipped = True
+ drawing.draw_item(drawing._painter)
+
+ drawing.show()
+
+
# # ---------------------------
# # PyDMDrawingIrregularPolygon
# # ---------------------------
-@pytest.mark.parametrize("num_points, points", [
- (3, [(-2, -2), (384, -2)]),
- (3, [(-2, -2), (384, -2), (-2, -2)]),
- (4, [(-1, 18), (-1, -1), (97, -1)]),
- (4, [(-1, 18), (-1, -1), "97, -1"]),
- (4, [(-1, 18), (-1, -1), "97, -1", "-1, 18"]),
- (4, [(-1, 18), (-1, -1), "97, -1", "-1 18"]),
- (4, [(-1, 18), (-1, -1), "97, -1", "-1 18", (-1, 18)]),
- (4, [(-1, 18), (-1, -1), "97, -1", "5"]),
- (4, [(-1, 18), (-1, -1), "97, -1", ""]),
- (4, [(-1, 18), (-1, -1), "97, -1", " "]),
- (None, [(-2, -2), ]),
-])
+@pytest.mark.parametrize(
+ "num_points, points",
+ [
+ (3, [(-2, -2), (384, -2)]),
+ (3, [(-2, -2), (384, -2), (-2, -2)]),
+ (4, [(-1, 18), (-1, -1), (97, -1)]),
+ (4, [(-1, 18), (-1, -1), "97, -1"]),
+ (4, [(-1, 18), (-1, -1), "97, -1", "-1, 18"]),
+ (4, [(-1, 18), (-1, -1), "97, -1", "-1 18"]),
+ (4, [(-1, 18), (-1, -1), "97, -1", "-1 18", (-1, 18)]),
+ (4, [(-1, 18), (-1, -1), "97, -1", "5"]),
+ (4, [(-1, 18), (-1, -1), "97, -1", ""]),
+ (4, [(-1, 18), (-1, -1), "97, -1", " "]),
+ (
+ None,
+ [
+ (-2, -2),
+ ],
+ ),
+ ],
+)
def test_pydmdrawingirregularpolygon_get_set_resetpoints(qapp, qtbot, num_points, points):
"""
Test the calculations of the point coordinates of a PyDMDrawingIrregularPolygon widget.
@@ -1257,19 +1310,22 @@ def test_pydmdrawingirregularpolygon_get_set_resetpoints(qapp, qtbot, num_points
# NEGATIVE TEST CASES
# --------------------
+
# # -------------
# # PyDMDrawing
# # -------------
-@pytest.mark.parametrize("width, height, rotation_deg", [
- (0, 10.35, 0.0),
- (10.35, 0, 0.0),
- (0, 0, 45.0),
- (-10.5, 10.35, 15.0),
- (10.35, -5, 17.5),
- (-10.7, -10, 45.50),
-])
-def test_get_inner_max_neg(qtbot, monkeypatch, caplog, width, height,
- rotation_deg):
+@pytest.mark.parametrize(
+ "width, height, rotation_deg",
+ [
+ (0, 10.35, 0.0),
+ (10.35, 0, 0.0),
+ (0, 0, 45.0),
+ (-10.5, 10.35, 15.0),
+ (10.35, -5, 17.5),
+ (-10.7, -10, 45.50),
+ ],
+)
+def test_get_inner_max_neg(qtbot, monkeypatch, caplog, width, height, rotation_deg):
"""
Test the handling of invalid width and/or height value during the inner rectangle calculations.
diff --git a/pydm/tests/widgets/test_embedded_display.py b/pydm/tests/widgets/test_embedded_display.py
index b50ccf9448..3bb647874e 100644
--- a/pydm/tests/widgets/test_embedded_display.py
+++ b/pydm/tests/widgets/test_embedded_display.py
@@ -4,8 +4,9 @@
from qtpy.QtWidgets import QApplication
test_ui_path_with_relative_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../test_data", "test_relative_filename_parent.ui")
+ os.path.dirname(os.path.realpath(__file__)), "../test_data", "test_relative_filename_parent.ui"
+)
+
def test_show_with_relative_filename(qtbot):
QApplication.instance().make_main_window()
@@ -14,18 +15,24 @@ def test_show_with_relative_filename(qtbot):
main_window.setWindowTitle("Embedded Display Test")
qtbot.addWidget(main_window)
display = main_window.home_widget.embeddedDisplay
+
# Default behavior should be to not follow symlinks (for backwards compat.).
# Same effect as: display.followSymlinks = False
def check_embed():
assert display.embedded_widget is not None
+
qtbot.waitUntil(check_embed)
-@pytest.mark.skipif(sys.platform == "win32" and sys.version_info < (3, 8), reason="os.path.realpath on Python 3.7 and prior does not resolve symlinks on Windows")
+
+@pytest.mark.skipif(
+ sys.platform == "win32" and sys.version_info < (3, 8),
+ reason="os.path.realpath on Python 3.7 and prior does not resolve symlinks on Windows",
+)
def test_show_with_relative_filename_and_symlink(qtbot, tmp_path):
symlinked_ui_file = tmp_path / "test_ui_with_relative_path.ui"
try:
os.symlink(test_ui_path_with_relative_path, symlinked_ui_file)
- except:
+ except Exception:
pytest.skip("Unable to create a symlink for testing purposes.")
QApplication.instance().make_main_window()
@@ -35,6 +42,8 @@ def test_show_with_relative_filename_and_symlink(qtbot, tmp_path):
qtbot.addWidget(main_window)
display = main_window.home_widget.embeddedDisplay
display.followSymlinks = True
+
def check_embed():
assert display.embedded_widget is not None
+
qtbot.waitUntil(check_embed)
diff --git a/pydm/tests/widgets/test_enum_button.py b/pydm/tests/widgets/test_enum_button.py
index 621229e898..cbd420285f 100644
--- a/pydm/tests/widgets/test_enum_button.py
+++ b/pydm/tests/widgets/test_enum_button.py
@@ -27,10 +27,7 @@ def test_construct(qtbot):
assert widget.minimumSizeHint() == QSize(50, 100)
-@pytest.mark.parametrize("widget_type", [
- WidgetType.PushButton,
- WidgetType.RadioButton
-])
+@pytest.mark.parametrize("widget_type", [WidgetType.PushButton, WidgetType.RadioButton])
def test_widget_type(qtbot, widget_type):
"""
Test the widget for a change in the widget type.
@@ -53,10 +50,7 @@ def test_widget_type(qtbot, widget_type):
assert isinstance(widget._widgets[0], class_for_type[widget_type])
-@pytest.mark.parametrize("orientation", [
- Qt.Horizontal,
- Qt.Vertical
-])
+@pytest.mark.parametrize("orientation", [Qt.Horizontal, Qt.Vertical])
def test_widget_orientation(qtbot, orientation):
"""
Test the widget for a change in the orientation.
@@ -91,33 +85,28 @@ def test_widget_orientation(qtbot, orientation):
assert isinstance(w, class_for_type[widget.widgetType])
-@pytest.mark.parametrize("connected, write_access, has_enum, is_app_read_only", [
- (True, True, True, True),
- (True, True, True, False),
-
- (True, True, False, True),
- (True, True, False, False),
-
- (True, False, False, True),
- (True, False, False, False),
-
- (True, False, True, True),
- (True, False, True, False),
-
- (False, True, True, True),
- (False, True, True, False),
-
- (False, False, True, True),
- (False, False, True, False),
-
- (False, True, False, True),
- (False, True, False, False),
-
- (False, False, False, True),
- (False, False, False, False),
-])
-def test_check_enable_state(qtbot, connected, write_access, has_enum,
- is_app_read_only):
+@pytest.mark.parametrize(
+ "connected, write_access, has_enum, is_app_read_only",
+ [
+ (True, True, True, True),
+ (True, True, True, False),
+ (True, True, False, True),
+ (True, True, False, False),
+ (True, False, False, True),
+ (True, False, False, False),
+ (True, False, True, True),
+ (True, False, True, False),
+ (False, True, True, True),
+ (False, True, True, False),
+ (False, False, True, True),
+ (False, False, True, False),
+ (False, True, False, True),
+ (False, True, False, False),
+ (False, False, False, True),
+ (False, False, False, False),
+ ],
+)
+def test_check_enable_state(qtbot, connected, write_access, has_enum, is_app_read_only):
"""
Test the tooltip generated depending on the channel connection, write access,
whether the widget has enum strings,
@@ -226,7 +215,7 @@ def test_enum_strings_signal_alters_items_prop(qtbot, signals):
assert len(widget._btn_group.buttons()) == 2
assert widget._btn_group.button(0).text() == "PLAY"
assert widget._btn_group.button(1).text() == "PAUSE"
- signals.enum_strings_signal[tuple].emit(("STOP", ))
+ signals.enum_strings_signal[tuple].emit(("STOP",))
assert len(widget._btn_group.buttons()) == 1
assert widget._btn_group.button(0).text() == "STOP"
diff --git a/pydm/tests/widgets/test_enum_combo_box.py b/pydm/tests/widgets/test_enum_combo_box.py
index df114fd76f..a884dbf8fa 100644
--- a/pydm/tests/widgets/test_enum_combo_box.py
+++ b/pydm/tests/widgets/test_enum_combo_box.py
@@ -3,7 +3,7 @@
import pytest
from logging import ERROR
-from qtpy.QtCore import Slot, Qt
+from qtpy.QtCore import Qt
from ...widgets.enum_combo_box import PyDMEnumComboBox
from ... import data_plugins
@@ -13,6 +13,7 @@
# POSITIVE TEST CASES
# --------------------
+
def test_construct(qtbot):
"""
Test the construction of the widget.
@@ -33,11 +34,14 @@ def test_construct(qtbot):
assert pydm_enumcombobox.contextMenuEvent == pydm_enumcombobox.open_context_menu
-@pytest.mark.parametrize("enums", [
- ("spam", "eggs", "ham"),
- ("spam",),
- ("",),
-])
+@pytest.mark.parametrize(
+ "enums",
+ [
+ ("spam", "eggs", "ham"),
+ ("spam",),
+ ("",),
+ ],
+)
def test_set_items(qtbot, enums):
"""
Test the populating of enum string (choices) to the widget.
@@ -64,31 +68,27 @@ def test_set_items(qtbot, enums):
assert pydm_enumcombobox._has_enums is True if len(enums) else pydm_enumcombobox._has_enums is False
-@pytest.mark.parametrize("connected, write_access, has_enum, is_app_read_only", [
- (True, True, True, True),
- (True, True, True, False),
-
- (True, True, False, True),
- (True, True, False, False),
-
- (True, False, False, True),
- (True, False, False, False),
-
- (True, False, True, True),
- (True, False, True, False),
-
- (False, True, True, True),
- (False, True, True, False),
-
- (False, False, True, True),
- (False, False, True, False),
-
- (False, True, False, True),
- (False, True, False, False),
-
- (False, False, False, True),
- (False, False, False, False),
-])
+@pytest.mark.parametrize(
+ "connected, write_access, has_enum, is_app_read_only",
+ [
+ (True, True, True, True),
+ (True, True, True, False),
+ (True, True, False, True),
+ (True, True, False, False),
+ (True, False, False, True),
+ (True, False, False, False),
+ (True, False, True, True),
+ (True, False, True, False),
+ (False, True, True, True),
+ (False, True, True, False),
+ (False, False, True, True),
+ (False, False, True, False),
+ (False, True, False, True),
+ (False, True, False, False),
+ (False, False, False, True),
+ (False, False, False, False),
+ ],
+)
def test_check_enable_state(qtbot, signals, connected, write_access, has_enum, is_app_read_only):
"""
Test the tooltip generated depending on the channel connection, write access, whether the widget has enum strings,
@@ -148,12 +148,15 @@ def test_check_enable_state(qtbot, signals, connected, write_access, has_enum, i
assert "Enums not available" in actual_tooltip
-@pytest.mark.parametrize("values, selected_index, expected", [
- (("RUN", "STOP"), 0, "RUN"),
- (("RUN", "STOP"), 1, "STOP"),
- (("RUN", "STOP"), "RUN", "RUN"),
- (("RUN", "STOP"), "STOP", "STOP"),
-])
+@pytest.mark.parametrize(
+ "values, selected_index, expected",
+ [
+ (("RUN", "STOP"), 0, "RUN"),
+ (("RUN", "STOP"), 1, "STOP"),
+ (("RUN", "STOP"), "RUN", "RUN"),
+ (("RUN", "STOP"), "STOP", "STOP"),
+ ],
+)
def test_enum_strings_changed(qtbot, signals, values, selected_index, expected):
"""
Test the widget's handling of enum strings, which are choices presented to the user, and the widget's ability to
@@ -190,11 +193,14 @@ def test_enum_strings_changed(qtbot, signals, values, selected_index, expected):
assert pydm_enumcombobox.currentText() == expected
-@pytest.mark.parametrize("index", [
- 0,
- 1,
- -1,
-])
+@pytest.mark.parametrize(
+ "index",
+ [
+ 0,
+ 1,
+ -1,
+ ],
+)
def test_internal_combo_box_activated_int(qtbot, signals, index):
"""
Test the the capability of the widget's activated slot in sending out a new enum string index value.
@@ -226,17 +232,21 @@ def test_internal_combo_box_activated_int(qtbot, signals, index):
# NEGATIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("enums, expected_error_message", [
- (None, "Invalid enum value '{0}'. The value is expected to be a valid list of string values.".format(None)),
- ((None, "abc"), "Invalid enum type '{0}'. The expected type is 'string'.".format(type(None))),
- ((None, 123.456), "Invalid enum type '{0}'. The expected type is 'string'".format(type(None))),
- ((None, None, None), "Invalid enum type '{0}'. The expected type is 'string'".format(type(None))),
- ((123,), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
- ((123.45,), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123.45))),
- ((123, 456), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
- ((123.456, None), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123.456))),
- (("spam", 123, "eggs", "ham"), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
-])
+
+@pytest.mark.parametrize(
+ "enums, expected_error_message",
+ [
+ (None, "Invalid enum value '{0}'. The value is expected to be a valid list of string values.".format(None)),
+ ((None, "abc"), "Invalid enum type '{0}'. The expected type is 'string'.".format(type(None))),
+ ((None, 123.456), "Invalid enum type '{0}'. The expected type is 'string'".format(type(None))),
+ ((None, None, None), "Invalid enum type '{0}'. The expected type is 'string'".format(type(None))),
+ ((123,), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
+ ((123.45,), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123.45))),
+ ((123, 456), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
+ ((123.456, None), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123.456))),
+ (("spam", 123, "eggs", "ham"), "Invalid enum type '{0}'. The expected type is 'string'".format(type(123))),
+ ],
+)
def test_set_items_neg(qtbot, caplog, enums, expected_error_message):
"""
Test sending setting the widget with an undefined list of enum strings.
@@ -263,10 +273,13 @@ def test_set_items_neg(qtbot, caplog, enums, expected_error_message):
assert expected_error_message in caplog.text
-@pytest.mark.parametrize("values, selected_index, expected", [
- (("ON", "OFF"), 3, ""),
- (("ON", "OFF"), -1, ""),
-])
+@pytest.mark.parametrize(
+ "values, selected_index, expected",
+ [
+ (("ON", "OFF"), 3, ""),
+ (("ON", "OFF"), -1, ""),
+ ],
+)
def test_enum_strings_changed_incorrect_index(qtbot, signals, values, selected_index, expected):
"""
Test the widget's handling of incorrectly provided enum string index.
diff --git a/pydm/tests/widgets/test_frame.py b/pydm/tests/widgets/test_frame.py
index 50fdf9fb91..aad24851f7 100644
--- a/pydm/tests/widgets/test_frame.py
+++ b/pydm/tests/widgets/test_frame.py
@@ -13,6 +13,7 @@
# POSITIVE TEST CASES
# --------------------
+
def test_construct(qtbot):
"""
Test the construction of the widget.
@@ -32,12 +33,7 @@ def test_construct(qtbot):
assert pydm_frame.alarmSensitiveBorder is False
-@pytest.mark.parametrize("init_value, new_value", [
- (False, True),
- (True, False),
- (False, False),
- (True, True)
-])
+@pytest.mark.parametrize("init_value, new_value", [(False, True), (True, False), (False, False), (True, True)])
def test_disable_on_disconnect(qtbot, init_value, new_value):
"""
Test setting the flag to disable the widget when there's a channel disconnection.
@@ -64,39 +60,38 @@ def test_disable_on_disconnect(qtbot, init_value, new_value):
assert pydm_frame.disableOnDisconnect == new_value
-@pytest.mark.parametrize("channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity", [
- (None, False, False, PyDMWidget.ALARM_NONE),
- (None, False, True, PyDMWidget.ALARM_NONE),
- (None, True, False, PyDMWidget.ALARM_NONE),
- (None, True, True, PyDMWidget.ALARM_NONE),
-
- (None, False, False, PyDMWidget.ALARM_MAJOR),
- (None, False, True, PyDMWidget.ALARM_MAJOR),
- (None, True, False, PyDMWidget.ALARM_MAJOR),
- (None, True, True, PyDMWidget.ALARM_MAJOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_NONE),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_MINOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_MAJOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_DISCONNECTED),
-])
-def test_alarm_severity_change(qtbot, signals, channel, alarm_sensitive_content, alarm_sensitive_border,
- new_alarm_severity):
+@pytest.mark.parametrize(
+ "channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity",
+ [
+ (None, False, False, PyDMWidget.ALARM_NONE),
+ (None, False, True, PyDMWidget.ALARM_NONE),
+ (None, True, False, PyDMWidget.ALARM_NONE),
+ (None, True, True, PyDMWidget.ALARM_NONE),
+ (None, False, False, PyDMWidget.ALARM_MAJOR),
+ (None, False, True, PyDMWidget.ALARM_MAJOR),
+ (None, True, False, PyDMWidget.ALARM_MAJOR),
+ (None, True, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_DISCONNECTED),
+ ],
+)
+def test_alarm_severity_change(
+ qtbot, signals, channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity
+):
"""
Test the style of the widget changing according to alarm sensitivity settings and alarm severity changes.
@@ -127,18 +122,21 @@ def test_alarm_severity_change(qtbot, signals, channel, alarm_sensitive_content,
pydm_frame.alarmSensitiveBorder = alarm_sensitive_border
-@pytest.mark.parametrize("channel_address, connected, write_access, is_app_read_only", [
- ("CA://MA_TEST", True, True, True),
- ("CA://MA_TEST", True, False, True),
- ("CA://MA_TEST", True, True, False),
- ("CA://MA_TEST", True, False, False),
- ("CA://MA_TEST", False, True, True),
- ("CA://MA_TEST", False, False, True),
- ("CA://MA_TEST", False, True, False),
- ("CA://MA_TEST", False, False, False),
- ("", False, False, False),
- (None, False, False, False),
-])
+@pytest.mark.parametrize(
+ "channel_address, connected, write_access, is_app_read_only",
+ [
+ ("CA://MA_TEST", True, True, True),
+ ("CA://MA_TEST", True, False, True),
+ ("CA://MA_TEST", True, True, False),
+ ("CA://MA_TEST", True, False, False),
+ ("CA://MA_TEST", False, True, True),
+ ("CA://MA_TEST", False, False, True),
+ ("CA://MA_TEST", False, True, False),
+ ("CA://MA_TEST", False, False, False),
+ ("", False, False, False),
+ (None, False, False, False),
+ ],
+)
def test_check_enable_state(qtbot, signals, channel_address, connected, write_access, is_app_read_only):
"""
Test the tooltip generated depending on the channel address validation, connection, write access, and whether the
diff --git a/pydm/tests/widgets/test_label.py b/pydm/tests/widgets/test_label.py
index 28f9a3ba02..665a6bbd73 100644
--- a/pydm/tests/widgets/test_label.py
+++ b/pydm/tests/widgets/test_label.py
@@ -17,6 +17,7 @@
# POSITIVE TEST CASES
# --------------------
+
def test_construct(qtbot):
"""
Test the basic instantiation of the widget.
@@ -38,6 +39,7 @@ def test_construct(qtbot):
assert display_format_type == pydm_label.DisplayFormat.Default
assert pydm_label._string_encoding == pydm_label.app.get_string_encoding() if is_pydm_app() else "utf_8"
+
def test_enable_rich_text(qtbot):
"""
Test the widget's option for enabling rich text.
@@ -54,42 +56,41 @@ def test_enable_rich_text(qtbot):
pydm_label = PyDMLabel()
assert pydm_label.textFormat() == Qt.PlainText
- pydm_label.enableRichText = True #invoke setter
+ pydm_label.enableRichText = True # invoke setter
qtbot.addWidget(pydm_label)
assert pydm_label.textFormat() == Qt.RichText
-@pytest.mark.parametrize("value, display_format", [
- ("abc", DisplayFormat.Default),
- (123, DisplayFormat.Default),
- (0b100, DisplayFormat.Default),
- (0x1FF, DisplayFormat.Default),
-
- ("abc", DisplayFormat.String),
- (123, DisplayFormat.String),
- (0b100, DisplayFormat.String),
- (0x1FF, DisplayFormat.String),
-
- ("abc", DisplayFormat.Decimal), # This setting is acceptable. The displayed value will be "abc"
- (123, DisplayFormat.Decimal),
- (123.45, DisplayFormat.Decimal),
- (0b100, DisplayFormat.Decimal),
- (0x1FF, DisplayFormat.Decimal),
-
- (123, DisplayFormat.Exponential),
- (3.000e-02, DisplayFormat.Exponential),
- (0b100, DisplayFormat.Exponential),
- (0x1FF, DisplayFormat.Exponential),
-
- (123, DisplayFormat.Hex),
- (3.000e-02, DisplayFormat.Hex),
- (0b100, DisplayFormat.Hex),
- (0x1FF, DisplayFormat.Hex),
-
- (123, DisplayFormat.Binary),
- (3.000e-02, DisplayFormat.Binary),
- (0b100, DisplayFormat.Binary),
- (0x1FF, DisplayFormat.Binary),
-])
+
+@pytest.mark.parametrize(
+ "value, display_format",
+ [
+ ("abc", DisplayFormat.Default),
+ (123, DisplayFormat.Default),
+ (0b100, DisplayFormat.Default),
+ (0x1FF, DisplayFormat.Default),
+ ("abc", DisplayFormat.String),
+ (123, DisplayFormat.String),
+ (0b100, DisplayFormat.String),
+ (0x1FF, DisplayFormat.String),
+ ("abc", DisplayFormat.Decimal), # This setting is acceptable. The displayed value will be "abc"
+ (123, DisplayFormat.Decimal),
+ (123.45, DisplayFormat.Decimal),
+ (0b100, DisplayFormat.Decimal),
+ (0x1FF, DisplayFormat.Decimal),
+ (123, DisplayFormat.Exponential),
+ (3.000e-02, DisplayFormat.Exponential),
+ (0b100, DisplayFormat.Exponential),
+ (0x1FF, DisplayFormat.Exponential),
+ (123, DisplayFormat.Hex),
+ (3.000e-02, DisplayFormat.Hex),
+ (0b100, DisplayFormat.Hex),
+ (0x1FF, DisplayFormat.Hex),
+ (123, DisplayFormat.Binary),
+ (3.000e-02, DisplayFormat.Binary),
+ (0b100, DisplayFormat.Binary),
+ (0x1FF, DisplayFormat.Binary),
+ ],
+)
def test_value_changed(qtbot, signals, value, display_format):
"""
Test the widget's handling of the value changed event.
@@ -117,18 +118,23 @@ def test_value_changed(qtbot, signals, value, display_format):
signals.new_value_signal[type(value)].emit(value)
pydm_label.displayFormat = display_format
- displayed_value = parse_value_for_display(value=pydm_label.value, precision=1,
- display_format_type=pydm_label.displayFormat, widget=pydm_label)
- expected_value = parse_value_for_display(value=value, precision=1,
- display_format_type=display_format, widget=pydm_label)
+ displayed_value = parse_value_for_display(
+ value=pydm_label.value, precision=1, display_format_type=pydm_label.displayFormat, widget=pydm_label
+ )
+ expected_value = parse_value_for_display(
+ value=value, precision=1, display_format_type=display_format, widget=pydm_label
+ )
assert displayed_value == expected_value
assert pydm_label.displayFormat == display_format
-@pytest.mark.parametrize("values, selected_index, expected", [
- (("ON", "OFF"), 0, "ON"),
- (("ON", "OFF"), 1, "OFF"),
-])
+@pytest.mark.parametrize(
+ "values, selected_index, expected",
+ [
+ (("ON", "OFF"), 0, "ON"),
+ (("ON", "OFF"), 1, "OFF"),
+ ],
+)
def test_enum_strings_changed(qtbot, signals, values, selected_index, expected):
"""
Test the widget's handling of enum strings, which are choices presented to the user, and the widget's ability to
@@ -165,38 +171,36 @@ def test_enum_strings_changed(qtbot, signals, values, selected_index, expected):
assert pydm_label.displayFormat == DisplayFormat.String
-@pytest.mark.parametrize("value, display_format, unit_name, expected", [
- ("abc", DisplayFormat.Default, "", "abc"),
- (123, DisplayFormat.Default, "", "123"),
- (0b100, DisplayFormat.Default, "", "4"),
- (0x1FF, DisplayFormat.Default, "", "511"),
-
- ("abc", DisplayFormat.String, "s", "abc s"),
- (123, DisplayFormat.String, "s", "123 s"),
- (0b100, DisplayFormat.String, "s", "4 s"),
- (0x1FF, DisplayFormat.String, "s", "511 s"),
-
- ("abc", DisplayFormat.Decimal, "light years", "abc light years"),
- (123, DisplayFormat.Decimal, "light years", "123 light years"),
- (123.45, DisplayFormat.Decimal, "light years", "123 light years"), # Using default precision of 0
- (0b100, DisplayFormat.Decimal, "light years", "4 light years"),
- (0x1FF, DisplayFormat.Decimal, "light years", "511 light years"),
-
- (123, DisplayFormat.Exponential, "ms", "1e+02 ms"),
- (3.000e-02, DisplayFormat.Exponential, "ms", "3e-02 ms"),
- (0b100, DisplayFormat.Exponential, "ms", "4e+00 ms"),
- (0x1FF, DisplayFormat.Exponential, "ms", "5e+02 ms"),
-
- (123, DisplayFormat.Hex, "ns", "0x7b ns"),
- (3.000e-02, DisplayFormat.Hex, "ns", "0x0 ns"),
- (0b100, DisplayFormat.Hex, "ns", "0x4 ns"),
- (0x1FF, DisplayFormat.Hex, "ns", "0x1ff ns"),
-
- (123, DisplayFormat.Binary, "light years", "0b1111011 light years"),
- (3.000e-02, DisplayFormat.Binary, "light years", "0b0 light years"),
- (0b100, DisplayFormat.Binary, "light years", "0b100 light years"),
- (0x1FF, DisplayFormat.Binary, "light years", "0b111111111 light years"),
-])
+@pytest.mark.parametrize(
+ "value, display_format, unit_name, expected",
+ [
+ ("abc", DisplayFormat.Default, "", "abc"),
+ (123, DisplayFormat.Default, "", "123"),
+ (0b100, DisplayFormat.Default, "", "4"),
+ (0x1FF, DisplayFormat.Default, "", "511"),
+ ("abc", DisplayFormat.String, "s", "abc s"),
+ (123, DisplayFormat.String, "s", "123 s"),
+ (0b100, DisplayFormat.String, "s", "4 s"),
+ (0x1FF, DisplayFormat.String, "s", "511 s"),
+ ("abc", DisplayFormat.Decimal, "light years", "abc light years"),
+ (123, DisplayFormat.Decimal, "light years", "123 light years"),
+ (123.45, DisplayFormat.Decimal, "light years", "123 light years"), # Using default precision of 0
+ (0b100, DisplayFormat.Decimal, "light years", "4 light years"),
+ (0x1FF, DisplayFormat.Decimal, "light years", "511 light years"),
+ (123, DisplayFormat.Exponential, "ms", "1e+02 ms"),
+ (3.000e-02, DisplayFormat.Exponential, "ms", "3e-02 ms"),
+ (0b100, DisplayFormat.Exponential, "ms", "4e+00 ms"),
+ (0x1FF, DisplayFormat.Exponential, "ms", "5e+02 ms"),
+ (123, DisplayFormat.Hex, "ns", "0x7b ns"),
+ (3.000e-02, DisplayFormat.Hex, "ns", "0x0 ns"),
+ (0b100, DisplayFormat.Hex, "ns", "0x4 ns"),
+ (0x1FF, DisplayFormat.Hex, "ns", "0x1ff ns"),
+ (123, DisplayFormat.Binary, "light years", "0b1111011 light years"),
+ (3.000e-02, DisplayFormat.Binary, "light years", "0b0 light years"),
+ (0b100, DisplayFormat.Binary, "light years", "0b100 light years"),
+ (0x1FF, DisplayFormat.Binary, "light years", "0b111111111 light years"),
+ ],
+)
def test_show_units(qtbot, signals, value, display_format, unit_name, expected):
"""
Test the widget's capability to display a unit following a value if the user enables unit displaying.
@@ -249,32 +253,31 @@ def test_show_units(qtbot, signals, value, display_format, unit_name, expected):
assert pydm_label.displayFormat == display_format
-@pytest.mark.parametrize("alarm_severity, alarm_sensitive_content, alarm_sensitive_border", [
- (PyDMWidget.ALARM_NONE, True, True),
- (PyDMWidget.ALARM_NONE, True, False),
- (PyDMWidget.ALARM_NONE, False, True),
- (PyDMWidget.ALARM_NONE, False, False),
-
- (PyDMWidget.ALARM_MINOR, True, True),
- (PyDMWidget.ALARM_MINOR, True, False),
- (PyDMWidget.ALARM_MINOR, False, True),
- (PyDMWidget.ALARM_MINOR, False, False),
-
- (PyDMWidget.ALARM_MAJOR, True, True),
- (PyDMWidget.ALARM_MAJOR, True, False),
- (PyDMWidget.ALARM_MAJOR, False, True),
- (PyDMWidget.ALARM_MAJOR, False, False),
-
- (PyDMWidget.ALARM_INVALID, True, True),
- (PyDMWidget.ALARM_INVALID, True, False),
- (PyDMWidget.ALARM_INVALID, False, True),
- (PyDMWidget.ALARM_INVALID, False, False),
-
- (PyDMWidget.ALARM_DISCONNECTED, True, True),
- (PyDMWidget.ALARM_DISCONNECTED, True, False),
- (PyDMWidget.ALARM_DISCONNECTED, False, True),
- (PyDMWidget.ALARM_DISCONNECTED, False, False),
-])
+@pytest.mark.parametrize(
+ "alarm_severity, alarm_sensitive_content, alarm_sensitive_border",
+ [
+ (PyDMWidget.ALARM_NONE, True, True),
+ (PyDMWidget.ALARM_NONE, True, False),
+ (PyDMWidget.ALARM_NONE, False, True),
+ (PyDMWidget.ALARM_NONE, False, False),
+ (PyDMWidget.ALARM_MINOR, True, True),
+ (PyDMWidget.ALARM_MINOR, True, False),
+ (PyDMWidget.ALARM_MINOR, False, True),
+ (PyDMWidget.ALARM_MINOR, False, False),
+ (PyDMWidget.ALARM_MAJOR, True, True),
+ (PyDMWidget.ALARM_MAJOR, True, False),
+ (PyDMWidget.ALARM_MAJOR, False, True),
+ (PyDMWidget.ALARM_MAJOR, False, False),
+ (PyDMWidget.ALARM_INVALID, True, True),
+ (PyDMWidget.ALARM_INVALID, True, False),
+ (PyDMWidget.ALARM_INVALID, False, True),
+ (PyDMWidget.ALARM_INVALID, False, False),
+ (PyDMWidget.ALARM_DISCONNECTED, True, True),
+ (PyDMWidget.ALARM_DISCONNECTED, True, False),
+ (PyDMWidget.ALARM_DISCONNECTED, False, True),
+ (PyDMWidget.ALARM_DISCONNECTED, False, False),
+ ],
+)
def test_label_alarms(qtbot, signals, alarm_severity, alarm_sensitive_content, alarm_sensitive_border):
"""
Test the widget's appearance changes according to changes in alarm severity.
@@ -285,7 +288,8 @@ def test_label_alarms(qtbot, signals, alarm_severity, alarm_sensitive_content, a
solid, transparent, etc.
3. The alarm color and border appearance will change only if each corresponding Boolean flag is set to True
- NOTE: This test depends on the default stylesheet having different values for 'color' for different alarm states of PyDMLabel.
+ NOTE: This test depends on the default stylesheet having different values for 'color' for different
+ alarm states of PyDMLabel.
Parameters
----------
@@ -330,19 +334,24 @@ def test_label_alarms(qtbot, signals, alarm_severity, alarm_sensitive_content, a
TOOLTIP_TEXT = "Testing with Alarm State Changes, Channel Provided."
-@pytest.mark.parametrize("alarm_sensitive_content, alarm_sensitive_border, tooltip", [
- (True, True, TOOLTIP_TEXT),
- (True, False, TOOLTIP_TEXT),
- (False, True, TOOLTIP_TEXT),
- (False, False, TOOLTIP_TEXT),
-
- (True, True, ""),
- (True, False, ""),
- (False, True, ""),
- (False, False, ""),
-])
-def test_label_channel_connection_changes_with_alarm(qtbot, signals, alarm_sensitive_content, alarm_sensitive_border,
- tooltip):
+
+
+@pytest.mark.parametrize(
+ "alarm_sensitive_content, alarm_sensitive_border, tooltip",
+ [
+ (True, True, TOOLTIP_TEXT),
+ (True, False, TOOLTIP_TEXT),
+ (False, True, TOOLTIP_TEXT),
+ (False, False, TOOLTIP_TEXT),
+ (True, True, ""),
+ (True, False, ""),
+ (False, True, ""),
+ (False, False, ""),
+ ],
+)
+def test_label_channel_connection_changes_with_alarm(
+ qtbot, signals, alarm_sensitive_content, alarm_sensitive_border, tooltip
+):
"""
Test the widget's appearance and tooltip changes if a data channel is provided, and the is disconnected,
and then is reconnected.
@@ -374,7 +383,7 @@ def test_label_channel_connection_changes_with_alarm(qtbot, signals, alarm_sensi
The tooltip for the widget. This can be an empty string
"""
pydm_label = PyDMLabel()
- pydm_label.setText('Custom Text')
+ pydm_label.setText("Custom Text")
qtbot.addWidget(pydm_label)
pydm_label.alarmSensitiveContent = alarm_sensitive_content
@@ -394,10 +403,10 @@ def test_label_channel_connection_changes_with_alarm(qtbot, signals, alarm_sensi
# Confirm alarm severity, style, connection state, enabling state, and tooltip
assert pydm_label._alarm_state == alarm_severity
- assert pydm_label._connected == True
+ assert pydm_label._connected is True
assert pydm_label.toolTip() == tooltip
- assert pydm_label.isEnabled() == True
- assert pydm_label.text() == 'Custom Text'
+ assert pydm_label.isEnabled() is True
+ assert pydm_label.text() == "Custom Text"
# Next, disconnect the alarm, and check for the alarm severity, style, text, connection state, enabling state, and
# tooltip
@@ -405,10 +414,10 @@ def test_label_channel_connection_changes_with_alarm(qtbot, signals, alarm_sensi
signals.connection_state_signal.emit(False)
assert pydm_label._alarm_state == alarm_severity
- assert pydm_label._connected == False
+ assert pydm_label._connected is False
assert all(i in pydm_label.toolTip() for i in (tooltip, "PV is disconnected."))
- assert pydm_label.isEnabled() == False
- assert pydm_label.text() == 'CA://MTEST'
+ assert pydm_label.isEnabled() is False
+ assert pydm_label.text() == "CA://MTEST"
# Finally, reconnect the alarm, and check for the same attributes
signals.connection_state_signal.emit(True)
@@ -416,24 +425,27 @@ def test_label_channel_connection_changes_with_alarm(qtbot, signals, alarm_sensi
# Confirm alarm severity, style, connection state, enabling state, and tooltip
# TODO Set alarm_severity back to NONE
assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE
- assert pydm_label._connected == True
+ assert pydm_label._connected is True
assert pydm_label.toolTip() == tooltip
- assert pydm_label.isEnabled() == True
-
-
-@pytest.mark.parametrize("alarm_sensitive_content, alarm_sensitive_border, tooltip", [
- (True, True, TOOLTIP_TEXT),
- (True, False, TOOLTIP_TEXT),
- (False, True, TOOLTIP_TEXT),
- (False, False, TOOLTIP_TEXT),
-
- (True, True, ""),
- (True, False, ""),
- (False, True, ""),
- (False, False, ""),
-])
-def test_label_connection_changes_with_alarm_and_no_channel(qtbot, signals, alarm_sensitive_content, alarm_sensitive_border,
- tooltip):
+ assert pydm_label.isEnabled() is True
+
+
+@pytest.mark.parametrize(
+ "alarm_sensitive_content, alarm_sensitive_border, tooltip",
+ [
+ (True, True, TOOLTIP_TEXT),
+ (True, False, TOOLTIP_TEXT),
+ (False, True, TOOLTIP_TEXT),
+ (False, False, TOOLTIP_TEXT),
+ (True, True, ""),
+ (True, False, ""),
+ (False, True, ""),
+ (False, False, ""),
+ ],
+)
+def test_label_connection_changes_with_alarm_and_no_channel(
+ qtbot, signals, alarm_sensitive_content, alarm_sensitive_border, tooltip
+):
"""
Test the widget's appearance and tooltip changes if a data channel is not provided, and the connection is not
available, and available again.
@@ -482,9 +494,9 @@ def test_label_connection_changes_with_alarm_and_no_channel(qtbot, signals, alar
# Confirm alarm severity, style, connection state, enabling state, and tooltip
assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE
- assert pydm_label._connected == True
+ assert pydm_label._connected is True
assert pydm_label.toolTip() == tooltip
- assert pydm_label.isEnabled() == True
+ assert pydm_label.isEnabled() is True
# Next, disconnect the alarm, and check for the alarm severity, style, connection state, enabling state, and
# tooltip
@@ -492,9 +504,9 @@ def test_label_connection_changes_with_alarm_and_no_channel(qtbot, signals, alar
blocker.wait()
assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE
- assert pydm_label._connected == False
+ assert pydm_label._connected is False
assert pydm_label.toolTip() == tooltip
- assert pydm_label.isEnabled() == True
+ assert pydm_label.isEnabled() is True
# Finally, reconnect the alarm, and check for the same attributes
signals.connection_state_signal.emit(True)
@@ -502,29 +514,33 @@ def test_label_connection_changes_with_alarm_and_no_channel(qtbot, signals, alar
# Confirm alarm severity, style, connection state, enabling state, and tooltip
assert pydm_label._alarm_state == PyDMWidget.ALARM_NONE
- assert pydm_label._connected == True
+ assert pydm_label._connected is True
assert pydm_label.toolTip() == tooltip
- assert pydm_label.isEnabled() == True
+ assert pydm_label.isEnabled() is True
# --------------------
# NEGATIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("value, display_format, expected", [
- (np.array([-1, -2]), DisplayFormat.String, "Could not decode"),
- (np.array([0xfffe, 0xffff]), DisplayFormat.String, "Could not decode"),
- ("aaa", DisplayFormat.Exponential, "Could not display value 'aaa' using displayFormat 'Exponential'"),
- ("zzz", DisplayFormat.Hex, "Could not display value 'zzz' using displayFormat 'Hex'"),
- ("zzz", DisplayFormat.Binary, "Could not display value 'zzz' using displayFormat 'Binary'"),
-])
+
+@pytest.mark.parametrize(
+ "value, display_format, expected",
+ [
+ (np.array([-1, -2]), DisplayFormat.String, "Could not decode"),
+ (np.array([0xFFFE, 0xFFFF]), DisplayFormat.String, "Could not decode"),
+ ("aaa", DisplayFormat.Exponential, "Could not display value 'aaa' using displayFormat 'Exponential'"),
+ ("zzz", DisplayFormat.Hex, "Could not display value 'zzz' using displayFormat 'Hex'"),
+ ("zzz", DisplayFormat.Binary, "Could not display value 'zzz' using displayFormat 'Binary'"),
+ ],
+)
def test_value_changed_incorrect_display_format(qtbot, signals, caplog, value, display_format, expected):
"""
Test the widget's handling of incorrect provided values.
Expectations:
The correct error message is output in stderr.
-
+
Parameters
----------
qtbot : fixture
@@ -553,9 +569,12 @@ def test_value_changed_incorrect_display_format(qtbot, signals, caplog, value, d
assert expected in caplog.text
-@pytest.mark.parametrize("value, selected_index, expected", [
- (("ON", "OFF"), 3, "**INVALID**"),
-])
+@pytest.mark.parametrize(
+ "value, selected_index, expected",
+ [
+ (("ON", "OFF"), 3, "**INVALID**"),
+ ],
+)
def test_enum_strings_changed_incorrect_index(qtbot, signals, value, selected_index, expected):
"""
Test the widget's handling of incorrect provided enum string index.
diff --git a/pydm/tests/widgets/test_lineedit.py b/pydm/tests/widgets/test_lineedit.py
index 53d888426c..cce9860bab 100644
--- a/pydm/tests/widgets/test_lineedit.py
+++ b/pydm/tests/widgets/test_lineedit.py
@@ -10,7 +10,7 @@
from qtpy.QtGui import QFocusEvent
from qtpy.QtWidgets import QMenu
from ...widgets.line_edit import PyDMLineEdit
-from ...data_plugins import set_read_only, is_read_only
+from ...data_plugins import set_read_only
from ...utilities import is_pydm_app, find_unit_options
from ...widgets.display_format import DisplayFormat, parse_value_for_display
@@ -19,11 +19,15 @@
# POSITIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("init_channel", [
- "CA://MTEST",
- "",
- None,
-])
+
+@pytest.mark.parametrize(
+ "init_channel",
+ [
+ "CA://MTEST",
+ "",
+ None,
+ ],
+)
def test_construct(qtbot, init_channel):
"""
Test the widget construct.
@@ -48,23 +52,29 @@ def test_construct(qtbot, init_channel):
assert pydm_lineedit._display is None
assert pydm_lineedit._scale == 1
assert pydm_lineedit._prec == 0
- assert pydm_lineedit.showUnits == False
+ assert pydm_lineedit.showUnits is False
+
+ assert pydm_lineedit.unitMenu is None
+ pydm_lineedit.widget_ctx_menu()
+
assert isinstance(pydm_lineedit.unitMenu, QMenu) and pydm_lineedit.unitMenu.title() == "Convert Units"
assert pydm_lineedit.displayFormat == pydm_lineedit.DisplayFormat.Default
- assert (pydm_lineedit._string_encoding == pydm_lineedit.app.get_string_encoding()
- if is_pydm_app() else "utf_8")
+ assert pydm_lineedit._string_encoding == pydm_lineedit.app.get_string_encoding() if is_pydm_app() else "utf_8"
assert find_action_from_menu(pydm_lineedit.unitMenu, "No Unit Conversions found")
-@pytest.mark.parametrize("display_format", [
- (DisplayFormat.Default),
- (DisplayFormat.Exponential),
- (DisplayFormat.String),
- (DisplayFormat.Binary),
- (DisplayFormat.Decimal),
- (DisplayFormat.Hex),
-])
+@pytest.mark.parametrize(
+ "display_format",
+ [
+ (DisplayFormat.Default),
+ (DisplayFormat.Exponential),
+ (DisplayFormat.String),
+ (DisplayFormat.Binary),
+ (DisplayFormat.Decimal),
+ (DisplayFormat.Hex),
+ ],
+)
def test_change_display_format_type(qtbot, display_format):
"""
Test the widget's DisplayFormat property and setter.
@@ -86,16 +96,19 @@ def test_change_display_format_type(qtbot, display_format):
assert pydm_lineedit.displayFormat == display_format
-@pytest.mark.parametrize("value, display_format, precision, scale, unit, show_unit, expected_display", [
- (123, DisplayFormat.Default, 3, 1, "s", True, "123.000 s"),
- (123.47, DisplayFormat.Decimal, 3, 2, "seconds", False, "246.940"),
- (123.4567, DisplayFormat.String, 2, 1, "", False, "123.46"),
- (123.4567, DisplayFormat.Decimal, 1, 1, "", False, "123.5"),
- (1e2, DisplayFormat.Exponential, 2, 2, "light years", True, "2.00e+02 light years"),
- (0x1FF, DisplayFormat.Hex, 0, 1, "Me", True, "0x1ff Me"),
- (0b100, DisplayFormat.Binary, 0, 1, "KB", True, "0b100 KB"),
- (np.array([123, 456]), DisplayFormat.Default, 3, 2, "light years", True, "[123 456] light years"),
-])
+@pytest.mark.parametrize(
+ "value, display_format, precision, scale, unit, show_unit, expected_display",
+ [
+ (123, DisplayFormat.Default, 3, 1, "s", True, "123.000 s"),
+ (123.47, DisplayFormat.Decimal, 3, 2, "seconds", False, "246.940"),
+ (123.4567, DisplayFormat.String, 2, 1, "", False, "123.46"),
+ (123.4567, DisplayFormat.Decimal, 1, 1, "", False, "123.5"),
+ (1e2, DisplayFormat.Exponential, 2, 2, "light years", True, "2.00e+02 light years"),
+ (0x1FF, DisplayFormat.Hex, 0, 1, "Me", True, "0x1ff Me"),
+ (0b100, DisplayFormat.Binary, 0, 1, "KB", True, "0b100 KB"),
+ (np.array([123, 456]), DisplayFormat.Default, 3, 2, "light years", True, "[123 456] light years"),
+ ],
+)
def test_value_change(qtbot, signals, value, display_format, precision, scale, unit, show_unit, expected_display):
"""
Test changing the value to be displayed by the widget, given the value's display format, precision, scale, and unit.
@@ -141,19 +154,52 @@ def test_value_change(qtbot, signals, value, display_format, precision, scale, u
assert pydm_lineedit._display == expected_display
-@pytest.mark.parametrize("init_value, user_typed_value, display_format, precision, scale, unit,"
- "show_units, expected_received_value, expected_display_value", [
- ("abc", "cdf", DisplayFormat.Default, 3, 5, "s", True, "abc", "cdf s"),
- ("abc", "cdf", DisplayFormat.String, 3, 5, "s", False, "abc", "cdf"),
- (np.array([65, 66]), "[C D]", DisplayFormat.Default, 0, 10, "light years", True, "[C D]",
- "[C D] light years"),
- (np.array(["A", "B"]), "[C D]", DisplayFormat.String, 0, 10, "ms", True, "[C D]", "[C D] ms"),
- (np.array(["A", "B"]), np.array(["C", "D"]), DisplayFormat.String, 0, 10, "ms", True, "C D ", "C D ms"),
- (np.array(["A", "B"]), np.array(["C", "D"]), DisplayFormat.Default, 0, 10, "ms", True, "['C' 'D']", "['C' 'D'] ms"),
- (0, 10, DisplayFormat.Default, 0, 1.0, "V", True, 10, "10 V"),
-])
-def test_send_value(qtbot, signals, init_value, user_typed_value, display_format, precision, scale, unit,
- show_units, expected_received_value, expected_display_value):
+@pytest.mark.parametrize(
+ "init_value, user_typed_value, display_format, precision, scale, unit,"
+ "show_units, expected_received_value, expected_display_value",
+ [
+ ("abc", "cdf", DisplayFormat.Default, 3, 5, "s", True, "abc", "cdf s"),
+ ("abc", "cdf", DisplayFormat.String, 3, 5, "s", False, "abc", "cdf"),
+ (np.array([65, 66]), "[C D]", DisplayFormat.Default, 0, 10, "light years", True, "[C D]", "[C D] light years"),
+ (np.array(["A", "B"]), "[C D]", DisplayFormat.String, 0, 10, "ms", True, "[C D]", "[C D] ms"),
+ (
+ np.array(["A", "B"]),
+ np.array(["C", "D"]),
+ DisplayFormat.String,
+ 0,
+ 10,
+ "ms",
+ True,
+ "C D ",
+ "C D ms",
+ ),
+ (
+ np.array(["A", "B"]),
+ np.array(["C", "D"]),
+ DisplayFormat.Default,
+ 0,
+ 10,
+ "ms",
+ True,
+ "['C' 'D']",
+ "['C' 'D'] ms",
+ ),
+ (0, 10, DisplayFormat.Default, 0, 1.0, "V", True, 10, "10 V"),
+ ],
+)
+def test_send_value(
+ qtbot,
+ signals,
+ init_value,
+ user_typed_value,
+ display_format,
+ precision,
+ scale,
+ unit,
+ show_units,
+ expected_received_value,
+ expected_display_value,
+):
"""
Test sending the value to the channel, and the displayed value by the widget.
@@ -214,26 +260,27 @@ def test_send_value(qtbot, signals, init_value, user_typed_value, display_format
# compare just the characters (in the right order)
assert pydm_lineedit.displayText().replace(" ", "") == expected_display_value.replace(" ", "")
- if all(x in (int, float) for x in (type(expected_received_value), type(signals.value))) :
+ if all(x in (int, float) for x in (type(expected_received_value), type(signals.value))):
# Testing the actual value sent to the data channel and the expected value using a tolerance due to floating
# point arithmetic
assert abs(signals.value - expected_received_value) < 0.00001
-@pytest.mark.parametrize("new_write_access, is_channel_connected, tooltip, is_app_read_only", [
- (True, True, "Write Access and Connected Channel", False),
- (False, True, "Only Connected Channel", False),
- (True, False, "Only Write Access", False),
- (False, False, "No Write Access and No Connected Channel", False),
-
- (True, True, "Write Access and Connected Channel", True),
- (False, True, "Only Connected Channel", True),
- (True, False, "Only Write Access", True),
- (False, False, "No Write Access and No Connected Channel", True),
-
- (True, True, "", False),
- (True, True, "", True),
-])
+@pytest.mark.parametrize(
+ "new_write_access, is_channel_connected, tooltip, is_app_read_only",
+ [
+ (True, True, "Write Access and Connected Channel", False),
+ (False, True, "Only Connected Channel", False),
+ (True, False, "Only Write Access", False),
+ (False, False, "No Write Access and No Connected Channel", False),
+ (True, True, "Write Access and Connected Channel", True),
+ (False, True, "Only Connected Channel", True),
+ (True, False, "Only Write Access", True),
+ (False, False, "No Write Access and No Connected Channel", True),
+ (True, True, "", False),
+ (True, True, "", True),
+ ],
+)
def test_write_access_changed(qapp, qtbot, signals, new_write_access, is_channel_connected, tooltip, is_app_read_only):
"""
Test the widget's write access status and tooltip, which depends on the connection status of the data channel and
@@ -292,13 +339,16 @@ def test_write_access_changed(qapp, qtbot, signals, new_write_access, is_channel
assert "Access denied by Channel Access Security." in actual_tooltip
-@pytest.mark.parametrize("is_precision_from_pv, pv_precision, non_pv_precision", [
- (True, 1, 3),
- (False, 5, 3),
- (True, 6, 0),
- (True, 3, None),
- (False, 3, None),
-])
+@pytest.mark.parametrize(
+ "is_precision_from_pv, pv_precision, non_pv_precision",
+ [
+ (True, 1, 3),
+ (False, 5, 3),
+ (True, 6, 0),
+ (True, 3, None),
+ (False, 3, None),
+ ],
+)
def test_precision_change(qtbot, signals, is_precision_from_pv, pv_precision, non_pv_precision):
"""
Test setting the precision for the widget's value.
@@ -336,11 +386,14 @@ def test_precision_change(qtbot, signals, is_precision_from_pv, pv_precision, no
assert pydm_lineedit.precision == (non_pv_precision if non_pv_precision is not None else 0)
-@pytest.mark.parametrize("new_unit", [
- "s",
- "light years",
- "",
-])
+@pytest.mark.parametrize(
+ "new_unit",
+ [
+ "s",
+ "light years",
+ "",
+ ],
+)
def test_unit_change(qtbot, signals, new_unit):
"""
Test setting the widget's unit.
@@ -388,15 +441,18 @@ def find_action_from_menu(menu, action_name):
return status
-@pytest.mark.parametrize("unit, show_units", [
- ("ms", True),
- ("s", False),
- ("MHz", True),
- ("V", True),
- ("in", True),
- ("mrad", True),
- ("MA", True),
-])
+@pytest.mark.parametrize(
+ "unit, show_units",
+ [
+ ("ms", True),
+ ("s", False),
+ ("MHz", True),
+ ("V", True),
+ ("in", True),
+ ("mrad", True),
+ ("MA", True),
+ ],
+)
def test_create_unit_options(qtbot, unit, show_units):
"""
Test to ensure the context menu contains all applicable units that can be converted to from a given unit.
@@ -437,10 +493,13 @@ def test_create_unit_options(qtbot, unit, show_units):
assert find_action_from_menu(action_menu, "No Unit Conversions found")
-@pytest.mark.parametrize("value, precision, unit, show_unit, expected_format_string", [
- (123, 0, "s", True, "{:.0f} s"),
- (123.456, 3, "mV", True, "{:.3f} mV"),
-])
+@pytest.mark.parametrize(
+ "value, precision, unit, show_unit, expected_format_string",
+ [
+ (123, 0, "s", True, "{:.0f} s"),
+ (123.456, 3, "mV", True, "{:.3f} mV"),
+ ],
+)
def test_apply_conversion(qtbot, value, precision, unit, show_unit, expected_format_string):
"""
Test the unit conversion by examining the resulted format string.
@@ -477,17 +536,20 @@ def test_apply_conversion(qtbot, value, precision, unit, show_unit, expected_for
assert pydm_lineedit.format_string == expected_format_string
-@pytest.mark.parametrize("value, has_focus, channel_type, display_format, precision, scale, unit, show_units", [
- (123, True, int, DisplayFormat.Default, 3, 1, "s", True),
- (123, False, int, DisplayFormat.Default, 3, 1, "s", True),
- (123, True, int, DisplayFormat.Default, 3, 1, "s", False),
- (123, False, int, DisplayFormat.Default, 3, 1, "s", False),
- (123.45, True, float, DisplayFormat.Decimal, 3, 2, "m", True),
- (1e3, True, int, DisplayFormat.Exponential, 2, 2, "GHz", True),
- (0x1FF, True, int, DisplayFormat.Hex, 0, 1, "Me", True),
- (0b100, True, int, DisplayFormat.Binary, 0, 1, "KB", True),
- (np.array([123, 456]), str, True, DisplayFormat.Default, 3, 2, "degree", True),
-])
+@pytest.mark.parametrize(
+ "value, has_focus, channel_type, display_format, precision, scale, unit, show_units",
+ [
+ (123, True, int, DisplayFormat.Default, 3, 1, "s", True),
+ (123, False, int, DisplayFormat.Default, 3, 1, "s", True),
+ (123, True, int, DisplayFormat.Default, 3, 1, "s", False),
+ (123, False, int, DisplayFormat.Default, 3, 1, "s", False),
+ (123.45, True, float, DisplayFormat.Decimal, 3, 2, "m", True),
+ (1e3, True, int, DisplayFormat.Exponential, 2, 2, "GHz", True),
+ (0x1FF, True, int, DisplayFormat.Hex, 0, 1, "Me", True),
+ (0b100, True, int, DisplayFormat.Binary, 0, 1, "KB", True),
+ (np.array([123, 456]), str, True, DisplayFormat.Default, 3, 2, "degree", True),
+ ],
+)
def test_set_display(qtbot, qapp, value, has_focus, channel_type, display_format, precision, scale, unit, show_units):
"""
Test the widget's displayed value.
@@ -535,6 +597,7 @@ def test_set_display(qtbot, qapp, value, has_focus, channel_type, display_format
pydm_lineedit.check_enable_state()
if has_focus:
+
def wait_focus():
pydm_lineedit.setFocus()
qapp.processEvents()
@@ -548,6 +611,7 @@ def wait_focus():
assert pydm_lineedit._display == "Empty"
else:
pydm_lineedit.clearFocus()
+
def wait_nofocus():
pydm_lineedit.clearFocus()
return not pydm_lineedit.hasFocus()
@@ -559,8 +623,9 @@ def wait_nofocus():
if not isinstance(value, (str, np.ndarray)):
new_value *= pydm_lineedit.channeltype(pydm_lineedit._scale)
- new_value = parse_value_for_display(value=new_value, precision=precision, display_format_type=display_format,
- widget=pydm_lineedit)
+ new_value = parse_value_for_display(
+ value=new_value, precision=precision, display_format_type=display_format, widget=pydm_lineedit
+ )
expected_display = str(new_value)
if display_format == DisplayFormat.Default and not isinstance(value, np.ndarray):
@@ -572,12 +637,16 @@ def wait_nofocus():
assert pydm_lineedit._display == expected_display
-@pytest.mark.parametrize("displayed_value, focus_reason, expected_focus", [
- (True, Qt.TabFocusReason, True),
- (True, Qt.ActiveWindowFocusReason, True),
- (False, Qt.TabFocusReason, False),
- (False, Qt.ActiveWindowFocusReason, False),
- (False, Qt.MouseFocusReason, True)])
+@pytest.mark.parametrize(
+ "displayed_value, focus_reason, expected_focus",
+ [
+ (True, Qt.TabFocusReason, True),
+ (True, Qt.ActiveWindowFocusReason, True),
+ (False, Qt.TabFocusReason, False),
+ (False, Qt.ActiveWindowFocusReason, False),
+ (False, Qt.MouseFocusReason, True),
+ ],
+)
def test_focus_in_event(qtbot, qapp, displayed_value, focus_reason, expected_focus):
"""
Ensure that the line edit's focusInEvent() override works as expected. When the widget has not yet been connected
@@ -591,7 +660,7 @@ def test_focus_in_event(qtbot, qapp, displayed_value, focus_reason, expected_foc
pydm_lineedit.show()
def wait_focus(focus_state, wait_on_clear):
- """ Verify the current focus state of the line edit """
+ """Verify the current focus state of the line edit"""
if focus_state:
pydm_lineedit.setFocus(Qt.OtherFocusReason)
elif wait_on_clear:
@@ -617,11 +686,14 @@ def wait_focus(focus_state, wait_on_clear):
qtbot.waitUntil(functools.partial(wait_focus, expected_focus, False), timeout=5000)
-@pytest.mark.parametrize("display_value", [
- "123",
- "123.456",
- "",
-])
+@pytest.mark.parametrize(
+ "display_value",
+ [
+ "123",
+ "123.456",
+ "",
+ ],
+)
def test_focus_out_event(qtbot, qapp, display_value):
"""
Test the widget's value revert capability if the user doesn't commit the value change.
@@ -650,6 +722,7 @@ def test_focus_out_event(qtbot, qapp, display_value):
pydm_lineedit.check_enable_state()
pydm_lineedit._display = display_value
+
def wait_focus():
pydm_lineedit.setFocus()
qapp.processEvents()
@@ -669,20 +742,48 @@ def wait_nofocus():
# Make sure the widget still retains the previously set value after the focusOut event
assert pydm_lineedit.text() == display_value
+
# --------------------
# NEGATIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("value, precision, initial_unit, unit, show_unit, expected", [
- (123, 0, None, int, True,
- ("Warning: Attempting to convert PyDMLineEdit unit, but no initial units supplied",)),
- (123.456, 3, float, int, True,
- ("Warning: Attempting to convert PyDMLineEdit unit, but ", "'float'>' can not be converted to ", "'int'>'.")),
- (123.456, 3, float, "foo", True,
- ("Warning: Attempting to convert PyDMLineEdit unit, but ", "'float'>' can not be converted to 'foo'.")),
- ("123.456", 3, "light years", "light years", False,
- ("Warning: Attempting to convert PyDMLineEdit unit, but 'light years' can not be converted to 'light years'.",)),
-])
+
+@pytest.mark.parametrize(
+ "value, precision, initial_unit, unit, show_unit, expected",
+ [
+ (123, 0, None, int, True, ("Warning: Attempting to convert PyDMLineEdit unit, but no initial units supplied",)),
+ (
+ 123.456,
+ 3,
+ float,
+ int,
+ True,
+ (
+ "Warning: Attempting to convert PyDMLineEdit unit, but ",
+ "'float'>' can not be converted to ",
+ "'int'>'.",
+ ),
+ ),
+ (
+ 123.456,
+ 3,
+ float,
+ "foo",
+ True,
+ ("Warning: Attempting to convert PyDMLineEdit unit, but ", "'float'>' can not be converted to 'foo'."),
+ ),
+ (
+ "123.456",
+ 3,
+ "light years",
+ "light years",
+ False,
+ (
+ "Warning: Attempting to convert PyDMLineEdit unit, but 'light years' can not be converted to 'light years'.", # noqa E501
+ ),
+ ),
+ ],
+)
def test_apply_conversion_wrong_unit(qtbot, caplog, value, precision, initial_unit, unit, show_unit, expected):
"""
Test the unit conversion error logging.
@@ -720,12 +821,25 @@ def test_apply_conversion_wrong_unit(qtbot, caplog, value, precision, initial_un
assert all(x in caplog.text for x in expected)
-@pytest.mark.parametrize("init_value, user_typed_value, display_format, precision, scale, unit,"
- "show_units, expected_errors", [
- (123, 345.678, DisplayFormat.Default, 0, 1, "s", True, ("Error trying to set data ", "with type ", "int'>")),
-])
-def test_send_value_neg(qtbot, caplog, signals, init_value, user_typed_value, display_format, precision, scale, unit,
- show_units, expected_errors):
+@pytest.mark.parametrize(
+ "init_value, user_typed_value, display_format, precision, scale, unit," "show_units, expected_errors",
+ [
+ (123, 345.678, DisplayFormat.Default, 0, 1, "s", True, ("Error trying to set data ", "with type ", "int'>")),
+ ],
+)
+def test_send_value_neg(
+ qtbot,
+ caplog,
+ signals,
+ init_value,
+ user_typed_value,
+ display_format,
+ precision,
+ scale,
+ unit,
+ show_units,
+ expected_errors,
+):
"""
Test sending the value to the channel error logging.
diff --git a/pydm/tests/widgets/test_logdisplay.py b/pydm/tests/widgets/test_logdisplay.py
index 329ce0edf6..44e6662224 100644
--- a/pydm/tests/widgets/test_logdisplay.py
+++ b/pydm/tests/widgets/test_logdisplay.py
@@ -4,9 +4,9 @@
from pydm.widgets.logdisplay import PyDMLogDisplay
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
def log():
- log = logging.getLogger('log_test.pydm')
+ log = logging.getLogger("log_test.pydm")
return log
@@ -17,26 +17,26 @@ def test_write(qtbot, log):
assert logd.logLevel == logging.INFO
assert logd.logName == log.name
# Watch our error message show up in the log
- err_msg = 'This is a test of the emergency broadcast system'
+ err_msg = "This is a test of the emergency broadcast system"
log.error(err_msg)
assert err_msg in logd.text.toPlainText()
# Debug shouldn't show up
- debug_msg = 'Pay no attention to the man behind the curtain'
+ debug_msg = "Pay no attention to the man behind the curtain"
log.debug(debug_msg)
assert debug_msg not in logd.text.toPlainText()
# Change the level so debug does show up
- logd.setLevel('DEBUG')
+ logd.setLevel("DEBUG")
assert logd.handler.level == logging.DEBUG
assert logd.log.level <= logging.DEBUG
log.debug(debug_msg)
assert debug_msg in logd.text.toPlainText()
# Change the name and make sure we still see what we need
- logd.logname = 'log_test'
- info_msg = 'The more things change the more they stay the same'
+ logd.logname = "log_test"
+ info_msg = "The more things change the more they stay the same"
log.info(info_msg)
assert info_msg in logd.text.toPlainText()
logd.clear()
- assert logd.text.toPlainText() == ''
+ assert logd.text.toPlainText() == ""
def test_handler_cleanup(qtbot, log):
diff --git a/pydm/tests/widgets/test_multiaxis_plot.py b/pydm/tests/widgets/test_multiaxis_plot.py
index 019d955b53..52f5b84460 100644
--- a/pydm/tests/widgets/test_multiaxis_plot.py
+++ b/pydm/tests/widgets/test_multiaxis_plot.py
@@ -7,35 +7,35 @@
@pytest.fixture
def sample_plot(qtbot, monkeypatch):
- """ Set up a plot with a couple of test axis items for use in test cases below """
+ """Set up a plot with a couple of test axis items for use in test cases below"""
plot = MultiAxisPlot()
scene = QGraphicsScene()
- monkeypatch.setattr(plot, 'scene', lambda: scene) # Have the plot return a dummy scene when needed
+ monkeypatch.setattr(plot, "scene", lambda: scene) # Have the plot return a dummy scene when needed
# Create two axes, one with a fixed range and one that should auto-range (adjust to always display all data)
- fixed_range_axis = AxisItem('left')
- plot.addAxis(fixed_range_axis, 'Fixed Range Axis', enableAutoRangeY=False, minRange=-5.0, maxRange=7.5)
- auto_range_axis = AxisItem('left')
- plot.addAxis(auto_range_axis, 'Auto Range Axis', enableAutoRangeY=True)
+ fixed_range_axis = AxisItem("left")
+ plot.addAxis(fixed_range_axis, "Fixed Range Axis", enableAutoRangeY=False, minRange=-5.0, maxRange=7.5)
+ auto_range_axis = AxisItem("left")
+ plot.addAxis(auto_range_axis, "Auto Range Axis", enableAutoRangeY=True)
return plot
def test_original_ranges_set_correctly(sample_plot):
- """ Check that the original ranges of x and y axes are preserved when adding them into the plot """
+ """Check that the original ranges of x and y axes are preserved when adding them into the plot"""
# The axis with a set range should have that preserved
- assert sample_plot.axesOriginalRanges['Fixed Range Axis'][0] == -5.0
- assert sample_plot.axesOriginalRanges['Fixed Range Axis'][1] == 7.5
+ assert sample_plot.axesOriginalRanges["Fixed Range Axis"][0] == -5.0
+ assert sample_plot.axesOriginalRanges["Fixed Range Axis"][1] == 7.5
# And the axis set to auto-range should be set to None indicating there is no fixed range to preserve
- assert sample_plot.axesOriginalRanges['Auto Range Axis'][0] is None
- assert sample_plot.axesOriginalRanges['Auto Range Axis'][1] is None
+ assert sample_plot.axesOriginalRanges["Auto Range Axis"][0] is None
+ assert sample_plot.axesOriginalRanges["Auto Range Axis"][1] is None
-@mock.patch('pyqtgraph.ViewBox.enableAutoRange')
-@mock.patch('pyqtgraph.PlotItem.setXRange')
-@mock.patch('pyqtgraph.ViewBox.setYRange')
+@mock.patch("pyqtgraph.ViewBox.enableAutoRange")
+@mock.patch("pyqtgraph.PlotItem.setXRange")
+@mock.patch("pyqtgraph.ViewBox.setYRange")
def test_restore_axis_ranges(mocked_y_range, mocked_x_range, mocked_auto_range, sample_plot):
"""
Verify that when axis ranges have been changed from their defaults, calling restore will set them back to
@@ -43,7 +43,7 @@ def test_restore_axis_ranges(mocked_y_range, mocked_x_range, mocked_auto_range,
"""
# First let's set a starting range on the bottom axis as well as those set in the fixture
- sample_plot.axesOriginalRanges['bottom'] = (0.0, 10.0)
+ sample_plot.axesOriginalRanges["bottom"] = (0.0, 10.0)
sample_plot.restoreAxisRanges()
@@ -55,80 +55,80 @@ def test_restore_axis_ranges(mocked_y_range, mocked_x_range, mocked_auto_range,
def test_link_data_to_logarithmic_axis(qtbot, monkeypatch, sample_plot):
- """ Verify that when a curve is added to an axis with log mode on, that curve is set to log mode as well """
+ """Verify that when a curve is added to an axis with log mode on, that curve is set to log mode as well"""
# Create one linear axis and one logarithmic axis
- linear_axis = AxisItem('left')
- log_axis = AxisItem('left')
+ linear_axis = AxisItem("left")
+ log_axis = AxisItem("left")
log_axis.setLogMode(True)
- sample_plot.addAxis(linear_axis, 'Linear Axis')
- sample_plot.addAxis(log_axis, 'Log Axis')
+ sample_plot.addAxis(linear_axis, "Linear Axis")
+ sample_plot.addAxis(log_axis, "Log Axis")
# Create a data item to go along with each axis
linear_data = PlotDataItem()
log_data = PlotDataItem()
# Upon creation, all data should default to non-log mode
- assert linear_data.opts['logMode'] == [False, False]
- assert log_data.opts['logMode'] == [False, False]
+ assert linear_data.opts["logMode"] == [False, False]
+ assert log_data.opts["logMode"] == [False, False]
- sample_plot.linkDataToAxis(linear_data, 'Linear Axis')
- sample_plot.linkDataToAxis(log_data, 'Log Axis')
+ sample_plot.linkDataToAxis(linear_data, "Linear Axis")
+ sample_plot.linkDataToAxis(log_data, "Log Axis")
# Now that we've linked the data to their associated axes, the log_data should have logMode set to true, while
# linear data should still have it set to false
- assert linear_data.opts['logMode'] == [False, False]
- assert log_data.opts['logMode'] == [False, True]
+ assert linear_data.opts["logMode"] == [False, False]
+ assert log_data.opts["logMode"] == [False, True]
def test_update_log_mode(qtbot, sample_plot):
- """ Verify toggling log mode on and off for the entire plot works as expected """
+ """Verify toggling log mode on and off for the entire plot works as expected"""
data_item = PlotDataItem()
sample_plot.addItem(data_item)
# For a brand new plot, log mode defaults to false for everything. Verify this is the case.
- assert data_item.opts['logMode'] == [False, False]
+ assert data_item.opts["logMode"] == [False, False]
for axis in sample_plot.getAxes():
assert not axis.logMode
# Now set log mode on for y-values only. Verify this gets set correctly, and x-values are left alone.
sample_plot.setLogMode(False, True)
- assert data_item.opts['logMode'] == [False, True]
+ assert data_item.opts["logMode"] == [False, True]
for axis in sample_plot.getAxes():
- if axis.orientation in ('bottom', 'top'):
+ if axis.orientation in ("bottom", "top"):
assert not axis.logMode
- elif axis.orientation in ('left', 'right'):
+ elif axis.orientation in ("left", "right"):
assert axis.logMode
else:
- raise ValueError(f'Invalid value for axis orientation: {axis.orientation}')
+ raise ValueError(f"Invalid value for axis orientation: {axis.orientation}")
# Now set log mode on for x-values only. Verify this gets set correctly, and y-values are left alone.
sample_plot.setLogMode(True, False)
- assert data_item.opts['logMode'] == [True, False]
+ assert data_item.opts["logMode"] == [True, False]
for axis in sample_plot.getAxes():
- if axis.orientation in ('bottom', 'top'):
+ if axis.orientation in ("bottom", "top"):
assert axis.logMode
- elif axis.orientation in ('left', 'right'):
+ elif axis.orientation in ("left", "right"):
assert not axis.logMode
else:
- raise ValueError(f'Invalid value for axis orientation: {axis.orientation}')
+ raise ValueError(f"Invalid value for axis orientation: {axis.orientation}")
# Now set log mode on for everything. Verify this is set across all items.
sample_plot.setLogMode(True, True)
- assert data_item.opts['logMode'] == [True, True]
+ assert data_item.opts["logMode"] == [True, True]
for axis in sample_plot.getAxes():
assert axis.logMode
# And finally return everything back to non-log mode. Verify all items are no longer in log mode for x or y.
sample_plot.setLogMode(False, False)
- assert data_item.opts['logMode'] == [False, False]
+ assert data_item.opts["logMode"] == [False, False]
for axis in sample_plot.getAxes():
assert not axis.logMode
def test_remove_item(qtbot, sample_plot: MultiAxisPlot):
- """ Verify that removing an item from the plot works as expected """
+ """Verify that removing an item from the plot works as expected"""
# First create a couple of mock data items to plot, and add them to the plot
data_item_one = PlotDataItem()
diff --git a/pydm/tests/widgets/test_pushbutton.py b/pydm/tests/widgets/test_pushbutton.py
index 2ae4b5fc7b..26ccdcb9c1 100644
--- a/pydm/tests/widgets/test_pushbutton.py
+++ b/pydm/tests/widgets/test_pushbutton.py
@@ -10,7 +10,6 @@
from qtpy.QtCore import QSize, Qt
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QInputDialog, QMessageBox
-from ...widgets.base import PyDMWidget
from ...widgets.pushbutton import PyDMPushButton
from ...utilities.iconfont import IconFont
@@ -19,37 +18,37 @@
# POSITIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("label, press_value, relative, init_channel, icon_font_name, icon_color", [
- # Testing different types of press value
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
- ("Test Button", 42, True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
- ("Test Button", 42.42, True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
-
- # Testing combinations of parameters
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", None, None),
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", "", None),
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", None, ""),
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cog", QColor(255, 0, 0)),
- ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
-
- ("", "Test Button PressValue", True, "", "fast-forward", QColor(0, 0, 0)),
- ("Test Button", "", True, None, "fast-forward", QColor(0, 0, 255)),
- ("", "", True, "CA://MTEST", "fast-forward", QColor(0, 255, 0)),
- ("", None, True, "CA://MTEST", "fast-forward", QColor(255, 0, 0)),
-
- ("Test Button", "Test Button PressValue", False, "CA://MTEST", None, None),
- ("Test Button", "Test Button PressValue", False, "CA://MTEST", "check", QColor(10, 20, 30)),
- ("", "Test Button PressValue", False, "", "check", QColor(10, 20, 30)),
- ("Test Button", "", False, None, "check", QColor(10, 20, 30)),
- ("", "", False, "CA://MTEST", "check", QColor(10, 20, 30)),
- ("", None, False, "CA://MTEST", "check", QColor(10, 20, 30)),
-
- # Testing variations of empty parameters
- (None, "", True, "", None, None),
- (None, None, True, None, None, None),
- (None, "", False, "", None, None),
- (None, None, False, None, None, None),
-])
+
+@pytest.mark.parametrize(
+ "label, press_value, relative, init_channel, icon_font_name, icon_color",
+ [
+ # Testing different types of press value
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
+ ("Test Button", 42, True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
+ ("Test Button", 42.42, True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
+ # Testing combinations of parameters
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", None, None),
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", "", None),
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", None, ""),
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cog", QColor(255, 0, 0)),
+ ("Test Button", "Test Button PressValue", True, "CA://MTEST", "cogs", QColor(255, 255, 255)),
+ ("", "Test Button PressValue", True, "", "fast-forward", QColor(0, 0, 0)),
+ ("Test Button", "", True, None, "fast-forward", QColor(0, 0, 255)),
+ ("", "", True, "CA://MTEST", "fast-forward", QColor(0, 255, 0)),
+ ("", None, True, "CA://MTEST", "fast-forward", QColor(255, 0, 0)),
+ ("Test Button", "Test Button PressValue", False, "CA://MTEST", None, None),
+ ("Test Button", "Test Button PressValue", False, "CA://MTEST", "check", QColor(10, 20, 30)),
+ ("", "Test Button PressValue", False, "", "check", QColor(10, 20, 30)),
+ ("Test Button", "", False, None, "check", QColor(10, 20, 30)),
+ ("", "", False, "CA://MTEST", "check", QColor(10, 20, 30)),
+ ("", None, False, "CA://MTEST", "check", QColor(10, 20, 30)),
+ # Testing variations of empty parameters
+ (None, "", True, "", None, None),
+ (None, None, True, None, None, None),
+ (None, "", False, "", None, None),
+ (None, None, False, None, None, None),
+ ],
+)
def test_construct(qtbot, label, press_value, relative, init_channel, icon_font_name, icon_color):
"""
Test the basic instantiation of the widget.
@@ -79,8 +78,9 @@ def test_construct(qtbot, label, press_value, relative, init_channel, icon_font_
if icon_font_name:
icon = IconFont().icon(icon_font_name, icon_color)
- pydm_pushbutton = PyDMPushButton(label=label, pressValue=press_value, relative=relative,
- init_channel=init_channel, icon=icon)
+ pydm_pushbutton = PyDMPushButton(
+ label=label, pressValue=press_value, relative=relative, init_channel=init_channel, icon=icon
+ )
qtbot.addWidget(pydm_pushbutton)
assert pydm_pushbutton.text() == label if label else pydm_pushbutton.text() == ""
@@ -102,17 +102,42 @@ def test_construct(qtbot, label, press_value, relative, init_channel, icon_font_
button_icon_pixmap = pydm_pushbutton.icon().pixmap(size)
assert icon_pixmap.toImage() == button_icon_pixmap.toImage()
- assert pydm_pushbutton.showConfirmDialog == False
+ # verify that qt standard icons can be set through our custom property
+ style = pydm_pushbutton.style()
+ test_icon = style.standardIcon(style.SP_DesktopIcon)
+ test_icon_image = test_icon.pixmap(size).toImage()
+
+ pydm_pushbutton.PyDMIcon = "SP_DesktopIcon"
+ push_btn_icon = pydm_pushbutton.icon()
+ push_btn_icon_image = push_btn_icon.pixmap(size).toImage()
+
+ assert test_icon_image == push_btn_icon_image
+
+ # verify that "Font Awesome" icons can be set through our custom property
+ icon_f = IconFont()
+ test_icon = icon_f.icon("eye-slash", color=None)
+ test_icon_image = test_icon.pixmap(size).toImage()
+
+ pydm_pushbutton.PyDMIcon = "eye-slash"
+ push_btn_icon = pydm_pushbutton.icon()
+ push_btn_icon_image = push_btn_icon.pixmap(size).toImage()
+
+ assert test_icon_image == push_btn_icon_image
+
+ assert pydm_pushbutton.showConfirmDialog is False
assert pydm_pushbutton.confirmMessage == PyDMPushButton.DEFAULT_CONFIRM_MESSAGE
- assert pydm_pushbutton.passwordProtected == False
+ assert pydm_pushbutton.passwordProtected is False
assert pydm_pushbutton.password == ""
assert pydm_pushbutton.protectedPassword == ""
-@pytest.mark.parametrize("password_is_protected", [
- (True),
- (False),
-])
+@pytest.mark.parametrize(
+ "password_is_protected",
+ [
+ (True),
+ (False),
+ ],
+)
def test_password_protected(qtbot, password_is_protected):
"""
Test that the password protected property is properly set. The password protected is flag that, if set to True,
@@ -136,10 +161,13 @@ def test_password_protected(qtbot, password_is_protected):
assert pydm_pushbutton._password_protected == password_is_protected
-@pytest.mark.parametrize("relative_choice", [
- (True),
- (False),
-])
+@pytest.mark.parametrize(
+ "relative_choice",
+ [
+ (True),
+ (False),
+ ],
+)
def test_relative_change(qtbot, relative_choice):
"""
Test that the relative attribute of the button.
@@ -161,14 +189,10 @@ def test_relative_change(qtbot, relative_choice):
assert pydm_pushbutton.relativeChange == relative_choice
-@pytest.mark.parametrize("password_protected, plain_text_password", [
- (True, "$L4C_p4$$wd"),
- (True, ""),
- (True, None),
- (False, "$L4C_p4$$wd"),
- (False, ""),
- (False, None)
-])
+@pytest.mark.parametrize(
+ "password_protected, plain_text_password",
+ [(True, "$L4C_p4$$wd"), (True, ""), (True, None), (False, "$L4C_p4$$wd"), (False, ""), (False, None)],
+)
def test_set_password(qtbot, password_protected, plain_text_password):
"""
Test the widget's password encryption mechanism.
@@ -212,25 +236,30 @@ def test_set_password(qtbot, password_protected, plain_text_password):
assert pydm_pushbutton._password_protected == password_protected
assert encrypted_password == expected_encrypted_password
-@pytest.mark.parametrize("is_widget_protected_with_password, plain_text_password, input_dialog_status,"
- "expected_validation_status", [
- (True, "$L4C_p4$$wd", True, True),
- (False, "$L4C_p4$$wd", True, True),
-
- (True, "", True, False),
- (False, "", True, True),
- (True, "$L4C_p4$$wd", False, False),
- (False, "$L4C_p4$$wd", False, True),
-
- (True, "Wrong_Password", True, False),
- (False, "Wrong_Password", True, False),
-
- (True, "Wrong_Password", False, False),
- (False, "Wrong_Password", False, False),
-])
-def test_validate_password(qtbot, monkeypatch, is_widget_protected_with_password, plain_text_password,
- input_dialog_status, expected_validation_status):
+@pytest.mark.parametrize(
+ "is_widget_protected_with_password, plain_text_password, input_dialog_status," "expected_validation_status",
+ [
+ (True, "$L4C_p4$$wd", True, True),
+ (False, "$L4C_p4$$wd", True, True),
+ (True, "", True, False),
+ (False, "", True, True),
+ (True, "$L4C_p4$$wd", False, False),
+ (False, "$L4C_p4$$wd", False, True),
+ (True, "Wrong_Password", True, False),
+ (False, "Wrong_Password", True, False),
+ (True, "Wrong_Password", False, False),
+ (False, "Wrong_Password", False, False),
+ ],
+)
+def test_validate_password(
+ qtbot,
+ monkeypatch,
+ is_widget_protected_with_password,
+ plain_text_password,
+ input_dialog_status,
+ expected_validation_status,
+):
"""
Test password validation.
@@ -266,45 +295,55 @@ def test_validate_password(qtbot, monkeypatch, is_widget_protected_with_password
# Then, we mock different scenarios with the input dialog's returning False (the user clicking on Cancel),
# or the user-entered password is not matching
pydm_pushbutton.password = "$L4C_p4$$wd"
- monkeypatch.setattr(QInputDialog, 'getText', lambda *args: (plain_text_password, input_dialog_status))
+ monkeypatch.setattr(QInputDialog, "getText", lambda *args: (plain_text_password, input_dialog_status))
if not expected_validation_status:
# Turn off the "Invalid password" dialog so that it won't interfere with the test
- monkeypatch.setattr(QMessageBox, 'exec_', lambda *args: (False,))
+ monkeypatch.setattr(QMessageBox, "exec_", lambda *args: (False,))
validation_status = pydm_pushbutton.validate_password()
assert validation_status == expected_validation_status
-@pytest.mark.parametrize("initial_value, press_value, is_password_protected, show_confirm_dialog,"
- "confirm_message, confirm_dialog_response, is_password_validated, is_value_relative,", [
- (0, 1, True, True, "Continue?", QMessageBox.Yes, True, False),
- (123, 345, True, True, "Continue?", QMessageBox.Yes, True, True),
- (123, "345", True, True, "", QMessageBox.Yes, True, True),
- (123.345, 345.678, True, True, "", QMessageBox.Yes, True, True),
- (123.345, "345.678", True, True, "", QMessageBox.Yes, True, True),
-
- ("123", 345, False, True, "", QMessageBox.Yes, True, True),
- ("123", 345.678, True, False, "", QMessageBox.Yes, True, True),
- ("123.345", 345.678, False, False, "", QMessageBox.Yes, True, True),
- ("123.345", "345.678", False, False, "", QMessageBox.Yes, True, True),
-
- ("123", 345, True, True, "", QMessageBox.No, True, True),
- ("123", 345, False, True, "", QMessageBox.No, True, True),
-
- ("123", 345, True, True, "Continue?", QMessageBox.Yes, True, False),
- ("123", 345, True, True, "", QMessageBox.Yes, True, False),
- ("123.345", 345.678, True, True, "", QMessageBox.Yes, True, False),
- ("123.345", "345.678", True, True, "", QMessageBox.Yes, True, False),
-
- ("123", 345, True, True, "Continue?", QMessageBox.Yes, False, True),
- ("123", 345, True, True, "", QMessageBox.Yes, False, False),
- ("abc", "def", True, True, "", QMessageBox.Yes, False, False),
- ("abc", None, True, True, "", QMessageBox.Yes, False, False),
- (None, "def", True, True, "", QMessageBox.Yes, False, False),
- (None, None, True, True, "", QMessageBox.Yes, False, False),
-])
-def test_send_value(qtbot, monkeypatch, signals, initial_value, press_value, is_password_protected, show_confirm_dialog,
- confirm_message, confirm_dialog_response, is_password_validated, is_value_relative):
+@pytest.mark.parametrize(
+ "initial_value, press_value, is_password_protected, show_confirm_dialog,"
+ "confirm_message, confirm_dialog_response, is_password_validated, is_value_relative,",
+ [
+ (0, 1, True, True, "Continue?", QMessageBox.Yes, True, False),
+ (123, 345, True, True, "Continue?", QMessageBox.Yes, True, True),
+ (123, "345", True, True, "", QMessageBox.Yes, True, True),
+ (123.345, 345.678, True, True, "", QMessageBox.Yes, True, True),
+ (123.345, "345.678", True, True, "", QMessageBox.Yes, True, True),
+ ("123", 345, False, True, "", QMessageBox.Yes, True, True),
+ ("123", 345.678, True, False, "", QMessageBox.Yes, True, True),
+ ("123.345", 345.678, False, False, "", QMessageBox.Yes, True, True),
+ ("123.345", "345.678", False, False, "", QMessageBox.Yes, True, True),
+ ("123", 345, True, True, "", QMessageBox.No, True, True),
+ ("123", 345, False, True, "", QMessageBox.No, True, True),
+ ("123", 345, True, True, "Continue?", QMessageBox.Yes, True, False),
+ ("123", 345, True, True, "", QMessageBox.Yes, True, False),
+ ("123.345", 345.678, True, True, "", QMessageBox.Yes, True, False),
+ ("123.345", "345.678", True, True, "", QMessageBox.Yes, True, False),
+ ("123", 345, True, True, "Continue?", QMessageBox.Yes, False, True),
+ ("123", 345, True, True, "", QMessageBox.Yes, False, False),
+ ("abc", "def", True, True, "", QMessageBox.Yes, False, False),
+ ("abc", None, True, True, "", QMessageBox.Yes, False, False),
+ (None, "def", True, True, "", QMessageBox.Yes, False, False),
+ (None, None, True, True, "", QMessageBox.Yes, False, False),
+ ],
+)
+def test_send_value(
+ qtbot,
+ monkeypatch,
+ signals,
+ initial_value,
+ press_value,
+ is_password_protected,
+ show_confirm_dialog,
+ confirm_message,
+ confirm_dialog_response,
+ is_password_validated,
+ is_value_relative,
+):
"""
Test sending a new value to the channel.
@@ -368,7 +407,7 @@ def test_send_value(qtbot, monkeypatch, signals, initial_value, press_value, is_
if show_confirm_dialog:
# Monkeypatch the confirm dialog call if popping up the dialog is enabled for testing
- monkeypatch.setattr(QMessageBox, 'exec_', lambda *args: confirm_dialog_response)
+ monkeypatch.setattr(QMessageBox, "exec_", lambda *args: confirm_dialog_response)
pydm_pushbutton.passwordProtected = is_password_protected
if is_password_protected:
@@ -376,7 +415,7 @@ def test_send_value(qtbot, monkeypatch, signals, initial_value, press_value, is_
# We assume the QInputDialog returns success. Further testing scenarios are performed at test_validate_password
plain_text_password = "$L4C_p4$$wd"
pydm_pushbutton.password = plain_text_password
- monkeypatch.setattr(QInputDialog, 'getText', lambda *args: (plain_text_password, is_password_validated))
+ monkeypatch.setattr(QInputDialog, "getText", lambda *args: (plain_text_password, is_password_validated))
send_value = pydm_pushbutton.sendValue()
if not pydm_pushbutton.pressValue or not initial_value:
@@ -398,25 +437,25 @@ def test_send_value(qtbot, monkeypatch, signals, initial_value, press_value, is_
assert signals.value == pydm_pushbutton.value + pydm_pushbutton.channeltype(pydm_pushbutton.pressValue)
-@pytest.mark.parametrize("current_channel_value, updated_value", [
- # Current channel value type is array, getting a new int value
- (np.array([123, 456]), 10),
-
- # Test if the current channel value type is int, and the widget is getting new int, float, or string value
- (10, 20),
- (10, 20.20),
- (10, "100"),
-
- # Test if the current channel value type is float, and the widget getting new int, float, or string value
- (10.10, 20.20),
- (10.10, 42),
- (10.10, "100.5"),
-
- # Test if the current channel value type is string, and the widget is getting new int, float, or string value
- ("Old str value", "New str value"),
- ("Old str value", 42),
- ("Old str value", 10.10),
-])
+@pytest.mark.parametrize(
+ "current_channel_value, updated_value",
+ [
+ # Current channel value type is array, getting a new int value
+ (np.array([123, 456]), 10),
+ # Test if the current channel value type is int, and the widget is getting new int, float, or string value
+ (10, 20),
+ (10, 20.20),
+ (10, "100"),
+ # Test if the current channel value type is float, and the widget getting new int, float, or string value
+ (10.10, 20.20),
+ (10.10, 42),
+ (10.10, "100.5"),
+ # Test if the current channel value type is string, and the widget is getting new int, float, or string value
+ ("Old str value", "New str value"),
+ ("Old str value", 42),
+ ("Old str value", 10.10),
+ ],
+)
def test_update_press_value(qtbot, signals, current_channel_value, updated_value):
"""
Test the conversion of a new press value given the existing channel type.
@@ -450,19 +489,24 @@ def test_update_press_value(qtbot, signals, current_channel_value, updated_value
# Verify the new value is assigned to be the new pressValue as a str
assert pydm_pushbutton.pressValue == str(type(current_channel_value)(updated_value))
+
# --------------------
# NEGATIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("current_channel_value, updated_value, expected_log_error", [
- (np.array([123.123, 456.456]), 10.10, "'10.1' is not a valid pressValue"),
- (np.array(["abc", "string in an array"]), "New str value", "'New str value' is not a valid pressValue"),
- (10, "New str value", "not a valid"),
- (10.10, "New str value", "not a valid"),
-])
-def test_update_press_value_incompatible_update_value(qtbot, signals, caplog, current_channel_value, updated_value,
- expected_log_error):
+@pytest.mark.parametrize(
+ "current_channel_value, updated_value, expected_log_error",
+ [
+ (np.array([123.123, 456.456]), 10.10, "'10.1' is not a valid pressValue"),
+ (np.array(["abc", "string in an array"]), "New str value", "'New str value' is not a valid pressValue"),
+ (10, "New str value", "not a valid"),
+ (10.10, "New str value", "not a valid"),
+ ],
+)
+def test_update_press_value_incompatible_update_value(
+ qtbot, signals, caplog, current_channel_value, updated_value, expected_log_error
+):
"""
Test if the widget will log the correct error message if the update value's type is incompatible with the current
data type established by the current data associated with the widget.
@@ -499,17 +543,23 @@ def test_update_press_value_incompatible_update_value(qtbot, signals, caplog, cu
assert expected_log_error in caplog.text
-
-@pytest.mark.parametrize("initial_value, press_value, release_value,", [
- (0, 1, -2),
- (123, 345, -3,),
-])
+@pytest.mark.parametrize(
+ "initial_value, press_value, release_value,",
+ [
+ (0, 1, -2),
+ (
+ 123,
+ 345,
+ -3,
+ ),
+ ],
+)
def test_send_on_release_value(qtbot, monkeypatch, signals, initial_value, press_value, release_value):
"""
Test send on release
Expectations:
- 1. If writeOnRelease is True, a value will be written on release
+ 1. If writeOnRelease is True, a value will be written on release
2. If writeOnRelease is Fale, no value will be written on the release
Parameters
@@ -543,4 +593,4 @@ def test_send_on_release_value(qtbot, monkeypatch, signals, initial_value, press
if pydm_pushbutton._write_when_release:
assert float(pydm_pushbutton.releaseValue) == signals.value
else:
- assert float(pydm_pushbutton.pressValue) == signals.value
\ No newline at end of file
+ assert float(pydm_pushbutton.pressValue) == signals.value
diff --git a/pydm/tests/widgets/test_related_display_button.py b/pydm/tests/widgets/test_related_display_button.py
index e82c05791f..6a33e688c9 100644
--- a/pydm/tests/widgets/test_related_display_button.py
+++ b/pydm/tests/widgets/test_related_display_button.py
@@ -1,20 +1,20 @@
import os
import pytest
import sys
-from qtpy.QtCore import Qt
-from qtpy.QtWidgets import QApplication, QVBoxLayout
+from qtpy.QtCore import Qt, QSize
+from qtpy.QtWidgets import QApplication
from ...utilities.stylesheet import global_style
from ...widgets.related_display_button import PyDMRelatedDisplayButton
+from ...utilities import IconFont
-test_ui_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../test_data", "test.ui")
+test_ui_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../test_data", "test.ui")
test_ui_path_with_stylesheet = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../test_data", "test_emb_style.ui")
+ os.path.dirname(os.path.realpath(__file__)), "../test_data", "test_emb_style.ui"
+)
test_ui_path_with_relative_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../test_data", "test_relative_filename_parent.ui")
+ os.path.dirname(os.path.realpath(__file__)), "../test_data", "test_relative_filename_parent.ui"
+)
+
def test_old_display_filename_property(qtbot):
# This test is mostly only checking that the related display button
@@ -31,10 +31,13 @@ def test_old_display_filename_property(qtbot):
qtbot.addWidget(button)
button._rebuild_menu()
qtbot.mouseRelease(button, Qt.LeftButton)
+
def check_title():
assert "Form" in QApplication.instance().main_window.windowTitle()
+
qtbot.waitUntil(check_title)
+
def test_press_with_filename(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -45,10 +48,47 @@ def test_press_with_filename(qtbot):
qtbot.addWidget(button)
button._rebuild_menu()
qtbot.mouseRelease(button, Qt.LeftButton)
+
+ # verify default icon is set as expected
+ DEFAULT_ICON_NAME = "file"
+ DEFAULT_ICON_SIZE = QSize(16, 16)
+
+ default_icon = IconFont().icon(DEFAULT_ICON_NAME)
+
+ default_icon_pixmap = default_icon.pixmap(DEFAULT_ICON_SIZE)
+ related_display_button_icon_pixmap = button.icon().pixmap(DEFAULT_ICON_SIZE)
+
+ assert related_display_button_icon_pixmap.toImage() == default_icon_pixmap.toImage()
+ assert button.cursor().pixmap().toImage() == default_icon_pixmap.toImage()
+
+ # verify that qt standard icons can be set through our custom property
+ style = button.style()
+ test_icon = style.standardIcon(style.SP_DesktopIcon)
+ test_icon_image = test_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ button.PyDMIcon = "SP_DesktopIcon"
+ shell_cmd_icon = button.icon()
+ shell_cmd_icon_image = shell_cmd_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ assert test_icon_image == shell_cmd_icon_image
+
+ # verify that "Font Awesome" icons can be set through our custom property
+ icon_f = IconFont()
+ test_icon = icon_f.icon("eye-slash", color=None)
+ test_icon_image = test_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ button.PyDMIcon = "eye-slash"
+ button_icon = button.icon()
+ push_btn_icon_image = button_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ assert test_icon_image == push_btn_icon_image
+
def check_title():
assert "Form" in QApplication.instance().main_window.windowTitle()
+
qtbot.waitUntil(check_title)
+
def test_press_without_filename(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -61,6 +101,7 @@ def test_press_without_filename(qtbot):
qtbot.wait(250)
assert "Form" not in QApplication.instance().main_window.windowTitle()
+
def test_no_menu_with_one_file(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -72,6 +113,7 @@ def test_no_menu_with_one_file(qtbot):
button._rebuild_menu()
assert button.menu() is None
+
def test_menu_with_additional_files(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -88,10 +130,13 @@ def test_menu_with_additional_files(qtbot):
qtbot.waitExposed(button.menu())
qtbot.mouseClick(button.menu(), Qt.LeftButton)
button.menu().actions()[0].trigger()
+
def check_title():
assert "Form" in QApplication.instance().main_window.windowTitle()
+
qtbot.waitUntil(check_title)
+
def test_menu_goes_away_when_files_removed(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -109,6 +154,7 @@ def test_menu_goes_away_when_files_removed(qtbot):
button._rebuild_menu()
assert button.menu() is None
+
def test_menu_goes_away_when_files_all_blank(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -121,6 +167,7 @@ def test_menu_goes_away_when_files_all_blank(qtbot):
button._rebuild_menu()
assert button.menu() is None
+
def test_press_with_relative_filename(qtbot):
QApplication.instance().make_main_window()
main_window = QApplication.instance().main_window
@@ -131,16 +178,22 @@ def test_press_with_relative_filename(qtbot):
# Default behavior should be to not follow symlinks (for backwards compat.).
# Same effect as: button.followSymlinks = False
qtbot.mouseRelease(button, Qt.LeftButton)
+
def check_title():
assert "Child" in QApplication.instance().main_window.windowTitle()
+
qtbot.waitUntil(check_title)
-@pytest.mark.skipif(sys.platform == "win32" and sys.version_info < (3, 8), reason="os.path.realpath on Python 3.7 and prior does not resolve symlinks on Windows")
+
+@pytest.mark.skipif(
+ sys.platform == "win32" and sys.version_info < (3, 8),
+ reason="os.path.realpath on Python 3.7 and prior does not resolve symlinks on Windows",
+)
def test_press_with_relative_filename_and_symlink(qtbot, tmp_path):
symlinked_ui_file = tmp_path / "test_ui_with_relative_path.ui"
try:
os.symlink(test_ui_path_with_relative_path, symlinked_ui_file)
- except:
+ except Exception:
pytest.skip("Unable to create a symlink for testing purposes.")
QApplication.instance().make_main_window()
@@ -151,10 +204,13 @@ def test_press_with_relative_filename_and_symlink(qtbot, tmp_path):
button = main_window.home_widget.relatedDisplayButton
button.followSymlinks = True
qtbot.mouseRelease(button, Qt.LeftButton)
+
def check_title():
assert "Child" in QApplication.instance().main_window.windowTitle()
+
qtbot.waitUntil(check_title)
+
def test_no_pydm_app_stylesheet(monkeypatch, qtbot):
local_is_pydm_app = True
diff --git a/pydm/tests/widgets/test_rules.py b/pydm/tests/widgets/test_rules.py
index df6e704498..12c9a3d8dd 100644
--- a/pydm/tests/widgets/test_rules.py
+++ b/pydm/tests/widgets/test_rules.py
@@ -1,6 +1,5 @@
import logging
import pytest
-import time
import weakref
from ...widgets.rules import RulesDispatcher
@@ -44,9 +43,14 @@ def test_unregister(qtbot):
widget = PyDMLabel()
qtbot.addWidget(widget)
- rules = [{'name': 'Rule #1', 'property': 'Visible',
- 'expression': 'ch[0] < 1',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Visible",
+ "expression": "ch[0] < 1",
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
dispatcher = RulesDispatcher()
dispatcher.register(widget, rules)
@@ -72,9 +76,14 @@ def test_rules_not_connected(qtbot, caplog):
widget.show()
assert widget.isVisible()
- rules = [{'name': 'Rule #1', 'property': 'Visible',
- 'expression': 'ch[0] < 1',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Visible",
+ "expression": "ch[0] < 1",
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
dispatcher = RulesDispatcher()
dispatcher.register(widget, rules)
@@ -82,7 +91,7 @@ def test_rules_not_connected(qtbot, caplog):
re = dispatcher.rules_engine
assert weakref.ref(widget) in re.widget_map
assert len(re.widget_map[weakref.ref(widget)]) == 1
- assert re.widget_map[weakref.ref(widget)][0]['rule'] == rules[0]
+ assert re.widget_map[weakref.ref(widget)][0]["rule"] == rules[0]
with caplog.at_level(logging.DEBUG):
re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value=1)
@@ -102,14 +111,20 @@ def test_rules_ok(qtbot, caplog):
caplog : fixture
To capture the log messages
"""
+
widget = PyDMLabel()
qtbot.addWidget(widget)
widget.show()
assert widget.isVisible()
- rules = [{'name': 'Rule #1', 'property': 'Visible',
- 'expression': 'ch[0] < 1',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Visible",
+ "expression": "ch[0] < 1",
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
dispatcher = RulesDispatcher()
dispatcher.register(widget, rules)
@@ -117,27 +132,20 @@ def test_rules_ok(qtbot, caplog):
re = dispatcher.rules_engine
assert weakref.ref(widget) in re.widget_map
assert len(re.widget_map[weakref.ref(widget)]) == 1
- assert re.widget_map[weakref.ref(widget)][0]['rule'] == rules[0]
+ assert re.widget_map[weakref.ref(widget)][0]["rule"] == rules[0]
blocker = qtbot.waitSignal(re.rule_signal, timeout=1000)
re.callback_conn(weakref.ref(widget), 0, 0, value=True)
re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value=5)
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is True
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is True
blocker.wait()
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is False
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is False
assert not widget.isVisible()
-@pytest.mark.parametrize(
- "use_enum, visible",
- [
- (None, True),
- (True, True),
- (False, False)
- ]
-)
+@pytest.mark.parametrize("use_enum, visible", [(None, True), (True, True), (False, False)])
def test_rules_enums(use_enum, visible, qtbot, caplog):
"""
Test the rules mechanism with enums.
@@ -154,9 +162,14 @@ def test_rules_enums(use_enum, visible, qtbot, caplog):
widget.show()
assert widget.isVisible()
- rules = [{'name': 'Rule #1', 'property': 'Visible',
- 'expression': 'ch[0] == "RUN"',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Visible",
+ "expression": 'ch[0] == "RUN"',
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
if use_enum is not None:
rules[0]["channels"][0]["use_enum"] = use_enum
@@ -166,17 +179,17 @@ def test_rules_enums(use_enum, visible, qtbot, caplog):
re = dispatcher.rules_engine
assert weakref.ref(widget) in re.widget_map
assert len(re.widget_map[weakref.ref(widget)]) == 1
- assert re.widget_map[weakref.ref(widget)][0]['rule'] == rules[0]
+ assert re.widget_map[weakref.ref(widget)][0]["rule"] == rules[0]
# First we test that we receive a value but we don't have enums yet
blocker = qtbot.waitSignal(re.rule_signal, timeout=1000)
re.callback_conn(weakref.ref(widget), 0, 0, value=True)
re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value=1)
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is True
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is True
blocker.wait()
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is False
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is False
assert not widget.isVisible()
blocker = qtbot.waitSignal(re.rule_signal, timeout=1000)
@@ -185,10 +198,10 @@ def test_rules_enums(use_enum, visible, qtbot, caplog):
# value was sent making the widget visible on condition of use_enum
re.callback_conn(weakref.ref(widget), 0, 0, value=True)
re.callback_enum(weakref.ref(widget), 0, 0, enums=["STOP", "RUN"])
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is True
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is True
blocker.wait()
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is False
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is False
assert widget.isVisible() == visible
@@ -208,9 +221,14 @@ def test_rules_invalid_expr(qtbot, caplog):
widget.show()
assert widget.isVisible()
- rules = [{'name': 'Rule #1', 'property': 'Visible',
- 'expression': 'ch[0] < 1',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Visible",
+ "expression": "ch[0] < 1",
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
dispatcher = RulesDispatcher()
dispatcher.register(widget, rules)
@@ -218,15 +236,15 @@ def test_rules_invalid_expr(qtbot, caplog):
re = dispatcher.rules_engine
assert weakref.ref(widget) in re.widget_map
assert len(re.widget_map[weakref.ref(widget)]) == 1
- assert re.widget_map[weakref.ref(widget)][0]['rule'] == rules[0]
+ assert re.widget_map[weakref.ref(widget)][0]["rule"] == rules[0]
caplog.clear()
- rules[0]['expression'] = 'foo'
+ rules[0]["expression"] = "foo"
dispatcher.register(widget, rules)
assert len(re.widget_map[weakref.ref(widget)]) == 1
re.callback_conn(weakref.ref(widget), 0, 0, value=True)
- re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value='a')
+ re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value="a")
# Wait for rule to execute but keep app responsive
qtbot.wait(1000)
@@ -255,10 +273,15 @@ def test_rules_initial_value(qtbot, caplog):
qtbot.addWidget(widget)
widget.show()
- rules = [{'name': 'Rule #1', 'property': 'Text',
- 'expression': 'str(ch[0])',
- 'initial_value': 'Initial Value Test',
- 'channels': [{'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules = [
+ {
+ "name": "Rule #1",
+ "property": "Text",
+ "expression": "str(ch[0])",
+ "initial_value": "Initial Value Test",
+ "channels": [{"channel": "ca://TESTRULES:Float", "trigger": True}],
+ }
+ ]
dispatcher = RulesDispatcher()
dispatcher.register(widget, rules)
@@ -266,14 +289,14 @@ def test_rules_initial_value(qtbot, caplog):
re = dispatcher.rules_engine
assert weakref.ref(widget) in re.widget_map
assert len(re.widget_map[weakref.ref(widget)]) == 1
- assert re.widget_map[weakref.ref(widget)][0]['rule'] == rules[0]
- assert widget.text() == 'Initial Value Test'
+ assert re.widget_map[weakref.ref(widget)][0]["rule"] == rules[0]
+ assert widget.text() == "Initial Value Test"
blocker = qtbot.waitSignal(re.rule_signal, timeout=1000)
re.callback_conn(weakref.ref(widget), 0, 0, value=True)
re.callback_value(weakref.ref(widget), 0, 0, trigger=True, value=5)
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is True
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is True
blocker.wait()
- assert re.widget_map[weakref.ref(widget)][0]['calculate'] is False
+ assert re.widget_map[weakref.ref(widget)][0]["calculate"] is False
assert widget.text() == str(5)
diff --git a/pydm/tests/widgets/test_rules_editor.py b/pydm/tests/widgets/test_rules_editor.py
index 3fc13e5fe4..0a77693fd0 100644
--- a/pydm/tests/widgets/test_rules_editor.py
+++ b/pydm/tests/widgets/test_rules_editor.py
@@ -16,6 +16,7 @@ class DummyWidget:
A stub class to play with the rules editor and not touch
the RulesEngine
"""
+
RULE_PROPERTIES = PyDMPrimitiveWidget.RULE_PROPERTIES
DEFAULT_RULE_PROPERTY = PyDMPrimitiveWidget.DEFAULT_RULE_PROPERTY
@@ -32,14 +33,7 @@ def rules(self, new_rules):
self._rules = new_rules
-@pytest.mark.parametrize(
- "use_enum, visible",
- [
- (None, False),
- (True, True),
- (False, False)
- ]
-)
+@pytest.mark.parametrize("use_enum, visible", [(None, False), (True, True), (False, False)])
def test_rules_editor(use_enum, visible, qtbot, monkeypatch):
"""
Test the rules editor in general.
@@ -70,11 +64,15 @@ def test_rules_editor(use_enum, visible, qtbot, monkeypatch):
empty.cancelChanges()
# Create the rules data for the widget
- rules_list = [{'name': 'Rule #1', 'property': 'Enable',
- 'initial_value': 'False',
- 'expression': 'ch[0] > 1',
- 'channels': [
- {'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules_list = [
+ {
+ "name": "Rule #1",
+ "property": "Enable",
+ "initial_value": "False",
+ "expression": "ch[0] > 1",
+ "channels": [{"channel": "ca://MTEST:Float", "trigger": True}],
+ }
+ ]
if use_enum is not None:
rules_list[0]["channels"][0]["use_enum"] = use_enum
@@ -96,75 +94,75 @@ def test_rules_editor(use_enum, visible, qtbot, monkeypatch):
re.lst_rules.setCurrentRow(0)
re.load_from_list()
assert re.frm_edit.isEnabled()
- assert re.txt_name.text() == 'Rule #1'
- assert re.cmb_property.currentText() == 'Enable'
+ assert re.txt_name.text() == "Rule #1"
+ assert re.cmb_property.currentText() == "Enable"
assert re.tbl_channels.rowCount() == 1
- assert re.tbl_channels.item(0, 0).text() == 'ca://MTEST:Float'
+ assert re.tbl_channels.item(0, 0).text() == "ca://MTEST:Float"
assert re.tbl_channels.item(0, 1).checkState() == QtCore.Qt.Checked
assert re.tbl_channels.item(0, 2).checkState() == ch_choices[visible]
- assert re.lbl_expected_type.text() == 'bool'
- assert re.txt_expression.text() == 'ch[0] > 1'
- assert re.txt_initial_value.text() == 'False'
+ assert re.lbl_expected_type.text() == "bool"
+ assert re.txt_expression.text() == "ch[0] > 1"
+ assert re.txt_initial_value.text() == "False"
- qtbot.keyClicks(re.txt_name, '-Test')
+ qtbot.keyClicks(re.txt_name, "-Test")
qtbot.keyClick(re.txt_name, QtCore.Qt.Key_Return)
- assert re.txt_name.text() == 'Rule #1-Test'
- assert re.rules[0]['name'] == 'Rule #1-Test'
+ assert re.txt_name.text() == "Rule #1-Test"
+ assert re.rules[0]["name"] == "Rule #1-Test"
qtbot.mouseClick(re.btn_add_channel, QtCore.Qt.LeftButton)
re.tbl_channels.item(1, 0).setText("ca://TEST")
- assert re.rules[0]['channels'][1]['channel'] == 'ca://TEST'
- assert re.rules[0]['channels'][1]['trigger'] is False
- assert re.rules[0]['channels'][1]['use_enum'] is True
+ assert re.rules[0]["channels"][1]["channel"] == "ca://TEST"
+ assert re.rules[0]["channels"][1]["trigger"] is False
+ assert re.rules[0]["channels"][1]["use_enum"] is True
re.txt_expression.clear()
- qtbot.keyClicks(re.txt_expression, 'ch[0] < 1')
+ qtbot.keyClicks(re.txt_expression, "ch[0] < 1")
qtbot.keyClick(re.txt_expression, QtCore.Qt.Key_Return)
- assert re.txt_expression.text() == 'ch[0] < 1'
- assert re.rules[0]['expression'] == 'ch[0] < 1'
+ assert re.txt_expression.text() == "ch[0] < 1"
+ assert re.rules[0]["expression"] == "ch[0] < 1"
re.txt_initial_value.clear()
- qtbot.keyClicks(re.txt_initial_value, 'True')
+ qtbot.keyClicks(re.txt_initial_value, "True")
qtbot.keyClick(re.txt_initial_value, QtCore.Qt.Key_Return)
- assert re.txt_initial_value.text() == 'True'
- assert re.rules[0]['initial_value'] == 'True'
+ assert re.txt_initial_value.text() == "True"
+ assert re.rules[0]["initial_value"] == "True"
# Test Delete with Confirm - NO
assert re.tbl_channels.rowCount() == 2
re.tbl_channels.setRangeSelected(QTableWidgetSelectionRange(1, 0, 1, 1), True)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.No)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.No)
qtbot.mouseClick(re.btn_del_channel, QtCore.Qt.LeftButton)
assert re.tbl_channels.rowCount() == 2
# Test Delete with Confirm - YES
re.tbl_channels.setRangeSelected(QTableWidgetSelectionRange(1, 0, 1, 1), True)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(re.btn_del_channel, QtCore.Qt.LeftButton)
assert re.tbl_channels.rowCount() == 1
- assert len(re.rules[0]['channels']) == 1
+ assert len(re.rules[0]["channels"]) == 1
# Test Delete with Invalid Selection
re.tbl_channels.setRangeSelected(QTableWidgetSelectionRange(1, 0, 1, 1), True)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(re.btn_del_channel, QtCore.Qt.LeftButton)
assert re.tbl_channels.rowCount() == 1
- assert len(re.rules[0]['channels']) == 1
+ assert len(re.rules[0]["channels"]) == 1
qtbot.mouseClick(re.btn_add_rule, QtCore.Qt.LeftButton)
assert re.lst_rules.count() == 2
assert re.frm_edit.isEnabled()
- assert re.txt_name.text() == 'New Rule'
+ assert re.txt_name.text() == "New Rule"
assert re.cmb_property.currentText() == widget.DEFAULT_RULE_PROPERTY
assert re.tbl_channels.rowCount() == 0
- assert re.txt_expression.text() == ''
+ assert re.txt_expression.text() == ""
qtbot.mouseClick(re.btn_add_channel, QtCore.Qt.LeftButton)
- assert re.tbl_channels.item(0, 0).text() == ''
+ assert re.tbl_channels.item(0, 0).text() == ""
assert re.tbl_channels.item(0, 1).checkState() == QtCore.Qt.Checked
assert re.tbl_channels.item(0, 2).checkState() == QtCore.Qt.Checked
qtbot.mouseClick(re.btn_add_channel, QtCore.Qt.LeftButton)
- assert re.tbl_channels.item(1, 0).text() == ''
+ assert re.tbl_channels.item(1, 0).text() == ""
assert re.tbl_channels.item(1, 1).checkState() == QtCore.Qt.Unchecked
assert re.tbl_channels.item(1, 2).checkState() == QtCore.Qt.Checked
@@ -173,24 +171,24 @@ def test_rules_editor(use_enum, visible, qtbot, monkeypatch):
re.lst_rules.setCurrentRow(1)
# Delete Rule 1 - Confirm - NO
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.No)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.No)
qtbot.mouseClick(re.btn_del_rule, QtCore.Qt.LeftButton)
assert re.lst_rules.count() == 2
# Delete Rule 1 - Confirm - YES
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(re.btn_del_rule, QtCore.Qt.LeftButton)
assert re.frm_edit.isEnabled() is False
assert re.lst_rules.count() == 1
re.lst_rules.setCurrentRow(0)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(re.btn_del_rule, QtCore.Qt.LeftButton)
assert re.frm_edit.isEnabled() is False
assert re.lst_rules.count() == 0
# Delete Empty List - Confirm - YES
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(re.btn_del_rule, QtCore.Qt.LeftButton)
assert re.frm_edit.isEnabled() is False
assert re.lst_rules.count() == 0
@@ -206,40 +204,45 @@ def test_rules_editor_data_valid(qtbot):
qtbot : fixture
pytest-qt window for widget test
"""
+
def validate(expected_status, expected_msg, definition):
status, msg = RulesEditor.is_data_valid(definition)
assert status == expected_status
assert expected_msg in msg
- rules_list = [{'name': 'Rule #1', 'property': 'Enable',
- 'expression': 'ch[0] > 1',
- 'channels': [
- {'channel': 'ca://MTEST:Float', 'trigger': True}]}]
+ rules_list = [
+ {
+ "name": "Rule #1",
+ "property": "Enable",
+ "expression": "ch[0] > 1",
+ "channels": [{"channel": "ca://MTEST:Float", "trigger": True}],
+ }
+ ]
- validate(True, '', rules_list)
+ validate(True, "", rules_list)
rules_original = copy.deepcopy(rules_list)
- rules_original[0]['name'] = ''
- validate(False, 'has no name', rules_original)
+ rules_original[0]["name"] = ""
+ validate(False, "has no name", rules_original)
rules_original = copy.deepcopy(rules_list)
- rules_original[0]['expression'] = ''
- validate(False, 'has no expression', rules_original)
+ rules_original[0]["expression"] = ""
+ validate(False, "has no expression", rules_original)
rules_original = copy.deepcopy(rules_list)
- old_channels = rules_original[0]['channels']
- rules_original[0]['channels'] = []
- validate(False, 'has no channel', rules_original)
- rules_original[0]['channels'] = old_channels
+ old_channels = rules_original[0]["channels"]
+ rules_original[0]["channels"] = []
+ validate(False, "has no channel", rules_original)
+ rules_original[0]["channels"] = old_channels
- rules_original[0]['channels'][0]['trigger'] = False
- validate(False, 'has no channel for trigger', rules_original)
+ rules_original[0]["channels"][0]["trigger"] = False
+ validate(False, "has no channel for trigger", rules_original)
- rules_original[0]['channels'][0]['channel'] = None
- validate(False, 'Ch. #0 has no channel.', rules_original)
- rules_original[0]['channels'][0]['channel'] = ''
- validate(False, 'Ch. #0 has no channel.', rules_original)
+ rules_original[0]["channels"][0]["channel"] = None
+ validate(False, "Ch. #0 has no channel.", rules_original)
+ rules_original[0]["channels"][0]["channel"] = ""
+ validate(False, "Ch. #0 has no channel.", rules_original)
def test_rules_editor_open_help(qtbot, monkeypatch):
@@ -267,10 +270,58 @@ def test_rules_editor_open_help(qtbot, monkeypatch):
url = re.open_help(open=False)
base_url = os.getenv("PYDM_DOCS_URL", "https://slaclab.github.io/pydm")
- exp_url = base_url+"/widgets/widget_rules/index.html"
+ exp_url = base_url + "/widgets/widget_rules/index.html"
assert url == exp_url
- monkeypatch.setattr(webbrowser, 'open',
- lambda *args, **kwargs: '')
+ monkeypatch.setattr(webbrowser, "open", lambda *args, **kwargs: "")
re.open_help()
re.cancelChanges()
+
+
+def test_rules_editor_add_notes(qtbot, monkeypatch):
+ """
+ Test adding notes for a widget's rules
+
+ Parameters
+ ----------
+ qtbot : fixture
+ pytest-qt window for widget test
+ monkeypatch : fixture
+ To override dialog behaviors
+ """
+ # Create the base widget
+ widget = DummyWidget()
+
+ # Create the rules data for the widget
+ rules_list = [
+ {
+ "name": "Rule #1",
+ "property": "Enable",
+ "initial_value": "False",
+ "expression": "ch[0] > 1",
+ "channels": [{"channel": "ca://MTEST:Float", "trigger": True}],
+ }
+ ]
+ # Add the rules to the widget
+ widget.rules = json.dumps(rules_list)
+
+ # Create a new Editor Window
+ re = RulesEditor(widget)
+ qtbot.addWidget(re)
+ re.show()
+
+ qtbot.waitExposed(re, timeout=5000)
+
+ re.lst_rules.setCurrentRow(0)
+
+ # make sure notes are saved and loaded correctly
+ # in the scope of the current rules-editor window
+ testing_text = "Testing adding note!"
+ re.open_notes_window()
+ re.notes_edit.setPlainText(testing_text)
+ re.save_notes()
+ re.open_notes_window()
+ loaded_text_from_curr_editor_instance = re.notes_edit.toPlainText()
+ assert loaded_text_from_curr_editor_instance == testing_text
+
+ re.cancelChanges()
diff --git a/pydm/tests/widgets/test_scatterplot.py b/pydm/tests/widgets/test_scatterplot.py
index 7c68cf889a..fd2de29790 100644
--- a/pydm/tests/widgets/test_scatterplot.py
+++ b/pydm/tests/widgets/test_scatterplot.py
@@ -2,10 +2,8 @@
import logging
import numpy as np
from collections import OrderedDict
-from pyqtgraph import AxisItem, BarGraphItem
-from unittest import mock
from ...widgets.channel import PyDMChannel
-from ...widgets.scatterplot import ScatterPlotCurveItem, PyDMScatterPlot, MINIMUM_BUFFER_SIZE, DEFAULT_BUFFER_SIZE
+from ...widgets.scatterplot import ScatterPlotCurveItem, MINIMUM_BUFFER_SIZE, DEFAULT_BUFFER_SIZE
from ...utilities import remove_protocol
logger = logging.getLogger(__name__)
@@ -21,22 +19,20 @@
(None, None, None, None, None),
("ca://test_value:FloatY", "ca://test_value:FloatX", ScatterPlotCurveItem.REDRAW_ON_BOTH, None, ""),
("ca://test_value:FloatY", "ca://test_value:FloatX", None, "ca://test_value:Int", "test_name"),
- ]
+ ],
)
def test_scatterplotcurveitem_construct(qtbot, y_addr, x_addr, redraw_mode, bufferSizeChannelAddress, name):
- plot_curve_item = ScatterPlotCurveItem(y_addr,
- x_addr,
- redraw_mode=redraw_mode,
- bufferSizeChannelAddress=bufferSizeChannelAddress,
- name=name)
+ plot_curve_item = ScatterPlotCurveItem(
+ y_addr, x_addr, redraw_mode=redraw_mode, bufferSizeChannelAddress=bufferSizeChannelAddress, name=name
+ )
assert plot_curve_item is not None
assert isinstance(plot_curve_item, ScatterPlotCurveItem)
assert plot_curve_item._bufferSize == DEFAULT_BUFFER_SIZE
assert plot_curve_item.redraw_mode == redraw_mode or ScatterPlotCurveItem.REDRAW_ON_EITHER
assert np.array_equal(
- plot_curve_item.data_buffer,
- np.zeros((2, plot_curve_item._bufferSize), order='f', dtype=float))
+ plot_curve_item.data_buffer, np.zeros((2, plot_curve_item._bufferSize), order="f", dtype=float)
+ )
for item in "x_connected y_connected bufferSizeChannel_connected".split():
assert getattr(plot_curve_item, item) is False
assert plot_curve_item.points_accumulated == 0
@@ -64,14 +60,12 @@ def test_scatterplotcurveitem_construct(qtbot, y_addr, x_addr, redraw_mode, buff
(None, None, None, None, None),
("ca://test_value:FloatY", "ca://test_value:FloatX", ScatterPlotCurveItem.REDRAW_ON_BOTH, None, ""),
("ca://test_value:FloatY", "ca://test_value:FloatX", None, "ca://test_value:Int", "test_name"),
- ]
+ ],
)
def test_scatterplotcurveitem_to_dict(qtbot, y_addr, x_addr, redraw_mode, bufferSizeChannelAddress, name):
- plot_curve_item = ScatterPlotCurveItem(y_addr,
- x_addr,
- redraw_mode=redraw_mode,
- bufferSizeChannelAddress=bufferSizeChannelAddress,
- name=name)
+ plot_curve_item = ScatterPlotCurveItem(
+ y_addr, x_addr, redraw_mode=redraw_mode, bufferSizeChannelAddress=bufferSizeChannelAddress, name=name
+ )
dictionary = plot_curve_item.to_dict()
assert isinstance(dictionary, OrderedDict)
@@ -87,15 +81,9 @@ def test_scatterplotcurveitem_to_dict(qtbot, y_addr, x_addr, redraw_mode, buffer
assert dictionary["name"] == name
-@pytest.mark.parametrize("new_address", [
- "new_address",
- "",
- None
-])
+@pytest.mark.parametrize("new_address", ["new_address", "", None])
def test_scatterplotcurveitem_properties_and_setters(qtbot, new_address):
- plot_curve_item = ScatterPlotCurveItem(new_address,
- new_address,
- bufferSizeChannelAddress=new_address)
+ plot_curve_item = ScatterPlotCurveItem(new_address, new_address, bufferSizeChannelAddress=new_address)
assert plot_curve_item.x_address in (None, new_address)
assert plot_curve_item.y_address in (None, new_address)
@@ -124,17 +112,17 @@ def test_scatterplotcurveitem_connection_state_changed(qtbot, signals):
@pytest.mark.parametrize(
"redraw_mode, new_data",
[
- (ScatterPlotCurveItem.REDRAW_ON_EITHER, [(0,0), (1,1), (2,2), (2.5,3.1)]),
- (ScatterPlotCurveItem.REDRAW_ON_BOTH, [(0,0), (1,1), (2,2), (2.5,3.1)]),
- (ScatterPlotCurveItem.REDRAW_ON_X, [(0,0), (1,1), (2,2), (2.5,3.1)]),
- (ScatterPlotCurveItem.REDRAW_ON_Y, [(0,0), (1,1), (2,2), (2.5,3.1)]),
- ]
+ (ScatterPlotCurveItem.REDRAW_ON_EITHER, [(0, 0), (1, 1), (2, 2), (2.5, 3.1)]),
+ (ScatterPlotCurveItem.REDRAW_ON_BOTH, [(0, 0), (1, 1), (2, 2), (2.5, 3.1)]),
+ (ScatterPlotCurveItem.REDRAW_ON_X, [(0, 0), (1, 1), (2, 2), (2.5, 3.1)]),
+ (ScatterPlotCurveItem.REDRAW_ON_Y, [(0, 0), (1, 1), (2, 2), (2.5, 3.1)]),
+ ],
)
def test_scatterplotcurveitem_receive_values(qtbot, signals, redraw_mode, new_data):
# REDRAW_ON_X, REDRAW_ON_Y, REDRAW_ON_EITHER, REDRAW_ON_BOTH
plot_curve_item = ScatterPlotCurveItem(None, None, redraw_mode=redraw_mode)
- expected_data_buffer = np.zeros((2, plot_curve_item._bufferSize), order='f', dtype=float)
+ expected_data_buffer = np.zeros((2, plot_curve_item._bufferSize), order="f", dtype=float)
expected_data_buffer[0] = plot_curve_item.data_buffer[0]
assert np.array_equal(expected_data_buffer, plot_curve_item.data_buffer)
@@ -145,11 +133,11 @@ def test_scatterplotcurveitem_receive_values(qtbot, signals, redraw_mode, new_da
plot_curve_item.receiveXValue(new_x)
assert plot_curve_item.latest_x_value == new_x
- assert plot_curve_item.points_accumulated == 2*i
+ assert plot_curve_item.points_accumulated == 2 * i
plot_curve_item.receiveYValue(new_y)
assert plot_curve_item.latest_y_value == new_y
- assert plot_curve_item.points_accumulated == 2*i + 1
+ assert plot_curve_item.points_accumulated == 2 * i + 1
def test_scatterplotcurve_initialize_buffer(qtbot):
@@ -158,18 +146,21 @@ def test_scatterplotcurve_initialize_buffer(qtbot):
plot_curve_item.initialize_buffer()
assert plot_curve_item.points_accumulated == 0
- expected_data_buffer = np.zeros((2, plot_curve_item._bufferSize), order='f', dtype=float)
+ expected_data_buffer = np.zeros((2, plot_curve_item._bufferSize), order="f", dtype=float)
expected_data_buffer[0] = plot_curve_item.data_buffer[0]
assert np.array_equal(expected_data_buffer, plot_curve_item.data_buffer)
-@pytest.mark.parametrize("new_buffer_size, expected_set_buffer_size", [
- (0, MINIMUM_BUFFER_SIZE),
- (-5, MINIMUM_BUFFER_SIZE),
- (100, 100),
- (MINIMUM_BUFFER_SIZE + 1, MINIMUM_BUFFER_SIZE + 1)
-])
+@pytest.mark.parametrize(
+ "new_buffer_size, expected_set_buffer_size",
+ [
+ (0, MINIMUM_BUFFER_SIZE),
+ (-5, MINIMUM_BUFFER_SIZE),
+ (100, 100),
+ (MINIMUM_BUFFER_SIZE + 1, MINIMUM_BUFFER_SIZE + 1),
+ ],
+)
def test_scatterplotcurve_get_set_reset_buffer_size(qtbot, new_buffer_size, expected_set_buffer_size):
plot_curve_item = ScatterPlotCurveItem(None, None)
@@ -188,9 +179,9 @@ def test_scatterplotcurve_get_set_reset_buffer_size(qtbot, new_buffer_size, expe
(None, None, DEFAULT_BUFFER_SIZE),
("", None, DEFAULT_BUFFER_SIZE),
("ca://test_value:Int", None, DEFAULT_BUFFER_SIZE),
- ]
+ ],
)
-def test_scatterplotcurve_get_set_reset_buffer_size(qtbot, addr, new_size, expected_buffer_size):
+def test_scatterplotcurve_get_set_reset_buffer_size_channel_addr(qtbot, addr, new_size, expected_buffer_size):
plot_curve_item = ScatterPlotCurveItem(None, None, bufferSizeChannelAddress=addr)
assert plot_curve_item.getBufferSize() == DEFAULT_BUFFER_SIZE
diff --git a/pydm/tests/widgets/test_shell_command.py b/pydm/tests/widgets/test_shell_command.py
index 57356b09f9..4b1bd590ec 100644
--- a/pydm/tests/widgets/test_shell_command.py
+++ b/pydm/tests/widgets/test_shell_command.py
@@ -18,12 +18,16 @@
# POSITIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("command, title", [
- ("foo", None),
- ("", None),
- (None, None),
- (["foo", "bar"], ["A", "B"]),
-])
+
+@pytest.mark.parametrize(
+ "command, title",
+ [
+ ("foo", None),
+ ("", None),
+ (None, None),
+ (["foo", "bar"], ["A", "B"]),
+ ],
+)
def test_construct(qtbot, command, title):
"""
Test the construct of the widget.
@@ -57,7 +61,7 @@ def test_construct(qtbot, command, title):
assert pydm_shell_command._titles == title
else:
assert pydm_shell_command._titles == []
-
+
DEFAULT_ICON_NAME = "cog"
DEFAULT_ICON_SIZE = QSize(16, 16)
@@ -69,12 +73,36 @@ def test_construct(qtbot, command, title):
assert shell_cmd_icon_pixmap.toImage() == default_icon_pixmap.toImage()
assert pydm_shell_command.cursor().pixmap().toImage() == default_icon_pixmap.toImage()
+ # verify that qt standard icons can be set through our custom property
+ style = pydm_shell_command.style()
+ test_icon = style.standardIcon(style.SP_DesktopIcon)
+ test_icon_image = test_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ pydm_shell_command.PyDMIcon = "SP_DesktopIcon"
+ shell_cmd_icon = pydm_shell_command.icon()
+ shell_cmd_icon_image = shell_cmd_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ assert test_icon_image == shell_cmd_icon_image
+
+ # verify that "Font Awesome" icons can be set through our custom property
+ icon_f = IconFont()
+ test_icon = icon_f.icon("eye-slash", color=None)
+ test_icon_image = test_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ pydm_shell_command.PyDMIcon = "eye-slash"
+ shell_cmd_icon = pydm_shell_command.icon()
+ shell_cmd_icon_image = shell_cmd_icon.pixmap(DEFAULT_ICON_SIZE).toImage()
+
+ assert test_icon_image == shell_cmd_icon_image
+
+
def test_deprecated_command_property_with_no_commands(qtbot):
pydm_shell_command = PyDMShellCommand()
qtbot.addWidget(pydm_shell_command)
pydm_shell_command.command = "test"
assert pydm_shell_command.commands == ["test"]
+
def test_deprecated_command_property_with_commands(qtbot):
pydm_shell_command = PyDMShellCommand()
qtbot.addWidget(pydm_shell_command)
@@ -83,46 +111,54 @@ def test_deprecated_command_property_with_commands(qtbot):
pydm_shell_command.command = "This shouldn't work"
assert pydm_shell_command.commands == existing_commands
+
def test_no_crash_without_any_commands(qtbot):
- pydm_shell_command = PyDMShellCommand()
- qtbot.addWidget(pydm_shell_command)
- pydm_shell_command.commands = None
- qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
+ pydm_shell_command = PyDMShellCommand()
+ qtbot.addWidget(pydm_shell_command)
+ pydm_shell_command.commands = None
+ qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
+
def test_no_crash_with_none_command(qtbot):
- pydm_shell_command = PyDMShellCommand()
- qtbot.addWidget(pydm_shell_command)
- pydm_shell_command.command = None
- qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
+ pydm_shell_command = PyDMShellCommand()
+ qtbot.addWidget(pydm_shell_command)
+ pydm_shell_command.command = None
+ qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
def test_no_error_without_env_variable(qtbot, caplog):
- """ Verify that the shell command works when the environment variable property is saved as an empty string """
+ """Verify that the shell command works when the environment variable property is saved as an empty string"""
pydm_shell_command = PyDMShellCommand()
qtbot.addWidget(pydm_shell_command)
pydm_shell_command.command = "echo hello"
pydm_shell_command.environmentVariables = ""
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
- assert 'error' not in caplog.text.lower()
-
-
-@pytest.mark.parametrize("cmd, retcode, stdout", [
- (["choice /c yn /d n /t 0"] if platform.system() == "Windows" else ["sleep 0"],
- [2] if platform.system() == "Windows" else [0], ["[Y,N]?N"] if platform.system() == "Windows" else [""]),
- (["pydm_shell_invalid_command_test invalid command"], [None], [""]),
- (["echo hello", "echo world"], [0, 0], ["hello", "world"])
-])
+ assert "error" not in caplog.text.lower()
+
+
+@pytest.mark.parametrize(
+ "cmd, retcode, stdout",
+ [
+ (
+ ["choice /c yn /d n /t 0"] if platform.system() == "Windows" else ["sleep 0"],
+ [2] if platform.system() == "Windows" else [0],
+ ["[Y,N]?N"] if platform.system() == "Windows" else [""],
+ ),
+ (["pydm_shell_invalid_command_test invalid command"], [None], [""]),
+ (["echo hello", "echo world"], [0, 0], ["hello", "world"]),
+ ],
+)
def test_mouse_release_event(qtbot, caplog, cmd, retcode, stdout):
"""
Test to ensure the widget's triggering of the Mouse Release event.
-
+
Expectations if len(cmd) == 1:
1. The mouse release will trigger the current shell command being assigned to the widget to execute
2. If the command is not valid, there will be an error message in the log
3. If the command is valid, there will be output in stdout (the result could be a success or failure, but at least
the command will send out text to stdout)
4. If the command is None or empty, there will be no output to stdout.
-
+
Expectations if len(cmd) > 1:
1. The mouse press will cause the widget to set its 'menu' attribute to an instance of QMenu.
2. Triggering each menu item runs the right command.
@@ -154,7 +190,7 @@ def check_command_output(command, expected_retcode, expected_stdout):
assert "Error in shell command" in caplog.text
pydm_shell_command.commands = cmd
-
+
if len(cmd) > 1:
for current_command, expected_retcode, expected_stdout in zip(cmd, retcode, stdout):
# We can't actually do the click and show the menu - it halts the test and waits
@@ -174,10 +210,13 @@ def check_command_output(command, expected_retcode, expected_stdout):
assert pydm_shell_command.process is None
-@pytest.mark.parametrize("allow_multiple", [
- True,
- False,
-])
+@pytest.mark.parametrize(
+ "allow_multiple",
+ [
+ True,
+ False,
+ ],
+)
def test_execute_multiple_commands(qtbot, signals, caplog, allow_multiple):
"""
Test the widget's ability to execute multiple shell commands when this setting is enabled.
@@ -230,11 +269,11 @@ def test_env_var(qtbot):
os.environ["PYDM_TEST_ENV_VAR"] = "this is a pydm test"
cmd = "echo Test: $PYDM_TEST_ENV_VAR"
- if platform.system() == 'Windows':
+ if platform.system() == "Windows":
cmd = "echo Test: %PYDM_TEST_ENV_VAR%"
pydm_shell_command.commands = [cmd]
qtbot.mouseClick(pydm_shell_command, QtCore.Qt.LeftButton)
stdout, stderr = pydm_shell_command.process.communicate()
assert pydm_shell_command.process.returncode == 0
- assert stdout.decode('utf-8') == "Test: this is a pydm test\n"
+ assert stdout.decode("utf-8") == "Test: this is a pydm test\n"
diff --git a/pydm/tests/widgets/test_slider.py b/pydm/tests/widgets/test_slider.py
index 27349d2b66..20df0db6e3 100644
--- a/pydm/tests/widgets/test_slider.py
+++ b/pydm/tests/widgets/test_slider.py
@@ -5,7 +5,7 @@
import numpy as np
from qtpy.QtWidgets import QLabel, QSlider, QVBoxLayout, QHBoxLayout, QSizePolicy
-from qtpy.QtCore import Qt, Signal, Property, QMargins, QPoint
+from qtpy.QtCore import Qt, QMargins, QPoint
from ...widgets.slider import PyDMSlider
from ...widgets.base import PyDMWidget
@@ -15,6 +15,7 @@
# POSITIVE TEST CASES
# --------------------
+
def test_construct(qtbot):
"""
Test the construction of the widget.
@@ -48,11 +49,11 @@ def test_construct(qtbot):
assert pydm_slider.low_lim_label.sizePolicy() == QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
assert pydm_slider.low_lim_label.alignment() == Qt.Alignment(int(Qt.AlignLeft | Qt.AlignTrailing | Qt.AlignVCenter))
-
assert type(pydm_slider.high_lim_label) == QLabel
assert pydm_slider.high_lim_label.sizePolicy() == QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
- assert pydm_slider.high_lim_label.alignment() == Qt.Alignment(int(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter))
-
+ assert pydm_slider.high_lim_label.alignment() == Qt.Alignment(
+ int(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter)
+ )
assert type(pydm_slider._slider) == QSlider
assert pydm_slider._slider.orientation() == Qt.Orientation(Qt.Horizontal)
@@ -106,10 +107,13 @@ def test_actions_triggered(qtbot, signals):
signals.internal_slider_clicked.emit()
-@pytest.mark.parametrize("new_value, mute_change", [
- (100.50, False),
- (-100, True),
-])
+@pytest.mark.parametrize(
+ "new_value, mute_change",
+ [
+ (100.50, False),
+ (-100, True),
+ ],
+)
def test_internal_slider_value_changed(qtbot, signals, new_value, mute_change):
"""
Test widget's change of its text value if its internal value has changed.
@@ -155,21 +159,20 @@ def test_internal_slider_value_changed(qtbot, signals, new_value, mute_change):
assert signals.value is None
-@pytest.mark.parametrize("value, step_size, precision, precision_from_pv", [
- ('0.5', '1', '5', False),
- ('1', '0.1', '3', False)
-])
+@pytest.mark.parametrize(
+ "value, step_size, precision, precision_from_pv", [("0.5", "1", "5", False), ("1", "0.1", "3", False)]
+)
def test_parameters_menu(qtbot, value, step_size, precision, precision_from_pv):
"""
- Tests the slider widgets parameters menu
+ Tests the slider widgets parameters menu
- Expectations:
- The values passed from the menu will update the corresponding values of the widget.
+ Expectations:
+ The values passed from the menu will update the corresponding values of the widget.
- Parameters
- ----------
- qtbot : fixture
- pytest-qt window for widget test
+ Parameters
+ ----------
+ qtbot : fixture
+ pytest-qt window for widget test
"""
pydm_slider = PyDMSlider()
qtbot.addWidget(pydm_slider)
@@ -194,14 +197,18 @@ def test_parameters_menu(qtbot, value, step_size, precision, precision_from_pv):
assert pydm_slider.step_size == float(step_size)
assert pydm_slider.precision == float(precision)
-@pytest.mark.parametrize("show_labels, tick_position", [
- (True, 0),
- (True, -10),
- (True, 10),
- (False, 0),
- (False, -10),
- (False, 10),
-])
+
+@pytest.mark.parametrize(
+ "show_labels, tick_position",
+ [
+ (True, 0),
+ (True, -10),
+ (True, 10),
+ (False, 0),
+ (False, -10),
+ (False, 10),
+ ],
+)
def test_properties_and_setters(qtbot, show_labels, tick_position):
"""
Test the widget's various properties and setters.
@@ -246,10 +253,7 @@ def test_properties_and_setters(qtbot, show_labels, tick_position):
assert not pydm_slider.value_label.isVisibleTo(pydm_slider)
-@pytest.mark.parametrize("new_orientation", [
- Qt.Horizontal,
- Qt.Vertical
-])
+@pytest.mark.parametrize("new_orientation", [Qt.Horizontal, Qt.Vertical])
def test_setup_widgets_for_orientation(qtbot, new_orientation):
"""
Test setting up the slider's orientation.
@@ -295,12 +299,15 @@ def test_setup_widgets_for_orientation(qtbot, new_orientation):
assert pydm_slider._slider.orientation() == new_orientation
-@pytest.mark.parametrize("minimum, maximum", [
- (10, 20.5),
- (10, 1),
- (10, 20),
- (-10, 20.5),
-])
+@pytest.mark.parametrize(
+ "minimum, maximum",
+ [
+ (10, 20.5),
+ (10, 1),
+ (10, 20),
+ (-10, 20.5),
+ ],
+)
def test_update_labels(qtbot, signals, minimum, maximum):
"""
Test that changes in the user minimum and user maximum update the limit labels.
@@ -319,6 +326,7 @@ def test_update_labels(qtbot, signals, minimum, maximum):
maximum : int
The slider's maximum value as set by the user
"""
+
def validate(value, widget):
if value is None:
assert widget.text() == ""
@@ -338,18 +346,21 @@ def validate(value, widget):
validate(maximum, pydm_slider.high_lim_label)
-@pytest.mark.parametrize("minimum, maximum, write_access, connected", [
- (None, None, True, True),
- (None, 10, True, True),
- (10, None, True, True),
- (10, 20, True, True),
- (20, 20, True, True),
- (20, 30, True, True),
- (-10, 20, True, True),
- (10, 20, True, False),
- (10, 20, False, True),
- (10, 20, False, False),
-])
+@pytest.mark.parametrize(
+ "minimum, maximum, write_access, connected",
+ [
+ (None, None, True, True),
+ (None, 10, True, True),
+ (10, None, True, True),
+ (10, 20, True, True),
+ (20, 20, True, True),
+ (20, 30, True, True),
+ (-10, 20, True, True),
+ (10, 20, True, False),
+ (10, 20, False, True),
+ (10, 20, False, False),
+ ],
+)
def test_reset_slider_limits(qtbot, signals, minimum, maximum, write_access, connected):
"""
Test the updating of the limits when the silder is reset.
@@ -397,21 +408,27 @@ def test_reset_slider_limits(qtbot, signals, minimum, maximum, write_access, con
assert pydm_slider._slider.maximum() == pydm_slider.num_steps - 1
assert pydm_slider._slider.singleStep() == 1
assert pydm_slider._slider.pageStep() == 10
- assert np.array_equal(pydm_slider._slider_position_to_value_map,
- np.linspace(pydm_slider.minimum, pydm_slider.maximum, num=pydm_slider._num_steps))
- assert pydm_slider.isEnabled() == (pydm_slider._write_access and pydm_slider._connected and not \
- pydm_slider._needs_limit_info)
-
-
-@pytest.mark.parametrize("new_value, minimum, maximum", [
- (10, -10, 20),
- (-10, -10, 20),
- (20, -10, 20),
- (-200, -10, 20),
- (200, -10, 20),
- (0, 0, 0),
- (10, 10, 10),
-])
+ assert np.array_equal(
+ pydm_slider._slider_position_to_value_map,
+ np.linspace(pydm_slider.minimum, pydm_slider.maximum, num=pydm_slider._num_steps),
+ )
+ assert pydm_slider.isEnabled() == (
+ pydm_slider._write_access and pydm_slider._connected and not pydm_slider._needs_limit_info
+ )
+
+
+@pytest.mark.parametrize(
+ "new_value, minimum, maximum",
+ [
+ (10, -10, 20),
+ (-10, -10, 20),
+ (20, -10, 20),
+ (-200, -10, 20),
+ (200, -10, 20),
+ (0, 0, 0),
+ (10, 10, 10),
+ ],
+)
def test_set_slider_to_closest_value(qtbot, new_value, minimum, maximum):
"""
Test the calculation of the slider's value. Also test set_slider_to_closest_value().
@@ -449,10 +466,13 @@ def test_set_slider_to_closest_value(qtbot, new_value, minimum, maximum):
assert pydm_slider._slider.value() == expected_slider_value
-@pytest.mark.parametrize("new_channel_value, is_slider_down", [
- (15, False),
- (15, True),
-])
+@pytest.mark.parametrize(
+ "new_channel_value, is_slider_down",
+ [
+ (15, False),
+ (15, True),
+ ],
+)
def test_value_changed(qtbot, signals, monkeypatch, new_channel_value, is_slider_down):
"""
Test the updating of the widget's slider component value when the channel value has changed.
@@ -497,49 +517,46 @@ def test_value_changed(qtbot, signals, monkeypatch, new_channel_value, is_slider
assert pydm_slider._slider.value() == 0
-@pytest.mark.parametrize("channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity", [
- (None, False, False, PyDMWidget.ALARM_NONE),
- (None, False, True, PyDMWidget.ALARM_NONE),
- (None, True, False, PyDMWidget.ALARM_NONE),
- (None, True, True, PyDMWidget.ALARM_NONE),
-
- (None, False, False, PyDMWidget.ALARM_MAJOR),
- (None, False, True, PyDMWidget.ALARM_MAJOR),
- (None, True, False, PyDMWidget.ALARM_MAJOR),
- (None, True, True, PyDMWidget.ALARM_MAJOR),
-
- ("", False, False, PyDMWidget.ALARM_NONE),
- ("", False, True, PyDMWidget.ALARM_NONE),
- ("", True, False, PyDMWidget.ALARM_NONE),
- ("", True, True, PyDMWidget.ALARM_NONE),
-
- ("", False, False, PyDMWidget.ALARM_MAJOR),
- ("", False, True, PyDMWidget.ALARM_MAJOR),
- ("", True, False, PyDMWidget.ALARM_MAJOR),
- ("", True, True, PyDMWidget.ALARM_MAJOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_NONE),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_NONE),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_MINOR),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_MINOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_MAJOR),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_MAJOR),
-
- ("CA://MTEST", False, False, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", False, True, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", True, False, PyDMWidget.ALARM_DISCONNECTED),
- ("CA://MTEST", True, True, PyDMWidget.ALARM_DISCONNECTED),
-])
-def test_alarm_severity_change(qtbot, signals, channel, alarm_sensitive_content, alarm_sensitive_border,
- new_alarm_severity):
+@pytest.mark.parametrize(
+ "channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity",
+ [
+ (None, False, False, PyDMWidget.ALARM_NONE),
+ (None, False, True, PyDMWidget.ALARM_NONE),
+ (None, True, False, PyDMWidget.ALARM_NONE),
+ (None, True, True, PyDMWidget.ALARM_NONE),
+ (None, False, False, PyDMWidget.ALARM_MAJOR),
+ (None, False, True, PyDMWidget.ALARM_MAJOR),
+ (None, True, False, PyDMWidget.ALARM_MAJOR),
+ (None, True, True, PyDMWidget.ALARM_MAJOR),
+ ("", False, False, PyDMWidget.ALARM_NONE),
+ ("", False, True, PyDMWidget.ALARM_NONE),
+ ("", True, False, PyDMWidget.ALARM_NONE),
+ ("", True, True, PyDMWidget.ALARM_NONE),
+ ("", False, False, PyDMWidget.ALARM_MAJOR),
+ ("", False, True, PyDMWidget.ALARM_MAJOR),
+ ("", True, False, PyDMWidget.ALARM_MAJOR),
+ ("", True, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_NONE),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_MINOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_MAJOR),
+ ("CA://MTEST", False, False, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", False, True, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", True, False, PyDMWidget.ALARM_DISCONNECTED),
+ ("CA://MTEST", True, True, PyDMWidget.ALARM_DISCONNECTED),
+ ],
+)
+def test_alarm_severity_change(
+ qtbot, signals, channel, alarm_sensitive_content, alarm_sensitive_border, new_alarm_severity
+):
"""
Test the style of the widget changing according to alarm sensitivity settings and alarm severity changes.
@@ -573,12 +590,15 @@ def test_alarm_severity_change(qtbot, signals, channel, alarm_sensitive_content,
signals.new_severity_signal.emit(new_alarm_severity)
-@pytest.mark.parametrize("which_limit, new_limit, user_defined_limits", [
- ("UPPER", 10.5, True),
- ("UPPER", 10.123, False),
- ("LOWER", -10.5, True),
- ("LOWER", -10.123, False),
-])
+@pytest.mark.parametrize(
+ "which_limit, new_limit, user_defined_limits",
+ [
+ ("UPPER", 10.5, True),
+ ("UPPER", 10.123, False),
+ ("LOWER", -10.5, True),
+ ("LOWER", -10.123, False),
+ ],
+)
def test_ctrl_limit_changed(qtbot, signals, which_limit, new_limit, user_defined_limits):
"""
Test the widget's handling of the upper and lower limit changes.
@@ -616,10 +636,13 @@ def test_ctrl_limit_changed(qtbot, signals, which_limit, new_limit, user_defined
assert pydm_slider.get_ctrl_limits()[0] == new_limit
-@pytest.mark.parametrize("value, precision, unit, show_unit, expected_format_string", [
- (123, 0, "s", True, "{:.0f} s"),
- (123.456, 3, "mV", True, "{:.3f} mV"),
-])
+@pytest.mark.parametrize(
+ "value, precision, unit, show_unit, expected_format_string",
+ [
+ (123, 0, "s", True, "{:.0f} s"),
+ (123.456, 3, "mV", True, "{:.3f} mV"),
+ ],
+)
def test_update_format_string(qtbot, value, precision, unit, show_unit, expected_format_string):
"""
Test the unit conversion by examining the resulted format string.
@@ -660,11 +683,15 @@ def test_update_format_string(qtbot, value, precision, unit, show_unit, expected
# NEGATIVE TEST CASES
# --------------------
-@pytest.mark.parametrize("new_orientation", [
- -1,
- 1000,
- None,
-])
+
+@pytest.mark.parametrize(
+ "new_orientation",
+ [
+ -1,
+ 1000,
+ None,
+ ],
+)
def test_setup_widgets_for_orientation_neg(qtbot, caplog, new_orientation):
"""
Test the widget's handling of invalid orientation values.
diff --git a/pydm/tests/widgets/test_spinbox.py b/pydm/tests/widgets/test_spinbox.py
index da23a39ed1..f169e7b37b 100644
--- a/pydm/tests/widgets/test_spinbox.py
+++ b/pydm/tests/widgets/test_spinbox.py
@@ -15,6 +15,7 @@
# POSITIVE TEST CASES
# --------------------
+
def test_construct(qtbot):
"""
Test the construction of the widget.
@@ -40,12 +41,16 @@ def test_construct(qtbot):
assert pydm_spinbox._write_on_press is False
-@pytest.mark.parametrize("first_key_pressed, second_key_pressed, keys_pressed_expected_results", [
- (Qt.Key_Left, Qt.Key_Right, (2, 1)),
- (Qt.Key_Right, Qt.Key_Left, (-2, -1)),
-])
-def test_key_press_event(qtbot, signals, monkeypatch, first_key_pressed, second_key_pressed,
- keys_pressed_expected_results):
+@pytest.mark.parametrize(
+ "first_key_pressed, second_key_pressed, keys_pressed_expected_results",
+ [
+ (Qt.Key_Left, Qt.Key_Right, (2, 1)),
+ (Qt.Key_Right, Qt.Key_Left, (-2, -1)),
+ ],
+)
+def test_key_press_event(
+ qtbot, signals, monkeypatch, first_key_pressed, second_key_pressed, keys_pressed_expected_results
+):
"""
Test the widget's handling of the key press events.
@@ -87,6 +92,7 @@ def test_key_press_event(qtbot, signals, monkeypatch, first_key_pressed, second_
def wait_focus():
return pydm_spinbox.hasFocus()
+
qtbot.waitUntil(wait_focus, timeout=5000)
def press_key_and_verify(key_pressed, key_mod, key_press_count, expected_exp, expected_value):
@@ -108,12 +114,12 @@ def press_key_and_verify(key_pressed, key_mod, key_press_count, expected_exp, ex
assert pydm_spinbox.value == expected_value
# Send out the first key event to effect the change of the step exponent
- press_key_and_verify(first_key_pressed, Qt.ControlModifier, 2, keys_pressed_expected_results[0],
- INIT_SPINBOX_VALUE)
+ press_key_and_verify(first_key_pressed, Qt.ControlModifier, 2, keys_pressed_expected_results[0], INIT_SPINBOX_VALUE)
# Send out the second key press event once to check if it can change the step exponent
- press_key_and_verify(second_key_pressed, Qt.ControlModifier, 1, keys_pressed_expected_results[1],
- INIT_SPINBOX_VALUE)
+ press_key_and_verify(
+ second_key_pressed, Qt.ControlModifier, 1, keys_pressed_expected_results[1], INIT_SPINBOX_VALUE
+ )
# Make sure the widget can process the UpArrow, DownArrow, PageUp, and PageDown keys
pydm_spinbox.keyPressEvent(QKeyEvent(QEvent.KeyPress, Qt.Key_Up, Qt.ControlModifier))
@@ -142,14 +148,7 @@ def test_widget_ctx_menu(qtbot):
assert find_action_from_menu(action_menu, "Toggle Show Step Size")
-@pytest.mark.parametrize("step_exp", [
- 10,
- 1,
- 0.01,
- 0,
- -10,
- -0.001
-])
+@pytest.mark.parametrize("step_exp", [10, 1, 0.01, 0, -10, -0.001])
def test_update_step_size(qtbot, step_exp):
"""
Test the incrementing of the widget's step exponent.
@@ -171,17 +170,20 @@ def test_update_step_size(qtbot, step_exp):
pydm_spinbox.step_exponent = step_exp
pydm_spinbox.update_step_size()
- assert pydm_spinbox.singleStep() == 10 ** step_exp
+ assert pydm_spinbox.singleStep() == 10**step_exp
-@pytest.mark.parametrize("show_unit, new_unit, step_exp, show_step_exp", [
- (True, "mJ", 0.01, True),
- (True, "light years", 10, True),
- (True, "s", -0.001, True),
- (True, "ms", 1, False),
- (False, "ns", 0.01, True),
- (False, "light years", 1, False),
-])
+@pytest.mark.parametrize(
+ "show_unit, new_unit, step_exp, show_step_exp",
+ [
+ (True, "mJ", 0.01, True),
+ (True, "light years", 10, True),
+ (True, "s", -0.001, True),
+ (True, "ms", 1, False),
+ (False, "ns", 0.01, True),
+ (False, "light years", 1, False),
+ ],
+)
def test_update_format_string(qtbot, signals, show_unit, new_unit, step_exp, show_step_exp):
"""
Test the widget's capability of updating the format string when various unit and step exponent paramaters are
@@ -240,13 +242,10 @@ def test_update_format_string(qtbot, signals, show_unit, new_unit, step_exp, sho
assert pydm_spinbox.lineEdit().toolTip() == expected_tooltip
-@pytest.mark.parametrize("init_value, user_typed_value, precision", [
- (123, 456, 3),
- (1.23, 4.56, 2),
- (1.23, 5, 0),
- (0, 12.3, 2),
- (-1.23, 4.6, 1)
-])
+@pytest.mark.parametrize(
+ "init_value, user_typed_value, precision",
+ [(123, 456, 3), (1.23, 4.56, 2), (1.23, 5, 0), (0, 12.3, 2), (-1.23, 4.6, 1)],
+)
def test_send_value(qtbot, signals, init_value, user_typed_value, precision):
"""
Test sending the value from the widget to the channel. This method tests value_changed() and precision_changed().
@@ -289,12 +288,10 @@ def test_send_value(qtbot, signals, init_value, user_typed_value, precision):
assert pydm_spinbox.value == user_typed_value
-@pytest.mark.parametrize("which_limit, new_limit, user_defined_limits", [
- ("UPPER", 123.456, False),
- ("LOWER", 12.345, False),
- ("UPPER", 987.654, True),
- ("LOWER", 9.321, True)
-])
+@pytest.mark.parametrize(
+ "which_limit, new_limit, user_defined_limits",
+ [("UPPER", 123.456, False), ("LOWER", 12.345, False), ("UPPER", 987.654, True), ("LOWER", 9.321, True)],
+)
def test_ctrl_limit_changed(qtbot, signals, which_limit, new_limit, user_defined_limits):
"""
Test the upper and lower limit settings.
@@ -380,14 +377,18 @@ def test_reset_limits(qtbot):
assert pydm_spinbox.maximum() == 10.5
-@pytest.mark.parametrize("key_pressed, initial_spinbox_value, expected_result, write_on_press", [
- (Qt.Key_Up, 1, 2, True),
- (Qt.Key_Down, 1, 0, True),
- (Qt.Key_Up, 1, 1, False),
- (Qt.Key_Down, 1, 1, False),
-])
-def test_write_on_press(qtbot, signals, monkeypatch, key_pressed, initial_spinbox_value,
- expected_result, write_on_press):
+@pytest.mark.parametrize(
+ "key_pressed, initial_spinbox_value, expected_result, write_on_press",
+ [
+ (Qt.Key_Up, 1, 2, True),
+ (Qt.Key_Down, 1, 0, True),
+ (Qt.Key_Up, 1, 1, False),
+ (Qt.Key_Down, 1, 1, False),
+ ],
+)
+def test_write_on_press(
+ qtbot, signals, monkeypatch, key_pressed, initial_spinbox_value, expected_result, write_on_press
+):
"""
Test sending the value from the widget to the channel on key press when writeOnPress enabled.
diff --git a/pydm/tests/widgets/test_symbol_editor.py b/pydm/tests/widgets/test_symbol_editor.py
index b8dbe8f56f..a2ac43a8ea 100644
--- a/pydm/tests/widgets/test_symbol_editor.py
+++ b/pydm/tests/widgets/test_symbol_editor.py
@@ -1,9 +1,5 @@
-import os
-import pytest
-import logging
import json
import copy
-import webbrowser
from qtpy import QtCore
from qtpy.QtWidgets import QMessageBox, QTableWidgetSelectionRange
@@ -40,7 +36,7 @@ def test_symbol_editor(qtbot, monkeypatch):
empty.cancelChanges()
# Create the rules data for the widget
- symbol_dict = {"1":"goodbye.jpg"}
+ symbol_dict = {"1": "goodbye.jpg"}
# Add the rules to the widget
widget.imageFiles = json.dumps(symbol_dict)
@@ -77,14 +73,14 @@ def test_symbol_editor(qtbot, monkeypatch):
# Test Delete Symbol with Confirm - NO
assert se.tbl_symbols.rowCount() == 2
se.tbl_symbols.setRangeSelected(QTableWidgetSelectionRange(1, 0, 1, 1), True)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.No)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.No)
qtbot.mouseClick(se.btn_del_symbol, QtCore.Qt.LeftButton)
assert se.tbl_symbols.rowCount() == 2
assert se.frm_edit.isEnabled()
# Test Delete Symbol with Confirm - YES
se.tbl_symbols.setRangeSelected(QTableWidgetSelectionRange(1, 0, 1, 1), True)
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(se.btn_del_symbol, QtCore.Qt.LeftButton)
assert se.tbl_symbols.rowCount() == 1
assert len(se.symbols) == 1
@@ -92,12 +88,13 @@ def test_symbol_editor(qtbot, monkeypatch):
# Test Delete Symbol with No Selection
se.tbl_symbols.clearSelection()
- monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
qtbot.mouseClick(se.btn_del_symbol, QtCore.Qt.LeftButton)
assert se.tbl_symbols.rowCount() == 1
assert len(se.symbols) == 1
assert se.frm_edit.isEnabled() is False
+
def test_symbol_editor_data_valid(qtbot):
"""
Test the rules form validation.
@@ -107,6 +104,7 @@ def test_symbol_editor_data_valid(qtbot):
qtbot : fixture
pytest-qt window for widget test
"""
+
def validate(expected_status, expected_msg):
status, msg = se.is_data_valid()
assert status == expected_status
@@ -117,7 +115,7 @@ def validate(expected_status, expected_msg):
qtbot.addWidget(widget)
# Create the rules data for the widget
- symbol_dict = {"1":"goodbye.jpg"}
+ symbol_dict = {"1": "goodbye.jpg"}
# Add the rules to the widget
widget.imageFiles = json.dumps(symbol_dict)
diff --git a/pydm/tests/widgets/test_template_repeater.py b/pydm/tests/widgets/test_template_repeater.py
index 04ad52dd97..d4f17be859 100644
--- a/pydm/tests/widgets/test_template_repeater.py
+++ b/pydm/tests/widgets/test_template_repeater.py
@@ -1,13 +1,9 @@
import os
-import pytest
-from qtpy.QtCore import Qt
-from qtpy.QtWidgets import QApplication
from ...widgets import PyDMSlider, PyDMTemplateRepeater
-test_template_path = os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- "../test_data", "template.ui")
-
+test_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../test_data", "template.ui")
+
+
def test_template_file(qtbot):
# Test that loading a template and setting data instantiates an instance
# of the template.
@@ -19,4 +15,4 @@ def test_template_file(qtbot):
assert template_repeater.count() == len(test_data)
slider = template_repeater.findChild(PyDMSlider, "bCtrlSlider")
assert slider is not None
- assert slider.channel == "ca://{}:BCTRL".format(test_data[0]["devname"])
\ No newline at end of file
+ assert slider.channel == "ca://{}:BCTRL".format(test_data[0]["devname"])
diff --git a/pydm/tests/widgets/test_timeplot.py b/pydm/tests/widgets/test_timeplot.py
index d33143ab72..737a8a8429 100644
--- a/pydm/tests/widgets/test_timeplot.py
+++ b/pydm/tests/widgets/test_timeplot.py
@@ -11,38 +11,52 @@
logger = logging.getLogger(__name__)
-@pytest.mark.parametrize("channel_address, name", [
- ("ca://test_value:Float", "test_name"),
- ("ca://test_value:Float", ""),
- ("ca://test_value:Float", None),
- ("", None),
- (None, None)
-])
+@pytest.mark.parametrize(
+ "channel_address, name",
+ [
+ ("ca://test_value:Float", "test_name"),
+ ("ca://test_value:Float", ""),
+ ("ca://test_value:Float", None),
+ ("", None),
+ (None, None),
+ ],
+)
def test_timeplotcurveitem_construct(qtbot, channel_address, name):
pydm_timeplot_curve_item = TimePlotCurveItem(channel_address=channel_address, name=name)
if not name:
- assert pydm_timeplot_curve_item.to_dict()["name"] == remove_protocol(channel_address) if channel_address else \
- not pydm_timeplot_curve_item.to_dict()["name"]
+ assert (
+ pydm_timeplot_curve_item.to_dict()["name"] == remove_protocol(channel_address)
+ if channel_address
+ else not pydm_timeplot_curve_item.to_dict()["name"]
+ )
assert pydm_timeplot_curve_item._bufferSize == MINIMUM_BUFFER_SIZE
assert pydm_timeplot_curve_item._update_mode == PyDMTimePlot.OnValueChange
- assert np.array_equal(pydm_timeplot_curve_item.data_buffer, np.zeros((2, pydm_timeplot_curve_item._bufferSize),
- order='f', dtype=float))
+ assert np.array_equal(
+ pydm_timeplot_curve_item.data_buffer,
+ np.zeros((2, pydm_timeplot_curve_item._bufferSize), order="f", dtype=float),
+ )
assert pydm_timeplot_curve_item.connected is False
assert pydm_timeplot_curve_item.points_accumulated == 0
assert pydm_timeplot_curve_item.latest_value is None
- assert pydm_timeplot_curve_item.address == channel_address if channel_address else \
- pydm_timeplot_curve_item.address is None
-
-
-@pytest.mark.parametrize("channel_address, name", [
- ("ca://test_value:Float", "test_name"),
- ("ca://test_value:Float", ""),
- ("ca://test_value:Float", None),
- ("", None),
- (None, None)
-])
+ assert (
+ pydm_timeplot_curve_item.address == channel_address
+ if channel_address
+ else pydm_timeplot_curve_item.address is None
+ )
+
+
+@pytest.mark.parametrize(
+ "channel_address, name",
+ [
+ ("ca://test_value:Float", "test_name"),
+ ("ca://test_value:Float", ""),
+ ("ca://test_value:Float", None),
+ ("", None),
+ (None, None),
+ ],
+)
def test_timeplotcurveitem_to_dict(qtbot, channel_address, name):
pydm_timeplot_curve_item = TimePlotCurveItem(channel_address=channel_address, name=name)
@@ -51,17 +65,16 @@ def test_timeplotcurveitem_to_dict(qtbot, channel_address, name):
assert dictionary["channel"] == channel_address if channel_address else dictionary["channel"] is None
if name:
- assert(dictionary["name"] == name)
+ assert dictionary["name"] == name
else:
- assert pydm_timeplot_curve_item.to_dict()["name"] == remove_protocol(channel_address) if channel_address else \
- not pydm_timeplot_curve_item.to_dict()["name"]
+ assert (
+ pydm_timeplot_curve_item.to_dict()["name"] == remove_protocol(channel_address)
+ if channel_address
+ else not pydm_timeplot_curve_item.to_dict()["name"]
+ )
-@pytest.mark.parametrize("new_address", [
- "new_address",
- "",
- None
-])
+@pytest.mark.parametrize("new_address", ["new_address", "", None])
def test_timeplotcurveitem_properties_and_setters(qtbot, new_address):
pydm_timeplot_curve_item = TimePlotCurveItem()
@@ -84,12 +97,7 @@ def test_timeplotcurveitem_connection_state_changed(qtbot, signals):
assert pydm_timeplot_curve_item.connected
-@pytest.mark.parametrize("async_update, new_data", [
- (False, -10),
- (False, 10.2333),
- (True, 100),
- (True, -123.456)
-])
+@pytest.mark.parametrize("async_update, new_data", [(False, -10), (False, 10.2333), (True, 100), (True, -123.456)])
def test_timeplotcurveitem_receive_value(qtbot, signals, async_update, new_data):
"""
Also testing setUpdatesAsynchronously, resetUpdatesAsynchronously, and initialize_buffer
@@ -108,10 +116,13 @@ def test_timeplotcurveitem_receive_value(qtbot, signals, async_update, new_data)
pydm_timeplot_curve_item.setUpdatesAsynchronously(async_update)
if async_update:
- assert pydm_timeplot_curve_item._update_mode == PyDMTimePlot.AtFixedRated if async_update else \
- pydm_timeplot_curve_item._update_mode == PyDMTimePlot.OnValueChange
+ assert (
+ pydm_timeplot_curve_item._update_mode == PyDMTimePlot.AtFixedRated
+ if async_update
+ else pydm_timeplot_curve_item._update_mode == PyDMTimePlot.OnValueChange
+ )
- expected_data_buffer = np.zeros((2, pydm_timeplot_curve_item._bufferSize), order='f', dtype=float)
+ expected_data_buffer = np.zeros((2, pydm_timeplot_curve_item._bufferSize), order="f", dtype=float)
expected_data_buffer[0] = pydm_timeplot_curve_item.data_buffer[0]
assert np.array_equal(expected_data_buffer, pydm_timeplot_curve_item.data_buffer)
@@ -121,20 +132,16 @@ def test_timeplotcurveitem_receive_value(qtbot, signals, async_update, new_data)
if async_update:
assert np.array_equal(pydm_timeplot_curve_item.latest_value, new_data)
else:
- assert np.array_equal(pydm_timeplot_curve_item.data_buffer[1, pydm_timeplot_curve_item._bufferSize - 1],
- new_data)
+ assert np.array_equal(
+ pydm_timeplot_curve_item.data_buffer[1, pydm_timeplot_curve_item._bufferSize - 1], new_data
+ )
assert pydm_timeplot_curve_item.points_accumulated == 1
pydm_timeplot_curve_item.resetUpdatesAsynchronously()
assert pydm_timeplot_curve_item._update_mode == PyDMTimePlot.OnValueChange
-@pytest.mark.parametrize("async_update, new_data", [
- (False, -10),
- (False, 10.2333),
- (True, 100),
- (True, -123.456)
-])
+@pytest.mark.parametrize("async_update, new_data", [(False, -10), (False, 10.2333), (True, 100), (True, -123.456)])
def test_timeplotcurveitem_async_update(qtbot, signals, async_update, new_data):
pydm_timeplot_curve_item = TimePlotCurveItem()
@@ -149,8 +156,9 @@ def test_timeplotcurveitem_async_update(qtbot, signals, async_update, new_data):
signals.new_value_signal[type(new_data)].emit(new_data)
if async_update:
- assert np.array_equal(pydm_timeplot_curve_item.data_buffer[1, pydm_timeplot_curve_item._bufferSize - 1],
- new_data)
+ assert np.array_equal(
+ pydm_timeplot_curve_item.data_buffer[1, pydm_timeplot_curve_item._bufferSize - 1], new_data
+ )
assert pydm_timeplot_curve_item.points_accumulated == 1
else:
assert pydm_timeplot_curve_item.points_accumulated == 2
@@ -162,18 +170,21 @@ def test_timeplotcurve_initialize_buffer(qtbot):
pydm_timeplot_curve_item.initialize_buffer()
assert pydm_timeplot_curve_item.points_accumulated == 0
- expected_data_buffer = np.zeros((2, pydm_timeplot_curve_item._bufferSize), order='f', dtype=float)
+ expected_data_buffer = np.zeros((2, pydm_timeplot_curve_item._bufferSize), order="f", dtype=float)
expected_data_buffer[0] = pydm_timeplot_curve_item.data_buffer[0]
assert np.array_equal(expected_data_buffer, pydm_timeplot_curve_item.data_buffer)
-@pytest.mark.parametrize("new_buffer_size, expected_set_buffer_size", [
- (0, MINIMUM_BUFFER_SIZE),
- (-5, MINIMUM_BUFFER_SIZE),
- (100, 100),
- (MINIMUM_BUFFER_SIZE + 1, MINIMUM_BUFFER_SIZE + 1)
-])
+@pytest.mark.parametrize(
+ "new_buffer_size, expected_set_buffer_size",
+ [
+ (0, MINIMUM_BUFFER_SIZE),
+ (-5, MINIMUM_BUFFER_SIZE),
+ (100, 100),
+ (MINIMUM_BUFFER_SIZE + 1, MINIMUM_BUFFER_SIZE + 1),
+ ],
+)
def test_timeplotcurve_get_set_reset_buffer_size(qtbot, new_buffer_size, expected_set_buffer_size):
pydm_timeplot_curve_item = TimePlotCurveItem()
@@ -210,15 +221,25 @@ def test_pydmtimeplot_construct(qtbot):
assert pydm_timeplot._bottom_axis.orientation == "bottom"
-@mock.patch('pydm.widgets.timeplot.TimePlotCurveItem.setData')
-@mock.patch('pyqtgraph.BarGraphItem.setOpts')
+def test_pydmtimeplot_add_curve(qtbot):
+ pydm_timeplot = PyDMTimePlot()
+ qtbot.addWidget(pydm_timeplot)
+
+ curve_pv = "loc://test:timeplot:pv"
+ pydm_timeplot.addYChannel(curve_pv)
+
+ assert pydm_timeplot.findCurve(curve_pv) is not None
+
+
+@mock.patch("pydm.widgets.timeplot.TimePlotCurveItem.setData")
+@mock.patch("pyqtgraph.BarGraphItem.setOpts")
def test_redraw_plot(mocked_set_opts, mocked_set_data, qtbot, monkeypatch):
- """ Test redrawing a time plot using both a line and a bar graph """
+ """Test redrawing a time plot using both a line and a bar graph"""
# Create a time plot and add two data items to it, one to be rendered as a line and one as a bar graph
time_plot = PyDMTimePlot()
line_item = TimePlotCurveItem()
- bar_item = TimePlotCurveItem(plot_style='Bar')
+ bar_item = TimePlotCurveItem(plot_style="Bar")
bar_item.bar_graph_item = BarGraphItem(x=[], height=[], width=1.0)
time_plot.addCurve(line_item)
time_plot.addCurve(bar_item)
@@ -232,20 +253,20 @@ def test_redraw_plot(mocked_set_opts, mocked_set_data, qtbot, monkeypatch):
time_plot.set_needs_redraw()
time_plot.plotItem.setXRange(1, 10) # Sets the visible x-range of this time plot
- monkeypatch.setattr(time_plot, 'updateXAxis', lambda: None) # Ensure the view box range is deterministic
+ monkeypatch.setattr(time_plot, "updateXAxis", lambda: None) # Ensure the view box range is deterministic
# Simulate a redraw of the plot
time_plot.redrawPlot()
# The line item should result in a call to set data displaying all available data points as defined above
- assert np.array_equal(mocked_set_data.call_args_list[2][1]['x'], np.array([1, 5, 10]))
- assert np.array_equal(mocked_set_data.call_args_list[2][1]['y'], np.array([10, 15, 12]))
+ assert np.array_equal(mocked_set_data.call_args_list[2][1]["x"], np.array([1, 5, 10]))
+ assert np.array_equal(mocked_set_data.call_args_list[2][1]["y"], np.array([10, 15, 12]))
# Because we want to limit rendering of bar graphs to only those data points which are visible, we should
# see a call made with only 4 of the 6 data points. The data points associated with the x values 0.5 and 11
# were omitted since they fell outside the viewable range of 1 to 10.
- assert np.array_equal(mocked_set_opts.call_args_list[1][1]['x'], np.array([1, 1.5, 2, 10]))
- assert np.array_equal(mocked_set_opts.call_args_list[1][1]['height'], np.array([50, 52, 40, 24]))
+ assert np.array_equal(mocked_set_opts.call_args_list[1][1]["x"], np.array([1, 1.5, 2, 10]))
+ assert np.array_equal(mocked_set_opts.call_args_list[1][1]["height"], np.array([50, 52, 40, 24]))
# After a call to redraw, the plot returns to this state until more data arrives
assert not time_plot._needs_redraw
diff --git a/pydm/tests/widgets/test_waveform_plot.py b/pydm/tests/widgets/test_waveform_plot.py
index 9cc6a42822..ddf12d6eaa 100644
--- a/pydm/tests/widgets/test_waveform_plot.py
+++ b/pydm/tests/widgets/test_waveform_plot.py
@@ -4,15 +4,15 @@
from ...widgets.waveformplot import PyDMWaveformPlot, WaveformCurveItem
-@mock.patch('pydm.widgets.waveformplot.WaveformCurveItem.setData')
-@mock.patch('pyqtgraph.BarGraphItem.setOpts')
+@mock.patch("pydm.widgets.waveformplot.WaveformCurveItem.setData")
+@mock.patch("pyqtgraph.BarGraphItem.setOpts")
def test_redraw_plot(mocked_set_opts, mocked_set_data, qtbot, monkeypatch):
- """ Test redrawing a waveform plot using both a line and a bar graph """
+ """Test redrawing a waveform plot using both a line and a bar graph"""
# Create a waveform plot and add two data items to it, one to be rendered as a line and one as a bar graph
waveform_plot = PyDMWaveformPlot()
line_item = WaveformCurveItem()
- bar_item = WaveformCurveItem(plot_style='Bar')
+ bar_item = WaveformCurveItem(plot_style="Bar")
bar_item.bar_graph_item = BarGraphItem(x=[], height=[], width=1.0)
waveform_plot.addCurve(line_item)
waveform_plot.addCurve(bar_item)
@@ -29,22 +29,22 @@ def test_redraw_plot(mocked_set_opts, mocked_set_data, qtbot, monkeypatch):
waveform_plot.redrawPlot()
# The line item should result in a call to set data displaying all available data points as defined above
- assert np.array_equal(mocked_set_data.call_args_list[2][1]['x'], np.array([1, 5, 10]))
- assert np.array_equal(mocked_set_data.call_args_list[2][1]['y'], np.array([10, 15, 12]))
+ assert np.array_equal(mocked_set_data.call_args_list[2][1]["x"], np.array([1, 5, 10]))
+ assert np.array_equal(mocked_set_data.call_args_list[2][1]["y"], np.array([10, 15, 12]))
# As should the bar item, using the set_opts call instead of setData
- assert np.array_equal(mocked_set_opts.call_args_list[1][1]['x'], np.array([0.5, 1, 1.5, 2, 10, 11]))
- assert np.array_equal(mocked_set_opts.call_args_list[1][1]['height'], np.array([45, 50, 52, 40, 24, 30]))
+ assert np.array_equal(mocked_set_opts.call_args_list[1][1]["x"], np.array([0.5, 1, 1.5, 2, 10, 11]))
+ assert np.array_equal(mocked_set_opts.call_args_list[1][1]["height"], np.array([45, 50, 52, 40, 24, 30]))
# After a call to redraw, the plot returns to this state until more data arrives
assert not waveform_plot._needs_redraw
def test_mismatched_shapes(qtbot):
- """ Test that the logic around waveforms with differing lengths works as expected """
+ """Test that the logic around waveforms with differing lengths works as expected"""
# Create a waveform plot and add data whose waveform components do not share the same length
- waveform_plot = PyDMWaveformPlot()
+ PyDMWaveformPlot()
data_item_1 = WaveformCurveItem()
# Start with the basic case, both waveforms share the same length
@@ -74,7 +74,7 @@ def test_mismatched_shapes(qtbot):
def test_clear_curves(qtbot):
- """ Verify that all curves are removed from a waveform plot when clearCurves() is called """
+ """Verify that all curves are removed from a waveform plot when clearCurves() is called"""
# Create a plot with two curves added to it
waveform_plot = PyDMWaveformPlot()
qtbot.addWidget(waveform_plot)
@@ -90,23 +90,23 @@ def test_clear_curves(qtbot):
def test_clear_axes(qtbot):
- """ Verify that when multiple y-axes are added to a plot, clearing out the curves and axes cleans up everything """
+ """Verify that when multiple y-axes are added to a plot, clearing out the curves and axes cleans up everything"""
# Create a plot with two separate y axes on it, each with its own associated view box
waveform_plot = PyDMWaveformPlot()
qtbot.addWidget(waveform_plot)
data_one = WaveformCurveItem()
data_two = WaveformCurveItem()
- waveform_plot.addCurve(data_one, y_axis_name='Axis 1')
- waveform_plot.addCurve(data_two, y_axis_name='Axis 2')
+ waveform_plot.addCurve(data_one, y_axis_name="Axis 1")
+ waveform_plot.addCurve(data_two, y_axis_name="Axis 2")
# Ensure both axes were properly added
- assert 'Axis 1' in waveform_plot.plotItem.axes
- assert 'Axis 2' in waveform_plot.plotItem.axes
+ assert "Axis 1" in waveform_plot.plotItem.axes
+ assert "Axis 2" in waveform_plot.plotItem.axes
assert len(waveform_plot.plotItem.stackedViews) == 3 # There is a main top level view in addition to the 2 we added
waveform_plot.clearAxes()
# After the call to clear both axes should be removed, and the stacked views are also empty until more data is added
- assert 'Axis 1' not in waveform_plot.plotItem.axes
- assert 'Axis 2' not in waveform_plot.plotItem.axes
+ assert "Axis 1" not in waveform_plot.plotItem.axes
+ assert "Axis 2" not in waveform_plot.plotItem.axes
assert len(waveform_plot.plotItem.stackedViews) == 0
diff --git a/pydm/tools/__init__.py b/pydm/tools/__init__.py
index d15a7d149f..cafce4680d 100644
--- a/pydm/tools/__init__.py
+++ b/pydm/tools/__init__.py
@@ -19,11 +19,7 @@
def _is_valid_external_tool_class(obj: Any) -> bool:
"""Is the object a valid external tool?"""
- return (
- inspect.isclass(obj)
- and issubclass(obj, ExternalTool)
- and obj is not ExternalTool
- )
+ return inspect.isclass(obj) and issubclass(obj, ExternalTool) and obj is not ExternalTool
@log_failures(logger, explanation="Failed to load External Tool: {args[0]}")
@@ -47,13 +43,7 @@ def _get_tools_from_source(source_filename: str) -> List[ExternalTool]:
If no subclassed external tools are found.
"""
module = import_module_by_filename(source_filename)
- classes = list(
- set(
- obj
- for _, obj in inspect.getmembers(module)
- if _is_valid_external_tool_class(obj)
- )
- )
+ classes = list(set(obj for _, obj in inspect.getmembers(module) if _is_valid_external_tool_class(obj)))
if not classes:
raise ValueError(
@@ -84,10 +74,7 @@ def install_external_tool(tool: Union[str, ExternalTool]) -> None:
for tool_obj in objects:
if not isinstance(tool_obj, ExternalTool):
- raise ValueError(
- f"Invalid tool found: {tool}. String or ExternalTool "
- f"expected."
- )
+ raise ValueError(f"Invalid tool found: {tool}. String or ExternalTool " f"expected.")
if tool_obj.group is not None and tool_obj.group:
if tool_obj.group not in ext_tools:
@@ -99,17 +86,11 @@ def install_external_tool(tool: Union[str, ExternalTool]) -> None:
ext_tools = collections.OrderedDict(sorted(ext_tools.items()))
for ext_tool_name in ext_tools:
if isinstance(ext_tools[ext_tool_name], dict):
- ext_tools[ext_tool_name] = collections.OrderedDict(
- sorted(ext_tools[ext_tool_name].items())
- )
+ ext_tools[ext_tool_name] = collections.OrderedDict(sorted(ext_tools[ext_tool_name].items()))
def assemble_tools_menu(
- parent_menu: QMenu,
- clear_menu: bool = False,
- widget_only: bool = False,
- widget: Optional[QWidget] = None,
- **kwargs
+ parent_menu: QMenu, clear_menu: bool = False, widget_only: bool = False, widget: Optional[QWidget] = None, **kwargs
) -> None:
"""
Assemble the Tools menu for a given parent menu.
@@ -134,6 +115,7 @@ def assemble_tools_menu(
instance. In general this dict is composed by `channels` which
is a list and `sender` which is a QWidget.
"""
+
def assemble_action(menu, tool_obj):
if tool_obj.icon is not None:
action = menu.addAction(tool_obj.name)
@@ -159,8 +141,7 @@ def assemble_action(menu, tool_obj):
continue
if widget is not None and not t.is_compatible_with(widget):
logger.debug(
- "Skipping tool %s as it is incompatible with "
- "widget %s.",
+ "Skipping tool %s as it is incompatible with " "widget %s.",
t.name,
widget,
)
@@ -189,18 +170,11 @@ def get_entrypoint_tools() -> Generator[ExternalTool, None, None]:
try:
tool_cls = entry.load()
except Exception as ex:
- logger.exception(
- "Failed to load %s entry %s: %s",
- ENTRYPOINT_EXTERNAL_TOOL, entry.name, ex
- )
+ logger.exception("Failed to load %s entry %s: %s", ENTRYPOINT_EXTERNAL_TOOL, entry.name, ex)
continue
if not _is_valid_external_tool_class(tool_cls):
- logger.warning(
- "Invalid external tool class specified in entrypoint "
- "%s: %s",
- entry.name, tool_cls
- )
+ logger.warning("Invalid external tool class specified in entrypoint " "%s: %s", entry.name, tool_cls)
continue
yield tool_cls()
@@ -211,10 +185,7 @@ def get_tools_from_path() -> Generator[ExternalTool, None, None]:
tools_path = os.getenv("PYDM_TOOLS_PATH", None)
if not tools_path:
- logger.debug(
- "External Tools not loaded from PYDM_TOOLS_PATH as no path "
- "was specified."
- )
+ logger.debug("External Tools not loaded from PYDM_TOOLS_PATH as no path " "was specified.")
return
logger.debug("Looking for external tools at: %s", tools_path)
diff --git a/pydm/tools/tools.py b/pydm/tools/tools.py
index fbe04ebc42..8fbd43f6f3 100644
--- a/pydm/tools/tools.py
+++ b/pydm/tools/tools.py
@@ -1,6 +1,4 @@
-
-
-class ExternalTool():
+class ExternalTool:
"""
PyDM base class for External Tools hook-up.
This class offers a boilerplate for tools to be added to PyDM.
@@ -28,8 +26,8 @@ class ExternalTool():
Whether or not this action should be rendered at locations other than a
widget Custom Context Menu.
"""
- def __init__(self, icon, name, group, author="", use_with_widgets=True,
- use_without_widget=True):
+
+ def __init__(self, icon, name, group, author="", use_with_widgets=True, use_without_widget=True):
self.icon = icon
self.name = name
self.group = group
@@ -84,12 +82,7 @@ def get_info(self):
`name` of the External Tool.
"""
- return {
- 'author': self.author,
- 'file': "",
- 'group': self.group,
- 'name': self.name
- }
+ return {"author": self.author, "file": "", "group": self.group, "name": self.name}
def is_compatible_with(self, widget):
"""
diff --git a/pydm/utilities/__init__.py b/pydm/utilities/__init__.py
index 242a72d51a..1a5a2594db 100644
--- a/pydm/utilities/__init__.py
+++ b/pydm/utilities/__init__.py
@@ -21,6 +21,21 @@
from .remove_protocol import protocol_and_address, remove_protocol, parsed_address
from .units import convert, find_unit_options, find_unittype
+__all__ = [
+ "colors",
+ "macro",
+ "shortcuts",
+ "close_widget_connections",
+ "establish_widget_connections",
+ "IconFont",
+ "protocol_and_address",
+ "remove_protocol",
+ "parsed_address",
+ "convert",
+ "find_unit_options",
+ "find_unittype",
+]
+
logger = logging.getLogger(__name__)
@@ -33,7 +48,7 @@ def is_ssh_session():
bool
True if it is a ssh session, False otherwise.
"""
- return os.getenv('SSH_CONNECTION') is not None
+ return os.getenv("SSH_CONNECTION") is not None
def setup_renderer():
@@ -42,7 +57,7 @@ def setup_renderer():
running in a SSH session.
"""
if is_ssh_session():
- logger.info('Using PyDM via SSH. Reverting to Software Rendering.')
+ logger.info("Using PyDM via SSH. Reverting to Software Rendering.")
from qtpy.QtCore import QCoreApplication, Qt
from qtpy.QtQuick import QQuickWindow, QSGRendererInterface
@@ -67,6 +82,7 @@ def is_pydm_app(app=None):
from qtpy.QtWidgets import QApplication
from ..application import PyDMApplication
+
if app is None:
app = QApplication.instance()
if isinstance(app, PyDMApplication):
@@ -85,6 +101,7 @@ def is_qt_designer():
True if inside Designer, False otherwise.
"""
from ..qtdesigner import DesignerHooks
+
return DesignerHooks().form_editor is not None
@@ -102,6 +119,7 @@ def get_designer_current_path():
return None
from ..qtdesigner import DesignerHooks
+
form_editor = DesignerHooks().form_editor
win_manager = form_editor.formWindowManager()
form_window = win_manager.activeFormWindow()
@@ -166,6 +184,7 @@ def _screen_file_extensions(preferred_extension):
extensions = [".py", ".ui"] # search for screens with these extensions
try:
import adl2pydm # proceed only if package is importable # noqa: F401
+
extensions.append(".adl")
except ImportError:
pass
@@ -204,9 +223,9 @@ def find_file(fname, base_path=None, mode=None, extra_path=None, raise_if_not_fo
Which ensure that the file exists and we can read it.
extra_path : list
Additional paths to look for file.
- raise_if_not_found : bool
- Flag which if False will add a check that raises a FileNotFoundError
- instead of returning None when the file is not found.
+ raise_if_not_found : bool
+ Flag which if False will add a check that raises a FileNotFoundError
+ instead of returning None when the file is not found.
Returns
-------
@@ -250,7 +269,7 @@ def find_file(fname, base_path=None, mode=None, extra_path=None, raise_if_not_fo
file_path = which(str(root) + str(e), mode=mode, pathext=e, extra_path=x_path)
if file_path is not None:
break # pick the first screen file found
-
+
if raise_if_not_found:
if not file_path:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), fname)
@@ -306,8 +325,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None, pathext=None, extra_path=None)
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
- return (os.path.exists(fn) and os.access(fn, mode) and
- not os.path.isdir(fn))
+ return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
# If we're given a path with a directory part, look it up directly
# rather than referring to PATH directories. This includes checking
@@ -377,14 +395,13 @@ def only_main_thread(func):
-------
wrapper
"""
+
@functools.wraps(func)
def wrapper(*args, **kwargs):
main_t = QtWidgets.QApplication.instance().thread()
curr_t = QtCore.QThread.currentThread()
if curr_t != main_t:
- msg = "{}.{} can only be invoked from the main Qt thread.".format(
- func.__module__, func.__name__
- )
+ msg = "{}.{} can only be invoked from the main Qt thread.".format(func.__module__, func.__name__)
logger.error(msg)
raise RuntimeError(msg)
return func(*args, **kwargs)
@@ -419,15 +436,14 @@ def log_failures(
level : int, optional
Logging level to use.
"""
+
def wrapper(func: callable):
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as ex:
- msg = explanation.format(
- func=func, args=args, kwargs=kwargs, ex=ex
- )
+ msg = explanation.format(func=func, args=args, kwargs=kwargs, ex=ex)
if include_traceback:
logger.log(level, msg, exc_info=ex)
else:
@@ -439,10 +455,7 @@ def wrapped(*args, **kwargs):
return wrapper
-def import_module_by_filename(
- source_filename: str, *,
- add_to_modules: bool = True
-) -> types.ModuleType:
+def import_module_by_filename(source_filename: str, *, add_to_modules: bool = True) -> types.ModuleType:
"""
For a given source filename, import it and search for objects.
@@ -471,6 +484,7 @@ def import_module_by_filename(
sys.modules[module_name] = module
return module
+
def get_clipboard() -> Optional[QtGui.QClipboard]:
"""Get the clipboard instance. Requires a QApplication."""
app = QtWidgets.QApplication.instance()
@@ -495,10 +509,7 @@ def get_clipboard_modes() -> List[int]:
if platform.system() == "Linux":
# Mode selection is only valid for X11.
- return [
- QtGui.QClipboard.Selection,
- QtGui.QClipboard.Clipboard
- ]
+ return [QtGui.QClipboard.Selection, QtGui.QClipboard.Clipboard]
return [QtGui.QClipboard.Clipboard]
@@ -528,13 +539,7 @@ def copy_to_clipboard(text: str, *, quiet: bool = False):
if not quiet:
logger.warning(
- (
- "Copied text to clipboard:\n"
- "-------------------------\n"
- "%s\n"
- "-------------------------\n"
- ),
- text
+ ("Copied text to clipboard:\n" "-------------------------\n" "%s\n" "-------------------------\n"), text
)
diff --git a/pydm/utilities/colors.py b/pydm/utilities/colors.py
index 268f70fb32..e669326588 100644
--- a/pydm/utilities/colors.py
+++ b/pydm/utilities/colors.py
@@ -5,9 +5,9 @@
svg_color_to_hex_map = None
hex_to_svg_color_map = None
-with open(os.path.join(current_dir, 'hex2color.pkl'), 'rb') as f:
+with open(os.path.join(current_dir, "hex2color.pkl"), "rb") as f:
hex_to_svg_color_map = pickle.load(f)
-with open(os.path.join(current_dir, 'color2hex.pkl'), 'rb') as f:
+with open(os.path.join(current_dir, "color2hex.pkl"), "rb") as f:
svg_color_to_hex_map = pickle.load(f)
@@ -54,7 +54,21 @@ def hex_from_svg_color(color_string):
return svg_color_to_hex_map[str(color_string).lower()]
-default_colors = ['white', 'red', 'dodgerblue', 'forestgreen', 'yellow',
- 'fuchsia', 'turquoise', 'deeppink', 'lime', 'orange',
- 'whitesmoke', 'beige', 'purple', 'teal', 'darksalmon',
- 'brown']
+default_colors = [
+ "white",
+ "red",
+ "dodgerblue",
+ "forestgreen",
+ "yellow",
+ "fuchsia",
+ "turquoise",
+ "deeppink",
+ "lime",
+ "orange",
+ "whitesmoke",
+ "beige",
+ "purple",
+ "teal",
+ "darksalmon",
+ "brown",
+]
diff --git a/pydm/utilities/connection.py b/pydm/utilities/connection.py
index 7b49595898..6ca878a2e9 100644
--- a/pydm/utilities/connection.py
+++ b/pydm/utilities/connection.py
@@ -23,7 +23,7 @@ def _change_connection_status(widget, status):
widgets.extend(widget.findChildren(QWidget))
for child_widget in widgets:
try:
- if hasattr(child_widget, 'channels'):
+ if hasattr(child_widget, "channels"):
if child_widget.channels() is None:
continue
for channel in child_widget.channels():
@@ -36,6 +36,7 @@ def _change_connection_status(widget, status):
except NameError:
continue
+
def establish_widget_connections(widget):
"""
Connect the inner channels of widgets on the given widget.
diff --git a/pydm/utilities/display_loading.py b/pydm/utilities/display_loading.py
index b9dfc85926..a737446136 100644
--- a/pydm/utilities/display_loading.py
+++ b/pydm/utilities/display_loading.py
@@ -1,9 +1,9 @@
import warnings
-warnings.warn('pydm.utilities.display_loading was deprecated in favor of pydm.display',
- DeprecationWarning)
+warnings.warn("pydm.utilities.display_loading was deprecated in favor of pydm.display", DeprecationWarning)
def load_py_file(*args, **kwargs):
from pydm.display import load_py_file
+
return load_py_file(*args, **kwargs)
diff --git a/pydm/utilities/iconfont.py b/pydm/utilities/iconfont.py
index e36be09c32..6245e5a36b 100644
--- a/pydm/utilities/iconfont.py
+++ b/pydm/utilities/iconfont.py
@@ -10,8 +10,7 @@
from qtpy import QtGui, QtWidgets
from qtpy.QtCore import QPoint, QRect, Qt, qRound
-from qtpy.QtGui import (QColor, QFont, QFontDatabase, QIcon, QIconEngine,
- QPainter, QPixmap)
+from qtpy.QtGui import QColor, QFont, QFontDatabase, QIcon, QIconEngine, QPainter, QPixmap
if sys.version_info[0] == 3:
unichr = chr
@@ -20,6 +19,7 @@
class IconFont(object):
"""IconFont represents an icon font. Users will generally want
to use IconFont.icon() to get a QIcon object for the character they want."""
+
__instance = None
def __init__(self):
@@ -43,6 +43,7 @@ def load_font(self, ttf_filename, charmap_filename):
Load font from ``ttf_filename`` with a mapping defined in
``charmap_filename``.
"""
+
def hook(obj):
result = {}
for key in obj:
@@ -67,10 +68,8 @@ def hook(obj):
self.font_name = font_families[0]
- filename = os.path.join(
- os.path.dirname(os.path.realpath(__file__)), charmap_filename
- )
- with open(filename, 'r') as codes:
+ filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), charmap_filename)
+ with open(filename, "r") as codes:
self.char_map = json.load(codes, object_hook=hook)
self.loaded_fonts[cache_key] = {
@@ -169,8 +168,11 @@ def __init__(self, icon_font, char, color=None):
self._base_color = QColor(90, 90, 90)
else:
self._base_color = color
- self._disabled_color = QColor.fromHslF(self._base_color.hueF(), self._base_color.saturationF(),
- max(min(self._base_color.lightnessF() + 0.25, 1.0), 0.0))
+ self._disabled_color = QColor.fromHslF(
+ self._base_color.hueF(),
+ self._base_color.saturationF(),
+ max(min(self._base_color.lightnessF() + 0.25, 1.0), 0.0),
+ )
def paint(self, painter, rect, mode, state):
painter.save()
diff --git a/pydm/utilities/macro.py b/pydm/utilities/macro.py
index 80fa7ead36..510f07de92 100644
--- a/pydm/utilities/macro.py
+++ b/pydm/utilities/macro.py
@@ -36,8 +36,10 @@ def replace_macros_in_template(template, macros):
expanded_text = ""
# Escape any single or double quotes to ensure macro substitution results in valid python code
# when replaced (e.g. xterm -e 'echo hi')
- macros = {key: re.sub(r'(? OrderedDict:
- """ Returns an OrderedDict representation with values for all properties needed to recreate this curve. """
- dic_ = OrderedDict([("useArchiveData", self.use_archive_data), ])
+ """Returns an OrderedDict representation with values for all properties needed to recreate this curve."""
+ dic_ = OrderedDict([("useArchiveData", self.useArchiveData)])
dic_.update(super(ArchivePlotCurveItem, self).to_dict())
return dic_
def setArchiveChannel(self, address: str) -> None:
- """ Creates the channel for the input address for communicating with the archiver appliance plugin. """
- archiver_prefix = 'archiver://pv='
- if address.startswith('ca://'):
- archive_address = address.replace('ca://', archiver_prefix, 1)
- elif address.startswith('pva://'):
- archive_address = address.replace('pva://', archiver_prefix, 1)
+ """Creates the channel for the input address for communicating with the archiver appliance plugin."""
+ archiver_prefix = "archiver://pv="
+ if address.startswith("ca://"):
+ archive_address = address.replace("ca://", archiver_prefix, 1)
+ elif address.startswith("pva://"):
+ archive_address = address.replace("pva://", archiver_prefix, 1)
else:
archive_address = archiver_prefix + address
- self.archive_channel = PyDMChannel(address=archive_address,
- value_slot=self.receiveArchiveData,
- value_signal=self.archive_data_request_signal)
+ self.archive_channel = PyDMChannel(
+ address=archive_address,
+ value_slot=self.receiveArchiveData,
+ value_signal=self.archive_data_request_signal,
+ )
@Slot(np.ndarray)
def receiveArchiveData(self, data: np.ndarray) -> None:
- """ Receive data from archiver appliance and place it into the archive data buffer.
- Will overwrite any previously existing data at the indices written to.
-
- Parameters
- ----------
- data : np.ndarray
- A numpy array of varying shape consisting of archived data for display.
- At a minimum, index 0 will contain the timestamps and index 1 the actual data observations.
- Additional indices may be used as well based on the type of request made to the archiver appliance.
- For example optimized data will include standard deviations, minimums, and maximums
+ """Receive data from archiver appliance and place it into the archive data buffer.
+ Will overwrite any previously existing data at the indices written to.
+
+ Parameters
+ ----------
+ data : np.ndarray
+ A numpy array of varying shape consisting of archived data for display.
+ At a minimum, index 0 will contain the timestamps and index 1 the actual data observations.
+ Additional indices may be used as well based on the type of request made to the archiver appliance.
+ For example optimized data will include standard deviations, minimums, and maximums
"""
archive_data_length = len(data[0])
- max_x = data[0][archive_data_length-1]
+ max_x = data[0][archive_data_length - 1]
if self.points_accumulated != 0:
while max_x > self.data_buffer[0][-self.points_accumulated]:
# Sometimes optimized queries return data past the current timestamp, this will delete those data points
data = np.delete(data, len(data[0]) - 1, axis=1)
archive_data_length -= 1
- max_x = data[0][archive_data_length-1]
+ max_x = data[0][archive_data_length - 1]
- self.archive_data_buffer[0, len(self.archive_data_buffer[0]) - archive_data_length:] = data[0]
- self.archive_data_buffer[1, len(self.archive_data_buffer[0]) - archive_data_length:] = data[1]
+ self.archive_data_buffer[0, len(self.archive_data_buffer[0]) - archive_data_length :] = data[0]
+ self.archive_data_buffer[1, len(self.archive_data_buffer[0]) - archive_data_length :] = data[1]
self.archive_points_accumulated = archive_data_length
# Error bars
if data.shape[0] == 5: # 5 indicates optimized data was requested from the archiver
- self.error_bar_item.setData(x=self.archive_data_buffer[0, -self.archive_points_accumulated:],
- y=self.archive_data_buffer[1, -self.archive_points_accumulated:],
- top=data[4] - data[1],
- bottom=data[1] - data[3],
- beam=0.5,
- pen={'color': self.color})
+ self.error_bar_item.setData(
+ x=self.archive_data_buffer[0, -self.archive_points_accumulated :],
+ y=self.archive_data_buffer[1, -self.archive_points_accumulated :],
+ top=data[4] - data[1],
+ bottom=data[1] - data[3],
+ beam=0.5,
+ pen={"color": self.color},
+ )
if self.error_bar_needs_set:
self.getViewBox().addItem(self.error_bar_item)
self.error_bar_needs_set = False
@@ -130,14 +140,16 @@ def insert_archive_data(self, data: np.ndarray) -> None:
"""
archive_data_length = len(data[0])
min_x = data[0][0]
- max_x = data[0][archive_data_length-1]
+ max_x = data[0][archive_data_length - 1]
# Get the indices between which we want to insert the data
min_insertion_index = np.searchsorted(self.archive_data_buffer[0], min_x)
max_insertion_index = np.searchsorted(self.archive_data_buffer[0], max_x)
# Delete any non-raw data between the indices so we don't have multiple data points for the same timestamp
- self.archive_data_buffer = np.delete(self.archive_data_buffer,
- slice(min_insertion_index, max_insertion_index),
- axis=1)
+ self.archive_data_buffer = np.delete(
+ self.archive_data_buffer,
+ slice(min_insertion_index, max_insertion_index),
+ axis=1,
+ )
num_points_deleted = max_insertion_index - min_insertion_index
delta_points = archive_data_length - num_points_deleted
if archive_data_length > num_points_deleted:
@@ -159,11 +171,19 @@ def redrawCurve(self, min_x=None, max_x=None) -> None:
super(ArchivePlotCurveItem, self).redrawCurve()
else:
try:
- x = np.concatenate((self.archive_data_buffer[0, -self.archive_points_accumulated:].astype(float),
- self.data_buffer[0, -self.points_accumulated:].astype(float)))
-
- y = np.concatenate((self.archive_data_buffer[1, -self.archive_points_accumulated:].astype(float),
- self.data_buffer[1, -self.points_accumulated:].astype(float)))
+ x = np.concatenate(
+ (
+ self.archive_data_buffer[0, -self.archive_points_accumulated :].astype(float),
+ self.data_buffer[0, -self.points_accumulated :].astype(float),
+ )
+ )
+
+ y = np.concatenate(
+ (
+ self.archive_data_buffer[1, -self.archive_points_accumulated :].astype(float),
+ self.data_buffer[1, -self.points_accumulated :].astype(float),
+ )
+ )
self.setData(y=y, x=x)
except (ZeroDivisionError, OverflowError, TypeError):
@@ -175,28 +195,33 @@ def initializeArchiveBuffer(self) -> None:
Initialize the archive data buffer used for this curve.
"""
self.archive_points_accumulated = 0
- self.archive_data_buffer = np.zeros((2, self._archiveBufferSize), order='f', dtype=float)
+ self.archive_data_buffer = np.zeros((2, self._archiveBufferSize), order="f", dtype=float)
def getArchiveBufferSize(self) -> int:
- """ Return the length of the archive buffer """
+ """Return the length of the archive buffer"""
return int(self._archiveBufferSize)
def setArchiveBufferSize(self, value: int) -> None:
- """ Set the length of the archive data buffer and zero it out """
+ """Set the length of the archive data buffer and zero it out"""
if self._archiveBufferSize != int(value):
self._archiveBufferSize = max(int(value), 2)
self.initializeArchiveBuffer()
def resetArchiveBufferSize(self) -> None:
- """ Reset the length of the archive buffer back to the default and zero it out """
+ """Reset the length of the archive buffer back to the default and zero it out"""
if self._archiveBufferSize != DEFAULT_ARCHIVE_BUFFER_SIZE:
self._archiveBufferSize = DEFAULT_ARCHIVE_BUFFER_SIZE
self.initializeArchiveBuffer()
def channels(self) -> List[PyDMChannel]:
- """ Return the list of channels this curve is connected to """
+ """Return the list of channels this curve is connected to"""
return [self.channel, self.archive_channel]
+ def receiveNewValue(self, new_value):
+ """ """
+ if self.liveData:
+ super().receiveNewValue(new_value)
+
class PyDMArchiverTimePlot(PyDMTimePlot):
"""
@@ -216,26 +241,37 @@ class PyDMArchiverTimePlot(PyDMTimePlot):
The number of bins of data returned from the archiver when using optimized requests
"""
- def __init__(self, parent: Optional[QObject] = None, init_y_channels: List[str] = [],
- background: str = 'default', optimized_data_bins: int = 2000):
- super(PyDMArchiverTimePlot, self).__init__(parent=parent, init_y_channels=init_y_channels,
- plot_by_timestamps=True, background=background,
- bottom_axis=DateAxisItem('bottom'))
+ def __init__(
+ self,
+ parent: Optional[QObject] = None,
+ init_y_channels: List[str] = [],
+ background: str = "default",
+ optimized_data_bins: int = 2000,
+ ):
+ super(PyDMArchiverTimePlot, self).__init__(
+ parent=parent,
+ init_y_channels=init_y_channels,
+ plot_by_timestamps=True,
+ background=background,
+ bottom_axis=DateAxisItem("bottom"),
+ )
self.optimized_data_bins = optimized_data_bins
self._min_x = None
self._prev_x = None # Holds the minimum x-value of the previous update of the plot
self._starting_timestamp = time.time() # The timestamp at which the plot was first rendered
self._archive_request_queued = False
+ self._live_data = True
def updateXAxis(self, update_immediately: bool = False) -> None:
- """ Manages the requests to archiver appliance. When the user pans or zooms the x axis to the left,
- a request will be made for backfill data """
+ """Manages the requests to archiver appliance. When the user pans or zooms the x axis to the left,
+ a request will be made for backfill data"""
+
if len(self._curves) == 0:
return
- min_x = self.plotItem.getAxis('bottom').range[0] # Gets the leftmost timestamp displayed on the x-axis
+ min_x = self.plotItem.getAxis("bottom").range[0] # Gets the leftmost timestamp displayed on the x-axis
max_x = max([curve.max_x() for curve in self._curves])
- max_range = self.plotItem.getAxis('bottom').range[1]
+ max_range = self.plotItem.getAxis("bottom").range[1]
if min_x == 0: # This is zero when the plot first renders
min_x = time.time()
self._min_x = min_x
@@ -264,7 +300,12 @@ def updateXAxis(self, update_immediately: bool = False) -> None:
self.setTimeSpan(max_x - min_x)
else:
# Keep the plot moving with a rolling window based on the current timestamp
- self.plotItem.setXRange(max_x - self.getTimeSpan(), max_x, padding=0.0, update=update_immediately)
+ self.plotItem.setXRange(
+ max_x - self.getTimeSpan(),
+ max_x,
+ padding=0.0,
+ update=update_immediately,
+ )
self._prev_x = min_x
def requestDataFromArchiver(self, min_x: Optional[float] = None, max_x: Optional[float] = None) -> None:
@@ -281,11 +322,11 @@ def requestDataFromArchiver(self, min_x: Optional[float] = None, max_x: Optional
to the timestamp of the oldest live data point in the buffer if available. If no live points are
recorded yet, then defaults to the timestamp at which the plot was first rendered.
"""
- processing_command = ''
+ processing_command = ""
if min_x is None:
min_x = self._min_x
for curve in self._curves:
- if curve.use_archive_data:
+ if curve.useArchiveData:
if max_x is None:
if curve.points_accumulated > 0:
max_x = curve.data_buffer[0][curve.getBufferSize() - curve.points_accumulated]
@@ -298,46 +339,45 @@ def requestDataFromArchiver(self, min_x: Optional[float] = None, max_x: Optional
# Max amount of raw data to return before using optimized data
max_data_request = int(0.80 * self.getArchiveBufferSize())
if requested_seconds > max_data_request:
- processing_command = 'optimized_' + str(self.optimized_data_bins)
+ processing_command = "optimized_" + str(self.optimized_data_bins)
curve.archive_data_request_signal.emit(min_x, max_x - 1, processing_command)
def getArchiveBufferSize(self) -> int:
- """ Returns the size of the data buffer used to store archived data """
+ """Returns the size of the data buffer used to store archived data"""
if len(self._curves) == 0:
return DEFAULT_ARCHIVE_BUFFER_SIZE
return self._curves[0].getArchiveBufferSize()
- def createCurveItem(self, y_channel: str, plot_by_timestamps: bool, name: str, color: Union[QColor, str],
- yAxisName: str, useArchiveData: bool, **plot_opts) -> ArchivePlotCurveItem:
- """ Create and return a curve item to be plotted """
- curve_item = ArchivePlotCurveItem(y_channel, use_archive_data=useArchiveData, plot_by_timestamps=plot_by_timestamps,
- name=name, color=color, yAxisName=yAxisName, **plot_opts)
+ def createCurveItem(self, *args, **kwargs) -> ArchivePlotCurveItem:
+ """Create and return a curve item to be plotted"""
+ print("is this called?")
+ curve_item = ArchivePlotCurveItem(*args, **kwargs)
curve_item.archive_data_received_signal.connect(self.archive_data_received)
return curve_item
@Slot()
def archive_data_received(self):
- """ Take any action needed when this plot receives new data from archiver appliance """
+ """Take any action needed when this plot receives new data from archiver appliance"""
max_x = max([curve.max_x() for curve in self._curves])
# Assure the user sees all data available whenever the request data is returned
self.plotItem.setXRange(max_x - self.getTimeSpan(), max_x, padding=0.0, update=True)
self._archive_request_queued = False
def setTimeSpan(self, value):
- """ Set the value of the plot's timespan """
+ """Set the value of the plot's timespan"""
if value < DEFAULT_TIME_SPAN: # Less than 5 seconds will break the plot
return
self._time_span = value
def clearCurves(self) -> None:
- """ Clear all curves from the plot """
+ """Clear all curves from the plot"""
for curve in self._curves:
# Need to clear out any bars from optimized data, then super() can handle the rest
if not curve.error_bar_needs_set:
curve.getViewBox().removeItem(curve.error_bar_item)
# reset _min_x to let updateXAxis make requests anew
- self._min_x = self._starting_timestamp
+ self._min_x = self._starting_timestamp
super().clearCurves()
def getCurves(self) -> List[str]:
@@ -364,17 +404,58 @@ def setCurves(self, new_list: List[str]) -> None:
return
self.clearCurves()
for d in new_list:
- color = d.get('color')
+ color = d.get("color")
if color:
color = QColor(color)
- self.addYChannel(d['channel'],
- name=d.get('name'),
- color=color,
- lineStyle=d.get('lineStyle'),
- lineWidth=d.get('lineWidth'),
- symbol=d.get('symbol'),
- symbolSize=d.get('symbolSize'),
- yAxisName=d.get('yAxisName'),
- useArchiveData=d.get('useArchiveData'))
+ self.addYChannel(
+ d["channel"],
+ name=d.get("name"),
+ color=color,
+ lineStyle=d.get("lineStyle"),
+ lineWidth=d.get("lineWidth"),
+ symbol=d.get("symbol"),
+ symbolSize=d.get("symbolSize"),
+ yAxisName=d.get("yAxisName"),
+ useArchiveData=d.get("useArchiveData"),
+ )
curves = Property("QStringList", getCurves, setCurves, designable=False)
+
+ def addYChannel(
+ self,
+ y_channel=None,
+ plot_style=None,
+ name=None,
+ color=None,
+ lineStyle=None,
+ lineWidth=None,
+ symbol=None,
+ symbolSize=None,
+ barWidth=None,
+ upperThreshold=None,
+ lowerThreshold=None,
+ thresholdColor=None,
+ yAxisName=None,
+ useArchiveData=False,
+ liveData=True
+ ):
+ """
+ Overrides timeplot addYChannel method to be able to pass the liveData flag.
+ """
+ super().addYChannel(
+ y_channel=y_channel,
+ plot_style=plot_style,
+ name=name,
+ color=color,
+ lineStyle=lineStyle,
+ lineWidth=lineWidth,
+ symbol=symbol,
+ symbolSize=symbolSize,
+ barWidth=barWidth,
+ upperThreshold=upperThreshold,
+ lowerThreshold=lowerThreshold,
+ thresholdColor=thresholdColor,
+ yAxisName=yAxisName,
+ useArchiveData=useArchiveData,
+ liveData=liveData)
+
\ No newline at end of file
diff --git a/pydm/widgets/archiver_time_plot_editor.py b/pydm/widgets/archiver_time_plot_editor.py
index 0fa799835a..181a8abd4b 100644
--- a/pydm/widgets/archiver_time_plot_editor.py
+++ b/pydm/widgets/archiver_time_plot_editor.py
@@ -8,14 +8,14 @@
class PyDMArchiverTimePlotCurvesModel(BasePlotCurvesModel):
- """ Model used in designer for editing archiver time plot curves. """
+ """Model used in designer for editing archiver time plot curves."""
def __init__(self, plot: BasePlot, parent: Optional[QObject] = None):
super().__init__(plot, parent=parent)
self._column_names = ("Channel", "Archive Data") + self._column_names
def get_data(self, column_name: str, curve: ArchivePlotCurveItem) -> Any:
- """ Get data for the input column name """
+ """Get data for the input column name"""
if column_name == "Channel":
if curve.address is None:
return QVariant()
@@ -25,7 +25,7 @@ def get_data(self, column_name: str, curve: ArchivePlotCurveItem) -> Any:
return super().get_data(column_name, curve)
def set_data(self, column_name: str, curve: ArchivePlotCurveItem, value: Any) -> bool:
- """ Set data on the input curve for the given name and value. Return true if successful. """
+ """Set data on the input curve for the given name and value. Return true if successful."""
if column_name == "Channel":
curve.address = str(value)
elif column_name == "Archive Data":
@@ -35,26 +35,27 @@ def set_data(self, column_name: str, curve: ArchivePlotCurveItem, value: Any) ->
return True
def append(self, address: Optional[str] = None, name: Optional[str] = None, color: Optional[QColor] = None) -> None:
- """ Add a row for a curve with the input address """
+ """Add a row for a curve with the input address"""
self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves))
self._plot.addYChannel(address, name, color)
self.endInsertRows()
def removeAtIndex(self, index: QModelIndex):
- """ Remove the row at the input index """
+ """Remove the row at the input index"""
self.beginRemoveRows(QModelIndex(), index.row(), index.row())
self._plot.removeYChannelAtIndex(index.row())
self.endRemoveRows()
class ArchiverTimePlotCurveEditorDialog(BasePlotCurveEditorDialog):
- """ ArchiverTimePlotCurveEditorDialog is a QDialog that is used in Qt Designer
+ """ArchiverTimePlotCurveEditorDialog is a QDialog that is used in Qt Designer
to edit the properties of the curves in a waveform plot. This dialog is
shown when you double-click the plot, or when you right click it and
choose 'edit curves'.
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = PyDMArchiverTimePlotCurvesModel
def __init__(self, plot, parent=None):
diff --git a/pydm/widgets/axis_table_model.py b/pydm/widgets/axis_table_model.py
index 1e3b06f4ba..e1cbf49744 100644
--- a/pydm/widgets/axis_table_model.py
+++ b/pydm/widgets/axis_table_model.py
@@ -3,16 +3,23 @@
class BasePlotAxesModel(QAbstractTableModel):
- """ The data model for the axes tab in the plot curve editor.
- Acts as a go-between for the axes in a plot, and QTableView items. """
+ """The data model for the axes tab in the plot curve editor.
+ Acts as a go-between for the axes in a plot, and QTableView items."""
name_for_orientations = {v: k for k, v in BasePlotAxisItem.axis_orientations.items()}
def __init__(self, plot, parent=None):
super(BasePlotAxesModel, self).__init__(parent=parent)
self._plot = plot
- self._column_names = ("Y-Axis Name", "Y-Axis Orientation", "Y-Axis Label",
- "Min Y Range", "Max Y Range", "Enable Auto Range", "Log Mode")
+ self._column_names = (
+ "Y-Axis Name",
+ "Y-Axis Orientation",
+ "Y-Axis Label",
+ "Min Y Range",
+ "Max Y Range",
+ "Enable Auto Range",
+ "Log Mode",
+ )
@property
def plot(self):
@@ -51,7 +58,7 @@ def get_data(self, column_name, axis):
if column_name == "Y-Axis Name":
return axis.name
elif column_name == "Y-Axis Orientation":
- return self.name_for_orientations.get(axis.orientation, 'Left')
+ return self.name_for_orientations.get(axis.orientation, "Left")
elif column_name == "Y-Axis Label":
return axis.label_text
elif column_name == "Min Y Range":
@@ -63,7 +70,6 @@ def get_data(self, column_name, axis):
elif column_name == "Log Mode":
return axis.log_mode
-
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
@@ -88,7 +94,7 @@ def set_data(self, column_name, axis, value):
axis.name = str(value)
elif column_name == "Y-Axis Orientation":
if value is None:
- axis.orientation = 'left' # The PyQtGraph default is the left axis
+ axis.orientation = "left" # The PyQtGraph default is the left axis
else:
axis.orientation = str(value)
elif column_name == "Y-Axis Label":
@@ -107,26 +113,26 @@ def set_data(self, column_name, axis, value):
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
- return super(BasePlotAxesModel, self).headerData(
- section, orientation, role)
+ return super(BasePlotAxesModel, self).headerData(section, orientation, role)
if orientation == Qt.Horizontal and section < self.columnCount():
return str(self._column_names[section])
elif orientation == Qt.Vertical and section < self.rowCount():
return section
+
# End QAbstractItemModel implementation.
def append(self, name):
- """ Append a row to the table """
+ """Append a row to the table"""
self.beginInsertRows(QModelIndex(), len(self._plot._axes), len(self._plot._axes))
- self._plot.addAxis(plot_data_item=None, name=name, orientation='left')
+ self._plot.addAxis(plot_data_item=None, name=name, orientation="left")
self.endInsertRows()
def removeAtIndex(self, index):
- """ Removes the axis at the given index on the plot, along with its row in the view """
+ """Removes the axis at the given index on the plot, along with its row in the view"""
self.beginRemoveRows(QModelIndex(), index.row(), index.row())
self._plot.removeAxisAtIndex(index.row())
self.endRemoveRows()
def getColumnIndex(self, column_name):
- """ Returns the column index of the name. Raises a ValueError if it's not a valid column name """
+ """Returns the column index of the name. Raises a ValueError if it's not a valid column name"""
return self._column_names.index(column_name)
diff --git a/pydm/widgets/base.py b/pydm/widgets/base.py
index 6367f533ba..29f68e44dd 100644
--- a/pydm/widgets/base.py
+++ b/pydm/widgets/base.py
@@ -8,8 +8,7 @@
import json
import copy
import numpy as np
-from qtpy.QtWidgets import (QApplication, QMenu, QGraphicsOpacityEffect,
- QToolTip, QWidget)
+from qtpy.QtWidgets import QApplication, QMenu, QGraphicsOpacityEffect, QToolTip, QWidget
from qtpy.QtGui import QCursor, QIcon
from qtpy.QtCore import Qt, QEvent, Signal, Slot, Property
from .channel import PyDMChannel
@@ -119,7 +118,7 @@ def refresh_style(widget):
try:
widgets.extend(widget.findChildren(QWidget))
- except:
+ except Exception:
# If we fail it means that widget is probably destroyed
return
for child_widget in widgets:
@@ -129,7 +128,8 @@ def refresh_style(widget):
child_widget.update()
except Exception as ex:
# Widget was probably destroyed
- logger.debug('Error while refreshing stylesheet. %s ', ex)
+ logger.debug("Error while refreshing stylesheet. %s ", ex)
+
class PyDMPrimitiveWidget(object):
"""
@@ -137,11 +137,12 @@ class PyDMPrimitiveWidget(object):
All Widget classes from PyDMWidget will be True for
isinstance(obj, PyDMPrimitiveWidget)
"""
+
DEFAULT_RULE_PROPERTY = "Visible"
RULE_PROPERTIES = {
- 'Enable': ['setEnabled', bool],
- 'Visible': ['setVisible', bool],
- 'Opacity': ['set_opacity', float]
+ "Enable": ["setEnabled", bool],
+ "Visible": ["setVisible", bool],
+ "Opacity": ["set_opacity", float],
}
def __init__(self, **kwargs):
@@ -199,12 +200,12 @@ def show_address_tooltip(self, event):
EDM. If the QWidget does not have a valid PyDMChannel nothing will be
displayed
"""
- channels_method = getattr(self, 'channels', None)
+ channels_method = getattr(self, "channels", None)
if channels_method is None:
return
channels = channels_method()
if not channels:
- logger.debug('Widget has no channels to display tooltip')
+ logger.debug("Widget has no channels to display tooltip")
return
addrs = []
@@ -225,7 +226,7 @@ def show_address_tooltip(self, event):
clipboard = QApplication.clipboard()
mode = clipboard.Clipboard
- if platform.system() == 'Linux':
+ if platform.system() == "Linux":
# Mode Selection is only valid for X11.
mode = clipboard.Selection
@@ -279,13 +280,12 @@ def rule_evaluated(self, payload):
-------
None
"""
- name = payload.get('name', '')
- prop = payload.get('property', '')
- value = payload.get('value', None)
+ name = payload.get("name", "")
+ prop = payload.get("property", "")
+ value = payload.get("value", None)
if prop not in self.RULE_PROPERTIES:
- logger.error('Error at Rule: %s. %s is not part of this widget properties.',
- name, prop)
+ logger.error("Error at Rule: %s. %s is not part of this widget properties.", name, prop)
return
method_name, data_type = self.RULE_PROPERTIES[prop]
@@ -303,10 +303,14 @@ def rule_evaluated(self, payload):
else:
setattr(self, method_name, val)
- except:
- logger.error('Error at Rule: %s. Could not execute method %s with '
- 'value %s and type as %s.',
- name, method_name, value, data_type.__name__)
+ except Exception:
+ logger.error(
+ "Error at Rule: %s. Could not execute method %s with " "value %s and type as %s.",
+ name,
+ method_name,
+ value,
+ data_type.__name__,
+ )
@Property(str, designable=False)
def rules(self):
@@ -339,8 +343,8 @@ def rules(self, new_rules):
rules_list = json.loads(self._rules)
if rules_list:
RulesDispatcher().register(self, rules_list)
- except JSONDecodeError as ex:
- logger.exception('Invalid format for Rules')
+ except JSONDecodeError:
+ logger.exception("Invalid format for Rules")
def find_parent_display(self):
widget = self.parent()
@@ -352,7 +356,8 @@ def find_parent_display(self):
class AlarmLimit(str, enum.Enum):
- """ An enum for holding values corresponding to the EPICS alarm limits """
+ """An enum for holding values corresponding to the EPICS alarm limits"""
+
HIHI = "HIHI"
HIGH = "HIGH"
LOW = "LOW"
@@ -360,7 +365,6 @@ class AlarmLimit(str, enum.Enum):
class TextFormatter(object):
-
default_precision_from_pv = True
def __init__(self):
@@ -482,7 +486,6 @@ def unit_changed(self, new_unit):
if self.value is not None:
self.value_changed(self.value)
-
@Property(bool)
def showUnits(self):
"""
@@ -541,9 +544,7 @@ def precisionFromPV(self):
True means that the widget will use the precision information
from the Channel if available.
"""
- return (self._precision_from_pv
- if self._precision_from_pv is not None
- else self.default_precision_from_pv)
+ return self._precision_from_pv if self._precision_from_pv is not None else self.default_precision_from_pv
@precisionFromPV.setter
def precisionFromPV(self, value):
@@ -584,10 +585,8 @@ def value_changed(self, new_val):
self.update_format_string()
-_positionRuleProperties = {
- 'Position - X': ['setX', int],
- 'Position - Y': ['setY', int]
- }
+_positionRuleProperties = {"Position - X": ["setX", int], "Position - Y": ["setY", int]}
+
class PyDMWidget(PyDMPrimitiveWidget, new_properties=_positionRuleProperties):
"""
@@ -601,6 +600,7 @@ class PyDMWidget(PyDMPrimitiveWidget, new_properties=_positionRuleProperties):
The channel to be used by the widget.
"""
+
# Alarm types
ALARM_NONE = 0
ALARM_MINOR = 1
@@ -636,19 +636,21 @@ def __init__(self, init_channel=None):
self._pydm_tool_tip = ""
self._tool_tip_substrings = []
- self._tool_tip_channel_table = {"address": '_channel',
- "connection": '_connected',
- "SEVR": '_alarm_state',
- "enum_strings": 'enum_strings',
- "EGU": '_unit',
- "PREC": '_prec',
- "DRVH": '_upper_ctrl_limit',
- "DRVL": '_lower_ctrl_limit',
- "HIHI": 'upper_alarm_limit',
- "LOLO": 'lower_alarm_limit',
- "HIGH": 'upper_warning_limit',
- "LOW": 'lower_warning_limit',
- "TIME": "timestamp"}
+ self._tool_tip_channel_table = {
+ "address": "_channel",
+ "connection": "_connected",
+ "SEVR": "_alarm_state",
+ "enum_strings": "enum_strings",
+ "EGU": "_unit",
+ "PREC": "_prec",
+ "DRVH": "_upper_ctrl_limit",
+ "DRVL": "_lower_ctrl_limit",
+ "HIHI": "upper_alarm_limit",
+ "LOLO": "lower_alarm_limit",
+ "HIGH": "upper_warning_limit",
+ "LOW": "lower_warning_limit",
+ "TIME": "timestamp",
+ }
# If this label is inside a PyDMApplication (not Designer) start it in
# the disconnected state.
@@ -660,9 +662,7 @@ def __init__(self, init_channel=None):
self.alarmSeverityChanged(self.ALARM_DISCONNECTED)
self.check_enable_state()
- self.destroyed.connect(
- functools.partial(widget_destroyed, self.channels, weakref.ref(self))
- )
+ self.destroyed.connect(functools.partial(widget_destroyed, self.channels, weakref.ref(self)))
def widget_ctx_menu(self):
"""
@@ -689,7 +689,7 @@ def generate_context_menu(self):
if menu is None:
menu = QMenu(parent=self)
- kwargs = {'channels': self.channels_for_tools(), 'sender': self}
+ kwargs = {"channels": self.channels_for_tools(), "sender": self}
tools.assemble_tools_menu(menu, widget_only=True, widget=self, **kwargs)
# Add a view help action if the parent display has an associated help file
@@ -697,7 +697,7 @@ def generate_context_menu(self):
if parent_display is not None and parent_display.help_window is not None:
if len(menu.actions()) > 0:
menu.addSeparator()
- menu.addAction('View Help for this Display', parent_display.show_help)
+ menu.addAction("View Help for this Display", parent_display.show_help)
return menu
@@ -710,7 +710,7 @@ def open_context_menu(self, ev):
ev : QEvent
"""
menu = self.generate_context_menu()
- action = menu.exec_(self.mapToGlobal(ev.pos()))
+ menu.exec_(self.mapToGlobal(ev.pos()))
del menu
def init_for_designer(self):
@@ -1144,12 +1144,13 @@ def parseTip(self, new_tip):
return new_tip
if not self._tool_tip_substrings:
- list_of_attributes = [substring.start() for substring in re.finditer('\$\(', new_tip)]
+ list_of_attributes = [substring.start() for substring in re.finditer("\$\(", new_tip)]
tool_tip_substrings = []
for index in list_of_attributes:
- tool_tip_substrings.append([new_tip[index+2:new_tip.index(")", index)],
- new_tip[index:new_tip.index(")", index)+1]])
+ tool_tip_substrings.append(
+ [new_tip[index + 2 : new_tip.index(")", index)], new_tip[index : new_tip.index(")", index) + 1]]
+ )
self._tool_tip_substrings = copy.deepcopy(tool_tip_substrings)
else:
@@ -1157,9 +1158,9 @@ def parseTip(self, new_tip):
if tool_tip_substrings:
for index, value in enumerate(tool_tip_substrings):
- if value[0] == 'name':
+ if value[0] == "name":
value_of_attribute = self.channel
- elif value[0].split('.')[0] == 'pv_value':
+ elif value[0].split(".")[0] == "pv_value":
if value[0].count(".") == 0:
value_of_attribute = self.value
else:
@@ -1210,45 +1211,46 @@ def channel(self, value):
self.set_channel(value)
def set_channel(self, value):
- """ A setter method without a pyqt decorator so subclasses can use this functionality """
+ """A setter method without a pyqt decorator so subclasses can use this functionality"""
if self._channel != value:
# Remove old connections
- for channel in [c for c in self._channels if
- c.address == self._channel]:
+ for channel in [c for c in self._channels if c.address == self._channel]:
channel.disconnect()
self._channels.remove(channel)
# Load new channel
self._channel = str(value)
if not self._channel:
- logger.debug('Channel was set to an empty string.')
+ logger.debug("Channel was set to an empty string.")
return
- channel = PyDMChannel(address=self._channel,
- connection_slot=self.connectionStateChanged,
- value_slot=self.channelValueChanged,
- severity_slot=self.alarmSeverityChanged,
- enum_strings_slot=self.enumStringsChanged,
- unit_slot=None,
- prec_slot=None,
- upper_ctrl_limit_slot=self.upperCtrlLimitChanged,
- lower_ctrl_limit_slot=self.lowerCtrlLimitChanged,
- upper_alarm_limit_slot=self.upper_alarm_limit_changed,
- lower_alarm_limit_slot=self.lower_alarm_limit_changed,
- upper_warning_limit_slot=self.upper_warning_limit_changed,
- lower_warning_limit_slot=self.lower_warning_limit_changed,
- value_signal=None,
- write_access_slot=None,
- timestamp_slot=self.timestamp_changed)
+ channel = PyDMChannel(
+ address=self._channel,
+ connection_slot=self.connectionStateChanged,
+ value_slot=self.channelValueChanged,
+ severity_slot=self.alarmSeverityChanged,
+ enum_strings_slot=self.enumStringsChanged,
+ unit_slot=None,
+ prec_slot=None,
+ upper_ctrl_limit_slot=self.upperCtrlLimitChanged,
+ lower_ctrl_limit_slot=self.lowerCtrlLimitChanged,
+ upper_alarm_limit_slot=self.upper_alarm_limit_changed,
+ lower_alarm_limit_slot=self.lower_alarm_limit_changed,
+ upper_warning_limit_slot=self.upper_warning_limit_changed,
+ lower_warning_limit_slot=self.lower_warning_limit_changed,
+ value_signal=None,
+ write_access_slot=None,
+ timestamp_slot=self.timestamp_changed,
+ )
# Load writeable channels if our widget requires them. These should
# not exist on the base PyDMWidget but prevents us from duplicating
# the method below to only make two more connections
- if hasattr(self, 'writeAccessChanged'):
+ if hasattr(self, "writeAccessChanged"):
channel.write_access_slot = self.writeAccessChanged
- if hasattr(self, 'send_value_signal'):
+ if hasattr(self, "send_value_signal"):
channel.value_signal = self.send_value_signal
# Do the same thing for classes that use the TextFormatter mixin.
- if hasattr(self, 'unitChanged'):
+ if hasattr(self, "unitChanged"):
channel.unit_slot = self.unitChanged
- if hasattr(self, 'precisionChanged'):
+ if hasattr(self, "precisionChanged"):
channel.prec_slot = self.precisionChanged
# Connect write channels if we have them
channel.connect()
@@ -1269,10 +1271,10 @@ def check_enable_state(self):
status = self._connected
tooltip = self.restore_original_tooltip()
if not status:
- if tooltip != '':
- tooltip += '\n'
+ if tooltip != "":
+ tooltip += "\n"
tooltip += "PV is disconnected."
- tooltip += '\n'
+ tooltip += "\n"
tooltip += self.get_address()
self.setToolTip(tooltip)
@@ -1335,7 +1337,7 @@ def eventFilter(self, obj, event):
return false.
"""
- if event.type() == QEvent.Enter:
+ if event.type() == QEvent.Enter and self._connected:
if not self._pydm_tool_tip:
self.setToolTip(self.parseTip(self.toolTip()))
else:
@@ -1361,7 +1363,7 @@ 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], [object])"
# Emitted when the user changes the value.
send_value_signal = Signal([int], [float], [str], [bool], [object])
@@ -1398,7 +1400,7 @@ def eventFilter(self, obj, event):
True to stop the event from being handled further; otherwise
return false.
"""
- channel = getattr(self, 'channel', None)
+ channel = getattr(self, "channel", None)
if is_channel_valid(channel):
status = self._write_access and self._connected
@@ -1456,14 +1458,14 @@ def channel(self, value: str) -> None:
return
base_channel = self._channel.split(".", 1)[0] if "." in self._channel else self._channel
- if self._disp_channel is None or self._disp_channel.address != f'{base_channel}.DISP':
+ if self._disp_channel is None or self._disp_channel.address != f"{base_channel}.DISP":
if self._disp_channel is not None:
self._disp_channel.disconnect()
- self._disp_channel = PyDMChannel(address=f'{base_channel}.DISP', value_slot=self.disp_value_changed)
+ self._disp_channel = PyDMChannel(address=f"{base_channel}.DISP", value_slot=self.disp_value_changed)
self._disp_channel.connect()
def disp_value_changed(self, new_disp_value: int) -> None:
- """ Callback function to receive changes to the DISP field of the monitored channel """
+ """Callback function to receive changes to the DISP field of the monitored channel"""
self._disable_put = new_disp_value
self.check_enable_state()
@@ -1502,22 +1504,22 @@ def check_enable_state(self) -> None:
status = self._write_access and self._connected and not self._disable_put
tooltip = self.restore_original_tooltip()
if not self._connected:
- if tooltip != '':
- tooltip += '\n'
+ if tooltip != "":
+ tooltip += "\n"
tooltip += "PV is disconnected."
- tooltip += '\n'
+ tooltip += "\n"
tooltip += self.get_address()
elif not self._write_access:
- if tooltip != '':
- tooltip += '\n'
+ if tooltip != "":
+ tooltip += "\n"
if data_plugins.is_read_only():
tooltip += "Running PyDM on Read-Only mode."
else:
tooltip += "Access denied by Channel Access Security."
elif self._disable_put:
- if tooltip != '':
- tooltip += '\n'
- tooltip += 'Access denied by DISP field'
+ if tooltip != "":
+ tooltip += "\n"
+ tooltip += "Access denied by DISP field"
self.setToolTip(tooltip)
self.setEnabled(status)
diff --git a/pydm/widgets/baseplot.py b/pydm/widgets/baseplot.py
index edb3bbdcc1..0ed26b2e4d 100644
--- a/pydm/widgets/baseplot.py
+++ b/pydm/widgets/baseplot.py
@@ -15,6 +15,7 @@
class NoDataError(Exception):
"""NoDataError is raised when a curve tries to perform an operation,
but does not yet have any data."""
+
pass
@@ -47,46 +48,56 @@ class BasePlotCurveItem(PlotDataItem):
"""
REDRAW_ON_X, REDRAW_ON_Y, REDRAW_ON_EITHER, REDRAW_ON_BOTH = range(4)
- symbols = OrderedDict([('None', None),
- ('Circle', 'o'),
- ('Square', 's'),
- ('Triangle', 't'),
- ('Star', 'star'),
- ('Pentagon', 'p'),
- ('Hexagon', 'h'),
- ('X', 'x'),
- ('Diamond', 'd'),
- ('Plus', '+')])
- lines = OrderedDict([('NoLine', Qt.NoPen),
- ('Solid', Qt.SolidLine),
- ('Dash', Qt.DashLine),
- ('Dot', Qt.DotLine),
- ('DashDot', Qt.DashDotLine),
- ('DashDotDot', Qt.DashDotDotLine)])
+ symbols = OrderedDict(
+ [
+ ("None", None),
+ ("Circle", "o"),
+ ("Square", "s"),
+ ("Triangle", "t"),
+ ("Star", "star"),
+ ("Pentagon", "p"),
+ ("Hexagon", "h"),
+ ("X", "x"),
+ ("Diamond", "d"),
+ ("Plus", "+"),
+ ]
+ )
+ lines = OrderedDict(
+ [
+ ("NoLine", Qt.NoPen),
+ ("Solid", Qt.SolidLine),
+ ("Dash", Qt.DashLine),
+ ("Dot", Qt.DotLine),
+ ("DashDot", Qt.DashDotLine),
+ ("DashDotDot", Qt.DashDotDotLine),
+ ]
+ )
data_changed = Signal()
- def __init__(self,
- color: Optional[QColor] = None,
- lineStyle: Optional[Qt.PenStyle] = None,
- lineWidth: Optional[int] = None,
- yAxisName: Optional[str] = None,
- **kws) -> None:
- self._color = QColor('white')
- self._thresholdColor = QColor('white')
+ def __init__(
+ self,
+ color: Optional[QColor] = None,
+ lineStyle: Optional[Qt.PenStyle] = None,
+ lineWidth: Optional[int] = None,
+ yAxisName: Optional[str] = None,
+ **kws
+ ) -> None:
+ self._color = QColor("white")
+ self._thresholdColor = QColor("white")
self._pen = mkPen(self._color)
if lineWidth is not None:
self._pen.setWidth(lineWidth)
if lineStyle is not None:
self._pen.setStyle(lineStyle)
- kws['pen'] = self._pen
+ kws["pen"] = self._pen
super(BasePlotCurveItem, self).__init__(**kws)
self.setSymbolBrush(None)
if color is not None:
self.color = color
if yAxisName is None:
- self._y_axis_name = 'Axis 1'
+ self._y_axis_name = "Axis 1"
else:
self._y_axis_name = yAxisName
@@ -98,8 +109,7 @@ def __init__(self,
self.bar_graph_item = None
if hasattr(self, "channels"):
- self.destroyed.connect(functools.partial(widget_destroyed,
- self.channels))
+ self.destroyed.connect(functools.partial(widget_destroyed, self.channels))
@property
def color_string(self) -> str:
@@ -112,8 +122,7 @@ def color_string(self) -> str:
-------
str
"""
- return str(utilities.colors.svg_color_from_hex(self.color.name(),
- hex_on_fail=True))
+ return str(utilities.colors.svg_color_from_hex(self.color.name(), hex_on_fail=True))
@color_string.setter
def color_string(self, new_color_string: str) -> None:
@@ -170,8 +179,7 @@ def threshold_color_string(self) -> str:
-------
str
"""
- return str(utilities.colors.svg_color_from_hex(self.threshold_color.name(),
- hex_on_fail=True))
+ return str(utilities.colors.svg_color_from_hex(self.threshold_color.name(), hex_on_fail=True))
@property
def threshold_color(self) -> QColor:
@@ -278,7 +286,7 @@ def symbol(self) -> Union[str, None]:
-------
str or None
"""
- return self.opts['symbol']
+ return self.opts["symbol"]
@symbol.setter
def symbol(self, new_symbol: Union[str, None]) -> None:
@@ -304,7 +312,7 @@ def symbolSize(self) -> int:
-------
int
"""
- return self.opts['symbolSize']
+ return self.opts["symbolSize"]
@symbolSize.setter
def symbolSize(self, new_size: int) -> None:
@@ -317,8 +325,13 @@ def symbolSize(self, new_size: int) -> None:
"""
self.setSymbolSize(int(new_size))
- def setBarGraphInfo(self, bar_width: Optional[float] = 1.0, upper_threshold: Optional[float] = None,
- lower_threshold: Optional[float] = None, color: Optional[QColor] = None) -> None:
+ def setBarGraphInfo(
+ self,
+ bar_width: Optional[float] = 1.0,
+ upper_threshold: Optional[float] = None,
+ lower_threshold: Optional[float] = None,
+ color: Optional[QColor] = None,
+ ) -> None:
"""
Set the attributes associated with displaying a plot as a bar graph. These will only be set
if the plot is to be rendered as a bar graph. And any or all of them may be omitted even if it
@@ -349,17 +362,21 @@ def to_dict(self) -> OrderedDict:
-------
OrderedDict
"""
- return OrderedDict([("name", self.name()),
- ("color", self.color_string),
- ("lineStyle", self.lineStyle),
- ("lineWidth", self.lineWidth),
- ("symbol", self.symbol),
- ("symbolSize", self.symbolSize),
- ("yAxisName", self.y_axis_name),
- ("barWidth", self.bar_width),
- ("upperThreshold", self.upper_threshold),
- ("lowerThreshold", self.lower_threshold),
- ("thresholdColor", self.threshold_color_string)])
+ return OrderedDict(
+ [
+ ("name", self.name()),
+ ("color", self.color_string),
+ ("lineStyle", self.lineStyle),
+ ("lineWidth", self.lineWidth),
+ ("symbol", self.symbol),
+ ("symbolSize", self.symbolSize),
+ ("yAxisName", self.y_axis_name),
+ ("barWidth", self.bar_width),
+ ("upperThreshold", self.upper_threshold),
+ ("lowerThreshold", self.lower_threshold),
+ ("thresholdColor", self.threshold_color_string),
+ ]
+ )
def close(self) -> None:
pass
@@ -390,18 +407,19 @@ class BasePlotAxisItem(AxisItem):
Extra arguments for CSS style options for this axis
"""
- axis_orientations = OrderedDict([('Left', 'left'),
- ('Right', 'right')])
-
- def __init__(self,
- name: str,
- orientation: Optional[str] = 'left',
- label: Optional[str] = None,
- minRange: Optional[float] = -1.0,
- maxRange: Optional[float] = 1.0,
- autoRange: Optional[bool] = True,
- logMode: Optional[bool] = False,
- **kws) -> None:
+ axis_orientations = OrderedDict([("Left", "left"), ("Right", "right")])
+
+ def __init__(
+ self,
+ name: str,
+ orientation: Optional[str] = "left",
+ label: Optional[str] = None,
+ minRange: Optional[float] = -1.0,
+ maxRange: Optional[float] = 1.0,
+ autoRange: Optional[bool] = True,
+ logMode: Optional[bool] = False,
+ **kws
+ ) -> None:
super(BasePlotAxisItem, self).__init__(orientation, **kws)
self._name = name
@@ -459,12 +477,12 @@ def orientation(self, orientation: str) -> None:
@property
def label_text(self) -> str:
- """ Return the label to be displayed along this axis. """
+ """Return the label to be displayed along this axis."""
return self._label
@label_text.setter
def label_text(self, label: str):
- """ Set the label to be displayed along this axis """
+ """Set the label to be displayed along this axis"""
self._label = label
@property
@@ -564,13 +582,17 @@ def to_dict(self) -> OrderedDict:
-------
OrderedDict
"""
- return OrderedDict([("name", self._name),
- ("orientation", self._orientation),
- ("label", self._label),
- ("minRange", self._min_range),
- ("maxRange", self._max_range),
- ("autoRange", self._auto_range),
- ("logMode", self._log_mode)])
+ return OrderedDict(
+ [
+ ("name", self._name),
+ ("orientation", self._orientation),
+ ("label", self._label),
+ ("minRange", self._min_range),
+ ("maxRange", self._max_range),
+ ("autoRange", self._auto_range),
+ ("logMode", self._log_mode),
+ ]
+ )
class BasePlot(PlotWidget, PyDMPrimitiveWidget):
@@ -590,17 +612,19 @@ class BasePlot(PlotWidget, PyDMPrimitiveWidget):
not specified, the default pyqtgraph axis items (top, bottom, left, right) will be used
"""
- def __init__(self,
- parent: Optional[QWidget] = None,
- background: Optional[str] = 'default',
- axisItems: Optional[Dict[str, AxisItem]] = None) -> None:
+ def __init__(
+ self,
+ parent: Optional[QWidget] = None,
+ background: Optional[str] = "default",
+ axisItems: Optional[Dict[str, AxisItem]] = None,
+ ) -> None:
# First create a custom MultiAxisPlot to pass to the base PlotWidget class to support multiple y axes. Note
# that this plot will still function just fine in the case the user doesn't need additional y axes.
plotItem = MultiAxisPlot(axisItems=axisItems)
- if axisItems is None or 'left' not in axisItems:
+ if axisItems is None or "left" not in axisItems:
# The pyqtgraph PlotItem.setAxisItems() will always add an an AxisItem called left whether you asked
# it to or not. This will clear it if not specifically requested.
- plotItem.removeAxis('left')
+ plotItem.removeAxis("left")
super(BasePlot, self).__init__(parent=parent, background=background, plotItem=plotItem)
self.plotItem = plotItem
@@ -642,16 +666,16 @@ def __init__(self,
# Mouse mode to 1 button (left button draw rectangle for zoom)
self.plotItem.getViewBox().setMouseMode(ViewBox.RectMode)
- if self.getAxis('bottom') is not None:
+ if self.getAxis("bottom") is not None:
# Disables unexpected axis tick behavior described here:
# https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/axisitem.html
- self.getAxis('bottom').enableAutoSIPrefix(False)
+ self.getAxis("bottom").enableAutoSIPrefix(False)
if utilities.is_qt_designer():
self.installEventFilter(self)
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
- """ Display a tool tip upon mousing over the plot in Qt designer explaining how to edit curves on it """
+ """Display a tool tip upon mousing over the plot in Qt designer explaining how to edit curves on it"""
ret = super(BasePlot, self).eventFilter(obj, event)
if utilities.is_qt_designer():
if event.type() == QEvent.Enter:
@@ -660,7 +684,8 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
'Edit plot curves via Right-Click and select "Edit Curves..."',
self,
QRect(0, 0, 200, 100),
- 4000)
+ 4000,
+ )
else:
# Somehow super here is not invoking the PyDMPrimitiveWidget
# eventFilter
@@ -668,10 +693,9 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool:
return ret
- def addCurve(self,
- plot_data_item: BasePlotCurveItem,
- curve_color: Optional[QColor] = None,
- y_axis_name: Optional[str] = None):
+ def addCurve(
+ self, plot_data_item: BasePlotCurveItem, curve_color: Optional[QColor] = None, y_axis_name: Optional[str] = None
+ ):
"""
Adds a curve to this plot.
@@ -691,8 +715,7 @@ def addCurve(self,
"""
if curve_color is None:
- curve_color = utilities.colors.default_colors[
- len(self._curves) % len(utilities.colors.default_colors)]
+ curve_color = utilities.colors.default_colors[len(self._curves) % len(utilities.colors.default_colors)]
plot_data_item.color_string = curve_color
self._curves.append(plot_data_item)
@@ -700,28 +723,35 @@ def addCurve(self,
if y_axis_name is None:
if utilities.is_qt_designer():
# If we are just in designer, add an axis that will not conflict with the pyqtgraph default
- self.addAxis(plot_data_item=plot_data_item, name='Axis 1', orientation='left')
+ self.addAxis(plot_data_item=plot_data_item, name="Axis 1", orientation="left")
# If not in designer and the user did not name the axis, use the pyqtgraph default one named left
- elif 'left' not in self.plotItem.axes:
- self.addAxis(plot_data_item=plot_data_item, name='left', orientation='left')
+ elif "left" not in self.plotItem.axes:
+ self.addAxis(plot_data_item=plot_data_item, name="left", orientation="left")
else:
- self.plotItem.linkDataToAxis(plot_data_item, 'left')
+ self.plotItem.linkDataToAxis(plot_data_item, "left")
elif y_axis_name in self.plotItem.axes:
# If the user has chosen an axis that already exists for this curve, simply link the data to that axis
self.plotItem.linkDataToAxis(plot_data_item, y_axis_name)
else:
# Otherwise we create a brand new axis for this data
- self.addAxis(plot_data_item, y_axis_name, 'left')
+ self.addAxis(plot_data_item, y_axis_name, "left")
self.redraw_timer.start()
# Connect channels
for chan in plot_data_item.channels():
if chan:
chan.connect()
- def addAxis(self, plot_data_item: BasePlotCurveItem, name: str, orientation: str,
- label: Optional[str] = None, min_range: Optional[float] = -1.0,
- max_range: Optional[float] = 1.0, enable_auto_range: Optional[bool] = True,
- log_mode: Optional[bool] = False):
+ def addAxis(
+ self,
+ plot_data_item: BasePlotCurveItem,
+ name: str,
+ orientation: str,
+ label: Optional[str] = None,
+ min_range: Optional[float] = -1.0,
+ max_range: Optional[float] = 1.0,
+ enable_auto_range: Optional[bool] = True,
+ log_mode: Optional[bool] = False,
+ ):
"""
Create an AxisItem with the input name and orientation, and add it to
this plot.
@@ -755,8 +785,15 @@ def addAxis(self, plot_data_item: BasePlotCurveItem, name: str, orientation: str
if name in self.plotItem.axes:
return
- axis = BasePlotAxisItem(name=name, orientation=orientation, label=label, minRange=min_range,
- maxRange=max_range, autoRange=enable_auto_range, logMode=log_mode)
+ axis = BasePlotAxisItem(
+ name=name,
+ orientation=orientation,
+ label=label,
+ minRange=min_range,
+ maxRange=max_range,
+ autoRange=enable_auto_range,
+ logMode=log_mode,
+ )
axis.setLabel(text=label)
axis.enableAutoSIPrefix(False)
if plot_data_item is not None:
@@ -764,10 +801,17 @@ def addAxis(self, plot_data_item: BasePlotCurveItem, name: str, orientation: str
axis.setLogMode(log_mode)
self._axes.append(axis)
# If the x axis is just timestamps, we don't want autorange on the x axis
- setXLink = hasattr(self, '_plot_by_timestamps') and self._plot_by_timestamps
- self.plotItem.addAxis(axis, name=name, plotDataItem=plot_data_item, setXLink=setXLink,
- enableAutoRangeX=self.getAutoRangeX(), enableAutoRangeY=enable_auto_range,
- minRange=min_range, maxRange=max_range)
+ setXLink = hasattr(self, "_plot_by_timestamps") and self._plot_by_timestamps
+ self.plotItem.addAxis(
+ axis,
+ name=name,
+ plotDataItem=plot_data_item,
+ setXLink=setXLink,
+ enableAutoRangeX=self.getAutoRangeX(),
+ enableAutoRangeY=enable_auto_range,
+ minRange=min_range,
+ maxRange=max_range,
+ )
def removeCurve(self, plot_item: BasePlotCurveItem) -> None:
"""
@@ -817,7 +861,7 @@ def curves(self) -> List[BasePlotCurveItem]:
return self._curves
def clear(self) -> None:
- """ Remove all curves from the plot, as well as all items from the main view box """
+ """Remove all curves from the plot, as well as all items from the main view box"""
legend_items = [label.text for (sample, label) in self._legend.items]
for item in legend_items:
self._legend.removeItem(item)
@@ -825,7 +869,7 @@ def clear(self) -> None:
self._curves = []
def clearAxes(self) -> None:
- """ Clear out any added axes on this plot """
+ """Clear out any added axes on this plot"""
for axis in self._axes:
axis.deleteLater()
self.plotItem.clearAxes()
@@ -836,7 +880,7 @@ def redrawPlot(self) -> None:
pass
def getShowXGrid(self) -> bool:
- """ True if showing x grid lines on the plot, False otherwise """
+ """True if showing x grid lines on the plot, False otherwise"""
return self._show_x_grid
def setShowXGrid(self, value: bool, alpha: Optional[float] = None) -> None:
@@ -890,11 +934,11 @@ def setBackgroundColor(self, color: QColor) -> None:
backgroundColor = Property(QColor, getBackgroundColor, setBackgroundColor)
def getAxisColor(self) -> QColor:
- return self.getAxis('bottom')._pen.color()
+ return self.getAxis("bottom")._pen.color()
def setAxisColor(self, color: QColor) -> None:
for axis in self.plotItem.axes.values():
- axis['item'].setPen(color)
+ axis["item"].setPen(color)
axisColor = Property(QColor, getAxisColor, setAxisColor)
@@ -928,19 +972,26 @@ def setYAxes(self, new_list: List[str]) -> None:
return
self.clearAxes()
for d in new_list:
- self.addAxis(plot_data_item=None, name=d.get('name'), orientation=d.get('orientation'),
- label=d.get('label'), min_range=d.get('minRange'), max_range=d.get('maxRange'),
- enable_auto_range=d.get('autoRange'), log_mode=d.get('logMode'))
- if 'bottom' in self.plotItem.axes:
+ self.addAxis(
+ plot_data_item=None,
+ name=d.get("name"),
+ orientation=d.get("orientation"),
+ label=d.get("label"),
+ min_range=d.get("minRange"),
+ max_range=d.get("maxRange"),
+ enable_auto_range=d.get("autoRange"),
+ log_mode=d.get("logMode"),
+ )
+ if "bottom" in self.plotItem.axes:
# Ensure the added y axes get the color that was set
- self.setAxisColor(self.getAxis('bottom')._pen.color())
+ self.setAxisColor(self.getAxis("bottom")._pen.color())
if self.getShowYGrid() or self.getShowXGrid():
self.plotItem.updateGrid()
yAxes = Property("QStringList", getYAxes, setYAxes, designable=False)
def getBottomAxisLabel(self) -> str:
- return self.getAxis('bottom').labelText
+ return self.getAxis("bottom").labelText
def getShowRightAxis(self) -> bool:
"""
@@ -1006,31 +1057,37 @@ def resetXLabels(self) -> None:
xLabels = Property("QStringList", getXLabels, setXLabels, resetXLabels)
def getYLabels(self):
- warnings.warn("Y Labels should be retrieved from the AxisItem. See: AxisItem.label or AxisItem.labelText"
- "Example: self.getAxis('Axis Name').labelText",
- DeprecationWarning,
- stacklevel=2)
+ warnings.warn(
+ "Y Labels should be retrieved from the AxisItem. See: AxisItem.label or AxisItem.labelText"
+ "Example: self.getAxis('Axis Name').labelText",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return self._y_labels
def setYLabels(self, labels):
- warnings.warn("Y Labels should now be set on the AxisItem itself. See: AxisItem.setLabel() "
- "Example: self.getAxis('Axis Name').setLabel('Label Name')",
- DeprecationWarning,
- stacklevel=2)
+ warnings.warn(
+ "Y Labels should now be set on the AxisItem itself. See: AxisItem.setLabel() "
+ "Example: self.getAxis('Axis Name').setLabel('Label Name')",
+ DeprecationWarning,
+ stacklevel=2,
+ )
if self._y_labels != labels:
self._y_labels = labels
label = ""
if len(self._y_labels) > 0:
# Hardcoded for now as we only have one axis
label = self._y_labels[0]
- if 'left' in self.plotItem.axes:
+ if "left" in self.plotItem.axes:
self.setLabel("left", text=label)
def resetYLabels(self):
- warnings.warn("Y Labels should now be set on the AxisItem itself. See: AxisItem.setLabel() "
- "Example: self.getAxis('Axis Name').setLabel('')",
- DeprecationWarning,
- stacklevel=2)
+ warnings.warn(
+ "Y Labels should now be set on the AxisItem itself. See: AxisItem.setLabel() "
+ "Example: self.getAxis('Axis Name').setLabel('')",
+ DeprecationWarning,
+ stacklevel=2,
+ )
self._y_labels = []
self.setLabel("left", text="")
@@ -1222,7 +1279,7 @@ def mouseEnabledX(self) -> bool:
-------
bool
"""
- return self.plotItem.getViewBox().state['mouseEnabled'][0]
+ return self.plotItem.getViewBox().state["mouseEnabled"][0]
@mouseEnabledX.setter
def mouseEnabledX(self, x_enabled: bool) -> None:
@@ -1244,7 +1301,7 @@ def mouseEnabledY(self) -> bool:
-------
bool
"""
- return self.plotItem.getViewBox().state['mouseEnabled'][1]
+ return self.plotItem.getViewBox().state["mouseEnabled"][1]
@mouseEnabledY.setter
def mouseEnabledY(self, y_enabled: bool) -> None:
@@ -1280,7 +1337,7 @@ def maxRedrawRate(self, redraw_rate: int) -> None:
redraw_rate : int
"""
self._redraw_rate = redraw_rate
- self.redraw_timer.setInterval(int((1.0/self._redraw_rate)*1000))
+ self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))
def pausePlotting(self) -> bool:
self.redraw_timer.stop() if self.redraw_timer.isActive() else self.redraw_timer.start()
@@ -1305,14 +1362,16 @@ def mouseMoved(self, evt: QMouseEvent) -> None:
self.crosshair_position_updated.emit(mouse_point.x(), mouse_point.y())
- def enableCrosshair(self,
- is_enabled: bool,
- starting_x_pos: float,
- starting_y_pos: float,
- vertical_angle: Optional[float] = 90,
- horizontal_angle: Optional[float] = 0,
- vertical_movable: Optional[bool] = False,
- horizontal_movable: Optional[bool] = False) -> None:
+ def enableCrosshair(
+ self,
+ is_enabled: bool,
+ starting_x_pos: float,
+ starting_y_pos: float,
+ vertical_angle: Optional[float] = 90,
+ horizontal_angle: Optional[float] = 0,
+ vertical_movable: Optional[bool] = False,
+ horizontal_movable: Optional[bool] = False,
+ ) -> None:
"""
Enable the crosshair to be drawn on the ViewBox.
@@ -1334,15 +1393,18 @@ def enableCrosshair(self,
False if the horizontal line can be moved by the user; False is not.
"""
if is_enabled:
- self.vertical_crosshair_line = InfiniteLine(pos=starting_x_pos, angle=vertical_angle,
- movable=vertical_movable)
- self.horizontal_crosshair_line = InfiniteLine(pos=starting_y_pos, angle=horizontal_angle,
- movable=horizontal_movable)
+ self.vertical_crosshair_line = InfiniteLine(
+ pos=starting_x_pos, angle=vertical_angle, movable=vertical_movable
+ )
+ self.horizontal_crosshair_line = InfiniteLine(
+ pos=starting_y_pos, angle=horizontal_angle, movable=horizontal_movable
+ )
self.plotItem.addItem(self.vertical_crosshair_line)
self.plotItem.addItem(self.horizontal_crosshair_line)
- self.crosshair_movement_proxy = SignalProxy(self.plotItem.scene().sigMouseMoved, rateLimit=60,
- slot=self.mouseMoved)
+ self.crosshair_movement_proxy = SignalProxy(
+ self.plotItem.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved
+ )
else:
if self.vertical_crosshair_line:
self.plotItem.removeItem(self.vertical_crosshair_line)
@@ -1355,5 +1417,5 @@ def enableCrosshair(self,
try:
proxy.signal.disconnect(proxy.signalReceived)
proxy.sigDelayed.disconnect(proxy.slot)
- except:
+ except Exception:
pass
diff --git a/pydm/widgets/baseplot_curve_editor.py b/pydm/widgets/baseplot_curve_editor.py
index 8713f2f0f0..a6a8bd12c3 100644
--- a/pydm/widgets/baseplot_curve_editor.py
+++ b/pydm/widgets/baseplot_curve_editor.py
@@ -1,7 +1,21 @@
-from qtpy.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTableView,
- QAbstractItemView, QSpacerItem, QSizePolicy,
- QDialogButtonBox, QPushButton, QStyleOptionViewItem, QTabWidget, QWidget,
- QComboBox, QStyledItemDelegate, QColorDialog, QHeaderView)
+from qtpy.QtWidgets import (
+ QDialog,
+ QVBoxLayout,
+ QHBoxLayout,
+ QTableView,
+ QAbstractItemView,
+ QSpacerItem,
+ QSizePolicy,
+ QDialogButtonBox,
+ QPushButton,
+ QStyleOptionViewItem,
+ QTabWidget,
+ QWidget,
+ QComboBox,
+ QStyledItemDelegate,
+ QColorDialog,
+ QHeaderView,
+)
from qtpy.QtCore import Qt, Slot, QAbstractItemModel, QModelIndex, QObject, QItemSelection
from qtpy.QtDesigner import QDesignerFormWindowInterface
from .baseplot import BasePlotAxisItem, BasePlotCurveItem
@@ -18,6 +32,7 @@ class BasePlotCurveEditorDialog(QDialog):
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = BasePlotCurvesModel
AXIS_MODEL_CLASS = BasePlotAxesModel
AXIS_MODEL_TAB_INDEX = 1
@@ -42,10 +57,8 @@ def __init__(self, plot, parent=None):
self.remove_axis_button.clicked.connect(self.removeSelectedAxis)
self.remove_axis_button.setEnabled(False)
self.add_axis_count = 0
- self.table_view.selectionModel().selectionChanged.connect(
- self.handleSelectionChange)
- self.axis_view.selectionModel().selectionChanged.connect(
- self.handleSelectionChange)
+ self.table_view.selectionModel().selectionChanged.connect(self.handleSelectionChange)
+ self.axis_view.selectionModel().selectionChanged.connect(self.handleSelectionChange)
self.table_view.doubleClicked.connect(self.handleDoubleClick)
self.resize(800, 300)
@@ -80,8 +93,7 @@ def setup_ui(self):
self.tab_widget.currentChanged.connect(self.fillAxisData)
self.add_remove_layout = QHBoxLayout()
- spacer = QSpacerItem(40, 20, QSizePolicy.Expanding,
- QSizePolicy.Minimum)
+ spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.add_remove_layout.addItem(spacer)
self.add_button = QPushButton("Add Curve", self)
self.add_remove_layout.addWidget(self.add_button)
@@ -107,13 +119,13 @@ def setup_ui(self):
def setup_delegate_columns(self):
symbol_delegate = SymbolColumnDelegate(self)
- self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex('Symbol'), symbol_delegate)
+ self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Symbol"), symbol_delegate)
line_delegate = LineColumnDelegate(self)
- self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex('Line Style'), line_delegate)
+ self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Line Style"), line_delegate)
color_delegate = ColorColumnDelegate(self)
- self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex('Color'), color_delegate)
+ self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Color"), color_delegate)
axis_delegate = AxisColumnDelegate(self)
- self.axis_view.setItemDelegateForColumn(self.axis_model.getColumnIndex('Y-Axis Orientation'), axis_delegate)
+ self.axis_view.setItemDelegateForColumn(self.axis_model.getColumnIndex("Y-Axis Orientation"), axis_delegate)
@Slot()
def addCurve(self):
@@ -126,12 +138,12 @@ def removeSelectedCurve(self):
@Slot()
def addAxis(self):
self.add_axis_count += 1
- default_axis_name = 'New Axis ' + str(self.add_axis_count)
+ default_axis_name = "New Axis " + str(self.add_axis_count)
# Just a quick way to ensure that the default named axes are always unique, even when the user closes
# out a plot widget and re-opens it later
while default_axis_name in self.plot.plotItem.axes:
self.add_axis_count += 1
- default_axis_name = 'New Axis ' + str(self.add_axis_count)
+ default_axis_name = "New Axis " + str(self.add_axis_count)
self.axis_model.append(default_axis_name)
@Slot()
@@ -140,17 +152,14 @@ def removeSelectedAxis(self):
@Slot(QItemSelection, QItemSelection)
def handleSelectionChange(self, selected, deselected):
- self.remove_button.setEnabled(
- self.table_view.selectionModel().hasSelection())
- self.remove_axis_button.setEnabled(
- self.axis_view.selectionModel().hasSelection())
+ self.remove_button.setEnabled(self.table_view.selectionModel().hasSelection())
+ self.remove_axis_button.setEnabled(self.axis_view.selectionModel().hasSelection())
@Slot(QModelIndex)
def handleDoubleClick(self, index):
if self.table_model.needsColorDialog(index):
# The table model returns a QBrush for BackgroundRole, not a QColor
- init_color = self.table_model.data(index,
- Qt.BackgroundRole).color()
+ init_color = self.table_model.data(index, Qt.BackgroundRole).color()
color = QColorDialog.getColor(init_color, self)
if color.isValid():
self.table_model.setData(index, color, role=Qt.EditRole)
@@ -165,7 +174,7 @@ def saveChanges(self):
@Slot(int)
def fillAxisData(self, tab_index):
- """ When the user clicks on the axis tab, prefill it with rows based on the curves they have created """
+ """When the user clicks on the axis tab, prefill it with rows based on the curves they have created"""
# Toggle visibility of the buttons every time the tab changes
self.add_button.setVisible(not self.add_button.isVisible())
@@ -177,15 +186,15 @@ def fillAxisData(self, tab_index):
return # Nothing else to do if this is just the original "curves" tab
# Fix a display issue on the left axis when editing plots
- if 'left' in self.plot.plotItem.axes:
- self.plot.plotItem.hideAxis('left')
+ if "left" in self.plot.plotItem.axes:
+ self.plot.plotItem.hideAxis("left")
- axis_name_col_index = self.table_model.getColumnIndex('Y-Axis Name')
- curve_axis_names = [str(self.table_model.index(i, axis_name_col_index).data())
- for i in range(self.table_model.rowCount())]
+ axis_name_col_index = self.table_model.getColumnIndex("Y-Axis Name")
+ curve_axis_names = [
+ str(self.table_model.index(i, axis_name_col_index).data()) for i in range(self.table_model.rowCount())
+ ]
- existing_axis_names = [str(self.axis_model.index(i, 0).data())
- for i in range(self.axis_model.rowCount())]
+ existing_axis_names = [str(self.axis_model.index(i, 0).data()) for i in range(self.axis_model.rowCount())]
# Removing duplicates here instead of using a set to preserve order
names_to_add = []
@@ -203,6 +212,7 @@ class ColorColumnDelegate(QStyledItemDelegate):
color column of the table view. Its only job is to ensure that the default
editor widget (a line edit) isn't displayed for items in the color column.
"""
+
def createEditor(self, parent, option, index):
return None
@@ -214,6 +224,7 @@ class AxisColumnDelegate(QStyledItemDelegate):
column value, which must map to the values expected by PyQtGraph. Helps ensure that the
user doesn't have to know what these exact values are, and prevents frustrating typos.
"""
+
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(BasePlotAxisItem.axis_orientations.keys())
@@ -235,6 +246,7 @@ class LineColumnDelegate(QStyledItemDelegate):
"""LineColumnDelegate draws a QComboBox in the Line Style column, so that users
can pick the styles they want to display from a list, instead of needing to
remember the PyQtGraph character codes."""
+
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(BasePlotCurveItem.lines.keys())
@@ -256,6 +268,7 @@ class SymbolColumnDelegate(QStyledItemDelegate):
"""SymbolColumnDelegate draws a QComboBox in the Symbol column, so that users
can pick the symbol they want to display from a list, instead of needing to
remember the PyQtGraph character codes."""
+
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
editor.addItems(BasePlotCurveItem.symbols.keys())
@@ -276,11 +289,15 @@ def updateEditorGeometry(self, editor, option, index):
class RedrawModeColumnDelegate(QStyledItemDelegate):
"""RedrawModeColumnDelegate draws a QComboBox in the Redraw Mode column, so
that users can pick the redraw mode from a list."""
- choices = OrderedDict([
- ('X or Y updates', BasePlotCurveItem.REDRAW_ON_EITHER),
- ('Y updates', BasePlotCurveItem.REDRAW_ON_Y),
- ('X updates', BasePlotCurveItem.REDRAW_ON_X),
- ('Both update', BasePlotCurveItem.REDRAW_ON_BOTH)])
+
+ choices = OrderedDict(
+ [
+ ("X or Y updates", BasePlotCurveItem.REDRAW_ON_EITHER),
+ ("Y updates", BasePlotCurveItem.REDRAW_ON_Y),
+ ("X updates", BasePlotCurveItem.REDRAW_ON_X),
+ ("Both update", BasePlotCurveItem.REDRAW_ON_BOTH),
+ ]
+ )
text_for_choices = {v: k for k, v in choices.items()}
def displayText(self, value, locale):
@@ -304,10 +321,10 @@ def updateEditorGeometry(self, editor, option, index):
class PlotStyleColumnDelegate(QStyledItemDelegate):
- """ Allows the user to toggle between line and bar graphs. Hides/shows relevant columns based on that choice. """
+ """Allows the user to toggle between line and bar graphs. Hides/shows relevant columns based on that choice."""
- line_columns_to_toggle = ('Line Style', 'Line Width', 'Symbol', 'Symbol Size')
- bar_columns_to_toggle = ('Bar Width', 'Upper Limit', 'Lower Limit', 'Limit Color')
+ line_columns_to_toggle = ("Line Style", "Line Width", "Symbol", "Symbol Size")
+ bar_columns_to_toggle = ("Bar Width", "Upper Limit", "Lower Limit", "Limit Color")
def __init__(self, parent: QObject, table_model: BasePlotCurvesModel, table_view: QTableView):
super().__init__(parent)
@@ -315,9 +332,9 @@ def __init__(self, parent: QObject, table_model: BasePlotCurvesModel, table_view
self.table_view = table_view
def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex) -> QWidget:
- """ Create a combo box that allows the user to choose the style of plot they want. """
+ """Create a combo box that allows the user to choose the style of plot they want."""
editor = QComboBox(parent)
- editor.addItems(('Line', 'Bar'))
+ editor.addItems(("Line", "Bar"))
return editor
def setEditorData(self, editor: QWidget, index: QModelIndex) -> None:
@@ -332,21 +349,21 @@ def updateEditorGeometry(self, editor: QWidget, option: QStyleOptionViewItem, in
editor.setGeometry(option.rect)
def toggleColumnVisibility(self):
- """ Toggle visibility of columns based on the current state of the associated curve editor table """
+ """Toggle visibility of columns based on the current state of the associated curve editor table"""
self.hideColumns(hide_line_columns=True, hide_bar_columns=True)
if len(self.table_model.plot._curves) > 0:
for curve in self.table_model.plot._curves:
plot_style = curve.plot_style
- if plot_style is None or plot_style == 'Line':
+ if plot_style is None or plot_style == "Line":
self.hideColumns(hide_line_columns=False)
- elif plot_style == 'Bar':
+ elif plot_style == "Bar":
self.hideColumns(hide_bar_columns=False)
else:
self.hideColumns(False, True) # Show line columns only as a default
def hideColumns(self, hide_line_columns: Optional[bool] = None, hide_bar_columns: Optional[bool] = None) -> None:
- """ Show or hide columns related to a specific plot style based on the input. If an input parameter
- is omitted (or explicitly set to None), the associated columns will be left alone. """
+ """Show or hide columns related to a specific plot style based on the input. If an input parameter
+ is omitted (or explicitly set to None), the associated columns will be left alone."""
if hide_line_columns is not None:
for column in self.line_columns_to_toggle:
diff --git a/pydm/widgets/baseplot_table_model.py b/pydm/widgets/baseplot_table_model.py
index 607ca34601..9a0a4ab576 100644
--- a/pydm/widgets/baseplot_table_model.py
+++ b/pydm/widgets/baseplot_table_model.py
@@ -14,9 +14,19 @@ class BasePlotCurvesModel(QAbstractTableModel):
def __init__(self, plot, parent=None):
super(BasePlotCurvesModel, self).__init__(parent=parent)
self._plot = plot
- self._column_names = ("Label", "Color", "Y-Axis Name", "Line Style",
- "Line Width", "Symbol", "Symbol Size", "Bar Width",
- "Upper Limit", "Lower Limit", "Limit Color")
+ self._column_names = (
+ "Label",
+ "Color",
+ "Y-Axis Name",
+ "Line Style",
+ "Line Width",
+ "Symbol",
+ "Symbol Size",
+ "Bar Width",
+ "Upper Limit",
+ "Lower Limit",
+ "Limit Color",
+ )
@property
def plot(self):
@@ -141,12 +151,12 @@ def set_data(self, column_name, curve, value):
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role != Qt.DisplayRole:
- return super(BasePlotCurvesModel, self).headerData(
- section, orientation, role)
+ return super(BasePlotCurvesModel, self).headerData(section, orientation, role)
if orientation == Qt.Horizontal and section < self.columnCount():
return str(self._column_names[section])
elif orientation == Qt.Vertical and section < self.rowCount():
return section
+
# End QAbstractItemModel implementation.
def append(self, name=None, color=None):
@@ -156,7 +166,7 @@ def removeAtIndex(self, index):
pass
def getColumnIndex(self, column_name):
- """ Returns the column index of the name. Raises a ValueError if it's not a valid column name """
+ """Returns the column index of the name. Raises a ValueError if it's not a valid column name"""
return self._column_names.index(column_name)
def needsColorDialog(self, index):
diff --git a/pydm/widgets/byte.py b/pydm/widgets/byte.py
index 22370a466f..9e161d861a 100644
--- a/pydm/widgets/byte.py
+++ b/pydm/widgets/byte.py
@@ -15,6 +15,7 @@ class PyDMBitIndicator(QWidget):
The parent widget for the Label
"""
+
def __init__(self, parent: Optional[QWidget] = None, circle: bool = False):
super(PyDMBitIndicator, self).__init__(parent)
self.circle = circle
@@ -77,6 +78,7 @@ class PyDMByteIndicator(QWidget, PyDMWidget):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent: Optional[QWidget] = None, init_channel=None):
QWidget.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
@@ -201,8 +203,7 @@ def update_indicators(self) -> None:
if value < 0:
value = 0
- bits = [(value >> i) & 1
- for i in range(self._num_bits)]
+ bits = [(value >> i) & 1 for i in range(self._num_bits)]
for bit, indicator in zip(bits, self._indicators):
if self._connected:
if self._alarm_state == 3:
@@ -352,10 +353,12 @@ def bigEndian(self, is_big_endian: bool) -> None:
"""
self._big_endian = is_big_endian
- origin_map = {(Qt.Vertical, True): Qt.BottomLeftCorner,
- (Qt.Vertical, False): Qt.TopLeftCorner,
- (Qt.Horizontal, True): Qt.TopRightCorner,
- (Qt.Horizontal, False): Qt.TopLeftCorner}
+ origin_map = {
+ (Qt.Vertical, True): Qt.BottomLeftCorner,
+ (Qt.Vertical, False): Qt.TopLeftCorner,
+ (Qt.Horizontal, True): Qt.TopRightCorner,
+ (Qt.Horizontal, False): Qt.TopLeftCorner,
+ }
origin = origin_map[(self.orientation, self.bigEndian)]
self.layout().setOriginCorner(origin)
@@ -436,8 +439,7 @@ def numBits(self, new_num_bits: int) -> None:
self._num_bits = new_num_bits
for indicator in self._indicators:
indicator.deleteLater()
- self._indicators = [PyDMBitIndicator(parent=self, circle=self.circles)
- for i in range(0, self._num_bits)]
+ self._indicators = [PyDMBitIndicator(parent=self, circle=self.circles) for i in range(0, self._num_bits)]
old_labels = self.labels
new_labels = ["Bit {}".format(i) for i in range(0, self._num_bits)]
for i, old_label in enumerate(old_labels):
@@ -469,7 +471,7 @@ def shift(self, new_shift: int) -> None:
self._shift = new_shift
self.update_indicators()
- @Property('QStringList')
+ @Property("QStringList")
def labels(self) -> List[str]:
"""
Labels for each bit.
@@ -478,7 +480,7 @@ def labels(self) -> List[str]:
-------
list
"""
- return [str(l.text()) for l in self._labels]
+ return [str(currLabel.text()) for currLabel in self._labels]
@labels.setter
def labels(self, new_labels: List[str]) -> None:
@@ -510,7 +512,7 @@ def value_changed(self, new_val: int) -> None:
try:
int(new_val)
self.update_indicators()
- except:
+ except Exception:
pass
def paintEvent(self, _) -> None:
@@ -551,5 +553,5 @@ def alarm_severity_changed(self, new_alarm_severity: int) -> None:
# Checks if _shift attribute exits because base class can call method
# before the object constructor is complete
- if hasattr(self, '_shift'):
+ if hasattr(self, "_shift"):
self.update_indicators()
diff --git a/pydm/widgets/channel.py b/pydm/widgets/channel.py
index f84a55223b..58fd7953cf 100644
--- a/pydm/widgets/channel.py
+++ b/pydm/widgets/channel.py
@@ -91,13 +91,26 @@ class PyDMChannel(object):
A function to be run when the timestamp updates
"""
- def __init__(self, address=None, connection_slot=None, value_slot=None,
- severity_slot=None, write_access_slot=None,
- enum_strings_slot=None, unit_slot=None, prec_slot=None,
- upper_ctrl_limit_slot=None, lower_ctrl_limit_slot=None,
- upper_alarm_limit_slot=None, lower_alarm_limit_slot=None,
- upper_warning_limit_slot=None, lower_warning_limit_slot=None,
- value_signal=None, timestamp_slot=None):
+
+ def __init__(
+ self,
+ address=None,
+ connection_slot=None,
+ value_slot=None,
+ severity_slot=None,
+ write_access_slot=None,
+ enum_strings_slot=None,
+ unit_slot=None,
+ prec_slot=None,
+ upper_ctrl_limit_slot=None,
+ lower_ctrl_limit_slot=None,
+ upper_alarm_limit_slot=None,
+ lower_alarm_limit_slot=None,
+ upper_warning_limit_slot=None,
+ lower_warning_limit_slot=None,
+ value_signal=None,
+ timestamp_slot=None,
+ ):
self._address = None
self.address = address
@@ -138,8 +151,7 @@ def connect(self):
try:
pydm.data_plugins.establish_connection(self)
except Exception:
- logger.exception("Unable to make proper connection "
- "for %r", self)
+ logger.exception("Unable to make proper connection " "for %r", self)
def disconnect(self, destroying=False):
"""
@@ -150,9 +162,8 @@ def disconnect(self, destroying=False):
if not plugin:
return
plugin.remove_connection(self, destroying=destroying)
- except Exception as exc:
- logger.exception("Unable to remove connection "
- "for %r", self)
+ except Exception:
+ logger.exception("Unable to remove connection " "for %r", self)
def __eq__(self, other):
if isinstance(self, other.__class__):
@@ -176,22 +187,24 @@ def __eq__(self, other):
if self.value_signal and other.value_signal:
value_signal_matched = self.value_signal.signal == other.value_signal.signal
- return (address_matched and
- connection_slot_matched and
- value_slot_matched and
- severity_slot_matched and
- enum_strings_slot_matched and
- unit_slot_matched and
- prec_slot_matched and
- upper_ctrl_slot_matched and
- lower_ctrl_slot_matched and
- upper_alarm_slot_matched and
- lower_alarm_slot_matched and
- upper_warning_slot_matched and
- lower_warning_slot_matched and
- write_access_slot_matched and
- value_signal_matched and
- timestamp_slot_matched)
+ return (
+ address_matched
+ and connection_slot_matched
+ and value_slot_matched
+ and severity_slot_matched
+ and enum_strings_slot_matched
+ and unit_slot_matched
+ and prec_slot_matched
+ and upper_ctrl_slot_matched
+ and lower_ctrl_slot_matched
+ and upper_alarm_slot_matched
+ and lower_alarm_slot_matched
+ and upper_warning_slot_matched
+ and lower_warning_slot_matched
+ and write_access_slot_matched
+ and value_signal_matched
+ and timestamp_slot_matched
+ )
return NotImplemented
@@ -205,4 +218,4 @@ def __hash__(self):
return id(self)
def __repr__(self):
- return ''.format(self.address)
+ return "".format(self.address)
diff --git a/pydm/widgets/checkbox.py b/pydm/widgets/checkbox.py
index 8b9ac1a0cf..ae415ee9a0 100644
--- a/pydm/widgets/checkbox.py
+++ b/pydm/widgets/checkbox.py
@@ -14,6 +14,7 @@ class PyDMCheckbox(QCheckBox, PyDMWritableWidget):
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
QCheckBox.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
diff --git a/pydm/widgets/colormaps.py b/pydm/widgets/colormaps.py
index dcff5b4bd5..46760e2571 100755
--- a/pydm/widgets/colormaps.py
+++ b/pydm/widgets/colormaps.py
@@ -15,1041 +15,1060 @@
import numpy as np
-__all__ = ['magma', 'inferno', 'plasma', 'viridis', 'jet', 'monochrome', 'hot']
+__all__ = ["magma", "inferno", "plasma", "viridis", "jet", "monochrome", "hot"]
-_magma_data = [[0.001462, 0.000466, 0.013866],
- [0.002258, 0.001295, 0.018331],
- [0.003279, 0.002305, 0.023708],
- [0.004512, 0.003490, 0.029965],
- [0.005950, 0.004843, 0.037130],
- [0.007588, 0.006356, 0.044973],
- [0.009426, 0.008022, 0.052844],
- [0.011465, 0.009828, 0.060750],
- [0.013708, 0.011771, 0.068667],
- [0.016156, 0.013840, 0.076603],
- [0.018815, 0.016026, 0.084584],
- [0.021692, 0.018320, 0.092610],
- [0.024792, 0.020715, 0.100676],
- [0.028123, 0.023201, 0.108787],
- [0.031696, 0.025765, 0.116965],
- [0.035520, 0.028397, 0.125209],
- [0.039608, 0.031090, 0.133515],
- [0.043830, 0.033830, 0.141886],
- [0.048062, 0.036607, 0.150327],
- [0.052320, 0.039407, 0.158841],
- [0.056615, 0.042160, 0.167446],
- [0.060949, 0.044794, 0.176129],
- [0.065330, 0.047318, 0.184892],
- [0.069764, 0.049726, 0.193735],
- [0.074257, 0.052017, 0.202660],
- [0.078815, 0.054184, 0.211667],
- [0.083446, 0.056225, 0.220755],
- [0.088155, 0.058133, 0.229922],
- [0.092949, 0.059904, 0.239164],
- [0.097833, 0.061531, 0.248477],
- [0.102815, 0.063010, 0.257854],
- [0.107899, 0.064335, 0.267289],
- [0.113094, 0.065492, 0.276784],
- [0.118405, 0.066479, 0.286321],
- [0.123833, 0.067295, 0.295879],
- [0.129380, 0.067935, 0.305443],
- [0.135053, 0.068391, 0.315000],
- [0.140858, 0.068654, 0.324538],
- [0.146785, 0.068738, 0.334011],
- [0.152839, 0.068637, 0.343404],
- [0.159018, 0.068354, 0.352688],
- [0.165308, 0.067911, 0.361816],
- [0.171713, 0.067305, 0.370771],
- [0.178212, 0.066576, 0.379497],
- [0.184801, 0.065732, 0.387973],
- [0.191460, 0.064818, 0.396152],
- [0.198177, 0.063862, 0.404009],
- [0.204935, 0.062907, 0.411514],
- [0.211718, 0.061992, 0.418647],
- [0.218512, 0.061158, 0.425392],
- [0.225302, 0.060445, 0.431742],
- [0.232077, 0.059889, 0.437695],
- [0.238826, 0.059517, 0.443256],
- [0.245543, 0.059352, 0.448436],
- [0.252220, 0.059415, 0.453248],
- [0.258857, 0.059706, 0.457710],
- [0.265447, 0.060237, 0.461840],
- [0.271994, 0.060994, 0.465660],
- [0.278493, 0.061978, 0.469190],
- [0.284951, 0.063168, 0.472451],
- [0.291366, 0.064553, 0.475462],
- [0.297740, 0.066117, 0.478243],
- [0.304081, 0.067835, 0.480812],
- [0.310382, 0.069702, 0.483186],
- [0.316654, 0.071690, 0.485380],
- [0.322899, 0.073782, 0.487408],
- [0.329114, 0.075972, 0.489287],
- [0.335308, 0.078236, 0.491024],
- [0.341482, 0.080564, 0.492631],
- [0.347636, 0.082946, 0.494121],
- [0.353773, 0.085373, 0.495501],
- [0.359898, 0.087831, 0.496778],
- [0.366012, 0.090314, 0.497960],
- [0.372116, 0.092816, 0.499053],
- [0.378211, 0.095332, 0.500067],
- [0.384299, 0.097855, 0.501002],
- [0.390384, 0.100379, 0.501864],
- [0.396467, 0.102902, 0.502658],
- [0.402548, 0.105420, 0.503386],
- [0.408629, 0.107930, 0.504052],
- [0.414709, 0.110431, 0.504662],
- [0.420791, 0.112920, 0.505215],
- [0.426877, 0.115395, 0.505714],
- [0.432967, 0.117855, 0.506160],
- [0.439062, 0.120298, 0.506555],
- [0.445163, 0.122724, 0.506901],
- [0.451271, 0.125132, 0.507198],
- [0.457386, 0.127522, 0.507448],
- [0.463508, 0.129893, 0.507652],
- [0.469640, 0.132245, 0.507809],
- [0.475780, 0.134577, 0.507921],
- [0.481929, 0.136891, 0.507989],
- [0.488088, 0.139186, 0.508011],
- [0.494258, 0.141462, 0.507988],
- [0.500438, 0.143719, 0.507920],
- [0.506629, 0.145958, 0.507806],
- [0.512831, 0.148179, 0.507648],
- [0.519045, 0.150383, 0.507443],
- [0.525270, 0.152569, 0.507192],
- [0.531507, 0.154739, 0.506895],
- [0.537755, 0.156894, 0.506551],
- [0.544015, 0.159033, 0.506159],
- [0.550287, 0.161158, 0.505719],
- [0.556571, 0.163269, 0.505230],
- [0.562866, 0.165368, 0.504692],
- [0.569172, 0.167454, 0.504105],
- [0.575490, 0.169530, 0.503466],
- [0.581819, 0.171596, 0.502777],
- [0.588158, 0.173652, 0.502035],
- [0.594508, 0.175701, 0.501241],
- [0.600868, 0.177743, 0.500394],
- [0.607238, 0.179779, 0.499492],
- [0.613617, 0.181811, 0.498536],
- [0.620005, 0.183840, 0.497524],
- [0.626401, 0.185867, 0.496456],
- [0.632805, 0.187893, 0.495332],
- [0.639216, 0.189921, 0.494150],
- [0.645633, 0.191952, 0.492910],
- [0.652056, 0.193986, 0.491611],
- [0.658483, 0.196027, 0.490253],
- [0.664915, 0.198075, 0.488836],
- [0.671349, 0.200133, 0.487358],
- [0.677786, 0.202203, 0.485819],
- [0.684224, 0.204286, 0.484219],
- [0.690661, 0.206384, 0.482558],
- [0.697098, 0.208501, 0.480835],
- [0.703532, 0.210638, 0.479049],
- [0.709962, 0.212797, 0.477201],
- [0.716387, 0.214982, 0.475290],
- [0.722805, 0.217194, 0.473316],
- [0.729216, 0.219437, 0.471279],
- [0.735616, 0.221713, 0.469180],
- [0.742004, 0.224025, 0.467018],
- [0.748378, 0.226377, 0.464794],
- [0.754737, 0.228772, 0.462509],
- [0.761077, 0.231214, 0.460162],
- [0.767398, 0.233705, 0.457755],
- [0.773695, 0.236249, 0.455289],
- [0.779968, 0.238851, 0.452765],
- [0.786212, 0.241514, 0.450184],
- [0.792427, 0.244242, 0.447543],
- [0.798608, 0.247040, 0.444848],
- [0.804752, 0.249911, 0.442102],
- [0.810855, 0.252861, 0.439305],
- [0.816914, 0.255895, 0.436461],
- [0.822926, 0.259016, 0.433573],
- [0.828886, 0.262229, 0.430644],
- [0.834791, 0.265540, 0.427671],
- [0.840636, 0.268953, 0.424666],
- [0.846416, 0.272473, 0.421631],
- [0.852126, 0.276106, 0.418573],
- [0.857763, 0.279857, 0.415496],
- [0.863320, 0.283729, 0.412403],
- [0.868793, 0.287728, 0.409303],
- [0.874176, 0.291859, 0.406205],
- [0.879464, 0.296125, 0.403118],
- [0.884651, 0.300530, 0.400047],
- [0.889731, 0.305079, 0.397002],
- [0.894700, 0.309773, 0.393995],
- [0.899552, 0.314616, 0.391037],
- [0.904281, 0.319610, 0.388137],
- [0.908884, 0.324755, 0.385308],
- [0.913354, 0.330052, 0.382563],
- [0.917689, 0.335500, 0.379915],
- [0.921884, 0.341098, 0.377376],
- [0.925937, 0.346844, 0.374959],
- [0.929845, 0.352734, 0.372677],
- [0.933606, 0.358764, 0.370541],
- [0.937221, 0.364929, 0.368567],
- [0.940687, 0.371224, 0.366762],
- [0.944006, 0.377643, 0.365136],
- [0.947180, 0.384178, 0.363701],
- [0.950210, 0.390820, 0.362468],
- [0.953099, 0.397563, 0.361438],
- [0.955849, 0.404400, 0.360619],
- [0.958464, 0.411324, 0.360014],
- [0.960949, 0.418323, 0.359630],
- [0.963310, 0.425390, 0.359469],
- [0.965549, 0.432519, 0.359529],
- [0.967671, 0.439703, 0.359810],
- [0.969680, 0.446936, 0.360311],
- [0.971582, 0.454210, 0.361030],
- [0.973381, 0.461520, 0.361965],
- [0.975082, 0.468861, 0.363111],
- [0.976690, 0.476226, 0.364466],
- [0.978210, 0.483612, 0.366025],
- [0.979645, 0.491014, 0.367783],
- [0.981000, 0.498428, 0.369734],
- [0.982279, 0.505851, 0.371874],
- [0.983485, 0.513280, 0.374198],
- [0.984622, 0.520713, 0.376698],
- [0.985693, 0.528148, 0.379371],
- [0.986700, 0.535582, 0.382210],
- [0.987646, 0.543015, 0.385210],
- [0.988533, 0.550446, 0.388365],
- [0.989363, 0.557873, 0.391671],
- [0.990138, 0.565296, 0.395122],
- [0.990871, 0.572706, 0.398714],
- [0.991558, 0.580107, 0.402441],
- [0.992196, 0.587502, 0.406299],
- [0.992785, 0.594891, 0.410283],
- [0.993326, 0.602275, 0.414390],
- [0.993834, 0.609644, 0.418613],
- [0.994309, 0.616999, 0.422950],
- [0.994738, 0.624350, 0.427397],
- [0.995122, 0.631696, 0.431951],
- [0.995480, 0.639027, 0.436607],
- [0.995810, 0.646344, 0.441361],
- [0.996096, 0.653659, 0.446213],
- [0.996341, 0.660969, 0.451160],
- [0.996580, 0.668256, 0.456192],
- [0.996775, 0.675541, 0.461314],
- [0.996925, 0.682828, 0.466526],
- [0.997077, 0.690088, 0.471811],
- [0.997186, 0.697349, 0.477182],
- [0.997254, 0.704611, 0.482635],
- [0.997325, 0.711848, 0.488154],
- [0.997351, 0.719089, 0.493755],
- [0.997351, 0.726324, 0.499428],
- [0.997341, 0.733545, 0.505167],
- [0.997285, 0.740772, 0.510983],
- [0.997228, 0.747981, 0.516859],
- [0.997138, 0.755190, 0.522806],
- [0.997019, 0.762398, 0.528821],
- [0.996898, 0.769591, 0.534892],
- [0.996727, 0.776795, 0.541039],
- [0.996571, 0.783977, 0.547233],
- [0.996369, 0.791167, 0.553499],
- [0.996162, 0.798348, 0.559820],
- [0.995932, 0.805527, 0.566202],
- [0.995680, 0.812706, 0.572645],
- [0.995424, 0.819875, 0.579140],
- [0.995131, 0.827052, 0.585701],
- [0.994851, 0.834213, 0.592307],
- [0.994524, 0.841387, 0.598983],
- [0.994222, 0.848540, 0.605696],
- [0.993866, 0.855711, 0.612482],
- [0.993545, 0.862859, 0.619299],
- [0.993170, 0.870024, 0.626189],
- [0.992831, 0.877168, 0.633109],
- [0.992440, 0.884330, 0.640099],
- [0.992089, 0.891470, 0.647116],
- [0.991688, 0.898627, 0.654202],
- [0.991332, 0.905763, 0.661309],
- [0.990930, 0.912915, 0.668481],
- [0.990570, 0.920049, 0.675675],
- [0.990175, 0.927196, 0.682926],
- [0.989815, 0.934329, 0.690198],
- [0.989434, 0.941470, 0.697519],
- [0.989077, 0.948604, 0.704863],
- [0.988717, 0.955742, 0.712242],
- [0.988367, 0.962878, 0.719649],
- [0.988033, 0.970012, 0.727077],
- [0.987691, 0.977154, 0.734536],
- [0.987387, 0.984288, 0.742002],
- [0.987053, 0.991438, 0.749504]]
+_magma_data = [
+ [0.001462, 0.000466, 0.013866],
+ [0.002258, 0.001295, 0.018331],
+ [0.003279, 0.002305, 0.023708],
+ [0.004512, 0.003490, 0.029965],
+ [0.005950, 0.004843, 0.037130],
+ [0.007588, 0.006356, 0.044973],
+ [0.009426, 0.008022, 0.052844],
+ [0.011465, 0.009828, 0.060750],
+ [0.013708, 0.011771, 0.068667],
+ [0.016156, 0.013840, 0.076603],
+ [0.018815, 0.016026, 0.084584],
+ [0.021692, 0.018320, 0.092610],
+ [0.024792, 0.020715, 0.100676],
+ [0.028123, 0.023201, 0.108787],
+ [0.031696, 0.025765, 0.116965],
+ [0.035520, 0.028397, 0.125209],
+ [0.039608, 0.031090, 0.133515],
+ [0.043830, 0.033830, 0.141886],
+ [0.048062, 0.036607, 0.150327],
+ [0.052320, 0.039407, 0.158841],
+ [0.056615, 0.042160, 0.167446],
+ [0.060949, 0.044794, 0.176129],
+ [0.065330, 0.047318, 0.184892],
+ [0.069764, 0.049726, 0.193735],
+ [0.074257, 0.052017, 0.202660],
+ [0.078815, 0.054184, 0.211667],
+ [0.083446, 0.056225, 0.220755],
+ [0.088155, 0.058133, 0.229922],
+ [0.092949, 0.059904, 0.239164],
+ [0.097833, 0.061531, 0.248477],
+ [0.102815, 0.063010, 0.257854],
+ [0.107899, 0.064335, 0.267289],
+ [0.113094, 0.065492, 0.276784],
+ [0.118405, 0.066479, 0.286321],
+ [0.123833, 0.067295, 0.295879],
+ [0.129380, 0.067935, 0.305443],
+ [0.135053, 0.068391, 0.315000],
+ [0.140858, 0.068654, 0.324538],
+ [0.146785, 0.068738, 0.334011],
+ [0.152839, 0.068637, 0.343404],
+ [0.159018, 0.068354, 0.352688],
+ [0.165308, 0.067911, 0.361816],
+ [0.171713, 0.067305, 0.370771],
+ [0.178212, 0.066576, 0.379497],
+ [0.184801, 0.065732, 0.387973],
+ [0.191460, 0.064818, 0.396152],
+ [0.198177, 0.063862, 0.404009],
+ [0.204935, 0.062907, 0.411514],
+ [0.211718, 0.061992, 0.418647],
+ [0.218512, 0.061158, 0.425392],
+ [0.225302, 0.060445, 0.431742],
+ [0.232077, 0.059889, 0.437695],
+ [0.238826, 0.059517, 0.443256],
+ [0.245543, 0.059352, 0.448436],
+ [0.252220, 0.059415, 0.453248],
+ [0.258857, 0.059706, 0.457710],
+ [0.265447, 0.060237, 0.461840],
+ [0.271994, 0.060994, 0.465660],
+ [0.278493, 0.061978, 0.469190],
+ [0.284951, 0.063168, 0.472451],
+ [0.291366, 0.064553, 0.475462],
+ [0.297740, 0.066117, 0.478243],
+ [0.304081, 0.067835, 0.480812],
+ [0.310382, 0.069702, 0.483186],
+ [0.316654, 0.071690, 0.485380],
+ [0.322899, 0.073782, 0.487408],
+ [0.329114, 0.075972, 0.489287],
+ [0.335308, 0.078236, 0.491024],
+ [0.341482, 0.080564, 0.492631],
+ [0.347636, 0.082946, 0.494121],
+ [0.353773, 0.085373, 0.495501],
+ [0.359898, 0.087831, 0.496778],
+ [0.366012, 0.090314, 0.497960],
+ [0.372116, 0.092816, 0.499053],
+ [0.378211, 0.095332, 0.500067],
+ [0.384299, 0.097855, 0.501002],
+ [0.390384, 0.100379, 0.501864],
+ [0.396467, 0.102902, 0.502658],
+ [0.402548, 0.105420, 0.503386],
+ [0.408629, 0.107930, 0.504052],
+ [0.414709, 0.110431, 0.504662],
+ [0.420791, 0.112920, 0.505215],
+ [0.426877, 0.115395, 0.505714],
+ [0.432967, 0.117855, 0.506160],
+ [0.439062, 0.120298, 0.506555],
+ [0.445163, 0.122724, 0.506901],
+ [0.451271, 0.125132, 0.507198],
+ [0.457386, 0.127522, 0.507448],
+ [0.463508, 0.129893, 0.507652],
+ [0.469640, 0.132245, 0.507809],
+ [0.475780, 0.134577, 0.507921],
+ [0.481929, 0.136891, 0.507989],
+ [0.488088, 0.139186, 0.508011],
+ [0.494258, 0.141462, 0.507988],
+ [0.500438, 0.143719, 0.507920],
+ [0.506629, 0.145958, 0.507806],
+ [0.512831, 0.148179, 0.507648],
+ [0.519045, 0.150383, 0.507443],
+ [0.525270, 0.152569, 0.507192],
+ [0.531507, 0.154739, 0.506895],
+ [0.537755, 0.156894, 0.506551],
+ [0.544015, 0.159033, 0.506159],
+ [0.550287, 0.161158, 0.505719],
+ [0.556571, 0.163269, 0.505230],
+ [0.562866, 0.165368, 0.504692],
+ [0.569172, 0.167454, 0.504105],
+ [0.575490, 0.169530, 0.503466],
+ [0.581819, 0.171596, 0.502777],
+ [0.588158, 0.173652, 0.502035],
+ [0.594508, 0.175701, 0.501241],
+ [0.600868, 0.177743, 0.500394],
+ [0.607238, 0.179779, 0.499492],
+ [0.613617, 0.181811, 0.498536],
+ [0.620005, 0.183840, 0.497524],
+ [0.626401, 0.185867, 0.496456],
+ [0.632805, 0.187893, 0.495332],
+ [0.639216, 0.189921, 0.494150],
+ [0.645633, 0.191952, 0.492910],
+ [0.652056, 0.193986, 0.491611],
+ [0.658483, 0.196027, 0.490253],
+ [0.664915, 0.198075, 0.488836],
+ [0.671349, 0.200133, 0.487358],
+ [0.677786, 0.202203, 0.485819],
+ [0.684224, 0.204286, 0.484219],
+ [0.690661, 0.206384, 0.482558],
+ [0.697098, 0.208501, 0.480835],
+ [0.703532, 0.210638, 0.479049],
+ [0.709962, 0.212797, 0.477201],
+ [0.716387, 0.214982, 0.475290],
+ [0.722805, 0.217194, 0.473316],
+ [0.729216, 0.219437, 0.471279],
+ [0.735616, 0.221713, 0.469180],
+ [0.742004, 0.224025, 0.467018],
+ [0.748378, 0.226377, 0.464794],
+ [0.754737, 0.228772, 0.462509],
+ [0.761077, 0.231214, 0.460162],
+ [0.767398, 0.233705, 0.457755],
+ [0.773695, 0.236249, 0.455289],
+ [0.779968, 0.238851, 0.452765],
+ [0.786212, 0.241514, 0.450184],
+ [0.792427, 0.244242, 0.447543],
+ [0.798608, 0.247040, 0.444848],
+ [0.804752, 0.249911, 0.442102],
+ [0.810855, 0.252861, 0.439305],
+ [0.816914, 0.255895, 0.436461],
+ [0.822926, 0.259016, 0.433573],
+ [0.828886, 0.262229, 0.430644],
+ [0.834791, 0.265540, 0.427671],
+ [0.840636, 0.268953, 0.424666],
+ [0.846416, 0.272473, 0.421631],
+ [0.852126, 0.276106, 0.418573],
+ [0.857763, 0.279857, 0.415496],
+ [0.863320, 0.283729, 0.412403],
+ [0.868793, 0.287728, 0.409303],
+ [0.874176, 0.291859, 0.406205],
+ [0.879464, 0.296125, 0.403118],
+ [0.884651, 0.300530, 0.400047],
+ [0.889731, 0.305079, 0.397002],
+ [0.894700, 0.309773, 0.393995],
+ [0.899552, 0.314616, 0.391037],
+ [0.904281, 0.319610, 0.388137],
+ [0.908884, 0.324755, 0.385308],
+ [0.913354, 0.330052, 0.382563],
+ [0.917689, 0.335500, 0.379915],
+ [0.921884, 0.341098, 0.377376],
+ [0.925937, 0.346844, 0.374959],
+ [0.929845, 0.352734, 0.372677],
+ [0.933606, 0.358764, 0.370541],
+ [0.937221, 0.364929, 0.368567],
+ [0.940687, 0.371224, 0.366762],
+ [0.944006, 0.377643, 0.365136],
+ [0.947180, 0.384178, 0.363701],
+ [0.950210, 0.390820, 0.362468],
+ [0.953099, 0.397563, 0.361438],
+ [0.955849, 0.404400, 0.360619],
+ [0.958464, 0.411324, 0.360014],
+ [0.960949, 0.418323, 0.359630],
+ [0.963310, 0.425390, 0.359469],
+ [0.965549, 0.432519, 0.359529],
+ [0.967671, 0.439703, 0.359810],
+ [0.969680, 0.446936, 0.360311],
+ [0.971582, 0.454210, 0.361030],
+ [0.973381, 0.461520, 0.361965],
+ [0.975082, 0.468861, 0.363111],
+ [0.976690, 0.476226, 0.364466],
+ [0.978210, 0.483612, 0.366025],
+ [0.979645, 0.491014, 0.367783],
+ [0.981000, 0.498428, 0.369734],
+ [0.982279, 0.505851, 0.371874],
+ [0.983485, 0.513280, 0.374198],
+ [0.984622, 0.520713, 0.376698],
+ [0.985693, 0.528148, 0.379371],
+ [0.986700, 0.535582, 0.382210],
+ [0.987646, 0.543015, 0.385210],
+ [0.988533, 0.550446, 0.388365],
+ [0.989363, 0.557873, 0.391671],
+ [0.990138, 0.565296, 0.395122],
+ [0.990871, 0.572706, 0.398714],
+ [0.991558, 0.580107, 0.402441],
+ [0.992196, 0.587502, 0.406299],
+ [0.992785, 0.594891, 0.410283],
+ [0.993326, 0.602275, 0.414390],
+ [0.993834, 0.609644, 0.418613],
+ [0.994309, 0.616999, 0.422950],
+ [0.994738, 0.624350, 0.427397],
+ [0.995122, 0.631696, 0.431951],
+ [0.995480, 0.639027, 0.436607],
+ [0.995810, 0.646344, 0.441361],
+ [0.996096, 0.653659, 0.446213],
+ [0.996341, 0.660969, 0.451160],
+ [0.996580, 0.668256, 0.456192],
+ [0.996775, 0.675541, 0.461314],
+ [0.996925, 0.682828, 0.466526],
+ [0.997077, 0.690088, 0.471811],
+ [0.997186, 0.697349, 0.477182],
+ [0.997254, 0.704611, 0.482635],
+ [0.997325, 0.711848, 0.488154],
+ [0.997351, 0.719089, 0.493755],
+ [0.997351, 0.726324, 0.499428],
+ [0.997341, 0.733545, 0.505167],
+ [0.997285, 0.740772, 0.510983],
+ [0.997228, 0.747981, 0.516859],
+ [0.997138, 0.755190, 0.522806],
+ [0.997019, 0.762398, 0.528821],
+ [0.996898, 0.769591, 0.534892],
+ [0.996727, 0.776795, 0.541039],
+ [0.996571, 0.783977, 0.547233],
+ [0.996369, 0.791167, 0.553499],
+ [0.996162, 0.798348, 0.559820],
+ [0.995932, 0.805527, 0.566202],
+ [0.995680, 0.812706, 0.572645],
+ [0.995424, 0.819875, 0.579140],
+ [0.995131, 0.827052, 0.585701],
+ [0.994851, 0.834213, 0.592307],
+ [0.994524, 0.841387, 0.598983],
+ [0.994222, 0.848540, 0.605696],
+ [0.993866, 0.855711, 0.612482],
+ [0.993545, 0.862859, 0.619299],
+ [0.993170, 0.870024, 0.626189],
+ [0.992831, 0.877168, 0.633109],
+ [0.992440, 0.884330, 0.640099],
+ [0.992089, 0.891470, 0.647116],
+ [0.991688, 0.898627, 0.654202],
+ [0.991332, 0.905763, 0.661309],
+ [0.990930, 0.912915, 0.668481],
+ [0.990570, 0.920049, 0.675675],
+ [0.990175, 0.927196, 0.682926],
+ [0.989815, 0.934329, 0.690198],
+ [0.989434, 0.941470, 0.697519],
+ [0.989077, 0.948604, 0.704863],
+ [0.988717, 0.955742, 0.712242],
+ [0.988367, 0.962878, 0.719649],
+ [0.988033, 0.970012, 0.727077],
+ [0.987691, 0.977154, 0.734536],
+ [0.987387, 0.984288, 0.742002],
+ [0.987053, 0.991438, 0.749504],
+]
-_inferno_data = [[0.001462, 0.000466, 0.013866],
- [0.002267, 0.001270, 0.018570],
- [0.003299, 0.002249, 0.024239],
- [0.004547, 0.003392, 0.030909],
- [0.006006, 0.004692, 0.038558],
- [0.007676, 0.006136, 0.046836],
- [0.009561, 0.007713, 0.055143],
- [0.011663, 0.009417, 0.063460],
- [0.013995, 0.011225, 0.071862],
- [0.016561, 0.013136, 0.080282],
- [0.019373, 0.015133, 0.088767],
- [0.022447, 0.017199, 0.097327],
- [0.025793, 0.019331, 0.105930],
- [0.029432, 0.021503, 0.114621],
- [0.033385, 0.023702, 0.123397],
- [0.037668, 0.025921, 0.132232],
- [0.042253, 0.028139, 0.141141],
- [0.046915, 0.030324, 0.150164],
- [0.051644, 0.032474, 0.159254],
- [0.056449, 0.034569, 0.168414],
- [0.061340, 0.036590, 0.177642],
- [0.066331, 0.038504, 0.186962],
- [0.071429, 0.040294, 0.196354],
- [0.076637, 0.041905, 0.205799],
- [0.081962, 0.043328, 0.215289],
- [0.087411, 0.044556, 0.224813],
- [0.092990, 0.045583, 0.234358],
- [0.098702, 0.046402, 0.243904],
- [0.104551, 0.047008, 0.253430],
- [0.110536, 0.047399, 0.262912],
- [0.116656, 0.047574, 0.272321],
- [0.122908, 0.047536, 0.281624],
- [0.129285, 0.047293, 0.290788],
- [0.135778, 0.046856, 0.299776],
- [0.142378, 0.046242, 0.308553],
- [0.149073, 0.045468, 0.317085],
- [0.155850, 0.044559, 0.325338],
- [0.162689, 0.043554, 0.333277],
- [0.169575, 0.042489, 0.340874],
- [0.176493, 0.041402, 0.348111],
- [0.183429, 0.040329, 0.354971],
- [0.190367, 0.039309, 0.361447],
- [0.197297, 0.038400, 0.367535],
- [0.204209, 0.037632, 0.373238],
- [0.211095, 0.037030, 0.378563],
- [0.217949, 0.036615, 0.383522],
- [0.224763, 0.036405, 0.388129],
- [0.231538, 0.036405, 0.392400],
- [0.238273, 0.036621, 0.396353],
- [0.244967, 0.037055, 0.400007],
- [0.251620, 0.037705, 0.403378],
- [0.258234, 0.038571, 0.406485],
- [0.264810, 0.039647, 0.409345],
- [0.271347, 0.040922, 0.411976],
- [0.277850, 0.042353, 0.414392],
- [0.284321, 0.043933, 0.416608],
- [0.290763, 0.045644, 0.418637],
- [0.297178, 0.047470, 0.420491],
- [0.303568, 0.049396, 0.422182],
- [0.309935, 0.051407, 0.423721],
- [0.316282, 0.053490, 0.425116],
- [0.322610, 0.055634, 0.426377],
- [0.328921, 0.057827, 0.427511],
- [0.335217, 0.060060, 0.428524],
- [0.341500, 0.062325, 0.429425],
- [0.347771, 0.064616, 0.430217],
- [0.354032, 0.066925, 0.430906],
- [0.360284, 0.069247, 0.431497],
- [0.366529, 0.071579, 0.431994],
- [0.372768, 0.073915, 0.432400],
- [0.379001, 0.076253, 0.432719],
- [0.385228, 0.078591, 0.432955],
- [0.391453, 0.080927, 0.433109],
- [0.397674, 0.083257, 0.433183],
- [0.403894, 0.085580, 0.433179],
- [0.410113, 0.087896, 0.433098],
- [0.416331, 0.090203, 0.432943],
- [0.422549, 0.092501, 0.432714],
- [0.428768, 0.094790, 0.432412],
- [0.434987, 0.097069, 0.432039],
- [0.441207, 0.099338, 0.431594],
- [0.447428, 0.101597, 0.431080],
- [0.453651, 0.103848, 0.430498],
- [0.459875, 0.106089, 0.429846],
- [0.466100, 0.108322, 0.429125],
- [0.472328, 0.110547, 0.428334],
- [0.478558, 0.112764, 0.427475],
- [0.484789, 0.114974, 0.426548],
- [0.491022, 0.117179, 0.425552],
- [0.497257, 0.119379, 0.424488],
- [0.503493, 0.121575, 0.423356],
- [0.509730, 0.123769, 0.422156],
- [0.515967, 0.125960, 0.420887],
- [0.522206, 0.128150, 0.419549],
- [0.528444, 0.130341, 0.418142],
- [0.534683, 0.132534, 0.416667],
- [0.540920, 0.134729, 0.415123],
- [0.547157, 0.136929, 0.413511],
- [0.553392, 0.139134, 0.411829],
- [0.559624, 0.141346, 0.410078],
- [0.565854, 0.143567, 0.408258],
- [0.572081, 0.145797, 0.406369],
- [0.578304, 0.148039, 0.404411],
- [0.584521, 0.150294, 0.402385],
- [0.590734, 0.152563, 0.400290],
- [0.596940, 0.154848, 0.398125],
- [0.603139, 0.157151, 0.395891],
- [0.609330, 0.159474, 0.393589],
- [0.615513, 0.161817, 0.391219],
- [0.621685, 0.164184, 0.388781],
- [0.627847, 0.166575, 0.386276],
- [0.633998, 0.168992, 0.383704],
- [0.640135, 0.171438, 0.381065],
- [0.646260, 0.173914, 0.378359],
- [0.652369, 0.176421, 0.375586],
- [0.658463, 0.178962, 0.372748],
- [0.664540, 0.181539, 0.369846],
- [0.670599, 0.184153, 0.366879],
- [0.676638, 0.186807, 0.363849],
- [0.682656, 0.189501, 0.360757],
- [0.688653, 0.192239, 0.357603],
- [0.694627, 0.195021, 0.354388],
- [0.700576, 0.197851, 0.351113],
- [0.706500, 0.200728, 0.347777],
- [0.712396, 0.203656, 0.344383],
- [0.718264, 0.206636, 0.340931],
- [0.724103, 0.209670, 0.337424],
- [0.729909, 0.212759, 0.333861],
- [0.735683, 0.215906, 0.330245],
- [0.741423, 0.219112, 0.326576],
- [0.747127, 0.222378, 0.322856],
- [0.752794, 0.225706, 0.319085],
- [0.758422, 0.229097, 0.315266],
- [0.764010, 0.232554, 0.311399],
- [0.769556, 0.236077, 0.307485],
- [0.775059, 0.239667, 0.303526],
- [0.780517, 0.243327, 0.299523],
- [0.785929, 0.247056, 0.295477],
- [0.791293, 0.250856, 0.291390],
- [0.796607, 0.254728, 0.287264],
- [0.801871, 0.258674, 0.283099],
- [0.807082, 0.262692, 0.278898],
- [0.812239, 0.266786, 0.274661],
- [0.817341, 0.270954, 0.270390],
- [0.822386, 0.275197, 0.266085],
- [0.827372, 0.279517, 0.261750],
- [0.832299, 0.283913, 0.257383],
- [0.837165, 0.288385, 0.252988],
- [0.841969, 0.292933, 0.248564],
- [0.846709, 0.297559, 0.244113],
- [0.851384, 0.302260, 0.239636],
- [0.855992, 0.307038, 0.235133],
- [0.860533, 0.311892, 0.230606],
- [0.865006, 0.316822, 0.226055],
- [0.869409, 0.321827, 0.221482],
- [0.873741, 0.326906, 0.216886],
- [0.878001, 0.332060, 0.212268],
- [0.882188, 0.337287, 0.207628],
- [0.886302, 0.342586, 0.202968],
- [0.890341, 0.347957, 0.198286],
- [0.894305, 0.353399, 0.193584],
- [0.898192, 0.358911, 0.188860],
- [0.902003, 0.364492, 0.184116],
- [0.905735, 0.370140, 0.179350],
- [0.909390, 0.375856, 0.174563],
- [0.912966, 0.381636, 0.169755],
- [0.916462, 0.387481, 0.164924],
- [0.919879, 0.393389, 0.160070],
- [0.923215, 0.399359, 0.155193],
- [0.926470, 0.405389, 0.150292],
- [0.929644, 0.411479, 0.145367],
- [0.932737, 0.417627, 0.140417],
- [0.935747, 0.423831, 0.135440],
- [0.938675, 0.430091, 0.130438],
- [0.941521, 0.436405, 0.125409],
- [0.944285, 0.442772, 0.120354],
- [0.946965, 0.449191, 0.115272],
- [0.949562, 0.455660, 0.110164],
- [0.952075, 0.462178, 0.105031],
- [0.954506, 0.468744, 0.099874],
- [0.956852, 0.475356, 0.094695],
- [0.959114, 0.482014, 0.089499],
- [0.961293, 0.488716, 0.084289],
- [0.963387, 0.495462, 0.079073],
- [0.965397, 0.502249, 0.073859],
- [0.967322, 0.509078, 0.068659],
- [0.969163, 0.515946, 0.063488],
- [0.970919, 0.522853, 0.058367],
- [0.972590, 0.529798, 0.053324],
- [0.974176, 0.536780, 0.048392],
- [0.975677, 0.543798, 0.043618],
- [0.977092, 0.550850, 0.039050],
- [0.978422, 0.557937, 0.034931],
- [0.979666, 0.565057, 0.031409],
- [0.980824, 0.572209, 0.028508],
- [0.981895, 0.579392, 0.026250],
- [0.982881, 0.586606, 0.024661],
- [0.983779, 0.593849, 0.023770],
- [0.984591, 0.601122, 0.023606],
- [0.985315, 0.608422, 0.024202],
- [0.985952, 0.615750, 0.025592],
- [0.986502, 0.623105, 0.027814],
- [0.986964, 0.630485, 0.030908],
- [0.987337, 0.637890, 0.034916],
- [0.987622, 0.645320, 0.039886],
- [0.987819, 0.652773, 0.045581],
- [0.987926, 0.660250, 0.051750],
- [0.987945, 0.667748, 0.058329],
- [0.987874, 0.675267, 0.065257],
- [0.987714, 0.682807, 0.072489],
- [0.987464, 0.690366, 0.079990],
- [0.987124, 0.697944, 0.087731],
- [0.986694, 0.705540, 0.095694],
- [0.986175, 0.713153, 0.103863],
- [0.985566, 0.720782, 0.112229],
- [0.984865, 0.728427, 0.120785],
- [0.984075, 0.736087, 0.129527],
- [0.983196, 0.743758, 0.138453],
- [0.982228, 0.751442, 0.147565],
- [0.981173, 0.759135, 0.156863],
- [0.980032, 0.766837, 0.166353],
- [0.978806, 0.774545, 0.176037],
- [0.977497, 0.782258, 0.185923],
- [0.976108, 0.789974, 0.196018],
- [0.974638, 0.797692, 0.206332],
- [0.973088, 0.805409, 0.216877],
- [0.971468, 0.813122, 0.227658],
- [0.969783, 0.820825, 0.238686],
- [0.968041, 0.828515, 0.249972],
- [0.966243, 0.836191, 0.261534],
- [0.964394, 0.843848, 0.273391],
- [0.962517, 0.851476, 0.285546],
- [0.960626, 0.859069, 0.298010],
- [0.958720, 0.866624, 0.310820],
- [0.956834, 0.874129, 0.323974],
- [0.954997, 0.881569, 0.337475],
- [0.953215, 0.888942, 0.351369],
- [0.951546, 0.896226, 0.365627],
- [0.950018, 0.903409, 0.380271],
- [0.948683, 0.910473, 0.395289],
- [0.947594, 0.917399, 0.410665],
- [0.946809, 0.924168, 0.426373],
- [0.946392, 0.930761, 0.442367],
- [0.946403, 0.937159, 0.458592],
- [0.946903, 0.943348, 0.474970],
- [0.947937, 0.949318, 0.491426],
- [0.949545, 0.955063, 0.507860],
- [0.951740, 0.960587, 0.524203],
- [0.954529, 0.965896, 0.540361],
- [0.957896, 0.971003, 0.556275],
- [0.961812, 0.975924, 0.571925],
- [0.966249, 0.980678, 0.587206],
- [0.971162, 0.985282, 0.602154],
- [0.976511, 0.989753, 0.616760],
- [0.982257, 0.994109, 0.631017],
- [0.988362, 0.998364, 0.644924]]
+_inferno_data = [
+ [0.001462, 0.000466, 0.013866],
+ [0.002267, 0.001270, 0.018570],
+ [0.003299, 0.002249, 0.024239],
+ [0.004547, 0.003392, 0.030909],
+ [0.006006, 0.004692, 0.038558],
+ [0.007676, 0.006136, 0.046836],
+ [0.009561, 0.007713, 0.055143],
+ [0.011663, 0.009417, 0.063460],
+ [0.013995, 0.011225, 0.071862],
+ [0.016561, 0.013136, 0.080282],
+ [0.019373, 0.015133, 0.088767],
+ [0.022447, 0.017199, 0.097327],
+ [0.025793, 0.019331, 0.105930],
+ [0.029432, 0.021503, 0.114621],
+ [0.033385, 0.023702, 0.123397],
+ [0.037668, 0.025921, 0.132232],
+ [0.042253, 0.028139, 0.141141],
+ [0.046915, 0.030324, 0.150164],
+ [0.051644, 0.032474, 0.159254],
+ [0.056449, 0.034569, 0.168414],
+ [0.061340, 0.036590, 0.177642],
+ [0.066331, 0.038504, 0.186962],
+ [0.071429, 0.040294, 0.196354],
+ [0.076637, 0.041905, 0.205799],
+ [0.081962, 0.043328, 0.215289],
+ [0.087411, 0.044556, 0.224813],
+ [0.092990, 0.045583, 0.234358],
+ [0.098702, 0.046402, 0.243904],
+ [0.104551, 0.047008, 0.253430],
+ [0.110536, 0.047399, 0.262912],
+ [0.116656, 0.047574, 0.272321],
+ [0.122908, 0.047536, 0.281624],
+ [0.129285, 0.047293, 0.290788],
+ [0.135778, 0.046856, 0.299776],
+ [0.142378, 0.046242, 0.308553],
+ [0.149073, 0.045468, 0.317085],
+ [0.155850, 0.044559, 0.325338],
+ [0.162689, 0.043554, 0.333277],
+ [0.169575, 0.042489, 0.340874],
+ [0.176493, 0.041402, 0.348111],
+ [0.183429, 0.040329, 0.354971],
+ [0.190367, 0.039309, 0.361447],
+ [0.197297, 0.038400, 0.367535],
+ [0.204209, 0.037632, 0.373238],
+ [0.211095, 0.037030, 0.378563],
+ [0.217949, 0.036615, 0.383522],
+ [0.224763, 0.036405, 0.388129],
+ [0.231538, 0.036405, 0.392400],
+ [0.238273, 0.036621, 0.396353],
+ [0.244967, 0.037055, 0.400007],
+ [0.251620, 0.037705, 0.403378],
+ [0.258234, 0.038571, 0.406485],
+ [0.264810, 0.039647, 0.409345],
+ [0.271347, 0.040922, 0.411976],
+ [0.277850, 0.042353, 0.414392],
+ [0.284321, 0.043933, 0.416608],
+ [0.290763, 0.045644, 0.418637],
+ [0.297178, 0.047470, 0.420491],
+ [0.303568, 0.049396, 0.422182],
+ [0.309935, 0.051407, 0.423721],
+ [0.316282, 0.053490, 0.425116],
+ [0.322610, 0.055634, 0.426377],
+ [0.328921, 0.057827, 0.427511],
+ [0.335217, 0.060060, 0.428524],
+ [0.341500, 0.062325, 0.429425],
+ [0.347771, 0.064616, 0.430217],
+ [0.354032, 0.066925, 0.430906],
+ [0.360284, 0.069247, 0.431497],
+ [0.366529, 0.071579, 0.431994],
+ [0.372768, 0.073915, 0.432400],
+ [0.379001, 0.076253, 0.432719],
+ [0.385228, 0.078591, 0.432955],
+ [0.391453, 0.080927, 0.433109],
+ [0.397674, 0.083257, 0.433183],
+ [0.403894, 0.085580, 0.433179],
+ [0.410113, 0.087896, 0.433098],
+ [0.416331, 0.090203, 0.432943],
+ [0.422549, 0.092501, 0.432714],
+ [0.428768, 0.094790, 0.432412],
+ [0.434987, 0.097069, 0.432039],
+ [0.441207, 0.099338, 0.431594],
+ [0.447428, 0.101597, 0.431080],
+ [0.453651, 0.103848, 0.430498],
+ [0.459875, 0.106089, 0.429846],
+ [0.466100, 0.108322, 0.429125],
+ [0.472328, 0.110547, 0.428334],
+ [0.478558, 0.112764, 0.427475],
+ [0.484789, 0.114974, 0.426548],
+ [0.491022, 0.117179, 0.425552],
+ [0.497257, 0.119379, 0.424488],
+ [0.503493, 0.121575, 0.423356],
+ [0.509730, 0.123769, 0.422156],
+ [0.515967, 0.125960, 0.420887],
+ [0.522206, 0.128150, 0.419549],
+ [0.528444, 0.130341, 0.418142],
+ [0.534683, 0.132534, 0.416667],
+ [0.540920, 0.134729, 0.415123],
+ [0.547157, 0.136929, 0.413511],
+ [0.553392, 0.139134, 0.411829],
+ [0.559624, 0.141346, 0.410078],
+ [0.565854, 0.143567, 0.408258],
+ [0.572081, 0.145797, 0.406369],
+ [0.578304, 0.148039, 0.404411],
+ [0.584521, 0.150294, 0.402385],
+ [0.590734, 0.152563, 0.400290],
+ [0.596940, 0.154848, 0.398125],
+ [0.603139, 0.157151, 0.395891],
+ [0.609330, 0.159474, 0.393589],
+ [0.615513, 0.161817, 0.391219],
+ [0.621685, 0.164184, 0.388781],
+ [0.627847, 0.166575, 0.386276],
+ [0.633998, 0.168992, 0.383704],
+ [0.640135, 0.171438, 0.381065],
+ [0.646260, 0.173914, 0.378359],
+ [0.652369, 0.176421, 0.375586],
+ [0.658463, 0.178962, 0.372748],
+ [0.664540, 0.181539, 0.369846],
+ [0.670599, 0.184153, 0.366879],
+ [0.676638, 0.186807, 0.363849],
+ [0.682656, 0.189501, 0.360757],
+ [0.688653, 0.192239, 0.357603],
+ [0.694627, 0.195021, 0.354388],
+ [0.700576, 0.197851, 0.351113],
+ [0.706500, 0.200728, 0.347777],
+ [0.712396, 0.203656, 0.344383],
+ [0.718264, 0.206636, 0.340931],
+ [0.724103, 0.209670, 0.337424],
+ [0.729909, 0.212759, 0.333861],
+ [0.735683, 0.215906, 0.330245],
+ [0.741423, 0.219112, 0.326576],
+ [0.747127, 0.222378, 0.322856],
+ [0.752794, 0.225706, 0.319085],
+ [0.758422, 0.229097, 0.315266],
+ [0.764010, 0.232554, 0.311399],
+ [0.769556, 0.236077, 0.307485],
+ [0.775059, 0.239667, 0.303526],
+ [0.780517, 0.243327, 0.299523],
+ [0.785929, 0.247056, 0.295477],
+ [0.791293, 0.250856, 0.291390],
+ [0.796607, 0.254728, 0.287264],
+ [0.801871, 0.258674, 0.283099],
+ [0.807082, 0.262692, 0.278898],
+ [0.812239, 0.266786, 0.274661],
+ [0.817341, 0.270954, 0.270390],
+ [0.822386, 0.275197, 0.266085],
+ [0.827372, 0.279517, 0.261750],
+ [0.832299, 0.283913, 0.257383],
+ [0.837165, 0.288385, 0.252988],
+ [0.841969, 0.292933, 0.248564],
+ [0.846709, 0.297559, 0.244113],
+ [0.851384, 0.302260, 0.239636],
+ [0.855992, 0.307038, 0.235133],
+ [0.860533, 0.311892, 0.230606],
+ [0.865006, 0.316822, 0.226055],
+ [0.869409, 0.321827, 0.221482],
+ [0.873741, 0.326906, 0.216886],
+ [0.878001, 0.332060, 0.212268],
+ [0.882188, 0.337287, 0.207628],
+ [0.886302, 0.342586, 0.202968],
+ [0.890341, 0.347957, 0.198286],
+ [0.894305, 0.353399, 0.193584],
+ [0.898192, 0.358911, 0.188860],
+ [0.902003, 0.364492, 0.184116],
+ [0.905735, 0.370140, 0.179350],
+ [0.909390, 0.375856, 0.174563],
+ [0.912966, 0.381636, 0.169755],
+ [0.916462, 0.387481, 0.164924],
+ [0.919879, 0.393389, 0.160070],
+ [0.923215, 0.399359, 0.155193],
+ [0.926470, 0.405389, 0.150292],
+ [0.929644, 0.411479, 0.145367],
+ [0.932737, 0.417627, 0.140417],
+ [0.935747, 0.423831, 0.135440],
+ [0.938675, 0.430091, 0.130438],
+ [0.941521, 0.436405, 0.125409],
+ [0.944285, 0.442772, 0.120354],
+ [0.946965, 0.449191, 0.115272],
+ [0.949562, 0.455660, 0.110164],
+ [0.952075, 0.462178, 0.105031],
+ [0.954506, 0.468744, 0.099874],
+ [0.956852, 0.475356, 0.094695],
+ [0.959114, 0.482014, 0.089499],
+ [0.961293, 0.488716, 0.084289],
+ [0.963387, 0.495462, 0.079073],
+ [0.965397, 0.502249, 0.073859],
+ [0.967322, 0.509078, 0.068659],
+ [0.969163, 0.515946, 0.063488],
+ [0.970919, 0.522853, 0.058367],
+ [0.972590, 0.529798, 0.053324],
+ [0.974176, 0.536780, 0.048392],
+ [0.975677, 0.543798, 0.043618],
+ [0.977092, 0.550850, 0.039050],
+ [0.978422, 0.557937, 0.034931],
+ [0.979666, 0.565057, 0.031409],
+ [0.980824, 0.572209, 0.028508],
+ [0.981895, 0.579392, 0.026250],
+ [0.982881, 0.586606, 0.024661],
+ [0.983779, 0.593849, 0.023770],
+ [0.984591, 0.601122, 0.023606],
+ [0.985315, 0.608422, 0.024202],
+ [0.985952, 0.615750, 0.025592],
+ [0.986502, 0.623105, 0.027814],
+ [0.986964, 0.630485, 0.030908],
+ [0.987337, 0.637890, 0.034916],
+ [0.987622, 0.645320, 0.039886],
+ [0.987819, 0.652773, 0.045581],
+ [0.987926, 0.660250, 0.051750],
+ [0.987945, 0.667748, 0.058329],
+ [0.987874, 0.675267, 0.065257],
+ [0.987714, 0.682807, 0.072489],
+ [0.987464, 0.690366, 0.079990],
+ [0.987124, 0.697944, 0.087731],
+ [0.986694, 0.705540, 0.095694],
+ [0.986175, 0.713153, 0.103863],
+ [0.985566, 0.720782, 0.112229],
+ [0.984865, 0.728427, 0.120785],
+ [0.984075, 0.736087, 0.129527],
+ [0.983196, 0.743758, 0.138453],
+ [0.982228, 0.751442, 0.147565],
+ [0.981173, 0.759135, 0.156863],
+ [0.980032, 0.766837, 0.166353],
+ [0.978806, 0.774545, 0.176037],
+ [0.977497, 0.782258, 0.185923],
+ [0.976108, 0.789974, 0.196018],
+ [0.974638, 0.797692, 0.206332],
+ [0.973088, 0.805409, 0.216877],
+ [0.971468, 0.813122, 0.227658],
+ [0.969783, 0.820825, 0.238686],
+ [0.968041, 0.828515, 0.249972],
+ [0.966243, 0.836191, 0.261534],
+ [0.964394, 0.843848, 0.273391],
+ [0.962517, 0.851476, 0.285546],
+ [0.960626, 0.859069, 0.298010],
+ [0.958720, 0.866624, 0.310820],
+ [0.956834, 0.874129, 0.323974],
+ [0.954997, 0.881569, 0.337475],
+ [0.953215, 0.888942, 0.351369],
+ [0.951546, 0.896226, 0.365627],
+ [0.950018, 0.903409, 0.380271],
+ [0.948683, 0.910473, 0.395289],
+ [0.947594, 0.917399, 0.410665],
+ [0.946809, 0.924168, 0.426373],
+ [0.946392, 0.930761, 0.442367],
+ [0.946403, 0.937159, 0.458592],
+ [0.946903, 0.943348, 0.474970],
+ [0.947937, 0.949318, 0.491426],
+ [0.949545, 0.955063, 0.507860],
+ [0.951740, 0.960587, 0.524203],
+ [0.954529, 0.965896, 0.540361],
+ [0.957896, 0.971003, 0.556275],
+ [0.961812, 0.975924, 0.571925],
+ [0.966249, 0.980678, 0.587206],
+ [0.971162, 0.985282, 0.602154],
+ [0.976511, 0.989753, 0.616760],
+ [0.982257, 0.994109, 0.631017],
+ [0.988362, 0.998364, 0.644924],
+]
-_plasma_data = [[0.050383, 0.029803, 0.527975],
- [0.063536, 0.028426, 0.533124],
- [0.075353, 0.027206, 0.538007],
- [0.086222, 0.026125, 0.542658],
- [0.096379, 0.025165, 0.547103],
- [0.105980, 0.024309, 0.551368],
- [0.115124, 0.023556, 0.555468],
- [0.123903, 0.022878, 0.559423],
- [0.132381, 0.022258, 0.563250],
- [0.140603, 0.021687, 0.566959],
- [0.148607, 0.021154, 0.570562],
- [0.156421, 0.020651, 0.574065],
- [0.164070, 0.020171, 0.577478],
- [0.171574, 0.019706, 0.580806],
- [0.178950, 0.019252, 0.584054],
- [0.186213, 0.018803, 0.587228],
- [0.193374, 0.018354, 0.590330],
- [0.200445, 0.017902, 0.593364],
- [0.207435, 0.017442, 0.596333],
- [0.214350, 0.016973, 0.599239],
- [0.221197, 0.016497, 0.602083],
- [0.227983, 0.016007, 0.604867],
- [0.234715, 0.015502, 0.607592],
- [0.241396, 0.014979, 0.610259],
- [0.248032, 0.014439, 0.612868],
- [0.254627, 0.013882, 0.615419],
- [0.261183, 0.013308, 0.617911],
- [0.267703, 0.012716, 0.620346],
- [0.274191, 0.012109, 0.622722],
- [0.280648, 0.011488, 0.625038],
- [0.287076, 0.010855, 0.627295],
- [0.293478, 0.010213, 0.629490],
- [0.299855, 0.009561, 0.631624],
- [0.306210, 0.008902, 0.633694],
- [0.312543, 0.008239, 0.635700],
- [0.318856, 0.007576, 0.637640],
- [0.325150, 0.006915, 0.639512],
- [0.331426, 0.006261, 0.641316],
- [0.337683, 0.005618, 0.643049],
- [0.343925, 0.004991, 0.644710],
- [0.350150, 0.004382, 0.646298],
- [0.356359, 0.003798, 0.647810],
- [0.362553, 0.003243, 0.649245],
- [0.368733, 0.002724, 0.650601],
- [0.374897, 0.002245, 0.651876],
- [0.381047, 0.001814, 0.653068],
- [0.387183, 0.001434, 0.654177],
- [0.393304, 0.001114, 0.655199],
- [0.399411, 0.000859, 0.656133],
- [0.405503, 0.000678, 0.656977],
- [0.411580, 0.000577, 0.657730],
- [0.417642, 0.000564, 0.658390],
- [0.423689, 0.000646, 0.658956],
- [0.429719, 0.000831, 0.659425],
- [0.435734, 0.001127, 0.659797],
- [0.441732, 0.001540, 0.660069],
- [0.447714, 0.002080, 0.660240],
- [0.453677, 0.002755, 0.660310],
- [0.459623, 0.003574, 0.660277],
- [0.465550, 0.004545, 0.660139],
- [0.471457, 0.005678, 0.659897],
- [0.477344, 0.006980, 0.659549],
- [0.483210, 0.008460, 0.659095],
- [0.489055, 0.010127, 0.658534],
- [0.494877, 0.011990, 0.657865],
- [0.500678, 0.014055, 0.657088],
- [0.506454, 0.016333, 0.656202],
- [0.512206, 0.018833, 0.655209],
- [0.517933, 0.021563, 0.654109],
- [0.523633, 0.024532, 0.652901],
- [0.529306, 0.027747, 0.651586],
- [0.534952, 0.031217, 0.650165],
- [0.540570, 0.034950, 0.648640],
- [0.546157, 0.038954, 0.647010],
- [0.551715, 0.043136, 0.645277],
- [0.557243, 0.047331, 0.643443],
- [0.562738, 0.051545, 0.641509],
- [0.568201, 0.055778, 0.639477],
- [0.573632, 0.060028, 0.637349],
- [0.579029, 0.064296, 0.635126],
- [0.584391, 0.068579, 0.632812],
- [0.589719, 0.072878, 0.630408],
- [0.595011, 0.077190, 0.627917],
- [0.600266, 0.081516, 0.625342],
- [0.605485, 0.085854, 0.622686],
- [0.610667, 0.090204, 0.619951],
- [0.615812, 0.094564, 0.617140],
- [0.620919, 0.098934, 0.614257],
- [0.625987, 0.103312, 0.611305],
- [0.631017, 0.107699, 0.608287],
- [0.636008, 0.112092, 0.605205],
- [0.640959, 0.116492, 0.602065],
- [0.645872, 0.120898, 0.598867],
- [0.650746, 0.125309, 0.595617],
- [0.655580, 0.129725, 0.592317],
- [0.660374, 0.134144, 0.588971],
- [0.665129, 0.138566, 0.585582],
- [0.669845, 0.142992, 0.582154],
- [0.674522, 0.147419, 0.578688],
- [0.679160, 0.151848, 0.575189],
- [0.683758, 0.156278, 0.571660],
- [0.688318, 0.160709, 0.568103],
- [0.692840, 0.165141, 0.564522],
- [0.697324, 0.169573, 0.560919],
- [0.701769, 0.174005, 0.557296],
- [0.706178, 0.178437, 0.553657],
- [0.710549, 0.182868, 0.550004],
- [0.714883, 0.187299, 0.546338],
- [0.719181, 0.191729, 0.542663],
- [0.723444, 0.196158, 0.538981],
- [0.727670, 0.200586, 0.535293],
- [0.731862, 0.205013, 0.531601],
- [0.736019, 0.209439, 0.527908],
- [0.740143, 0.213864, 0.524216],
- [0.744232, 0.218288, 0.520524],
- [0.748289, 0.222711, 0.516834],
- [0.752312, 0.227133, 0.513149],
- [0.756304, 0.231555, 0.509468],
- [0.760264, 0.235976, 0.505794],
- [0.764193, 0.240396, 0.502126],
- [0.768090, 0.244817, 0.498465],
- [0.771958, 0.249237, 0.494813],
- [0.775796, 0.253658, 0.491171],
- [0.779604, 0.258078, 0.487539],
- [0.783383, 0.262500, 0.483918],
- [0.787133, 0.266922, 0.480307],
- [0.790855, 0.271345, 0.476706],
- [0.794549, 0.275770, 0.473117],
- [0.798216, 0.280197, 0.469538],
- [0.801855, 0.284626, 0.465971],
- [0.805467, 0.289057, 0.462415],
- [0.809052, 0.293491, 0.458870],
- [0.812612, 0.297928, 0.455338],
- [0.816144, 0.302368, 0.451816],
- [0.819651, 0.306812, 0.448306],
- [0.823132, 0.311261, 0.444806],
- [0.826588, 0.315714, 0.441316],
- [0.830018, 0.320172, 0.437836],
- [0.833422, 0.324635, 0.434366],
- [0.836801, 0.329105, 0.430905],
- [0.840155, 0.333580, 0.427455],
- [0.843484, 0.338062, 0.424013],
- [0.846788, 0.342551, 0.420579],
- [0.850066, 0.347048, 0.417153],
- [0.853319, 0.351553, 0.413734],
- [0.856547, 0.356066, 0.410322],
- [0.859750, 0.360588, 0.406917],
- [0.862927, 0.365119, 0.403519],
- [0.866078, 0.369660, 0.400126],
- [0.869203, 0.374212, 0.396738],
- [0.872303, 0.378774, 0.393355],
- [0.875376, 0.383347, 0.389976],
- [0.878423, 0.387932, 0.386600],
- [0.881443, 0.392529, 0.383229],
- [0.884436, 0.397139, 0.379860],
- [0.887402, 0.401762, 0.376494],
- [0.890340, 0.406398, 0.373130],
- [0.893250, 0.411048, 0.369768],
- [0.896131, 0.415712, 0.366407],
- [0.898984, 0.420392, 0.363047],
- [0.901807, 0.425087, 0.359688],
- [0.904601, 0.429797, 0.356329],
- [0.907365, 0.434524, 0.352970],
- [0.910098, 0.439268, 0.349610],
- [0.912800, 0.444029, 0.346251],
- [0.915471, 0.448807, 0.342890],
- [0.918109, 0.453603, 0.339529],
- [0.920714, 0.458417, 0.336166],
- [0.923287, 0.463251, 0.332801],
- [0.925825, 0.468103, 0.329435],
- [0.928329, 0.472975, 0.326067],
- [0.930798, 0.477867, 0.322697],
- [0.933232, 0.482780, 0.319325],
- [0.935630, 0.487712, 0.315952],
- [0.937990, 0.492667, 0.312575],
- [0.940313, 0.497642, 0.309197],
- [0.942598, 0.502639, 0.305816],
- [0.944844, 0.507658, 0.302433],
- [0.947051, 0.512699, 0.299049],
- [0.949217, 0.517763, 0.295662],
- [0.951344, 0.522850, 0.292275],
- [0.953428, 0.527960, 0.288883],
- [0.955470, 0.533093, 0.285490],
- [0.957469, 0.538250, 0.282096],
- [0.959424, 0.543431, 0.278701],
- [0.961336, 0.548636, 0.275305],
- [0.963203, 0.553865, 0.271909],
- [0.965024, 0.559118, 0.268513],
- [0.966798, 0.564396, 0.265118],
- [0.968526, 0.569700, 0.261721],
- [0.970205, 0.575028, 0.258325],
- [0.971835, 0.580382, 0.254931],
- [0.973416, 0.585761, 0.251540],
- [0.974947, 0.591165, 0.248151],
- [0.976428, 0.596595, 0.244767],
- [0.977856, 0.602051, 0.241387],
- [0.979233, 0.607532, 0.238013],
- [0.980556, 0.613039, 0.234646],
- [0.981826, 0.618572, 0.231287],
- [0.983041, 0.624131, 0.227937],
- [0.984199, 0.629718, 0.224595],
- [0.985301, 0.635330, 0.221265],
- [0.986345, 0.640969, 0.217948],
- [0.987332, 0.646633, 0.214648],
- [0.988260, 0.652325, 0.211364],
- [0.989128, 0.658043, 0.208100],
- [0.989935, 0.663787, 0.204859],
- [0.990681, 0.669558, 0.201642],
- [0.991365, 0.675355, 0.198453],
- [0.991985, 0.681179, 0.195295],
- [0.992541, 0.687030, 0.192170],
- [0.993032, 0.692907, 0.189084],
- [0.993456, 0.698810, 0.186041],
- [0.993814, 0.704741, 0.183043],
- [0.994103, 0.710698, 0.180097],
- [0.994324, 0.716681, 0.177208],
- [0.994474, 0.722691, 0.174381],
- [0.994553, 0.728728, 0.171622],
- [0.994561, 0.734791, 0.168938],
- [0.994495, 0.740880, 0.166335],
- [0.994355, 0.746995, 0.163821],
- [0.994141, 0.753137, 0.161404],
- [0.993851, 0.759304, 0.159092],
- [0.993482, 0.765499, 0.156891],
- [0.993033, 0.771720, 0.154808],
- [0.992505, 0.777967, 0.152855],
- [0.991897, 0.784239, 0.151042],
- [0.991209, 0.790537, 0.149377],
- [0.990439, 0.796859, 0.147870],
- [0.989587, 0.803205, 0.146529],
- [0.988648, 0.809579, 0.145357],
- [0.987621, 0.815978, 0.144363],
- [0.986509, 0.822401, 0.143557],
- [0.985314, 0.828846, 0.142945],
- [0.984031, 0.835315, 0.142528],
- [0.982653, 0.841812, 0.142303],
- [0.981190, 0.848329, 0.142279],
- [0.979644, 0.854866, 0.142453],
- [0.977995, 0.861432, 0.142808],
- [0.976265, 0.868016, 0.143351],
- [0.974443, 0.874622, 0.144061],
- [0.972530, 0.881250, 0.144923],
- [0.970533, 0.887896, 0.145919],
- [0.968443, 0.894564, 0.147014],
- [0.966271, 0.901249, 0.148180],
- [0.964021, 0.907950, 0.149370],
- [0.961681, 0.914672, 0.150520],
- [0.959276, 0.921407, 0.151566],
- [0.956808, 0.928152, 0.152409],
- [0.954287, 0.934908, 0.152921],
- [0.951726, 0.941671, 0.152925],
- [0.949151, 0.948435, 0.152178],
- [0.946602, 0.955190, 0.150328],
- [0.944152, 0.961916, 0.146861],
- [0.941896, 0.968590, 0.140956],
- [0.940015, 0.975158, 0.131326]]
+_plasma_data = [
+ [0.050383, 0.029803, 0.527975],
+ [0.063536, 0.028426, 0.533124],
+ [0.075353, 0.027206, 0.538007],
+ [0.086222, 0.026125, 0.542658],
+ [0.096379, 0.025165, 0.547103],
+ [0.105980, 0.024309, 0.551368],
+ [0.115124, 0.023556, 0.555468],
+ [0.123903, 0.022878, 0.559423],
+ [0.132381, 0.022258, 0.563250],
+ [0.140603, 0.021687, 0.566959],
+ [0.148607, 0.021154, 0.570562],
+ [0.156421, 0.020651, 0.574065],
+ [0.164070, 0.020171, 0.577478],
+ [0.171574, 0.019706, 0.580806],
+ [0.178950, 0.019252, 0.584054],
+ [0.186213, 0.018803, 0.587228],
+ [0.193374, 0.018354, 0.590330],
+ [0.200445, 0.017902, 0.593364],
+ [0.207435, 0.017442, 0.596333],
+ [0.214350, 0.016973, 0.599239],
+ [0.221197, 0.016497, 0.602083],
+ [0.227983, 0.016007, 0.604867],
+ [0.234715, 0.015502, 0.607592],
+ [0.241396, 0.014979, 0.610259],
+ [0.248032, 0.014439, 0.612868],
+ [0.254627, 0.013882, 0.615419],
+ [0.261183, 0.013308, 0.617911],
+ [0.267703, 0.012716, 0.620346],
+ [0.274191, 0.012109, 0.622722],
+ [0.280648, 0.011488, 0.625038],
+ [0.287076, 0.010855, 0.627295],
+ [0.293478, 0.010213, 0.629490],
+ [0.299855, 0.009561, 0.631624],
+ [0.306210, 0.008902, 0.633694],
+ [0.312543, 0.008239, 0.635700],
+ [0.318856, 0.007576, 0.637640],
+ [0.325150, 0.006915, 0.639512],
+ [0.331426, 0.006261, 0.641316],
+ [0.337683, 0.005618, 0.643049],
+ [0.343925, 0.004991, 0.644710],
+ [0.350150, 0.004382, 0.646298],
+ [0.356359, 0.003798, 0.647810],
+ [0.362553, 0.003243, 0.649245],
+ [0.368733, 0.002724, 0.650601],
+ [0.374897, 0.002245, 0.651876],
+ [0.381047, 0.001814, 0.653068],
+ [0.387183, 0.001434, 0.654177],
+ [0.393304, 0.001114, 0.655199],
+ [0.399411, 0.000859, 0.656133],
+ [0.405503, 0.000678, 0.656977],
+ [0.411580, 0.000577, 0.657730],
+ [0.417642, 0.000564, 0.658390],
+ [0.423689, 0.000646, 0.658956],
+ [0.429719, 0.000831, 0.659425],
+ [0.435734, 0.001127, 0.659797],
+ [0.441732, 0.001540, 0.660069],
+ [0.447714, 0.002080, 0.660240],
+ [0.453677, 0.002755, 0.660310],
+ [0.459623, 0.003574, 0.660277],
+ [0.465550, 0.004545, 0.660139],
+ [0.471457, 0.005678, 0.659897],
+ [0.477344, 0.006980, 0.659549],
+ [0.483210, 0.008460, 0.659095],
+ [0.489055, 0.010127, 0.658534],
+ [0.494877, 0.011990, 0.657865],
+ [0.500678, 0.014055, 0.657088],
+ [0.506454, 0.016333, 0.656202],
+ [0.512206, 0.018833, 0.655209],
+ [0.517933, 0.021563, 0.654109],
+ [0.523633, 0.024532, 0.652901],
+ [0.529306, 0.027747, 0.651586],
+ [0.534952, 0.031217, 0.650165],
+ [0.540570, 0.034950, 0.648640],
+ [0.546157, 0.038954, 0.647010],
+ [0.551715, 0.043136, 0.645277],
+ [0.557243, 0.047331, 0.643443],
+ [0.562738, 0.051545, 0.641509],
+ [0.568201, 0.055778, 0.639477],
+ [0.573632, 0.060028, 0.637349],
+ [0.579029, 0.064296, 0.635126],
+ [0.584391, 0.068579, 0.632812],
+ [0.589719, 0.072878, 0.630408],
+ [0.595011, 0.077190, 0.627917],
+ [0.600266, 0.081516, 0.625342],
+ [0.605485, 0.085854, 0.622686],
+ [0.610667, 0.090204, 0.619951],
+ [0.615812, 0.094564, 0.617140],
+ [0.620919, 0.098934, 0.614257],
+ [0.625987, 0.103312, 0.611305],
+ [0.631017, 0.107699, 0.608287],
+ [0.636008, 0.112092, 0.605205],
+ [0.640959, 0.116492, 0.602065],
+ [0.645872, 0.120898, 0.598867],
+ [0.650746, 0.125309, 0.595617],
+ [0.655580, 0.129725, 0.592317],
+ [0.660374, 0.134144, 0.588971],
+ [0.665129, 0.138566, 0.585582],
+ [0.669845, 0.142992, 0.582154],
+ [0.674522, 0.147419, 0.578688],
+ [0.679160, 0.151848, 0.575189],
+ [0.683758, 0.156278, 0.571660],
+ [0.688318, 0.160709, 0.568103],
+ [0.692840, 0.165141, 0.564522],
+ [0.697324, 0.169573, 0.560919],
+ [0.701769, 0.174005, 0.557296],
+ [0.706178, 0.178437, 0.553657],
+ [0.710549, 0.182868, 0.550004],
+ [0.714883, 0.187299, 0.546338],
+ [0.719181, 0.191729, 0.542663],
+ [0.723444, 0.196158, 0.538981],
+ [0.727670, 0.200586, 0.535293],
+ [0.731862, 0.205013, 0.531601],
+ [0.736019, 0.209439, 0.527908],
+ [0.740143, 0.213864, 0.524216],
+ [0.744232, 0.218288, 0.520524],
+ [0.748289, 0.222711, 0.516834],
+ [0.752312, 0.227133, 0.513149],
+ [0.756304, 0.231555, 0.509468],
+ [0.760264, 0.235976, 0.505794],
+ [0.764193, 0.240396, 0.502126],
+ [0.768090, 0.244817, 0.498465],
+ [0.771958, 0.249237, 0.494813],
+ [0.775796, 0.253658, 0.491171],
+ [0.779604, 0.258078, 0.487539],
+ [0.783383, 0.262500, 0.483918],
+ [0.787133, 0.266922, 0.480307],
+ [0.790855, 0.271345, 0.476706],
+ [0.794549, 0.275770, 0.473117],
+ [0.798216, 0.280197, 0.469538],
+ [0.801855, 0.284626, 0.465971],
+ [0.805467, 0.289057, 0.462415],
+ [0.809052, 0.293491, 0.458870],
+ [0.812612, 0.297928, 0.455338],
+ [0.816144, 0.302368, 0.451816],
+ [0.819651, 0.306812, 0.448306],
+ [0.823132, 0.311261, 0.444806],
+ [0.826588, 0.315714, 0.441316],
+ [0.830018, 0.320172, 0.437836],
+ [0.833422, 0.324635, 0.434366],
+ [0.836801, 0.329105, 0.430905],
+ [0.840155, 0.333580, 0.427455],
+ [0.843484, 0.338062, 0.424013],
+ [0.846788, 0.342551, 0.420579],
+ [0.850066, 0.347048, 0.417153],
+ [0.853319, 0.351553, 0.413734],
+ [0.856547, 0.356066, 0.410322],
+ [0.859750, 0.360588, 0.406917],
+ [0.862927, 0.365119, 0.403519],
+ [0.866078, 0.369660, 0.400126],
+ [0.869203, 0.374212, 0.396738],
+ [0.872303, 0.378774, 0.393355],
+ [0.875376, 0.383347, 0.389976],
+ [0.878423, 0.387932, 0.386600],
+ [0.881443, 0.392529, 0.383229],
+ [0.884436, 0.397139, 0.379860],
+ [0.887402, 0.401762, 0.376494],
+ [0.890340, 0.406398, 0.373130],
+ [0.893250, 0.411048, 0.369768],
+ [0.896131, 0.415712, 0.366407],
+ [0.898984, 0.420392, 0.363047],
+ [0.901807, 0.425087, 0.359688],
+ [0.904601, 0.429797, 0.356329],
+ [0.907365, 0.434524, 0.352970],
+ [0.910098, 0.439268, 0.349610],
+ [0.912800, 0.444029, 0.346251],
+ [0.915471, 0.448807, 0.342890],
+ [0.918109, 0.453603, 0.339529],
+ [0.920714, 0.458417, 0.336166],
+ [0.923287, 0.463251, 0.332801],
+ [0.925825, 0.468103, 0.329435],
+ [0.928329, 0.472975, 0.326067],
+ [0.930798, 0.477867, 0.322697],
+ [0.933232, 0.482780, 0.319325],
+ [0.935630, 0.487712, 0.315952],
+ [0.937990, 0.492667, 0.312575],
+ [0.940313, 0.497642, 0.309197],
+ [0.942598, 0.502639, 0.305816],
+ [0.944844, 0.507658, 0.302433],
+ [0.947051, 0.512699, 0.299049],
+ [0.949217, 0.517763, 0.295662],
+ [0.951344, 0.522850, 0.292275],
+ [0.953428, 0.527960, 0.288883],
+ [0.955470, 0.533093, 0.285490],
+ [0.957469, 0.538250, 0.282096],
+ [0.959424, 0.543431, 0.278701],
+ [0.961336, 0.548636, 0.275305],
+ [0.963203, 0.553865, 0.271909],
+ [0.965024, 0.559118, 0.268513],
+ [0.966798, 0.564396, 0.265118],
+ [0.968526, 0.569700, 0.261721],
+ [0.970205, 0.575028, 0.258325],
+ [0.971835, 0.580382, 0.254931],
+ [0.973416, 0.585761, 0.251540],
+ [0.974947, 0.591165, 0.248151],
+ [0.976428, 0.596595, 0.244767],
+ [0.977856, 0.602051, 0.241387],
+ [0.979233, 0.607532, 0.238013],
+ [0.980556, 0.613039, 0.234646],
+ [0.981826, 0.618572, 0.231287],
+ [0.983041, 0.624131, 0.227937],
+ [0.984199, 0.629718, 0.224595],
+ [0.985301, 0.635330, 0.221265],
+ [0.986345, 0.640969, 0.217948],
+ [0.987332, 0.646633, 0.214648],
+ [0.988260, 0.652325, 0.211364],
+ [0.989128, 0.658043, 0.208100],
+ [0.989935, 0.663787, 0.204859],
+ [0.990681, 0.669558, 0.201642],
+ [0.991365, 0.675355, 0.198453],
+ [0.991985, 0.681179, 0.195295],
+ [0.992541, 0.687030, 0.192170],
+ [0.993032, 0.692907, 0.189084],
+ [0.993456, 0.698810, 0.186041],
+ [0.993814, 0.704741, 0.183043],
+ [0.994103, 0.710698, 0.180097],
+ [0.994324, 0.716681, 0.177208],
+ [0.994474, 0.722691, 0.174381],
+ [0.994553, 0.728728, 0.171622],
+ [0.994561, 0.734791, 0.168938],
+ [0.994495, 0.740880, 0.166335],
+ [0.994355, 0.746995, 0.163821],
+ [0.994141, 0.753137, 0.161404],
+ [0.993851, 0.759304, 0.159092],
+ [0.993482, 0.765499, 0.156891],
+ [0.993033, 0.771720, 0.154808],
+ [0.992505, 0.777967, 0.152855],
+ [0.991897, 0.784239, 0.151042],
+ [0.991209, 0.790537, 0.149377],
+ [0.990439, 0.796859, 0.147870],
+ [0.989587, 0.803205, 0.146529],
+ [0.988648, 0.809579, 0.145357],
+ [0.987621, 0.815978, 0.144363],
+ [0.986509, 0.822401, 0.143557],
+ [0.985314, 0.828846, 0.142945],
+ [0.984031, 0.835315, 0.142528],
+ [0.982653, 0.841812, 0.142303],
+ [0.981190, 0.848329, 0.142279],
+ [0.979644, 0.854866, 0.142453],
+ [0.977995, 0.861432, 0.142808],
+ [0.976265, 0.868016, 0.143351],
+ [0.974443, 0.874622, 0.144061],
+ [0.972530, 0.881250, 0.144923],
+ [0.970533, 0.887896, 0.145919],
+ [0.968443, 0.894564, 0.147014],
+ [0.966271, 0.901249, 0.148180],
+ [0.964021, 0.907950, 0.149370],
+ [0.961681, 0.914672, 0.150520],
+ [0.959276, 0.921407, 0.151566],
+ [0.956808, 0.928152, 0.152409],
+ [0.954287, 0.934908, 0.152921],
+ [0.951726, 0.941671, 0.152925],
+ [0.949151, 0.948435, 0.152178],
+ [0.946602, 0.955190, 0.150328],
+ [0.944152, 0.961916, 0.146861],
+ [0.941896, 0.968590, 0.140956],
+ [0.940015, 0.975158, 0.131326],
+]
-_viridis_data = [[0.267004, 0.004874, 0.329415],
- [0.268510, 0.009605, 0.335427],
- [0.269944, 0.014625, 0.341379],
- [0.271305, 0.019942, 0.347269],
- [0.272594, 0.025563, 0.353093],
- [0.273809, 0.031497, 0.358853],
- [0.274952, 0.037752, 0.364543],
- [0.276022, 0.044167, 0.370164],
- [0.277018, 0.050344, 0.375715],
- [0.277941, 0.056324, 0.381191],
- [0.278791, 0.062145, 0.386592],
- [0.279566, 0.067836, 0.391917],
- [0.280267, 0.073417, 0.397163],
- [0.280894, 0.078907, 0.402329],
- [0.281446, 0.084320, 0.407414],
- [0.281924, 0.089666, 0.412415],
- [0.282327, 0.094955, 0.417331],
- [0.282656, 0.100196, 0.422160],
- [0.282910, 0.105393, 0.426902],
- [0.283091, 0.110553, 0.431554],
- [0.283197, 0.115680, 0.436115],
- [0.283229, 0.120777, 0.440584],
- [0.283187, 0.125848, 0.444960],
- [0.283072, 0.130895, 0.449241],
- [0.282884, 0.135920, 0.453427],
- [0.282623, 0.140926, 0.457517],
- [0.282290, 0.145912, 0.461510],
- [0.281887, 0.150881, 0.465405],
- [0.281412, 0.155834, 0.469201],
- [0.280868, 0.160771, 0.472899],
- [0.280255, 0.165693, 0.476498],
- [0.279574, 0.170599, 0.479997],
- [0.278826, 0.175490, 0.483397],
- [0.278012, 0.180367, 0.486697],
- [0.277134, 0.185228, 0.489898],
- [0.276194, 0.190074, 0.493001],
- [0.275191, 0.194905, 0.496005],
- [0.274128, 0.199721, 0.498911],
- [0.273006, 0.204520, 0.501721],
- [0.271828, 0.209303, 0.504434],
- [0.270595, 0.214069, 0.507052],
- [0.269308, 0.218818, 0.509577],
- [0.267968, 0.223549, 0.512008],
- [0.266580, 0.228262, 0.514349],
- [0.265145, 0.232956, 0.516599],
- [0.263663, 0.237631, 0.518762],
- [0.262138, 0.242286, 0.520837],
- [0.260571, 0.246922, 0.522828],
- [0.258965, 0.251537, 0.524736],
- [0.257322, 0.256130, 0.526563],
- [0.255645, 0.260703, 0.528312],
- [0.253935, 0.265254, 0.529983],
- [0.252194, 0.269783, 0.531579],
- [0.250425, 0.274290, 0.533103],
- [0.248629, 0.278775, 0.534556],
- [0.246811, 0.283237, 0.535941],
- [0.244972, 0.287675, 0.537260],
- [0.243113, 0.292092, 0.538516],
- [0.241237, 0.296485, 0.539709],
- [0.239346, 0.300855, 0.540844],
- [0.237441, 0.305202, 0.541921],
- [0.235526, 0.309527, 0.542944],
- [0.233603, 0.313828, 0.543914],
- [0.231674, 0.318106, 0.544834],
- [0.229739, 0.322361, 0.545706],
- [0.227802, 0.326594, 0.546532],
- [0.225863, 0.330805, 0.547314],
- [0.223925, 0.334994, 0.548053],
- [0.221989, 0.339161, 0.548752],
- [0.220057, 0.343307, 0.549413],
- [0.218130, 0.347432, 0.550038],
- [0.216210, 0.351535, 0.550627],
- [0.214298, 0.355619, 0.551184],
- [0.212395, 0.359683, 0.551710],
- [0.210503, 0.363727, 0.552206],
- [0.208623, 0.367752, 0.552675],
- [0.206756, 0.371758, 0.553117],
- [0.204903, 0.375746, 0.553533],
- [0.203063, 0.379716, 0.553925],
- [0.201239, 0.383670, 0.554294],
- [0.199430, 0.387607, 0.554642],
- [0.197636, 0.391528, 0.554969],
- [0.195860, 0.395433, 0.555276],
- [0.194100, 0.399323, 0.555565],
- [0.192357, 0.403199, 0.555836],
- [0.190631, 0.407061, 0.556089],
- [0.188923, 0.410910, 0.556326],
- [0.187231, 0.414746, 0.556547],
- [0.185556, 0.418570, 0.556753],
- [0.183898, 0.422383, 0.556944],
- [0.182256, 0.426184, 0.557120],
- [0.180629, 0.429975, 0.557282],
- [0.179019, 0.433756, 0.557430],
- [0.177423, 0.437527, 0.557565],
- [0.175841, 0.441290, 0.557685],
- [0.174274, 0.445044, 0.557792],
- [0.172719, 0.448791, 0.557885],
- [0.171176, 0.452530, 0.557965],
- [0.169646, 0.456262, 0.558030],
- [0.168126, 0.459988, 0.558082],
- [0.166617, 0.463708, 0.558119],
- [0.165117, 0.467423, 0.558141],
- [0.163625, 0.471133, 0.558148],
- [0.162142, 0.474838, 0.558140],
- [0.160665, 0.478540, 0.558115],
- [0.159194, 0.482237, 0.558073],
- [0.157729, 0.485932, 0.558013],
- [0.156270, 0.489624, 0.557936],
- [0.154815, 0.493313, 0.557840],
- [0.153364, 0.497000, 0.557724],
- [0.151918, 0.500685, 0.557587],
- [0.150476, 0.504369, 0.557430],
- [0.149039, 0.508051, 0.557250],
- [0.147607, 0.511733, 0.557049],
- [0.146180, 0.515413, 0.556823],
- [0.144759, 0.519093, 0.556572],
- [0.143343, 0.522773, 0.556295],
- [0.141935, 0.526453, 0.555991],
- [0.140536, 0.530132, 0.555659],
- [0.139147, 0.533812, 0.555298],
- [0.137770, 0.537492, 0.554906],
- [0.136408, 0.541173, 0.554483],
- [0.135066, 0.544853, 0.554029],
- [0.133743, 0.548535, 0.553541],
- [0.132444, 0.552216, 0.553018],
- [0.131172, 0.555899, 0.552459],
- [0.129933, 0.559582, 0.551864],
- [0.128729, 0.563265, 0.551229],
- [0.127568, 0.566949, 0.550556],
- [0.126453, 0.570633, 0.549841],
- [0.125394, 0.574318, 0.549086],
- [0.124395, 0.578002, 0.548287],
- [0.123463, 0.581687, 0.547445],
- [0.122606, 0.585371, 0.546557],
- [0.121831, 0.589055, 0.545623],
- [0.121148, 0.592739, 0.544641],
- [0.120565, 0.596422, 0.543611],
- [0.120092, 0.600104, 0.542530],
- [0.119738, 0.603785, 0.541400],
- [0.119512, 0.607464, 0.540218],
- [0.119423, 0.611141, 0.538982],
- [0.119483, 0.614817, 0.537692],
- [0.119699, 0.618490, 0.536347],
- [0.120081, 0.622161, 0.534946],
- [0.120638, 0.625828, 0.533488],
- [0.121380, 0.629492, 0.531973],
- [0.122312, 0.633153, 0.530398],
- [0.123444, 0.636809, 0.528763],
- [0.124780, 0.640461, 0.527068],
- [0.126326, 0.644107, 0.525311],
- [0.128087, 0.647749, 0.523491],
- [0.130067, 0.651384, 0.521608],
- [0.132268, 0.655014, 0.519661],
- [0.134692, 0.658636, 0.517649],
- [0.137339, 0.662252, 0.515571],
- [0.140210, 0.665859, 0.513427],
- [0.143303, 0.669459, 0.511215],
- [0.146616, 0.673050, 0.508936],
- [0.150148, 0.676631, 0.506589],
- [0.153894, 0.680203, 0.504172],
- [0.157851, 0.683765, 0.501686],
- [0.162016, 0.687316, 0.499129],
- [0.166383, 0.690856, 0.496502],
- [0.170948, 0.694384, 0.493803],
- [0.175707, 0.697900, 0.491033],
- [0.180653, 0.701402, 0.488189],
- [0.185783, 0.704891, 0.485273],
- [0.191090, 0.708366, 0.482284],
- [0.196571, 0.711827, 0.479221],
- [0.202219, 0.715272, 0.476084],
- [0.208030, 0.718701, 0.472873],
- [0.214000, 0.722114, 0.469588],
- [0.220124, 0.725509, 0.466226],
- [0.226397, 0.728888, 0.462789],
- [0.232815, 0.732247, 0.459277],
- [0.239374, 0.735588, 0.455688],
- [0.246070, 0.738910, 0.452024],
- [0.252899, 0.742211, 0.448284],
- [0.259857, 0.745492, 0.444467],
- [0.266941, 0.748751, 0.440573],
- [0.274149, 0.751988, 0.436601],
- [0.281477, 0.755203, 0.432552],
- [0.288921, 0.758394, 0.428426],
- [0.296479, 0.761561, 0.424223],
- [0.304148, 0.764704, 0.419943],
- [0.311925, 0.767822, 0.415586],
- [0.319809, 0.770914, 0.411152],
- [0.327796, 0.773980, 0.406640],
- [0.335885, 0.777018, 0.402049],
- [0.344074, 0.780029, 0.397381],
- [0.352360, 0.783011, 0.392636],
- [0.360741, 0.785964, 0.387814],
- [0.369214, 0.788888, 0.382914],
- [0.377779, 0.791781, 0.377939],
- [0.386433, 0.794644, 0.372886],
- [0.395174, 0.797475, 0.367757],
- [0.404001, 0.800275, 0.362552],
- [0.412913, 0.803041, 0.357269],
- [0.421908, 0.805774, 0.351910],
- [0.430983, 0.808473, 0.346476],
- [0.440137, 0.811138, 0.340967],
- [0.449368, 0.813768, 0.335384],
- [0.458674, 0.816363, 0.329727],
- [0.468053, 0.818921, 0.323998],
- [0.477504, 0.821444, 0.318195],
- [0.487026, 0.823929, 0.312321],
- [0.496615, 0.826376, 0.306377],
- [0.506271, 0.828786, 0.300362],
- [0.515992, 0.831158, 0.294279],
- [0.525776, 0.833491, 0.288127],
- [0.535621, 0.835785, 0.281908],
- [0.545524, 0.838039, 0.275626],
- [0.555484, 0.840254, 0.269281],
- [0.565498, 0.842430, 0.262877],
- [0.575563, 0.844566, 0.256415],
- [0.585678, 0.846661, 0.249897],
- [0.595839, 0.848717, 0.243329],
- [0.606045, 0.850733, 0.236712],
- [0.616293, 0.852709, 0.230052],
- [0.626579, 0.854645, 0.223353],
- [0.636902, 0.856542, 0.216620],
- [0.647257, 0.858400, 0.209861],
- [0.657642, 0.860219, 0.203082],
- [0.668054, 0.861999, 0.196293],
- [0.678489, 0.863742, 0.189503],
- [0.688944, 0.865448, 0.182725],
- [0.699415, 0.867117, 0.175971],
- [0.709898, 0.868751, 0.169257],
- [0.720391, 0.870350, 0.162603],
- [0.730889, 0.871916, 0.156029],
- [0.741388, 0.873449, 0.149561],
- [0.751884, 0.874951, 0.143228],
- [0.762373, 0.876424, 0.137064],
- [0.772852, 0.877868, 0.131109],
- [0.783315, 0.879285, 0.125405],
- [0.793760, 0.880678, 0.120005],
- [0.804182, 0.882046, 0.114965],
- [0.814576, 0.883393, 0.110347],
- [0.824940, 0.884720, 0.106217],
- [0.835270, 0.886029, 0.102646],
- [0.845561, 0.887322, 0.099702],
- [0.855810, 0.888601, 0.097452],
- [0.866013, 0.889868, 0.095953],
- [0.876168, 0.891125, 0.095250],
- [0.886271, 0.892374, 0.095374],
- [0.896320, 0.893616, 0.096335],
- [0.906311, 0.894855, 0.098125],
- [0.916242, 0.896091, 0.100717],
- [0.926106, 0.897330, 0.104071],
- [0.935904, 0.898570, 0.108131],
- [0.945636, 0.899815, 0.112838],
- [0.955300, 0.901065, 0.118128],
- [0.964894, 0.902323, 0.123941],
- [0.974417, 0.903590, 0.130215],
- [0.983868, 0.904867, 0.136897],
- [0.993248, 0.906157, 0.143936]]
+_viridis_data = [
+ [0.267004, 0.004874, 0.329415],
+ [0.268510, 0.009605, 0.335427],
+ [0.269944, 0.014625, 0.341379],
+ [0.271305, 0.019942, 0.347269],
+ [0.272594, 0.025563, 0.353093],
+ [0.273809, 0.031497, 0.358853],
+ [0.274952, 0.037752, 0.364543],
+ [0.276022, 0.044167, 0.370164],
+ [0.277018, 0.050344, 0.375715],
+ [0.277941, 0.056324, 0.381191],
+ [0.278791, 0.062145, 0.386592],
+ [0.279566, 0.067836, 0.391917],
+ [0.280267, 0.073417, 0.397163],
+ [0.280894, 0.078907, 0.402329],
+ [0.281446, 0.084320, 0.407414],
+ [0.281924, 0.089666, 0.412415],
+ [0.282327, 0.094955, 0.417331],
+ [0.282656, 0.100196, 0.422160],
+ [0.282910, 0.105393, 0.426902],
+ [0.283091, 0.110553, 0.431554],
+ [0.283197, 0.115680, 0.436115],
+ [0.283229, 0.120777, 0.440584],
+ [0.283187, 0.125848, 0.444960],
+ [0.283072, 0.130895, 0.449241],
+ [0.282884, 0.135920, 0.453427],
+ [0.282623, 0.140926, 0.457517],
+ [0.282290, 0.145912, 0.461510],
+ [0.281887, 0.150881, 0.465405],
+ [0.281412, 0.155834, 0.469201],
+ [0.280868, 0.160771, 0.472899],
+ [0.280255, 0.165693, 0.476498],
+ [0.279574, 0.170599, 0.479997],
+ [0.278826, 0.175490, 0.483397],
+ [0.278012, 0.180367, 0.486697],
+ [0.277134, 0.185228, 0.489898],
+ [0.276194, 0.190074, 0.493001],
+ [0.275191, 0.194905, 0.496005],
+ [0.274128, 0.199721, 0.498911],
+ [0.273006, 0.204520, 0.501721],
+ [0.271828, 0.209303, 0.504434],
+ [0.270595, 0.214069, 0.507052],
+ [0.269308, 0.218818, 0.509577],
+ [0.267968, 0.223549, 0.512008],
+ [0.266580, 0.228262, 0.514349],
+ [0.265145, 0.232956, 0.516599],
+ [0.263663, 0.237631, 0.518762],
+ [0.262138, 0.242286, 0.520837],
+ [0.260571, 0.246922, 0.522828],
+ [0.258965, 0.251537, 0.524736],
+ [0.257322, 0.256130, 0.526563],
+ [0.255645, 0.260703, 0.528312],
+ [0.253935, 0.265254, 0.529983],
+ [0.252194, 0.269783, 0.531579],
+ [0.250425, 0.274290, 0.533103],
+ [0.248629, 0.278775, 0.534556],
+ [0.246811, 0.283237, 0.535941],
+ [0.244972, 0.287675, 0.537260],
+ [0.243113, 0.292092, 0.538516],
+ [0.241237, 0.296485, 0.539709],
+ [0.239346, 0.300855, 0.540844],
+ [0.237441, 0.305202, 0.541921],
+ [0.235526, 0.309527, 0.542944],
+ [0.233603, 0.313828, 0.543914],
+ [0.231674, 0.318106, 0.544834],
+ [0.229739, 0.322361, 0.545706],
+ [0.227802, 0.326594, 0.546532],
+ [0.225863, 0.330805, 0.547314],
+ [0.223925, 0.334994, 0.548053],
+ [0.221989, 0.339161, 0.548752],
+ [0.220057, 0.343307, 0.549413],
+ [0.218130, 0.347432, 0.550038],
+ [0.216210, 0.351535, 0.550627],
+ [0.214298, 0.355619, 0.551184],
+ [0.212395, 0.359683, 0.551710],
+ [0.210503, 0.363727, 0.552206],
+ [0.208623, 0.367752, 0.552675],
+ [0.206756, 0.371758, 0.553117],
+ [0.204903, 0.375746, 0.553533],
+ [0.203063, 0.379716, 0.553925],
+ [0.201239, 0.383670, 0.554294],
+ [0.199430, 0.387607, 0.554642],
+ [0.197636, 0.391528, 0.554969],
+ [0.195860, 0.395433, 0.555276],
+ [0.194100, 0.399323, 0.555565],
+ [0.192357, 0.403199, 0.555836],
+ [0.190631, 0.407061, 0.556089],
+ [0.188923, 0.410910, 0.556326],
+ [0.187231, 0.414746, 0.556547],
+ [0.185556, 0.418570, 0.556753],
+ [0.183898, 0.422383, 0.556944],
+ [0.182256, 0.426184, 0.557120],
+ [0.180629, 0.429975, 0.557282],
+ [0.179019, 0.433756, 0.557430],
+ [0.177423, 0.437527, 0.557565],
+ [0.175841, 0.441290, 0.557685],
+ [0.174274, 0.445044, 0.557792],
+ [0.172719, 0.448791, 0.557885],
+ [0.171176, 0.452530, 0.557965],
+ [0.169646, 0.456262, 0.558030],
+ [0.168126, 0.459988, 0.558082],
+ [0.166617, 0.463708, 0.558119],
+ [0.165117, 0.467423, 0.558141],
+ [0.163625, 0.471133, 0.558148],
+ [0.162142, 0.474838, 0.558140],
+ [0.160665, 0.478540, 0.558115],
+ [0.159194, 0.482237, 0.558073],
+ [0.157729, 0.485932, 0.558013],
+ [0.156270, 0.489624, 0.557936],
+ [0.154815, 0.493313, 0.557840],
+ [0.153364, 0.497000, 0.557724],
+ [0.151918, 0.500685, 0.557587],
+ [0.150476, 0.504369, 0.557430],
+ [0.149039, 0.508051, 0.557250],
+ [0.147607, 0.511733, 0.557049],
+ [0.146180, 0.515413, 0.556823],
+ [0.144759, 0.519093, 0.556572],
+ [0.143343, 0.522773, 0.556295],
+ [0.141935, 0.526453, 0.555991],
+ [0.140536, 0.530132, 0.555659],
+ [0.139147, 0.533812, 0.555298],
+ [0.137770, 0.537492, 0.554906],
+ [0.136408, 0.541173, 0.554483],
+ [0.135066, 0.544853, 0.554029],
+ [0.133743, 0.548535, 0.553541],
+ [0.132444, 0.552216, 0.553018],
+ [0.131172, 0.555899, 0.552459],
+ [0.129933, 0.559582, 0.551864],
+ [0.128729, 0.563265, 0.551229],
+ [0.127568, 0.566949, 0.550556],
+ [0.126453, 0.570633, 0.549841],
+ [0.125394, 0.574318, 0.549086],
+ [0.124395, 0.578002, 0.548287],
+ [0.123463, 0.581687, 0.547445],
+ [0.122606, 0.585371, 0.546557],
+ [0.121831, 0.589055, 0.545623],
+ [0.121148, 0.592739, 0.544641],
+ [0.120565, 0.596422, 0.543611],
+ [0.120092, 0.600104, 0.542530],
+ [0.119738, 0.603785, 0.541400],
+ [0.119512, 0.607464, 0.540218],
+ [0.119423, 0.611141, 0.538982],
+ [0.119483, 0.614817, 0.537692],
+ [0.119699, 0.618490, 0.536347],
+ [0.120081, 0.622161, 0.534946],
+ [0.120638, 0.625828, 0.533488],
+ [0.121380, 0.629492, 0.531973],
+ [0.122312, 0.633153, 0.530398],
+ [0.123444, 0.636809, 0.528763],
+ [0.124780, 0.640461, 0.527068],
+ [0.126326, 0.644107, 0.525311],
+ [0.128087, 0.647749, 0.523491],
+ [0.130067, 0.651384, 0.521608],
+ [0.132268, 0.655014, 0.519661],
+ [0.134692, 0.658636, 0.517649],
+ [0.137339, 0.662252, 0.515571],
+ [0.140210, 0.665859, 0.513427],
+ [0.143303, 0.669459, 0.511215],
+ [0.146616, 0.673050, 0.508936],
+ [0.150148, 0.676631, 0.506589],
+ [0.153894, 0.680203, 0.504172],
+ [0.157851, 0.683765, 0.501686],
+ [0.162016, 0.687316, 0.499129],
+ [0.166383, 0.690856, 0.496502],
+ [0.170948, 0.694384, 0.493803],
+ [0.175707, 0.697900, 0.491033],
+ [0.180653, 0.701402, 0.488189],
+ [0.185783, 0.704891, 0.485273],
+ [0.191090, 0.708366, 0.482284],
+ [0.196571, 0.711827, 0.479221],
+ [0.202219, 0.715272, 0.476084],
+ [0.208030, 0.718701, 0.472873],
+ [0.214000, 0.722114, 0.469588],
+ [0.220124, 0.725509, 0.466226],
+ [0.226397, 0.728888, 0.462789],
+ [0.232815, 0.732247, 0.459277],
+ [0.239374, 0.735588, 0.455688],
+ [0.246070, 0.738910, 0.452024],
+ [0.252899, 0.742211, 0.448284],
+ [0.259857, 0.745492, 0.444467],
+ [0.266941, 0.748751, 0.440573],
+ [0.274149, 0.751988, 0.436601],
+ [0.281477, 0.755203, 0.432552],
+ [0.288921, 0.758394, 0.428426],
+ [0.296479, 0.761561, 0.424223],
+ [0.304148, 0.764704, 0.419943],
+ [0.311925, 0.767822, 0.415586],
+ [0.319809, 0.770914, 0.411152],
+ [0.327796, 0.773980, 0.406640],
+ [0.335885, 0.777018, 0.402049],
+ [0.344074, 0.780029, 0.397381],
+ [0.352360, 0.783011, 0.392636],
+ [0.360741, 0.785964, 0.387814],
+ [0.369214, 0.788888, 0.382914],
+ [0.377779, 0.791781, 0.377939],
+ [0.386433, 0.794644, 0.372886],
+ [0.395174, 0.797475, 0.367757],
+ [0.404001, 0.800275, 0.362552],
+ [0.412913, 0.803041, 0.357269],
+ [0.421908, 0.805774, 0.351910],
+ [0.430983, 0.808473, 0.346476],
+ [0.440137, 0.811138, 0.340967],
+ [0.449368, 0.813768, 0.335384],
+ [0.458674, 0.816363, 0.329727],
+ [0.468053, 0.818921, 0.323998],
+ [0.477504, 0.821444, 0.318195],
+ [0.487026, 0.823929, 0.312321],
+ [0.496615, 0.826376, 0.306377],
+ [0.506271, 0.828786, 0.300362],
+ [0.515992, 0.831158, 0.294279],
+ [0.525776, 0.833491, 0.288127],
+ [0.535621, 0.835785, 0.281908],
+ [0.545524, 0.838039, 0.275626],
+ [0.555484, 0.840254, 0.269281],
+ [0.565498, 0.842430, 0.262877],
+ [0.575563, 0.844566, 0.256415],
+ [0.585678, 0.846661, 0.249897],
+ [0.595839, 0.848717, 0.243329],
+ [0.606045, 0.850733, 0.236712],
+ [0.616293, 0.852709, 0.230052],
+ [0.626579, 0.854645, 0.223353],
+ [0.636902, 0.856542, 0.216620],
+ [0.647257, 0.858400, 0.209861],
+ [0.657642, 0.860219, 0.203082],
+ [0.668054, 0.861999, 0.196293],
+ [0.678489, 0.863742, 0.189503],
+ [0.688944, 0.865448, 0.182725],
+ [0.699415, 0.867117, 0.175971],
+ [0.709898, 0.868751, 0.169257],
+ [0.720391, 0.870350, 0.162603],
+ [0.730889, 0.871916, 0.156029],
+ [0.741388, 0.873449, 0.149561],
+ [0.751884, 0.874951, 0.143228],
+ [0.762373, 0.876424, 0.137064],
+ [0.772852, 0.877868, 0.131109],
+ [0.783315, 0.879285, 0.125405],
+ [0.793760, 0.880678, 0.120005],
+ [0.804182, 0.882046, 0.114965],
+ [0.814576, 0.883393, 0.110347],
+ [0.824940, 0.884720, 0.106217],
+ [0.835270, 0.886029, 0.102646],
+ [0.845561, 0.887322, 0.099702],
+ [0.855810, 0.888601, 0.097452],
+ [0.866013, 0.889868, 0.095953],
+ [0.876168, 0.891125, 0.095250],
+ [0.886271, 0.892374, 0.095374],
+ [0.896320, 0.893616, 0.096335],
+ [0.906311, 0.894855, 0.098125],
+ [0.916242, 0.896091, 0.100717],
+ [0.926106, 0.897330, 0.104071],
+ [0.935904, 0.898570, 0.108131],
+ [0.945636, 0.899815, 0.112838],
+ [0.955300, 0.901065, 0.118128],
+ [0.964894, 0.902323, 0.123941],
+ [0.974417, 0.903590, 0.130215],
+ [0.983868, 0.904867, 0.136897],
+ [0.993248, 0.906157, 0.143936],
+]
-_jet_data = [[0,0,127,255],[0,0,255,255],[0,127,255,255],[0,255,255,255],[127,255,127,255],[255,255,0,255],[255,127,0,255],[255,0,0,255], [127,0,0,255]]
-_monochrome_data = [[0,0,0,255],[255,255,255,255]]
-_hot_data = [[0,0,0,255],[255,0,0,255],[255,255,0,255],[255,255,255,255]]
+_jet_data = [
+ [0, 0, 127, 255],
+ [0, 0, 255, 255],
+ [0, 127, 255, 255],
+ [0, 255, 255, 255],
+ [127, 255, 127, 255],
+ [255, 255, 0, 255],
+ [255, 127, 0, 255],
+ [255, 0, 0, 255],
+ [127, 0, 0, 255],
+]
+_monochrome_data = [[0, 0, 0, 255], [255, 255, 255, 255]]
+_hot_data = [[0, 0, 0, 255], [255, 0, 0, 255], [255, 255, 0, 255], [255, 255, 255, 255]]
cmaps = {}
+
class PyDMColorMap(object):
Magma = 0
Inferno = 1
@@ -1059,13 +1078,16 @@ class PyDMColorMap(object):
Monochrome = 5
Hot = 6
-for (name, data) in ((PyDMColorMap.Magma, np.array(_magma_data)),
- (PyDMColorMap.Inferno, np.array(_inferno_data)),
- (PyDMColorMap.Plasma, np.array(_plasma_data)),
- (PyDMColorMap.Viridis, np.array(_viridis_data)),
- (PyDMColorMap.Jet, np.array(_jet_data)),
- (PyDMColorMap.Monochrome, np.array(_monochrome_data)),
- (PyDMColorMap.Hot, np.array(_hot_data))):
+
+for name, data in (
+ (PyDMColorMap.Magma, np.array(_magma_data)),
+ (PyDMColorMap.Inferno, np.array(_inferno_data)),
+ (PyDMColorMap.Plasma, np.array(_plasma_data)),
+ (PyDMColorMap.Viridis, np.array(_viridis_data)),
+ (PyDMColorMap.Jet, np.array(_jet_data)),
+ (PyDMColorMap.Monochrome, np.array(_monochrome_data)),
+ (PyDMColorMap.Hot, np.array(_hot_data)),
+):
cmaps[name] = data
# pyqtgraph > 0.10.0 does not take normalized floating point values as in
diff --git a/pydm/widgets/datetime.py b/pydm/widgets/datetime.py
index c2f468c8f2..fdc52af142 100644
--- a/pydm/widgets/datetime.py
+++ b/pydm/widgets/datetime.py
@@ -25,6 +25,7 @@ class PyDMDateTimeEdit(QtWidgets.QDateTimeEdit, PyDMWritableWidget, TimeBase):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
self._block_past_date = True
self._relative = True
@@ -80,7 +81,7 @@ def send_value(self):
val = self.dateTime()
now = QtCore.QDateTime.currentDateTime()
if self._block_past_date and val < now:
- logger.error('Selected date cannot be lower than current date.')
+ logger.error("Selected date cannot be lower than current date.")
return
if self.relative:
diff --git a/pydm/widgets/designer_settings.py b/pydm/widgets/designer_settings.py
index 5e6f612941..855d72bc34 100644
--- a/pydm/widgets/designer_settings.py
+++ b/pydm/widgets/designer_settings.py
@@ -37,6 +37,7 @@ def _context_menu(self, pos):
self.menu = QtWidgets.QMenu(self)
item = self.itemAt(pos)
if item is not None:
+
def copy(*_):
copy_to_clipboard(item.text())
@@ -48,9 +49,7 @@ def copy(*_):
def paste(*_):
item.setText(clipboard_text)
- paste_action = self.menu.addAction(
- f"&Paste: {clipboard_text[:100]}"
- )
+ paste_action = self.menu.addAction(f"&Paste: {clipboard_text[:100]}")
paste_action.triggered.connect(paste)
def delete_row(*_):
@@ -73,18 +72,9 @@ def add_row(*_):
@property
def dictionary(self) -> dict:
- items = [
- (self.item(row, 0), self.item(row, 1))
- for row in range(self.rowCount())
- ]
- key_value_pairs = [
- (key.text() if key else "", value.text() if value else "")
- for key, value in items
- ]
- return {
- key.strip(): value
- for key, value in key_value_pairs
- }
+ items = [(self.item(row, 0), self.item(row, 1)) for row in range(self.rowCount())]
+ key_value_pairs = [(key.text() if key else "", value.text() if value else "") for key, value in items]
+ return {key.strip(): value for key, value in key_value_pairs}
@dictionary.setter
def dictionary(self, dct):
@@ -110,10 +100,34 @@ def __init__(self, values=None, *args, parent=None, **kwargs):
self.customContextMenuRequested.connect(self._context_menu)
self.values
+ # we let user swap points by dragging one on-top of the other
+ self.setDragEnabled(True)
+ self.setAcceptDrops(True)
+ self.drag_source_row = None
+
+ def startDrag(self, event):
+ self.drag_source_row = self.currentRow()
+ # call super() since we use the default dragging functionality
+ super().startDrag(event)
+
+ def dropEvent(self, event):
+ # don't call super() here, functionality messes with our swapping!
+ source_row = self.drag_source_row
+ target_row = self.indexAt(event.pos()).row()
+ if target_row >= 0 and source_row != target_row:
+ self.swap_rows(source_row, target_row)
+
+ def swap_rows(self, row1, row2):
+ item1 = self.takeItem(row1, 0)
+ item2 = self.takeItem(row2, 0)
+ self.setItem(row1, 0, item2)
+ self.setItem(row2, 0, item1)
+
def _context_menu(self, pos):
self.menu = QtWidgets.QMenu(self)
item = self.itemAt(pos)
if item is not None:
+
def copy(*_):
copy_to_clipboard(item.text())
@@ -148,11 +162,7 @@ def add_row(*_):
@property
def values(self) -> list:
items = [self.item(row, 0) for row in range(self.rowCount())]
- return [
- item.text().strip()
- for item in items
- if item is not None
- ]
+ return [item.text().strip() for item in items if item is not None]
@values.setter
def values(self, values):
@@ -202,11 +212,7 @@ def saved_value(self) -> Optional[Any]:
def save_settings(self):
value = self.saved_value
if value is not None:
- update_property_for_widget(
- self._property_widget,
- self._property_name,
- value
- )
+ update_property_for_widget(self._property_widget, self._property_name, value)
class PropertyRuleEditor(_PropertyHelper, QtWidgets.QPushButton):
@@ -219,6 +225,7 @@ def __init__(self, *args, **kwargs):
def _open_rules_editor(self):
from .rules_editor import RulesEditor
+
self._rules_editor = RulesEditor(self._property_widget, parent=self)
self._rules_editor.exec_()
@@ -354,15 +361,18 @@ def setup_ui(self):
self.property_widgets.append(helper_widget)
buttons_layout = QtWidgets.QHBoxLayout()
- save_btn = QtWidgets.QPushButton("&Save", parent=self)
- save_btn.setAutoDefault(True)
- save_btn.setDefault(True)
- save_btn.clicked.connect(self.save_changes)
+ self.save_btn = QtWidgets.QPushButton("&Save", parent=self)
+ self.save_btn.setAutoDefault(True)
+ self.save_btn.setDefault(True)
+ self.save_btn.clicked.connect(self.save_changes)
+ self.update_btn = QtWidgets.QPushButton("&Update", parent=self)
+ self.update_btn.clicked.connect(self.save_changes)
cancel_btn = QtWidgets.QPushButton("&Cancel", parent=self)
cancel_btn.clicked.connect(self.cancel_changes)
buttons_layout.addStretch()
buttons_layout.addWidget(cancel_btn)
- buttons_layout.addWidget(save_btn)
+ buttons_layout.addWidget(self.update_btn)
+ buttons_layout.addWidget(self.save_btn)
vlayout.addLayout(buttons_layout)
@@ -378,10 +388,7 @@ def _create_helper_widgets(self, settings_form: QtWidgets.QFormLayout):
continue
prop_type = getattr(prop, "type", None)
- helper_widget_cls = self._common_attributes_.get(
- attr,
- self._type_to_widget_.get(prop_type, None)
- )
+ helper_widget_cls = self._common_attributes_.get(attr, self._type_to_widget_.get(prop_type, None))
if helper_widget_cls is not None:
helper_widget = helper_widget_cls(
property_widget=self.widget,
@@ -402,10 +409,11 @@ def save_changes(self):
"Failed to save settings for %s.%s = %r",
self.widget.objectName(),
helper._property_name,
- helper.saved_value
+ helper.saved_value,
)
- self.accept()
+ if self.sender() == self.save_btn:
+ self.accept()
@QtCore.Slot()
def cancel_changes(self):
diff --git a/pydm/widgets/display_format.py b/pydm/widgets/display_format.py
index fc7e274658..c3a2da569f 100644
--- a/pydm/widgets/display_format.py
+++ b/pydm/widgets/display_format.py
@@ -10,6 +10,7 @@
class DisplayFormat(object):
"""Display format for showing data in a PyDM widget."""
+
#: The default display format.
Default = 0
#: Show the data as a string.
@@ -56,7 +57,7 @@ def parse_value_for_display(
return ""
try:
widget_name = widget.objectName()
- except(AttributeError, TypeError):
+ except (AttributeError, TypeError):
widget_name = ""
if display_format_type == DisplayFormat.Default:
@@ -70,11 +71,12 @@ def parse_value_for_display(
warnings.simplefilter("ignore")
zeros = np.where(value == 0)[0]
if zeros.size > 0:
- value = value[:zeros[0]]
+ value = value[: zeros[0]]
r = value.tobytes().decode(string_encoding)
except Exception:
- logger.error("Could not decode {0} using {1} at widget named '{2}'.".format(
- value, string_encoding, widget_name))
+ logger.error(
+ "Could not decode {0} using {1} at widget named '{2}'.".format(value, string_encoding, widget_name)
+ )
return value
return r
else:
@@ -88,23 +90,29 @@ def parse_value_for_display(
try:
r = fmt_string.format(value)
except (ValueError, TypeError):
- logger.error("Could not display value '{0}' using displayFormat 'Exponential' at widget named "
- "'{1}'.".format(value, widget_name))
+ logger.error(
+ "Could not display value '{0}' using displayFormat 'Exponential' at widget named "
+ "'{1}'.".format(value, widget_name)
+ )
r = value
return r
elif display_format_type == DisplayFormat.Hex:
try:
r = hex(int(math.floor(value)))
except (ValueError, TypeError):
- logger.error("Could not display value '{0}' using displayFormat 'Hex' at widget named "
- "'{1}'.".format(value, widget_name))
+ logger.error(
+ "Could not display value '{0}' using displayFormat 'Hex' at widget named "
+ "'{1}'.".format(value, widget_name)
+ )
r = value
return r
elif display_format_type == DisplayFormat.Binary:
try:
r = bin(int(math.floor(value)))
except (ValueError, TypeError):
- logger.error("Could not display value '{0}' using displayFormat 'Binary' at widget named "
- "'{1}'.".format(value, widget_name))
+ logger.error(
+ "Could not display value '{0}' using displayFormat 'Binary' at widget named "
+ "'{1}'.".format(value, widget_name)
+ )
r = value
return r
diff --git a/pydm/widgets/drawing.py b/pydm/widgets/drawing.py
index 668264cbf8..b22bd20d9f 100644
--- a/pydm/widgets/drawing.py
+++ b/pydm/widgets/drawing.py
@@ -3,9 +3,8 @@
import os
import logging
-from qtpy.QtWidgets import (QWidget, QStyle, QStyleOption)
-from qtpy.QtGui import (QColor, QPainter, QBrush, QPen, QPolygon, QPolygonF, QPixmap,
- QMovie)
+from qtpy.QtWidgets import QWidget, QStyle, QStyleOption
+from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QPolygonF, QPixmap, QMovie
from qtpy.QtCore import Property, Qt, QPoint, QPointF, QSize, Slot, QTimer, QRectF
from qtpy.QtDesigner import QDesignerFormWindowInterface
from .base import PyDMWidget
@@ -64,6 +63,7 @@ class PyDMDrawing(QWidget, PyDMWidget):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
self._rotation = 0.0
self._brush = QBrush(Qt.SolidPattern)
@@ -222,7 +222,7 @@ def get_inner_max(self):
logger.error("Invalid height. The value must be greater than {0}".format(origHeight))
return
- if (origWidth <= origHeight):
+ if origWidth <= origHeight:
w0 = origWidth
h0 = origHeight
else:
@@ -236,7 +236,7 @@ def get_inner_max(self):
c = w0 / (h0 * math.sin(ang) + w0 * math.cos(ang))
w = 0
h = 0
- if (origWidth <= origHeight):
+ if origWidth <= origHeight:
w = w0 * c
h = h0 * c
else:
@@ -478,7 +478,7 @@ def _arrow_points(startpoint, endpoint, height, width):
diff_x = startpoint.x() - endpoint.x()
diff_y = startpoint.y() - endpoint.y()
- length = math.sqrt(diff_x ** 2 + diff_y ** 2)
+ length = max(math.sqrt(diff_x**2 + diff_y**2), 1.0)
norm_x = diff_x / length
norm_y = diff_y / length
@@ -519,7 +519,7 @@ def draw_item(self, painter):
# Find the angle of the rop right corner of the bounding box
try:
- critical_angle = math.atan(h/w)
+ critical_angle = math.atan(h / w)
except ZeroDivisionError:
critical_angle = math.pi / 2
@@ -537,9 +537,9 @@ def draw_item(self, painter):
# Define endpoints potentially outside the bounding box
# Will land on the bounding box after rotation
- midpoint = x + w/2
- start_point = QPointF(midpoint - length/2, 0)
- end_point = QPointF(midpoint + length/2, 0)
+ midpoint = x + w / 2
+ start_point = QPointF(midpoint - length / 2, 0)
+ end_point = QPointF(midpoint + length / 2, 0)
mid_point = QPointF(midpoint, 0)
# Draw the line
@@ -646,7 +646,7 @@ def flipMidPointArrow(self):
def flipMidPointArrow(self, new_selection):
"""
Flips the direction of the midpoint arrow.
-
+
Parameters
-------
new_selection : bool
@@ -655,6 +655,7 @@ def flipMidPointArrow(self, new_selection):
self._mid_point_arrow_flipped = new_selection
self.update()
+
class PyDMDrawingImage(PyDMDrawing):
"""
Renders an image given by the ``filename`` property.
@@ -672,6 +673,7 @@ class PyDMDrawingImage(PyDMDrawing):
null_color : Qt.Color
QColor to fill the image if the filename is not found.
"""
+
null_color = Qt.gray
def __init__(self, parent=None, init_channel=None, filename=""):
@@ -822,15 +824,14 @@ def draw_item(self, painter):
super(PyDMDrawingImage, self).draw_item(painter)
x, y, w, h = self.get_bounds(maxsize=True, force_no_pen=True)
if not isinstance(self._pixmap, QMovie):
- _scaled = self._pixmap.scaled(int(w), int(h), self._aspect_ratio_mode,
- Qt.SmoothTransformation)
+ _scaled = self._pixmap.scaled(int(w), int(h), self._aspect_ratio_mode, Qt.SmoothTransformation)
# Make sure the image is centered if smaller than the widget itself
if w > _scaled.width():
logger.debug("Centering image horizontally ...")
- x += (w-_scaled.width())/2
+ x += (w - _scaled.width()) / 2
if h > _scaled.height():
logger.debug("Centering image vertically ...")
- y += (h - _scaled.height())/2
+ y += (h - _scaled.height()) / 2
painter.drawPixmap(QPointF(x, y), _scaled)
def movie_frame_changed(self, frame_no):
@@ -879,6 +880,7 @@ class PyDMDrawingRectangle(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingRectangle, self).__init__(parent, init_channel)
@@ -904,15 +906,12 @@ class PyDMDrawingTriangle(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingTriangle, self).__init__(parent, init_channel)
def _calculate_drawing_points(self, x, y, w, h):
- return [
- QPointF(x, h / 2.0),
- QPointF(x, y),
- QPointF(w / 2.0, y)
- ]
+ return [QPointF(x, h / 2.0), QPointF(x, y), QPointF(w / 2.0, y)]
def draw_item(self, painter):
"""
@@ -938,6 +937,7 @@ class PyDMDrawingEllipse(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingEllipse, self).__init__(parent, init_channel)
@@ -964,6 +964,7 @@ class PyDMDrawingCircle(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingCircle, self).__init__(parent, init_channel)
@@ -993,6 +994,7 @@ class PyDMDrawingArc(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingArc, self).__init__(parent, init_channel)
self.penStyle = Qt.SolidLine
@@ -1075,6 +1077,7 @@ class PyDMDrawingPie(PyDMDrawingArc):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPie, self).__init__(parent, init_channel)
@@ -1101,6 +1104,7 @@ class PyDMDrawingChord(PyDMDrawingArc):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingChord, self).__init__(parent, init_channel)
@@ -1127,6 +1131,7 @@ class PyDMDrawingPolygon(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPolygon, self).__init__(parent, init_channel)
self._num_points = 3
@@ -1150,9 +1155,9 @@ def numberOfPoints(self, points):
self.update()
def _calculate_drawing_points(self, x, y, w, h):
- #(x + r*cos(theta), y + r*sin(theta))
- r = min(w, h)/2.0
- deg_step = 360.0/self._num_points
+ # (x + r*cos(theta), y + r*sin(theta))
+ r = min(w, h) / 2.0
+ deg_step = 360.0 / self._num_points
points = []
for i in range(self._num_points):
@@ -1168,7 +1173,7 @@ def draw_item(self, painter):
```PyDMDrawing.draw_item```.
"""
super(PyDMDrawingPolygon, self).draw_item(painter)
- maxsize = not self.is_square()
+ not self.is_square()
x, y, w, h = self.get_bounds(maxsize=not self.is_square())
poly = self._calculate_drawing_points(x, y, w, h)
painter.drawPolygon(QPolygonF(poly))
@@ -1186,8 +1191,13 @@ class PyDMDrawingPolyline(PyDMDrawing):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
super(PyDMDrawingPolyline, self).__init__(parent, init_channel)
+ self._arrow_end_point_selection = False
+ self._arrow_start_point_selection = False
+ self._arrow_mid_point_selection = False
+ self._arrow_mid_point_flipped = False
self.penStyle = Qt.SolidLine
self.penWidth = 1
self._points = []
@@ -1210,18 +1220,38 @@ def p2d(pt):
# adl2pydm tags up to 0.0.2
pt = tuple(map(int, pt.split(",")))
u, v = pt
- return QPointF(u+x, v+y)
+ return QPointF(u + x, v + y)
if len(self._points) > 1:
for i, p1 in enumerate(self._points[:-1]):
- painter.drawLine(p2d(p1), p2d(self._points[i+1]))
+ painter.drawLine(p2d(p1), p2d(self._points[i + 1]))
+ if self._arrow_mid_point_selection:
+ point1 = p2d(p1)
+ point2 = p2d(self._points[i + 1])
+ if self._arrow_mid_point_flipped:
+ point1, point2 = point2, point1 # swap values
+
+ # arrow points at midpoint of line
+ midpoint_x = (point1.x() + point2.x()) / 2
+ midpoint_y = (point1.y() + point2.y()) / 2
+ midpoint = QPointF(midpoint_x, midpoint_y)
+ points = PyDMDrawingLine._arrow_points(point1, midpoint, 6, 6) # 6 = arbitrary arrow size
+ painter.drawPolygon(points)
+
+ # Draw the arrows
+ if self._arrow_end_point_selection and (len(self._points[1]) >= 2):
+ points = PyDMDrawingLine._arrow_points(p2d(self._points[1]), p2d(self._points[0]), 6, 6)
+ painter.drawPolygon(points)
+
+ if self._arrow_start_point_selection and (len(self._points[1]) >= 2):
+ points = PyDMDrawingLine._arrow_points(
+ p2d(self._points[len(self._points) - 2]), p2d(self._points[len(self._points) - 1]), 6, 6
+ )
+ painter.drawPolygon(points)
def getPoints(self):
"""Convert internal points representation for use as QStringList."""
- points = [
- f"{pt[0]}, {pt[1]}"
- for pt in self._points
- ]
+ points = [f"{pt[0]}, {pt[1]}" for pt in self._points]
return points
def _validator(self, value):
@@ -1241,13 +1271,14 @@ def _validator(self, value):
List of `tuple(number, number)`.
"""
+
def isfloat(value):
if isinstance(value, str):
value = value.strip()
try:
float(value)
return True
- except:
+ except Exception:
return False
def validate_point(i, point):
@@ -1291,6 +1322,100 @@ def resetPoints(self):
self._points = []
self.update()
+ @Property(bool)
+ def arrowEndPoint(self):
+ """
+ If True, an arrow will be drawn at the start of the last polyline segment.
+
+ Returns
+ -------
+ bool
+ """
+ return self._arrow_end_point_selection
+
+ @arrowEndPoint.setter
+ def arrowEndPoint(self, new_selection):
+ """
+ If True, an arrow will be drawn at the start of the last polyline segment.
+
+ Parameters
+ -------
+ new_selection : bool
+ """
+ if self._arrow_end_point_selection != new_selection:
+ self._arrow_end_point_selection = new_selection
+ self.update()
+
+ @Property(bool)
+ def arrowStartPoint(self):
+ """
+ If True, an arrow will be drawn at the start of the first polyline segment.
+
+ Returns
+ -------
+ bool
+ """
+ return self._arrow_start_point_selection
+
+ @arrowStartPoint.setter
+ def arrowStartPoint(self, new_selection):
+ """
+ If True, an arrow will be drawn at the start of the first polyline segment.
+
+ Parameters
+ -------
+ new_selection : bool
+ """
+ if self._arrow_start_point_selection != new_selection:
+ self._arrow_start_point_selection = new_selection
+ self.update()
+
+ @Property(bool)
+ def arrowMidPoint(self):
+ """
+ If True, an arrows will be drawn at the midpoints of the segments of the polyline.
+ Returns
+ -------
+ bool
+ """
+ return self._arrow_mid_point_selection
+
+ @arrowMidPoint.setter
+ def arrowMidPoint(self, new_selection):
+ """
+ If True, an arrows will be drawn at the midpoints of the segments of the polyline.
+ Parameters
+ -------
+ new_selection : bool
+ """
+ if self._arrow_mid_point_selection != new_selection:
+ self._arrow_mid_point_selection = new_selection
+ self.update()
+
+ @Property(bool)
+ def flipMidPointArrow(self):
+ """
+ Flips the direction of the midpoint arrows.
+
+ Returns
+ -------
+ bool
+ """
+ return self._arrow_mid_point_flipped
+
+ @flipMidPointArrow.setter
+ def flipMidPointArrow(self, new_selection):
+ """
+ Flips the direction of the midpoint arrows.
+
+ Parameters
+ -------
+ new_selection : bool
+ """
+ if self._arrow_mid_point_flipped != new_selection:
+ self._arrow_mid_point_flipped = new_selection
+ self.update()
+
points = Property("QStringList", getPoints, setPoints, resetPoints)
diff --git a/pydm/widgets/embedded_display.py b/pydm/widgets/embedded_display.py
index 32f597ec18..a1d1713c95 100644
--- a/pydm/widgets/embedded_display.py
+++ b/pydm/widgets/embedded_display.py
@@ -1,5 +1,5 @@
from pyqtgraph.GraphicsScene.mouseEvents import MouseClickEvent
-from qtpy.QtWidgets import QAction, QFrame, QApplication, QLabel, QMenu, QVBoxLayout, QWidget
+from qtpy.QtWidgets import QAction, QFrame, QApplication, QLabel, QMenu, QVBoxLayout
from qtpy.QtCore import QPoint, Qt, QSize, Property, QTimer
import copy
@@ -7,14 +7,20 @@
import logging
from .base import PyDMPrimitiveWidget
from .baseplot import BasePlot
-from ..utilities import (is_pydm_app, establish_widget_connections,
- close_widget_connections, macro, is_qt_designer,
- find_file)
-from ..display import (load_file, ScreenTarget)
+from ..utilities import (
+ is_pydm_app,
+ establish_widget_connections,
+ close_widget_connections,
+ macro,
+ is_qt_designer,
+ find_file,
+)
+from ..display import load_file, ScreenTarget
logger = logging.getLogger(__name__)
-_embeddedDisplayRuleProperties = {'Filename': ['filename', str]}
+_embeddedDisplayRuleProperties = {"Filename": ["filename", str]}
+
class PyDMEmbeddedDisplay(QFrame, PyDMPrimitiveWidget, new_properties=_embeddedDisplayRuleProperties):
"""
@@ -43,7 +49,7 @@ def __init__(self, parent=None):
self._follow_symlinks = False
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
- self.open_in_new_window_action = QAction('Open in New Window', self)
+ self.open_in_new_window_action = QAction("Open in New Window", self)
self.open_in_new_window_action.triggered.connect(self.open_display_in_new_window)
self.layout = QVBoxLayout(self)
@@ -194,8 +200,7 @@ def parsed_macros(self):
return parent_macros
def load_if_needed(self):
- if self._needs_load and (
- not self._only_load_when_shown or self.isVisible() or is_qt_designer()):
+ if self._needs_load and (not self._only_load_when_shown or self.isVisible() or is_qt_designer()):
self.embedded_widget = self.open_file()
def open_file(self, force=False):
@@ -208,7 +213,7 @@ def open_file(self, force=False):
"""
if (not force) and (not self._needs_load):
return
-
+
if not self.filename:
return
@@ -244,9 +249,7 @@ def clear_error_text(self):
self.err_label.hide()
def display_error_text(self, e):
- self.err_label.setText(
- "Could not open {filename}.\nError: {err}".format(
- filename=self._filename, err=e))
+ self.err_label.setText("Could not open {filename}.\nError: {err}".format(filename=self._filename, err=e))
self.err_label.show()
@property
@@ -269,7 +272,6 @@ def embedded_widget(self, new_widget):
----------
new_widget : QWidget
"""
- should_reconnect = False
if new_widget is self._embedded_widget:
return
if self._embedded_widget is not None:
@@ -312,16 +314,16 @@ def loadWhenShown(self):
if you have many different PyDMEmbeddedWidgets in different tabs of a
QTabBar or PyDMTabBar: only the tab that the user is looking at will
be loaded, which can greatly speed up the launch time of a display.
-
+
If this property is changed from 'True' to 'False', and the file has
not been loaded yet, it will be loaded immediately.
-
+
Returns
-------
bool
"""
return self._only_load_when_shown
-
+
@loadWhenShown.setter
def loadWhenShown(self, val):
self._only_load_when_shown = val
@@ -352,8 +354,9 @@ def disconnectWhenHidden(self, disconnect_when_hidden):
@Property(bool)
def followSymlinks(self) -> bool:
"""
- If True, any symlinks in the path to filename (including the base path of the parent display) will be followed, so that it
- will always use the canonical path. If False (default), the file will be searched without canonicalizing the path beforehand.
+ If True, any symlinks in the path to filename (including the base path of the parent display)
+ will be followed, so that it will always use the canonical path. If False (default),
+ the file will be searched without canonicalizing the path beforehand.
Note that it will not work on Windows if you're using a Python version prior to 3.8.
@@ -366,8 +369,9 @@ def followSymlinks(self) -> bool:
@followSymlinks.setter
def followSymlinks(self, follow_symlinks: bool) -> None:
"""
- If True, any symlinks in the path to filename (including the base path of the parent display) will be followed, so that it
- will always use the canonical path. If False (default), the file will be searched using the non-canonical path.
+ If True, any symlinks in the path to filename (including the base path of the parent display)
+ will be followed, so that it will always use the canonical path.
+ If False (default), the file will be searched using the non-canonical path.
Note that it will not work on Windows if you're using a Python version prior to 3.8.
@@ -410,19 +414,19 @@ def _display_designer_load_error(self):
self.display_error_text(self._load_error)
def open_display_in_new_window(self) -> None:
- """ Open the embedded display in a new window """
+ """Open the embedded display in a new window"""
if not self.filename:
return
- file_path = find_file(self.filename, base_path='', raise_if_not_found=True)
+ file_path = find_file(self.filename, base_path="", raise_if_not_found=True)
macros = self.parsed_macros()
if is_pydm_app():
load_file(file_path, macros=macros)
else:
- w = load_file(file_path, macros=macros, target=ScreenTarget.DIALOG)
+ load_file(file_path, macros=macros, target=ScreenTarget.DIALOG)
def create_context_menu(self, pos: QPoint) -> QMenu:
- """ Create the right-click context menu for this embedded widget based on the location of the mouse click """
+ """Create the right-click context menu for this embedded widget based on the location of the mouse click"""
if self._embedded_widget is None:
return
@@ -458,7 +462,7 @@ def create_context_menu(self, pos: QPoint) -> QMenu:
return menu
def show_context_menu(self, pos: QPoint) -> None:
- """ Display the right-click context menu for this embedded widget at the location of the mouse click """
+ """Display the right-click context menu for this embedded widget at the location of the mouse click"""
menu = self.create_context_menu(pos)
if menu is not None:
menu.exec_(self.mapToGlobal(pos))
diff --git a/pydm/widgets/enum_button.py b/pydm/widgets/enum_button.py
index f6e1f7a7fb..ef806cf3ce 100644
--- a/pydm/widgets/enum_button.py
+++ b/pydm/widgets/enum_button.py
@@ -1,9 +1,8 @@
import logging
-from qtpy.QtCore import (Qt, QSize, Property, Slot, Q_ENUMS, QMargins)
+from qtpy.QtCore import Qt, QSize, Property, Slot, Q_ENUMS, QMargins
from qtpy.QtGui import QPainter
-from qtpy.QtWidgets import (QWidget, QButtonGroup, QGridLayout, QPushButton,
- QRadioButton, QStyleOption, QStyle)
+from qtpy.QtWidgets import QWidget, QButtonGroup, QGridLayout, QPushButton, QRadioButton, QStyleOption, QStyle
from .base import PyDMWritableWidget
from .. import data_plugins
@@ -18,6 +17,7 @@ class WidgetType(object):
logger = logging.getLogger(__name__)
+
class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType):
"""
A QWidget that renders buttons for every option of Enum Items.
@@ -37,6 +37,7 @@ class PyDMEnumButton(QWidget, PyDMWritableWidget, WidgetType):
send_value_signal : int, float, str, bool or np.ndarray
Emitted when the user changes the value.
"""
+
Q_ENUMS(WidgetType)
WidgetType = WidgetType
@@ -139,9 +140,9 @@ def customOrder(self):
def customOrder(self, value):
if value != self._custom_order:
try:
- v = [int(v) for v in value]
+ [int(v) for v in value]
except ValueError:
- logger.error('customOrder values can only be integers.')
+ logger.error("customOrder values can only be integers.")
return
self._custom_order = value
if self.useCustomOrder and self._has_enums:
@@ -435,8 +436,11 @@ def rebuild_layout(self):
except IndexError:
if self._has_enums:
logger.error(
- 'Invalid index for PyDMEnumButton %s. Index: %s, Range: 0 to %s',
- self.objectName(), idx, len(self._widgets) - 1)
+ "Invalid index for PyDMEnumButton %s. Index: %s, Range: 0 to %s",
+ self.objectName(),
+ idx,
+ len(self._widgets) - 1,
+ )
continue
if self.orientation == Qt.Vertical:
self.layout().addWidget(widget, i, 0)
@@ -491,8 +495,7 @@ def enum_strings_changed(self, new_enum_strings):
new_enum_strings : tuple
The new list of values
"""
- if new_enum_strings is not None \
- and new_enum_strings != self.enum_strings:
+ if new_enum_strings is not None and new_enum_strings != self.enum_strings:
super(PyDMEnumButton, self).enum_strings_changed(new_enum_strings)
self._has_enums = True
self.check_enable_state()
diff --git a/pydm/widgets/enum_combo_box.py b/pydm/widgets/enum_combo_box.py
index 6d63e37426..9101ff8e9c 100644
--- a/pydm/widgets/enum_combo_box.py
+++ b/pydm/widgets/enum_combo_box.py
@@ -1,12 +1,12 @@
import logging
-logger = logging.getLogger(__name__)
-
import six
from qtpy.QtWidgets import QComboBox
from qtpy.QtCore import Slot, Qt
from .base import PyDMWritableWidget
from .. import data_plugins
+logger = logging.getLogger(__name__)
+
class PyDMEnumComboBox(QComboBox, PyDMWritableWidget):
"""
@@ -31,6 +31,7 @@ class PyDMEnumComboBox(QComboBox, PyDMWritableWidget):
Emitted when an item in the combobox popup list is highlighted
by the user.
"""
+
def __init__(self, parent=None, init_channel=None):
QComboBox.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
@@ -45,7 +46,7 @@ def __init__(self, parent=None, init_channel=None):
self._new_item_added = False
def wheelEvent(self, e):
- #To ignore mouse wheel events
+ # To ignore mouse wheel events
e.ignore()
return
@@ -82,7 +83,7 @@ def setItemText(self, index, text):
super(PyDMEnumComboBox, self).enum_strings_changed(tuple(self.itemText(i) for i in range(self.count())))
self._has_enums = True
self.check_enable_state()
-
+
# Internal methods
def set_items(self, enums):
"""
@@ -95,8 +96,9 @@ def set_items(self, enums):
The new list of values
"""
if not enums:
- logger.error("Invalid enum value '{0}'. The value is expected to be a valid list of string values."
- .format(enums))
+ logger.error(
+ "Invalid enum value '{0}'. The value is expected to be a valid list of string values.".format(enums)
+ )
return
self.clear()
@@ -109,7 +111,8 @@ def set_items(self, enums):
self.addItem(e_str)
except TypeError as error:
logger.error(
- "Invalid enum type '{0}'. The expected type is 'string'. Exception: {1}".format(type(e_str), error))
+ "Invalid enum type '{0}'. The expected type is 'string'. Exception: {1}".format(type(e_str), error)
+ )
self._has_enums = True
self.check_enable_state()
@@ -125,7 +128,7 @@ def check_enable_state(self):
if not self._connected:
tooltip += "PV is disconnected."
if self.channel:
- tooltip += '\n'
+ tooltip += "\n"
tooltip += self.get_address()
elif not self._write_access:
if data_plugins.is_read_only():
@@ -173,14 +176,11 @@ def value_changed(self, new_val):
# findText return -1 when we can not find the text inside the
# QComboBox
if idx == -1:
- logger.error("Can not change value to %r. "
- "Not an option in PyDMComboBox",
- new_val)
+ logger.error("Can not change value to %r. " "Not an option in PyDMComboBox", new_val)
return
# Handle bool, float, and ndarray
else:
- logger.error("Invalid type for PyDMComboBox %s",
- type(new_val))
+ logger.error("Invalid type for PyDMComboBox %s", type(new_val))
return
# Set the index
self.setCurrentIndex(idx)
diff --git a/pydm/widgets/eventplot.py b/pydm/widgets/eventplot.py
index f1320e3a6c..c09157151d 100644
--- a/pydm/widgets/eventplot.py
+++ b/pydm/widgets/eventplot.py
@@ -6,7 +6,6 @@
from qtpy.QtCore import Slot, Property, Qt
from .baseplot import BasePlot, NoDataError, BasePlotCurveItem
from .channel import PyDMChannel
-from ..utilities import remove_protocol
DEFAULT_BUFFER_SIZE = 1200
@@ -14,7 +13,7 @@
class EventPlotCurveItem(BasePlotCurveItem):
- _channels = ('channel')
+ _channels = "channel"
def __init__(self, addr, y_idx, x_idx, bufferSizeChannelAddress=None, **kws):
self.channel = None
@@ -22,18 +21,17 @@ def __init__(self, addr, y_idx, x_idx, bufferSizeChannelAddress=None, **kws):
self.x_idx = x_idx
self.y_idx = y_idx
self.connected = False
- if kws.get('name') is None:
- kws['name'] = ""
+ if kws.get("name") is None:
+ kws["name"] = ""
self.bufferSizeChannel = None
self.bufferSizeChannel_connected = False
self._bufferSize = DEFAULT_BUFFER_SIZE
- self.data_buffer = np.zeros((2, self._bufferSize),
- order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
self.points_accumulated = 0
- if 'symbol' not in kws.keys():
- kws['symbol'] = 'o'
- if 'lineStyle' not in kws.keys():
- kws['lineStyle'] = Qt.NoPen
+ if "symbol" not in kws.keys():
+ kws["symbol"] = "o"
+ if "lineStyle" not in kws.keys():
+ kws["lineStyle"] = Qt.NoPen
super(EventPlotCurveItem, self).__init__(**kws)
self.bufferSizeChannelAddress = bufferSizeChannelAddress
@@ -47,11 +45,9 @@ def to_dict(self):
Representation with values for all properties
needed to recreate this curve.
"""
- dic_ = OrderedDict([("channel", self.address),
- ("y_idx", self.y_idx),
- ("x_idx", self.x_idx)])
+ dic_ = OrderedDict([("channel", self.address), ("y_idx", self.y_idx), ("x_idx", self.x_idx)])
dic_.update(super(EventPlotCurveItem, self).to_dict())
- dic_['buffer_size'] = self.getBufferSize()
+ dic_["buffer_size"] = self.getBufferSize()
dic_["bufferSizeChannelAddress"] = self.bufferSizeChannelAddress
return dic_
@@ -82,9 +78,8 @@ def address(self, new_address):
self.channel = None
return
self.channel = PyDMChannel(
- address=new_address,
- connection_slot=self.connectionStateChanged,
- value_slot=self.receiveValue)
+ address=new_address, connection_slot=self.connectionStateChanged, value_slot=self.receiveValue
+ )
@Slot(bool)
def connectionStateChanged(self, connected):
@@ -106,8 +101,8 @@ def receiveValue(self, new_data):
if self.x_idx is None or self.y_idx is None:
return
if not isinstance(self.x_idx, int) or not isinstance(self.y_idx, int):
- """ The x_idx and y_idx typing is made this late so that macros can
- can be used alongside regular indexing. """
+ """The x_idx and y_idx typing is made this late so that macros can
+ can be used alongside regular indexing."""
self.x_idx = int(self.x_idx)
self.y_idx = int(self.y_idx)
if len(new_data) <= self.x_idx or len(new_data) <= self.y_idx:
@@ -121,8 +116,7 @@ def receiveValue(self, new_data):
def initialize_buffer(self):
self.points_accumulated = 0
- self.data_buffer = np.zeros((2, self._bufferSize),
- order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
def getBufferSize(self):
return int(self._bufferSize)
@@ -168,7 +162,8 @@ def bufferSizeChannelAddress(self, new_address):
self.bufferSizeChannel = PyDMChannel(
address=new_address,
connection_slot=self.bufferSizeConnectionStateChanged,
- value_slot=self.bufferSizeChannelValueReceiver)
+ value_slot=self.bufferSizeChannelValueReceiver,
+ )
self.bufferSizeChannel.connect()
@Slot(bool)
@@ -196,8 +191,10 @@ def redrawCurve(self):
Called by the curve's parent plot whenever the curve needs to be
re-drawn with new data.
"""
- self.setData(x=self.data_buffer[0, -self.points_accumulated:].astype(float),
- y=self.data_buffer[1, -self.points_accumulated:].astype(float))
+ self.setData(
+ x=self.data_buffer[0, -self.points_accumulated :].astype(float),
+ y=self.data_buffer[1, -self.points_accumulated :].astype(float),
+ )
def limits(self):
"""
@@ -210,10 +207,9 @@ def limits(self):
"""
if self.points_accumulated == 0:
raise NoDataError("Curve has no data, cannot determine limits.")
- x_data = self.data_buffer[0, -self.points_accumulated:]
- y_data = self.data_buffer[1, -self.points_accumulated:]
- return ((float(np.amin(x_data)), float(np.amax(x_data))),
- (float(np.amin(y_data)), float(np.amax(y_data))))
+ x_data = self.data_buffer[0, -self.points_accumulated :]
+ y_data = self.data_buffer[1, -self.points_accumulated :]
+ return ((float(np.amin(x_data)), float(np.amax(x_data))), (float(np.amin(y_data)), float(np.amax(y_data))))
def channels(self):
return [self.channel]
@@ -223,7 +219,7 @@ class PyDMEventPlot(BasePlot):
"""
PyDMEventPlot is a widget to plot one scalar value against another.
All of the values arrive in a single event-built array, and indices are
- used to identify which values to plot. Multiple scalar pairs can be
+ used to identify which values to plot. Multiple scalar pairs can be
plotted on the same plot. Each pair has a buffer which stores previous
values. All values in the buffer are drawn. The buffer size for each
pair is user configurable.
@@ -258,8 +254,8 @@ class PyDMEventPlot(BasePlot):
The background color for the plot. Accepts any arguments that
pyqtgraph.mkColor will accept.
"""
- def __init__(self, parent=None, channel=None, init_x_indices=[], init_y_indices=[],
- background='default'):
+
+ def __init__(self, parent=None, channel=None, init_x_indices=[], init_y_indices=[], background="default"):
super(PyDMEventPlot, self).__init__(parent, background)
# If the user supplies a single integer instead of a list,
# wrap it in a list.
@@ -270,19 +266,17 @@ def __init__(self, parent=None, channel=None, init_x_indices=[], init_y_indices=
if init_y_indices is None:
init_y_indices = []
if init_x_indices is None or len(init_x_indices) == 0:
- init_x_indices = list(itertools.repeat(None,
- len(init_y_indices)))
+ init_x_indices = list(itertools.repeat(None, len(init_y_indices)))
if len(init_x_indices) == 1:
- init_x_indices = init_x_indices*len(init_y_indices)
+ init_x_indices = init_x_indices * len(init_y_indices)
if len(init_x_indices) != len(init_y_indices):
- raise ValueError("If lists are provided for both X and Y " +
- "indices, they must be the same length.")
+ raise ValueError("If lists are provided for both X and Y " + "indices, they must be the same length.")
# self.index_pairs is an ordered dictionary that is keyed on a
# (x_idx, y_idx) tuple, with EventPlotCurveItem values.
# It gets populated in self.addChannel().
self.index_pairs = OrderedDict()
init_index_pairs = zip(init_x_indices, init_y_indices)
- for (x_idx, y_idx) in init_index_pairs:
+ for x_idx, y_idx in init_index_pairs:
self.addChannel(channel=channel, y_idx=y_idx, x_idx=x_idx)
self._needs_redraw = True
@@ -291,10 +285,21 @@ def initialize_for_designer(self):
# This function gets called by PyDMTimePlot's designer plugin.
pass
- def addChannel(self, channel=None, y_idx=None, x_idx=None, name=None,
- color=None, lineStyle=None, lineWidth=None,
- symbol='o', symbolSize=5, buffer_size=None,
- yAxisName=None, bufferSizeChannelAddress=None):
+ def addChannel(
+ self,
+ channel=None,
+ y_idx=None,
+ x_idx=None,
+ name=None,
+ color=None,
+ lineStyle=None,
+ lineWidth=None,
+ symbol="o",
+ symbolSize=5,
+ buffer_size=None,
+ yAxisName=None,
+ bufferSizeChannelAddress=None,
+ ):
"""
Add a new curve to the plot. In addition to the arguments below,
all other keyword arguments are passed to the underlying
@@ -331,27 +336,32 @@ def addChannel(self, channel=None, y_idx=None, x_idx=None, name=None,
doesn't yet exist
"""
plot_opts = {}
- plot_opts['symbol'] = symbol
+ plot_opts["symbol"] = symbol
if symbolSize is not None:
- plot_opts['symbolSize'] = symbolSize
+ plot_opts["symbolSize"] = symbolSize
if lineStyle is not None:
- plot_opts['lineStyle'] = lineStyle
+ plot_opts["lineStyle"] = lineStyle
if lineWidth is not None:
- plot_opts['lineWidth'] = lineWidth
- curve = EventPlotCurveItem(addr=channel,
- y_idx=y_idx,
- x_idx=x_idx,
- name=name,
- color=color,
- yAxisName=yAxisName,
- bufferSizeChannelAddress=bufferSizeChannelAddress,
- **plot_opts)
+ plot_opts["lineWidth"] = lineWidth
+ curve = self.createCurveItem(
+ addr=channel,
+ y_idx=y_idx,
+ x_idx=x_idx,
+ name=name,
+ color=color,
+ yAxisName=yAxisName,
+ bufferSizeChannelAddress=bufferSizeChannelAddress,
+ **plot_opts
+ )
if buffer_size is not None:
curve.setBufferSize(buffer_size)
self.index_pairs[(x_idx, y_idx)] = curve
self.addCurve(curve, curve_color=color, y_axis_name=yAxisName)
curve.data_changed.connect(self.set_needs_redraw)
+ def createCurveItem(self, *args, **kwargs):
+ return EventPlotCurveItem(*args, *kwargs)
+
def removeChannel(self, curve):
"""
Remove a curve from the plot.
@@ -423,19 +433,23 @@ def setCurves(self, new_list):
return
self.clearCurves()
for d in new_list:
- color = d.get('color')
+ color = d.get("color")
if color:
color = QColor(color)
- self.addChannel(channel=d['channel'],
- y_idx=d['y_idx'], x_idx=d['x_idx'],
- name=d.get('name'), color=color,
- lineStyle=d.get('lineStyle'),
- lineWidth=d.get('lineWidth'),
- symbol=d.get('symbol'),
- symbolSize=d.get('symbolSize'),
- buffer_size=d.get('buffer_size'),
- bufferSizeChannelAddress=d.get('bufferSizeChannelAddress'),
- yAxisName=d.get('yAxisName'))
+ self.addChannel(
+ channel=d["channel"],
+ y_idx=d["y_idx"],
+ x_idx=d["x_idx"],
+ name=d.get("name"),
+ color=color,
+ lineStyle=d.get("lineStyle"),
+ lineWidth=d.get("lineWidth"),
+ symbol=d.get("symbol"),
+ symbolSize=d.get("symbolSize"),
+ buffer_size=d.get("buffer_size"),
+ bufferSizeChannelAddress=d.get("bufferSizeChannelAddress"),
+ yAxisName=d.get("yAxisName"),
+ )
curves = Property("QStringList", getCurves, setCurves, designable=False)
@@ -449,39 +463,61 @@ def channels(self):
"""
chans = []
chans.extend([curve.channel for curve in self._curves])
- chans.extend([curve.bufferSizeChannel
- for curve in self._curves
- if curve.bufferSizeChannel is not None])
+ chans.extend([curve.bufferSizeChannel for curve in self._curves if curve.bufferSizeChannel is not None])
return chans
# The methods for autoRangeX, minXRange, maxXRange, autoRangeY, minYRange,
# and maxYRange are all defined in BasePlot, but we don't expose them as
# properties there, because not all plot subclasses necessarily want
# them to be user-configurable in Designer.
- autoRangeX = Property(bool, BasePlot.getAutoRangeX,
- BasePlot.setAutoRangeX, BasePlot.resetAutoRangeX,
- doc="""
+ autoRangeX = Property(
+ bool,
+ BasePlot.getAutoRangeX,
+ BasePlot.setAutoRangeX,
+ BasePlot.resetAutoRangeX,
+ doc="""
Whether or not the X-axis automatically rescales to fit the data.
-If true, the values in minXRange and maxXRange are ignored.""")
-
- minXRange = Property(float, BasePlot.getMinXRange,
- BasePlot.setMinXRange, doc="""
-Minimum X-axis value visible on the plot.""")
-
- maxXRange = Property(float, BasePlot.getMaxXRange,
- BasePlot.setMaxXRange, doc="""
-Maximum X-axis value visible on the plot.""")
-
- autoRangeY = Property(bool, BasePlot.getAutoRangeY,
- BasePlot.setAutoRangeY, BasePlot.resetAutoRangeY,
- doc="""
+If true, the values in minXRange and maxXRange are ignored.""",
+ )
+
+ minXRange = Property(
+ float,
+ BasePlot.getMinXRange,
+ BasePlot.setMinXRange,
+ doc="""
+Minimum X-axis value visible on the plot.""",
+ )
+
+ maxXRange = Property(
+ float,
+ BasePlot.getMaxXRange,
+ BasePlot.setMaxXRange,
+ doc="""
+Maximum X-axis value visible on the plot.""",
+ )
+
+ autoRangeY = Property(
+ bool,
+ BasePlot.getAutoRangeY,
+ BasePlot.setAutoRangeY,
+ BasePlot.resetAutoRangeY,
+ doc="""
Whether or not the Y-axis automatically rescales to fit the data.
-If true, the values in minYRange and maxYRange are ignored.""")
-
- minYRange = Property(float, BasePlot.getMinYRange,
- BasePlot.setMinYRange, doc="""
-Minimum Y-axis value visible on the plot.""")
-
- maxYRange = Property(float, BasePlot.getMaxYRange,
- BasePlot.setMaxYRange, doc="""
-Maximum Y-axis value visible on the plot.""")
+If true, the values in minYRange and maxYRange are ignored.""",
+ )
+
+ minYRange = Property(
+ float,
+ BasePlot.getMinYRange,
+ BasePlot.setMinYRange,
+ doc="""
+Minimum Y-axis value visible on the plot.""",
+ )
+
+ maxYRange = Property(
+ float,
+ BasePlot.getMaxYRange,
+ BasePlot.setMaxYRange,
+ doc="""
+Maximum Y-axis value visible on the plot.""",
+ )
diff --git a/pydm/widgets/eventplot_curve_editor.py b/pydm/widgets/eventplot_curve_editor.py
index 7d5df6012d..683ace5c6d 100644
--- a/pydm/widgets/eventplot_curve_editor.py
+++ b/pydm/widgets/eventplot_curve_editor.py
@@ -1,18 +1,18 @@
from qtpy.QtCore import QModelIndex, QVariant
from .baseplot_table_model import BasePlotCurvesModel
-from .baseplot_curve_editor import BasePlotCurveEditorDialog, PlotStyleColumnDelegate, RedrawModeColumnDelegate
+from .baseplot_curve_editor import BasePlotCurveEditorDialog, PlotStyleColumnDelegate
class PyDMEventPlotCurvesModel(BasePlotCurvesModel):
- """ This is the data model used by the waveform plot curve editor.
+ """This is the data model used by the waveform plot curve editor.
It basically acts as a go-between for the curves in a plot, and
QTableView items.
"""
def __init__(self, plot, parent=None):
super(PyDMEventPlotCurvesModel, self).__init__(plot, parent=parent)
- self._column_names = ('Channel', 'Y Index', 'X Index') + self._column_names
- self._column_names += ('Buffer Size', 'Buffer Size Channel')
+ self._column_names = ("Channel", "Y Index", "X Index") + self._column_names
+ self._column_names += ("Buffer Size", "Buffer Size Channel")
def get_data(self, column_name, curve):
if column_name == "Channel":
@@ -31,8 +31,7 @@ def get_data(self, column_name, curve):
return curve.getBufferSize()
elif column_name == "Buffer Size Channel":
return curve.bufferSizeChannelAddress or ""
- return super(PyDMEventPlotCurvesModel, self).get_data(
- column_name, curve)
+ return super(PyDMEventPlotCurvesModel, self).get_data(column_name, curve)
def set_data(self, column_name, curve, value):
if column_name == "Channel":
@@ -48,13 +47,11 @@ def set_data(self, column_name, curve, value):
value = None
curve.bufferSizeChannelAddress = str(value)
else:
- return super(PyDMEventPlotCurvesModel, self).set_data(
- column_name=column_name, curve=curve, value=value)
+ return super(PyDMEventPlotCurvesModel, self).set_data(column_name=column_name, curve=curve, value=value)
return True
def append(self, channel=None, y_idx=None, x_idx=None, name=None, color=None):
- self.beginInsertRows(QModelIndex(), len(self._plot._curves),
- len(self._plot._curves))
+ self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves))
self._plot.addChannel(channel, y_idx, x_idx, name, color)
self.endInsertRows()
@@ -72,6 +69,7 @@ class EventPlotCurveEditorDialog(BasePlotCurveEditorDialog):
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = PyDMEventPlotCurvesModel
def __init__(self, plot, parent=None):
diff --git a/pydm/widgets/frame.py b/pydm/widgets/frame.py
index 25b9b5ed74..25cf82ecd0 100644
--- a/pydm/widgets/frame.py
+++ b/pydm/widgets/frame.py
@@ -15,6 +15,7 @@ class PyDMFrame(QFrame, PyDMWidget):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
QFrame.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
diff --git a/pydm/widgets/image.py b/pydm/widgets/image.py
index 9418ff7084..4b751d77a8 100644
--- a/pydm/widgets/image.py
+++ b/pydm/widgets/image.py
@@ -4,7 +4,6 @@
from pyqtgraph import ColorMap
from pyqtgraph.graphicsItems.ViewBox.ViewBoxMenu import ViewBoxMenu
import numpy as np
-import threading
import logging
from .channel import PyDMChannel
from .colormaps import cmaps, cmap_names, PyDMColorMap
@@ -15,6 +14,7 @@
class ReadingOrder(object):
"""Class to build ReadingOrder ENUM property."""
+
Fortranlike = 0
Clike = 1
@@ -42,14 +42,13 @@ def run(self):
if image_dimensions == 1:
if width < 1:
# We don't have a width for this image yet, so we can't draw it
- logging.debug(
- "ImageUpdateThread - no width available. Aborting.")
+ logging.debug("ImageUpdateThread - no width available. Aborting.")
return
try:
if reading_order == ReadingOrder.Clike:
- img = img.reshape((-1, width), order='C')
+ img = img.reshape((-1, width), order="C")
else:
- img = img.reshape((width, -1), order='F')
+ img = img.reshape((width, -1), order="F")
except ValueError:
logger.error("Invalid width for image during reshape: %d", width)
@@ -128,14 +127,13 @@ def __init__(self, parent=None, image_channel=None, width_channel=None):
PyDMWidget.__init__(self)
self._channels = [None, None]
self.thread = None
- self.axes = dict({'t': None, "x": 0, "y": 1, "c": None})
+ self.axes = dict({"t": None, "x": 0, "y": 1, "c": None})
self.showAxes = self._show_axes
self.imageItem.setOpts(axisOrder="row-major")
# Hide some itens of the widget.
self.ui.histogram.hide()
- self.getImageItem().sigImageChanged.disconnect(
- self.ui.histogram.imageChanged)
+ self.getImageItem().sigImageChanged.disconnect(self.ui.histogram.imageChanged)
self.ui.roiBtn.hide()
self.ui.menuBtn.hide()
@@ -157,9 +155,9 @@ def __init__(self, parent=None, image_channel=None, width_channel=None):
self.newImageSignal = self.getImageItem().sigImageChanged
# Set live channels if requested on initialization
if image_channel:
- self.imageChannel = image_channel or ''
+ self.imageChannel = image_channel or ""
if width_channel:
- self.widthChannel = width_channel or ''
+ self.widthChannel = width_channel or ""
@Property(str, designable=False)
def channel(self):
@@ -397,8 +395,7 @@ def redrawImage(self):
If necessary, reshape the image to 2D first.
"""
if self.thread is not None and not self.thread.isFinished():
- logger.warning(
- "Image processing has taken longer than the refresh rate.")
+ logger.warning("Image processing has taken longer than the refresh rate.")
return
self.thread = ImageUpdateThread(self)
self.thread.updateSignal.connect(self.__updateDisplay)
@@ -411,10 +408,7 @@ def __updateDisplay(self, data):
mini, maxi = data[0], data[1]
img = data[2]
self.getImageItem().setLevels([mini, maxi])
- self.getImageItem().setImage(
- img,
- autoLevels=False,
- autoDownsample=self.autoDownsample)
+ self.getImageItem().setImage(img, autoLevels=False, autoDownsample=self.autoDownsample)
@Property(bool)
def autoDownsample(self):
@@ -463,8 +457,7 @@ def imageWidth(self, new_width):
----------
new_width: int
"""
- if (self._image_width != int(new_width) and
- (self._widthchannel is None or self._widthchannel == '')):
+ if self._image_width != int(new_width) and (self._widthchannel is None or self._widthchannel == ""):
self._image_width = int(new_width)
@Property(bool)
@@ -531,7 +524,7 @@ def imageChannel(self):
if self._imagechannel:
return str(self._imagechannel.address)
else:
- return ''
+ return ""
@imageChannel.setter
def imageChannel(self, value):
@@ -549,10 +542,11 @@ def imageChannel(self, value):
self._imagechannel.disconnect()
# Create and connect new channel
self._imagechannel = PyDMChannel(
- address=value,
- connection_slot=self.image_connection_state_changed,
- value_slot=self.image_value_changed,
- severity_slot=self.alarmSeverityChanged)
+ address=value,
+ connection_slot=self.image_connection_state_changed,
+ value_slot=self.image_value_changed,
+ severity_slot=self.alarmSeverityChanged,
+ )
self._channels[0] = self._imagechannel
self._imagechannel.connect()
@@ -569,7 +563,7 @@ def widthChannel(self):
if self._widthchannel:
return str(self._widthchannel.address)
else:
- return ''
+ return ""
@widthChannel.setter
def widthChannel(self, value):
@@ -587,14 +581,14 @@ def widthChannel(self, value):
self._widthchannel.disconnect()
# Create and connect new channel
self._widthchannel = PyDMChannel(
- address=value,
- connection_slot=self.connectionStateChanged,
- value_slot=self.image_width_changed,
- severity_slot=self.alarmSeverityChanged)
+ address=value,
+ connection_slot=self.connectionStateChanged,
+ value_slot=self.image_width_changed,
+ severity_slot=self.alarmSeverityChanged,
+ )
self._channels[1] = self._widthchannel
self._widthchannel.connect()
-
def channels(self):
"""
Return the channels being used for this Widget.
@@ -647,8 +641,8 @@ def showAxes(self):
@showAxes.setter
def showAxes(self, show):
self._show_axes = show
- self.getView().showAxis('left', show=show)
- self.getView().showAxis('bottom', show=show)
+ self.getView().showAxis("left", show=show)
+ self.getView().showAxis("bottom", show=show)
@Property(float)
def scaleXAxis(self):
@@ -659,13 +653,13 @@ def scaleXAxis(self):
xAxisScale to 1/100 = 0.01 to make the X Axis report in millimeter units.
"""
# protect against access to not yet initialized view
- if hasattr(self, 'view'):
- return self.getView().getAxis('bottom').scale
+ if hasattr(self, "view"):
+ return self.getView().getAxis("bottom").scale
return None
@scaleXAxis.setter
def scaleXAxis(self, new_scale):
- self.getView().getAxis('bottom').setScale(new_scale)
+ self.getView().getAxis("bottom").setScale(new_scale)
@Property(float)
def scaleYAxis(self):
@@ -676,10 +670,10 @@ def scaleYAxis(self):
yAxisScale to 1/100 = 0.01 to make the Y Axis report in millimeter units.
"""
# protect against access to not yet initialized view
- if hasattr(self, 'view'):
- return self.getView().getAxis('left').scale
+ if hasattr(self, "view"):
+ return self.getView().getAxis("left").scale
return None
@scaleYAxis.setter
def scaleYAxis(self, new_scale):
- self.getView().getAxis('left').setScale(new_scale)
+ self.getView().getAxis("left").setScale(new_scale)
diff --git a/pydm/widgets/label.py b/pydm/widgets/label.py
index 4c6d40224e..787018a709 100644
--- a/pydm/widgets/label.py
+++ b/pydm/widgets/label.py
@@ -6,18 +6,18 @@
from pydm import config
from pydm.widgets.base import only_if_channel_set
-_labelRuleProperties = {'Text': ['value_changed', str]}
+_labelRuleProperties = {"Text": ["value_changed", str]}
+
class PyDMLabel(QLabel, TextFormatter, PyDMWidget, DisplayFormat, new_properties=_labelRuleProperties):
- Q_ENUMS(DisplayFormat)
- DisplayFormat = DisplayFormat
"""
A QLabel with support for setting the text via a PyDM Channel, or
through the PyDM Rules system.
-
- Note: If a PyDMLabel is configured to use a Channel, and also with a rule
- which changes the 'Text' property, the behavior is undefined. Use either
- the Channel *or* a text rule, but not both.
+
+ .. note::
+ If a PyDMLabel is configured to use a Channel, and also with a rule which changes the 'Text' property,
+ the behavior is undefined. Use either
+ the Channel *or* a text rule, but not both.
Parameters
----------
@@ -27,6 +27,9 @@ class PyDMLabel(QLabel, TextFormatter, PyDMWidget, DisplayFormat, new_properties
The channel to be used by the widget.
"""
+ Q_ENUMS(DisplayFormat)
+ DisplayFormat = DisplayFormat
+
def __init__(self, parent=None, init_channel=None):
QLabel.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
@@ -57,6 +60,13 @@ def enableRichText(self, new_value):
@Property(DisplayFormat)
def displayFormat(self):
+ """
+ displayFormat property.
+
+ :getter: Returns the displayFormat
+ :setter: Sets the displayFormat
+ :type: int
+ """
return self._display_format_type
@displayFormat.setter
@@ -79,10 +89,13 @@ def value_changed(self, new_value):
The new value from the channel. The type depends on the channel.
"""
super(PyDMLabel, self).value_changed(new_value)
- new_value = parse_value_for_display(value=new_value, precision=self.precision,
- display_format_type=self._display_format_type,
- string_encoding=self._string_encoding,
- widget=self)
+ new_value = parse_value_for_display(
+ value=new_value,
+ precision=self.precision,
+ display_format_type=self._display_format_type,
+ string_encoding=self._string_encoding,
+ widget=self,
+ )
# If the value is a string, just display it as-is, no formatting
# needed.
if isinstance(new_value, str_types):
@@ -109,7 +122,7 @@ def value_changed(self, new_value):
@only_if_channel_set
def check_enable_state(self):
- """ If the channel this label is connected to becomes disconnected, display only the name of the channel. """
+ """If the channel this label is connected to becomes disconnected, display only the name of the channel."""
if not self._connected:
self.setText(self.channel)
super().check_enable_state()
diff --git a/pydm/widgets/line_edit.py b/pydm/widgets/line_edit.py
index 1e715a4fe7..545e5c2250 100755
--- a/pydm/widgets/line_edit.py
+++ b/pydm/widgets/line_edit.py
@@ -15,8 +15,6 @@
class PyDMLineEdit(QLineEdit, TextFormatter, PyDMWritableWidget, DisplayFormat):
- Q_ENUMS(DisplayFormat)
- DisplayFormat = DisplayFormat
"""
A QLineEdit (writable text field) with support for Channels and more
from PyDM.
@@ -31,6 +29,9 @@ class PyDMLineEdit(QLineEdit, TextFormatter, PyDMWritableWidget, DisplayFormat):
The channel to be used by the widget.
"""
+ Q_ENUMS(DisplayFormat)
+ DisplayFormat = DisplayFormat
+
def __init__(self, parent=None, init_channel=None):
QLineEdit.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
@@ -40,8 +41,7 @@ def __init__(self, parent=None, init_channel=None):
self._scale = 1
self.returnPressed.connect(self.send_value)
- self.unitMenu = QMenu('Convert Units', self)
- self.create_unit_options()
+ self.unitMenu = None
self._display_format_type = self.DisplayFormat.Default
self._string_encoding = "utf_8"
self._user_set_read_only = False # Are we *really* read only?
@@ -86,7 +86,7 @@ def send_value(self):
send_value = str(self.text())
# Clean text of unit string
if self._show_units and self._unit and self._unit in send_value:
- send_value = send_value[:-len(self._unit)].strip()
+ send_value = send_value[: -len(self._unit)].strip()
try:
if self.channeltype not in [str, np.ndarray, bool]:
scale = self._scale
@@ -114,7 +114,8 @@ def send_value(self):
self.send_value_signal[str].emit(send_value)
else:
arr_value = list(
- filter(None, ast.literal_eval(str(shlex.split(send_value.replace("[", "").replace("]", ""))))))
+ filter(None, ast.literal_eval(str(shlex.split(send_value.replace("[", "").replace("]", "")))))
+ )
arr_value = np.array(arr_value, dtype=self.subtype)
self.send_value_signal[np.ndarray].emit(arr_value)
elif self.channeltype == bool:
@@ -129,8 +130,11 @@ def send_value(self):
# Lets just send what we have after all
self.send_value_signal[str].emit(send_value)
except ValueError:
- logger.exception("Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'."
- .format(self.text(), self.channeltype, self._display_format_type, self.objectName()))
+ logger.exception(
+ "Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'.".format(
+ self.text(), self.channeltype, self._display_format_type, self.objectName()
+ )
+ )
self.clearFocus()
self.set_display()
@@ -157,7 +161,6 @@ def unit_changed(self, new_unit):
"""
super(PyDMLineEdit, self).unit_changed(new_unit)
self._scale = 1
- self.create_unit_options()
def create_unit_options(self):
"""
@@ -169,18 +172,17 @@ def create_unit_options(self):
:attr:`.showUnits` attribute is set to False, the menu will tell
the user that there are no available conversions
"""
- self.unitMenu.clear()
+ if self.unitMenu is None:
+ self.unitMenu = QMenu("Convert Units", self)
+ else:
+ self.unitMenu.clear()
+
units = utilities.find_unit_options(self._unit)
if units and self._show_units:
for choice in units:
- self.unitMenu.addAction(choice,
- partial(
- self.apply_conversion,
- choice
- )
- )
+ self.unitMenu.addAction(choice, partial(self.apply_conversion, choice))
else:
- self.unitMenu.addAction('No Unit Conversions found')
+ self.unitMenu.addAction("No Unit Conversions found")
def apply_conversion(self, unit):
"""
@@ -207,8 +209,11 @@ def apply_conversion(self, unit):
self.clearFocus()
self.set_display()
else:
- logging.warning("Warning: Attempting to convert PyDMLineEdit unit, but '{0}' can not be converted to '{1}'."
- .format(self._unit, unit))
+ logging.warning(
+ "Warning: Attempting to convert PyDMLineEdit unit, but '{0}' can not be converted to '{1}'.".format(
+ self._unit, unit
+ )
+ )
def widget_ctx_menu(self):
"""
@@ -219,6 +224,8 @@ def widget_ctx_menu(self):
QMenu or None
If the return of this method is None a new QMenu will be created by `assemble_tools_menu`.
"""
+ self.create_unit_options()
+
menu = self.createStandardContextMenu()
menu.addSeparator()
menu.addMenu(self.unitMenu)
@@ -241,22 +248,30 @@ def set_display(self):
new_value = self.value
- if self._display_format_type in [DisplayFormat.Default,
- DisplayFormat.Decimal,
- DisplayFormat.Exponential,
- DisplayFormat.Hex,
- DisplayFormat.Binary]:
+ if self._display_format_type in [
+ DisplayFormat.Default,
+ DisplayFormat.Decimal,
+ DisplayFormat.Exponential,
+ DisplayFormat.Hex,
+ DisplayFormat.Binary,
+ ]:
if self.channeltype not in (str, np.ndarray):
try:
new_value *= self.channeltype(self._scale)
except TypeError:
- logger.error("Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
- self._scale, self._channel, self.channeltype))
-
- new_value = parse_value_for_display(value=new_value, precision=self.precision,
- display_format_type=self._display_format_type,
- string_encoding=self._string_encoding,
- widget=self)
+ logger.error(
+ "Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
+ self._scale, self._channel, self.channeltype
+ )
+ )
+
+ new_value = parse_value_for_display(
+ value=new_value,
+ precision=self.precision,
+ display_format_type=self._display_format_type,
+ string_encoding=self._string_encoding,
+ widget=self,
+ )
self._has_displayed_value_yet = True
if type(new_value) in str_types:
@@ -281,7 +296,9 @@ def focusInEvent(self, event: QFocusEvent) -> None:
widget this behavior can lead to a race condition where if the widget is given focus before the PV has been
connected long enough to receive a value, then the widget never loads the initial text from the PV.
"""
- if not self._has_displayed_value_yet and (event.reason() == Qt.ActiveWindowFocusReason or event.reason() == Qt.TabFocusReason):
+ if not self._has_displayed_value_yet and (
+ event.reason() == Qt.ActiveWindowFocusReason or event.reason() == Qt.TabFocusReason
+ ):
# Clearing focus ensures that the widget will display the value for the PV
self.clearFocus()
return
@@ -299,8 +316,8 @@ def focusOutEvent(self, event):
@staticmethod
def strtobool(val):
- valid_true = ['Y', 'YES', 'T', 'TRUE', 'ON', '1']
- valid_false = ['N', 'NO', 'F', 'FALSE', 'OFF', '0']
+ valid_true = ["Y", "YES", "T", "TRUE", "ON", "1"]
+ valid_false = ["N", "NO", "F", "FALSE", "OFF", "0"]
if val.upper() in valid_true:
return 1
diff --git a/pydm/widgets/logdisplay.py b/pydm/widgets/logdisplay.py
index 87ea7c9a8f..ec879c4be4 100644
--- a/pydm/widgets/logdisplay.py
+++ b/pydm/widgets/logdisplay.py
@@ -3,11 +3,18 @@
from collections import OrderedDict
-from qtpy.QtCore import (QObject, Slot, Signal, Property,
- Q_ENUMS, QSize)
-from qtpy.QtWidgets import (QWidget, QPlainTextEdit, QComboBox, QLabel,
- QPushButton, QHBoxLayout, QVBoxLayout,
- QStyleOption, QStyle)
+from qtpy.QtCore import QObject, Slot, Signal, Property, Q_ENUMS, QSize
+from qtpy.QtWidgets import (
+ QWidget,
+ QPlainTextEdit,
+ QComboBox,
+ QLabel,
+ QPushButton,
+ QHBoxLayout,
+ QVBoxLayout,
+ QStyleOption,
+ QStyle,
+)
from qtpy.QtGui import QPainter
logger = logging.getLogger(__name__)
@@ -56,6 +63,7 @@ class GuiHandler(QObject, logging.Handler):
parent: QObject, optional
"""
+
message = Signal(str)
def __init__(self, level=logging.NOTSET, parent=None):
@@ -70,7 +78,7 @@ def emit(self, record):
try:
self.message.emit(self.format(record))
except RuntimeError:
- logger.debug('Handler was destroyed at the C++ level.')
+ logger.debug("Handler was destroyed at the C++ level.")
class LogLevels(object):
@@ -91,8 +99,11 @@ def as_dict():
OrderedDict
"""
# First let's remove the internals
- entries = [(k, v) for k, v in LogLevels.__dict__.items() if
- not k.startswith("__") and not callable(v) and not isinstance(v, staticmethod)]
+ entries = [
+ (k, v)
+ for k, v in LogLevels.__dict__.items()
+ if not k.startswith("__") and not callable(v) and not isinstance(v, staticmethod)
+ ]
return OrderedDict(sorted(entries, key=lambda x: x[1], reverse=False))
@@ -117,16 +128,17 @@ class PyDMLogDisplay(QWidget, LogLevels):
Initial level of log display
"""
+
Q_ENUMS(LogLevels)
LogLevels = LogLevels
- terminator = '\n'
- default_format = '%(asctime)s %(message)s'
+ terminator = "\n"
+ default_format = "%(asctime)s %(message)s"
default_level = logging.INFO
def __init__(self, parent=None, logname=None, level=logging.NOTSET):
QWidget.__init__(self, parent=parent)
# Create Widgets
- self.label = QLabel('Minimum displayed log level: ', parent=self)
+ self.label = QLabel("Minimum displayed log level: ", parent=self)
self.combo = QComboBox(parent=self)
self.text = QPlainTextEdit(parent=self)
self.text.setReadOnly(True)
@@ -153,7 +165,7 @@ def __init__(self, parent=None, logname=None, level=logging.NOTSET):
# Create logger. Either as a root or given logname
self.log = None
self.level = None
- self.logName = logname or ''
+ self.logName = logname or ""
self.logLevel = level
self.destroyed.connect(functools.partial(logger_destroyed, self.log))
@@ -219,9 +231,8 @@ def setLevel(self, level):
# Get the level from the incoming string specification
try:
level = getattr(logging, level.upper())
- except AttributeError as exc:
- logger.exception("Invalid logging level specified %s",
- level.upper())
+ except AttributeError:
+ logger.exception("Invalid logging level specified %s", level.upper())
else:
# Set the existing handler and logger to this level
self.handler.setLevel(level)
diff --git a/pydm/widgets/multi_axis_plot.py b/pydm/widgets/multi_axis_plot.py
index ebbd68892d..211da61f3f 100644
--- a/pydm/widgets/multi_axis_plot.py
+++ b/pydm/widgets/multi_axis_plot.py
@@ -46,8 +46,17 @@ def __init__(self, parent=None, axisItems=None, **kargs):
self.connectMenuSignals(self.vb.menu)
self.stackedViews.add(self.vb)
- def addAxis(self, axis, name, plotDataItem=None, setXLink=False, enableAutoRangeX=True, enableAutoRangeY=True,
- minRange=-1.0, maxRange=1.0):
+ def addAxis(
+ self,
+ axis,
+ name,
+ plotDataItem=None,
+ setXLink=False,
+ enableAutoRangeX=True,
+ enableAutoRangeY=True,
+ minRange=-1.0,
+ maxRange=1.0,
+ ):
"""
Add an axis to this plot by creating a new view box to link it with. Links the PlotDataItem
with this axis if provided
@@ -74,14 +83,17 @@ def addAxis(self, axis, name, plotDataItem=None, setXLink=False, enableAutoRange
"""
# Create a new view box to link this axis with
- self.axes[str(name)] = {'item': axis, 'pos': None} # The None will become an actual position in rebuildLayout() below
+ self.axes[str(name)] = {
+ "item": axis,
+ "pos": None,
+ } # The None will become an actual position in rebuildLayout() below
view = MultiAxisViewBox()
view.setYRange(minRange, maxRange)
view.enableAutoRange(axis=ViewBox.XAxis, enable=enableAutoRangeX)
view.enableAutoRange(axis=ViewBox.YAxis, enable=enableAutoRangeY)
- self.axes['bottom']['item'].linkToView(view) # Ensure the x axis will update when the view does
+ self.axes["bottom"]["item"].linkToView(view) # Ensure the x axis will update when the view does
- view.setMouseMode(self.vb.state['mouseMode']) # Ensure that mouse behavior is consistent between stacked views
+ view.setMouseMode(self.vb.state["mouseMode"]) # Ensure that mouse behavior is consistent between stacked views
axis.linkToView(view)
if plotDataItem is not None:
@@ -163,7 +175,7 @@ def linkDataToAxis(self, plotDataItem: PlotDataItem, axisName: str) -> None:
if plotDataItem is None:
return
- axisToLink = self.axes.get(axisName)['item']
+ axisToLink = self.axes.get(axisName)["item"]
# If this data is being moved from an existing view box, unlink that view box first
currentView = plotDataItem.getViewBox()
@@ -190,9 +202,9 @@ def linkDataToAxis(self, plotDataItem: PlotDataItem, axisName: str) -> None:
if plotDataItem.name() and not axisToLink.labelText:
axisToLink.setLabel(plotDataItem.name(), color=plotDataItem.color_string)
- elif axisToLink.labelText and plotDataItem.color_string and axisToLink.labelStyle['color'] == '#969696':
+ elif axisToLink.labelText and plotDataItem.color_string and axisToLink.labelStyle["color"] == "#969696":
# The color for the axis was not specified by the user (#969696 is default) so set it appropriately
- axisToLink.labelStyle['color'] = plotDataItem.color_string
+ axisToLink.labelStyle["color"] = plotDataItem.color_string
axisToLink._updateLabel()
if self.legend is not None and plotDataItem.name():
self.legend.addItem(plotDataItem, name=plotDataItem.name())
@@ -208,7 +220,7 @@ def removeAxis(self, axisName):
self.curvesPerAxis[axisName] = 0
- oldAxis = self.axes[axisName]['item']
+ oldAxis = self.axes[axisName]["item"]
self.layout.removeItem(oldAxis)
if oldAxis.scene() is not None:
oldAxis.scene().removeItem(oldAxis)
@@ -247,8 +259,8 @@ def setXRange(self, minX, maxX, padding=0, update=True):
for view in self.stackedViews:
view.setXRange(minX, maxX, padding=padding)
- if 'bottom' not in self.axesOriginalRanges:
- self.axesOriginalRanges['bottom'] = (minX, maxX)
+ if "bottom" not in self.axesOriginalRanges:
+ self.axesOriginalRanges["bottom"] = (minX, maxX)
super(MultiAxisPlot, self).setXRange(minX, maxX, padding=padding)
def setYRange(self, minY, maxY, padding=0, update=True):
@@ -272,20 +284,20 @@ def setYRange(self, minY, maxY, padding=0, update=True):
super(MultiAxisPlot, self).setYRange(minY, maxY, padding=padding)
def isAnyXAutoRange(self) -> bool:
- """ Return true if any view boxes are set to autorange on the x-axis, false otherwise """
+ """Return true if any view boxes are set to autorange on the x-axis, false otherwise"""
for view in self.stackedViews:
- if view.state['autoRange'][0]:
+ if view.state["autoRange"][0]:
return True
return False
def disableXAutoRange(self):
- """ Disable x-axis autorange for all views in the plot. """
+ """Disable x-axis autorange for all views in the plot."""
for view in self.stackedViews:
view.enableAutoRange(x=False)
def getAxes(self) -> List[AxisItem]:
- """ Returns all axes that have been added to this plot """
- return [val['item'] for val in self.axes.values()]
+ """Returns all axes that have been added to this plot"""
+ return [val["item"] for val in self.axes.values()]
def clearAxes(self):
"""
@@ -298,28 +310,32 @@ def clearAxes(self):
self.stackedViews.clear()
# Reset the axes associated with all y axis curves
- allAxes = [val['item'] for val in self.axes.values()]
+ allAxes = [val["item"] for val in self.axes.values()]
for oldAxis in allAxes:
- if oldAxis.orientation != 'bottom': # Currently only multiple y axes are supported
+ if oldAxis.orientation != "bottom": # Currently only multiple y axes are supported
self.layout.removeItem(oldAxis)
if oldAxis.scene() is not None:
oldAxis.scene().removeItem(oldAxis)
oldAxis.unlinkFromView()
# Retain the x axis
- bottomAxis = self.axes['bottom']
- self.axes = {'bottom': bottomAxis}
+ bottomAxis = self.axes["bottom"]
+ self.axes = {"bottom": bottomAxis}
def restoreAxisRanges(self):
- """ Restore the min and max range of all axes on the plot to their original values """
+ """Restore the min and max range of all axes on the plot to their original values"""
if len(self.axes) == 0 or len(self.axesOriginalRanges) == 0:
return
# First restore the range for all y-axis items added to this plot
for axisName, axisValue in self.axes.items():
- axisItem = axisValue['item']
+ axisItem = axisValue["item"]
linkedView = axisItem.linkedView()
- if linkedView is None or axisItem.orientation not in ('left', 'right') or axisName not in self.axesOriginalRanges:
+ if (
+ linkedView is None
+ or axisItem.orientation not in ("left", "right")
+ or axisName not in self.axesOriginalRanges
+ ):
continue
original_ranges = self.axesOriginalRanges[axisName]
@@ -329,8 +345,8 @@ def restoreAxisRanges(self):
linkedView.setYRange(original_ranges[0], original_ranges[1])
# Now restore the x-axis range as well if needed
- if 'bottom' in self.axesOriginalRanges and self.axesOriginalRanges['bottom'][0] is not None:
- self.setXRange(self.axesOriginalRanges['bottom'][0], self.axesOriginalRanges['bottom'][1])
+ if "bottom" in self.axesOriginalRanges and self.axesOriginalRanges["bottom"][0] is not None:
+ self.setXRange(self.axesOriginalRanges["bottom"][0], self.axesOriginalRanges["bottom"][1])
else:
self.setPlotAutoRange(x=True)
@@ -362,8 +378,9 @@ def setPlotAutoPan(self, auto_pan_x: Optional[bool] = None, auto_pan_y: Optional
for stackedView in self.stackedViews:
stackedView.setAutoPan(x=auto_pan_x, y=auto_pan_y)
- def setPlotAutoRangeVisibleOnly(self, visible_only_x: Optional[bool] = None,
- visible_only_y: Optional[bool] = None) -> None:
+ def setPlotAutoRangeVisibleOnly(
+ self, visible_only_x: Optional[bool] = None, visible_only_y: Optional[bool] = None
+ ) -> None:
"""
Toggle if auto range should use only visible data when calculating the range to show
@@ -377,7 +394,6 @@ def setPlotAutoRangeVisibleOnly(self, visible_only_x: Optional[bool] = None,
for stackedView in self.stackedViews:
stackedView.setAutoVisible(x=visible_only_x, y=visible_only_y)
-
def invertAxis(self, axis: int, inverted: bool) -> None:
"""
Toggle whether or not the input axis should be inverted.
@@ -415,7 +431,7 @@ def removeItem(self, item):
self.updateParamList()
# Then let any view box it is associated with remove it from its internal lists as well
- if hasattr(item, 'getViewBox'):
+ if hasattr(item, "getViewBox"):
linked_view = item.getViewBox()
if linked_view is not None:
linked_view.removeItem(item)
@@ -438,7 +454,7 @@ def rebuildLayout(self):
orientations = {"left": [], "right": [], "top": [], "bottom": []}
- allAxes = [val['item'] for val in self.axes.values()]
+ allAxes = [val["item"] for val in self.axes.values()]
for axis in allAxes:
orientations[axis.orientation].append(axis)
@@ -455,18 +471,18 @@ def rebuildLayout(self):
self.layout.addItem(axis, y, leftOffset)
def updateGrid(self, *args) -> None:
- """ Show or hide the grid on a per-axis basis """
+ """Show or hide the grid on a per-axis basis"""
if is_qt_designer():
return
# Get the user-set value for the alpha used to draw the grid lines
alpha = self.ctrl.gridAlphaSlider.value()
x = alpha if self.ctrl.xGridCheck.isChecked() else False
y = alpha if self.ctrl.yGridCheck.isChecked() else False
- all_axes = [val['item'] for val in self.axes.values()]
+ all_axes = [val["item"] for val in self.axes.values()]
for axis in all_axes:
- if axis.orientation in ('left', 'right'):
+ if axis.orientation in ("left", "right"):
axis.setGrid(y)
- elif axis.orientation in ('top', 'bottom'):
+ elif axis.orientation in ("top", "bottom"):
axis.setGrid(x)
def handleWheelEvent(self, view, ev, axis):
@@ -521,35 +537,35 @@ def changeMouseMode(self, mode):
self.vb.setLeftButtonAction(mode)
def updateXAutoRange(self, val):
- """ Update the autorange values for the x-axis on all view boxes """
+ """Update the autorange values for the x-axis on all view boxes"""
self.vb.enableAutoRange(ViewBox.XAxis, val)
for stackedView in self.stackedViews:
stackedView.enableAutoRange(ViewBox.XAxis, val)
def updateYAutoRange(self, val):
- """ Update the autorange values for the y-axis on all view boxes """
+ """Update the autorange values for the y-axis on all view boxes"""
self.vb.enableAutoRange(ViewBox.YAxis, val)
for stackedView in self.stackedViews:
stackedView.enableAutoRange(ViewBox.YAxis, val)
def updateLogMode(self) -> None:
- """ Toggle log mode on or off for each item in the plot """
+ """Toggle log mode on or off for each item in the plot"""
x = self.ctrl.logXCheck.isChecked()
y = self.ctrl.logYCheck.isChecked()
allAxes = self.getAxes()
for axis in allAxes:
- if axis.orientation in ('bottom', 'top'):
+ if axis.orientation in ("bottom", "top"):
axis.setLogMode(x)
- elif axis.orientation in ('left', 'right'):
+ elif axis.orientation in ("left", "right"):
axis.setLogMode(y)
for i in self.items:
- if hasattr(i, 'setLogMode'):
+ if hasattr(i, "setLogMode"):
i.setLogMode(x, y)
for i in self.dataItems:
- if hasattr(i, 'setLogMode'):
+ if hasattr(i, "setLogMode"):
i.setLogMode(x, y)
self.enableAutoRange()
diff --git a/pydm/widgets/multi_axis_viewbox.py b/pydm/widgets/multi_axis_viewbox.py
index c17af2bd5c..812f1ac822 100644
--- a/pydm/widgets/multi_axis_viewbox.py
+++ b/pydm/widgets/multi_axis_viewbox.py
@@ -1,4 +1,4 @@
-from pyqtgraph import GraphicsWidget, ViewBox
+from pyqtgraph import ViewBox
from qtpy.QtCore import Qt, Signal
@@ -53,7 +53,7 @@ def mouseDragEvent(self, ev, axis=None, fromSignal=False):
if axis != ViewBox.YAxis and not fromSignal:
# This event happened within the view box area itself or the x axis so propagate to any stacked view boxes
self.sigMouseDragged.emit(self, ev, axis)
- if ev.isFinish() and self.state['mouseMode'] == ViewBox.RectMode and axis is None:
+ if ev.isFinish() and self.state["mouseMode"] == ViewBox.RectMode and axis is None:
self.sigMouseDraggedDone.emit() # Indicates the end of a mouse drag event
super(MultiAxisViewBox, self).mouseDragEvent(ev, axis)
@@ -72,9 +72,9 @@ def keyPressEvent(self, ev):
"""
ev.accept()
- if ev.text() == '-':
+ if ev.text() == "-":
self.scaleHistory(-1)
- elif ev.text() in ['+', '=']:
+ elif ev.text() in ["+", "="]:
self.scaleHistory(1)
elif ev.key() == Qt.Key.Key_Backspace:
self.scaleHistory(0)
diff --git a/pydm/widgets/multi_axis_viewbox_menu.py b/pydm/widgets/multi_axis_viewbox_menu.py
index c467458d89..79d5b5891d 100644
--- a/pydm/widgets/multi_axis_viewbox_menu.py
+++ b/pydm/widgets/multi_axis_viewbox_menu.py
@@ -47,83 +47,83 @@ def __init__(self, view):
self.insertAction(self.restoreRangesAction, self.viewAll)
def set3ButtonMode(self):
- """ Change the mouse left-click functionality to pan the plot """
+ """Change the mouse left-click functionality to pan the plot"""
super(MultiAxisViewBoxMenu, self).set3ButtonMode()
- self.sigMouseModeChanged.emit('pan')
+ self.sigMouseModeChanged.emit("pan")
def set1ButtonMode(self):
- """ Change the mouse left-click functionality to zoom in on the plot """
+ """Change the mouse left-click functionality to zoom in on the plot"""
super(MultiAxisViewBoxMenu, self).set1ButtonMode()
- self.sigMouseModeChanged.emit('rect')
+ self.sigMouseModeChanged.emit("rect")
def xAutoClicked(self):
- """ Update the auto-range value for each view box """
+ """Update the auto-range value for each view box"""
super().xAutoClicked()
val = self.ctrl[0].autoPercentSpin.value() * 0.01
self.sigXAutoRangeChanged.emit(val)
def xManualClicked(self):
- """ Disable x auto-range for each view box """
+ """Disable x auto-range for each view box"""
super().xManualClicked()
self.sigXAutoRangeChanged.emit(False)
def xRangeTextChanged(self):
- """ Manually set the x-axis range to the user's input. Range will be unchanged if input was invalid """
+ """Manually set the x-axis range to the user's input. Range will be unchanged if input was invalid"""
super().xRangeTextChanged()
updated_values = self._validateRangeText(ViewBox.XAxis)
self.sigXManualRange.emit(*updated_values)
def yAutoClicked(self):
- """ Update the y auto-range value for each view box """
+ """Update the y auto-range value for each view box"""
super().yAutoClicked()
val = self.ctrl[1].autoPercentSpin.value() * 0.01
self.sigYAutoRangeChanged.emit(val)
def yManualClicked(self):
- """ Disable y auto-range for each view box """
+ """Disable y auto-range for each view box"""
super().yManualClicked()
self.sigYAutoRangeChanged.emit(False)
def yRangeTextChanged(self):
- """ Manually set the y-axis range to the user's input. Range will be unchanged if input was invalid """
+ """Manually set the y-axis range to the user's input. Range will be unchanged if input was invalid"""
super().yRangeTextChanged()
updated_values = self._validateRangeText(ViewBox.YAxis)
self.sigYManualRange.emit(*updated_values)
def xAutoPanToggled(self, autoPan: bool):
- """ Toggle the auto pan status of the x-axis """
+ """Toggle the auto pan status of the x-axis"""
super().xAutoPanToggled(autoPan)
self.sigAutoPan.emit(autoPan, None)
def xVisibleOnlyToggled(self, autoVisible: bool):
- """ Toggle the visible only status of autorange for the x-axis """
+ """Toggle the visible only status of autorange for the x-axis"""
super().xVisibleOnlyToggled(autoVisible)
self.sigVisibleOnly.emit(autoVisible, None)
def yAutoPanToggled(self, autoPan: bool):
- """ Toggle the auto pan status of the y-axis """
+ """Toggle the auto pan status of the y-axis"""
super().yAutoPanToggled(autoPan)
self.sigAutoPan.emit(None, autoPan)
def yVisibleOnlyToggled(self, autoVisible: bool):
- """ Toggle the visible only status of autorange for the y-axis """
+ """Toggle the visible only status of autorange for the y-axis"""
super().yVisibleOnlyToggled(autoVisible)
self.sigVisibleOnly.emit(None, autoVisible)
def yInvertToggled(self, inverted: bool):
- """ Toggle the inverted status of the y-axis. """
+ """Toggle the inverted status of the y-axis."""
super().yInvertToggled(inverted)
self.sigInvertAxis.emit(ViewBox.YAxis, inverted)
def xInvertToggled(self, inverted: bool):
- """ Toggle the inverted status of the x-axis """
+ """Toggle the inverted status of the x-axis"""
super().xInvertToggled(inverted)
self.sigInvertAxis.emit(ViewBox.XAxis, inverted)
def autoRange(self):
- """ Sets autorange to True for all elements on the plot """
+ """Sets autorange to True for all elements on the plot"""
self.sigSetAutorange.emit(True, True)
def restoreRanges(self):
- """ Restore the original x and y axis ranges for this plot """
+ """Restore the original x and y axis ranges for this plot"""
self.sigRestoreRanges.emit()
diff --git a/pydm/widgets/nt_table.py b/pydm/widgets/nt_table.py
index e31b6ef206..15df9286d7 100644
--- a/pydm/widgets/nt_table.py
+++ b/pydm/widgets/nt_table.py
@@ -8,8 +8,7 @@
class PythonTableModel(QtCore.QAbstractTableModel):
- def __init__(self, column_names, initial_list=None, parent=None,
- edit_method=None, can_edit_method=None):
+ 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
@@ -39,8 +38,7 @@ def flags(self, index):
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()])
+ editable = self.can_edit_method(self._list[index.row()][index.column()])
if editable:
f = f | QtCore.Qt.ItemIsEditable
return f
@@ -82,24 +80,22 @@ def setData(self, index, value, role=QtCore.Qt.EditRole):
return False
success = self.edit_method(self.parent, index.row(), index.column(), value)
-
+
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 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)
+ sort_reversed = order == QtCore.Qt.AscendingOrder
self._list.sort(key=itemgetter(col), reverse=sort_reversed)
self.layoutChanged.emit()
@@ -133,14 +129,12 @@ def __delitem__(self, index):
self.endRemoveRows()
def append(self, value):
- self.beginInsertRows(QtCore.QModelIndex(), len(self._list),
- len(self._list))
+ 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.beginInsertRows(QtCore.QModelIndex(), len(self._list), len(self._list) + len(values) - 1)
self._list.extend(values)
self.endInsertRows()
@@ -170,25 +164,26 @@ def reverse(self):
class PyDMNTTable(QtWidgets.QWidget, PyDMWritableWidget):
"""
- The PyDMNTTable is a table widget used to display PVA NTTable data.
+ 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.
+ 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.
-
+ 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.
+ ----------
+ 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):
- self._read_only = True
+ self._read_only = True
super().__init__(parent=parent)
PyDMWidget.__init__(self, init_channel=init_channel)
@@ -208,7 +203,7 @@ def readOnly(self):
def readOnly(self, value):
if self._read_only != value:
self._read_only = value
-
+
def check_enable_state(self):
"""
Checks whether or not the widget should be disable.
@@ -218,11 +213,11 @@ def check_enable_state(self):
self.setEnabled(True)
tooltip = self.toolTip()
- if self.readOnly:
- if tooltip != '':
- tooltip += '\n'
+ if self.readOnly:
+ if tooltip != "":
+ tooltip += "\n"
tooltip += "Running PyDMNTTable on Read-Only mode."
-
+
self.setToolTip(tooltip)
def value_changed(self, data=None):
@@ -232,42 +227,38 @@ def value_changed(self, data=None):
Parameters
----------
data : dict
- The new value from the channel.
+ The new value from the channel.
"""
if data is None:
return
-
+
super(PyDMNTTable, self).value_changed(data)
-
- labels = data.get('labels', None)
- values = data.get('value', {})
- if not values:
+ labels = data.get("labels", None)
+ values = data.get("value", {})
+
+ if not values:
values = data.values()
-
+
if labels is None or len(labels) == 0:
labels = data.keys()
labels = list(labels)
-
+
try:
- values = list(zip(*[v for k, v in data.items() if k != 'labels']))
+ values = list(zip(*[v for k, v in data.items() if k != "labels"]))
except TypeError:
logger.exception("NTTable value items must be iterables.")
self._table_values = values
if labels != self._table_labels:
-
if not self.readOnly:
self.edit_method = PyDMNTTable.send_table
else:
self.edit_method = None
self._table_labels = labels
- self._model = PythonTableModel(labels,
- initial_list=values,
- parent=self,
- edit_method=self.edit_method)
+ self._model = PythonTableModel(labels, initial_list=values, parent=self, edit_method=self.edit_method)
self._table.setModel(self._model)
else:
self._model.list = values
@@ -275,13 +266,13 @@ def value_changed(self, data=None):
def send_table(self, row, column, value):
"""
Update Channel value when cell value is changed.
-
+
Parameters
----------
- row : int
+ row : int
index of row
- column : int
- index of column
+ column : int
+ index of column
value : str
new value of cell
"""
@@ -296,11 +287,11 @@ def send_table(self, row, column, value):
else:
self.value[self._table_labels[column]][row] = value
- value_to_send = {k: v for k, v in self.value.items() if k != 'labels'}
+ value_to_send = {k: v for k, v in self.value.items() if k != "labels"}
# dictionary needs to be wrapped in another dictionary with a key 'value'
- # to be passed back to the p4p plugin.
- emit_dict = {'value': value_to_send}
-
+ # to be passed back to the p4p plugin.
+ emit_dict = {"value": value_to_send}
+
self.send_value_signal[dict].emit(emit_dict)
return True
diff --git a/pydm/widgets/pushbutton.py b/pydm/widgets/pushbutton.py
index 75b037e452..a90492571d 100644
--- a/pydm/widgets/pushbutton.py
+++ b/pydm/widgets/pushbutton.py
@@ -1,11 +1,13 @@
-import os
import hashlib
-from qtpy.QtWidgets import QPushButton, QMessageBox, QInputDialog, QLineEdit, QDialogButtonBox
+from qtpy.QtGui import QColor
+from qtpy.QtWidgets import QPushButton, QMessageBox, QInputDialog, QLineEdit, QStyle
from qtpy.QtCore import Slot, Property
+from qtpy import QtDesigner
from .base import PyDMWritableWidget
-
+from ..utilities import IconFont
import logging
+
logger = logging.getLogger(__name__)
@@ -49,9 +51,9 @@ class PyDMPushButton(QPushButton, PyDMWritableWidget):
DEFAULT_CONFIRM_MESSAGE = "Are you sure you want to proceed?"
- def __init__(self, parent=None, label=None, icon=None,
- pressValue=None, releaseValue=None, relative=False,
- init_channel=None):
+ def __init__(
+ self, parent=None, label=None, icon=None, pressValue=None, releaseValue=None, relative=False, init_channel=None
+ ):
if icon:
QPushButton.__init__(self, icon, label, parent)
elif label:
@@ -72,6 +74,82 @@ def __init__(self, parent=None, label=None, icon=None,
self._released = False
self.clicked.connect(self.sendValue)
+ # Standard icons (which come with the qt install, and work cross-platform),
+ # and icons from the "Font Awesome" icon set (https://fontawesome.com/)
+ # can not be set with a widget's "icon" property in designer, only in python.
+ # so we provide our own property to specify standard icons and set them with python in the prop's setter.
+ self._pydm_icon_name = ""
+ # The color of "Font Awesome" icons can be set,
+ # but standard icons are already colored and can not be set.
+ self._pydm_icon_color = QColor(90, 90, 90)
+
+ @Property(str)
+ def PyDMIcon(self) -> str:
+ """
+ Name of icon to be set from Qt provided standard icons or from the fontawesome icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable fontawesome icons.
+
+ Returns
+ -------
+ str
+ """
+ return self._pydm_icon_name
+
+ @PyDMIcon.setter
+ def PyDMIcon(self, value: str) -> None:
+ """
+ Name of icon to be set from Qt provided standard icons or from the "Font Awesome" icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable "Font Awesome" icons.
+
+ Parameters
+ ----------
+ value : str
+ """
+ if self._pydm_icon_name == value:
+ return
+
+ # We don't know if user is trying to use a standard icon or an icon from "Font Awesome",
+ # so 1st try to create a Font Awesome one, which hits exception if icon name is not valid.
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(value, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ icon = getattr(QStyle, value, None)
+ if icon:
+ self.setIcon(self.style().standardIcon(icon))
+
+ self._pydm_icon_name = value
+
+ @Property(QColor)
+ def PyDMIconColor(self) -> QColor:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Returns
+ -------
+ QColor
+ """
+ return self._pydm_icon_color
+
+ @PyDMIconColor.setter
+ def PyDMIconColor(self, state_color: QColor) -> None:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Parameters
+ ----------
+ new_color : QColor
+ """
+ if state_color != self._pydm_icon_color:
+ self._pydm_icon_color = state_color
+ # apply the new color
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(self._pydm_icon_name, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ return
@Property(bool)
def passwordProtected(self):
@@ -128,6 +206,11 @@ def password(self, value):
# new one, and only updates if the new password is different
self.protectedPassword = sha.hexdigest()
+ # Make sure designer knows it should save the protectedPassword field
+ formWindow = QtDesigner.QDesignerFormWindowInterface.findFormWindow(self)
+ if formWindow:
+ formWindow.cursor().setProperty("protectedPassword", self.protectedPassword)
+
@Property(str)
def protectedPassword(self):
"""
@@ -147,7 +230,7 @@ def protectedPassword(self, value):
@Property(bool)
def showConfirmDialog(self):
"""
- Wether or not to display a confirmation dialog.
+ Whether or not to display a confirmation dialog.
Returns
-------
@@ -158,7 +241,7 @@ def showConfirmDialog(self):
@showConfirmDialog.setter
def showConfirmDialog(self, value):
"""
- Wether or not to display a confirmation dialog.
+ Whether or not to display a confirmation dialog.
Parameters
----------
@@ -221,7 +304,6 @@ def pressValue(self, value):
if str(value) != self._pressValue:
self._pressValue = str(value)
-
@Property(str)
def releaseValue(self):
"""
@@ -253,7 +335,6 @@ def releaseValue(self, value):
if str(value) != self._releaseValue:
self._releaseValue = str(value)
-
@Property(bool)
def relativeChange(self):
"""
@@ -313,17 +394,13 @@ def confirm_dialog(self, is_release=False):
msg = QMessageBox()
msg.setIcon(QMessageBox.Question)
- relative = "Yes" if self._relative else "No"
- val = self._pressValue
- op = "Press"
if is_release:
- val = self._releaseValue
- op = "Release"
+ pass
msg.setText(self._confirm_message)
# Force "Yes" button to be on the right (as on macOS) to follow common design practice
- msg.setStyleSheet("button-layout: 1") # MacLayout
+ msg.setStyleSheet("button-layout: 1") # MacLayout
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg.setDefaultButton(QMessageBox.No)
@@ -346,8 +423,7 @@ def validate_password(self):
if not self._password_protected:
return True
- pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:",
- QLineEdit.Password, "")
+ pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:", QLineEdit.Password, "")
pwd = str(pwd)
if not ok or pwd == "":
return False
@@ -396,8 +472,7 @@ def sendValue(self):
return val
- def __execute_send(self, new_value, skip_confirm=False, skip_password=False,
- is_release=False):
+ def __execute_send(self, new_value, skip_confirm=False, skip_password=False, is_release=False):
"""
Execute the send operation for push and release.
@@ -433,15 +508,12 @@ def __execute_send(self, new_value, skip_confirm=False, skip_password=False,
if not self._relative or self.channeltype == str:
send_value = new_value
- self.send_value_signal[self.channeltype].emit(
- self.channeltype(send_value)
- )
+ self.send_value_signal[self.channeltype].emit(self.channeltype(send_value))
else:
send_value = self.value + self.channeltype(new_value)
self.send_value_signal[self.channeltype].emit(send_value)
return send_value
-
@Slot()
def sendReleaseValue(self):
"""
@@ -488,13 +560,13 @@ def updatePressValue(self, value):
"""
try:
self.pressValue = self.channeltype(value)
- except(ValueError, TypeError):
+ except (ValueError, TypeError):
logger.error("'{0}' is not a valid pressValue for '{1}'.".format(value, self.channel))
@Property(bool)
def writeWhenRelease(self):
"""
- Wether or not to write releaseValue on release
+ Whether or not to write releaseValue on release
Returns
-------
@@ -505,7 +577,7 @@ def writeWhenRelease(self):
@writeWhenRelease.setter
def writeWhenRelease(self, value):
"""
- Wether or not to write releaseValue on release
+ Whether or not to write releaseValue on release
Parameters
----------
@@ -520,7 +592,6 @@ def writeWhenRelease(self, value):
self.pressed.connect(self.sendValue)
self.released.connect(self.sendReleaseValue)
-
@Slot(int)
@Slot(float)
@Slot(str)
@@ -539,5 +610,5 @@ def updateReleaseValue(self, value):
"""
try:
self.releaseValue = self.channeltype(value)
- except(ValueError, TypeError):
- logger.error("'{0}' is not a valid releaseValue for '{1}'.".format(value, self.channel))
\ No newline at end of file
+ except (ValueError, TypeError):
+ logger.error("'{0}' is not a valid releaseValue for '{1}'.".format(value, self.channel))
diff --git a/pydm/widgets/qtplugin_base.py b/pydm/widgets/qtplugin_base.py
index 2dd7f299e5..7145191d5d 100644
--- a/pydm/widgets/qtplugin_base.py
+++ b/pydm/widgets/qtplugin_base.py
@@ -35,6 +35,7 @@
class WidgetCategory(str, enum.Enum):
"""Categories for PyDM Widgets in the Qt Designer."""
+
#: Widgets which can contain other widgets.
CONTAINER = "PyDM Container Widgets"
#: Widgets which contain displays.
@@ -54,7 +55,7 @@ def qtplugin_factory(
is_container: bool = False,
group: str = "PyDM Widgets",
extensions: Optional[List[Type]] = None,
- icon: Optional[QtGui.QIcon] = None
+ icon: Optional[QtGui.QIcon] = None,
) -> Type["PyDMDesignerPlugin"]:
"""
Helper function to create a generic PyDMDesignerPlugin class.
@@ -81,8 +82,7 @@ class Plugin(PyDMDesignerPlugin):
__doc__ = "PyDMDesigner Plugin for {}".format(cls.__name__)
def __init__(self):
- super(Plugin, self).__init__(cls, is_container, group, extensions,
- icon)
+ super(Plugin, self).__init__(cls, is_container, group, extensions, icon)
return Plugin
@@ -146,9 +146,7 @@ def initialize(self, core):
self.manager = core.extensionManager()
if self.manager:
factory = PyDMExtensionFactory(parent=self.manager)
- self.manager.registerExtensions(
- factory,
- 'org.qt-project.Qt.Designer.TaskMenu') # Qt5
+ self.manager.registerExtensions(factory, "org.qt-project.Qt.Designer.TaskMenu") # Qt5
def isInitialized(self):
"""
@@ -216,8 +214,8 @@ def domXml(self):
XML Description of the widget's properties.
"""
return (
- "\n"
- " \n"
+ '\n'
+ ' \n'
" {1}\n"
" \n"
"\n"
@@ -244,26 +242,17 @@ def create_designer_widget_from_widget(
what :func:`qtplugin_factory` expects.
"""
if not inspect.isclass(widget_cls):
- raise ValueError(
- f"Expected a class, got a {type(widget_cls).__name__}"
- )
+ raise ValueError(f"Expected a class, got a {type(widget_cls).__name__}")
if issubclass(widget_cls, PyDMDesignerPlugin):
return widget_cls
if issubclass(widget_cls, QtWidgets.QWidget):
- designer_kwargs = getattr(
- widget_cls, "_qt_designer_", None
- ) or {}
+ designer_kwargs = getattr(widget_cls, "_qt_designer_", None) or {}
return qtplugin_factory(widget_cls, **designer_kwargs)
- raise ValueError(
- f"Expected a PyDMDesignerPlugin or a QWidget subclass, "
- f"got a {type(widget_cls).__name__}"
- )
+ raise ValueError(f"Expected a PyDMDesignerPlugin or a QWidget subclass, " f"got a {type(widget_cls).__name__}")
-def get_widgets_from_entrypoints(
- key: str = config.ENTRYPOINT_WIDGET
-) -> Dict[str, Type[PyDMDesignerPlugin]]:
+def get_widgets_from_entrypoints(key: str = config.ENTRYPOINT_WIDGET) -> Dict[str, Type[PyDMDesignerPlugin]]:
"""
Get all widgets from entrypoint definitions.
@@ -283,22 +272,13 @@ def get_widgets_from_entrypoints(
try:
plugin_cls = entry.load()
except Exception as ex:
- logger.exception(
- "Failed to load %s entry %s: %s",
- key, entry.name, ex
- )
+ logger.exception("Failed to load %s entry %s: %s", key, entry.name, ex)
continue
try:
- designer_widget = create_designer_widget_from_widget(
- plugin_cls
- )
+ designer_widget = create_designer_widget_from_widget(plugin_cls)
except Exception:
- logger.warning(
- "Invalid widget class specified in entrypoint "
- "%s: %s",
- entry.name, plugin_cls
- )
+ logger.warning("Invalid widget class specified in entrypoint " "%s: %s", entry.name, plugin_cls)
else:
widgets[entry.name] = designer_widget
diff --git a/pydm/widgets/qtplugin_extensions.py b/pydm/widgets/qtplugin_extensions.py
index 6aa65c068e..258f991f27 100644
--- a/pydm/widgets/qtplugin_extensions.py
+++ b/pydm/widgets/qtplugin_extensions.py
@@ -45,7 +45,7 @@ def __init__(self, widget, parent):
self.widget = widget
self.__actions = None
self.__extensions = []
- extensions = getattr(widget, 'extensions', None)
+ extensions = getattr(widget, "extensions", None)
if extensions is not None:
for ex in extensions:
@@ -79,17 +79,13 @@ class BasicSettingsExtension(PyDMExtension):
def __init__(self, widget):
super(BasicSettingsExtension, self).__init__(widget)
self.widget = widget
- self.edit_settings_action = QtWidgets.QAction(
- "Py&DM basic settings...", self.widget
- )
+ self.edit_settings_action = QtWidgets.QAction("Py&DM basic settings...", self.widget)
self.edit_settings_action.triggered.connect(self.open_dialog)
if not hasattr(widget, "channel"):
self.channel_menu_action = None
else:
- self.channel_menu_action = QtWidgets.QAction(
- "PyDM C&hannel", self.widget
- )
+ self.channel_menu_action = QtWidgets.QAction("PyDM C&hannel", self.widget)
# self.channel_menu_action.triggered.connect(self.open_channel_menu)
self.channel_menu = QtWidgets.QMenu()
self.copy_channel_action = self.channel_menu.addAction("")
@@ -97,24 +93,16 @@ def __init__(self, widget):
self.paste_channel_action = self.channel_menu.addAction("")
self.paste_channel_action.triggered.connect(self.paste_channel)
self.channel_menu.aboutToShow.connect(self.update_action_clipboard_text)
- edit_channel = self.channel_menu.addAction(
- "&Edit channel..."
- )
+ edit_channel = self.channel_menu.addAction("&Edit channel...")
edit_channel.triggered.connect(self.open_dialog)
- copy_channel_value = self.channel_menu.addAction(
- "C&opy current value"
- )
+ copy_channel_value = self.channel_menu.addAction("C&opy current value")
copy_channel_value.triggered.connect(self.copy_channel_value)
self.channel_menu_action.setMenu(self.channel_menu)
def update_action_clipboard_text(self):
- self.copy_channel_action.setText(
- f"&Copy to clipboard: {self.widget.channel}"
- )
+ self.copy_channel_action.setText(f"&Copy to clipboard: {self.widget.channel}")
clipboard_text = get_clipboard_text() or ""
- self.paste_channel_action.setText(
- f"&Paste from clipboard: {clipboard_text[:100]}"
- )
+ self.paste_channel_action.setText(f"&Paste from clipboard: {clipboard_text[:100]}")
def copy_channel(self, _):
channel = self.widget.channel
diff --git a/pydm/widgets/qtplugins.py b/pydm/widgets/qtplugins.py
index 8be76bdbb4..bc760a42d1 100644
--- a/pydm/widgets/qtplugins.py
+++ b/pydm/widgets/qtplugins.py
@@ -6,11 +6,20 @@
from .byte import PyDMByteIndicator
from .checkbox import PyDMCheckbox
from .datetime import PyDMDateTimeEdit, PyDMDateTimeLabel
-from .drawing import (PyDMDrawingArc, PyDMDrawingChord, PyDMDrawingCircle,
- PyDMDrawingEllipse, PyDMDrawingImage,
- PyDMDrawingIrregularPolygon, PyDMDrawingLine,
- PyDMDrawingPie, PyDMDrawingPolygon, PyDMDrawingPolyline,
- PyDMDrawingRectangle, PyDMDrawingTriangle)
+from .drawing import (
+ PyDMDrawingArc,
+ PyDMDrawingChord,
+ PyDMDrawingCircle,
+ PyDMDrawingEllipse,
+ PyDMDrawingImage,
+ PyDMDrawingIrregularPolygon,
+ PyDMDrawingLine,
+ PyDMDrawingPie,
+ PyDMDrawingPolygon,
+ PyDMDrawingPolyline,
+ PyDMDrawingRectangle,
+ PyDMDrawingTriangle,
+)
from .embedded_display import PyDMEmbeddedDisplay
from .enum_button import PyDMEnumButton
from .enum_combo_box import PyDMEnumComboBox
@@ -20,15 +29,17 @@
from .line_edit import PyDMLineEdit
from .logdisplay import PyDMLogDisplay
from .pushbutton import PyDMPushButton
-from .qtplugin_base import (WidgetCategory, get_widgets_from_entrypoints,
- qtplugin_factory)
-from .qtplugin_extensions import (ArchiveTimeCurveEditorExtension,
- BasicSettingsExtension, RulesExtension,
- ScatterCurveEditorExtension,
- EventCurveEditorExtension,
- SymbolExtension,
- TimeCurveEditorExtension,
- WaveformCurveEditorExtension)
+from .qtplugin_base import WidgetCategory, get_widgets_from_entrypoints, qtplugin_factory
+from .qtplugin_extensions import (
+ ArchiveTimeCurveEditorExtension,
+ BasicSettingsExtension,
+ RulesExtension,
+ ScatterCurveEditorExtension,
+ EventCurveEditorExtension,
+ SymbolExtension,
+ TimeCurveEditorExtension,
+ WaveformCurveEditorExtension,
+)
from .related_display_button import PyDMRelatedDisplayButton
from .scale import PyDMScaleIndicator
from .scatterplot import PyDMScatterPlot
@@ -37,19 +48,13 @@
from .spinbox import PyDMSpinbox
from .symbol import PyDMSymbol
from .waveformtable import PyDMWaveformTable
-from .scale import PyDMScaleIndicator
from .analog_indicator import PyDMAnalogIndicator
from .timeplot import PyDMTimePlot
-from .archiver_time_plot import PyDMArchiverTimePlot
from .waveformplot import PyDMWaveformPlot
-from .scatterplot import PyDMScatterPlot
from .eventplot import PyDMEventPlot
from .tab_bar_qtplugin import TabWidgetPlugin
from .template_repeater import PyDMTemplateRepeater
from .terminator import PyDMTerminator
-from .timeplot import PyDMTimePlot
-from .waveformplot import PyDMWaveformPlot
-from .waveformtable import PyDMWaveformTable
from .nt_table import PyDMNTTable
logger = logging.getLogger(__name__)
@@ -60,238 +65,213 @@
# Label plugin
-PyDMLabelPlugin = qtplugin_factory(PyDMLabel, group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("tag"))
+PyDMLabelPlugin = qtplugin_factory(
+ PyDMLabel, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("tag")
+)
# Time Plot plugin
-PyDMTimePlotPlugin = qtplugin_factory(PyDMTimePlot, group=WidgetCategory.PLOT,
- extensions=[TimeCurveEditorExtension,
- RulesExtension],
- icon=ifont.icon("chart-line"))
+PyDMTimePlotPlugin = qtplugin_factory(
+ PyDMTimePlot,
+ group=WidgetCategory.PLOT,
+ extensions=[TimeCurveEditorExtension, RulesExtension],
+ icon=ifont.icon("chart-line"),
+)
# In order to keep the archiver functionality invisible to users who do not have access to an instance of the
# archiver appliance, only load this if the user has the associated environment variable set
if "PYDM_ARCHIVER_URL" in os.environ:
# Time Plot with archiver appliance support plugin
- PyDMArchiverTimePlotPlugin = qtplugin_factory(PyDMArchiverTimePlot, group=WidgetCategory.PLOT,
- extensions=[ArchiveTimeCurveEditorExtension,
- RulesExtension],
- icon=ifont.icon("chart-line"))
+ PyDMArchiverTimePlotPlugin = qtplugin_factory(
+ PyDMArchiverTimePlot,
+ group=WidgetCategory.PLOT,
+ extensions=[ArchiveTimeCurveEditorExtension, RulesExtension],
+ icon=ifont.icon("chart-line"),
+ )
# Waveform Plot plugin
-PyDMWaveformPlotPlugin = qtplugin_factory(PyDMWaveformPlot,
- group=WidgetCategory.PLOT,
- extensions=[
- WaveformCurveEditorExtension,
- RulesExtension],
- icon=ifont.icon("wave-square"))
+PyDMWaveformPlotPlugin = qtplugin_factory(
+ PyDMWaveformPlot,
+ group=WidgetCategory.PLOT,
+ extensions=[WaveformCurveEditorExtension, RulesExtension],
+ icon=ifont.icon("wave-square"),
+)
# Scatter Plot plugin
-PyDMScatterPlotPlugin = qtplugin_factory(PyDMScatterPlot,
- group=WidgetCategory.PLOT,
- extensions=[
- ScatterCurveEditorExtension,
- RulesExtension],
- icon=ifont.icon("project-diagram"))
+PyDMScatterPlotPlugin = qtplugin_factory(
+ PyDMScatterPlot,
+ group=WidgetCategory.PLOT,
+ extensions=[ScatterCurveEditorExtension, RulesExtension],
+ icon=ifont.icon("project-diagram"),
+)
# Event Plot plugin
-PyDMEventPlotPlugin = qtplugin_factory(PyDMEventPlot,
- group=WidgetCategory.PLOT,
- extensions=[
- EventCurveEditorExtension,
- RulesExtension],
- icon=ifont.icon("project-diagram"))
+PyDMEventPlotPlugin = qtplugin_factory(
+ PyDMEventPlot,
+ group=WidgetCategory.PLOT,
+ extensions=[EventCurveEditorExtension, RulesExtension],
+ icon=ifont.icon("project-diagram"),
+)
# Byte plugin
-PyDMByteIndicatorPlugin = qtplugin_factory(PyDMByteIndicator,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("ellipsis-v"))
+PyDMByteIndicatorPlugin = qtplugin_factory(
+ PyDMByteIndicator, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("ellipsis-v")
+)
# Checkbox plugin
-PyDMCheckboxPlugin = qtplugin_factory(PyDMCheckbox, group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("check-square"))
+PyDMCheckboxPlugin = qtplugin_factory(
+ PyDMCheckbox, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("check-square")
+)
# Date/Time plugins
-PyDMDateTimeEditPlugin = qtplugin_factory(PyDMDateTimeEdit,
- group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("calendar-minus"))
-
-PyDMDateTimeLabelPlugin = qtplugin_factory(PyDMDateTimeLabel,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("calendar-alt"))
+PyDMDateTimeEditPlugin = qtplugin_factory(
+ PyDMDateTimeEdit, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("calendar-minus")
+)
+
+PyDMDateTimeLabelPlugin = qtplugin_factory(
+ PyDMDateTimeLabel, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("calendar-alt")
+)
# Drawing plugins
-PyDMDrawingArcPlugin = qtplugin_factory(PyDMDrawingArc,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("circle-notch"))
-PyDMDrawingChordPlugin = qtplugin_factory(PyDMDrawingChord,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("moon"))
-PyDMDrawingCirclePlugin = qtplugin_factory(PyDMDrawingCircle,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("circle"))
-PyDMDrawingEllipsePlugin = qtplugin_factory(PyDMDrawingEllipse,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("ellipsis-h"))
-PyDMDrawingImagePlugin = qtplugin_factory(PyDMDrawingImage,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("image"))
-PyDMDrawingLinePlugin = qtplugin_factory(PyDMDrawingLine,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("minus"))
-PyDMDrawingPiePlugin = qtplugin_factory(PyDMDrawingPie,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("pizza-slice"))
-
-PyDMDrawingRectanglePlugin = qtplugin_factory(PyDMDrawingRectangle,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("border-style"))
-PyDMDrawingTrianglePlugin = qtplugin_factory(PyDMDrawingTriangle,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("caret-up"))
-
-PyDMDrawingPolygonPlugin = qtplugin_factory(PyDMDrawingPolygon,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("draw-polygon"))
-
-PyDMDrawingPolylinePlugin = qtplugin_factory(PyDMDrawingPolyline,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("share-alt"))
-
-PyDMDrawingIrregularPolygonPlugin = qtplugin_factory(PyDMDrawingIrregularPolygon,
- group=WidgetCategory.DRAWING,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("draw-polygon"))
+PyDMDrawingArcPlugin = qtplugin_factory(
+ PyDMDrawingArc, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("circle-notch")
+)
+PyDMDrawingChordPlugin = qtplugin_factory(
+ PyDMDrawingChord, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("moon")
+)
+PyDMDrawingCirclePlugin = qtplugin_factory(
+ PyDMDrawingCircle, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("circle")
+)
+PyDMDrawingEllipsePlugin = qtplugin_factory(
+ PyDMDrawingEllipse, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("ellipsis-h")
+)
+PyDMDrawingImagePlugin = qtplugin_factory(
+ PyDMDrawingImage, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("image")
+)
+PyDMDrawingLinePlugin = qtplugin_factory(
+ PyDMDrawingLine, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("minus")
+)
+PyDMDrawingPiePlugin = qtplugin_factory(
+ PyDMDrawingPie, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("pizza-slice")
+)
+
+PyDMDrawingRectanglePlugin = qtplugin_factory(
+ PyDMDrawingRectangle, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("border-style")
+)
+PyDMDrawingTrianglePlugin = qtplugin_factory(
+ PyDMDrawingTriangle, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("caret-up")
+)
+
+PyDMDrawingPolygonPlugin = qtplugin_factory(
+ PyDMDrawingPolygon, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("draw-polygon")
+)
+
+PyDMDrawingPolylinePlugin = qtplugin_factory(
+ PyDMDrawingPolyline, group=WidgetCategory.DRAWING, extensions=BASE_EXTENSIONS, icon=ifont.icon("share-alt")
+)
+
+PyDMDrawingIrregularPolygonPlugin = qtplugin_factory(
+ PyDMDrawingIrregularPolygon,
+ group=WidgetCategory.DRAWING,
+ extensions=BASE_EXTENSIONS,
+ icon=ifont.icon("draw-polygon"),
+)
# Embedded Display plugin
-PyDMEmbeddedDisplayPlugin = qtplugin_factory(PyDMEmbeddedDisplay,
- group=WidgetCategory.CONTAINER,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("layer-group"))
+PyDMEmbeddedDisplayPlugin = qtplugin_factory(
+ PyDMEmbeddedDisplay, group=WidgetCategory.CONTAINER, extensions=BASE_EXTENSIONS, icon=ifont.icon("layer-group")
+)
# Enum Button plugin
-PyDMEnumButtonPlugin = qtplugin_factory(PyDMEnumButton,
- group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("bars"))
+PyDMEnumButtonPlugin = qtplugin_factory(
+ PyDMEnumButton, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("bars")
+)
# Enum Combobox plugin
-PyDMEnumComboBoxPlugin = qtplugin_factory(PyDMEnumComboBox,
- group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("list-ol"))
+PyDMEnumComboBoxPlugin = qtplugin_factory(
+ PyDMEnumComboBox, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("list-ol")
+)
# Frame plugin
-PyDMFramePlugin = qtplugin_factory(PyDMFrame, group=WidgetCategory.CONTAINER,
- is_container=True,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("expand"))
+PyDMFramePlugin = qtplugin_factory(
+ PyDMFrame, group=WidgetCategory.CONTAINER, is_container=True, extensions=BASE_EXTENSIONS, icon=ifont.icon("expand")
+)
# Image plugin
-PyDMImageViewPlugin = qtplugin_factory(PyDMImageView,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("camera"))
+PyDMImageViewPlugin = qtplugin_factory(
+ PyDMImageView, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("camera")
+)
# Line Edit plugin
-PyDMLineEditPlugin = qtplugin_factory(PyDMLineEdit, group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("edit"))
+PyDMLineEditPlugin = qtplugin_factory(
+ PyDMLineEdit, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("edit")
+)
# Log Viewer
-PyDMLogDisplayPlugin = qtplugin_factory(PyDMLogDisplay,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("clipboard"))
+PyDMLogDisplayPlugin = qtplugin_factory(
+ PyDMLogDisplay, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("clipboard")
+)
# Push Button plugin
-PyDMPushButtonPlugin = qtplugin_factory(PyDMPushButton,
- group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("mouse"))
+PyDMPushButtonPlugin = qtplugin_factory(
+ PyDMPushButton, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("mouse")
+)
# Related Display Button plugin
-PyDMRelatedDisplayButtonPlugin = qtplugin_factory(PyDMRelatedDisplayButton,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon(
- "window-maximize"))
+PyDMRelatedDisplayButtonPlugin = qtplugin_factory(
+ PyDMRelatedDisplayButton,
+ group=WidgetCategory.DISPLAY,
+ extensions=BASE_EXTENSIONS,
+ icon=ifont.icon("window-maximize"),
+)
# Shell Command plugin
-PyDMShellCommandPlugin = qtplugin_factory(PyDMShellCommand,
- group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("terminal"))
+PyDMShellCommandPlugin = qtplugin_factory(
+ PyDMShellCommand, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("terminal")
+)
# Slider plugin
-PyDMSliderPlugin = qtplugin_factory(PyDMSlider, group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("sliders-h"))
+PyDMSliderPlugin = qtplugin_factory(
+ PyDMSlider, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("sliders-h")
+)
# Spinbox plugin
-PyDMSpinboxplugin = qtplugin_factory(PyDMSpinbox, group=WidgetCategory.INPUT,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("sort-numeric-up"))
+PyDMSpinboxplugin = qtplugin_factory(
+ PyDMSpinbox, group=WidgetCategory.INPUT, extensions=BASE_EXTENSIONS, icon=ifont.icon("sort-numeric-up")
+)
# Scale Indicator plugin
-PyDMScaleIndicatorPlugin = qtplugin_factory(PyDMScaleIndicator,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("level-up-alt")
- )
+PyDMScaleIndicatorPlugin = qtplugin_factory(
+ PyDMScaleIndicator, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("level-up-alt")
+)
# Analog Indicator plugin
-PyDMAnalogIndicatorPlugin = qtplugin_factory(PyDMAnalogIndicator,
- group=WidgetCategory.DISPLAY,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("level-up-alt")
- )
+PyDMAnalogIndicatorPlugin = qtplugin_factory(
+ PyDMAnalogIndicator, group=WidgetCategory.DISPLAY, extensions=BASE_EXTENSIONS, icon=ifont.icon("level-up-alt")
+)
# Symbol plugin
-PyDMSymbolPlugin = qtplugin_factory(PyDMSymbol, group=WidgetCategory.DISPLAY,
- extensions=[SymbolExtension,
- RulesExtension],
- icon=ifont.icon("icons"))
+PyDMSymbolPlugin = qtplugin_factory(
+ PyDMSymbol, group=WidgetCategory.DISPLAY, extensions=[SymbolExtension, RulesExtension], icon=ifont.icon("icons")
+)
# Waveform Table plugin
-PyDMWaveformTablePlugin = qtplugin_factory(PyDMWaveformTable,
- 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"))
+PyDMWaveformTablePlugin = qtplugin_factory(
+ PyDMWaveformTable, 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)
# Template Repeater plugin
-PyDMTemplateRepeaterPlugin = qtplugin_factory(PyDMTemplateRepeater,
- group=WidgetCategory.CONTAINER,
- extensions=BASE_EXTENSIONS,
- icon=ifont.icon("align-justify"))
+PyDMTemplateRepeaterPlugin = qtplugin_factory(
+ PyDMTemplateRepeater, group=WidgetCategory.CONTAINER, extensions=BASE_EXTENSIONS, icon=ifont.icon("align-justify")
+)
# Terminator Widget plugin
-PyDMTerminatorPlugin = qtplugin_factory(PyDMTerminator,
- group=WidgetCategory.MISC,
- extensions=BASE_EXTENSIONS)
+PyDMTerminatorPlugin = qtplugin_factory(PyDMTerminator, group=WidgetCategory.MISC, extensions=BASE_EXTENSIONS)
# **********************************************
# NOTE: Add in new PyDM widgets above this line.
diff --git a/pydm/widgets/related_display_button.py b/pydm/widgets/related_display_button.py
index 08df4dfd72..06505d095c 100644
--- a/pydm/widgets/related_display_button.py
+++ b/pydm/widgets/related_display_button.py
@@ -4,22 +4,20 @@
import warnings
from functools import partial
import hashlib
-from qtpy.QtWidgets import QPushButton, QMenu, QAction, QMessageBox, QInputDialog, QLineEdit, QWidget
-from qtpy.QtGui import QCursor, QIcon, QMouseEvent
+from qtpy.QtWidgets import QPushButton, QMenu, QAction, QMessageBox, QInputDialog, QLineEdit, QWidget, QStyle
+from qtpy.QtGui import QCursor, QIcon, QMouseEvent, QColor
from qtpy.QtCore import Slot, Property, Qt, QSize, QPoint
+from qtpy import QtDesigner
from .base import PyDMWidget, only_if_channel_set
from ..utilities import IconFont, find_file, is_pydm_app
from ..utilities.macro import parse_macro_string
from ..utilities.stylesheet import merge_widget_stylesheet
-from ..display import (load_file, ScreenTarget)
+from ..display import load_file, ScreenTarget
from typing import Optional, List
logger = logging.getLogger(__name__)
-_relatedDisplayRuleProperties = {
- 'Text': ['setText', str],
- 'Filenames': ['filenames', list]
- }
+_relatedDisplayRuleProperties = {"Text": ["setText", str], "Filenames": ["filenames", list]}
class PyDMRelatedDisplayButton(QPushButton, PyDMWidget, new_properties=_relatedDisplayRuleProperties):
@@ -36,11 +34,14 @@ class PyDMRelatedDisplayButton(QPushButton, PyDMWidget, new_properties=_relatedD
init_channel : str, optional
The channel to be used by the widget
"""
+
# Constants for determining where to open the display.
EXISTING_WINDOW = 0
NEW_WINDOW = 1
- def __init__(self,parent: Optional[QWidget] = None, filename: str = None, init_channel: Optional[str] = None) -> None:
+ def __init__(
+ self, parent: Optional[QWidget] = None, filename: str = None, init_channel: Optional[str] = None
+ ) -> None:
QPushButton.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
@@ -64,8 +65,7 @@ def __init__(self,parent: Optional[QWidget] = None, filename: str = None, init_c
self._macro_string = None
self._open_in_new_window = False
self.open_in_new_window_action = QAction("Open in New Window", self)
- self.open_in_new_window_action.triggered.connect(
- self.handle_open_new_window_action)
+ self.open_in_new_window_action.triggered.connect(self.handle_open_new_window_action)
self._show_icon = True
self._menu_needs_rebuild = True
@@ -75,6 +75,15 @@ def __init__(self,parent: Optional[QWidget] = None, filename: str = None, init_c
self._follow_symlinks = False
+ # Standard icons (which come with the qt install, and work cross-platform),
+ # and icons from the "Font Awesome" icon set (https://fontawesome.com/)
+ # can not be set with a widget's "icon" property in designer, only in python.
+ # so we provide our own property to specify standard icons and set them with python in the prop's setter.
+ self._pydm_icon_name = ""
+ # The color of "Font Awesome" icons can be set,
+ # but standard icons are already colored and can not be set.
+ self._pydm_icon_color = QColor(90, 90, 90)
+
@only_if_channel_set
def check_enable_state(self) -> None:
"""
@@ -84,29 +93,97 @@ def check_enable_state(self) -> None:
status = self._connected
tooltip = self.restore_original_tooltip()
if not status:
- if tooltip != '':
- tooltip += '\n'
+ if tooltip != "":
+ tooltip += "\n"
tooltip += "Alarm PV is disconnected."
- tooltip += '\n'
+ tooltip += "\n"
tooltip += self.get_address()
self.setToolTip(tooltip)
- @Property('QStringList')
+ @Property(str)
+ def PyDMIcon(self) -> str:
+ """
+ Name of icon to be set from Qt provided standard icons or from the fontawesome icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable fontawesome icons.
+
+ Returns
+ -------
+ str
+ """
+ return self._pydm_icon_name
+
+ @PyDMIcon.setter
+ def PyDMIcon(self, value: str) -> None:
+ """
+ Name of icon to be set from Qt provided standard icons or from the "Font Awesome" icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable "Font Awesome" icons.
+
+ Parameters
+ ----------
+ value : str
+ """
+ if self._pydm_icon_name == value:
+ return
+
+ # We don't know if user is trying to use a standard icon or an icon from "Font Awesome",
+ # so 1st try to create a Font Awesome one, which hits exception if icon name is not valid.
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(value, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ icon = getattr(QStyle, value, None)
+ if icon:
+ self.setIcon(self.style().standardIcon(icon))
+
+ self._pydm_icon_name = value
+
+ @Property(QColor)
+ def PyDMIconColor(self) -> QColor:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Returns
+ -------
+ QColor
+ """
+ return self._pydm_icon_color
+
+ @PyDMIconColor.setter
+ def PyDMIconColor(self, state_color: QColor) -> None:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Parameters
+ ----------
+ new_color : QColor
+ """
+ if state_color != self._pydm_icon_color:
+ self._pydm_icon_color = state_color
+ # apply the new color
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(self._pydm_icon_name, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ return
+
+ @Property("QStringList")
def filenames(self) -> List[str]:
return self._filenames
-
+
@filenames.setter
- def filenames(self, val : List[str]) -> None:
+ def filenames(self, val: List[str]) -> None:
self._filenames = val
self._menu_needs_rebuild = True
-
- @Property('QStringList')
+
+ @Property("QStringList")
def titles(self) -> List[str]:
return self._titles
-
+
@titles.setter
- def titles(self, val : List[str]) -> None:
+ def titles(self, val: List[str]) -> None:
self._titles = val
self._menu_needs_rebuild = True
@@ -124,15 +201,15 @@ def _get_items(self):
for i, filename in enumerate(self.filenames):
if not filename:
continue
- item = {'filename': filename}
+ item = {"filename": filename}
if i >= len(self.titles):
- item['title'] = filename
+ item["title"] = filename
else:
- item['title'] = self.titles[i]
+ item["title"] = self.titles[i]
if i < len(self.macros):
- item['macros'] = self.macros[i]
+ item["macros"] = self.macros[i]
else:
- item['macros'] = ""
+ item["macros"] = ""
yield item
def _rebuild_menu(self) -> None:
@@ -207,16 +284,18 @@ def displayFilename(self, value: str) -> None:
----------
value : str
"""
- warnings.warn("'PyDMRelatedDisplayButton.displayFilename' is deprecated, "
- "use 'PyDMRelatedDisplayButton.filenames' instead.")
+ warnings.warn(
+ "'PyDMRelatedDisplayButton.displayFilename' is deprecated, "
+ "use 'PyDMRelatedDisplayButton.filenames' instead."
+ )
if value:
if value in self.filenames:
return
file_list = [value]
self.filenames = self.filenames + file_list
self._display_filename = ""
-
- @Property('QStringList')
+
+ @Property("QStringList")
def macros(self) -> List[str]:
"""
The macro substitutions to use when launching the display, in JSON object format.
@@ -236,7 +315,7 @@ def macros(self, new_macros: List[str]) -> None:
----------
new_macros : list of str
"""
- #Handle the deprecated form of macros where it was a single string.
+ # Handle the deprecated form of macros where it was a single string.
if isinstance(new_macros, str):
new_macros = [new_macros]
self._macros = new_macros
@@ -267,6 +346,7 @@ def openInNewWindow(self, open_in_new: bool) -> None:
def passwordProtected(self) -> bool:
"""
Whether or not this button is password protected.
+
Returns
-------
bool
@@ -278,6 +358,7 @@ def passwordProtected(self) -> bool:
def passwordProtected(self, value: bool) -> None:
"""
Whether or not this button is password protected.
+
Parameters
----------
value : bool
@@ -291,8 +372,7 @@ def password(self) -> str:
Password to be encrypted using SHA256.
.. warning::
- To avoid issues exposing the password this method
- always returns an empty string.
+ To avoid issues exposing the password this method always returns an empty string.
Returns
-------
@@ -317,6 +397,11 @@ def password(self, value: str) -> None:
# new one, and only updates if the new password is different
self.protectedPassword = sha.hexdigest()
+ # Make sure designer knows it should save the protectedPassword field
+ formWindow = QtDesigner.QDesignerFormWindowInterface.findFormWindow(self)
+ if formWindow:
+ formWindow.cursor().setProperty("protectedPassword", self.protectedPassword)
+
@Property(str)
def protectedPassword(self) -> str:
"""
@@ -336,8 +421,9 @@ def protectedPassword(self, value: str) -> None:
@Property(bool)
def followSymlinks(self) -> bool:
"""
- If True, any symlinks in the path to filename (including the base path of the parent display) will be followed, so that it
- will always use the canonical path. If False (default), the file will be searched without canonicalizing the path beforehand.
+ If True, any symlinks in the path to filename (including the base path of the parent display) will be followed,
+ so that it will always use the canonical path. If False (default), the file will be searched without
+ canonicalizing the path beforehand.
Note that it will not work on Windows if you're using a Python version prior to 3.8.
@@ -350,8 +436,9 @@ def followSymlinks(self) -> bool:
@followSymlinks.setter
def followSymlinks(self, follow_symlinks: bool) -> None:
"""
- If True, any symlinks in the path to filename (including the base path of the parent display) will be followed, so that it
- will always use the canonical path. If False (default), the file will be searched using the non-canonical path.
+ If True, any symlinks in the path to filename (including the base path of the parent display)
+ will be followed, so that it will always use the canonical path.
+ If False (default), the file will be searched using the non-canonical path.
Note that it will not work on Windows if you're using a Python version prior to 3.8.
@@ -392,14 +479,12 @@ def push_button_release_event(self, mouse_event: QMouseEvent) -> None:
return super(PyDMRelatedDisplayButton, self).mouseReleaseEvent(mouse_event)
try:
for item in self._get_items():
- self.open_display(item['filename'], item['macros'],
- target=None)
+ self.open_display(item["filename"], item["macros"], target=None)
break
except Exception:
logger.exception("Failed to open display.")
finally:
- super(PyDMRelatedDisplayButton, self).mouseReleaseEvent(
- mouse_event)
+ super(PyDMRelatedDisplayButton, self).mouseReleaseEvent(mouse_event)
def validate_password(self) -> bool:
"""
@@ -415,8 +500,7 @@ def validate_password(self) -> bool:
if not self._password_protected:
return True
- pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:",
- QLineEdit.Password, "")
+ pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:", QLineEdit.Password, "")
pwd = str(pwd)
if not ok or pwd == "":
return False
@@ -448,18 +532,15 @@ def handle_open_new_window_action(self) -> None:
"""
for item in self._get_items():
try:
- self.open_display(item['filename'], item['macros'],
- target=self.NEW_WINDOW)
+ self.open_display(item["filename"], item["macros"], target=self.NEW_WINDOW)
except Exception:
logger.exception("Failed to open display.")
def _assemble_menu(self, menu, target=None):
for item in self._get_items():
try:
- action = menu.addAction(item['title'])
- action.triggered.connect(
- partial(self.open_display, item['filename'],
- item['macros'], target=target))
+ action = menu.addAction(item["title"])
+ action.triggered.connect(partial(self.open_display, item["filename"], item["macros"], target=target))
except Exception:
logger.exception("Failed to open display.")
@@ -526,7 +607,7 @@ def open_display(self, filename, macro_string="", target=None):
def context_menu(self):
try:
menu = super(PyDMRelatedDisplayButton, self).context_menu()
- except:
+ except Exception:
menu = QMenu(self)
if len(menu.findChildren(QAction)) > 0:
menu.addSeparator()
@@ -541,4 +622,3 @@ def context_menu(self):
def show_context_menu(self, pos: QPoint) -> None:
menu = self.context_menu()
menu.exec_(self.mapToGlobal(pos))
-
diff --git a/pydm/widgets/rules.py b/pydm/widgets/rules.py
index f59ebbdaab..bb7d7cf087 100644
--- a/pydm/widgets/rules.py
+++ b/pydm/widgets/rules.py
@@ -27,7 +27,7 @@ def unregister_widget_rules(widget):
widgets.extend(widget.findChildren(QWidget))
for child_widget in widgets:
try:
- if hasattr(child_widget, 'rules'):
+ if hasattr(child_widget, "rules"):
if child_widget.rules:
RulesDispatcher().unregister(weakref.ref(child_widget))
except Exception:
@@ -37,7 +37,7 @@ def unregister_widget_rules(widget):
def register_widget_rules(widget):
"""
Given a widget to start from, traverse the tree of child widgets,
- and try to unregister rules to any widgets.
+ and try to register rules to any widgets.
Parameters
----------
@@ -47,7 +47,7 @@ def register_widget_rules(widget):
widgets.extend(widget.findChildren(QWidget))
for child_widget in widgets:
try:
- if hasattr(child_widget, 'rules'):
+ if hasattr(child_widget, "rules"):
if child_widget.rules:
rules = json.loads(child_widget.rules)
RulesDispatcher().register(child_widget, rules)
@@ -60,6 +60,7 @@ class RulesDispatcher(object):
Singleton class responsible for handling all the interactions with the
RulesEngine and dispatch the payloads from the rules thread to the widget.
"""
+
__instance = None
def __init__(self):
@@ -120,17 +121,17 @@ def dispatch(self, payload):
The payload data including the widget for method invoking.
"""
try:
- widget = payload.pop('widget')
+ widget = payload.pop("widget")
if isinstance(widget, weakref.ref):
widget_ref = widget
widget = widget()
- if widget is None: # Widget is dead... lets unregister the ref
+ if widget is None: # Widget is dead... lets unregister the ref
self.rules_engine.unregister(widget_ref)
else:
widget.rule_evaluated(payload)
- except RuntimeError as ex:
+ except RuntimeError:
logger.debug("Widget reference was gone but not yet for Python.")
- except Exception as ex:
+ except Exception:
logger.exception("Error at RulesDispatcher.")
@@ -144,6 +145,7 @@ class RulesEngine(QThread):
rule_signal : dict
Emitted when a new value for the property is calculated by the engine.
"""
+
rule_signal = Signal(dict)
def __init__(self):
@@ -163,30 +165,26 @@ def register(self, widget, rules):
rules_db = []
for idx, rule in enumerate(rules):
- channels_list = rule.get('channels', [])
+ channels_list = rule.get("channels", [])
item = dict()
- item['rule'] = rule
- initial_val = rule.get('initial_value', "").strip()
- name = rule.get('name')
- prop = rule.get('property')
- item['initial_value'] = initial_val
- item['calculate'] = False
- item['values'] = [None] * len(channels_list)
- item['enums'] = [None] * len(channels_list)
- item['conn'] = [False] * len(channels_list)
- item['channels'] = []
+ item["rule"] = rule
+ initial_val = rule.get("initial_value", "").strip()
+ name = rule.get("name")
+ prop = rule.get("property")
+ item["initial_value"] = initial_val
+ item["calculate"] = False
+ item["values"] = [None] * len(channels_list)
+ item["enums"] = [None] * len(channels_list)
+ item["conn"] = [False] * len(channels_list)
+ item["channels"] = []
for ch_idx, ch in enumerate(channels_list):
- conn_cb = functools.partial(self.callback_conn, widget_ref,
- idx, ch_idx)
- value_cb = functools.partial(self.callback_value, widget_ref,
- idx, ch_idx, ch['trigger'])
- enums_cb = functools.partial(self.callback_enum, widget_ref,
- idx, ch_idx)
- c = PyDMChannel(ch['channel'], connection_slot=conn_cb,
- value_slot=value_cb, enum_strings_slot=enums_cb)
- item['channels'].append(c)
+ conn_cb = functools.partial(self.callback_conn, widget_ref, idx, ch_idx)
+ value_cb = functools.partial(self.callback_value, widget_ref, idx, ch_idx, ch["trigger"])
+ enums_cb = functools.partial(self.callback_enum, widget_ref, idx, ch_idx)
+ c = PyDMChannel(ch["channel"], connection_slot=conn_cb, value_slot=value_cb, enum_strings_slot=enums_cb)
+ item["channels"].append(c)
rules_db.append(item)
if initial_val and not is_qt_designer():
@@ -195,7 +193,7 @@ def register(self, widget, rules):
if rules_db:
self.widget_map[widget_ref] = rules_db
for rule in rules_db:
- for ch in rule['channels']:
+ for ch in rule["channels"]:
ch.connect()
def unregister(self, widget_ref):
@@ -212,7 +210,7 @@ def unregister(self, widget_ref):
return
for rule in w_data:
- for ch in rule['channels']:
+ for ch in rule["channels"]:
ch.disconnect(destroying=widget_ref() is None)
del w_data
@@ -225,7 +223,7 @@ def run(self):
w_map = self.widget_map.copy()
for widget_ref, rules in w_map.items():
for idx, rule in enumerate(rules):
- if rule['calculate']:
+ if rule["calculate"]:
self.calculate_expression(widget_ref, idx, rule)
self.msleep(33) # 30Hz
@@ -256,11 +254,11 @@ def callback_enum(self, widget_ref, index, ch_index, enums):
"""
try:
w_map = self.widget_map[widget_ref]
- w_map[index]['enums'][ch_index] = enums
- if not all(w_map[index]['conn']):
+ w_map[index]["enums"][ch_index] = enums
+ if not all(w_map[index]["conn"]):
self.warn_unconnected_channels(widget_ref, index)
return
- w_map[index]['calculate'] = True
+ w_map[index]["calculate"] = True
except (KeyError, IndexError):
pass
@@ -288,12 +286,12 @@ def callback_value(self, widget_ref, index, ch_index, trigger, value):
"""
try:
w_map = self.widget_map[widget_ref]
- w_map[index]['values'][ch_index] = value
+ w_map[index]["values"][ch_index] = value
if trigger:
- if not all(w_map[index]['conn']):
+ if not all(w_map[index]["conn"]):
self.warn_unconnected_channels(widget_ref, index)
return
- w_map[index]['calculate'] = True
+ w_map[index]["calculate"] = True
except (KeyError, IndexError):
pass
@@ -317,7 +315,7 @@ def callback_conn(self, widget_ref, index, ch_index, value):
None
"""
try:
- self.widget_map[widget_ref][index]['conn'][ch_index] = value
+ self.widget_map[widget_ref][index]["conn"][ch_index] = value
except (KeyError, IndexError):
# widget_ref was destroyed
pass
@@ -325,7 +323,8 @@ def callback_conn(self, widget_ref, index, ch_index, value):
def warn_unconnected_channels(self, widget_ref, index):
logger.debug(
"Rule '%s': Not all channels are connected, skipping execution.",
- self.widget_map[widget_ref][index]['rule']['name'])
+ self.widget_map[widget_ref][index]["rule"]["name"],
+ )
def calculate_expression(self, widget_ref, idx, rule):
"""
@@ -340,11 +339,11 @@ def calculate_expression(self, widget_ref, idx, rule):
-------
None
"""
- rule['calculate'] = False
+ rule["calculate"] = False
- vals = rule['values']
- enums = rule['enums']
- channel_properties = rule["rule"]['channels']
+ vals = rule["values"]
+ enums = rule["enums"]
+ channel_properties = rule["rule"]["channels"]
calc_vals = []
for en, val, chan in zip(enums, vals, channel_properties):
@@ -356,21 +355,19 @@ def calculate_expression(self, widget_ref, idx, rule):
pass
calc_vals.append(v)
- eval_env = {'np': np,
- 'ch': calc_vals}
- eval_env.update({k: v
- for k, v in math.__dict__.items()
- if k[0] != '_'})
+ eval_env = {"np": np, "ch": calc_vals}
+ eval_env.update({k: v for k, v in math.__dict__.items() if k[0] != "_"})
- expression = rule['rule']['expression']
- name = rule['rule']['name']
- prop = rule['rule']['property']
+ expression = rule["rule"]["expression"]
+ name = rule["rule"]["name"]
+ prop = rule["rule"]["property"]
try:
val = eval(expression, eval_env)
self.emit_value(widget_ref, name, prop, val)
- except Exception as e:
- logger.exception(f"Error while evaluating Rule with name: {name} and type: {prop} "
- f"and expression: {expression}")
+ except Exception:
+ logger.exception(
+ f"Error while evaluating Rule with name: {name} and type: {prop} " f"and expression: {expression}"
+ )
def emit_value(self, widget_ref, name, prop, val):
"""
@@ -387,6 +384,5 @@ def emit_value(self, widget_ref, name, prop, val):
val : object
The value to emit
"""
- payload = {'widget': widget_ref, 'name': name, 'property': prop,
- 'value': val}
+ payload = {"widget": widget_ref, "name": name, "property": prop, "value": val}
self.rule_signal.emit(payload)
diff --git a/pydm/widgets/rules_editor.py b/pydm/widgets/rules_editor.py
index 505445f94d..0e34b3940f 100644
--- a/pydm/widgets/rules_editor.py
+++ b/pydm/widgets/rules_editor.py
@@ -4,6 +4,7 @@
import webbrowser
from qtpy import QtWidgets, QtCore, QtDesigner
+from qtpy.QtCore import Qt
from ..utilities.iconfont import IconFont
@@ -32,11 +33,11 @@ def __init__(self, widget, parent=None):
try:
self.rules = json.loads(widget.rules)
- except:
+ except Exception:
self.rules = []
for ac in self.rules:
- self.lst_rules.addItem(ac.get("name", ''))
+ self.lst_rules.addItem(ac.get("name", ""))
def setup_ui(self):
"""
@@ -92,7 +93,9 @@ def setup_ui(self):
lf_layout.addLayout(lf_btn_layout)
self.lst_rules = QtWidgets.QListWidget()
- self.lst_rules.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding))
+ self.lst_rules.setSizePolicy(
+ QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)
+ )
self.lst_rules.itemSelectionChanged.connect(self.load_from_list)
lf_layout.addWidget(self.lst_rules)
@@ -180,8 +183,7 @@ def setup_ui(self):
lbl_expected = QtWidgets.QLabel("Expected Type:")
self.lbl_expected_type = QtWidgets.QLabel(parent=self)
# self.lbl_expected_type.setText("")
- self.lbl_expected_type.setStyleSheet(
- "color: rgb(0, 128, 255); font-weight: bold;")
+ self.lbl_expected_type.setStyleSheet("color: rgb(0, 128, 255); font-weight: bold;")
expression_layout.addRow(lbl_expected, self.lbl_expected_type)
lbl_initial = QtWidgets.QLabel("Initial Value:")
@@ -190,24 +192,72 @@ def setup_ui(self):
expression_layout.addRow(lbl_initial, self.txt_initial_value)
lbl_expression = QtWidgets.QLabel("Expression:")
- expr_help_layout = QtWidgets.QHBoxLayout()
+ expr_layout = QtWidgets.QHBoxLayout()
self.txt_expression = QtWidgets.QLineEdit()
self.txt_expression.editingFinished.connect(self.expression_changed)
- expr_help_layout.addWidget(self.txt_expression)
+ expr_layout.addWidget(self.txt_expression)
+ expression_layout.addRow(lbl_expression, expr_layout)
+
+ notes_help_layout = QtWidgets.QHBoxLayout()
self.btn_help = QtWidgets.QPushButton()
self.btn_help.setAutoDefault(False)
self.btn_help.setDefault(False)
self.btn_help.setText("Help")
self.btn_help.setStyleSheet("background-color: rgb(176, 227, 255);")
self.btn_help.clicked.connect(functools.partial(self.open_help, open=True))
- expr_help_layout.addWidget(self.btn_help)
- expression_layout.addRow(lbl_expression, expr_help_layout)
+ self.btn_open_notes = QtWidgets.QPushButton("Notes")
+ notes_help_layout.addWidget(self.btn_open_notes)
+ self.btn_open_notes.clicked.connect(self.open_notes_window)
+ notes_help_layout.addWidget(self.btn_help)
+ expression_layout.addRow(notes_help_layout)
self.cmb_property.currentIndexChanged.connect(self.property_changed)
self.cmb_property.setCurrentText(self.default_property)
frm_edit_layout.addLayout(expression_layout)
+ def open_notes_window(self):
+ """Window for reading and adding to a rule's notes"""
+ self.notes_window = QtWidgets.QDialog(self)
+ self.notes_window.setWindowTitle("Notes")
+
+ # Allow window to be resized and maximized
+ self.notes_window.setWindowFlag(Qt.WindowMinMaxButtonsHint)
+ notes_layout = QtWidgets.QVBoxLayout()
+
+ self.notes_title_label = QtWidgets.QLabel("Notes:")
+ notes_layout.addWidget(self.notes_title_label)
+
+ self.notes_edit = QtWidgets.QPlainTextEdit(self)
+
+ # Load notes saved in current instance of rules editor
+ item = self.lst_rules.currentItem()
+ idx = self.lst_rules.indexFromItem(item).row()
+ data = self.rules[idx]
+ curr_notes = data.get("notes", "")
+ self.notes_edit.setPlainText(curr_notes)
+ notes_layout.addWidget(self.notes_edit)
+
+ buttons_layout = QtWidgets.QHBoxLayout()
+ self.save_button = QtWidgets.QPushButton("Save")
+ self.save_button.clicked.connect(self.save_notes)
+ self.cancel_button = QtWidgets.QPushButton("Cancel")
+ self.cancel_button.clicked.connect(self.notes_window.close)
+ buttons_layout.addStretch()
+ buttons_layout.addWidget(self.cancel_button)
+ buttons_layout.addWidget(self.save_button)
+
+ notes_layout.addLayout(buttons_layout)
+ self.notes_window.setLayout(notes_layout)
+ self.notes_window.show()
+
+ def save_notes(self):
+ """Save notes text and close the notes window"""
+ item = self.lst_rules.currentItem()
+ idx = self.lst_rules.indexFromItem(item).row()
+ self.rules[idx]["notes"] = self.notes_edit.toPlainText()
+ self.notes_window.close()
+
def clear_form(self):
"""Clear the form and reset the fields."""
self.loading_data = True
@@ -237,24 +287,23 @@ def load_from_list(self):
self.loading_data = True
self.lst_rule_item = item
data = self.rules[idx]
- self.txt_name.setText(data.get('name', ''))
- self.cmb_property.setCurrentText(data.get('property', ''))
+ self.txt_name.setText(data.get("name", ""))
+ self.cmb_property.setCurrentText(data.get("property", ""))
self.property_changed(0)
- self.txt_initial_value.setText(data.get('initial_value', ''))
- self.txt_expression.setText(data.get('expression', ''))
+ self.txt_initial_value.setText(data.get("initial_value", ""))
+ self.txt_expression.setText(data.get("expression", ""))
- channels = data.get('channels', [])
+ channels = data.get("channels", [])
self.tbl_channels.clearContents()
self.tbl_channels.setRowCount(len(channels))
vlabel = [str(i) for i in range(len(channels))]
self.tbl_channels.setVerticalHeaderLabels(vlabel)
ch_choices = {True: QtCore.Qt.Checked, False: QtCore.Qt.Unchecked}
for row, ch in enumerate(channels):
- ch_name = ch.get('channel', '')
- ch_tr = ch.get('trigger', False)
- ch_use_enum = ch.get('use_enum', False)
- self.tbl_channels.setItem(row, 0,
- QtWidgets.QTableWidgetItem(str(ch_name)))
+ ch_name = ch.get("channel", "")
+ ch_tr = ch.get("trigger", False)
+ ch_use_enum = ch.get("use_enum", False)
+ self.tbl_channels.setItem(row, 0, QtWidgets.QTableWidgetItem(str(ch_name)))
checkbox_trigger = QtWidgets.QTableWidgetItem()
checkbox_trigger.setCheckState(ch_choices[ch_tr])
self.tbl_channels.setItem(row, 1, checkbox_trigger)
@@ -267,12 +316,14 @@ def load_from_list(self):
def add_rule(self):
"""Add a new rule to the list of rules."""
default_name = "New Rule"
- data = {"name": default_name,
- "property": self.default_property,
- "initial_value": "",
- "expression": "",
- "channels": []
- }
+ data = {
+ "name": default_name,
+ "property": self.default_property,
+ "initial_value": "",
+ "expression": "",
+ "channels": [],
+ "notes": "",
+ }
self.rules.append(data)
self.lst_rule_item = QtWidgets.QListWidgetItem()
self.lst_rule_item.setText(default_name)
@@ -321,12 +372,10 @@ def del_rule(self):
return
name = self.lst_rule_item.text()
- confirm_message = "Are you sure you want to delete Rule: {}?".format(
- name)
- reply = QtWidgets.QMessageBox().question(self, 'Message',
- confirm_message,
- QtWidgets.QMessageBox.Yes,
- QtWidgets.QMessageBox.No)
+ confirm_message = "Are you sure you want to delete Rule: {}?".format(name)
+ reply = QtWidgets.QMessageBox().question(
+ self, "Message", confirm_message, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
+ )
if reply == QtWidgets.QMessageBox.Yes:
self.lst_rules.takeItem(idx)
@@ -349,11 +398,11 @@ def add_channel(self):
self.tbl_channels.setItem(row, 0, QtWidgets.QTableWidgetItem(""))
checkbox_trigger = QtWidgets.QTableWidgetItem()
checkbox_trigger.setCheckState(state)
- checkbox_trigger.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsUserCheckable)
+ checkbox_trigger.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable)
self.tbl_channels.setItem(row, 1, checkbox_trigger)
checkbox_use_enum = QtWidgets.QTableWidgetItem()
checkbox_use_enum.setCheckState(QtCore.Qt.Checked)
- checkbox_use_enum.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsUserCheckable)
+ checkbox_use_enum.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable)
self.tbl_channels.setItem(row, 2, checkbox_use_enum)
vlabel = [str(i) for i in range(self.tbl_channels.rowCount())]
self.tbl_channels.setVerticalHeaderLabels(vlabel)
@@ -368,10 +417,9 @@ def del_channel(self):
c = "channel" if len(items) == 1 else "channels"
confirm_message = "Delete the selected {}?".format(c)
- reply = QtWidgets.QMessageBox().question(self, 'Message',
- confirm_message,
- QtWidgets.QMessageBox.Yes,
- QtWidgets.QMessageBox.No)
+ reply = QtWidgets.QMessageBox().question(
+ self, "Message", confirm_message, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
+ )
if reply == QtWidgets.QMessageBox.Yes:
for itm in reversed(items):
@@ -414,9 +462,9 @@ def property_changed(self, index):
try:
prop = self.cmb_property.currentData()
self.lbl_expected_type.setText(prop[1].__name__)
- idx = self.get_current_index()
+ self.get_current_index()
self.change_entry("property", self.cmb_property.currentText())
- except:
+ except Exception:
self.lbl_expected_type.setText("")
def tbl_channels_changed(self, topleft=None, bottomright=None, roles=None):
@@ -428,10 +476,8 @@ def tbl_channels_changed(self, topleft=None, bottomright=None, roles=None):
for row in range(self.tbl_channels.rowCount()):
ch = self.tbl_channels.item(row, 0).text()
- tr = self.tbl_channels.item(row,
- 1).checkState() == QtCore.Qt.Checked
- en = self.tbl_channels.item(row,
- 2).checkState() == QtCore.Qt.Checked
+ tr = self.tbl_channels.item(row, 1).checkState() == QtCore.Qt.Checked
+ en = self.tbl_channels.item(row, 2).checkState() == QtCore.Qt.Checked
new_channels.append({"channel": ch, "trigger": tr, "use_enum": en})
self.change_entry("channels", new_channels)
@@ -476,15 +522,12 @@ def is_data_valid(rules):
found_trigger = False
for ch_idx, ch in enumerate(channels):
if not ch.get("channel", ""):
- errors.append(
- "Rule #{} - Ch. #{} has no channel.".format(idx + 1,
- ch_idx))
+ errors.append("Rule #{} - Ch. #{} has no channel.".format(idx + 1, ch_idx))
if ch.get("trigger", False) and not found_trigger:
found_trigger = True
if not found_trigger:
- errors.append(
- "Rule #{} has no channel for trigger.".format(idx + 1))
+ errors.append("Rule #{} has no channel for trigger.".format(idx + 1))
if len(errors) > 0:
error_msg = os.linesep.join(errors)
@@ -512,8 +555,7 @@ def saveChanges(self):
formWindow.cursor().setProperty("rules", data)
self.accept()
else:
- QtWidgets.QMessageBox.critical(self, "Error Saving", message,
- QtWidgets.QMessageBox.Ok)
+ QtWidgets.QMessageBox.critical(self, "Error Saving", message, QtWidgets.QMessageBox.Ok)
@QtCore.Slot()
def cancelChanges(self):
diff --git a/pydm/widgets/scale.py b/pydm/widgets/scale.py
index ded43bc3ed..b6650a7e8f 100644
--- a/pydm/widgets/scale.py
+++ b/pydm/widgets/scale.py
@@ -17,6 +17,7 @@ class QScale(QFrame):
parent : QWidget
The parent widget for the Scale
"""
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
super(QScale, self).__init__(parent)
self._value = 1
@@ -24,17 +25,17 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self._upper_limit = 5
self.position = None # unit: pixel
- self._bg_color = QColor('darkgray')
- self._bg_size_rate = 0.8 # from 0 to 1
+ self._bg_color = QColor("darkgray")
+ self._bg_size_rate = 0.8 # from 0 to 1
- self._indicator_color = QColor('black')
+ self._indicator_color = QColor("black")
self._pointer_width_rate = 0.05
self._barIndicator = False
self._num_divisions = 10
self._show_ticks = True
self._tick_pen = QPen()
- self._tick_color = QColor('black')
+ self._tick_color = QColor("black")
self._tick_width = 0
self._tick_size_rate = 0.1 # from 0 to 1
self._painter = QPainter()
@@ -111,9 +112,9 @@ def draw_ticks(self) -> None:
self._painter.setPen(self._tick_pen)
division_size = self._widget_width / self._num_divisions
tick_y0 = self._widget_height
- tick_yf = int((1 - self._tick_size_rate)*self._widget_height)
- for i in range(self._num_divisions+1):
- x = int(i*division_size)
+ tick_yf = int((1 - self._tick_size_rate) * self._widget_height)
+ for i in range(self._num_divisions + 1):
+ x = int(i * division_size)
self._painter.drawLine(x, tick_y0, x, tick_yf) # x1, y1, x2, y2
def draw_bar(self) -> None:
@@ -146,7 +147,7 @@ def draw_pointer(self) -> None:
QPoint(self.position, 0),
QPoint(self.position + pointer_width // 2, pointer_height // 2),
QPoint(self.position, pointer_height),
- QPoint(self.position - pointer_width // 2, pointer_height // 2)
+ QPoint(self.position - pointer_width // 2, pointer_height // 2),
]
self._painter.drawPolygon(QPolygon(points))
@@ -186,7 +187,7 @@ def paintEvent(self, event: QPaintEvent) -> None:
self._painter.translate(self._painter_translation_x, 0) # Invert appearance if needed
self._painter.scale(self._painter_scale_x, 1)
- self._painter.translate(0, self._flip_traslation_y) # Invert scale if needed
+ self._painter.translate(0, self._flip_traslation_y) # Invert scale if needed
self._painter.scale(1, self._flip_scale_y)
self._painter.setRenderHint(QPainter.Antialiasing)
@@ -201,8 +202,12 @@ def calculate_position_for_value(self, value: float) -> int:
"""
Calculate the position (pixel) in which the pointer should be drawn for a given value.
"""
- if value is None or value < self._lower_limit or value > self._upper_limit or \
- self._upper_limit - self._lower_limit == 0:
+ if (
+ value is None
+ or value < self._lower_limit
+ or value > self._upper_limit
+ or self._upper_limit - self._lower_limit == 0
+ ):
proportion = -1 # Invalid
else:
proportion = (value - self._lower_limit) / (self._upper_limit - self._lower_limit)
@@ -389,9 +394,9 @@ def __init__(self, parent: Optional[QWidget] = None, init_channel: Optional[str]
self.lower_label = QLabel()
self.upper_label = QLabel()
- self.value_label.setText('')
- self.lower_label.setText('')
- self.upper_label.setText('')
+ self.value_label.setText("")
+ self.lower_label.setText("")
+ self.upper_label.setText("")
self._value_position = Qt.TopEdge
self._limits_from_channel = True
@@ -452,8 +457,9 @@ def lowerCtrlLimitChanged(self, new_limit: float) -> None:
self.scale_indicator.set_lower_limit(new_limit)
self.update_labels()
- def setup_widgets_for_orientation(self, new_orientation: Qt.Orientation, flipped: bool, inverted: bool,
- value_position: Qt.Edge) -> None:
+ def setup_widgets_for_orientation(
+ self, new_orientation: Qt.Orientation, flipped: bool, inverted: bool, value_position: Qt.Edge
+ ) -> None:
"""
Reconstruct the widget given the orientation.
@@ -529,8 +535,7 @@ def setup_widgets_for_orientation(self, new_orientation: Qt.Orientation, flipped
elif new_orientation == Qt.Vertical:
self.limits_layout = QVBoxLayout()
- if (value_position == Qt.RightEdge and not flipped) or \
- (value_position == Qt.LeftEdge and flipped):
+ if (value_position == Qt.RightEdge and not flipped) or (value_position == Qt.LeftEdge and flipped):
add_value_between_limits = True
else:
add_value_between_limits = False
@@ -949,12 +954,12 @@ def valuePosition(self) -> Qt.Edge:
@valuePosition.setter
def valuePosition(self, position: Qt.Edge) -> None:
"""
- The position of the value label (Top, Bottom, Left or Right).
+ The position of the value label (Top, Bottom, Left or Right).
- Parameters
- ----------
- position : int
- Qt.TopEdge, Qt.BottomEdge, Qt.LeftEdge or Qt.RightEdge
+ Parameters
+ ----------
+ position : int
+ Qt.TopEdge, Qt.BottomEdge, Qt.LeftEdge or Qt.RightEdge
"""
self._value_position = position
self.setup_widgets_for_orientation(self.orientation, self.flipScale, self.invertedAppearance, position)
diff --git a/pydm/widgets/scatterplot.py b/pydm/widgets/scatterplot.py
index e8af4b14ae..bce7cbae76 100644
--- a/pydm/widgets/scatterplot.py
+++ b/pydm/widgets/scatterplot.py
@@ -14,7 +14,7 @@
class ScatterPlotCurveItem(BasePlotCurveItem):
- _channels = ('x_channel', 'y_channel')
+ _channels = ("x_channel", "y_channel")
def __init__(self, y_addr, x_addr, redraw_mode=None, bufferSizeChannelAddress=None, **kws):
self.x_channel = None
@@ -24,29 +24,27 @@ def __init__(self, y_addr, x_addr, redraw_mode=None, bufferSizeChannelAddress=No
self.x_connected = False
self.y_connected = False
# If a name wasn't specified, use the addresses to make one.
- if kws.get('name') is None:
+ if kws.get("name") is None:
if y_addr is None and x_addr is None:
- kws['name'] = ""
+ kws["name"] = ""
else:
y_name = remove_protocol(y_addr if y_addr is not None else "")
x_name = remove_protocol(x_addr if x_addr is not None else "")
- kws['name'] = "{y} vs. {x}".format(y=y_name, x=x_name)
- self.redraw_mode = (redraw_mode if redraw_mode is not None
- else self.REDRAW_ON_EITHER)
+ kws["name"] = "{y} vs. {x}".format(y=y_name, x=x_name)
+ self.redraw_mode = redraw_mode if redraw_mode is not None else self.REDRAW_ON_EITHER
self.bufferSizeChannel = None
self.bufferSizeChannel_connected = False
self._bufferSize = DEFAULT_BUFFER_SIZE
- self.data_buffer = np.zeros((2, self._bufferSize),
- order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
self.points_accumulated = 0
self.latest_x_value = None
self.latest_y_value = None
self.needs_new_x = True
self.needs_new_y = True
- if 'symbol' not in kws.keys():
- kws['symbol'] = 'o'
- if 'lineStyle' not in kws.keys():
- kws['lineStyle'] = Qt.NoPen
+ if "symbol" not in kws.keys():
+ kws["symbol"] = "o"
+ if "lineStyle" not in kws.keys():
+ kws["lineStyle"] = Qt.NoPen
super(ScatterPlotCurveItem, self).__init__(**kws)
self.bufferSizeChannelAddress = bufferSizeChannelAddress
@@ -60,11 +58,10 @@ def to_dict(self):
Representation with values for all properties
needed to recreate this curve.
"""
- dic_ = OrderedDict([("y_channel", self.y_address),
- ("x_channel", self.x_address)])
+ dic_ = OrderedDict([("y_channel", self.y_address), ("x_channel", self.x_address)])
dic_.update(super(ScatterPlotCurveItem, self).to_dict())
dic_["redraw_mode"] = self.redraw_mode
- dic_['buffer_size'] = self.getBufferSize()
+ dic_["buffer_size"] = self.getBufferSize()
dic_["bufferSizeChannelAddress"] = self.bufferSizeChannelAddress
return dic_
@@ -95,9 +92,8 @@ def x_address(self, new_address):
self.x_channel = None
return
self.x_channel = PyDMChannel(
- address=new_address,
- connection_slot=self.xConnectionStateChanged,
- value_slot=self.receiveXValue)
+ address=new_address, connection_slot=self.xConnectionStateChanged, value_slot=self.receiveXValue
+ )
@property
def y_address(self):
@@ -126,9 +122,8 @@ def y_address(self, new_address):
self.y_channel = None
return
self.y_channel = PyDMChannel(
- address=new_address,
- connection_slot=self.yConnectionStateChanged,
- value_slot=self.receiveYValue)
+ address=new_address, connection_slot=self.yConnectionStateChanged, value_slot=self.receiveYValue
+ )
@Slot(bool)
def xConnectionStateChanged(self, connected):
@@ -213,8 +208,7 @@ def update_buffer(self):
def initialize_buffer(self):
self.points_accumulated = 0
- self.data_buffer = np.zeros((2, self._bufferSize),
- order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
def getBufferSize(self):
return int(self._bufferSize)
@@ -260,7 +254,8 @@ def bufferSizeChannelAddress(self, new_address):
self.bufferSizeChannel = PyDMChannel(
address=new_address,
connection_slot=self.bufferSizeConnectionStateChanged,
- value_slot=self.bufferSizeChannelValueReceiver)
+ value_slot=self.bufferSizeChannelValueReceiver,
+ )
self.bufferSizeChannel.connect()
@Slot(bool)
@@ -288,8 +283,10 @@ def redrawCurve(self):
Called by the curve's parent plot whenever the curve needs to be
re-drawn with new data.
"""
- self.setData(x=self.data_buffer[0, -self.points_accumulated:].astype(float),
- y=self.data_buffer[1, -self.points_accumulated:].astype(float))
+ self.setData(
+ x=self.data_buffer[0, -self.points_accumulated :].astype(float),
+ y=self.data_buffer[1, -self.points_accumulated :].astype(float),
+ )
self.needs_new_x = True
self.needs_new_y = True
@@ -304,10 +301,9 @@ def limits(self):
"""
if self.points_accumulated == 0:
raise NoDataError("Curve has no data, cannot determine limits.")
- x_data = self.data_buffer[0, -self.points_accumulated:]
- y_data = self.data_buffer[1, -self.points_accumulated:]
- return ((float(np.amin(x_data)), float(np.amax(x_data))),
- (float(np.amin(y_data)), float(np.amax(y_data))))
+ x_data = self.data_buffer[0, -self.points_accumulated :]
+ y_data = self.data_buffer[1, -self.points_accumulated :]
+ return ((float(np.amin(x_data)), float(np.amax(x_data))), (float(np.amin(y_data)), float(np.amax(y_data))))
def channels(self):
return [self.y_channel, self.x_channel]
@@ -343,8 +339,8 @@ class PyDMScatterPlot(BasePlot):
The background color for the plot. Accepts any arguments that
pyqtgraph.mkColor will accept.
"""
- def __init__(self, parent=None, init_x_channels=[], init_y_channels=[],
- background='default'):
+
+ def __init__(self, parent=None, init_x_channels=[], init_y_channels=[], background="default"):
super(PyDMScatterPlot, self).__init__(parent, background)
# If the user supplies a single string instead of a list,
# wrap it in a list.
@@ -353,17 +349,15 @@ def __init__(self, parent=None, init_x_channels=[], init_y_channels=[],
if isinstance(init_y_channels, str):
init_y_channels = [init_y_channels]
if len(init_x_channels) == 0:
- init_x_channels = list(itertools.repeat(None,
- len(init_y_channels)))
+ init_x_channels = list(itertools.repeat(None, len(init_y_channels)))
if len(init_x_channels) != len(init_y_channels):
- raise ValueError("If lists are provided for both X and Y " +
- "channels, they must be the same length.")
+ raise ValueError("If lists are provided for both X and Y " + "channels, they must be the same length.")
# self.channel_pairs is an ordered dictionary that is keyed on a
# (x_channel, y_channel) tuple, with ScatterPlotCurveItem values.
# It gets populated in self.addChannel().
self.channel_pairs = OrderedDict()
init_channel_pairs = zip(init_x_channels, init_y_channels)
- for (x_chan, y_chan) in init_channel_pairs:
+ for x_chan, y_chan in init_channel_pairs:
self.addChannel(y_channel=y_chan, x_channel=x_chan)
self._needs_redraw = True
@@ -372,10 +366,21 @@ def initialize_for_designer(self):
# This function gets called by PyDMTimePlot's designer plugin.
pass
- def addChannel(self, y_channel=None, x_channel=None, name=None,
- color=None, lineStyle=None, lineWidth=None,
- symbol=None, symbolSize=None, redraw_mode=None,
- buffer_size=None, yAxisName=None, bufferSizeChannelAddress=None):
+ def addChannel(
+ self,
+ y_channel=None,
+ x_channel=None,
+ name=None,
+ color=None,
+ lineStyle=None,
+ lineWidth=None,
+ symbol=None,
+ symbolSize=None,
+ redraw_mode=None,
+ buffer_size=None,
+ yAxisName=None,
+ bufferSizeChannelAddress=None,
+ ):
"""
Add a new curve to the plot. In addition to the arguments below,
all other keyword arguments are passed to the underlying
@@ -419,28 +424,33 @@ def addChannel(self, y_channel=None, x_channel=None, name=None,
doesn't yet exist
"""
plot_opts = {}
- plot_opts['symbol'] = symbol
+ plot_opts["symbol"] = symbol
if symbolSize is not None:
- plot_opts['symbolSize'] = symbolSize
+ plot_opts["symbolSize"] = symbolSize
if lineStyle is not None:
- plot_opts['lineStyle'] = lineStyle
+ plot_opts["lineStyle"] = lineStyle
if lineWidth is not None:
- plot_opts['lineWidth'] = lineWidth
+ plot_opts["lineWidth"] = lineWidth
if redraw_mode is not None:
- plot_opts['redraw_mode'] = redraw_mode
- curve = ScatterPlotCurveItem(y_addr=y_channel,
- x_addr=x_channel,
- name=name,
- color=color,
- yAxisName=yAxisName,
- bufferSizeChannelAddress=bufferSizeChannelAddress,
- **plot_opts)
+ plot_opts["redraw_mode"] = redraw_mode
+ curve = self.createCurveItem(
+ y_addr=y_channel,
+ x_addr=x_channel,
+ name=name,
+ color=color,
+ yAxisName=yAxisName,
+ bufferSizeChannelAddress=bufferSizeChannelAddress,
+ **plot_opts
+ )
if buffer_size is not None:
curve.setBufferSize(buffer_size)
self.channel_pairs[(x_channel, y_channel)] = curve
self.addCurve(curve, curve_color=color, y_axis_name=yAxisName)
curve.data_changed.connect(self.set_needs_redraw)
+ def createCurveItem(self, *args, **kwargs):
+ return ScatterPlotCurveItem(*args, **kwargs)
+
def removeChannel(self, curve):
"""
Remove a curve from the plot.
@@ -512,19 +522,23 @@ def setCurves(self, new_list):
return
self.clearCurves()
for d in new_list:
- color = d.get('color')
+ color = d.get("color")
if color:
color = QColor(color)
- self.addChannel(y_channel=d['y_channel'], x_channel=d['x_channel'],
- name=d.get('name'), color=color,
- lineStyle=d.get('lineStyle'),
- lineWidth=d.get('lineWidth'),
- symbol=d.get('symbol'),
- symbolSize=d.get('symbolSize'),
- redraw_mode=d.get('redraw_mode'),
- buffer_size=d.get('buffer_size'),
- bufferSizeChannelAddress=d.get('bufferSizeChannelAddress'),
- yAxisName=d.get('yAxisName'))
+ self.addChannel(
+ y_channel=d["y_channel"],
+ x_channel=d["x_channel"],
+ name=d.get("name"),
+ color=color,
+ lineStyle=d.get("lineStyle"),
+ lineWidth=d.get("lineWidth"),
+ symbol=d.get("symbol"),
+ symbolSize=d.get("symbolSize"),
+ redraw_mode=d.get("redraw_mode"),
+ buffer_size=d.get("buffer_size"),
+ bufferSizeChannelAddress=d.get("bufferSizeChannelAddress"),
+ yAxisName=d.get("yAxisName"),
+ )
curves = Property("QStringList", getCurves, setCurves, designable=False)
@@ -539,39 +553,61 @@ def channels(self):
chans = []
chans.extend([curve.y_channel for curve in self._curves])
chans.extend([curve.x_channel for curve in self._curves])
- chans.extend([curve.bufferSizeChannel
- for curve in self._curves
- if curve.bufferSizeChannel is not None])
+ chans.extend([curve.bufferSizeChannel for curve in self._curves if curve.bufferSizeChannel is not None])
return chans
# The methods for autoRangeX, minXRange, maxXRange, autoRangeY, minYRange,
# and maxYRange are all defined in BasePlot, but we don't expose them as
# properties there, because not all plot subclasses necessarily want
# them to be user-configurable in Designer.
- autoRangeX = Property(bool, BasePlot.getAutoRangeX,
- BasePlot.setAutoRangeX, BasePlot.resetAutoRangeX,
- doc="""
+ autoRangeX = Property(
+ bool,
+ BasePlot.getAutoRangeX,
+ BasePlot.setAutoRangeX,
+ BasePlot.resetAutoRangeX,
+ doc="""
Whether or not the X-axis automatically rescales to fit the data.
-If true, the values in minXRange and maxXRange are ignored.""")
-
- minXRange = Property(float, BasePlot.getMinXRange,
- BasePlot.setMinXRange, doc="""
-Minimum X-axis value visible on the plot.""")
-
- maxXRange = Property(float, BasePlot.getMaxXRange,
- BasePlot.setMaxXRange, doc="""
-Maximum X-axis value visible on the plot.""")
-
- autoRangeY = Property(bool, BasePlot.getAutoRangeY,
- BasePlot.setAutoRangeY, BasePlot.resetAutoRangeY,
- doc="""
+If true, the values in minXRange and maxXRange are ignored.""",
+ )
+
+ minXRange = Property(
+ float,
+ BasePlot.getMinXRange,
+ BasePlot.setMinXRange,
+ doc="""
+Minimum X-axis value visible on the plot.""",
+ )
+
+ maxXRange = Property(
+ float,
+ BasePlot.getMaxXRange,
+ BasePlot.setMaxXRange,
+ doc="""
+Maximum X-axis value visible on the plot.""",
+ )
+
+ autoRangeY = Property(
+ bool,
+ BasePlot.getAutoRangeY,
+ BasePlot.setAutoRangeY,
+ BasePlot.resetAutoRangeY,
+ doc="""
Whether or not the Y-axis automatically rescales to fit the data.
-If true, the values in minYRange and maxYRange are ignored.""")
-
- minYRange = Property(float, BasePlot.getMinYRange,
- BasePlot.setMinYRange, doc="""
-Minimum Y-axis value visible on the plot.""")
-
- maxYRange = Property(float, BasePlot.getMaxYRange,
- BasePlot.setMaxYRange, doc="""
-Maximum Y-axis value visible on the plot.""")
+If true, the values in minYRange and maxYRange are ignored.""",
+ )
+
+ minYRange = Property(
+ float,
+ BasePlot.getMinYRange,
+ BasePlot.setMinYRange,
+ doc="""
+Minimum Y-axis value visible on the plot.""",
+ )
+
+ maxYRange = Property(
+ float,
+ BasePlot.getMaxYRange,
+ BasePlot.setMaxYRange,
+ doc="""
+Maximum Y-axis value visible on the plot.""",
+ )
diff --git a/pydm/widgets/scatterplot_curve_editor.py b/pydm/widgets/scatterplot_curve_editor.py
index 03b90a43eb..92ea0030ac 100644
--- a/pydm/widgets/scatterplot_curve_editor.py
+++ b/pydm/widgets/scatterplot_curve_editor.py
@@ -4,15 +4,15 @@
class PyDMScatterPlotCurvesModel(BasePlotCurvesModel):
- """ This is the data model used by the waveform plot curve editor.
+ """This is the data model used by the waveform plot curve editor.
It basically acts as a go-between for the curves in a plot, and
QTableView items.
"""
def __init__(self, plot, parent=None):
super(PyDMScatterPlotCurvesModel, self).__init__(plot, parent=parent)
- self._column_names = ('Y Channel', 'X Channel') + self._column_names
- self._column_names += ('Redraw Mode', 'Buffer Size', 'Buffer Size Channel')
+ self._column_names = ("Y Channel", "X Channel") + self._column_names
+ self._column_names += ("Redraw Mode", "Buffer Size", "Buffer Size Channel")
def get_data(self, column_name, curve):
if column_name == "Y Channel":
@@ -29,8 +29,7 @@ def get_data(self, column_name, curve):
return curve.getBufferSize()
elif column_name == "Buffer Size Channel":
return curve.bufferSizeChannelAddress or ""
- return super(PyDMScatterPlotCurvesModel, self).get_data(
- column_name, curve)
+ return super(PyDMScatterPlotCurvesModel, self).get_data(column_name, curve)
def set_data(self, column_name, curve, value):
if column_name == "Y Channel":
@@ -46,13 +45,11 @@ def set_data(self, column_name, curve, value):
value = None
curve.bufferSizeChannelAddress = str(value)
else:
- return super(PyDMScatterPlotCurvesModel, self).set_data(
- column_name=column_name, curve=curve, value=value)
+ return super(PyDMScatterPlotCurvesModel, self).set_data(column_name=column_name, curve=curve, value=value)
return True
def append(self, y_address=None, x_address=None, name=None, color=None):
- self.beginInsertRows(QModelIndex(), len(self._plot._curves),
- len(self._plot._curves))
+ self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves))
self._plot.addChannel(y_address, x_address, name, color)
self.endInsertRows()
@@ -70,6 +67,7 @@ class ScatterPlotCurveEditorDialog(BasePlotCurveEditorDialog):
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = PyDMScatterPlotCurvesModel
def __init__(self, plot, parent=None):
diff --git a/pydm/widgets/shell_command.py b/pydm/widgets/shell_command.py
index 3919a9615b..ac12804110 100644
--- a/pydm/widgets/shell_command.py
+++ b/pydm/widgets/shell_command.py
@@ -7,9 +7,10 @@
import warnings
import hashlib
from ast import literal_eval
-from qtpy.QtWidgets import QPushButton, QMenu, QMessageBox, QInputDialog, QLineEdit, QWidget
-from qtpy.QtGui import QCursor, QIcon, QMouseEvent
+from qtpy.QtWidgets import QPushButton, QMenu, QMessageBox, QInputDialog, QLineEdit, QWidget, QStyle
+from qtpy.QtGui import QCursor, QIcon, QMouseEvent, QColor
from qtpy.QtCore import Property, QSize, Qt, QTimer
+from qtpy import QtDesigner
from .base import PyDMWidget, only_if_channel_set
from ..utilities import IconFont
from typing import Optional, Union, List
@@ -35,15 +36,18 @@ class PyDMShellCommand(QPushButton, PyDMWidget):
DEFAULT_CONFIRM_MESSAGE = "Are you sure you want to proceed?"
- def __init__(self, parent: Optional[QWidget] = None,
- command: Optional[Union[str, List[str]]] = None,
- title: Optional[Union[str, List[str]]] = None,
- init_channel: Optional[str] = None) -> None:
+ def __init__(
+ self,
+ parent: Optional[QWidget] = None,
+ command: Optional[Union[str, List[str]]] = None,
+ title: Optional[Union[str, List[str]]] = None,
+ init_channel: Optional[str] = None,
+ ) -> None:
QPushButton.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
self.iconFont = IconFont()
self._icon = self.iconFont.icon("cog")
- self._warning_icon = self.iconFont.icon('exclamation-circle')
+ self._warning_icon = self.iconFont.icon("exclamation-circle")
self.setIconSize(QSize(16, 16))
self.setIcon(self._icon)
self.setCursor(QCursor(self._icon.pixmap(16, 16)))
@@ -64,6 +68,9 @@ def __init__(self, parent: Optional[QWidget] = None,
self.process = None
self._show_icon = True
self._redirect_output = False
+ # shell allows for more options such as command chaining ("cmd1;cmd2", "cmd1 && cmd2", etc ...),
+ # use of environment variables, glob expansion ('ls *.txt'), etc...
+ self._run_commands_in_full_shell = False
self._password_protected = False
self._password = ""
@@ -72,7 +79,16 @@ def __init__(self, parent: Optional[QWidget] = None,
self._show_confirm_dialog = False
self._confirm_message = PyDMShellCommand.DEFAULT_CONFIRM_MESSAGE
-
+
+ # Standard icons (which come with the qt install, and work cross-platform),
+ # and icons from the "Font Awesome" icon set (https://fontawesome.com/)
+ # can not be set with a widget's "icon" property in designer, only in python.
+ # so we provide our own property to specify standard icons and set them with python in the prop's setter.
+ self._pydm_icon_name = ""
+ # The color of "Font Awesome" icons can be set,
+ # but standard icons are already colored and can not be set.
+ self._pydm_icon_color = QColor(90, 90, 90)
+
def confirmDialog(self) -> bool:
"""
Show the confirmation dialog with the proper message in case
@@ -87,7 +103,7 @@ def confirmDialog(self) -> bool:
if self._show_confirm_dialog:
if self._confirm_message == "":
self._confirm_message = PyDMShellCommand.DEFAULT_CONFIRM_MESSAGE
-
+
msg = QMessageBox()
msg.setIcon(QMessageBox.Question)
msg.setText(self._confirm_message)
@@ -99,10 +115,78 @@ def confirmDialog(self) -> bool:
return True
+ @Property(str)
+ def PyDMIcon(self) -> str:
+ """
+ Name of icon to be set from Qt provided standard icons or from the fontawesome icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable fontawesome icons.
+
+ Returns
+ -------
+ str
+ """
+ return self._pydm_icon_name
+
+ @PyDMIcon.setter
+ def PyDMIcon(self, value: str) -> None:
+ """
+ Name of icon to be set from Qt provided standard icons or from the "Font Awesome" icon-set.
+ See "enum QStyle::StandardPixmap" in Qt's QStyle documentation for full list of usable standard icons.
+ See https://fontawesome.com/icons?d=gallery for list of usable "Font Awesome" icons.
+
+ Parameters
+ ----------
+ value : str
+ """
+ if self._pydm_icon_name == value:
+ return
+
+ # We don't know if user is trying to use a standard icon or an icon from "Font Awesome",
+ # so 1st try to create a Font Awesome one, which hits exception if icon name is not valid.
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(value, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ icon = getattr(QStyle, value, None)
+ if icon:
+ self.setIcon(self.style().standardIcon(icon))
+
+ self._pydm_icon_name = value
+
+ @Property(QColor)
+ def PyDMIconColor(self) -> QColor:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Returns
+ -------
+ QColor
+ """
+ return self._pydm_icon_color
+
+ @PyDMIconColor.setter
+ def PyDMIconColor(self, state_color: QColor) -> None:
+ """
+ The color of the icon (color is only applied if using icon from the "Font Awesome" set)
+ Parameters
+ ----------
+ new_color : QColor
+ """
+ if state_color != self._pydm_icon_color:
+ self._pydm_icon_color = state_color
+ # apply the new color
+ try:
+ icon_f = IconFont()
+ i = icon_f.icon(self._pydm_icon_name, color=self._pydm_icon_color)
+ self.setIcon(i)
+ except Exception:
+ return
+
@Property(bool)
def showConfirmDialog(self) -> bool:
"""
- Wether or not to display a confirmation dialog.
+ Whether or not to display a confirmation dialog.
Returns
-------
@@ -113,7 +197,7 @@ def showConfirmDialog(self) -> bool:
@showConfirmDialog.setter
def showConfirmDialog(self, value: bool) -> None:
"""
- Wether or not to display a confirmation dialog.
+ Whether or not to display a confirmation dialog.
Parameters
----------
@@ -122,6 +206,29 @@ def showConfirmDialog(self, value: bool) -> None:
if self._show_confirm_dialog != value:
self._show_confirm_dialog = value
+ @Property(bool)
+ def runCommandsInFullShell(self) -> bool:
+ """
+ Whether or not to run cmds with Popen's option for running them through a shell subprocess.
+
+ Returns
+ -------
+ bool
+ """
+ return self._run_commands_in_full_shell
+
+ @runCommandsInFullShell.setter
+ def runCommandsInFullShell(self, value: bool) -> None:
+ """
+ Whether or not to run cmds with Popen's option for running them through a shell subprocess.
+
+ Parameters
+ ----------
+ value : bool
+ """
+ if self._run_commands_in_full_shell != value:
+ self._run_commands_in_full_shell = value
+
@Property(str)
def confirmMessage(self) -> str:
"""
@@ -154,14 +261,14 @@ def check_enable_state(self) -> None:
status = self._connected
tooltip = self.restore_original_tooltip()
if not status:
- if tooltip != '':
- tooltip += '\n'
+ if tooltip != "":
+ tooltip += "\n"
tooltip += "Alarm PV is disconnected."
- tooltip += '\n'
+ tooltip += "\n"
tooltip += self.get_address()
self.setToolTip(tooltip)
-
+
@Property(str)
def environmentVariables(self) -> str:
"""
@@ -266,7 +373,7 @@ def allowMultipleExecutions(self, value: bool) -> None:
if self._allow_multiple != value:
self._allow_multiple = value
- @Property('QStringList')
+ @Property("QStringList")
def titles(self) -> List[str]:
return self._titles
@@ -275,7 +382,7 @@ def titles(self, val: List[str]) -> None:
self._titles = val
self._menu_needs_rebuild = True
- @Property('QStringList')
+ @Property("QStringList")
def commands(self) -> List[str]:
return self._commands
@@ -315,8 +422,7 @@ def command(self, value: str) -> None:
----------
value : str
"""
- warnings.warn("'PyDMShellCommand.command' is deprecated, "
- "use 'PyDMShellCommand.commands' instead.")
+ warnings.warn("'PyDMShellCommand.command' is deprecated, " "use 'PyDMShellCommand.commands' instead.")
if not self._commands:
if value:
self.commands = [value]
@@ -327,6 +433,7 @@ def command(self, value: str) -> None:
def passwordProtected(self) -> bool:
"""
Whether or not this button is password protected.
+
Returns
-------
bool
@@ -338,6 +445,7 @@ def passwordProtected(self) -> bool:
def passwordProtected(self, value: bool) -> None:
"""
Whether or not this button is password protected.
+
Parameters
----------
value : bool
@@ -351,8 +459,7 @@ def password(self) -> str:
Password to be encrypted using SHA256.
.. warning::
- To avoid issues exposing the password this method
- always returns an empty string.
+ To avoid issues exposing the password this method always returns an empty string.
Returns
-------
@@ -377,6 +484,11 @@ def password(self, value: str) -> None:
# new one, and only updates if the new password is different
self.protectedPassword = sha.hexdigest()
+ # Make sure designer knows it should save the protectedPassword field
+ formWindow = QtDesigner.QDesignerFormWindowInterface.findFormWindow(self)
+ if formWindow:
+ formWindow.cursor().setProperty("protectedPassword", self.protectedPassword)
+
@Property(str)
def protectedPassword(self) -> str:
"""
@@ -393,10 +505,10 @@ def protectedPassword(self, value: str) -> None:
"""
Setter for the encrypted password.
- Parameters
- -------
- value: str
- """
+ Parameters
+ -------
+ value: str
+ """
if self._protected_password != value:
self._protected_password = value
@@ -447,13 +559,13 @@ def mouseReleaseEvent(self, mouse_event: QMouseEvent) -> None:
super(PyDMShellCommand, self).mouseReleaseEvent(mouse_event)
def show_warning_icon(self) -> None:
- """ Show the warning icon. This is called when a shell command fails
- (i.e. exits with nonzero status) """
+ """Show the warning icon. This is called when a shell command fails
+ (i.e. exits with nonzero status)"""
self.setIcon(self._warning_icon)
QTimer.singleShot(5000, self.hide_warning_icon)
def hide_warning_icon(self) -> None:
- """ Hide the warning icon. This is called on a timer after the warning
+ """Hide the warning icon. This is called on a timer after the warning
icon is shown."""
if self._show_icon:
self.setIcon(self._icon)
@@ -474,8 +586,7 @@ def validate_password(self) -> bool:
if not self._password_protected:
return True
- pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:",
- QLineEdit.Password, "")
+ pwd, ok = QInputDialog().getText(None, "Authentication", "Please enter your password:", QLineEdit.Password, "")
pwd = str(pwd)
if not ok or pwd == "":
return False
@@ -502,7 +613,7 @@ def execute_command(self, command: str) -> None:
Parameters
----------
- command : str
+ command : str
Shell command
"""
if not command:
@@ -517,7 +628,10 @@ def execute_command(self, command: str) -> None:
if (self.process is None or self.process.poll() is not None) or self._allow_multiple:
cmd = os.path.expanduser(os.path.expandvars(command))
- args = shlex.split(cmd, posix='win' not in sys.platform)
+ args = shlex.split(cmd, posix="win" not in sys.platform)
+ # when shell enabled, Popen should take the cmds as a single string (not list)
+ if self._run_commands_in_full_shell:
+ args = cmd
try:
logger.debug("Launching process: %s", repr(args))
stdout = subprocess.PIPE
@@ -530,10 +644,11 @@ def execute_command(self, command: str) -> None:
if self._redirect_output:
stdout = None
self.process = subprocess.Popen(
- args, stdout=stdout, stderr=subprocess.PIPE, env=env_var)
+ args, stdout=stdout, stderr=subprocess.PIPE, env=env_var, shell=self._run_commands_in_full_shell
+ )
+
except Exception as exc:
self.show_warning_icon()
logger.error("Error in shell command: %s", exc)
else:
logger.error("Command '%s' already active.", command)
-
diff --git a/pydm/widgets/slider.py b/pydm/widgets/slider.py
index 4c47e647e1..6dca604617 100644
--- a/pydm/widgets/slider.py
+++ b/pydm/widgets/slider.py
@@ -2,8 +2,19 @@
import numpy as np
from decimal import Decimal
from qtpy.QtCore import Qt, Signal, Slot, Property
-from qtpy.QtWidgets import QFrame, QLabel, QSlider, QVBoxLayout, QHBoxLayout, QSizePolicy, \
- QWidget, QLineEdit, QPushButton, QCheckBox, QComboBox
+from qtpy.QtWidgets import (
+ QFrame,
+ QLabel,
+ QSlider,
+ QVBoxLayout,
+ QHBoxLayout,
+ QSizePolicy,
+ QWidget,
+ QLineEdit,
+ QPushButton,
+ QCheckBox,
+ QComboBox,
+)
from pydm.widgets import PyDMLabel
from .base import PyDMWritableWidget, TextFormatter, is_channel_valid
from .channel import PyDMChannel
@@ -23,6 +34,7 @@ class PyDMSlider(QFrame, TextFormatter, PyDMWritableWidget):
init_channel : str, optional
The channel to be used by the widget.
"""
+
actionTriggered = Signal(int)
rangeChanged = Signal(float, float)
sliderMoved = Signal(float)
@@ -132,11 +144,20 @@ def slider_parameters_menu(self, position_of_click):
self.slider_parameters_menu_buttons = []
self.menu_layout = []
- text_info = ['Value', 'Increment', 'Increment scale', 'Precision', 'Precision from PV', 'Number Format',
- 'OK', 'Apply', 'Cancel']
-
- combo_box_table_scale = ['1e0', '1e1', '1e2', '1e3', '1e4', '1e5']
- combo_box_table_format = ['Float', 'Exp']
+ text_info = [
+ "Value",
+ "Increment",
+ "Increment scale",
+ "Precision",
+ "Precision from PV",
+ "Number Format",
+ "OK",
+ "Apply",
+ "Cancel",
+ ]
+
+ combo_box_table_scale = ["1e0", "1e1", "1e2", "1e3", "1e4", "1e5"]
+ combo_box_table_format = ["Float", "Exp"]
for key in range(0, 6):
self.slider_parameters_menu_input_widgets.append(key)
@@ -176,7 +197,7 @@ def slider_parameters_menu(self, position_of_click):
for key in range(0, 3):
self.slider_parameters_menu_buttons.append(key)
self.slider_parameters_menu_buttons[key] = QPushButton(self.slider_parameters_menu_widget)
- self.slider_parameters_menu_buttons[key].setText(text_info[key+6])
+ self.slider_parameters_menu_buttons[key].setText(text_info[key + 6])
self.menu_layout[3].addWidget(self.slider_parameters_menu_buttons[key])
main_layout.addLayout(self.menu_layout[3])
@@ -198,7 +219,7 @@ def apply_step_size_menu_changes(self):
"""
try:
new_step_size = float(self.slider_parameters_menu_input_widgets[1].text())
- new_step_size_scaled = new_step_size*float(self.slider_parameters_menu_input_widgets[2].currentText())
+ new_step_size_scaled = new_step_size * float(self.slider_parameters_menu_input_widgets[2].currentText())
if new_step_size_scaled > 0:
self.step_size = new_step_size_scaled
@@ -238,7 +259,7 @@ def apply_step_size_menu_changes(self):
format_type = self.slider_parameters_menu_input_widgets[5].currentText()
- if format_type == 'Float':
+ if format_type == "Float":
self.update_labels()
else:
self.update_labels(True)
@@ -323,6 +344,7 @@ def update_labels(self, exp=False):
"""
Update the limits and value labels with the correct values.
"""
+
def set_label(value, label_widget, exp_format):
if value is None:
label_widget.setText("")
@@ -330,7 +352,7 @@ def set_label(value, label_widget, exp_format):
if not exp_format:
label_widget.setText(self.format_string.format(value))
else:
- text = '%.' + str(self.precision) + 'E'
+ text = "%." + str(self.precision) + "E"
text = text % Decimal(str(value))
if self._show_units and self._unit != "":
text += " {}".format(self._unit)
@@ -381,10 +403,11 @@ def float_range(self):
-------
new_indexes : list of floats
"""
- scale = 10 ** (len(str(self.step_size)) - str(self.step_size).find('.') - 1)
- new_indexes_scaled = list(range(int(self.minimum * scale), int((self.maximum + self.step_size) * scale),
- int(self.step_size * scale)))
- new_indexes = [(index/scale) for index in new_indexes_scaled]
+ scale = 10 ** (len(str(self.step_size)) - str(self.step_size).find(".") - 1)
+ new_indexes_scaled = list(
+ range(int(self.minimum * scale), int((self.maximum + self.step_size) * scale), int(self.step_size * scale))
+ )
+ new_indexes = [(index / scale) for index in new_indexes_scaled]
return new_indexes
def find_closest_slider_position_to_value(self, val):
@@ -507,7 +530,7 @@ def update_format_string(self):
def set_enable_state(self):
"""
- Determines wether or not the widget must be enabled or not depending
+ Determines Whether or not the widget must be enabled or not depending
on the write access, connection state and presence of limits information
"""
# Even though by documentation disabling parent QFrame (self), should disable internal
@@ -580,18 +603,18 @@ def tracking(self):
timespan.
"""
return self._slider.hasTracking()
-
+
@tracking.setter
def tracking(self, checked):
self._slider.setTracking(checked)
-
+
def hasTracking(self):
"""
An alternative function to get the tracking property, to match what
Qt provides for QSlider.
"""
return self.tracking
-
+
def setTracking(self, checked):
"""
An alternative function to set the tracking property, to match what
@@ -607,7 +630,7 @@ def ignoreMouseWheel(self):
don't want to accidentally change the slider as you are scrolling.
"""
return self._ignore_mouse_wheel
-
+
@ignoreMouseWheel.setter
def ignoreMouseWheel(self, checked):
self._ignore_mouse_wheel = checked
@@ -695,7 +718,7 @@ def tickPosition(self, position):
@Property(bool)
def userDefinedLimits(self):
"""
- Wether or not to use limits defined by the user and not from the
+ Whether or not to use limits defined by the user and not from the
channel
Returns
@@ -707,7 +730,7 @@ def userDefinedLimits(self):
@userDefinedLimits.setter
def userDefinedLimits(self, user_defined_limits):
"""
- Wether or not to use limits defined by the user and not from the
+ Whether or not to use limits defined by the user and not from the
channel
Parameters
diff --git a/pydm/widgets/spinbox.py b/pydm/widgets/spinbox.py
index 10ed9fad06..d9ddda1479 100644
--- a/pydm/widgets/spinbox.py
+++ b/pydm/widgets/spinbox.py
@@ -1,5 +1,5 @@
from qtpy.QtWidgets import QDoubleSpinBox, QApplication, QLineEdit
-from qtpy.QtCore import Property, QEvent, Qt
+from qtpy.QtCore import Property, Qt
from .base import PyDMWritableWidget, TextFormatter
@@ -14,6 +14,7 @@ class PyDMSpinbox(QDoubleSpinBox, TextFormatter, PyDMWritableWidget):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
QDoubleSpinBox.__init__(self, parent)
PyDMWritableWidget.__init__(self, init_channel=init_channel)
@@ -72,7 +73,7 @@ def keyPressEvent(self, ev):
elif ev.key() in (Qt.Key_Return, Qt.Key_Enter):
self.send_value()
-
+
else:
super(PyDMSpinbox, self).keyPressEvent(ev)
@@ -85,6 +86,7 @@ def widget_ctx_menu(self):
QMenu or None
If the return of this method is None a new QMenu will be created by `assemble_tools_menu`.
"""
+
def toggle_step():
self.showStepExponent = not self.showStepExponent
@@ -93,10 +95,10 @@ def toggle_write():
menu = self.lineEdit().createStandardContextMenu()
menu.addSeparator()
- ac = menu.addAction('Toggle Show Step Size')
+ ac = menu.addAction("Toggle Show Step Size")
ac.triggered.connect(toggle_step)
- ac_write = menu.addAction('Toggle Write On Press')
+ ac_write = menu.addAction("Toggle Write On Press")
ac_write.triggered.connect(toggle_write)
return menu
@@ -105,7 +107,7 @@ def update_step_size(self):
"""
Update the Single Step size on the QDoubleSpinBox.
"""
- self.setSingleStep(10 ** self.step_exponent)
+ self.setSingleStep(10**self.step_exponent)
self.update_format_string()
def update_format_string(self):
@@ -127,7 +129,7 @@ def update_format_string(self):
self.lineEdit().setToolTip("")
else:
self.setSuffix(units)
- self.lineEdit().setToolTip('Step: 1E{0:+d}'.format(self.step_exponent))
+ self.lineEdit().setToolTip("Step: 1E{0:+d}".format(self.step_exponent))
def value_changed(self, new_val):
"""
diff --git a/pydm/widgets/symbol.py b/pydm/widgets/symbol.py
index 3f13738518..9f376f9e38 100644
--- a/pydm/widgets/symbol.py
+++ b/pydm/widgets/symbol.py
@@ -5,12 +5,13 @@
from qtpy.QtGui import QPainter, QPixmap
from qtpy.QtCore import Property, Qt, QSize, QSizeF, QRectF, qInstallMessageHandler
from qtpy.QtSvg import QSvgRenderer
-from ..utilities import is_pydm_app, find_file
+from ..utilities import find_file
from .base import PyDMWidget
logger = logging.getLogger(__name__)
-_symbolRuleProperties = {'Index': ['set_current_key', int]}
+_symbolRuleProperties = {"Index": ["set_current_key", int]}
+
class PyDMSymbol(QWidget, PyDMWidget, new_properties=_symbolRuleProperties):
"""
@@ -23,6 +24,7 @@ class PyDMSymbol(QWidget, PyDMWidget, new_properties=_symbolRuleProperties):
init_channel : str, optional
The channel to be used by the widget.
"""
+
def __init__(self, parent=None, init_channel=None):
QWidget.__init__(self, parent)
PyDMWidget.__init__(self, init_channel=init_channel)
@@ -91,7 +93,7 @@ def imageFiles(self, new_files):
if parent_display:
base_path = os.path.dirname(parent_display.loaded_file())
- for (state, filename) in new_file_dict.items():
+ for state, filename in new_file_dict.items():
file_path = find_file(filename, base_path=base_path)
# First, lets try SVG. We have to try SVG first, otherwise
# QPixmap will happily load the SVG and turn it into a raster image.
diff --git a/pydm/widgets/symbol_editor.py b/pydm/widgets/symbol_editor.py
index b2a06eb667..b6fa80e341 100644
--- a/pydm/widgets/symbol_editor.py
+++ b/pydm/widgets/symbol_editor.py
@@ -2,12 +2,11 @@
import json
from qtpy import QtWidgets, QtCore, QtDesigner
-from qtpy.QtWidgets import QApplication, QWidget, QStyle, QStyleOption
+from qtpy.QtWidgets import QStyle, QStyleOption
from qtpy.QtGui import QPainter, QPixmap
-from qtpy.QtCore import Property, Qt, QSize, QSizeF, QRectF, qInstallMessageHandler
+from qtpy.QtCore import Qt, QSize, QSizeF, QRectF, qInstallMessageHandler
from qtpy.QtSvg import QSvgRenderer
-from ..utilities import is_pydm_app, is_qt_designer, find_file
-from .base import PyDMWidget
+from ..utilities import is_qt_designer, find_file
class SymbolEditor(QtWidgets.QDialog):
@@ -23,7 +22,7 @@ class SymbolEditor(QtWidgets.QDialog):
def __init__(self, widget, parent=None):
super(SymbolEditor, self).__init__(parent)
-
+
self.widget = widget
self.lst_file_item = None
self.lst_state_item = None
@@ -33,7 +32,7 @@ def __init__(self, widget, parent=None):
try:
self.symbols = json.loads(widget.imageFiles)
- except:
+ except Exception:
self.symbols = {}
for state, filename in self.symbols.items():
@@ -223,10 +222,9 @@ def del_symbol(self):
return
confirm_message = "Delete the selected symbol?"
- reply = QtWidgets.QMessageBox().question(self, 'Message',
- confirm_message,
- QtWidgets.QMessageBox.Yes,
- QtWidgets.QMessageBox.No)
+ reply = QtWidgets.QMessageBox().question(
+ self, "Message", confirm_message, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
+ )
if reply == QtWidgets.QMessageBox.Yes:
row = self.tbl_symbols.currentRow()
@@ -292,7 +290,7 @@ def paintEvent(self, event):
sf = min(size.width() / w, size.height() / h)
scale = (sf, sf)
_painter.scale(scale[0], scale[1])
- _painter.drawPixmap(335/sf, 120/sf, image_to_draw)
+ _painter.drawPixmap(335 / sf, 120 / sf, image_to_draw)
elif isinstance(image_to_draw, QSvgRenderer):
draw_size = QSizeF(image_to_draw.defaultSize())
draw_size.scale(QSizeF(size), Qt.KeepAspectRatio)
@@ -331,8 +329,7 @@ def check_image(self, filename):
parent_display = self.widget.find_parent_display()
base_path = None
if parent_display:
- base_path = os.path.dirname(
- parent_display.loaded_file())
+ base_path = os.path.dirname(parent_display.loaded_file())
abs_path = find_file(abs_path, base_path=base_path)
except Exception as ex:
print("Exception: ", ex)
@@ -416,8 +413,7 @@ def saveChanges(self):
formWindow.cursor().setProperty("imageFiles", data)
self.accept()
else:
- QtWidgets.QMessageBox.critical(self, "Error Saving", message,
- QtWidgets.QMessageBox.Ok)
+ QtWidgets.QMessageBox.critical(self, "Error Saving", message, QtWidgets.QMessageBox.Ok)
@QtCore.Slot()
def cancelChanges(self):
diff --git a/pydm/widgets/tab_bar.py b/pydm/widgets/tab_bar.py
index ddc7fadd7e..aa731b4068 100644
--- a/pydm/widgets/tab_bar.py
+++ b/pydm/widgets/tab_bar.py
@@ -1,4 +1,4 @@
-from qtpy.QtWidgets import (QTabBar, QTabWidget, QWidget)
+from qtpy.QtWidgets import QTabBar, QTabWidget
from qtpy.QtGui import QIcon, QColor
from qtpy.QtCore import QByteArray
from .base import PyDMWidget
@@ -33,7 +33,7 @@ def currentTabAlarmChannel(self):
try:
return str(self.tab_channels[self.currentIndex()]["address"])
except KeyError:
- return ''
+ return ""
@currentTabAlarmChannel.setter
def currentTabAlarmChannel(self, new_alarm_channel):
@@ -59,9 +59,11 @@ def set_channel_for_tab(self, index, channel):
self.set_initial_icon_for_tab(index)
if channel:
# Create PyDMChannel and connect
- chan = PyDMChannel(address=str(channel),
- connection_slot=partial(self.connection_changed_for_tab, index),
- severity_slot=partial(self.alarm_changed_for_tab, index))
+ chan = PyDMChannel(
+ address=str(channel),
+ connection_slot=partial(self.connection_changed_for_tab, index),
+ severity_slot=partial(self.alarm_changed_for_tab, index),
+ )
self.tab_channels[index]["channel"] = chan
chan.connect()
self._channels.append(chan)
@@ -97,8 +99,7 @@ def set_initial_icon_for_tab(self, index):
self.setTabIcon(index, QIcon())
else:
icon_index = self.ALARM_DISCONNECTED
- if self.tab_connection_status.get(index,
- False) and index in self.tab_alarm_severity:
+ if self.tab_connection_status.get(index, False) and index in self.tab_alarm_severity:
icon_index = self.tab_alarm_severity[index]
self.setTabIcon(index, self.alarm_icons[icon_index])
@@ -160,14 +161,11 @@ def disconnectedAlarmIconColor(self, new_color):
def generate_alarm_icons(self):
self.alarm_icons = (
- IconFont().icon('circle', color=self.noAlarmIconColor),
- IconFont().icon('circle', color=self.minorAlarmIconColor),
- IconFont().icon('exclamation-circle',
- color=self.majorAlarmIconColor),
- IconFont().icon('question-circle',
- color=self.invalidAlarmIconColor),
- IconFont().icon('times-circle',
- color=self.disconnectedAlarmIconColor)
+ IconFont().icon("circle", color=self.noAlarmIconColor),
+ IconFont().icon("circle", color=self.minorAlarmIconColor),
+ IconFont().icon("exclamation-circle", color=self.majorAlarmIconColor),
+ IconFont().icon("question-circle", color=self.invalidAlarmIconColor),
+ IconFont().icon("times-circle", color=self.disconnectedAlarmIconColor),
)
for i in range(0, self.count()):
self.set_initial_icon_for_tab(i)
@@ -200,8 +198,7 @@ def currentTabAlarmChannel(self):
str
"""
if self.tabBar().currentTabAlarmChannel:
- return bytearray(
- self.tabBar().currentTabAlarmChannel.encode('utf-8'))
+ return bytearray(self.tabBar().currentTabAlarmChannel.encode("utf-8"))
else:
return bytearray()
@@ -315,8 +312,7 @@ def disconnectedAlarmIconColor(self):
def disconnectedAlarmIconColor(self, new_color):
self.tabBar().disconnectedAlarmIconColor = new_color
- alarmChannels = Property("QStringList", getAlarmChannels,
- setAlarmChannels, designable=False)
+ alarmChannels = Property("QStringList", getAlarmChannels, setAlarmChannels, designable=False)
# We make a bunch of dummy properties to block out properties available on QTabWidget,
# but that we don't want to support on PyDMTabWidget.
diff --git a/pydm/widgets/tab_bar_qtplugin.py b/pydm/widgets/tab_bar_qtplugin.py
index 2f706da020..d218bfb7d1 100644
--- a/pydm/widgets/tab_bar_qtplugin.py
+++ b/pydm/widgets/tab_bar_qtplugin.py
@@ -5,33 +5,33 @@
class TabWidgetPlugin(PyDMDesignerPlugin):
"""TabWidgetPlugin needs a custom plugin so that it can
populate itself with an initial tab."""
+
TabClass = PyDMTabWidget
def __init__(self, extensions=None):
- super(TabWidgetPlugin, self).__init__(self.TabClass,
- group=WidgetCategory.CONTAINER,
- extensions=extensions)
+ super(TabWidgetPlugin, self).__init__(self.TabClass, group=WidgetCategory.CONTAINER, extensions=extensions)
def domXml(self):
"""
XML Description of the widget's properties.
"""
- return ("\n"
- " \n"
- " {1}\n"
- " \n"
- " \n"
- " {2}\n"
- " \n"
- "\n"
- "\n"
- " \n"
- "\n"
- "\n"
- "\n"
- " \n"
- " Page 1\n"
- " \n"
- "\n"
- ""
- ).format(self.name(), self.toolTip(), self.whatsThis())
+ return (
+ '\n'
+ ' \n'
+ " {1}\n"
+ " \n"
+ ' \n'
+ " {2}\n"
+ " \n"
+ '\n'
+ "\n"
+ " \n"
+ "\n"
+ "\n"
+ '\n'
+ ' \n'
+ " Page 1\n"
+ " \n"
+ "\n"
+ ""
+ ).format(self.name(), self.toolTip(), self.whatsThis())
diff --git a/pydm/widgets/template_repeater.py b/pydm/widgets/template_repeater.py
index 46c4cc62f2..fbb2ef805b 100644
--- a/pydm/widgets/template_repeater.py
+++ b/pydm/widgets/template_repeater.py
@@ -2,15 +2,14 @@
import json
import copy
import logging
-from qtpy.QtWidgets import (QFrame, QApplication, QLabel, QVBoxLayout,
- QHBoxLayout, QWidget, QStyle, QSizePolicy,
- QLayout)
+from qtpy.QtWidgets import QFrame, QApplication, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QStyle, QSizePolicy, QLayout
from qtpy.QtCore import Qt, QSize, QRect, Property, QPoint, Q_ENUMS
from .base import PyDMPrimitiveWidget
from pydm.utilities import is_qt_designer
import pydm.data_plugins
-from ..utilities import macro, find_file
+from ..utilities import find_file
from ..display import load_file
+
logger = logging.getLogger(__name__)
@@ -21,61 +20,61 @@ def __init__(self, parent=None, margin=-1, h_spacing=-1, v_spacing=-1):
self.m_h_space = h_spacing
self.m_v_space = v_spacing
self.item_list = []
-
+
def addItem(self, item):
self.item_list.append(item)
-
+
def horizontalSpacing(self):
if self.m_h_space >= 0:
return self.m_h_space
else:
return self.smart_spacing(QStyle.PM_LayoutHorizontalSpacing)
-
+
def verticalSpacing(self):
if self.m_v_space >= 0:
return self.m_v_space
else:
return self.smart_spacing(QStyle.PM_LayoutVerticalSpacing)
-
+
def count(self):
return len(self.item_list)
-
+
def itemAt(self, index):
if index >= 0 and index < len(self.item_list):
return self.item_list[index]
else:
return None
-
+
def takeAt(self, index):
if index >= 0 and index < len(self.item_list):
return self.item_list.pop(index)
else:
return None
-
+
def expandingDirections(self):
return Qt.Orientations(0)
-
+
def hasHeightForWidth(self):
return True
-
+
def heightForWidth(self, width):
- return self.do_layout(QRect(0,0, width, 0), True)
-
+ return self.do_layout(QRect(0, 0, width, 0), True)
+
def setGeometry(self, rect):
super(FlowLayout, self).setGeometry(rect)
self.do_layout(rect, False)
-
+
def sizeHint(self):
return self.minimumSize()
-
+
def minimumSize(self):
size = QSize()
for item in self.item_list:
size = size.expandedTo(item.minimumSize())
- #size += QSize(2*self.margin(), 2*self.margin())
- size += QSize(2*8, 2*8)
+ # size += QSize(2*self.margin(), 2*self.margin())
+ size += QSize(2 * 8, 2 * 8)
return size
-
+
def do_layout(self, rect, test_only):
(left, top, right, bottom) = self.getContentsMargins()
effective_rect = rect.adjusted(left, top, -right, -bottom)
@@ -101,7 +100,7 @@ def do_layout(self, rect, test_only):
x = next_x
line_height = max(line_height, item.sizeHint().height())
return y + line_height - rect.y() + bottom
-
+
def smart_spacing(self, pm):
parent = self.parent()
if not parent:
@@ -140,6 +139,7 @@ class PyDMTemplateRepeater(QFrame, PyDMPrimitiveWidget, LayoutType):
parent : optional
The parent of this widget.
"""
+
Q_ENUMS(LayoutType)
LayoutType = LayoutType
@@ -157,7 +157,7 @@ def __init__(self, parent=None):
self._temp_layout_spacing = 4
self.app = QApplication.instance()
self.rebuild()
-
+
@Property(LayoutType)
def layoutType(self):
"""
@@ -176,7 +176,8 @@ def layoutType(self, new_type):
Options are:
- **Vertical**: Instances of the template are laid out vertically, in rows.
- **Horizontal**: Instances of the template are laid out horizontally, in columns.
- - **Flow**: Instances of the template are laid out horizontally until they reach the edge of the template, at which point they "wrap" into a new row.
+ - **Flow**: Instances of the template are laid out horizontally until they reach the edge of the template,
+ at which point they "wrap" into a new row.
Parameters
----------
@@ -191,31 +192,31 @@ def layoutSpacing(self):
if self.layout():
return self.layout().spacing()
return self._temp_layout_spacing
-
+
@layoutSpacing.setter
def layoutSpacing(self, new_spacing):
self._temp_layout_spacing = new_spacing
if self.layout():
self.layout().setSpacing(new_spacing)
-
+
@Property(int)
def countShownInDesigner(self):
"""
The number of instances to show in Qt Designer. This property has no
effect outside of Designer.
-
+
Returns
-------
int
"""
return self._count_shown_in_designer
-
+
@countShownInDesigner.setter
def countShownInDesigner(self, new_count):
"""
The number of instances to show in Qt Designer. This property has no
effect outside of Designer.
-
+
Parameters
----------
new_count : int
@@ -231,23 +232,23 @@ def countShownInDesigner(self, new_count):
if new_count != self._count_shown_in_designer:
self._count_shown_in_designer = new_count
self.rebuild()
-
+
@Property(str)
def templateFilename(self):
"""
The path to the .ui file to use as a template.
-
+
Returns
-------
str
"""
return self._template_filename
-
+
@templateFilename.setter
def templateFilename(self, new_filename):
"""
The path to the .ui file to use as a template.
-
+
Parameters
----------
new_filename : str
@@ -286,25 +287,25 @@ def dataSource(self):
"""
The path to the JSON file or a valid JSON string to fill in each
instance of the template.
-
+
Returns
-------
str
"""
return self._data_source
-
+
@dataSource.setter
def dataSource(self, data_source):
"""
Sets the path to the JSON file or a valid JSON string to fill in each
instance of the template.
-
+
For example, if you build a template that contains two macro variables,
${NAME} and ${UNIT}, your JSON file should be a list of dictionaries,
each with keys for NAME and UNIT, like this:
-
+
[{"NAME": "First Device", "UNIT": 1}, {"NAME": "Second Device", "UNIT": 2}]
-
+
Parameters
-------
data_source : str
@@ -314,30 +315,35 @@ def dataSource(self, data_source):
if self._data_source:
is_json, data = self._is_json(data_source)
if is_json:
- logger.debug('TemplateRepeater dataSource is a valid JSON.')
+ logger.debug("TemplateRepeater dataSource is a valid JSON.")
self.data = data
else:
- logger.debug('TemplateRepeater dataSource is not a valid JSON. Assuming it is a file path.')
+ logger.debug("TemplateRepeater dataSource is not a valid JSON. Assuming it is a file path.")
try:
parent_display = self.find_parent_display()
base_path = None
if parent_display:
- base_path = os.path.dirname(
- parent_display.loaded_file())
+ base_path = os.path.dirname(parent_display.loaded_file())
fname = find_file(self._data_source, base_path=base_path, raise_if_not_found=True)
if not fname:
if not is_qt_designer():
- logger.error('Cannot locate data source file {} for PyDMTemplateRepeater.'.format(self._data_source))
+ logger.error(
+ "Cannot locate data source file {} for PyDMTemplateRepeater.".format(
+ self._data_source
+ )
+ )
self.data = []
else:
with open(fname) as f:
try:
self.data = json.load(f)
except ValueError:
- logger.error('Failed to parse data source file {} for PyDMTemplateRepeater.'.format(fname))
+ logger.error(
+ "Failed to parse data source file {} for PyDMTemplateRepeater.".format(fname)
+ )
self.data = []
- except IOError as e:
+ except IOError:
self.data = []
else:
self.clear()
@@ -345,7 +351,7 @@ def dataSource(self, data_source):
def open_template_file(self, variables=None):
"""
Opens the widget specified in the templateFilename property.
-
+
Parameters
----------
variables : dict
@@ -361,8 +367,7 @@ def open_template_file(self, variables=None):
parent_display = self.find_parent_display()
base_path = None
if parent_display:
- base_path = os.path.dirname(
- parent_display.loaded_file())
+ base_path = os.path.dirname(parent_display.loaded_file())
fname = find_file(self.templateFilename, base_path=base_path, raise_if_not_found=True)
if self._parent_macros is None:
@@ -375,24 +380,24 @@ def open_template_file(self, variables=None):
try:
w = load_file(fname, macros=parent_macros, target=None)
except Exception as ex:
- w = QLabel('Error: could not load template: ' + str(ex))
+ w = QLabel("Error: could not load template: " + str(ex))
return w
def rebuild(self):
- """ Clear out all existing widgets, and populate the list using the
+ """Clear out all existing widgets, and populate the list using the
template file and data source."""
self.clear()
if (not self.templateFilename) or (not self.data):
return
self.setUpdatesEnabled(False)
-
+
layout_class = layout_class_for_type[self.layoutType]
if type(self.layout()) != layout_class:
if self.layout() is not None:
# Trick to remove the existing layout by re-parenting it in an empty widget.
QWidget().setLayout(self.layout())
- l = layout_class(self)
- self.setLayout(l)
+ currLayoutClass = layout_class(self)
+ self.setLayout(currLayoutClass)
self.layout().setSpacing(self._temp_layout_spacing)
try:
with pydm.data_plugins.connection_queue(defer_connections=True):
@@ -405,8 +410,8 @@ def rebuild(self):
w.setText("No Template Loaded. Data: {}".format(variables))
w.setParent(self)
self.layout().addWidget(w)
- except:
- logger.exception('Template repeater failed to rebuild.')
+ except Exception:
+ logger.exception("Template repeater failed to rebuild.")
finally:
# If issues happen during the rebuild we should still enable
# updates and establish connection for the widgets added.
@@ -415,9 +420,9 @@ def rebuild(self):
# staled.
self.setUpdatesEnabled(True)
pydm.data_plugins.establish_queued_connections()
-
+
def clear(self):
- """ Clear out any existing instances of the template inside
+ """Clear out any existing instances of the template inside
the widget."""
if not self.layout():
return
@@ -425,13 +430,13 @@ def clear(self):
item = self.layout().takeAt(0)
item.widget().deleteLater()
del item
-
+
def count(self):
if not self.layout():
return 0
return self.layout().count()
-
- @property
+
+ @property
def data(self):
"""
The dictionary used by the widget to fill in each instance of the template.
@@ -439,13 +444,13 @@ def data(self):
property.
"""
return self._data
-
+
@data.setter
def data(self, new_data):
"""
- Sets the dictionary used by the widget to fill in each instance of
+ Sets the dictionary used by the widget to fill in each instance of
the template. This property will be overwritten if the user changes
- the dataSource property. After setting this property, `rebuild`
+ the dataSource property. After setting this property, `rebuild`
is automatically called to refresh the widget.
"""
self._data = new_data
diff --git a/pydm/widgets/terminator.py b/pydm/widgets/terminator.py
index fc61ecfe80..c9f06e7db6 100644
--- a/pydm/widgets/terminator.py
+++ b/pydm/widgets/terminator.py
@@ -69,12 +69,12 @@ def _find_window(self):
def _setup_activity_hook(self):
if is_qt_designer():
return
- logger.debug('Setup Hook')
+ logger.debug("Setup Hook")
if self._hook_setup:
- logger.debug('Setup Hook Already there')
+ logger.debug("Setup Hook Already there")
return
self._window = self._find_window()
- logger.debug('Install event filter at window')
+ logger.debug("Install event filter at window")
# We must install the event filter in the application otherwise
# it won't stop when typing or moving over other widgets or even
@@ -98,7 +98,7 @@ def start(self):
if is_qt_designer():
return
interval = SLOW_TIMER_INTERVAL
- if self._time_rem_ms < 60*1000:
+ if self._time_rem_ms < 60 * 1000:
interval = FAST_TIMER_INTERVAL
self._timer.setInterval(interval)
@@ -123,6 +123,7 @@ def _get_time_text(self, value):
-------
str
"""
+
def time_msg(unit, val):
return "{} {}{}".format(val, unit, "s" if val > 1 else "")
@@ -132,9 +133,9 @@ def time_msg(unit, val):
values = [0, 0, 0, 0]
rem = value
for idx, sc in enumerate(scale):
- val_scaled, rem = int(rem//sc), rem % sc
+ val_scaled, rem = int(rem // sc), rem % sc
if val_scaled >= 1:
- val_scaled = math.ceil(val_scaled+(rem/sc))
+ val_scaled = math.ceil(val_scaled + (rem / sc))
values[idx] = val_scaled
break
values[idx] = val_scaled
@@ -149,7 +150,7 @@ def time_msg(unit, val):
def _update_label(self):
"""Updates the label text with the remaining time."""
- rem_time_s = self._time_rem_ms/1000.0
+ rem_time_s = self._time_rem_ms / 1000.0
text = self._get_time_text(rem_time_s)
self.setText("This screen will close in {}.".format(text))
@@ -190,15 +191,11 @@ def handle_timeout(self):
QApplication.instance().removeEventFilter(self)
if self._window:
- logger.debug('Time to close the window')
+ logger.debug("Time to close the window")
self._window.close()
msg = QMessageBox()
msg.setIcon(QMessageBox.Warning)
- msg.setText(
- "Your window was closed due to inactivity for {}.".format(
- self._get_time_text(self.timeout)
- )
- )
+ msg.setText("Your window was closed due to inactivity for {}.".format(self._get_time_text(self.timeout)))
msg.setStandardButtons(QMessageBox.Ok)
msg.setDefaultButton(QMessageBox.Ok)
msg.exec_()
diff --git a/pydm/widgets/timeplot.py b/pydm/widgets/timeplot.py
index c778869b8b..6499c013be 100644
--- a/pydm/widgets/timeplot.py
+++ b/pydm/widgets/timeplot.py
@@ -8,9 +8,10 @@
from qtpy.QtCore import Signal, Slot, Property, QTimer, Q_ENUMS
from .baseplot import BasePlot, BasePlotCurveItem
from .channel import PyDMChannel
-from .. utilities import remove_protocol
+from ..utilities import remove_protocol
import logging
+
logger = logging.getLogger(__name__)
MINIMUM_BUFFER_SIZE = 2
@@ -24,7 +25,8 @@
class updateMode(object):
- """ updateMode as new type for plot update """
+ """updateMode as new type for plot update"""
+
OnValueChange = 1
AtFixedRate = 2
@@ -60,9 +62,10 @@ class TimePlotCurveItem(BasePlotCurveItem):
Additional parameters supported by pyqtgraph.PlotDataItem,
like 'symbol' and 'symbolSize'.
"""
- _channels = ('channel',)
- def __init__(self, channel_address=None, plot_by_timestamps=True, plot_style='Line', **kws):
+ _channels = ("channel",)
+
+ def __init__(self, channel_address=None, plot_by_timestamps=True, plot_style="Line", **kws):
"""
Parameters
----------
@@ -85,14 +88,13 @@ def __init__(self, channel_address=None, plot_by_timestamps=True, plot_style='Li
self._plot_by_timestamps = plot_by_timestamps
self.plot_style = plot_style
-
self._bufferSize = MINIMUM_BUFFER_SIZE
self._update_mode = PyDMTimePlot.OnValueChange
self._min_y_value = None
self._max_y_value = None
- self.data_buffer = np.zeros((2, self._bufferSize), order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
self.connected = False
self.points_accumulated = 0
self.latest_value = None
@@ -101,8 +103,7 @@ def __init__(self, channel_address=None, plot_by_timestamps=True, plot_style='Li
super(TimePlotCurveItem, self).__init__(**kws)
def to_dict(self):
- dic_ = OrderedDict([("channel", self.address),
- ("plot_style", self.plot_style)])
+ dic_ = OrderedDict([("channel", self.address), ("plot_style", self.plot_style)])
dic_.update(super(TimePlotCurveItem, self).to_dict())
return dic_
@@ -114,12 +115,18 @@ def address(self):
@address.setter
def address(self, new_address):
+ self.set_address(new_address)
+
+ def set_address(self, new_address):
+ """A setter method without a pyqt decorator so subclasses can use this functionality"""
if new_address is None or len(str(new_address)) < 1:
self.channel = None
return
- self.channel = PyDMChannel(address=new_address,
- connection_slot=self.connectionStateChanged,
- value_slot=self.receiveNewValue)
+ self.channel = PyDMChannel(
+ address=new_address,
+ connection_slot=self.connectionStateChanged,
+ value_slot=self.receiveNewValue,
+ )
@property
def plotByTimeStamps(self):
@@ -239,8 +246,7 @@ def initialize_buffer(self):
# If you don't specify dtype=float, you don't have enough
# resolution for the timestamp data.
- self.data_buffer = np.zeros((2, self._bufferSize),
- order='f', dtype=float)
+ self.data_buffer = np.zeros((2, self._bufferSize), order="f", dtype=float)
self.data_buffer[0].fill(time.time())
def getBufferSize(self):
@@ -275,15 +281,15 @@ def redrawCurve(self, min_x: Optional[float] = None, max_x: Optional[float] = No
The maximum timestamp to render when plotting as a bar graph.
"""
try:
- x = self.data_buffer[0, -self.points_accumulated:].astype(float)
- y = self.data_buffer[1, -self.points_accumulated:].astype(float)
+ x = self.data_buffer[0, -self.points_accumulated :].astype(float)
+ y = self.data_buffer[1, -self.points_accumulated :].astype(float)
if not self._plot_by_timestamps:
x -= time.time()
- if self.plot_style is None or self.plot_style == 'Line':
+ if self.plot_style is None or self.plot_style == "Line":
self.setData(y=y, x=x)
- elif self.plot_style == 'Bar':
+ elif self.plot_style == "Bar":
# In cases where the buffer size is large, we don't want to render 10,000+ bars on every update
# if only a fraction of those are actually visible. These 2 indices represent the visible range
# of the plot, and we will only render bars within that range.
@@ -295,8 +301,8 @@ def redrawCurve(self, min_x: Optional[float] = None, max_x: Optional[float] = No
pass
def _setBarGraphItem(self, x, y):
- """ Set the plots points to render as bars. No need to call this directly as it will automatically
- be handled by redrawCurve() """
+ """Set the plots points to render as bars. No need to call this directly as it will automatically
+ be handled by redrawCurve()"""
if self.points_accumulated == 0 or len(x) == 0 or len(y) == 0:
return
@@ -314,12 +320,12 @@ def setUpdatesAsynchronously(self, value):
"""
Check if value is from updatesAsynchronously(bool) or updateMode(int)
"""
- if type(value)==int and value == updateMode.AtFixedRate or type(value)==bool and value is True:
+ if isinstance(value, int) and value == updateMode.AtFixedRate or isinstance(value, bool) and value is True:
self._update_mode = PyDMTimePlot.AtFixedRate
else:
self._update_mode = PyDMTimePlot.OnValueChange
self.initialize_buffer()
-
+
def resetUpdatesAsynchronously(self):
self._update_mode = PyDMTimePlot.OnValueChange
self.initialize_buffer()
@@ -351,7 +357,7 @@ def channels(self):
return [self.channel]
-class PyDMTimePlot(BasePlot,updateMode):
+class PyDMTimePlot(BasePlot, updateMode):
"""
PyDMTimePlot is a widget to plot one or more channels vs. time.
@@ -375,6 +381,7 @@ class PyDMTimePlot(BasePlot,updateMode):
Will set the bottom axis of this plot to the input axis. If not set, will default
to either a TimeAxisItem if plot_by_timestamps is true, or a regular AxisItem otherwise
"""
+
OnValueChange = 1
AtFixedRated = 2
@@ -383,7 +390,14 @@ class PyDMTimePlot(BasePlot,updateMode):
plot_redrawn_signal = Signal(TimePlotCurveItem)
- def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, background='default', bottom_axis=None):
+ def __init__(
+ self,
+ parent=None,
+ init_y_channels=[],
+ plot_by_timestamps=True,
+ background="default",
+ bottom_axis=None,
+ ):
"""
Parameters
----------
@@ -406,13 +420,16 @@ def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, bac
if bottom_axis is not None:
self._bottom_axis = bottom_axis
elif plot_by_timestamps:
- self._bottom_axis = TimeAxisItem('bottom')
+ self._bottom_axis = TimeAxisItem("bottom")
else:
self.starting_epoch_time = time.time()
- self._bottom_axis = AxisItem('bottom')
+ self._bottom_axis = AxisItem("bottom")
- super(PyDMTimePlot, self).__init__(parent=parent, background=background,
- axisItems={"bottom": self._bottom_axis})
+ super(PyDMTimePlot, self).__init__(
+ parent=parent,
+ background=background,
+ axisItems={"bottom": self._bottom_axis},
+ )
# Removing the downsampling while PR 763 is not merged at pyqtgraph
# Reference: https://github.com/pyqtgraph/pyqtgraph/pull/763
@@ -435,17 +452,9 @@ def __init__(self, parent=None, init_y_channels=[], plot_by_timestamps=True, bac
self._update_mode = PyDMTimePlot.OnValueChange
self._needs_redraw = True
- self.labels = {
- "left": None,
- "right": None,
- "bottom": None
- }
+ self.labels = {"left": None, "right": None, "bottom": None}
- self.units = {
- "left": None,
- "right": None,
- "bottom": None
- }
+ self.units = {"left": None, "right": None, "bottom": None}
for channel in init_y_channels:
self.addYChannel(channel)
@@ -455,10 +464,24 @@ def initialize_for_designer(self):
# This function gets called by PyDMTimePlot's designer plugin.
self.redraw_timer.setSingleShot(True)
- def addYChannel(self, y_channel=None, plot_style=None, name=None, color=None,
- lineStyle=None, lineWidth=None, symbol=None, symbolSize=None,
- barWidth=None, upperThreshold=None, lowerThreshold=None, thresholdColor=None,
- yAxisName=None, useArchiveData=False):
+ def addYChannel(
+ self,
+ y_channel=None,
+ plot_style=None,
+ name=None,
+ color=None,
+ lineStyle=None,
+ lineWidth=None,
+ symbol=None,
+ symbolSize=None,
+ barWidth=None,
+ upperThreshold=None,
+ lowerThreshold=None,
+ thresholdColor=None,
+ yAxisName=None,
+ useArchiveData=False,
+ **kwargs
+ ):
"""
Adds a new curve to the current plot
@@ -498,24 +521,33 @@ def addYChannel(self, y_channel=None, plot_style=None, name=None, color=None,
new_curve : TimePlotCurveItem
The newly created curve.
"""
- plot_opts = dict()
- plot_opts['symbol'] = symbol
+ plot_opts = dict()
+ plot_opts["symbol"] = symbol
if symbolSize is not None:
- plot_opts['symbolSize'] = symbolSize
+ plot_opts["symbolSize"] = symbolSize
if lineStyle is not None:
- plot_opts['lineStyle'] = lineStyle
+ plot_opts["lineStyle"] = lineStyle
if lineWidth is not None:
- plot_opts['lineWidth'] = lineWidth
-
+ plot_opts["lineWidth"] = lineWidth
+ if kwargs:
+ plot_opts.update(kwargs)
+
# Add curve
- new_curve = self.createCurveItem(y_channel=y_channel, plot_by_timestamps=self._plot_by_timestamps,
- plot_style=plot_style, name=name, color=color, yAxisName=yAxisName,
- useArchiveData=useArchiveData, **plot_opts)
+ new_curve = self.createCurveItem(
+ channel_address=y_channel,
+ plot_by_timestamps=self._plot_by_timestamps,
+ plot_style=plot_style,
+ name=name,
+ color=color,
+ yAxisName=yAxisName,
+ useArchiveData=useArchiveData,
+ **plot_opts
+ )
new_curve.setUpdatesAsynchronously(self.updateMode)
new_curve.setBufferSize(self._bufferSize)
self.update_timer.timeout.connect(new_curve.asyncUpdate)
- if plot_style == 'Bar':
+ if plot_style == "Bar":
if barWidth is None:
barWidth = 1.0 # Can't use default since it can be explicitly set to None and avoided
new_curve.bar_graph_item = BarGraphItem(x=[], height=[], width=barWidth, brush=color)
@@ -527,13 +559,10 @@ def addYChannel(self, y_channel=None, plot_style=None, name=None, color=None,
new_curve.data_changed.connect(self.set_needs_redraw)
self.redraw_timer.start()
-
return new_curve
- def createCurveItem(self, y_channel, plot_by_timestamps, plot_style, name,
- color, yAxisName, useArchiveData, **plot_opts):
- return TimePlotCurveItem(y_channel, plot_by_timestamps=plot_by_timestamps, plot_style=plot_style,
- name=name, color=color, yAxisName=yAxisName, **plot_opts)
+ def createCurveItem(self, *args, **kwargs):
+ return TimePlotCurveItem(*args, **kwargs)
def removeYChannel(self, curve):
"""
@@ -577,8 +606,8 @@ def redrawPlot(self):
self.updateXAxis()
# The minimum and maximum x-axis timestamps visible to the user
- min_x = self.plotItem.getViewBox().state['viewRange'][0][0]
- max_x = self.plotItem.getViewBox().state['viewRange'][0][1]
+ min_x = self.plotItem.getViewBox().state["viewRange"][0][0]
+ max_x = self.plotItem.getViewBox().state["viewRange"][0][1]
for curve in self._curves:
curve.redrawCurve(min_x=min_x, max_x=max_x)
self.plot_redrawn_signal.emit(curve)
@@ -603,9 +632,10 @@ def updateXAxis(self, update_immediately=False):
else:
maxrange = time.time()
minrange = maxrange - self._time_span
- current_min_x = self.plotItem.getAxis('bottom').range[0] # Minimum x value currently displayed on the plot
- if not self.plotItem.isAnyXAutoRange() or (self.plotItem.isAnyXAutoRange() and
- maxrange - current_min_x >= self._time_span):
+ current_min_x = self.plotItem.getAxis("bottom").range[0] # Minimum x value currently displayed on the plot
+ if not self.plotItem.isAnyXAutoRange() or (
+ self.plotItem.isAnyXAutoRange() and maxrange - current_min_x >= self._time_span
+ ):
# Keep the rolling window of data moving, unless the user asked for autorange and we've
# not yet hit the maximum amount of data to display based on the time span
self.plotItem.setXRange(minrange, maxrange, padding=0.0, update=update_immediately)
@@ -651,25 +681,27 @@ def setCurves(self, new_list):
return
self.clearCurves()
for d in new_list:
- color = d.get('color')
- thresholdColor = d.get('thresholdColor')
+ color = d.get("color")
+ thresholdColor = d.get("thresholdColor")
if color:
color = QColor(color)
if thresholdColor:
thresholdColor = QColor(thresholdColor)
- self.addYChannel(d['channel'],
- plot_style=d.get('plot_style'),
- name=d.get('name'),
- color=color,
- lineStyle=d.get('lineStyle'),
- lineWidth=d.get('lineWidth'),
- symbol=d.get('symbol'),
- symbolSize=d.get('symbolSize'),
- barWidth=d.get('barWidth'),
- upperThreshold=d.get('upperThreshold'),
- lowerThreshold=d.get('lowerThreshold'),
- thresholdColor=thresholdColor,
- yAxisName=d.get('yAxisName'))
+ self.addYChannel(
+ d["channel"],
+ plot_style=d.get("plot_style"),
+ name=d.get("name"),
+ color=color,
+ lineStyle=d.get("lineStyle"),
+ lineWidth=d.get("lineWidth"),
+ symbol=d.get("symbol"),
+ symbolSize=d.get("symbolSize"),
+ barWidth=d.get("barWidth"),
+ upperThreshold=d.get("upperThreshold"),
+ lowerThreshold=d.get("lowerThreshold"),
+ thresholdColor=thresholdColor,
+ yAxisName=d.get("yAxisName"),
+ )
curves = Property("QStringList", getCurves, setCurves, designable=False)
@@ -705,12 +737,21 @@ def refreshCurve(self, curve):
curve = self.findCurve(curve.channel)
if curve:
self.removeYChannel(curve)
- self.addYChannel(y_channel=curve.address, plot_style=curve.plot_style, color=curve.color,
- name=curve.address, lineStyle=curve.lineStyle,
- lineWidth=curve.lineWidth, symbol=curve.symbol,
- symbolSize=curve.symbolSize, barWidth=curve.bar_width,
- upperThreshold=curve.upper_threshold, lowerThreshold=curve.lower_threshold,
- thresholdColor=curve.threshold_color, yAxisName=curve.y_axis_name)
+ self.addYChannel(
+ y_channel=curve.address,
+ plot_style=curve.plot_style,
+ color=curve.color,
+ name=curve.address,
+ lineStyle=curve.lineStyle,
+ lineWidth=curve.lineWidth,
+ symbol=curve.symbol,
+ symbolSize=curve.symbolSize,
+ barWidth=curve.bar_width,
+ upperThreshold=curve.upper_threshold,
+ lowerThreshold=curve.lower_threshold,
+ thresholdColor=curve.threshold_color,
+ yAxisName=curve.y_axis_name,
+ )
def addLegendItem(self, item, pv_name, force_show_legend=False):
"""
@@ -786,13 +827,13 @@ def resetBufferSize(self):
def getUpdatesAsynchronously(self):
return self._update_mode == PyDMTimePlot.AtFixedRated
- def setUpdatesAsynchronously(self, value):
+ def setUpdatesAsynchronously(self, value):
for curve in self._curves:
curve.setUpdatesAsynchronously(value)
"""
Check if value is from updatesAsynchronously(bool) or updateMode(int)
"""
- if type(value)==int and value == updateMode.AtFixedRate or type(value)==bool and value is True:
+ if isinstance(value, int) and value == updateMode.AtFixedRate or isinstance(value, bool) and value is True:
self._update_mode = PyDMTimePlot.AtFixedRate
self.update_timer.start()
else:
@@ -805,10 +846,13 @@ def resetUpdatesAsynchronously(self):
for curve in self._curves:
curve.resetUpdatesAsynchronously()
- updatesAsynchronously = Property("bool",
- getUpdatesAsynchronously,
- setUpdatesAsynchronously,
- resetUpdatesAsynchronously, designable=False)
+ updatesAsynchronously = Property(
+ "bool",
+ getUpdatesAsynchronously,
+ setUpdatesAsynchronously,
+ resetUpdatesAsynchronously,
+ designable=False,
+ )
@Property(updateMode)
def updateMode(self):
@@ -918,8 +962,7 @@ def resetUpdateInterval(self):
if self.getUpdatesAsynchronously():
self.setBufferSize(int((self._time_span * 1000.0) / self._update_interval))
- updateInterval = Property(float, getUpdateInterval,
- setUpdateInterval, resetUpdateInterval)
+ updateInterval = Property(float, getUpdateInterval, setUpdateInterval, resetUpdateInterval)
def getAutoRangeX(self):
if self._plot_by_timestamps:
@@ -939,23 +982,43 @@ def channels(self):
# The methods for autoRangeY, minYRange, and maxYRange are
# all defined in BasePlot, but we don't expose them as properties there, because not all plot
# subclasses necessarily want them to be user-configurable in Designer.
- autoRangeY = Property(bool, BasePlot.getAutoRangeY,
- BasePlot.setAutoRangeY,
- BasePlot.resetAutoRangeY, doc="""
+ autoRangeY = Property(
+ bool,
+ BasePlot.getAutoRangeY,
+ BasePlot.setAutoRangeY,
+ BasePlot.resetAutoRangeY,
+ doc="""
Whether or not the Y-axis automatically rescales to fit the data.
If true, the values in minYRange and maxYRange are ignored.
- """)
-
- minYRange = Property(float, BasePlot.getMinYRange,
- BasePlot.setMinYRange, doc="""
- Minimum Y-axis value visible on the plot.""")
-
- maxYRange = Property(float, BasePlot.getMaxYRange,
- BasePlot.setMaxYRange, doc="""
- Maximum Y-axis value visible on the plot.""")
-
- def enableCrosshair(self, is_enabled, starting_x_pos=DEFAULT_X_MIN, starting_y_pos=DEFAULT_Y_MIN, vertical_angle=90,
- horizontal_angle=0, vertical_movable=False, horizontal_movable=False):
+ """,
+ )
+
+ minYRange = Property(
+ float,
+ BasePlot.getMinYRange,
+ BasePlot.setMinYRange,
+ doc="""
+ Minimum Y-axis value visible on the plot.""",
+ )
+
+ maxYRange = Property(
+ float,
+ BasePlot.getMaxYRange,
+ BasePlot.setMaxYRange,
+ doc="""
+ Maximum Y-axis value visible on the plot.""",
+ )
+
+ def enableCrosshair(
+ self,
+ is_enabled,
+ starting_x_pos=DEFAULT_X_MIN,
+ starting_y_pos=DEFAULT_Y_MIN,
+ vertical_angle=90,
+ horizontal_angle=0,
+ vertical_movable=False,
+ horizontal_movable=False,
+ ):
"""
Display a crosshair on the graph.
@@ -976,14 +1039,22 @@ def enableCrosshair(self, is_enabled, starting_x_pos=DEFAULT_X_MIN, starting_y_p
horizontal_movable : bool
True if the user can move the horizontal line; False if not
"""
- super(PyDMTimePlot, self).enableCrosshair(is_enabled, starting_x_pos, starting_y_pos, vertical_angle,
- horizontal_angle, vertical_movable, horizontal_movable)
+ super(PyDMTimePlot, self).enableCrosshair(
+ is_enabled,
+ starting_x_pos,
+ starting_y_pos,
+ vertical_angle,
+ horizontal_angle,
+ vertical_movable,
+ horizontal_movable,
+ )
class TimeAxisItem(AxisItem):
"""
TimeAxisItem formats a unix time axis into a human-readable format.
"""
+
def __init__(self, *args, **kwargs):
super(TimeAxisItem, self).__init__(*args, **kwargs)
self.enableAutoSIPrefix(False)
diff --git a/pydm/widgets/timeplot_curve_editor.py b/pydm/widgets/timeplot_curve_editor.py
index c0783f8ccf..dbe10c39ba 100644
--- a/pydm/widgets/timeplot_curve_editor.py
+++ b/pydm/widgets/timeplot_curve_editor.py
@@ -6,7 +6,6 @@
class PyDMTimePlotCurvesModel(BasePlotCurvesModel):
-
def __init__(self, plot, parent=None):
super(PyDMTimePlotCurvesModel, self).__init__(plot, parent=parent)
self._column_names = ("Channel", "Style") + self._column_names
@@ -18,8 +17,7 @@ def get_data(self, column_name, curve):
return str(curve.address)
elif column_name == "Style":
return curve.plot_style
- return super(PyDMTimePlotCurvesModel, self).get_data(column_name,
- curve)
+ return super(PyDMTimePlotCurvesModel, self).get_data(column_name, curve)
def set_data(self, column_name, curve, value):
if column_name == "Channel":
@@ -27,13 +25,11 @@ def set_data(self, column_name, curve, value):
elif column_name == "Style":
curve.plot_style = str(value)
else:
- return super(PyDMTimePlotCurvesModel, self).set_data(
- column_name=column_name, curve=curve, value=value)
+ return super(PyDMTimePlotCurvesModel, self).set_data(column_name=column_name, curve=curve, value=value)
return True
def append(self, address=None, name=None, color=None):
- self.beginInsertRows(QModelIndex(), len(self._plot._curves),
- len(self._plot._curves))
+ self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves))
self._plot.addYChannel(address, name, color)
self.endInsertRows()
@@ -51,14 +47,16 @@ class TimePlotCurveEditorDialog(BasePlotCurveEditorDialog):
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = PyDMTimePlotCurvesModel
def __init__(self, plot: PyDMTimePlot, parent: Optional[QObject] = None):
super().__init__(plot, parent)
threshold_color_delegate = ColorColumnDelegate(self)
- self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex('Limit Color'),
- threshold_color_delegate)
+ self.table_view.setItemDelegateForColumn(
+ self.table_model.getColumnIndex("Limit Color"), threshold_color_delegate
+ )
plot_style_delegate = PlotStyleColumnDelegate(self, self.table_model, self.table_view)
self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Style"), plot_style_delegate)
diff --git a/pydm/widgets/waveformplot.py b/pydm/widgets/waveformplot.py
index 3be99b656c..81f057dd5a 100644
--- a/pydm/widgets/waveformplot.py
+++ b/pydm/widgets/waveformplot.py
@@ -37,7 +37,7 @@ class WaveformCurveItem(BasePlotCurveItem):
Width of the line connecting the data points.
redraw_mode: int, optional
Must be one four values:
-
+
- WaveformCurveItem.REDRAW_ON_EITHER: (Default) Redraw after either X or Y receives new data.
- WaveformCurveItem.REDRAW_ON_X: Redraw after X receives new data.
- WaveformCurveItem.REDRAW_ON_Y: Redraw after Y receives new data.
@@ -45,20 +45,20 @@ class WaveformCurveItem(BasePlotCurveItem):
**kargs: optional
PlotDataItem keyword arguments, such as symbol and symbolSize.
"""
- _channels = ('x_channel', 'y_channel')
- def __init__(self, y_addr=None, x_addr=None, redraw_mode=None, plot_style='Line', **kws):
+ _channels = ("x_channel", "y_channel")
+
+ def __init__(self, y_addr=None, x_addr=None, redraw_mode=None, plot_style="Line", **kws):
y_addr = "" if y_addr is None else y_addr
- if kws.get('name') is None:
+ if kws.get("name") is None:
y_name = remove_protocol(y_addr)
if x_addr is None:
plot_name = y_name
else:
x_name = remove_protocol(x_addr)
plot_name = "{y} vs. {x}".format(y=y_name, x=x_name)
- kws['name'] = plot_name
- self.redraw_mode = (redraw_mode if redraw_mode is not None
- else self.REDRAW_ON_EITHER)
+ kws["name"] = plot_name
+ self.redraw_mode = redraw_mode if redraw_mode is not None else self.REDRAW_ON_EITHER
self.needs_new_x = True
self.needs_new_y = True
self.x_channel = None
@@ -86,9 +86,9 @@ def to_dict(self):
-------
OrderedDict
"""
- dic_ = OrderedDict([("y_channel", self.y_address),
- ("x_channel", self.x_address),
- ("plot_style", self.plot_style)])
+ dic_ = OrderedDict(
+ [("y_channel", self.y_address), ("x_channel", self.x_address), ("plot_style", self.plot_style)]
+ )
dic_.update(super(WaveformCurveItem, self).to_dict())
dic_["redraw_mode"] = self.redraw_mode
return dic_
@@ -119,9 +119,8 @@ def x_address(self, new_address):
self.x_channel = None
return
self.x_channel = PyDMChannel(
- address=new_address,
- connection_slot=self.xConnectionStateChanged,
- value_slot=self.receiveXWaveform)
+ address=new_address, connection_slot=self.xConnectionStateChanged, value_slot=self.receiveXWaveform
+ )
@property
def y_address(self):
@@ -149,9 +148,8 @@ def y_address(self, new_address):
self.y_channel = None
return
self.y_channel = PyDMChannel(
- address=new_address,
- connection_slot=self.yConnectionStateChanged,
- value_slot=self.receiveYWaveform)
+ address=new_address, connection_slot=self.yConnectionStateChanged, value_slot=self.receiveYWaveform
+ )
def update_waveforms_if_ready(self):
"""
@@ -194,7 +192,7 @@ def receiveXWaveform(self, new_waveform):
Handler for new x waveform data. This method is usually called by a
PyDMChannel when it updates. You can call this yourself to inject data
into the curve.
-
+
Parameters
----------
new_waveform: numpy.ndarray
@@ -216,7 +214,7 @@ def receiveYWaveform(self, new_waveform):
Handler for new y waveform data. This method is usually called by a
PyDMChannel when it updates. You can call this yourself to inject data
into the curve.
-
+
Parameters
----------
new_waveform: numpy.ndarray
@@ -243,27 +241,27 @@ def redrawCurve(self):
return
if self.x_waveform is not None:
if self.x_waveform.shape[0] > self.y_waveform.shape[0]:
- self.x_waveform = self.x_waveform[:self.y_waveform.shape[0]]
+ self.x_waveform = self.x_waveform[: self.y_waveform.shape[0]]
elif self.x_waveform.shape[0] < self.y_waveform.shape[0]:
- self.y_waveform = self.y_waveform[:self.x_waveform.shape[0]]
+ self.y_waveform = self.y_waveform[: self.x_waveform.shape[0]]
- if self.plot_style is None or self.plot_style == 'Line':
+ if self.plot_style is None or self.plot_style == "Line":
self._setCurveData()
- elif self.plot_style == 'Bar':
+ elif self.plot_style == "Bar":
self._setBarGraphItem()
self.needs_new_x = True
self.needs_new_y = True
def _setCurveData(self):
- """ Sets the most recently received waveform data for display as a line graph. """
+ """Sets the most recently received waveform data for display as a line graph."""
if self.x_waveform is None:
self.setData(y=self.y_waveform.astype(float))
return
self.setData(x=self.x_waveform.astype(float), y=self.y_waveform.astype(float))
def _setBarGraphItem(self):
- """ Sets the most recently received waveform data for display as a bar graph. """
+ """Sets the most recently received waveform data for display as a bar graph."""
if self.y_waveform is None:
return
@@ -275,14 +273,10 @@ def _setBarGraphItem(self):
brushes[np.argwhere(self.y_waveform < self.lower_threshold)] = self.threshold_color
if self.x_waveform is None:
- self.bar_graph_item.setOpts(x=np.arange(len(self.y_waveform)),
- height=self.y_waveform,
- brushes=brushes)
+ self.bar_graph_item.setOpts(x=np.arange(len(self.y_waveform)), height=self.y_waveform, brushes=brushes)
return
- self.bar_graph_item.setOpts(x=self.x_waveform,
- height=self.y_waveform,
- brushes=brushes)
+ self.bar_graph_item.setOpts(x=self.x_waveform, height=self.y_waveform, brushes=brushes)
def limits(self):
"""
@@ -296,14 +290,16 @@ def limits(self):
if self.y_waveform is None or self.y_waveform.shape[0] == 0:
raise NoDataError("Curve has no Y data, cannot determine limits.")
if self.x_waveform is None:
- yspan = (float(np.amax(self.y_waveform)) -
- float(np.amin(self.y_waveform)))
- return ((0, len(self.y_waveform)),
- (float(np.amin(self.y_waveform) - yspan),
- float(np.amax(self.y_waveform) + yspan)))
+ yspan = float(np.amax(self.y_waveform)) - float(np.amin(self.y_waveform))
+ return (
+ (0, len(self.y_waveform)),
+ (float(np.amin(self.y_waveform) - yspan), float(np.amax(self.y_waveform) + yspan)),
+ )
else:
- return ((float(np.amin(self.x_waveform)), float(np.amax(self.x_waveform))),
- (float(np.amin(self.y_waveform)), float(np.amax(self.y_waveform))))
+ return (
+ (float(np.amin(self.x_waveform)), float(np.amax(self.x_waveform))),
+ (float(np.amin(self.y_waveform)), float(np.amax(self.y_waveform))),
+ )
def channels(self):
return [self.y_channel, self.x_channel]
@@ -340,8 +336,7 @@ class PyDMWaveformPlot(BasePlot):
pyqtgraph.mkColor will accept.
"""
- def __init__(self, parent=None, init_x_channels=[], init_y_channels=[],
- background='default'):
+ def __init__(self, parent=None, init_x_channels=[], init_y_channels=[], background="default"):
super(PyDMWaveformPlot, self).__init__(parent, background)
# If the user supplies a single string instead of a list,
# wrap it in a list.
@@ -350,17 +345,15 @@ def __init__(self, parent=None, init_x_channels=[], init_y_channels=[],
if isinstance(init_y_channels, str):
init_y_channels = [init_y_channels]
if len(init_x_channels) == 0:
- init_x_channels = list(itertools.repeat(None,
- len(init_y_channels)))
+ init_x_channels = list(itertools.repeat(None, len(init_y_channels)))
if len(init_x_channels) != len(init_y_channels):
- raise ValueError("If lists are provided for both X and Y " +
- "channels, they must be the same length.")
+ raise ValueError("If lists are provided for both X and Y " + "channels, they must be the same length.")
# self.channel_pairs is an ordered dictionary that is keyed on a
# (x_channel, y_channel) tuple, with WaveformCurveItem values.
# It gets populated in self.addChannel().
self.channel_pairs = OrderedDict()
init_channel_pairs = zip(init_x_channels, init_y_channels)
- for (x_chan, y_chan) in init_channel_pairs:
+ for x_chan, y_chan in init_channel_pairs:
self.addChannel(y_chan, x_channel=x_chan)
def initialize_for_designer(self):
@@ -368,10 +361,24 @@ def initialize_for_designer(self):
# This function gets called by PyDMTimePlot's designer plugin.
pass
- def addChannel(self, y_channel=None, x_channel=None, plot_style=None, name=None,
- color=None, lineStyle=None, lineWidth=None,
- symbol=None, symbolSize=None, barWidth=None, upperThreshold=None,
- lowerThreshold=None, thresholdColor=None, redraw_mode=None, yAxisName=None):
+ def addChannel(
+ self,
+ y_channel=None,
+ x_channel=None,
+ plot_style=None,
+ name=None,
+ color=None,
+ lineStyle=None,
+ lineWidth=None,
+ symbol=None,
+ symbolSize=None,
+ barWidth=None,
+ upperThreshold=None,
+ lowerThreshold=None,
+ thresholdColor=None,
+ redraw_mode=None,
+ yAxisName=None,
+ ):
"""
Add a new curve to the plot. In addition to the arguments below,
all other keyword arguments are passed to the underlying
@@ -419,25 +426,27 @@ def addChannel(self, y_channel=None, x_channel=None, plot_style=None, name=None,
doesn't yet exist
"""
plot_opts = {}
- plot_opts['symbol'] = symbol
+ plot_opts["symbol"] = symbol
if symbolSize is not None:
- plot_opts['symbolSize'] = symbolSize
+ plot_opts["symbolSize"] = symbolSize
if lineStyle is not None:
- plot_opts['lineStyle'] = lineStyle
+ plot_opts["lineStyle"] = lineStyle
if lineWidth is not None:
- plot_opts['lineWidth'] = lineWidth
+ plot_opts["lineWidth"] = lineWidth
if redraw_mode is not None:
- plot_opts['redraw_mode'] = redraw_mode
+ plot_opts["redraw_mode"] = redraw_mode
self._needs_redraw = False
- curve = WaveformCurveItem(y_addr=y_channel,
- x_addr=x_channel,
- plot_style=plot_style,
- name=name,
- color=color,
- yAxisName=yAxisName,
- **plot_opts)
+ curve = self.createCurveItem(
+ y_addr=y_channel,
+ x_addr=x_channel,
+ plot_style=plot_style,
+ name=name,
+ color=color,
+ yAxisName=yAxisName,
+ **plot_opts
+ )
self.channel_pairs[(y_channel, x_channel)] = curve
- if plot_style == 'Bar':
+ if plot_style == "Bar":
if barWidth is None:
barWidth = 1.0 # Can't use default since it can be explicitly set to None and avoided
curve.bar_graph_item = BarGraphItem(x=[], height=[], width=barWidth, brush=color)
@@ -448,6 +457,9 @@ def addChannel(self, y_channel=None, x_channel=None, plot_style=None, name=None,
curve.getViewBox().addItem(curve.bar_graph_item)
curve.data_changed.connect(self.set_needs_redraw)
+ def createCurveItem(self, *args, **kwargs):
+ return WaveformCurveItem(*args, **kwargs)
+
def removeChannel(self, curve):
"""
Remove a curve from the plot.
@@ -519,26 +531,29 @@ def setCurves(self, new_list):
return
self.clearCurves()
for d in new_list:
- color = d.get('color')
- thresholdColor = d.get('thresholdColor')
+ color = d.get("color")
+ thresholdColor = d.get("thresholdColor")
if color:
color = QColor(color)
if thresholdColor:
thresholdColor = QColor(thresholdColor)
- self.addChannel(d['y_channel'], d['x_channel'],
- plot_style=d.get('plot_style'),
- name=d.get('name'), color=color,
- lineStyle=d.get('lineStyle'),
- lineWidth=d.get('lineWidth'),
- symbol=d.get('symbol'),
- symbolSize=d.get('symbolSize'),
- barWidth=d.get('barWidth'),
- upperThreshold=d.get('upperThreshold'),
- lowerThreshold=d.get('lowerThreshold'),
- thresholdColor=thresholdColor,
- redraw_mode=d.get('redraw_mode'),
- yAxisName=d.get('yAxisName')
- )
+ self.addChannel(
+ d["y_channel"],
+ d["x_channel"],
+ plot_style=d.get("plot_style"),
+ name=d.get("name"),
+ color=color,
+ lineStyle=d.get("lineStyle"),
+ lineWidth=d.get("lineWidth"),
+ symbol=d.get("symbol"),
+ symbolSize=d.get("symbolSize"),
+ barWidth=d.get("barWidth"),
+ upperThreshold=d.get("upperThreshold"),
+ lowerThreshold=d.get("lowerThreshold"),
+ thresholdColor=thresholdColor,
+ redraw_mode=d.get("redraw_mode"),
+ yAxisName=d.get("yAxisName"),
+ )
curves = Property("QStringList", getCurves, setCurves, designable=False)
@@ -552,38 +567,61 @@ def channels(self):
"""
chans = []
chans.extend([curve.y_channel for curve in self._curves])
- chans.extend([curve.x_channel for curve in self._curves
- if curve.x_channel is not None])
+ chans.extend([curve.x_channel for curve in self._curves if curve.x_channel is not None])
return chans
# The methods for autoRangeX, minXRange, maxXRange, autoRangeY, minYRange,
# and maxYRange are all defined in BasePlot, but we don't expose them as
# properties there, because not all plot subclasses necessarily want them
# to be user-configurable in Designer.
- autoRangeX = Property(bool, BasePlot.getAutoRangeX,
- BasePlot.setAutoRangeX, BasePlot.resetAutoRangeX,
- doc="""
+ autoRangeX = Property(
+ bool,
+ BasePlot.getAutoRangeX,
+ BasePlot.setAutoRangeX,
+ BasePlot.resetAutoRangeX,
+ doc="""
Whether or not the X-axis automatically rescales to fit the data.
-If true, the values in minXRange and maxXRange are ignored.""")
-
- minXRange = Property(float, BasePlot.getMinXRange,
- BasePlot.setMinXRange, doc="""
-Minimum X-axis value visible on the plot.""")
-
- maxXRange = Property(float, BasePlot.getMaxXRange,
- BasePlot.setMaxXRange, doc="""
-Maximum X-axis value visible on the plot.""")
-
- autoRangeY = Property(bool, BasePlot.getAutoRangeY,
- BasePlot.setAutoRangeY, BasePlot.resetAutoRangeY,
- doc="""
+If true, the values in minXRange and maxXRange are ignored.""",
+ )
+
+ minXRange = Property(
+ float,
+ BasePlot.getMinXRange,
+ BasePlot.setMinXRange,
+ doc="""
+Minimum X-axis value visible on the plot.""",
+ )
+
+ maxXRange = Property(
+ float,
+ BasePlot.getMaxXRange,
+ BasePlot.setMaxXRange,
+ doc="""
+Maximum X-axis value visible on the plot.""",
+ )
+
+ autoRangeY = Property(
+ bool,
+ BasePlot.getAutoRangeY,
+ BasePlot.setAutoRangeY,
+ BasePlot.resetAutoRangeY,
+ doc="""
Whether or not the Y-axis automatically rescales to fit the data.
-If true, the values in minYRange and maxYRange are ignored.""")
-
- minYRange = Property(float, BasePlot.getMinYRange,
- BasePlot.setMinYRange, doc="""
-Minimum Y-axis value visible on the plot.""")
-
- maxYRange = Property(float, BasePlot.getMaxYRange,
- BasePlot.setMaxYRange, doc="""
-Maximum Y-axis value visible on the plot.""")
+If true, the values in minYRange and maxYRange are ignored.""",
+ )
+
+ minYRange = Property(
+ float,
+ BasePlot.getMinYRange,
+ BasePlot.setMinYRange,
+ doc="""
+Minimum Y-axis value visible on the plot.""",
+ )
+
+ maxYRange = Property(
+ float,
+ BasePlot.getMaxYRange,
+ BasePlot.setMaxYRange,
+ doc="""
+Maximum Y-axis value visible on the plot.""",
+ )
diff --git a/pydm/widgets/waveformplot_curve_editor.py b/pydm/widgets/waveformplot_curve_editor.py
index 5475fd2f49..6b330a977f 100644
--- a/pydm/widgets/waveformplot_curve_editor.py
+++ b/pydm/widgets/waveformplot_curve_editor.py
@@ -1,20 +1,24 @@
from qtpy.QtCore import QModelIndex, QObject, QVariant
from .baseplot_table_model import BasePlotCurvesModel
-from .baseplot_curve_editor import (BasePlotCurveEditorDialog, ColorColumnDelegate,
- PlotStyleColumnDelegate, RedrawModeColumnDelegate)
+from .baseplot_curve_editor import (
+ BasePlotCurveEditorDialog,
+ ColorColumnDelegate,
+ PlotStyleColumnDelegate,
+ RedrawModeColumnDelegate,
+)
from .waveformplot import PyDMWaveformPlot
class PyDMWaveformPlotCurvesModel(BasePlotCurvesModel):
- """ This is the data model used by the waveform plot curve editor.
+ """This is the data model used by the waveform plot curve editor.
It basically acts as a go-between for the curves in a plot, and
QTableView items.
"""
def __init__(self, plot, parent=None):
super(PyDMWaveformPlotCurvesModel, self).__init__(plot, parent=parent)
- self._column_names = ('Y Channel', 'X Channel', 'Style') + self._column_names
- self._column_names += ('Redraw Mode',)
+ self._column_names = ("Y Channel", "X Channel", "Style") + self._column_names
+ self._column_names += ("Redraw Mode",)
def get_data(self, column_name, curve):
if column_name == "Y Channel":
@@ -29,8 +33,7 @@ def get_data(self, column_name, curve):
return curve.plot_style
elif column_name == "Redraw Mode":
return curve.redraw_mode
- return super(PyDMWaveformPlotCurvesModel, self).get_data(
- column_name, curve)
+ return super(PyDMWaveformPlotCurvesModel, self).get_data(column_name, curve)
def set_data(self, column_name, curve, value):
if column_name == "Y Channel":
@@ -42,13 +45,11 @@ def set_data(self, column_name, curve, value):
elif column_name == "Redraw Mode":
curve.redraw_mode = int(value)
else:
- return super(PyDMWaveformPlotCurvesModel, self).set_data(
- column_name=column_name, curve=curve, value=value)
+ return super(PyDMWaveformPlotCurvesModel, self).set_data(column_name=column_name, curve=curve, value=value)
return True
def append(self, y_address=None, x_address=None, name=None, color=None):
- self.beginInsertRows(QModelIndex(), len(self._plot._curves),
- len(self._plot._curves))
+ self.beginInsertRows(QModelIndex(), len(self._plot._curves), len(self._plot._curves))
self._plot.addChannel(y_address, x_address, name, color)
self.endInsertRows()
@@ -66,6 +67,7 @@ class WaveformPlotCurveEditorDialog(BasePlotCurveEditorDialog):
This thing is mostly just a wrapper for a table view, with a couple
buttons to add and remove curves, and a button to save the changes."""
+
TABLE_MODEL_CLASS = PyDMWaveformPlotCurvesModel
def __init__(self, plot: PyDMWaveformPlot, parent: QObject = None):
@@ -75,8 +77,9 @@ def __init__(self, plot: PyDMWaveformPlot, parent: QObject = None):
self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Redraw Mode"), redraw_mode_delegate)
threshold_color_delegate = ColorColumnDelegate(self)
- self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex('Limit Color'),
- threshold_color_delegate)
+ self.table_view.setItemDelegateForColumn(
+ self.table_model.getColumnIndex("Limit Color"), threshold_color_delegate
+ )
plot_style_delegate = PlotStyleColumnDelegate(self, self.table_model, self.table_view)
self.table_view.setItemDelegateForColumn(self.table_model.getColumnIndex("Style"), plot_style_delegate)
diff --git a/pydm/widgets/waveformtable.py b/pydm/widgets/waveformtable.py
index d7f95692d4..94fded6a8f 100644
--- a/pydm/widgets/waveformtable.py
+++ b/pydm/widgets/waveformtable.py
@@ -26,9 +26,7 @@ def __init__(self, parent=None, init_channel=None):
PyDMWritableWidget.__init__(self, init_channel=init_channel)
self._columnHeaders = ["Value"]
self._rowHeaders = []
- self._itemsFlags = (Qt.ItemIsSelectable |
- Qt.ItemIsEditable |
- Qt.ItemIsEnabled)
+ self._itemsFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
self.waveform = None
self._valueBeingSet = False
self.setColumnCount(1)
@@ -48,10 +46,10 @@ def value_changed(self, new_waveform):
self.waveform = new_waveform
col_count = self.columnCount()
len_wave = len(new_waveform)
- row_count = len_wave//col_count + (1 if len_wave % col_count else 0)
+ row_count = len_wave // col_count + (1 if len_wave % col_count else 0)
self.setRowCount(row_count)
for ind, element in enumerate(new_waveform):
- i, j = ind//col_count, ind % col_count
+ i, j = ind // col_count, ind % col_count
value_cell = QTableWidgetItem(str(element))
value_cell.setFlags(self._itemsFlags)
self.setItem(i, j, value_cell)
@@ -76,7 +74,7 @@ def send_waveform(self, row, column):
item = self.item(row, column)
if item and self.subtype:
new_val = self.subtype(item.text())
- ind = row*self.columnCount() + column
+ ind = row * self.columnCount() + column
self.waveform[ind] = new_val
self.send_value_signal[np.ndarray].emit(self.waveform)
@@ -88,9 +86,9 @@ def check_enable_state(self):
PyDMWritableWidget.check_enable_state(self)
self.setEnabled(True)
if self._write_access and self._connected:
- self._itemsFlags = Qt.ItemIsSelectable|Qt.ItemIsEditable|Qt.ItemIsEnabled
+ self._itemsFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled
elif self._connected:
- self._itemsFlags = Qt.ItemIsSelectable|Qt.ItemIsEnabled
+ self._itemsFlags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
else:
self._itemsFlags = Qt.ItemIsSelectable
for col in range(0, self.columnCount()):
diff --git a/pydm_banner.png b/pydm_banner.png
new file mode 100644
index 0000000000..8056f762d6
Binary files /dev/null and b/pydm_banner.png differ
diff --git a/pydm_banner_full.png b/pydm_banner_full.png
new file mode 100644
index 0000000000..2e26439861
Binary files /dev/null and b/pydm_banner_full.png differ
diff --git a/pydm_designer_plugin.py b/pydm_designer_plugin.py
index 7e3e743726..2838e1ba74 100644
--- a/pydm_designer_plugin.py
+++ b/pydm_designer_plugin.py
@@ -1,2 +1,2 @@
print("Loading PyDM Widgets")
-from pydm.widgets.qtplugins import *
+from pydm.widgets.qtplugins import * # noqa: E402, F403
diff --git a/pydm_launcher/__init__.py b/pydm_launcher/__init__.py
index cc0964fd80..f5d1f1da41 100644
--- a/pydm_launcher/__init__.py
+++ b/pydm_launcher/__init__.py
@@ -1,3 +1,3 @@
-__all__ = ['main']
+__all__ = ["main"] # noqa: F405
-from pydm_launcher import *
+from pydm_launcher import * # noqa: F403,F405
diff --git a/pydm_launcher/icons/pydm_128.png b/pydm_launcher/icons/pydm_128.png
new file mode 100644
index 0000000000..1ada7ff881
Binary files /dev/null and b/pydm_launcher/icons/pydm_128.png differ
diff --git a/pydm_launcher/icons/pydm_16.png b/pydm_launcher/icons/pydm_16.png
new file mode 100644
index 0000000000..e3a3febaea
Binary files /dev/null and b/pydm_launcher/icons/pydm_16.png differ
diff --git a/pydm_launcher/icons/pydm_24.png b/pydm_launcher/icons/pydm_24.png
new file mode 100644
index 0000000000..48267c43eb
Binary files /dev/null and b/pydm_launcher/icons/pydm_24.png differ
diff --git a/pydm_launcher/icons/pydm_256.png b/pydm_launcher/icons/pydm_256.png
new file mode 100644
index 0000000000..5b0dce4bdc
Binary files /dev/null and b/pydm_launcher/icons/pydm_256.png differ
diff --git a/pydm_launcher/icons/pydm_32.png b/pydm_launcher/icons/pydm_32.png
new file mode 100644
index 0000000000..47b39af76a
Binary files /dev/null and b/pydm_launcher/icons/pydm_32.png differ
diff --git a/pydm_launcher/icons/pydm_64.png b/pydm_launcher/icons/pydm_64.png
new file mode 100644
index 0000000000..89f3b0e78a
Binary files /dev/null and b/pydm_launcher/icons/pydm_64.png differ
diff --git a/pydm_launcher/main.py b/pydm_launcher/main.py
index ebb5b1b2b7..accde126a6 100755
--- a/pydm_launcher/main.py
+++ b/pydm_launcher/main.py
@@ -1,15 +1,17 @@
import argparse
import cProfile
import logging
+import os
import pstats
import sys
import faulthandler
+from qtpy import QtCore, QtGui
def main():
- logger = logging.getLogger('')
+ logger = logging.getLogger("")
handler = logging.StreamHandler()
- formatter = logging.Formatter('[%(asctime)s] [%(levelname)-8s] - %(message)s')
+ formatter = logging.Formatter("[%(asctime)s] [%(levelname)-8s] - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel("INFO")
@@ -25,101 +27,73 @@ def main():
otherwise we get the following error if someone adds a WebView at Designer:
ImportError: QtWebEngineWidgets must be imported before a QCoreApplication instance is created
"""
- from qtpy import QtWebEngineWidgets
+ from qtpy import QtWebEngineWidgets # noqa: F401
except ImportError:
- logger.debug('QtWebEngine is not supported.')
+ logger.debug("QtWebEngine is not supported.")
import pydm
from pydm.utilities.macro import parse_macro_string
parser = argparse.ArgumentParser(description="Python Display Manager")
parser.add_argument(
- 'displayfile',
- help='A PyDM file to display.' +
- ' Can be either a Qt .ui file, or a Python file.',
- nargs='?',
- default=None
- )
- parser.add_argument(
- '--homefile',
- help='Path to a PyDM file to return to when the home button is clicked in the navigation bar'
+ "displayfile",
+ help="A PyDM file to display." + " Can be either a Qt .ui file, or a Python file.",
+ nargs="?",
+ default=None,
)
parser.add_argument(
- '--perfmon',
- action='store_true',
- help='Enable performance monitoring,' +
- ' and print CPU usage to the terminal.'
- )
- parser.add_argument(
- '--profile',
- action='store_true',
- help='Enable cProfile function profiling, printing on exit.'
+ "--homefile", help="Path to a PyDM file to return to when the home button is clicked in the navigation bar"
)
parser.add_argument(
- '--faulthandler',
- action='store_true',
- help='Enable faulthandler to trace segmentation faults.'
+ "--perfmon",
+ action="store_true",
+ help="Enable performance monitoring," + " and print CPU usage to the terminal.",
)
+ parser.add_argument("--profile", action="store_true", help="Enable cProfile function profiling, printing on exit.")
+ parser.add_argument("--faulthandler", action="store_true", help="Enable faulthandler to trace segmentation faults.")
+ parser.add_argument("--hide-nav-bar", action="store_true", help="Start PyDM with the navigation bar hidden.")
+ parser.add_argument("--hide-menu-bar", action="store_true", help="Start PyDM with the menu bar hidden.")
+ parser.add_argument("--hide-status-bar", action="store_true", help="Start PyDM with the status bar hidden.")
+ parser.add_argument("--fullscreen", action="store_true", help="Start PyDM in full screen mode.")
+ parser.add_argument("--read-only", action="store_true", help="Start PyDM in a Read-Only mode.")
parser.add_argument(
- '--hide-nav-bar',
- action='store_true',
- help='Start PyDM with the navigation bar hidden.'
- )
- parser.add_argument(
- '--hide-menu-bar',
- action='store_true',
- help='Start PyDM with the menu bar hidden.'
- )
- parser.add_argument(
- '--hide-status-bar',
- action='store_true',
- help='Start PyDM with the status bar hidden.'
- )
- parser.add_argument(
- '--fullscreen',
- action='store_true',
- help='Start PyDM in full screen mode.'
- )
- parser.add_argument(
- '--read-only',
- action='store_true',
- help='Start PyDM in a Read-Only mode.'
- )
+ "--log_level",
+ help="Configure level of log display",
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+ default="INFO",
+ )
parser.add_argument(
- '--log_level',
- help='Configure level of log display',
- choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
- default='INFO'
- )
- parser.add_argument('--version', action='version',
- version='PyDM {version}'.format(version=pydm.__version__),
- help="Show PyDM's version number and exit.")
+ "--version",
+ action="version",
+ version="PyDM {version}".format(version=pydm.__version__),
+ help="Show PyDM's version number and exit.",
+ )
parser.add_argument(
- '-m', '--macro',
- help='Specify macro replacements to use, in JSON object format.' +
- ' Reminder: JSON requires double quotes for strings, ' +
- 'so you should wrap this whole argument in single quotes.' +
- ' Example: -m \'{"sector": "LI25", "facility": "LCLS"}\'' +
- '--or-- specify macro replacements as KEY=value pairs ' +
- ' using a comma as delimiter If you want to uses spaces ' +
- ' after the delimiters or around the = signs, ' +
- ' wrap the entire set with quotes ' +
- ' Example: -m "sector = LI25, facility=LCLS"'
- )
+ "-m",
+ "--macro",
+ help="Specify macro replacements to use, in JSON object format."
+ + " Reminder: JSON requires double quotes for strings, "
+ + "so you should wrap this whole argument in single quotes."
+ + ' Example: -m \'{"sector": "LI25", "facility": "LCLS"}\''
+ + "--or-- specify macro replacements as KEY=value pairs "
+ + " using a comma as delimiter If you want to uses spaces "
+ + " after the delimiters or around the = signs, "
+ + " wrap the entire set with quotes "
+ + ' Example: -m "sector = LI25, facility=LCLS"',
+ )
parser.add_argument(
- '--stylesheet',
- help='Specify the full path to a CSS stylesheet file, which' +
- ' can be used to customize the appearance of PyDM and' +
- ' Qt widgets.',
- default=None
- )
+ "--stylesheet",
+ help="Specify the full path to a CSS stylesheet file, which"
+ + " can be used to customize the appearance of PyDM and"
+ + " Qt widgets.",
+ default=None,
+ )
parser.add_argument(
- 'display_args',
- help='Arguments to be passed to the PyDM client application' +
- ' (which is a QApplication subclass).',
+ "display_args",
+ help="Arguments to be passed to the PyDM client application" + " (which is a QApplication subclass).",
nargs=argparse.REMAINDER,
- default=None
- )
+ default=None,
+ )
pydm_args = parser.parse_args()
if pydm_args.profile:
@@ -148,11 +122,24 @@ def main():
read_only=pydm_args.read_only,
macros=macros,
stylesheet_path=pydm_args.stylesheet,
- home_file=pydm_args.homefile
- )
+ home_file=pydm_args.homefile,
+ )
+
+ base_path = os.path.dirname(os.path.realpath(__file__))
+ icon_path_mask = os.path.join(base_path, "icons", "pydm_{}.png")
+
+ app_icon = QtGui.QIcon()
+ app_icon.addFile(icon_path_mask.format(16), QtCore.QSize(16, 16))
+ app_icon.addFile(icon_path_mask.format(24), QtCore.QSize(24, 24))
+ app_icon.addFile(icon_path_mask.format(32), QtCore.QSize(32, 32))
+ app_icon.addFile(icon_path_mask.format(64), QtCore.QSize(64, 64))
+ app_icon.addFile(icon_path_mask.format(128), QtCore.QSize(128, 128))
+ app_icon.addFile(icon_path_mask.format(256), QtCore.QSize(256, 256))
+
+ app.setWindowIcon(app_icon)
+ app.setApplicationName("PyDM")
- pydm.utilities.shortcuts.install_connection_inspector(
- parent=app.main_window)
+ pydm.utilities.shortcuts.install_connection_inspector(parent=app.main_window)
exit_code = app.exec_()
diff --git a/run_tests.py b/run_tests.py
index a530b3733c..4d3a49456b 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -3,25 +3,26 @@
import sys
import pytest
-if __name__ == '__main__':
+if __name__ == "__main__":
# Show output results from every test function
# Show the message output for skipped and expected failures
- args = ['-v', '-vrxs']
+ args = ["-v", "-vrxs"]
# Add extra arguments
if len(sys.argv) > 1:
args.extend(sys.argv[1:])
# Show coverage
- if '--show-cov' in args:
- args.extend(['--cov=pydm', '--cov-report', 'term-missing'])
- args.remove('--show-cov')
+ if "--show-cov" in args:
+ args.extend(["--cov=pydm", "--cov-report", "term-missing"])
+ args.remove("--show-cov")
- # Exclude p4p and pyca tests on Windows until p4p/pyepics compatibility issue is resolved and a Windows PyCA build exists
- if os.name == 'nt':
- args.append('--ignore=pydm/tests/data_plugins/test_p4p_plugin_component.py')
- args.append('--ignore=pydm/tests/data_plugins/test_psp_plugin_component.py')
+ # Exclude p4p and pyca tests on Windows until p4p/pyepics compatibility issue is resolved
+ # and a Windows PyCA build exists
+ if os.name == "nt":
+ args.append("--ignore=pydm/tests/data_plugins/test_p4p_plugin_component.py")
+ args.append("--ignore=pydm/tests/data_plugins/test_psp_plugin_component.py")
- print('pytest arguments: {}'.format(args))
+ print("pytest arguments: {}".format(args))
sys.exit(pytest.main(args))
diff --git a/setup.py b/setup.py
index ec3c4ca774..1bbdaa27df 100644
--- a/setup.py
+++ b/setup.py
@@ -8,32 +8,27 @@
cur_dir = path.abspath(path.dirname(__file__))
-with open(path.join(cur_dir, 'requirements.txt'), 'r') as f:
+with open(path.join(cur_dir, "requirements.txt"), "r") as f:
requirements = f.read().split()
dev_requirements = []
-with open(path.join(cur_dir, 'dev-requirements.txt'), 'r') as f:
+with open(path.join(cur_dir, "dev-requirements.txt"), "r") as f:
dev_requirements = f.read().split()
-with open(path.join(cur_dir, 'README.md'), 'r', encoding='utf-8') as f:
+with open(path.join(cur_dir, "README.md"), "r", encoding="utf-8") as f:
long_description = f.read()
# Remove the 'optional' requirements
-optional = ('PyQt5', 'PySide', 'psutil', 'pcaspy', 'pyepics')
+optional = ("PyQt5", "PySide", "psutil", "pcaspy", "pyepics")
for package in optional:
if package in requirements:
requirements.remove(package)
-extras_require = {
- 'PySide': ['PySide'],
- 'pyepics': ['pyepics'],
- 'perf': ['psutil'],
- 'dev': dev_requirements
-}
+extras_require = {"PySide": ["PySide"], "pyepics": ["pyepics"], "perf": ["psutil"], "dev": dev_requirements}
if "CONDA_PREFIX" not in environ:
- extras_require['PyQt5'] = ['PyQt5']
+ extras_require["PyQt5"] = ["PyQt5"]
else:
print("******************************************************************")
print("* WARNING *")
@@ -46,43 +41,37 @@
print("https://github.com/ContinuumIO/anaconda-issues/issues/1554")
print("******************************************************************")
-extras_require['all'] = sorted(set(sum(extras_require.values(), [])))
+extras_require["all"] = sorted(set(sum(extras_require.values(), [])))
# Preference for PyQt5 if you select ALL...
-extras_require['all'].remove('PySide')
+extras_require["all"].remove("PySide")
entry_style = "gui_scripts"
if platform.system() == "Windows":
entry_style = "console_scripts"
setup(
- name='pydm',
+ name="pydm",
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
# Author details
- author='SLAC National Accelerator Laboratory',
-
+ author="SLAC National Accelerator Laboratory",
packages=find_packages(),
- package_dir={'pydm':'pydm', 'pydm_launcher':'pydm_launcher'},
- description='Python Display Manager',
+ package_dir={"pydm": "pydm", "pydm_launcher": "pydm_launcher"},
+ description="Python Display Manager",
long_description=long_description,
- long_description_content_type='text/markdown',
- url='https://github.com/slaclab/pydm',
- entry_points={
- entry_style: [
- 'pydm=pydm_launcher.main:main'
- ]
- },
- license='BSD',
-
+ long_description_content_type="text/markdown",
+ url="https://github.com/slaclab/pydm",
+ entry_points={entry_style: ["pydm=pydm_launcher.main:main"]},
+ license="BSD",
install_requires=requirements,
extras_require=extras_require,
include_package_data=True,
classifiers=[
- 'License :: OSI Approved :: BSD License',
- 'Development Status :: 4 - Beta',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- ]
+ "License :: OSI Approved :: BSD License",
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ ],
)
diff --git a/versioneer.py b/versioneer.py
index f84667eb7b..09e709a9ab 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -1,4 +1,3 @@
-
# Version: 0.18
"""The Versioneer - like a rocketeer, but for versions.
@@ -277,6 +276,7 @@
"""
from __future__ import print_function
+
try:
import configparser
except ImportError:
@@ -308,11 +308,13 @@ def get_root():
setup_py = os.path.join(root, "setup.py")
versioneer_py = os.path.join(root, "versioneer.py")
if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)):
- err = ("Versioneer was unable to run the project root directory. "
- "Versioneer requires setup.py to be executed from "
- "its immediate directory (like 'python setup.py COMMAND'), "
- "or in a way that lets it use sys.argv[0] to find the root "
- "(like 'python path/to/setup.py COMMAND').")
+ err = (
+ "Versioneer was unable to run the project root directory. "
+ "Versioneer requires setup.py to be executed from "
+ "its immediate directory (like 'python setup.py COMMAND'), "
+ "or in a way that lets it use sys.argv[0] to find the root "
+ "(like 'python path/to/setup.py COMMAND')."
+ )
raise VersioneerBadRootError(err)
try:
# Certain runtime workflows (setup.py install/develop in a setuptools
@@ -325,8 +327,7 @@ def get_root():
me_dir = os.path.normcase(os.path.splitext(me)[0])
vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0])
if me_dir != vsr_dir:
- print("Warning: build in %s is using versioneer.py from %s"
- % (os.path.dirname(me), versioneer_py))
+ print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py))
except NameError:
pass
return root
@@ -347,6 +348,7 @@ def get(parser, name):
if parser.has_option("versioneer", name):
return parser.get("versioneer", name)
return None
+
cfg = VersioneerConfig()
cfg.VCS = VCS
cfg.style = get(parser, "style") or ""
@@ -371,17 +373,18 @@ class NotThisMethod(Exception):
def register_vcs_handler(vcs, method): # decorator
"""Decorator to mark a method as the handler for a particular VCS."""
+
def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f
+
return decorate
-def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
- env=None):
+def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
p = None
@@ -389,10 +392,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
try:
dispcmd = str([c] + args)
# remember shell=False, so use git.cmd on windows, not just git
- p = subprocess.Popen([c] + args, cwd=cwd, env=env,
- stdout=subprocess.PIPE,
- stderr=(subprocess.PIPE if hide_stderr
- else None))
+ p = subprocess.Popen(
+ [c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)
+ )
break
except EnvironmentError:
e = sys.exc_info()[1]
@@ -417,7 +419,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
return stdout, p.returncode
-LONG_VERSION_PY['git'] = '''
+LONG_VERSION_PY[
+ "git"
+] = '''
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (built by setup.py sdist) and build
@@ -992,7 +996,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
TAG = "tag: "
- tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
+ tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)])
if not tags:
# Either we're using git < 1.8.3, or there really are no tags. We use
# a heuristic: assume all version tags have a digit. The old git %d
@@ -1001,7 +1005,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
# between branches and tags. By ignoring refnames without digits, we
# filter out many common branch names like "release" and
# "stabilization", as well as "HEAD" and "master".
- tags = set([r for r in refs if re.search(r'\d', r)])
+ tags = set([r for r in refs if re.search(r"\d", r)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
@@ -1009,19 +1013,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
- r = ref[len(tag_prefix):]
+ r = ref[len(tag_prefix) :]
if verbose:
print("picking %s" % r)
- return {"version": r,
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": None,
- "date": date}
+ return {
+ "version": r,
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False,
+ "error": None,
+ "date": date,
+ }
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
- return {"version": "0+unknown",
- "full-revisionid": keywords["full"].strip(),
- "dirty": False, "error": "no suitable tags", "date": None}
+ return {
+ "version": "0+unknown",
+ "full-revisionid": keywords["full"].strip(),
+ "dirty": False,
+ "error": "no suitable tags",
+ "date": None,
+ }
@register_vcs_handler("git", "pieces_from_vcs")
@@ -1036,8 +1047,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
- out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
- hide_stderr=True)
+ out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
@@ -1045,10 +1055,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
# if there isn't one, this yields HEX[-dirty] (no NUM)
- describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
- "--always", "--long",
- "--match", "%s*" % tag_prefix],
- cwd=root)
+ describe_out, rc = run_command(
+ GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root
+ )
# --long was added in git-1.5.5
if describe_out is None:
raise NotThisMethod("'git describe' failed")
@@ -1071,17 +1080,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
- git_describe = git_describe[:git_describe.rindex("-dirty")]
+ git_describe = git_describe[: git_describe.rindex("-dirty")]
# now we have TAG-NUM-gHEX or HEX
if "-" in git_describe:
# TAG-NUM-gHEX
- mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
+ mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
- pieces["error"] = ("unable to parse git-describe output: '%s'"
- % describe_out)
+ pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
return pieces
# tag
@@ -1090,10 +1098,9 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
- pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
- % (full_tag, tag_prefix))
+ pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)
return pieces
- pieces["closest-tag"] = full_tag[len(tag_prefix):]
+ pieces["closest-tag"] = full_tag[len(tag_prefix) :]
# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
@@ -1104,13 +1111,11 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
else:
# HEX: no tags
pieces["closest-tag"] = None
- count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
- cwd=root)
+ count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
pieces["distance"] = int(count_out) # total number of commits
# commit date: see ISO-8601 comment in git_versions_from_keywords()
- date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
- cwd=root)[0].strip()
+ date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
return pieces
@@ -1166,16 +1171,19 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
for i in range(3):
dirname = os.path.basename(root)
if dirname.startswith(parentdir_prefix):
- return {"version": dirname[len(parentdir_prefix):],
- "full-revisionid": None,
- "dirty": False, "error": None, "date": None}
+ return {
+ "version": dirname[len(parentdir_prefix) :],
+ "full-revisionid": None,
+ "dirty": False,
+ "error": None,
+ "date": None,
+ }
else:
rootdirs.append(root)
root = os.path.dirname(root) # up a level
if verbose:
- print("Tried directories %s but none started with prefix %s" %
- (str(rootdirs), parentdir_prefix))
+ print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@@ -1204,11 +1212,9 @@ def versions_from_file(filename):
contents = f.read()
except EnvironmentError:
raise NotThisMethod("unable to read _version.py")
- mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON",
- contents, re.M | re.S)
+ mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S)
if not mo:
- mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON",
- contents, re.M | re.S)
+ mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S)
if not mo:
raise NotThisMethod("no version_json in _version.py")
return json.loads(mo.group(1))
@@ -1217,8 +1223,7 @@ def versions_from_file(filename):
def write_to_version_file(filename, versions):
"""Write the given version number to the given _version.py file."""
os.unlink(filename)
- contents = json.dumps(versions, sort_keys=True,
- indent=1, separators=(",", ": "))
+ contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": "))
with open(filename, "w") as f:
f.write(SHORT_VERSION_PY % contents)
@@ -1250,8 +1255,7 @@ def render_pep440(pieces):
rendered += ".dirty"
else:
# exception #1
- rendered = "0+untagged.%d.g%s" % (pieces["distance"],
- pieces["short"])
+ rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
@@ -1365,11 +1369,13 @@ def render_git_describe_long(pieces):
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
- return {"version": "unknown",
- "full-revisionid": pieces.get("long"),
- "dirty": None,
- "error": pieces["error"],
- "date": None}
+ return {
+ "version": "unknown",
+ "full-revisionid": pieces.get("long"),
+ "dirty": None,
+ "error": pieces["error"],
+ "date": None,
+ }
if not style or style == "default":
style = "pep440" # the default
@@ -1389,9 +1395,13 @@ def render(pieces, style):
else:
raise ValueError("unknown style '%s'" % style)
- return {"version": rendered, "full-revisionid": pieces["long"],
- "dirty": pieces["dirty"], "error": None,
- "date": pieces.get("date")}
+ return {
+ "version": rendered,
+ "full-revisionid": pieces["long"],
+ "dirty": pieces["dirty"],
+ "error": None,
+ "date": pieces.get("date"),
+ }
class VersioneerBadRootError(Exception):
@@ -1414,8 +1424,7 @@ def get_versions(verbose=False):
handlers = HANDLERS.get(cfg.VCS)
assert handlers, "unrecognized VCS '%s'" % cfg.VCS
verbose = verbose or cfg.verbose
- assert cfg.versionfile_source is not None, \
- "please set versioneer.versionfile_source"
+ assert cfg.versionfile_source is not None, "please set versioneer.versionfile_source"
assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix"
versionfile_abs = os.path.join(root, cfg.versionfile_source)
@@ -1469,9 +1478,13 @@ def get_versions(verbose=False):
if verbose:
print("unable to compute version")
- return {"version": "0+unknown", "full-revisionid": None,
- "dirty": None, "error": "unable to compute version",
- "date": None}
+ return {
+ "version": "0+unknown",
+ "full-revisionid": None,
+ "dirty": None,
+ "error": "unable to compute version",
+ "date": None,
+ }
def get_version():
@@ -1520,6 +1533,7 @@ def run(self):
print(" date: %s" % vers.get("date"))
if vers["error"]:
print(" error: %s" % vers["error"])
+
cmds["version"] = cmd_version
# we override "build_py" in both distutils and setuptools
@@ -1552,14 +1566,15 @@ def run(self):
# now locate _version.py in the new build/ directory and replace
# it with an updated value
if cfg.versionfile_build:
- target_versionfile = os.path.join(self.build_lib,
- cfg.versionfile_build)
+ target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build)
print("UPDATING %s" % target_versionfile)
write_to_version_file(target_versionfile, versions)
+
cmds["build_py"] = cmd_build_py
if "cx_Freeze" in sys.modules: # cx_freeze enabled?
from cx_Freeze.dist import build_exe as _build_exe
+
# nczeczulin reports that py2exe won't like the pep440-style string
# as FILEVERSION, but it can be used for PRODUCTVERSION, e.g.
# setup(console=[{
@@ -1580,17 +1595,21 @@ def run(self):
os.unlink(target_versionfile)
with open(cfg.versionfile_source, "w") as f:
LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG %
- {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
+ f.write(
+ LONG
+ % {
+ "DOLLAR": "$",
+ "STYLE": cfg.style,
+ "TAG_PREFIX": cfg.tag_prefix,
+ "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+ "VERSIONFILE_SOURCE": cfg.versionfile_source,
+ }
+ )
+
cmds["build_exe"] = cmd_build_exe
del cmds["build_py"]
- if 'py2exe' in sys.modules: # py2exe enabled?
+ if "py2exe" in sys.modules: # py2exe enabled?
try:
from py2exe.distutils_buildexe import py2exe as _py2exe # py3
except ImportError:
@@ -1609,13 +1628,17 @@ def run(self):
os.unlink(target_versionfile)
with open(cfg.versionfile_source, "w") as f:
LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG %
- {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
+ f.write(
+ LONG
+ % {
+ "DOLLAR": "$",
+ "STYLE": cfg.style,
+ "TAG_PREFIX": cfg.tag_prefix,
+ "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+ "VERSIONFILE_SOURCE": cfg.versionfile_source,
+ }
+ )
+
cmds["py2exe"] = cmd_py2exe
# we override different "sdist" commands for both environments
@@ -1642,8 +1665,8 @@ def make_release_tree(self, base_dir, files):
# updated value
target_versionfile = os.path.join(base_dir, cfg.versionfile_source)
print("UPDATING %s" % target_versionfile)
- write_to_version_file(target_versionfile,
- self._versioneer_generated_versions)
+ write_to_version_file(target_versionfile, self._versioneer_generated_versions)
+
cmds["sdist"] = cmd_sdist
return cmds
@@ -1698,11 +1721,9 @@ def do_setup():
root = get_root()
try:
cfg = get_config_from_root(root)
- except (EnvironmentError, configparser.NoSectionError,
- configparser.NoOptionError) as e:
+ except (EnvironmentError, configparser.NoSectionError, configparser.NoOptionError) as e:
if isinstance(e, (EnvironmentError, configparser.NoSectionError)):
- print("Adding sample versioneer config to setup.cfg",
- file=sys.stderr)
+ print("Adding sample versioneer config to setup.cfg", file=sys.stderr)
with open(os.path.join(root, "setup.cfg"), "a") as f:
f.write(SAMPLE_CONFIG)
print(CONFIG_ERROR, file=sys.stderr)
@@ -1711,15 +1732,18 @@ def do_setup():
print(" creating %s" % cfg.versionfile_source)
with open(cfg.versionfile_source, "w") as f:
LONG = LONG_VERSION_PY[cfg.VCS]
- f.write(LONG % {"DOLLAR": "$",
- "STYLE": cfg.style,
- "TAG_PREFIX": cfg.tag_prefix,
- "PARENTDIR_PREFIX": cfg.parentdir_prefix,
- "VERSIONFILE_SOURCE": cfg.versionfile_source,
- })
-
- ipy = os.path.join(os.path.dirname(cfg.versionfile_source),
- "__init__.py")
+ f.write(
+ LONG
+ % {
+ "DOLLAR": "$",
+ "STYLE": cfg.style,
+ "TAG_PREFIX": cfg.tag_prefix,
+ "PARENTDIR_PREFIX": cfg.parentdir_prefix,
+ "VERSIONFILE_SOURCE": cfg.versionfile_source,
+ }
+ )
+
+ ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py")
if os.path.exists(ipy):
try:
with open(ipy, "r") as f:
@@ -1761,8 +1785,7 @@ def do_setup():
else:
print(" 'versioneer.py' already in MANIFEST.in")
if cfg.versionfile_source not in simple_includes:
- print(" appending versionfile_source ('%s') to MANIFEST.in" %
- cfg.versionfile_source)
+ print(" appending versionfile_source ('%s') to MANIFEST.in" % cfg.versionfile_source)
with open(manifest_in, "a") as f:
f.write("include %s\n" % cfg.versionfile_source)
else: