Skip to content

Commit 58a5d38

Browse files
committed
Added support for QtPy 2, PyQt6, PySide6
1 parent 4fc7a03 commit 58a5d38

25 files changed

+149
-188
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# PythonQwt Releases
22

3+
## Version 0.10.0
4+
5+
- Added support for QtPy 2, PyQt6 and PySide6.
6+
- Dropped support for Python 2.
7+
38
## Version 0.9.2
49

510
- Curve plotting: added support for `numpy.float32` data type.

README.md

+5-59
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ from qwt import tests
5555
tests.run()
5656
```
5757

58-
or from the command line (script name depends on Python major version number):
58+
or from the command line:
5959

6060
```bash
61-
PythonQwt-py3
61+
PythonQwt
6262
```
6363

6464
Tests may also be executed in unattended mode:
6565

6666
```bash
67-
PythonQwt-tests-py3 --mode unattended
67+
PythonQwt-tests --mode unattended
6868
```
6969

7070
## Overview
@@ -87,65 +87,11 @@ for more details on API limitations when comparing to Qwt.
8787

8888
### Requirements
8989

90-
- Python >=2.6 or Python >=3.2
91-
- PyQt4 >=4.4 or PyQt5 >= 5.5 (or PySide2, still experimental, see below)
90+
- Python >=3.4
91+
- PyQt4, PyQt5, PyQt6 or PySide6
9292
- QtPy >= 1.3
9393
- NumPy >= 1.5
9494

95-
### Why PySide2 support is still experimental
96-
97-
![PyQt5 vs PySide2](doc/images/pyqt5_vs_pyside2.png)
98-
99-
Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
100-
huge performance issue with PySide2 (see screenshot above). This is due to the fact
101-
that `QPainter.drawPolyline` (the `QPainter.drawPolyline` method has already been
102-
optimized thanks to Cristian Maureira-Fredes from Python-Qt development team, see
103-
[this bug report](https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366)) is
104-
much more efficient in PyQt5 than it is in PySide2 (see
105-
[this bug report](https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1540)).
106-
107-
As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
108-
instead of PySide2 when it comes to representing huge data sets (except if you do not
109-
use the "dots" style for drawing curves).
110-
111-
However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
112-
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see the part related to
113-
PySide2 in the code below).
114-
115-
```python
116-
def array2d_to_qpolygonf(xdata, ydata):
117-
"""
118-
Utility function to convert two 1D-NumPy arrays representing curve data
119-
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
120-
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
121-
122-
License/copyright: MIT License © Pierre Raybaut 2020.
123-
124-
:param numpy.ndarray xdata: 1D-NumPy array (numpy.float64)
125-
:param numpy.ndarray ydata: 1D-NumPy array (numpy.float64)
126-
:return: Polyline
127-
:rtype: QtGui.QPolygonF
128-
"""
129-
dtype = np.float64
130-
if not (
131-
xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]
132-
and xdata.dtype == ydata.dtype == dtype
133-
):
134-
raise ValueError("Arguments must be 1D, float64 NumPy arrays with same size")
135-
size = xdata.size
136-
polyline = QPolygonF(size)
137-
if PYSIDE2: # PySide2 (obviously...)
138-
address = shiboken2.getCppPointer(polyline.data())[0]
139-
buffer = (ctypes.c_double * 2 * size).from_address(address)
140-
else: # PyQt4, PyQt5
141-
buffer = polyline.data()
142-
buffer.setsize(2 * size * np.finfo(dtype).dtype.itemsize)
143-
memory = np.frombuffer(buffer, dtype)
144-
memory[: (size - 1) * 2 + 1 : 2] = xdata
145-
memory[1 : (size - 1) * 2 + 2 : 2] = ydata
146-
return polyline
147-
```
148-
14995
## Installation
15096

15197
From the source package:

doc/examples/index.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ The two lines above execute the ``PythonQwt`` test launcher:
1616
.. image:: /../qwt/tests/data/testlauncher.png
1717

1818
GUI-based test launcher can be executed from the command line thanks to the
19-
``PythonQwt-py3`` test script (or ``PythonQwt-py2`` for Python 2).
19+
``PythonQwt`` test script.
2020

2121
Unit tests may be executed from the commande line thanks to the console-based script
22-
``PythonQwt-tests-py3``: ``PythonQwt-tests-py3 --mode unattended``.
22+
``PythonQwt-tests``: ``PythonQwt-tests --mode unattended``.
2323

2424
Tests
2525
-----

doc/images/QwtPlot_example.png

-22.3 KB
Loading

doc/images/pyqt5_vs_pyside2.png

-18.1 KB
Binary file not shown.

doc/images/symbol_path_example.png

-17 Bytes
Loading

doc/installation.rst

+2-21
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ Dependencies
55
------------
66

77
Requirements:
8-
* Python 2.x (x>=6) or 3.x (x>=2)
9-
* PyQt4 4.x (x>=4) or PyQt5 5.x (x>=5) or PySide2 (still experimental, see below)
8+
* Python 3.x (x>=4)
9+
* PyQt4 4.x (x>=4), PyQt5 5.x (x>=5), PyQt6 or PySide6
1010
* QtPy >= 1.3
1111
* NumPy 1.x (x>=5)
1212
* Sphinx 1.x (x>=1) for documentation generation
@@ -18,25 +18,6 @@ From the source package:
1818

1919
`python setup.py install`
2020

21-
Why PySide2 support is still experimental
22-
-----------------------------------------
23-
24-
.. image:: /images/pyqt5_vs_pyside2.png
25-
26-
Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
27-
huge performance issue with PySide2 (see screenshot above). This is due to the fact
28-
that `QPainter.drawPolyline` is much more efficient in PyQt5 than it is in PySide2
29-
(see `this bug report <https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366>`_).
30-
31-
As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
32-
instead of PySide2 when it comes to representing huge data sets.
33-
34-
However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
35-
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see code below).
36-
37-
.. literalinclude:: /../qwt/plot_curve.py
38-
:pyobject: array2d_to_qpolygonf
39-
4021
Help and support
4122
----------------
4223

qwt/__init__.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
1111
The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical
1212
user interfaces for the Python programming language. It is compatible with
13-
both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it
14-
could be in the near future as it would "only" requires testing to support
15-
it as a stable alternative to PyQt).
13+
``PyQt4``, ``PyQt5``, ``PyQt6`` and ``PySide6``.
1614
1715
It consists of a single Python package named `qwt` which is a pure Python
1816
implementation of Qwt C++ library with some limitations.
@@ -28,7 +26,7 @@
2826
.. _GitHubPage: http://pierreraybaut.github.io/PythonQwt
2927
.. _GitHub: https://github.com/PierreRaybaut/PythonQwt
3028
"""
31-
__version__ = "0.9.2"
29+
__version__ = "0.10.0"
3230
QWT_VERSION_STR = "6.1.5"
3331

3432
import warnings

qwt/legend.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,6 @@ def sizeHint(self):
390390
sz.setHeight(max([sz.height(), self.__data.icon.height() + 4]))
391391
if self.__data.itemMode != QwtLegendData.ReadOnly:
392392
sz += buttonShift(self)
393-
sz = sz.expandedTo(QApplication.globalStrut())
394393
return sz
395394

396395
def paintEvent(self, e):
@@ -912,7 +911,7 @@ def renderLegend(self, painter, rect, fillBackground):
912911
legendLayout = self.__data.view.contentsWidget.layout()
913912
if legendLayout is None:
914913
return
915-
left, right, top, bottom = self.getContentsMargins()
914+
left, right, top, bottom = self.layout().getContentsMargins()
916915
layoutRect = QRect()
917916
layoutRect.setLeft(math.ceil(rect.left()) + left)
918917
layoutRect.setTop(math.ceil(rect.top()) + top)

qwt/null_paintdevice.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
:members:
1414
"""
1515

16+
import os
17+
1618
from qtpy.QtGui import QPaintEngine, QPainterPath, QPaintDevice
17-
from qtpy import PYSIDE2
19+
20+
QT_API = os.environ["QT_API"]
1821

1922

2023
class QwtNullPaintDevice_PrivateData(object):
@@ -59,7 +62,7 @@ def drawLines(self, lines, lineCount=None):
5962
device = self.nullDevice()
6063
if device is None:
6164
return
62-
if device.mode() != QwtNullPaintDevice.NormalMode and not PYSIDE2:
65+
if device.mode() != QwtNullPaintDevice.NormalMode and QT_API.startswith("pyqt"):
6366
try:
6467
QPaintEngine.drawLines(lines, lineCount)
6568
except TypeError:

qwt/plot.py

-4
Original file line numberDiff line numberDiff line change
@@ -1437,10 +1437,6 @@ def drawItems(self, painter, canvasRect, maps):
14371437
QPainter.Antialiasing,
14381438
item.testRenderHint(QwtPlotItem.RenderAntialiased),
14391439
)
1440-
painter.setRenderHint(
1441-
QPainter.HighQualityAntialiasing,
1442-
item.testRenderHint(QwtPlotItem.RenderAntialiased),
1443-
)
14441440
item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect)
14451441
painter.restore()
14461442

qwt/plot_canvas.py

+20-11
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313
:members:
1414
"""
1515

16+
import os
17+
1618
from qwt.null_paintdevice import QwtNullPaintDevice
1719
from qwt.painter import QwtPainter
1820

19-
from qtpy import PYQT5
2021
from qtpy.QtGui import (
2122
QPaintEngine,
2223
QPen,
@@ -30,11 +31,20 @@
3031
qAlpha,
3132
QPolygonF,
3233
)
33-
from qtpy.QtWidgets import QFrame, QStyleOption, QStyle, QStyleOptionFrame
34+
from qtpy.QtWidgets import QFrame, QStyleOption, QStyle
3435
from qtpy.QtCore import Qt, QSizeF, QEvent, QPointF, QRectF
3536
from qtpy import QtCore as QC
3637

38+
3739
QT_MAJOR_VERSION = int(QC.__version__.split(".")[0])
40+
QT_API = os.environ["QT_API"]
41+
42+
if QT_API in ("pyqt", "pyqt4"):
43+
from PyQt4.QtGui import QStyleOptionFrameV3 as QStyleOptionFrame
44+
elif QT_API == "pyside2":
45+
from PySide2.QtWidgets import QStyleOptionFrame
46+
else:
47+
from qtpy.QtWidgets import QStyleOptionFrame
3848

3949

4050
class Border(object):
@@ -126,7 +136,7 @@ def alignCornerRects(self, rect):
126136
def _rects_conv_PyQt5(rects):
127137
# PyQt5 compatibility: the conversion from QRect to QRectF should not
128138
# be necessary but it seems to be anyway... PyQt5 bug?
129-
if PYQT5:
139+
if QT_API == "pyqt5":
130140
return [QRectF(rect) for rect in rects]
131141
else:
132142
return rects
@@ -172,7 +182,13 @@ def qwtDrawBackground(painter, canvas):
172182
else:
173183
painter.setPen(Qt.NoPen)
174184
painter.setBrush(brush)
175-
painter.drawRects(_rects_conv_PyQt5(painter.clipRegion().rects()))
185+
clipregion = painter.clipRegion()
186+
try:
187+
rects = clipregion.rects()
188+
except AttributeError:
189+
# Qt6: no equivalent to 'rects' method...
190+
rects = [clipregion.begin()]
191+
painter.drawRects(_rects_conv_PyQt5(rects))
176192

177193
painter.restore()
178194

@@ -731,13 +747,6 @@ def drawBorder(self, painter):
731747
self.frameStyle(),
732748
)
733749
else:
734-
if PYQT5:
735-
from qtpy.QtWidgets import QStyleOptionFrame
736-
else:
737-
try:
738-
from PyQt4.QtGui import QStyleOptionFrameV3 as QStyleOptionFrame
739-
except ImportError:
740-
from PySide2.QtWidgets import QStyleOptionFrame
741750
opt = QStyleOptionFrame()
742751
opt.initFrom(self)
743752
frameShape = self.frameStyle() & QFrame.Shape_Mask

qwt/plot_curve.py

+31-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
:members:
1414
"""
1515

16+
import os
17+
1618
from qwt.text import QwtText
1719
from qwt.plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData
1820
from qwt._math import qwtSqr
@@ -27,12 +29,16 @@
2729
from qwt.plot_directpainter import QwtPlotDirectPainter
2830
from qwt.qthelpers import qcolor_from_str
2931

30-
from qtpy import PYSIDE2
3132
from qtpy.QtGui import QPen, QBrush, QPainter, QPolygonF, QColor
3233
from qtpy.QtCore import QSize, Qt, QRectF, QPointF
3334

34-
if PYSIDE2:
35-
import shiboken2
35+
QT_API = os.environ["QT_API"]
36+
37+
if QT_API == "pyside2":
38+
import shiboken2 as shiboken
39+
import ctypes
40+
elif QT_API == "pyside6":
41+
import shiboken6 as shiboken
3642
import ctypes
3743

3844
import numpy as np
@@ -62,12 +68,12 @@ def qwtVerifyRange(size, i1, i2):
6268

6369
def array2d_to_qpolygonf(xdata, ydata):
6470
"""
65-
Utility function to convert two 1D-NumPy arrays representing curve data
66-
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
71+
Utility function to convert two 1D-NumPy arrays representing curve data
72+
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
6773
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
68-
74+
6975
License/copyright: MIT License © Pierre Raybaut 2020-2021.
70-
76+
7177
:param numpy.ndarray xdata: 1D-NumPy array
7278
:param numpy.ndarray ydata: 1D-NumPy array
7379
:return: Polyline
@@ -76,11 +82,16 @@ def array2d_to_qpolygonf(xdata, ydata):
7682
if not (xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]):
7783
raise ValueError("Arguments must be 1D NumPy arrays with same size")
7884
size = xdata.size
79-
polyline = QPolygonF(size)
80-
if PYSIDE2: # PySide2 (obviously...)
81-
address = shiboken2.getCppPointer(polyline.data())[0]
85+
if QT_API.startswith("pyside"): # PySide (obviously...)
86+
if QT_API == "pyside2":
87+
polyline = QPolygonF(size)
88+
else:
89+
polyline = QPolygonF()
90+
polyline.resize(size)
91+
address = shiboken.getCppPointer(polyline.data())[0]
8292
buffer = (ctypes.c_double * 2 * size).from_address(address)
8393
else: # PyQt4, PyQt5
94+
polyline = QPolygonF(size)
8495
buffer = polyline.data()
8596
buffer.setsize(16 * size) # 16 bytes per point: 8 bytes per X,Y value (float64)
8697
memory = np.frombuffer(buffer, np.float64)
@@ -685,7 +696,12 @@ def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to):
685696
:py:meth:`draw()`, :py:meth:`drawSticks()`,
686697
:py:meth:`drawDots()`, :py:meth:`drawLines()`
687698
"""
688-
polygon = QPolygonF(2 * (to - from_) + 1)
699+
size = 2 * (to - from_) + 1
700+
if QT_API == "pyside6":
701+
polygon = QPolygonF()
702+
polygon.resize(size)
703+
else:
704+
polygon = QPolygonF(size)
689705
inverted = self.orientation() == Qt.Vertical
690706
if self.__data.attributes & self.Inverted:
691707
inverted = not inverted
@@ -787,14 +803,14 @@ def closePolyline(self, painter, xMap, yMap, polygon):
787803
if yMap.transformation():
788804
baseline = yMap.transformation().bounded(baseline)
789805
refY = yMap.transform(baseline)
790-
polygon += QPointF(polygon.last().x(), refY)
791-
polygon += QPointF(polygon.first().x(), refY)
806+
polygon.append(QPointF(polygon.last().x(), refY))
807+
polygon.append(QPointF(polygon.first().x(), refY))
792808
else:
793809
if xMap.transformation():
794810
baseline = xMap.transformation().bounded(baseline)
795811
refX = xMap.transform(baseline)
796-
polygon += QPointF(refX, polygon.last().y())
797-
polygon += QPointF(refX, polygon.first().y())
812+
polygon.append(QPointF(refX, polygon.last().y()))
813+
polygon.append(QPointF(refX, polygon.first().y()))
798814

799815
def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to):
800816
"""

0 commit comments

Comments
 (0)