PythonQwt 0.15.0__tar.gz → 0.16.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonqwt-0.15.0/PythonQwt.egg-info → pythonqwt-0.16.1}/PKG-INFO +22 -10
- {pythonqwt-0.15.0 → pythonqwt-0.16.1/PythonQwt.egg-info}/PKG-INFO +22 -10
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt.egg-info/SOURCES.txt +1 -1
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt.egg-info/requires.txt +3 -1
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/README.md +16 -6
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/conf.py +6 -1
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/index.rst +7 -7
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/installation.rst +4 -0
- pythonqwt-0.16.1/doc/issue93_optimization_summary.md +284 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/overview.rst +6 -6
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/pyproject.toml +5 -5
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/__init__.py +3 -3
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/graphic.py +30 -13
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/null_paintdevice.py +5 -1
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/painter_command.py +49 -12
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_canvas.py +1 -1
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/scale_div.py +5 -3
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/scale_draw.py +79 -36
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/scale_engine.py +17 -10
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/scale_map.py +19 -10
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_bodedemo.py +1 -2
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_relativemargin.py +2 -2
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/text.py +162 -37
- pythonqwt-0.15.0/doc/requirements.txt +0 -4
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/LICENSE +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/MANIFEST.in +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt-tests.desktop +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt.egg-info/dependency_links.txt +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt.egg-info/entry_points.txt +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/PythonQwt.egg-info/top_level.txt +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/_static/PythonQwt_logo.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/_static/QwtPlot_example.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/_static/panorama.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/_static/symbol_path_example.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/bodedemo.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/cartesian.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/cpudemo.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/curvebenchmark1.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/curvebenchmark2.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/curvedemo1.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/curvedemo2.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/data.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/errorbar.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/eventfilter.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/image.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/logcurve.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/mapdemo.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/multidemo.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/simple.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/examples/vertical.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/index.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/plot_example.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/graphic.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/index.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/interval.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/plot.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/plot_directpainter.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/plot_layout.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/plot_series.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/scale.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/symbol.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/text.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/toqimage.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/reference/transform.rst +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/doc/symbol_path_example.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/_math.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/color_map.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/column_symbol.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/dyngrid_layout.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/interval.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/legend.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/painter.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_curve.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_directpainter.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_grid.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_layout.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_marker.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_renderer.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/plot_series.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/qthelpers.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/scale_widget.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/symbol.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/__init__.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/comparative_benchmarks.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/conftest.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/PythonQwt.svg +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/bodedemo.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/cartesian.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/cpudemo.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/curvebenchmark1.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/curvebenchmark2.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/curvedemo1.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/curvedemo2.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/data.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/errorbar.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/eventfilter.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/image.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/loadtest.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/logcurve.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/mapdemo.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/multidemo.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/simple.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/stylesheet.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/symbol.svg +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/symbols.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/testlauncher.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/data/vertical.png +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_backingstore.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_cartesian.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_cpudemo.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_curvebenchmark1.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_curvebenchmark2.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_curvedemo1.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_curvedemo2.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_data.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_errorbar.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_eventfilter.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_highdpi.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_image.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_loadtest.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_logcurve.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_mapdemo.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_multidemo.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_simple.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_stylesheet.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_symbols.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/test_vertical.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/tests/utils.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/toqimage.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/qwt/transform.py +0 -0
- {pythonqwt-0.15.0 → pythonqwt-0.16.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PythonQwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.1
|
|
4
4
|
Summary: Qt plotting widgets for Python
|
|
5
5
|
Author-email: Pierre Raybaut <pierre.raybaut@gmail.com>
|
|
6
6
|
License: PythonQwt License Agreement
|
|
@@ -666,12 +666,11 @@ License: PythonQwt License Agreement
|
|
|
666
666
|
Project-URL: Homepage, https://github.com/PlotPyStack/PythonQwt/
|
|
667
667
|
Project-URL: Documentation, https://PythonQwt.readthedocs.io/en/latest/
|
|
668
668
|
Classifier: Topic :: Scientific/Engineering
|
|
669
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
669
670
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
670
671
|
Classifier: Topic :: Software Development :: Widget Sets
|
|
671
672
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
672
673
|
Classifier: Topic :: Utilities
|
|
673
|
-
Classifier: Topic :: Scientific/Engineering
|
|
674
|
-
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
675
674
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
676
675
|
Classifier: Operating System :: MacOS
|
|
677
676
|
Classifier: Operating System :: Microsoft :: Windows
|
|
@@ -683,15 +682,18 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
683
682
|
Classifier: Programming Language :: Python :: 3.11
|
|
684
683
|
Classifier: Programming Language :: Python :: 3.12
|
|
685
684
|
Classifier: Programming Language :: Python :: 3.13
|
|
685
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
686
686
|
Requires-Python: <4,>=3.9
|
|
687
687
|
Description-Content-Type: text/markdown
|
|
688
688
|
License-File: LICENSE
|
|
689
|
-
Requires-Dist: NumPy>=1.
|
|
689
|
+
Requires-Dist: NumPy>=1.21
|
|
690
690
|
Requires-Dist: QtPy>=1.9
|
|
691
691
|
Provides-Extra: dev
|
|
692
|
+
Requires-Dist: build; extra == "dev"
|
|
692
693
|
Requires-Dist: ruff; extra == "dev"
|
|
693
694
|
Requires-Dist: pylint; extra == "dev"
|
|
694
695
|
Requires-Dist: Coverage; extra == "dev"
|
|
696
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
695
697
|
Provides-Extra: doc
|
|
696
698
|
Requires-Dist: PyQt5; extra == "doc"
|
|
697
699
|
Requires-Dist: sphinx>6; extra == "doc"
|
|
@@ -760,7 +762,7 @@ tests.run()
|
|
|
760
762
|
or from the command line:
|
|
761
763
|
|
|
762
764
|
```bash
|
|
763
|
-
PythonQwt
|
|
765
|
+
PythonQwt-tests
|
|
764
766
|
```
|
|
765
767
|
|
|
766
768
|
Tests may also be executed in unattended mode:
|
|
@@ -773,9 +775,9 @@ PythonQwt-tests --mode unattended
|
|
|
773
775
|
|
|
774
776
|
The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations.
|
|
775
777
|
|
|
776
|
-
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `
|
|
778
|
+
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
|
|
777
779
|
|
|
778
|
-
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `
|
|
780
|
+
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `PlotPy`): `QwtPlotItem` (base class), `QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
|
|
779
781
|
|
|
780
782
|
See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt.
|
|
781
783
|
|
|
@@ -798,14 +800,14 @@ Compatibility table:
|
|
|
798
800
|
|
|
799
801
|
| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 |
|
|
800
802
|
|-------------------|-------|-------|---------|---------|
|
|
801
|
-
| 0.
|
|
803
|
+
| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
802
804
|
| Latest | ✅ | ✅ | ❌ | ✅ |
|
|
803
805
|
|
|
804
806
|
### Requirements
|
|
805
807
|
|
|
806
808
|
- Python >=3.9
|
|
807
|
-
- QtPy >= 1.
|
|
808
|
-
- NumPy >= 1.
|
|
809
|
+
- QtPy >= 1.9 (and a Python-to-Qt binding library, see above)
|
|
810
|
+
- NumPy >= 1.21
|
|
809
811
|
|
|
810
812
|
### Optional dependencies
|
|
811
813
|
|
|
@@ -814,12 +816,22 @@ Compatibility table:
|
|
|
814
816
|
|
|
815
817
|
### Installation
|
|
816
818
|
|
|
819
|
+
From PyPI:
|
|
820
|
+
|
|
821
|
+
```bash
|
|
822
|
+
pip install PythonQwt
|
|
823
|
+
```
|
|
824
|
+
|
|
817
825
|
From the source package:
|
|
818
826
|
|
|
819
827
|
```bash
|
|
820
828
|
python -m build
|
|
821
829
|
```
|
|
822
830
|
|
|
831
|
+
## Performance investigation
|
|
832
|
+
|
|
833
|
+
Tooling for performance benchmarks, profiling and visual-regression checks across PyQt5/PyQt6/PySide6 lives in [`scripts/`](scripts/README.md). See [`doc/issue93_optimization_summary.md`](doc/issue93_optimization_summary.md) for a worked example.
|
|
834
|
+
|
|
823
835
|
## Copyrights
|
|
824
836
|
|
|
825
837
|
### Main code base
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PythonQwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.1
|
|
4
4
|
Summary: Qt plotting widgets for Python
|
|
5
5
|
Author-email: Pierre Raybaut <pierre.raybaut@gmail.com>
|
|
6
6
|
License: PythonQwt License Agreement
|
|
@@ -666,12 +666,11 @@ License: PythonQwt License Agreement
|
|
|
666
666
|
Project-URL: Homepage, https://github.com/PlotPyStack/PythonQwt/
|
|
667
667
|
Project-URL: Documentation, https://PythonQwt.readthedocs.io/en/latest/
|
|
668
668
|
Classifier: Topic :: Scientific/Engineering
|
|
669
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
669
670
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
670
671
|
Classifier: Topic :: Software Development :: Widget Sets
|
|
671
672
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
672
673
|
Classifier: Topic :: Utilities
|
|
673
|
-
Classifier: Topic :: Scientific/Engineering
|
|
674
|
-
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
675
674
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
676
675
|
Classifier: Operating System :: MacOS
|
|
677
676
|
Classifier: Operating System :: Microsoft :: Windows
|
|
@@ -683,15 +682,18 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
683
682
|
Classifier: Programming Language :: Python :: 3.11
|
|
684
683
|
Classifier: Programming Language :: Python :: 3.12
|
|
685
684
|
Classifier: Programming Language :: Python :: 3.13
|
|
685
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
686
686
|
Requires-Python: <4,>=3.9
|
|
687
687
|
Description-Content-Type: text/markdown
|
|
688
688
|
License-File: LICENSE
|
|
689
|
-
Requires-Dist: NumPy>=1.
|
|
689
|
+
Requires-Dist: NumPy>=1.21
|
|
690
690
|
Requires-Dist: QtPy>=1.9
|
|
691
691
|
Provides-Extra: dev
|
|
692
|
+
Requires-Dist: build; extra == "dev"
|
|
692
693
|
Requires-Dist: ruff; extra == "dev"
|
|
693
694
|
Requires-Dist: pylint; extra == "dev"
|
|
694
695
|
Requires-Dist: Coverage; extra == "dev"
|
|
696
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
695
697
|
Provides-Extra: doc
|
|
696
698
|
Requires-Dist: PyQt5; extra == "doc"
|
|
697
699
|
Requires-Dist: sphinx>6; extra == "doc"
|
|
@@ -760,7 +762,7 @@ tests.run()
|
|
|
760
762
|
or from the command line:
|
|
761
763
|
|
|
762
764
|
```bash
|
|
763
|
-
PythonQwt
|
|
765
|
+
PythonQwt-tests
|
|
764
766
|
```
|
|
765
767
|
|
|
766
768
|
Tests may also be executed in unattended mode:
|
|
@@ -773,9 +775,9 @@ PythonQwt-tests --mode unattended
|
|
|
773
775
|
|
|
774
776
|
The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations.
|
|
775
777
|
|
|
776
|
-
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `
|
|
778
|
+
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
|
|
777
779
|
|
|
778
|
-
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `
|
|
780
|
+
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `PlotPy`): `QwtPlotItem` (base class), `QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
|
|
779
781
|
|
|
780
782
|
See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt.
|
|
781
783
|
|
|
@@ -798,14 +800,14 @@ Compatibility table:
|
|
|
798
800
|
|
|
799
801
|
| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 |
|
|
800
802
|
|-------------------|-------|-------|---------|---------|
|
|
801
|
-
| 0.
|
|
803
|
+
| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
802
804
|
| Latest | ✅ | ✅ | ❌ | ✅ |
|
|
803
805
|
|
|
804
806
|
### Requirements
|
|
805
807
|
|
|
806
808
|
- Python >=3.9
|
|
807
|
-
- QtPy >= 1.
|
|
808
|
-
- NumPy >= 1.
|
|
809
|
+
- QtPy >= 1.9 (and a Python-to-Qt binding library, see above)
|
|
810
|
+
- NumPy >= 1.21
|
|
809
811
|
|
|
810
812
|
### Optional dependencies
|
|
811
813
|
|
|
@@ -814,12 +816,22 @@ Compatibility table:
|
|
|
814
816
|
|
|
815
817
|
### Installation
|
|
816
818
|
|
|
819
|
+
From PyPI:
|
|
820
|
+
|
|
821
|
+
```bash
|
|
822
|
+
pip install PythonQwt
|
|
823
|
+
```
|
|
824
|
+
|
|
817
825
|
From the source package:
|
|
818
826
|
|
|
819
827
|
```bash
|
|
820
828
|
python -m build
|
|
821
829
|
```
|
|
822
830
|
|
|
831
|
+
## Performance investigation
|
|
832
|
+
|
|
833
|
+
Tooling for performance benchmarks, profiling and visual-regression checks across PyQt5/PyQt6/PySide6 lives in [`scripts/`](scripts/README.md). See [`doc/issue93_optimization_summary.md`](doc/issue93_optimization_summary.md) for a worked example.
|
|
834
|
+
|
|
823
835
|
## Copyrights
|
|
824
836
|
|
|
825
837
|
### Main code base
|
|
@@ -12,9 +12,9 @@ PythonQwt.egg-info/top_level.txt
|
|
|
12
12
|
doc/conf.py
|
|
13
13
|
doc/index.rst
|
|
14
14
|
doc/installation.rst
|
|
15
|
+
doc/issue93_optimization_summary.md
|
|
15
16
|
doc/overview.rst
|
|
16
17
|
doc/plot_example.py
|
|
17
|
-
doc/requirements.txt
|
|
18
18
|
doc/symbol_path_example.py
|
|
19
19
|
doc/_static/PythonQwt_logo.png
|
|
20
20
|
doc/_static/QwtPlot_example.png
|
|
@@ -57,7 +57,7 @@ tests.run()
|
|
|
57
57
|
or from the command line:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
PythonQwt
|
|
60
|
+
PythonQwt-tests
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Tests may also be executed in unattended mode:
|
|
@@ -70,9 +70,9 @@ PythonQwt-tests --mode unattended
|
|
|
70
70
|
|
|
71
71
|
The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations.
|
|
72
72
|
|
|
73
|
-
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `
|
|
73
|
+
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
|
|
74
74
|
|
|
75
|
-
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `
|
|
75
|
+
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `PlotPy`): `QwtPlotItem` (base class), `QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
|
|
76
76
|
|
|
77
77
|
See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt.
|
|
78
78
|
|
|
@@ -95,14 +95,14 @@ Compatibility table:
|
|
|
95
95
|
|
|
96
96
|
| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 |
|
|
97
97
|
|-------------------|-------|-------|---------|---------|
|
|
98
|
-
| 0.
|
|
98
|
+
| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
99
99
|
| Latest | ✅ | ✅ | ❌ | ✅ |
|
|
100
100
|
|
|
101
101
|
### Requirements
|
|
102
102
|
|
|
103
103
|
- Python >=3.9
|
|
104
|
-
- QtPy >= 1.
|
|
105
|
-
- NumPy >= 1.
|
|
104
|
+
- QtPy >= 1.9 (and a Python-to-Qt binding library, see above)
|
|
105
|
+
- NumPy >= 1.21
|
|
106
106
|
|
|
107
107
|
### Optional dependencies
|
|
108
108
|
|
|
@@ -111,12 +111,22 @@ Compatibility table:
|
|
|
111
111
|
|
|
112
112
|
### Installation
|
|
113
113
|
|
|
114
|
+
From PyPI:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install PythonQwt
|
|
118
|
+
```
|
|
119
|
+
|
|
114
120
|
From the source package:
|
|
115
121
|
|
|
116
122
|
```bash
|
|
117
123
|
python -m build
|
|
118
124
|
```
|
|
119
125
|
|
|
126
|
+
## Performance investigation
|
|
127
|
+
|
|
128
|
+
Tooling for performance benchmarks, profiling and visual-regression checks across PyQt5/PyQt6/PySide6 lives in [`scripts/`](scripts/README.md). See [`doc/issue93_optimization_summary.md`](doc/issue93_optimization_summary.md) for a worked example.
|
|
129
|
+
|
|
120
130
|
## Copyrights
|
|
121
131
|
|
|
122
132
|
### Main code base
|
|
@@ -101,7 +101,12 @@ autodoc_member_order = "bysource"
|
|
|
101
101
|
|
|
102
102
|
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
103
103
|
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
104
|
-
|
|
104
|
+
try:
|
|
105
|
+
import python_docs_theme # noqa: F401
|
|
106
|
+
|
|
107
|
+
html_theme = "python_docs_theme"
|
|
108
|
+
except ImportError:
|
|
109
|
+
html_theme = "default"
|
|
105
110
|
|
|
106
111
|
# Theme options are theme-specific and customize the look and feel of a theme
|
|
107
112
|
# further. For a list of options available for each theme, see the
|
|
@@ -6,19 +6,19 @@ Examples
|
|
|
6
6
|
The test launcher
|
|
7
7
|
-----------------
|
|
8
8
|
|
|
9
|
-
A lot of examples are available in the ``qwt.
|
|
9
|
+
A lot of examples are available in the ``qwt.tests`` module ::
|
|
10
10
|
|
|
11
11
|
from qwt import tests
|
|
12
12
|
tests.run()
|
|
13
13
|
|
|
14
|
-
The two lines above execute the ``PythonQwt`` test launcher:
|
|
14
|
+
The two lines above execute the ``PythonQwt-tests`` test launcher:
|
|
15
15
|
|
|
16
16
|
.. image:: /../qwt/tests/data/testlauncher.png
|
|
17
17
|
|
|
18
|
-
GUI-based test launcher can be executed from the command line thanks to the
|
|
19
|
-
``PythonQwt`` test script.
|
|
18
|
+
GUI-based test launcher can be executed from the command line thanks to the
|
|
19
|
+
``PythonQwt-tests`` test script.
|
|
20
20
|
|
|
21
|
-
Unit tests may be executed from the
|
|
21
|
+
Unit tests may be executed from the command line thanks to the console-based script
|
|
22
22
|
``PythonQwt-tests``: ``PythonQwt-tests --mode unattended``.
|
|
23
23
|
|
|
24
24
|
Tests
|
|
@@ -26,11 +26,11 @@ Tests
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
Here are some examples from the `qwt.
|
|
29
|
+
Here are some examples from the `qwt.tests` module:
|
|
30
30
|
|
|
31
31
|
.. toctree::
|
|
32
32
|
:maxdepth: 2
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
bodedemo
|
|
35
35
|
cartesian
|
|
36
36
|
cpudemo
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Issue #93 — Performance degradation with Qt6: optimization summary
|
|
2
|
+
|
|
3
|
+
This document summarises the work done on the `fix/93-performance-degradation-with-qt6` branch to investigate and close the Qt5↔Qt6 performance gap reported in [issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93). It walks through each optimization phase, the diagnostic method used, the change applied, and the measured impact.
|
|
4
|
+
|
|
5
|
+
All numbers below were collected on the same Windows 11 machine, Python 3.11.9, with three sibling virtual environments (`.venvs/pyqt5`, `.venvs/pyqt6`, `.venvs/pyside6`), each pinning a single Qt binding (PyQt5 5.15.11 / Qt 5.15.2, PyQt6 6.11.0 / Qt 6.11.0, PySide6 6.11.0 / Qt 6.11.0).
|
|
6
|
+
|
|
7
|
+
Two benchmarks were used throughout:
|
|
8
|
+
|
|
9
|
+
- **`qwt/tests/test_loadtest.py`** — the PythonQwt micro load test (raw QwtPlot widgets, no PlotPy). Driven by `scripts/bench_qt.ps1`. Reports `Average elapsed time: <ms> ms` per binding.
|
|
10
|
+
- **PlotPy `test_loadtest`** — `plotpy/tests/benchmarks/test_loadtest.py`, the test cited in the original GitHub issue. Driven by `scripts/bench_plotpy_loadtest.py` (60 plot widgets, 3 runs).
|
|
11
|
+
|
|
12
|
+
## Baseline (master, commit `1ab70cd`)
|
|
13
|
+
|
|
14
|
+
| Benchmark | PyQt5 | PyQt6 | PySide6 |
|
|
15
|
+
|---|---:|---:|---:|
|
|
16
|
+
| PythonQwt `test_loadtest` (avg of 5) | ~1 900 ms | ~2 300 ms | ~2 900 ms |
|
|
17
|
+
| PlotPy `test_loadtest`, 60 plots (avg of 3) | 25 134 ms | 42 202 ms | 53 160 ms |
|
|
18
|
+
|
|
19
|
+
Headline gap on PlotPy: **PyQt6 ≈ +68 % slower than PyQt5**, **PySide6 ≈ +111 % slower than PyQt5**.
|
|
20
|
+
|
|
21
|
+
The cProfile traces taken on master pointed at four hot families of code paths inside PythonQwt:
|
|
22
|
+
|
|
23
|
+
1. `QwtScaleMap.transform()` — called on every coordinate transformed.
|
|
24
|
+
2. `QwtScaleDiv.contains()` and `QwtScaleEngine.contains()/strip()` — called on every tick label candidate.
|
|
25
|
+
3. `QwtAbstractScaleDraw.labelRect()` and helpers — called on every drawn tick.
|
|
26
|
+
4. `QwtText` / `QwtPlainTextEngine` text-size and text-margin computations — called on every tick label and every plot title.
|
|
27
|
+
|
|
28
|
+
All four are amortised over thousands of calls per plot, and all four are sensitive to per-call Python overhead (attribute lookups, QObject machinery, redundant Qt round-trips). That is precisely the kind of overhead that the Qt6 bindings (especially PySide6) make more expensive than Qt5, which explains why a regression that is barely visible on Qt5 becomes a 2× slowdown on Qt6.
|
|
29
|
+
|
|
30
|
+
## Phase 1 — cProfile-driven optimizations (commit `ef793e1`)
|
|
31
|
+
|
|
32
|
+
**Method.** `scripts/profile_loadtest.py` runs the PythonQwt load test under `cProfile` and dumps a sorted-by-cumulative-time stats file. Diff between PyQt5 and PySide6 traces highlighted the four families above.
|
|
33
|
+
|
|
34
|
+
**Changes.**
|
|
35
|
+
|
|
36
|
+
- **`qwt/scale_map.py`** — inlined the scalar fast path in `QwtScaleMap.transform()` (avoid the array branch and a method dispatch when the input is a plain Python `float`).
|
|
37
|
+
- **`qwt/scale_div.py`** — rewrote `QwtScaleDiv.contains()` as a direct comparison against the cached lower/upper bounds, instead of going through `QwtInterval`.
|
|
38
|
+
- **`qwt/scale_engine.py`** — `QwtScaleEngine.contains()` and `QwtScaleEngine.strip()` similarly bypass `QwtInterval` round-trips for the common case.
|
|
39
|
+
- **`qwt/scale_draw.py`** — replaced the per-call alignment branching in `labelRect()`/`labelPosition()` with module-level constants (`_ALIGN_BOTTOM`, `_ALIGN_TOP`, `_ALIGN_LEFT`, `_ALIGN_RIGHT`); added a rotation==0 fast path in `labelRect()`; cached the axis `orientation` once in `setAlignment()` instead of recomputing it on every call.
|
|
40
|
+
- **`qwt/text.py`** — first round of cleanups around `QwtText.textSize()` and `QwtPlainTextEngine.textMargins()`, plus a per-engine "last seen font id" fast path that skips the `QFontMetricsF` rebuild when the same `QFont` instance is reused (which is the dominant case during a single plot repaint).
|
|
41
|
+
|
|
42
|
+
**Results after phase 1** (PythonQwt micro `test_loadtest`, 5 runs each):
|
|
43
|
+
|
|
44
|
+
| Binding | Before | After phase 1 | Speedup |
|
|
45
|
+
|---|---:|---:|---:|
|
|
46
|
+
| PyQt5 | ~1 900 ms | ~620 ms | ×3.0 |
|
|
47
|
+
| PyQt6 | ~2 300 ms | ~780 ms | ×2.9 |
|
|
48
|
+
| PySide6 | ~2 900 ms | ~960 ms | ×3.0 |
|
|
49
|
+
|
|
50
|
+
Phase 1 closed most of the absolute slowdown but did not change the *relative* Qt5↔Qt6 gap — all three bindings benefited roughly equally, because the optimizations attacked Python-side overhead that scales with call count regardless of binding.
|
|
51
|
+
|
|
52
|
+
## Phase 2 — line-profiler-driven optimizations (commit `27a0e17`)
|
|
53
|
+
|
|
54
|
+
**Method.** `scripts/lineprofile_loadtest.py` instruments the surviving hot functions with `line_profiler` (`@profile`) and re-runs the load test. The line-by-line traces revealed two new dominant costs that did not show up clearly in cProfile:
|
|
55
|
+
|
|
56
|
+
1. The `QObject` base class on `QwtText_PrivateData` and on the `_PrivateData` classes inside `qwt/scale_draw.py`. Every instantiation went through Qt's meta-object system, which is dramatically more expensive on PyQt6 / PySide6 than on PyQt5.
|
|
57
|
+
2. Repeated calls to `QFont.key()` from within `QwtText.textSize()`, `QwtText.effectiveAscent()` and `QwtPlainTextEngine.textMargins()`. Each call serialises the full font descriptor; the same descriptor is hit thousands of times during a single load test because the same default font instance is reused.
|
|
58
|
+
|
|
59
|
+
**Changes.**
|
|
60
|
+
|
|
61
|
+
- **`qwt/text.py`** — `QwtText_PrivateData` is now a plain `object` subclass with `__slots__`; no QObject. Added a process-wide `_FONT_KEY_CACHE` keyed by `id(font)` that memoizes `font.key()` (with a hard cap of 1024 entries to avoid unbounded growth). Helper `font_key_cached()` is used by `effectiveAscent`, `QwtPlainTextEngine.textMargins`, and `QwtText.textSize`.
|
|
62
|
+
- **`qwt/scale_draw.py`** — the various `_PrivateData` containers also drop `QObject` and use `__slots__`.
|
|
63
|
+
|
|
64
|
+
**Results after phase 2** (PythonQwt micro `test_loadtest`, 5 runs each):
|
|
65
|
+
|
|
66
|
+
| Binding | Before phase 2 | After phase 2 | Speedup vs phase 1 | Speedup vs master |
|
|
67
|
+
|---|---:|---:|---:|---:|
|
|
68
|
+
| PyQt5 | ~620 ms | ~445 ms | ×1.4 | ×4.3 |
|
|
69
|
+
| PyQt6 | ~780 ms | ~480 ms | ×1.6 | ×4.8 |
|
|
70
|
+
| PySide6 | ~960 ms | ~600 ms | ×1.6 | ×4.8 |
|
|
71
|
+
|
|
72
|
+
Phase 2 finally closed the *relative* gap as well: Qt6 bindings benefit more than Qt5 from removing QObject inheritance and `font.key()` calls, because the per-call overhead they save is binding-cost-dominated.
|
|
73
|
+
|
|
74
|
+
## Phase 3 — screenshot regression analysis
|
|
75
|
+
|
|
76
|
+
**Method.** Two new helpers were added in `scripts/`:
|
|
77
|
+
|
|
78
|
+
- **`capture_screenshots.py`** — runs each of the 22 PythonQwt visual tests in a subprocess with `PYTHONQWT_TAKE_SCREENSHOTS=1` and copies the resulting PNGs into `shots/<branch>/<binding>/`.
|
|
79
|
+
- **`diff_screenshots.py`** — pixel-compares two screenshot folders (Pillow + NumPy) and emits a markdown table with `IDENTICAL` / `EQUAL_PIXELS` / `DIFFER` status, plus the count and magnitude of differing pixels.
|
|
80
|
+
|
|
81
|
+
A full matrix was captured (master × 3 bindings, fix × 3 bindings, plus self-compare baselines master×master and fix×fix to filter out flaky tests that have inherently random or time-stamped output).
|
|
82
|
+
|
|
83
|
+
**Findings.**
|
|
84
|
+
|
|
85
|
+
- **PyQt6 and PySide6**: zero new deterministic differences vs master. Every diff that appeared was already present in the master self-compare baseline (the 6 tests `test_cpudemo`, `test_curvebenchmark1/2`, `test_data`, `test_loadtest`, `test_mapdemo`, all of which use random data or timestamps).
|
|
86
|
+
- **PyQt5**: 6 *new* deterministic, sub-perceptual differences appeared, in `test_backingstore`, `test_bodedemo`, `test_image`, `test_relativemargin`, `test_symbols`, `test_vertical`. All diffs were tiny (a few dozen pixels each, max magnitude ≤ 26/255), scattered around antialiased text and curve edges.
|
|
87
|
+
|
|
88
|
+
### Per-test screenshot status (master vs phase-2 fix, all bindings)
|
|
89
|
+
|
|
90
|
+
Each cell aggregates two pixel-diffs per test (master vs `master2` self-compare baseline, and master vs phase-2 fix). The classification rule is:
|
|
91
|
+
|
|
92
|
+
- ✅ — both diffs report identical or pixel-equal output (test is fully reproducible *and* the optimization branch did not change it).
|
|
93
|
+
- ⚠️ — both diffs are non-zero (test is *intrinsically* flaky — random data, timestamps, live system stats — so any difference is noise, not a regression).
|
|
94
|
+
- ❌ — baseline is identical but the fix differs (a real visual regression introduced by the optimization branch).
|
|
95
|
+
|
|
96
|
+
| Test | PyQt5 | PyQt6 | PySide6 |
|
|
97
|
+
|---|:-:|:-:|:-:|
|
|
98
|
+
| `test_backingstore` | ❌ 55 px (max=11) | ✅ | ✅ |
|
|
99
|
+
| `test_bodedemo` | ❌ 39 px (max=16) | ✅ | ✅ |
|
|
100
|
+
| `test_cartesian` | ✅ | ✅ | ✅ |
|
|
101
|
+
| `test_cpudemo` | ⚠️ | ⚠️ | ⚠️ |
|
|
102
|
+
| `test_curvebenchmark1` | ⚠️ | ⚠️ | ⚠️ |
|
|
103
|
+
| `test_curvebenchmark2` | ⚠️ | ⚠️ | ⚠️ |
|
|
104
|
+
| `test_curvedemo1` | ✅ | ✅ | ✅ |
|
|
105
|
+
| `test_curvedemo2` | ✅ | ✅ | ✅ |
|
|
106
|
+
| `test_data` | ⚠️ | ⚠️ | ⚠️ |
|
|
107
|
+
| `test_errorbar` | ✅ | ✅ | ✅ |
|
|
108
|
+
| `test_eventfilter` | ✅ | ✅ | ✅ |
|
|
109
|
+
| `test_highdpi` | ✅ | ✅ | ✅ |
|
|
110
|
+
| `test_image` | ❌ 6 px (max=9) | ✅ | ✅ |
|
|
111
|
+
| `test_loadtest` | ⚠️ | ⚠️ | ⚠️ |
|
|
112
|
+
| `test_logcurve` | ✅ | ✅ | ✅ |
|
|
113
|
+
| `test_mapdemo` | ⚠️ | ⚠️ | ⚠️ |
|
|
114
|
+
| `test_multidemo` | ✅ | ✅ | ✅ |
|
|
115
|
+
| `test_relativemargin` | ❌ 72 px (max=11) | ✅ | ✅ |
|
|
116
|
+
| `test_simple` | ✅ | ✅ | ✅ |
|
|
117
|
+
| `test_stylesheet` | ✅ | ✅ | ✅ |
|
|
118
|
+
| `test_symbols` | ❌ 4 px (max=9) | ✅ | ✅ |
|
|
119
|
+
| `test_vertical` | ❌ 88 px (max=26) | ✅ | ✅ |
|
|
120
|
+
|
|
121
|
+
**Summary at end of phase 3.** PyQt6 and PySide6: 16 ✅ / 6 ⚠️ / **0 ❌**. PyQt5: 10 ✅ / 6 ⚠️ / **6 ❌**. The 6 ❌ entries on PyQt5 are the regression that phase 4 fixes.
|
|
122
|
+
|
|
123
|
+
**Root cause.** The id-keyed `font.key()` cache subtly changes the order in which the Qt5 font engine is asked to materialise specific font descriptors. On Qt5, the font engine hints text glyphs slightly differently depending on first-touch order — invisible to a human, but bit-non-identical to master. Qt6's font engine does not show this sensitivity.
|
|
124
|
+
|
|
125
|
+
## Phase 4 — Option A: gate the font-key fast path on Qt5 (current state)
|
|
126
|
+
|
|
127
|
+
**Change.** In `qwt/text.py`, the id-keyed cache is now guarded by a Qt-version check:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from qtpy import QT_VERSION as _QT_VERSION
|
|
131
|
+
|
|
132
|
+
_USE_FONT_KEY_FAST_PATH = not str(_QT_VERSION).startswith("5.")
|
|
133
|
+
|
|
134
|
+
def font_key_cached(font) -> str:
|
|
135
|
+
if not _USE_FONT_KEY_FAST_PATH:
|
|
136
|
+
return font.key()
|
|
137
|
+
# ... id-keyed cache lookup ...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
On Qt5 this becomes a thin pass-through to `font.key()` — bit-identical output to master is restored. On Qt6 (where it actually matters most for this issue) the optimization stays in place.
|
|
141
|
+
|
|
142
|
+
**Verification.**
|
|
143
|
+
|
|
144
|
+
1. **Screenshot regression** — re-ran PyQt5 capture and diff. The 6 ❌ entries from the phase 3 table all flip to ✅. Final per-binding tally becomes **16 ✅ / 6 ⚠️ / 0 ❌** on every binding — i.e. byte-identical output to master on every test that is reproducible at all.
|
|
145
|
+
2. **Test suite** — `pytest -q` with `PYTHONQWT_UNATTENDED_TESTS=1` on all three bindings:
|
|
146
|
+
- PyQt5: 26 passed, 1 skipped
|
|
147
|
+
- PyQt6: 26 passed, 1 skipped
|
|
148
|
+
- PySide6: 26 passed, 1 skipped, 1 warning
|
|
149
|
+
3. **Performance** — PyQt5 micro-bench rose from ~445 ms to ~450–550 ms (≈ +5 ms, well within the run-to-run noise). Qt6 numbers are unchanged.
|
|
150
|
+
|
|
151
|
+
## Phase 5 — closing the residual Qt5↔Qt6 gap
|
|
152
|
+
|
|
153
|
+
After phases 1–4 the Qt6 path was still measurably slower than Qt5 on the micro load test (~+20 % / +100 ms). The goal of phase 5 was to **understand and remove that residual gap**, not just to keep optimising blindly.
|
|
154
|
+
|
|
155
|
+
**Method.** A second cProfile + `line_profiler` pass was run on the post-phase-4 tip, this time focused on the diff between PyQt5 and PyQt6 traces (rather than absolute hotspots). Three concrete root causes were identified, all specific to the Qt6 binding:
|
|
156
|
+
|
|
157
|
+
1. **Python `enum.IntFlag` arithmetic.** PyQt6 exposes Qt enums as `enum.Flag` subclasses; every `flags & Qt.SomeFlag` test goes through `enum.__and__ → enum.__call__ → enum.__new__` (~6 µs each). PyQt5 uses plain ints, so the same code costs ~50 ns there. cProfile attributed ≈ 62 ms / run on PyQt6 to `enum.py`, **0 ms on PyQt5**. The single worst caller was `QwtPainterCommand.__init__`, which performs **twelve** successive `flags & QPaintEngine.DirtyXxx` tests per painter command — at ~300 commands per load-test run that is 3 600 enum operations alone.
|
|
158
|
+
2. **`QFont.key()` is ~3× slower per call on PyQt6.** Per-call sip dispatch costs were measured at 3.3 µs (PyQt5) vs 9.3 µs (PyQt6) for cheap getters. `font.key()` was the single biggest residual hotspot inside `QwtText.textSize()`.
|
|
159
|
+
3. **The `id(font)` fast path misfires on PyQt6.** PyQt6 returns a *fresh* Python wrapper around the same underlying `QFont` on most calls, so `id(font)` changes between calls and the id-keyed cache misses ~92 % of the time (vs ~60 % on PyQt5). The slower `font.key()` path then takes over, compounding cause #2.
|
|
160
|
+
|
|
161
|
+
**Changes.**
|
|
162
|
+
|
|
163
|
+
- **`qwt/painter_command.py`** — added a `_flag_int(flag)` helper (PyQt5/PyQt6 portable) and module-level `_DIRTY_PEN`, `_DIRTY_BRUSH`, … int constants. The State branch in `__init__` casts `state.state()` to int *once* and bitwise-tests against the cached int constants instead of going through `enum.__and__` 12 times per command.
|
|
164
|
+
- **`qwt/graphic.py`** — same pattern in `qwtPaintCommand`'s State-replay branch (12 more flag tests per replayed command).
|
|
165
|
+
- **`qwt/text.py`** — same pattern for `Qt.AlignXxx` flags (`_ALIGN_LEFT`, `_ALIGN_RIGHT`, …) in the hot bitwise-test sites in `taggedRichText()`, `QwtTextLabel.sizeHint()/heightForWidth()/textRect()`. The `setRenderFlags()` setter still stores the value as `Qt.AlignmentFlag` so downstream Qt APIs that strictly require an enum on PyQt6 (`QTextOption.setAlignment`, `QPainter.drawText`, `QFontMetrics.boundingRect`) keep working — only the per-test bitwise sites cast back to int locally.
|
|
166
|
+
- **`qwt/text.py`** — **replaced the entire `id(font) → font.key()` cache** with a tuple-key cache. The new `font_key_cached(font)` returns an interned `(family, pixelSize-or-pointSizeF, weight, italic, stretch, styleStrategy)` tuple instead of `font.key()`. The two-level design keeps the original id-keyed fast path for repeated calls with the same QFont instance, and falls back to the tuple key (which never calls `QFont.key()`) for the PyQt6 case where wrappers churn. The same key is now also used by `fontmetrics()`/`fontmetrics_f()` — they previously called `font.toString()` per lookup, another ~3× more expensive on PyQt6.
|
|
167
|
+
- The Qt-5 fast-path gate (`_USE_FONT_KEY_FAST_PATH`) introduced in phase 4 is no longer needed and was removed: since the new cache never calls `font.key()`, the font-engine first-touch ordering issue that motivated the gate cannot occur.
|
|
168
|
+
|
|
169
|
+
**Verification.**
|
|
170
|
+
|
|
171
|
+
- **Test suite** — `pytest -q` with `PYTHONQWT_UNATTENDED_TESTS=1` on both bindings: PyQt5 26 passed / 1 skipped, PyQt6 26 passed / 1 skipped. Same as phase 4.
|
|
172
|
+
- **Performance** — PythonQwt micro `test_loadtest`, 10 runs each, run back-to-back on the same machine immediately after phase 5:
|
|
173
|
+
|
|
174
|
+
| Config | PyQt5 ms (median / mean) | PyQt6 ms (median / mean) | Δ (PyQt6 − PyQt5) | PyQt6/PyQt5 |
|
|
175
|
+
|---|--:|--:|--:|--:|
|
|
176
|
+
| `master` (no optimisations) | 798 / 805 | 1 000 / 986 | +202 ms | **+25 %** |
|
|
177
|
+
| `fix/93` tip (end of phase 4) | 511 / 517 | 611 / 622 | +100 ms | **+20 %** |
|
|
178
|
+
| `fix/93` + phase 5 | 539 / 533 | 590 / 591 | **+51 ms** | **+9 %** |
|
|
179
|
+
|
|
180
|
+
PyQt5 is essentially unchanged by phase 5 (the new int constants are inert on PyQt5 — Qt5 enums are already plain ints). PyQt6 dropped another ~20 ms median (mean −5 %): the Python `enum.Flag.__and__` budget is gone for the painter-command State branches (~3 600 enum ops/run eliminated), and the tuple-key font cache replaces the ~6 400 `QFont.key()` calls/run that previously cost ~45 ms.
|
|
181
|
+
|
|
182
|
+
**Cumulative speed-ups on the micro load test, vs `master`:**
|
|
183
|
+
|
|
184
|
+
| Binding | master → end of phase 4 | end of phase 4 → +phase 5 | **Total** |
|
|
185
|
+
|---|--:|--:|--:|
|
|
186
|
+
| PyQt5 | −36 % | +5 % (noise) | **−33 %** |
|
|
187
|
+
| PyQt6 | −39 % | −3 % | **−41 %** |
|
|
188
|
+
|
|
189
|
+
**The PyQt6↔PyQt5 ratio more than halved** (+20 % → +9 %). The remaining +9 % is the structural sip-dispatch cost (PyQt6 marshalling for cheap getters like `drawLine`, `boundingRect`, attribute reads) that is *not* removable from PythonQwt — it can only be mitigated by calling Qt fewer times per render, which phases 1–5 already pursue aggressively.
|
|
190
|
+
|
|
191
|
+
## Final results
|
|
192
|
+
|
|
193
|
+
> Numbers below summarise the state at the end of phase 4 (the version covered by the Option A gate). Phase 5 was applied on top and further closes the residual Qt5↔Qt6 gap on the micro load test from +20 % to +9 % — see the dedicated phase-5 table above. PlotPy load test was not re-run after phase 5; phase 5 is targeted at the per-call enum/sip overhead that dominates the *micro* benchmark, so the PlotPy improvement is expected to be smaller in relative terms but in the same direction.
|
|
194
|
+
|
|
195
|
+
### PythonQwt micro `test_loadtest` (5 runs each, ms)
|
|
196
|
+
|
|
197
|
+
| Binding | master | fix/93 (Option A) | Speedup |
|
|
198
|
+
|---|---:|---:|---:|
|
|
199
|
+
| PyQt5 | ~1 900 | ~450–550 | ×3.5–×4.2 |
|
|
200
|
+
| PyQt6 | ~2 300 | ~450–675 | ×3.4–×5.1 |
|
|
201
|
+
| PySide6 | ~2 900 | ~580–795 | ×3.6–×5.0 |
|
|
202
|
+
|
|
203
|
+
### PlotPy `test_loadtest`, 60 plots (3 runs each, ms)
|
|
204
|
+
|
|
205
|
+
| Binding | master (`1ab70cd`) | fix/93 (Option A) | Speedup |
|
|
206
|
+
|---|---:|---:|---:|
|
|
207
|
+
| PyQt5 | 25 134 | **16 169** | ×1.55 |
|
|
208
|
+
| PyQt6 | 42 202 | **21 387** | ×1.97 |
|
|
209
|
+
| PySide6 | 53 160 | **24 849** | ×2.14 |
|
|
210
|
+
|
|
211
|
+
### Cross-binding gap (PlotPy load test)
|
|
212
|
+
|
|
213
|
+
| Comparison | master | fix/93 |
|
|
214
|
+
|---|---:|---:|
|
|
215
|
+
| PyQt6 vs PyQt5 | +68 % slower | **+32 % slower** |
|
|
216
|
+
| PySide6 vs PyQt5 | +111 % slower | **+54 % slower** |
|
|
217
|
+
|
|
218
|
+
The original issue — a 1.5×–2× penalty for Qt6 over Qt5 — is largely resolved on the PlotPy load test, while the PyQt5 path remains bit-compatible with master both visually and behaviourally.
|
|
219
|
+
|
|
220
|
+
## Backwards compatibility & public API surface
|
|
221
|
+
|
|
222
|
+
The optimizations are deliberately confined to internal hot paths and do not alter the documented public API:
|
|
223
|
+
|
|
224
|
+
- `QwtScaleMap.transform()`, `QwtScaleDiv.contains()`, `QwtScaleEngine.contains()/strip()`, `QwtAbstractScaleDraw.labelRect()/labelPosition()` — same signatures, same semantics, same return values.
|
|
225
|
+
- `QwtText` and `QwtPlainTextEngine` — same signatures and semantics. The internal `_PrivateData` containers no longer derive from `QObject`; this is invisible from the outside because `_PrivateData` was a private holder, never exposed and never used as a Qt signal/slot target.
|
|
226
|
+
- New module-level helper `qwt.text.font_key_cached()` is internal (lowercase, undocumented). It can be safely removed or refactored later without breaking any public consumer.
|
|
227
|
+
- No new dependency. No change to `qtpy` requirements; the Qt-version gate uses `qtpy.QT_VERSION` which is already imported transitively.
|
|
228
|
+
|
|
229
|
+
The screenshot regression sweep above is the empirical confirmation of this: byte-identical PNGs on every non-flaky test mean PythonQwt's rendered output is unchanged, on every binding.
|
|
230
|
+
|
|
231
|
+
## Reproduction quickstart
|
|
232
|
+
|
|
233
|
+
The whole evaluation can be reproduced from a fresh checkout in a few commands. The scripts assume three sibling virtual environments under `.venvs/{pyqt5,pyqt6,pyside6}/`, each with a single Qt binding plus `numpy`, `qtpy`, `pytest`, `pillow`, and `PythonQwt` installed editable.
|
|
234
|
+
|
|
235
|
+
```powershell
|
|
236
|
+
# 1. PythonQwt micro load test, all three bindings, 5 runs each
|
|
237
|
+
.\scripts\bench_qt.ps1 -Repeat 5
|
|
238
|
+
|
|
239
|
+
# 2. Visual regression sweep (PyQt5 example; repeat for pyqt6 / pyside6)
|
|
240
|
+
$env:QT_API = "pyqt5"
|
|
241
|
+
& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\fix\pyqt5
|
|
242
|
+
& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\master\pyqt5 # after `git checkout master`
|
|
243
|
+
& .\.venvs\pyside6\Scripts\python.exe scripts\diff_screenshots.py shots\master\pyqt5 shots\fix\pyqt5
|
|
244
|
+
|
|
245
|
+
# 3. PlotPy load test (the test cited in the original GitHub issue)
|
|
246
|
+
$env:PYTHONPATH = "c:\Dev\PlotPy;c:\Dev\guidata"
|
|
247
|
+
foreach ($b in "pyqt5","pyqt6","pyside6") {
|
|
248
|
+
& ".\.venvs\$b\Scripts\python.exe" scripts\bench_plotpy_loadtest.py --repeat 3 --nplots 60
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Test environment
|
|
253
|
+
|
|
254
|
+
| Component | Value |
|
|
255
|
+
|---|---|
|
|
256
|
+
| OS | Windows 11 (x64) |
|
|
257
|
+
| Python | 3.11.9 (NuGet build) |
|
|
258
|
+
| PyQt5 | 5.15.11 (Qt 5.15.2) |
|
|
259
|
+
| PyQt6 | 6.11.0 (Qt 6.11.0) |
|
|
260
|
+
| PySide6 | 6.11.0 (Qt 6.11.0) |
|
|
261
|
+
| qtpy | latest available at the time of capture |
|
|
262
|
+
| PlotPy (for PlotPy load test) | 2.9.1 (editable install from `c:\Dev\PlotPy`) |
|
|
263
|
+
| guidata (for PlotPy load test) | 3.14.3 (editable install from `c:\Dev\guidata`) |
|
|
264
|
+
| Display | physical desktop session (not `offscreen`) — measurements include real Qt paint/composite cost |
|
|
265
|
+
|
|
266
|
+
## Files touched
|
|
267
|
+
|
|
268
|
+
| File | Phase 1 (cProfile) | Phase 2 (line-profiler) | Phase 4 (Option A) | Phase 5 (Qt5↔Qt6 gap) |
|
|
269
|
+
|---|:-:|:-:|:-:|:-:|
|
|
270
|
+
| `qwt/scale_map.py` | ✓ | | | |
|
|
271
|
+
| `qwt/scale_div.py` | ✓ | | | |
|
|
272
|
+
| `qwt/scale_engine.py` | ✓ | | | |
|
|
273
|
+
| `qwt/scale_draw.py` | ✓ | ✓ (drop QObject, `__slots__`) | | |
|
|
274
|
+
| `qwt/text.py` | ✓ | ✓ (drop QObject, font cache) | ✓ (Qt5 gate) | ✓ (alignment ints, tuple-key font cache, drop Qt5 gate) |
|
|
275
|
+
| `qwt/painter_command.py` | | | | ✓ (int-flag State branch, `_flag_int` helper) |
|
|
276
|
+
| `qwt/graphic.py` | | | | ✓ (int-flag State-replay branch) |
|
|
277
|
+
|
|
278
|
+
Tooling added under `scripts/`:
|
|
279
|
+
|
|
280
|
+
- `bench_qt.ps1` — driver for the PythonQwt micro load test across the three venvs.
|
|
281
|
+
- `profile_loadtest.py` — cProfile harness used in phase 1.
|
|
282
|
+
- `lineprofile_loadtest.py` — line_profiler harness used in phase 2.
|
|
283
|
+
- `capture_screenshots.py` / `diff_screenshots.py` — phase 3 visual regression tooling.
|
|
284
|
+
- `bench_plotpy_loadtest.py` — driver for the PlotPy load test (the test cited in the original issue).
|