PythonQwt 0.15.0__tar.gz → 0.16.0__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.
Files changed (132) hide show
  1. {pythonqwt-0.15.0/PythonQwt.egg-info → pythonqwt-0.16.0}/PKG-INFO +22 -10
  2. {pythonqwt-0.15.0 → pythonqwt-0.16.0/PythonQwt.egg-info}/PKG-INFO +22 -10
  3. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt.egg-info/SOURCES.txt +1 -1
  4. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt.egg-info/requires.txt +3 -1
  5. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/README.md +16 -6
  6. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/conf.py +6 -1
  7. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/index.rst +7 -7
  8. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/installation.rst +4 -0
  9. pythonqwt-0.16.0/doc/issue93_optimization_summary.md +284 -0
  10. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/overview.rst +6 -6
  11. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/pyproject.toml +4 -4
  12. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/__init__.py +3 -3
  13. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/graphic.py +30 -13
  14. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/null_paintdevice.py +5 -1
  15. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/painter_command.py +49 -12
  16. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_canvas.py +1 -1
  17. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/scale_div.py +5 -3
  18. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/scale_draw.py +103 -41
  19. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/scale_engine.py +17 -10
  20. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/scale_map.py +19 -10
  21. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_bodedemo.py +1 -2
  22. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_relativemargin.py +2 -2
  23. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/text.py +170 -39
  24. pythonqwt-0.15.0/doc/requirements.txt +0 -4
  25. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/LICENSE +0 -0
  26. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/MANIFEST.in +0 -0
  27. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt-tests.desktop +0 -0
  28. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt.egg-info/dependency_links.txt +0 -0
  29. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt.egg-info/entry_points.txt +0 -0
  30. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/PythonQwt.egg-info/top_level.txt +0 -0
  31. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/_static/PythonQwt_logo.png +0 -0
  32. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/_static/QwtPlot_example.png +0 -0
  33. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/_static/panorama.png +0 -0
  34. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/_static/symbol_path_example.png +0 -0
  35. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/bodedemo.rst +0 -0
  36. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/cartesian.rst +0 -0
  37. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/cpudemo.rst +0 -0
  38. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/curvebenchmark1.rst +0 -0
  39. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/curvebenchmark2.rst +0 -0
  40. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/curvedemo1.rst +0 -0
  41. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/curvedemo2.rst +0 -0
  42. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/data.rst +0 -0
  43. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/errorbar.rst +0 -0
  44. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/eventfilter.rst +0 -0
  45. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/image.rst +0 -0
  46. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/logcurve.rst +0 -0
  47. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/mapdemo.rst +0 -0
  48. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/multidemo.rst +0 -0
  49. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/simple.rst +0 -0
  50. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/examples/vertical.rst +0 -0
  51. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/index.rst +0 -0
  52. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/plot_example.py +0 -0
  53. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/graphic.rst +0 -0
  54. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/index.rst +0 -0
  55. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/interval.rst +0 -0
  56. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/plot.rst +0 -0
  57. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/plot_directpainter.rst +0 -0
  58. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/plot_layout.rst +0 -0
  59. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/plot_series.rst +0 -0
  60. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/scale.rst +0 -0
  61. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/symbol.rst +0 -0
  62. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/text.rst +0 -0
  63. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/toqimage.rst +0 -0
  64. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/reference/transform.rst +0 -0
  65. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/doc/symbol_path_example.py +0 -0
  66. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/_math.py +0 -0
  67. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/color_map.py +0 -0
  68. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/column_symbol.py +0 -0
  69. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/dyngrid_layout.py +0 -0
  70. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/interval.py +0 -0
  71. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/legend.py +0 -0
  72. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/painter.py +0 -0
  73. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot.py +0 -0
  74. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_curve.py +0 -0
  75. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_directpainter.py +0 -0
  76. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_grid.py +0 -0
  77. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_layout.py +0 -0
  78. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_marker.py +0 -0
  79. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_renderer.py +0 -0
  80. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/plot_series.py +0 -0
  81. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/qthelpers.py +0 -0
  82. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/scale_widget.py +0 -0
  83. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/symbol.py +0 -0
  84. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/__init__.py +0 -0
  85. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/comparative_benchmarks.py +0 -0
  86. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/conftest.py +0 -0
  87. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/PythonQwt.svg +0 -0
  88. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/bodedemo.png +0 -0
  89. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/cartesian.png +0 -0
  90. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/cpudemo.png +0 -0
  91. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/curvebenchmark1.png +0 -0
  92. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/curvebenchmark2.png +0 -0
  93. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/curvedemo1.png +0 -0
  94. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/curvedemo2.png +0 -0
  95. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/data.png +0 -0
  96. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/errorbar.png +0 -0
  97. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/eventfilter.png +0 -0
  98. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/image.png +0 -0
  99. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/loadtest.png +0 -0
  100. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/logcurve.png +0 -0
  101. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/mapdemo.png +0 -0
  102. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/multidemo.png +0 -0
  103. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/simple.png +0 -0
  104. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/stylesheet.png +0 -0
  105. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/symbol.svg +0 -0
  106. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/symbols.png +0 -0
  107. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/testlauncher.png +0 -0
  108. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/data/vertical.png +0 -0
  109. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_backingstore.py +0 -0
  110. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_cartesian.py +0 -0
  111. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_cpudemo.py +0 -0
  112. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_curvebenchmark1.py +0 -0
  113. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_curvebenchmark2.py +0 -0
  114. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_curvedemo1.py +0 -0
  115. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_curvedemo2.py +0 -0
  116. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_data.py +0 -0
  117. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_errorbar.py +0 -0
  118. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_eventfilter.py +0 -0
  119. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_highdpi.py +0 -0
  120. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_image.py +0 -0
  121. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_loadtest.py +0 -0
  122. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_logcurve.py +0 -0
  123. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_mapdemo.py +0 -0
  124. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_multidemo.py +0 -0
  125. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_simple.py +0 -0
  126. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_stylesheet.py +0 -0
  127. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_symbols.py +0 -0
  128. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/test_vertical.py +0 -0
  129. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/tests/utils.py +0 -0
  130. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/toqimage.py +0 -0
  131. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/qwt/transform.py +0 -0
  132. {pythonqwt-0.15.0 → pythonqwt-0.16.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonQwt
3
- Version: 0.15.0
3
+ Version: 0.16.0
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.19
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 `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
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 `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
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.16 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
803
+ | 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
802
804
  | Latest | ✅ | ✅ | ❌ | ✅ |
803
805
 
804
806
  ### Requirements
805
807
 
806
808
  - Python >=3.9
807
- - QtPy >= 1.3 (and a Python-to-Qt binding library, see above)
808
- - NumPy >= 1.5
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.15.0
3
+ Version: 0.16.0
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.19
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 `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
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 `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
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.16 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
803
+ | 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
802
804
  | Latest | ✅ | ✅ | ❌ | ✅ |
803
805
 
804
806
  ### Requirements
805
807
 
806
808
  - Python >=3.9
807
- - QtPy >= 1.3 (and a Python-to-Qt binding library, see above)
808
- - NumPy >= 1.5
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
@@ -1,10 +1,12 @@
1
- NumPy>=1.19
1
+ NumPy>=1.21
2
2
  QtPy>=1.9
3
3
 
4
4
  [dev]
5
+ build
5
6
  ruff
6
7
  pylint
7
8
  Coverage
9
+ pre-commit
8
10
 
9
11
  [doc]
10
12
  PyQt5
@@ -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 `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
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 `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
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.16 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
98
+ | 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
99
99
  | Latest | ✅ | ✅ | ❌ | ✅ |
100
100
 
101
101
  ### Requirements
102
102
 
103
103
  - Python >=3.9
104
- - QtPy >= 1.3 (and a Python-to-Qt binding library, see above)
105
- - NumPy >= 1.5
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
- html_theme = "default"
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.test`` module ::
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 commande line thanks to the console-based script
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.test` module:
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
@@ -15,6 +15,10 @@ Requirements:
15
15
  Installation
16
16
  ------------
17
17
 
18
+ From PyPI:
19
+
20
+ `pip install PythonQwt`
21
+
18
22
  From the source package:
19
23
 
20
24
  `python -m build`
@@ -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).