PythonQwt 0.15.0__py3-none-any.whl → 0.16.1__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonQwt
3
- Version: 0.15.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.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,17 +1,17 @@
1
- pythonqwt-0.15.0.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
- qwt/__init__.py,sha256=IYGoZiDuvezTXga1nUybeNvDi9I-ntKYbQc0qwRIFpc,5993
1
+ pythonqwt-0.16.1.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
+ qwt/__init__.py,sha256=Aux0XzsSsxiS6O8LFosjbqk7X0rIU4kE3Si0a9jGBaE,5990
3
3
  qwt/_math.py,sha256=fNcPJcaK-ldCCFB20T2N58LEkQ1lwZY1-q5veXXhwl8,1501
4
4
  qwt/color_map.py,sha256=fLRpymqOXtLrhuPrbbCxTvNINbr2GH7pYBG4fZidrcs,11812
5
5
  qwt/column_symbol.py,sha256=0aFyQ05ryTKbaSTctkhtXa0pB_tWG444zlH3991_bt0,5760
6
6
  qwt/dyngrid_layout.py,sha256=wgff6mZ1okNhQRRbwaw1I__vOXqGdUG0uafkShuns08,13477
7
- qwt/graphic.py,sha256=lISwinyParPXoJCL8D_9x9Q0JyJVeykrTWSm1Ubluzo,28177
7
+ qwt/graphic.py,sha256=rSxvgIAm9ctcyszvZrN_03KpBhKSHabbPYdcrztSta8,28962
8
8
  qwt/interval.py,sha256=PMrCQxV9NXGk2JG0MNjJ3zGTNTSSRfa_noxbpm-Okns,12287
9
9
  qwt/legend.py,sha256=GBUim67HWgwLat5tOWDpK98fWcFxV-RkxWjoyWd9Gao,31313
10
- qwt/null_paintdevice.py,sha256=AAVyQCVgZKyIw7i-J9ElB0_-8KRV9OXDr140TwMxLSs,9144
10
+ qwt/null_paintdevice.py,sha256=1pgbcvyRTVN32RGzuPzUOm0iUImjvPQ4l5OfJVyA3hg,9375
11
11
  qwt/painter.py,sha256=k345puF4MAoTye0WBveiTNpCNWXx1K2o62_I0PZYlIA,16536
12
- qwt/painter_command.py,sha256=b_uZb56YlzYFG3ZmB5ykjkBQQAadQD8epknsQSEai54,6957
12
+ qwt/painter_command.py,sha256=7D3goNsX_2Ufr5kCpC3Cr1t8GoD5Yz0G1oIZJKvoGBY,8501
13
13
  qwt/plot.py,sha256=d8rf4F-fpQgdpTTqNKQFJh7B0FlQ4WlquooeJQRn_eo,75660
14
- qwt/plot_canvas.py,sha256=PiIRrdZsK1YcV5cQkIpz6H5H6A-dTr2HUJ5MHvDHDG0,30312
14
+ qwt/plot_canvas.py,sha256=INRcxB17dvyGI7Jqi2FX4gm16EG4gzJKULj9inHkibA,30309
15
15
  qwt/plot_curve.py,sha256=ThlfxUjB4yiP_Dh96P7PXusyqUfDWcRcyt31OHvu2vk,37059
16
16
  qwt/plot_directpainter.py,sha256=JCWNhCC21L1A1HZnxd-C95QNlrVJbLztmGab-X8gCGc,10165
17
17
  qwt/plot_grid.py,sha256=h-cDTzibEzQyX2g25SBg5ysyXwmONmMFy9NcuzFySYc,16607
@@ -20,20 +20,20 @@ qwt/plot_marker.py,sha256=6vDHdMsyGClMruWLA4lChUCpoLzmxGNkvmICm0KjP6I,21284
20
20
  qwt/plot_renderer.py,sha256=v-fSwicMGQBa_kQx4hJJptlVNpNsKbgWk9jjqG-r_1g,27201
21
21
  qwt/plot_series.py,sha256=Wa_Wunu-t_L8vl95RJq6__k-XA8UV2YNIsc8yNGpTT8,10654
22
22
  qwt/qthelpers.py,sha256=GpvLr96Ip8SIGwyFfs0f7ocHyKPB7PEjHBQyrpw84xw,1405
23
- qwt/scale_div.py,sha256=2Ay5mgduXgSm4OrDMcaEWIzenAiBuEASZRoX99PWEE4,9276
24
- qwt/scale_draw.py,sha256=P9WGlfcldgpaHRq6hsrRgnFgtYP7JgTgybVCMbZLFic,38564
25
- qwt/scale_engine.py,sha256=yEjcSBWbpdUKCqXBxJccsyz2ZFcR278_QxfgX_fVVCY,36365
26
- qwt/scale_map.py,sha256=WobkNna6_RPXYZjGK7gZLykNxN_kYnrcB6fEaWgnL2w,9182
23
+ qwt/scale_div.py,sha256=_iI5cy4s8H6wigE8xsn7W44T2-DqVADqdjQ31uJPa_I,9266
24
+ qwt/scale_draw.py,sha256=KYal9YOpvHjFWGkBFPVVNq_64cxM1vsHg_f_n-2nKNU,40547
25
+ qwt/scale_engine.py,sha256=7KQB5k-JXuJMsEMQXBJBNppCN-rTN97UKQCYXdazvqQ,36674
26
+ qwt/scale_map.py,sha256=rHmPv7e9voEFuAIInQJq60mTtIGrLMViNlgJYu20znA,9605
27
27
  qwt/scale_widget.py,sha256=p202DrRgMraGbVcubzkatvVXOLbO9CPho729hz6gqa0,27125
28
28
  qwt/symbol.py,sha256=DIXtIOgniO70pScKF-H_mee8GgBuycW3YcsopvfCRGY,39147
29
- qwt/text.py,sha256=vLegaksF4ZqnWIf_HidMbVRgu-eXPX8CwM1ykyNt6OE,42467
29
+ qwt/text.py,sha256=DrrWbEzYA65kcU8aBcKu5YpQ2LzPnOmAO3R7jF8s0J8,48058
30
30
  qwt/toqimage.py,sha256=H2u_qDt32n7HGZakWvs3Ve0G3u04Uk2ZQYp8MuZkkmU,1723
31
31
  qwt/transform.py,sha256=bFkdmI2wDjOJrUZU6E9-sAvwoo_umXycxq6LPvD6N_c,6078
32
32
  qwt/tests/__init__.py,sha256=diBkA_H_Xuxb84tNelsNU4djTlUnKJ5HZNZr02EwnjE,1102
33
33
  qwt/tests/comparative_benchmarks.py,sha256=hlLFkyWzadbUyUX2vka_l2bdcGe8U9sOKBDny3o3U40,1821
34
34
  qwt/tests/conftest.py,sha256=LwqVA58_Cv7Fitpcn9bGdhbWDBwID_3gaZg2LkJQtGQ,2196
35
35
  qwt/tests/test_backingstore.py,sha256=AIiRPqcNPY1riXSQXU43tqB2_z02PhxmK1zA4OxQixM,524
36
- qwt/tests/test_bodedemo.py,sha256=bOIApjBQA1J4aZkVBnzByiKqhqEcmQpag1TvIfsuHMs,9386
36
+ qwt/tests/test_bodedemo.py,sha256=A9iZ4FmTmn96YAwxeIQIPBwIZtj4Jk_ftB78bJAIV_c,9367
37
37
  qwt/tests/test_cartesian.py,sha256=0VCrvclqbQJSEGhWpZLq6brA9Qm9oRIgluEBbxOTOds,3855
38
38
  qwt/tests/test_cpudemo.py,sha256=1XECvuJwOzMjI5JWCBUwvnQ6dCkcDEqZlhQ7KU8N-Rs,12853
39
39
  qwt/tests/test_curvebenchmark1.py,sha256=a53CvA149Ng1Vj4oruKo4Gp952-0A5gLXvRPOiNSYQk,5891
@@ -49,7 +49,7 @@ qwt/tests/test_loadtest.py,sha256=ZTHatcGAbLyvS0BpbOWbzPD2Bjz4rckF7oxZsUVQ5KM,18
49
49
  qwt/tests/test_logcurve.py,sha256=XOtR0asSqFfzc0VL0qfXN0fXNvxvLnz5fxmNZ2gzQWk,1129
50
50
  qwt/tests/test_mapdemo.py,sha256=e7rvB8IPKYVVii8UgdHSbOHLc3i7eB21EQNKZ151Kc8,3423
51
51
  qwt/tests/test_multidemo.py,sha256=YS-P08qcakD41OJy6ZI53ofKOnUvD-2_uum0yOLiIRc,2530
52
- qwt/tests/test_relativemargin.py,sha256=E2PqK4e8Ml-NZGuhslBlCsIPEcFm08nLwsn0jZT_h70,2093
52
+ qwt/tests/test_relativemargin.py,sha256=Shn0aE7UeETSSaQFpt4DrMOFeMN3p4rmEHKqPhXfQTI,2097
53
53
  qwt/tests/test_simple.py,sha256=4opQp_-RkcVdpRyYSh89ddBkeXCPrppB-_2KTiu5vjI,2347
54
54
  qwt/tests/test_stylesheet.py,sha256=cuL1iY4pp5ZoMX4OI86amJA6R5SzQsH1JUWLtEiitZ4,950
55
55
  qwt/tests/test_symbols.py,sha256=YcGM0_Em8QtcbqpLSU98PxvGo7BIIYviP2o_4Fg2LbI,5607
@@ -77,8 +77,8 @@ qwt/tests/data/symbol.svg,sha256=ONkkohVqpg3OO2-XEZge7UZLqqNyOQXbSpLgHXcTBFU,127
77
77
  qwt/tests/data/symbols.png,sha256=wiqRBc2MJluJ-am9LtZ0K9DHV9BnreYMiEvfzJUFzCk,55586
78
78
  qwt/tests/data/testlauncher.png,sha256=WmucObp30QYYKsVRBs5-6t1aEN0FbIWRmOVFN1gt-vQ,139676
79
79
  qwt/tests/data/vertical.png,sha256=EvyG6q6rZm6pTT08Y_nzdBYCnqmoX1QDuEQtp2pyGlw,34076
80
- pythonqwt-0.15.0.dist-info/METADATA,sha256=QWFqtT14JgbY-aEXt3CoS8Jj0iKN0ZNSpdC8wE4GgZE,45097
81
- pythonqwt-0.15.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- pythonqwt-0.15.0.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
83
- pythonqwt-0.15.0.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
84
- pythonqwt-0.15.0.dist-info/RECORD,,
80
+ pythonqwt-0.16.1.dist-info/METADATA,sha256=bE1-gjZ7myzMI-HDjjIwU-zE6mBgkDAS74jYVfPUwqA,45515
81
+ pythonqwt-0.16.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
82
+ pythonqwt-0.16.1.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
83
+ pythonqwt-0.16.1.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
84
+ pythonqwt-0.16.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
qwt/__init__.py CHANGED
@@ -22,8 +22,8 @@ External resources:
22
22
  * Project page on GitHub: `GitHubPage`_
23
23
  * Bug reports and feature requests: `GitHub`_
24
24
 
25
- .. _PyPI: https://pypi.python.org/pypi/PythonQwt
26
- .. _GitHubPage: http://pierreraybaut.github.io/PythonQwt
25
+ .. _PyPI: https://pypi.org/project/PythonQwt/
26
+ .. _GitHubPage: https://github.com/PlotPyStack/PythonQwt
27
27
  .. _GitHub: https://github.com/PlotPyStack/PythonQwt
28
28
  """
29
29
 
@@ -63,7 +63,7 @@ from qwt.symbol import QwtSymbol as QSbl # see deprecated section
63
63
  from qwt.text import QwtText # noqa: F401
64
64
  from qwt.toqimage import array_to_qimage as toQImage # noqa: F401
65
65
 
66
- __version__ = "0.15.0"
66
+ __version__ = "0.16.1"
67
67
  QWT_VERSION_STR = "6.1.5"
68
68
 
69
69
 
qwt/graphic.py CHANGED
@@ -26,7 +26,23 @@ from qtpy.QtGui import (
26
26
  )
27
27
 
28
28
  from qwt.null_paintdevice import QwtNullPaintDevice
29
- from qwt.painter_command import QwtPainterCommand
29
+ from qwt.painter_command import QwtPainterCommand, _flag_int
30
+
31
+ # See painter_command.py for the rationale: cache the QPaintEngine.DirtyXxx
32
+ # flags as plain ints so the State-replay branch below does plain int bitwise
33
+ # tests instead of going through Python's enum.Flag.__and__ on PyQt6.
34
+ _DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen)
35
+ _DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush)
36
+ _DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin)
37
+ _DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont)
38
+ _DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground)
39
+ _DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform)
40
+ _DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled)
41
+ _DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion)
42
+ _DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath)
43
+ _DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints)
44
+ _DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode)
45
+ _DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity)
30
46
 
31
47
 
32
48
  def qwtHasScalablePen(painter):
@@ -83,35 +99,36 @@ def qwtExecCommand(painter, cmd, renderHints, transform, initialTransform):
83
99
  painter.drawImage(data.rect, data.image, data.subRect, data.flags)
84
100
  elif cmd.type() == QwtPainterCommand.State:
85
101
  data = cmd.stateData()
86
- if data.flags & QPaintEngine.DirtyPen:
102
+ flags = _flag_int(data.flags)
103
+ if flags & _DIRTY_PEN:
87
104
  painter.setPen(data.pen)
88
- if data.flags & QPaintEngine.DirtyBrush:
105
+ if flags & _DIRTY_BRUSH:
89
106
  painter.setBrush(data.brush)
90
- if data.flags & QPaintEngine.DirtyBrushOrigin:
107
+ if flags & _DIRTY_BRUSH_ORIGIN:
91
108
  painter.setBrushOrigin(data.brushOrigin)
92
- if data.flags & QPaintEngine.DirtyFont:
109
+ if flags & _DIRTY_FONT:
93
110
  painter.setFont(data.font)
94
- if data.flags & QPaintEngine.DirtyBackground:
111
+ if flags & _DIRTY_BACKGROUND:
95
112
  painter.setBackgroundMode(data.backgroundMode)
96
113
  painter.setBackground(data.backgroundBrush)
97
- if data.flags & QPaintEngine.DirtyTransform:
114
+ if flags & _DIRTY_TRANSFORM:
98
115
  painter.setTransform(data.transform)
99
- if data.flags & QPaintEngine.DirtyClipEnabled:
116
+ if flags & _DIRTY_CLIP_ENABLED:
100
117
  painter.setClipping(data.isClipEnabled)
101
- if data.flags & QPaintEngine.DirtyClipRegion:
118
+ if flags & _DIRTY_CLIP_REGION:
102
119
  painter.setClipRegion(data.clipRegion, data.clipOperation)
103
- if data.flags & QPaintEngine.DirtyClipPath:
120
+ if flags & _DIRTY_CLIP_PATH:
104
121
  painter.setClipPath(data.clipPath, data.clipOperation)
105
- if data.flags & QPaintEngine.DirtyHints:
122
+ if flags & _DIRTY_HINTS:
106
123
  for hint in (
107
124
  QPainter.Antialiasing,
108
125
  QPainter.TextAntialiasing,
109
126
  QPainter.SmoothPixmapTransform,
110
127
  ):
111
128
  painter.setRenderHint(hint, bool(data.renderHints & hint))
112
- if data.flags & QPaintEngine.DirtyCompositionMode:
129
+ if flags & _DIRTY_COMPOSITION_MODE:
113
130
  painter.setCompositionMode(data.compositionMode)
114
- if data.flags & QPaintEngine.DirtyOpacity:
131
+ if flags & _DIRTY_OPACITY:
115
132
  painter.setOpacity(data.opacity)
116
133
 
117
134
 
qwt/null_paintdevice.py CHANGED
@@ -272,8 +272,12 @@ class QwtNullPaintDevice(QPaintDevice):
272
272
  * 25.4
273
273
  / self.metric(QPaintDevice.PdmDpiY)
274
274
  )
275
+ elif deviceMetric == QPaintDevice.PdmDevicePixelRatio:
276
+ value = 1
277
+ elif deviceMetric == QPaintDevice.PdmDevicePixelRatioScaled:
278
+ value = 1
275
279
  else:
276
- value = 0
280
+ value = super(QwtNullPaintDevice, self).metric(deviceMetric)
277
281
  return value
278
282
 
279
283
  def drawRects(self, rects, rectCount):
qwt/painter_command.py CHANGED
@@ -18,6 +18,40 @@ import copy
18
18
  from qtpy.QtGui import QPaintEngine, QPainterPath
19
19
 
20
20
 
21
+ def _flag_int(flag):
22
+ """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6).
23
+
24
+ PyQt5 exposes Qt enums as plain ints (``int(flag)`` works). PyQt6 wraps
25
+ them as ``enum.Flag`` instances which are not ``int`` subclasses, so
26
+ ``int(flag)`` raises -- the value must be read from ``flag.value``.
27
+ """
28
+ try:
29
+ return flag.value
30
+ except AttributeError:
31
+ return int(flag)
32
+
33
+
34
+ # Cache QPaintEngine.DirtyXxx flags as plain Python ints once at import time.
35
+ # On PyQt6, Qt enums are full ``enum.Flag`` instances and every ``flags &
36
+ # Member`` test goes through Python's ``enum.__and__`` machinery (~6 us each).
37
+ # In ``QwtPainterCommand.__init__`` below, the State branch performs twelve
38
+ # successive flag tests per painter command -- on PyQt6 alone this accounted
39
+ # for ~20 ms of the residual perf gap on the load test. Casting once to int
40
+ # and bitwise-testing against int constants brings each test back to ~50 ns.
41
+ _DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen)
42
+ _DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush)
43
+ _DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin)
44
+ _DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont)
45
+ _DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground)
46
+ _DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform)
47
+ _DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled)
48
+ _DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion)
49
+ _DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath)
50
+ _DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints)
51
+ _DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode)
52
+ _DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity)
53
+
54
+
21
55
  class PixmapData(object):
22
56
  def __init__(self):
23
57
  self.rect = None
@@ -125,32 +159,35 @@ class QwtPainterCommand(object):
125
159
  self.__type = self.State
126
160
  self.__stateData = StateData()
127
161
  self.__stateData.flags = state.state()
128
- if self.__stateData.flags & QPaintEngine.DirtyPen:
162
+ # Cast to int once: subsequent bitwise tests are done against
163
+ # the cached _DIRTY_* int constants (see top of module).
164
+ flags = _flag_int(self.__stateData.flags)
165
+ if flags & _DIRTY_PEN:
129
166
  self.__stateData.pen = state.pen()
130
- if self.__stateData.flags & QPaintEngine.DirtyBrush:
167
+ if flags & _DIRTY_BRUSH:
131
168
  self.__stateData.brush = state.brush()
132
- if self.__stateData.flags & QPaintEngine.DirtyBrushOrigin:
169
+ if flags & _DIRTY_BRUSH_ORIGIN:
133
170
  self.__stateData.brushOrigin = state.brushOrigin()
134
- if self.__stateData.flags & QPaintEngine.DirtyFont:
171
+ if flags & _DIRTY_FONT:
135
172
  self.__stateData.font = state.font()
136
- if self.__stateData.flags & QPaintEngine.DirtyBackground:
173
+ if flags & _DIRTY_BACKGROUND:
137
174
  self.__stateData.backgroundMode = state.backgroundMode()
138
175
  self.__stateData.backgroundBrush = state.backgroundBrush()
139
- if self.__stateData.flags & QPaintEngine.DirtyTransform:
176
+ if flags & _DIRTY_TRANSFORM:
140
177
  self.__stateData.transform = state.transform()
141
- if self.__stateData.flags & QPaintEngine.DirtyClipEnabled:
178
+ if flags & _DIRTY_CLIP_ENABLED:
142
179
  self.__stateData.isClipEnabled = state.isClipEnabled()
143
- if self.__stateData.flags & QPaintEngine.DirtyClipRegion:
180
+ if flags & _DIRTY_CLIP_REGION:
144
181
  self.__stateData.clipRegion = state.clipRegion()
145
182
  self.__stateData.clipOperation = state.clipOperation()
146
- if self.__stateData.flags & QPaintEngine.DirtyClipPath:
183
+ if flags & _DIRTY_CLIP_PATH:
147
184
  self.__stateData.clipPath = state.clipPath()
148
185
  self.__stateData.clipOperation = state.clipOperation()
149
- if self.__stateData.flags & QPaintEngine.DirtyHints:
186
+ if flags & _DIRTY_HINTS:
150
187
  self.__stateData.renderHints = state.renderHints()
151
- if self.__stateData.flags & QPaintEngine.DirtyCompositionMode:
188
+ if flags & _DIRTY_COMPOSITION_MODE:
152
189
  self.__stateData.compositionMode = state.compositionMode()
153
- if self.__stateData.flags & QPaintEngine.DirtyOpacity:
190
+ if flags & _DIRTY_OPACITY:
154
191
  self.__stateData.opacity = state.opacity()
155
192
  elif len(args) == 3:
156
193
  rect, pixmap, subRect = args
qwt/plot_canvas.py CHANGED
@@ -787,7 +787,7 @@ class QwtPlotCanvas(QFrame):
787
787
  import warnings
788
788
 
789
789
  warnings.warn(
790
- "`invalidatePaintCache` has been removed: " "please use `replot` instead",
790
+ "`invalidatePaintCache` has been removed: please use `replot` instead",
791
791
  RuntimeWarning,
792
792
  )
793
793
  self.replot()
qwt/scale_div.py CHANGED
@@ -235,9 +235,11 @@ class QwtScaleDiv(object):
235
235
  :param float value: Value
236
236
  :return: True/False
237
237
  """
238
- min_ = min([self.__lowerBound, self.__upperBound])
239
- max_ = max([self.__lowerBound, self.__upperBound])
240
- return value >= min_ and value <= max_
238
+ lb = self.__lowerBound
239
+ ub = self.__upperBound
240
+ if lb <= ub:
241
+ return lb <= value <= ub
242
+ return ub <= value <= lb
241
243
 
242
244
  def invert(self):
243
245
  """
qwt/scale_draw.py CHANGED
@@ -39,11 +39,23 @@ from qwt.scale_div import QwtScaleDiv
39
39
  from qwt.scale_map import QwtScaleMap
40
40
  from qwt.text import QwtText
41
41
 
42
+ # Plain-int aliases for Qt alignment flags. Qt6 exposes alignment flags as
43
+ # IntEnum members and bitwise operations on them go through Python's
44
+ # enum machinery (`__and__`/`__call__`), which is one of the dominant costs
45
+ # of label layout. Casting to int once and using these constants makes the
46
+ # bitwise tests in `labelTransformation` ~10x cheaper without changing
47
+ # semantics.
48
+ _ALIGN_LEFT = int(Qt.AlignLeft)
49
+ _ALIGN_RIGHT = int(Qt.AlignRight)
50
+ _ALIGN_TOP = int(Qt.AlignTop)
51
+ _ALIGN_BOTTOM = int(Qt.AlignBottom)
52
+
42
53
 
43
54
  class QwtAbstractScaleDraw_PrivateData(QObject):
55
+ # QObject base class restored for Qt parent/child ownership semantics.
56
+
44
57
  def __init__(self):
45
58
  QObject.__init__(self)
46
-
47
59
  self.spacing = 4
48
60
  self.penWidth = 0
49
61
  self.minExtent = 0.0
@@ -471,11 +483,15 @@ class QwtAbstractScaleDraw(object):
471
483
 
472
484
 
473
485
  class QwtScaleDraw_PrivateData(QObject):
486
+ # QObject base class restored for Qt parent/child ownership semantics.
487
+
474
488
  def __init__(self):
475
489
  QObject.__init__(self)
476
-
477
490
  self.len = 0
478
491
  self.alignment = QwtScaleDraw.BottomScale
492
+ # Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment``
493
+ # so that the very hot ``orientation()`` accessor avoids any test.
494
+ self.orientation = Qt.Horizontal
479
495
  self.labelAlignment = 0
480
496
  self.labelRotation = 0.0
481
497
  self.labelAutoSize = True
@@ -554,6 +570,11 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
554
570
  :py:meth:`alignment()`
555
571
  """
556
572
  self.__data.alignment = align
573
+ # Keep cached orientation in sync (see ``orientation()``).
574
+ if align == self.BottomScale or align == self.TopScale:
575
+ self.__data.orientation = Qt.Horizontal
576
+ else:
577
+ self.__data.orientation = Qt.Vertical
557
578
 
558
579
  def orientation(self):
559
580
  """
@@ -568,10 +589,8 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
568
589
 
569
590
  :py:meth:`alignment()`
570
591
  """
571
- if self.__data.alignment in (self.TopScale, self.BottomScale):
572
- return Qt.Horizontal
573
- elif self.__data.alignment in (self.LeftScale, self.RightScale):
574
- return Qt.Vertical
592
+ # Pre-computed by ``setAlignment`` - this method is called per tick.
593
+ return self.__data.orientation
575
594
 
576
595
  def getBorderDistHint(self, font):
577
596
  """
@@ -597,17 +616,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
597
616
  if len(ticks) == 0:
598
617
  return start, end
599
618
 
619
+ scale_map = self.scaleMap()
620
+ transform = scale_map.transform
600
621
  minTick = ticks[0]
601
- minPos = self.scaleMap().transform(minTick)
622
+ minPos = transform(minTick)
602
623
  maxTick = minTick
603
624
  maxPos = minPos
604
625
 
605
626
  for tick in ticks:
606
- tickPos = self.scaleMap().transform(tick)
627
+ tickPos = transform(tick)
607
628
  if tickPos < minPos:
608
629
  minTick = tick
609
630
  minPos = tickPos
610
- if tickPos > self.scaleMap().transform(maxTick):
631
+ if tickPos > maxPos:
611
632
  maxTick = tick
612
633
  maxPos = tickPos
613
634
 
@@ -615,16 +636,16 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
615
636
  e = 0.0
616
637
  if self.orientation() == Qt.Vertical:
617
638
  s = -self.labelRect(font, minTick).top()
618
- s -= abs(minPos - round(self.scaleMap().p2()))
639
+ s -= abs(minPos - round(scale_map.p2()))
619
640
 
620
641
  e = self.labelRect(font, maxTick).bottom()
621
- e -= abs(maxPos - self.scaleMap().p1())
642
+ e -= abs(maxPos - scale_map.p1())
622
643
  else:
623
644
  s = -self.labelRect(font, minTick).left()
624
- s -= abs(minPos - self.scaleMap().p1())
645
+ s -= abs(minPos - scale_map.p1())
625
646
 
626
647
  e = self.labelRect(font, maxTick).right()
627
- e -= abs(maxPos - self.scaleMap().p2())
648
+ e -= abs(maxPos - scale_map.p2())
628
649
 
629
650
  return max(math.ceil(s), 0), max(math.ceil(e), 0)
630
651
 
@@ -763,27 +784,22 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
763
784
  """
764
785
  tval = self.scaleMap().transform(value)
765
786
  dist = self.spacing()
766
- if self.hasComponent(QwtAbstractScaleDraw.Backbone):
767
- dist += max([1, self.penWidth()])
768
- if self.hasComponent(QwtAbstractScaleDraw.Ticks):
787
+ hasComponent = self.hasComponent
788
+ if hasComponent(QwtAbstractScaleDraw.Backbone):
789
+ dist += max(1, self.penWidth())
790
+ if hasComponent(QwtAbstractScaleDraw.Ticks):
769
791
  dist += self.tickLength(QwtScaleDiv.MajorTick)
770
792
 
771
- px = 0
772
- py = 0
773
- if self.alignment() == self.RightScale:
774
- px = self.__data.pos.x() + dist
775
- py = tval
776
- elif self.alignment() == self.LeftScale:
777
- px = self.__data.pos.x() - dist
778
- py = tval
779
- elif self.alignment() == self.BottomScale:
780
- px = tval
781
- py = self.__data.pos.y() + dist
782
- elif self.alignment() == self.TopScale:
783
- px = tval
784
- py = self.__data.pos.y() - dist
785
-
786
- return QPointF(px, py)
793
+ alignment = self.alignment()
794
+ pos = self.__data.pos
795
+ if alignment == self.RightScale:
796
+ return QPointF(pos.x() + dist, tval)
797
+ if alignment == self.LeftScale:
798
+ return QPointF(pos.x() - dist, tval)
799
+ if alignment == self.BottomScale:
800
+ return QPointF(tval, pos.y() + dist)
801
+ # TopScale
802
+ return QPointF(tval, pos.y() - dist)
787
803
 
788
804
  def drawTick(self, painter, value, len_):
789
805
  """
@@ -1007,17 +1023,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
1007
1023
  flags = self.labelAlignment()
1008
1024
  if flags == 0:
1009
1025
  flags = self.Flags[self.alignment()]
1026
+ # Cast to plain int once to avoid the per-bit Qt6 enum overhead.
1027
+ flags = int(flags)
1010
1028
 
1011
- if flags & Qt.AlignLeft:
1029
+ if flags & _ALIGN_LEFT:
1012
1030
  x = -size.width()
1013
- elif flags & Qt.AlignRight:
1031
+ elif flags & _ALIGN_RIGHT:
1014
1032
  x = 0.0
1015
1033
  else:
1016
1034
  x = -(0.5 * size.width())
1017
1035
 
1018
- if flags & Qt.AlignTop:
1036
+ if flags & _ALIGN_TOP:
1019
1037
  y = -size.height()
1020
- elif flags & Qt.AlignBottom:
1038
+ elif flags & _ALIGN_BOTTOM:
1021
1039
  y = 0
1022
1040
  else:
1023
1041
  y = -(0.5 * size.height())
@@ -1039,6 +1057,31 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
1039
1057
  lbl, labelSize = self.tickLabel(font, value)
1040
1058
  if not lbl or lbl.isEmpty():
1041
1059
  return QRectF(0.0, 0.0, 0.0, 0.0)
1060
+ # Fast path: when the label is not rotated, the contribution of
1061
+ # ``pos`` cancels out (transform.translate(pos) followed by
1062
+ # br.translate(-pos)). This avoids ``labelPosition``,
1063
+ # ``labelTransformation`` and ``QTransform.mapRect`` entirely - all
1064
+ # of which are dominant costs in tick-heavy layouts.
1065
+ if self.labelRotation() == 0.0:
1066
+ flags = self.labelAlignment()
1067
+ if flags == 0:
1068
+ flags = self.Flags[self.alignment()]
1069
+ flags = int(flags)
1070
+ w = labelSize.width()
1071
+ h = labelSize.height()
1072
+ if flags & _ALIGN_LEFT:
1073
+ x = -w
1074
+ elif flags & _ALIGN_RIGHT:
1075
+ x = 0.0
1076
+ else:
1077
+ x = -0.5 * w
1078
+ if flags & _ALIGN_TOP:
1079
+ y = -h
1080
+ elif flags & _ALIGN_BOTTOM:
1081
+ y = 0.0
1082
+ else:
1083
+ y = -0.5 * h
1084
+ return QRectF(x, y, w, h)
1042
1085
  pos = self.labelPosition(value)
1043
1086
  transform = self.labelTransformation(pos, labelSize)
1044
1087
  br = transform.mapRect(QRectF(QPointF(0, 0), labelSize))
qwt/scale_engine.py CHANGED
@@ -324,11 +324,10 @@ class QwtScaleEngine(object):
324
324
  """
325
325
  if not interval.isValid():
326
326
  return False
327
- eps = abs(1.0e-6 * interval.width())
328
- if interval.minValue() - value > eps or value - interval.maxValue() > eps:
329
- return False
330
- else:
331
- return True
327
+ min_v = interval.minValue()
328
+ max_v = interval.maxValue()
329
+ eps = abs(1.0e-6 * (max_v - min_v))
330
+ return not (min_v - value > eps or value - max_v > eps)
332
331
 
333
332
  def strip(self, ticks, interval):
334
333
  """
@@ -340,9 +339,17 @@ class QwtScaleEngine(object):
340
339
  """
341
340
  if not interval.isValid() or not ticks:
342
341
  return []
343
- if self.contains(interval, ticks[0]) and self.contains(interval, ticks[-1]):
342
+ # Inline ``contains`` to avoid one Python call per tick: ``strip`` is
343
+ # called by buildTicks for every layout pass and is one of the
344
+ # dominant costs in tick-heavy plots.
345
+ min_v = interval.minValue()
346
+ max_v = interval.maxValue()
347
+ eps = abs(1.0e-6 * (max_v - min_v))
348
+ lo = min_v - eps
349
+ hi = max_v + eps
350
+ if lo <= ticks[0] and ticks[-1] <= hi:
344
351
  return ticks
345
- return [tick for tick in ticks if self.contains(interval, tick)]
352
+ return [tick for tick in ticks if lo <= tick <= hi]
346
353
 
347
354
  def buildInterval(self, value):
348
355
  """
@@ -594,7 +601,7 @@ class QwtLinearScaleEngine(QwtScaleEngine):
594
601
  numTicks = int(math.ceil(abs(stepSize / minStep)) - 1)
595
602
  medIndex = -1
596
603
  if numTicks % 2:
597
- medIndex = numTicks / 2
604
+ medIndex = numTicks // 2
598
605
  for val in ticks[QwtScaleDiv.MajorTick]:
599
606
  for k in range(numTicks):
600
607
  val += minStep
@@ -837,7 +844,7 @@ class QwtLogScaleEngine(QwtScaleEngine):
837
844
 
838
845
  mediumTickIndex = -1
839
846
  if numSteps > 2 and numSteps % 2 == 0:
840
- mediumTickIndex = numSteps / 2
847
+ mediumTickIndex = numSteps // 2
841
848
 
842
849
  for v in ticks[QwtScaleDiv.MajorTick]:
843
850
  s = logBase / numSteps
@@ -872,7 +879,7 @@ class QwtLogScaleEngine(QwtScaleEngine):
872
879
 
873
880
  mediumTickIndex = -1
874
881
  if numTicks > 2 and numTicks % 2:
875
- mediumTickIndex = numTicks / 2
882
+ mediumTickIndex = numTicks // 2
876
883
 
877
884
  minFactor = max([math.pow(logBase, minStep), float(logBase)])
878
885
 
qwt/scale_map.py CHANGED
@@ -217,8 +217,12 @@ class QwtScaleMap(object):
217
217
  if self.__transform:
218
218
  self.__ts1 = self.__transform.transform(self.__ts1)
219
219
  ts2 = self.__transform.transform(ts2)
220
- self.__cnv = 1.0
221
- if self.__ts1 != ts2:
220
+ if self.__ts1 == ts2:
221
+ # Degenerate scale: collapse every value to ``p1`` (matches the
222
+ # symmetric guard in ``invTransform_scalar`` and the C++ Qwt
223
+ # behaviour).
224
+ self.__cnv = 0.0
225
+ else:
222
226
  self.__cnv = (self.__p2 - self.__p1) / (ts2 - self.__ts1)
223
227
 
224
228
  def transform(self, *args):
@@ -245,13 +249,18 @@ class QwtScaleMap(object):
245
249
 
246
250
  :py:meth:`invTransform()`
247
251
  """
248
- if len(args) == 1:
249
- # Scalar transform
250
- return self.transform_scalar(args[0])
251
- elif len(args) == 3 and isinstance(args[2], QPointF):
252
+ nargs = len(args)
253
+ if nargs == 1:
254
+ # Scalar transform: inline the fast path for the dominant case
255
+ # (avoids one Python call frame per tick label).
256
+ s = args[0]
257
+ if self.__transform:
258
+ s = self.__transform.transform(s)
259
+ return self.__p1 + (s - self.__ts1) * self.__cnv
260
+ elif nargs == 3 and isinstance(args[2], QPointF):
252
261
  xMap, yMap, pos = args
253
262
  return QPointF(xMap.transform(pos.x()), yMap.transform(pos.y()))
254
- elif len(args) == 3 and isinstance(args[2], QRectF):
263
+ elif nargs == 3 and isinstance(args[2], QRectF):
255
264
  xMap, yMap, rect = args
256
265
  x1 = xMap.transform(rect.left())
257
266
  x2 = xMap.transform(rect.right())
@@ -269,7 +278,7 @@ class QwtScaleMap(object):
269
278
  y1 = 0.0
270
279
  if qwtFuzzyCompare(y2, 0.0, y2 - y1) == 0:
271
280
  y2 = 0.0
272
- return QRectF(x1, y1, x2 - x1 + 1, y2 - y1 + 1)
281
+ return QRectF(x1, y1, x2 - x1, y2 - y1)
273
282
  else:
274
283
  raise TypeError(
275
284
  "%s().transform() takes 1 or 3 argument(s) (%s "
@@ -292,8 +301,8 @@ class QwtScaleMap(object):
292
301
  elif isinstance(args[2], QRectF):
293
302
  xMap, yMap, rect = args
294
303
  x1 = xMap.invTransform(rect.left())
295
- x2 = xMap.invTransform(rect.right() - 1)
304
+ x2 = xMap.invTransform(rect.right())
296
305
  y1 = yMap.invTransform(rect.top())
297
- y2 = yMap.invTransform(rect.bottom() - 1)
306
+ y2 = yMap.invTransform(rect.bottom())
298
307
  r = QRectF(x1, y1, x2 - x1, y2 - y1)
299
308
  return r.normalized()
@@ -144,8 +144,7 @@ class BodePlot(QwtPlot):
144
144
  yvalue=-20.0,
145
145
  align=Qt.AlignRight | Qt.AlignBottom,
146
146
  label=QwtText.make(
147
- "[1-(\u03c9/\u03c9<sub>0</sub>)<sup>2</sup>+2j\u03c9/Q]"
148
- "<sup>-1</sup>",
147
+ "[1-(\u03c9/\u03c9<sub>0</sub>)<sup>2</sup>+2j\u03c9/Q]<sup>-1</sup>",
149
148
  color=Qt.white,
150
149
  borderradius=2,
151
150
  borderpen=QPen(Qt.lightGray, 5),
@@ -30,9 +30,9 @@ class RelativeMarginDemo(QW.QWidget):
30
30
  def_margin = plot.axisMargin(qwt.QwtPlot.yLeft)
31
31
  scale_str = "lin/lin" if not log_scale else "log/lin"
32
32
  if relative_margin is None:
33
- margin_str = f"default ({def_margin*100:.0f}%)"
33
+ margin_str = f"default ({def_margin * 100:.0f}%)"
34
34
  else:
35
- margin_str = f"{relative_margin*100:.0f}%"
35
+ margin_str = f"{relative_margin * 100:.0f}%"
36
36
  plot.setTitle(f"{scale_str}, margin: {margin_str}")
37
37
  if relative_margin is not None:
38
38
  plot.setAxisMargin(qwt.QwtPlot.yLeft, relative_margin)
qwt/text.py CHANGED
@@ -72,13 +72,34 @@ QWIDGETSIZE_MAX = (1 << 24) - 1
72
72
  QT_API = os.environ["QT_API"]
73
73
 
74
74
 
75
+ # Cache Qt alignment flags as plain ints once at import time. On PyQt6 these
76
+ # are ``Qt.AlignmentFlag`` enum members and every bitwise test goes through
77
+ # ``enum.__and__`` (~6 us each). The test code below combines them in hot
78
+ # paths called per-tick / per-label / per-paint event.
79
+ def _flag_int(flag):
80
+ """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6)."""
81
+ try:
82
+ return flag.value
83
+ except AttributeError:
84
+ return int(flag)
85
+
86
+
87
+ _ALIGN_LEFT = _flag_int(Qt.AlignLeft)
88
+ _ALIGN_RIGHT = _flag_int(Qt.AlignRight)
89
+ _ALIGN_TOP = _flag_int(Qt.AlignTop)
90
+ _ALIGN_BOTTOM = _flag_int(Qt.AlignBottom)
91
+ _ALIGN_HCENTER = _flag_int(Qt.AlignHCenter)
92
+ _ALIGN_JUSTIFY = _flag_int(Qt.AlignJustify)
93
+ _ALIGN_CENTER = _flag_int(Qt.AlignCenter)
94
+
95
+
75
96
  def taggedRichText(text, flags):
76
97
  richText = text
77
- if flags & Qt.AlignJustify:
98
+ if flags & _ALIGN_JUSTIFY:
78
99
  richText = '<div align="justify">' + richText + "</div>"
79
- elif flags & Qt.AlignRight:
100
+ elif flags & _ALIGN_RIGHT:
80
101
  richText = '<div align="right">' + richText + "</div>"
81
- elif flags & Qt.AlignHCenter:
102
+ elif flags & _ALIGN_HCENTER:
82
103
  richText = '<div align="center">' + richText + "</div>"
83
104
  return richText
84
105
 
@@ -189,6 +210,66 @@ class QwtTextEngine(object):
189
210
 
190
211
  ASCENTCACHE = {}
191
212
 
213
+ # Module-level cache: ``id(font) -> tuple_key`` (fast path) and
214
+ # ``tuple_key -> tuple_key`` (slow path). The tuple key is built from a
215
+ # handful of QFont attributes that uniquely identify the *logical* font for
216
+ # metrics purposes. Tick-rendering uses very few distinct fonts in practice
217
+ # so both dicts stay tiny.
218
+ #
219
+ # This replaces the previous ``id(font) -> font.key()`` design. Two reasons:
220
+ #
221
+ # 1. ``QFont.key()`` is a sip dispatch that costs ~3.3 us/call on PyQt5 and
222
+ # ~9.3 us/call on PyQt6 -- it became the single biggest residual hotspot
223
+ # in ``QwtText.textSize`` on PyQt6.
224
+ # 2. PyQt6 returns a fresh Python wrapper around the same QFont on most
225
+ # calls, so ``id(font)`` changes between calls and the id-keyed fast path
226
+ # misses ~92% of the time. The tuple-key second level recovers the hits
227
+ # those misses would have produced, without paying for ``font.key()``.
228
+ #
229
+ # The tuple key uses ``(family, pixelSize-or-pointSizeF, weight, italic,
230
+ # stretch, styleStrategy)``. This is what determines ``QFontMetrics`` output
231
+ # in practice; if two QFonts share these values they share metrics.
232
+
233
+ _FONT_KEY_CACHE: dict = {} # id(font) -> tuple_key (fast path)
234
+ _FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts
235
+ # as the "have we seen this logical font" set)
236
+ _FONT_KEY_CACHE_LIMIT = 1024
237
+ _FM_CACHE_LIMIT = 256 # max QFontMetrics/QFontMetricsF entries per engine
238
+
239
+
240
+ def _font_tuple_key(font):
241
+ """Build a hashable tuple identifying the logical font."""
242
+ px = font.pixelSize()
243
+ return (
244
+ font.family(),
245
+ px if px > 0 else font.pointSizeF(),
246
+ font.weight(),
247
+ font.italic(),
248
+ font.stretch(),
249
+ font.styleStrategy(),
250
+ )
251
+
252
+
253
+ def font_key_cached(font):
254
+ """Return a hashable cache key uniquely identifying ``font`` for metrics.
255
+
256
+ The returned value is **not** ``QFont.key()`` -- it is a tuple computed
257
+ from a handful of QFont attributes. It is safe to use as a dict key for
258
+ metrics caches (callers in this module always compare by ``==`` only).
259
+ """
260
+ fid = id(font)
261
+ entry = _FONT_KEY_CACHE.get(fid)
262
+ if entry is not None:
263
+ return entry[1]
264
+ tkey = _font_tuple_key(font)
265
+ # Intern: reuse the same tuple object across all id() variants so dict
266
+ # lookups in caller-side caches benefit from object-identity hash hits.
267
+ interned = _FONT_TUPLE_CACHE.setdefault(tkey, tkey)
268
+ if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT:
269
+ _FONT_KEY_CACHE.clear()
270
+ _FONT_KEY_CACHE[fid] = (font, interned)
271
+ return interned
272
+
192
273
 
193
274
  def get_screen_resolution():
194
275
  """Return screen resolution: tuple of floats (DPIx, DPIy)"""
@@ -226,19 +307,28 @@ class QwtPlainTextEngine(QwtTextEngine):
226
307
  self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
227
308
  self._fm_cache = {}
228
309
  self._fm_cache_f = {}
310
+ self._margins_cache = {}
311
+ # Fast path: when textMargins is called repeatedly with the same
312
+ # QFont instance, skip the (expensive) font.key() Qt call.
313
+ self._margins_last_id = -1
314
+ self._margins_last_value = None
229
315
 
230
316
  def fontmetrics(self, font):
231
- fid = font.toString()
317
+ fid = font_key_cached(font)
232
318
  try:
233
319
  return self._fm_cache[fid]
234
320
  except KeyError:
321
+ if len(self._fm_cache) >= _FM_CACHE_LIMIT:
322
+ self._fm_cache.clear()
235
323
  return self._fm_cache.setdefault(fid, QFontMetrics(font))
236
324
 
237
325
  def fontmetrics_f(self, font):
238
- fid = font.toString()
326
+ fid = font_key_cached(font)
239
327
  try:
240
328
  return self._fm_cache_f[fid]
241
329
  except KeyError:
330
+ if len(self._fm_cache_f) >= _FM_CACHE_LIMIT:
331
+ self._fm_cache_f.clear()
242
332
  return self._fm_cache_f.setdefault(fid, QFontMetricsF(font))
243
333
 
244
334
  def heightForWidth(self, font, flags, text, width):
@@ -270,10 +360,12 @@ class QwtPlainTextEngine(QwtTextEngine):
270
360
 
271
361
  def effectiveAscent(self, font):
272
362
  global ASCENTCACHE
273
- fontKey = font.key()
363
+ fontKey = font_key_cached(font)
274
364
  ascent = ASCENTCACHE.get(fontKey)
275
365
  if ascent is not None:
276
366
  return ascent
367
+ if len(ASCENTCACHE) >= _FM_CACHE_LIMIT:
368
+ ASCENTCACHE.clear()
277
369
  return ASCENTCACHE.setdefault(fontKey, self.findAscent(font))
278
370
 
279
371
  def findAscent(self, font):
@@ -317,11 +409,21 @@ class QwtPlainTextEngine(QwtTextEngine):
317
409
  :param QFont font: Font of the text
318
410
  :return: tuple (left, right, top, bottom) representing margins
319
411
  """
320
- left = right = 0
321
- fm = self.fontmetrics(font)
322
- top = fm.ascent() - self.effectiveAscent(font)
323
- bottom = fm.descent()
324
- return left, right, top, bottom
412
+ # Fast path: same QFont object as the previous call.
413
+ font_id = id(font)
414
+ if font_id == self._margins_last_id:
415
+ return self._margins_last_value
416
+ fkey = font_key_cached(font)
417
+ cached = self._margins_cache.get(fkey)
418
+ if cached is None:
419
+ fm = self.fontmetrics(font)
420
+ cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
421
+ if len(self._margins_cache) >= _FM_CACHE_LIMIT:
422
+ self._margins_cache.clear()
423
+ self._margins_cache[fkey] = cached
424
+ self._margins_last_id = font_id
425
+ self._margins_last_value = cached
426
+ return cached
325
427
 
326
428
  def draw(self, painter, rect, flags, text):
327
429
  """
@@ -465,9 +567,10 @@ class QwtRichTextEngine(QwtTextEngine):
465
567
 
466
568
 
467
569
  class QwtText_PrivateData(QObject):
570
+ # QObject base class restored for Qt parent/child ownership semantics.
571
+
468
572
  def __init__(self):
469
573
  QObject.__init__(self)
470
-
471
574
  self.renderFlags = Qt.AlignCenter
472
575
  self.borderRadius = 0
473
576
  self.borderPen = Qt.NoPen
@@ -484,10 +587,13 @@ class QwtText_PrivateData(QObject):
484
587
  class QwtText_LayoutCache(object):
485
588
  def __init__(self):
486
589
  self.textSize = None
487
- self.font = None
590
+ self.fontKey = None
591
+ self.fontId = -1
488
592
 
489
593
  def invalidate(self):
490
594
  self.textSize = None
595
+ self.fontKey = None
596
+ self.fontId = -1
491
597
 
492
598
 
493
599
  class QwtText(object):
@@ -727,7 +833,13 @@ class QwtText(object):
727
833
  :py:meth:`renderFlags()`,
728
834
  :py:meth:`qwt.text.QwtTextEngine.draw()`
729
835
  """
730
- renderFlags = Qt.AlignmentFlag(renderFlags)
836
+ # Wrap into Qt.AlignmentFlag so that downstream Qt APIs (notably
837
+ # ``QTextOption.setAlignment``, ``QPainter.drawText``,
838
+ # ``QFontMetrics.boundingRect``) that strictly require an enum on
839
+ # PyQt6 keep working. Hot bitwise-test sites locally cast back to
840
+ # int to avoid the per-test enum.__and__ cost.
841
+ if not isinstance(renderFlags, Qt.AlignmentFlag):
842
+ renderFlags = Qt.AlignmentFlag(renderFlags)
731
843
  if renderFlags != self.__data.renderFlags:
732
844
  self.__data.renderFlags = renderFlags
733
845
  self.__layoutCache.invalidate()
@@ -994,17 +1106,24 @@ class QwtText(object):
994
1106
  :param QFont defaultFont Font, used for the calculation if the text has no font
995
1107
  :return: Caluclated size
996
1108
  """
997
- font = QFont(self.usedFont(defaultFont))
998
- if (
999
- self.__layoutCache.textSize is None
1000
- or not self.__layoutCache.textSize.isValid()
1001
- or self.__layoutCache.font is not font
1002
- ):
1003
- self.__layoutCache.textSize = self.__data.textEngine.textSize(
1004
- font, self.__data.renderFlags, self.__data.text
1005
- )
1006
- self.__layoutCache.font = font
1007
- sz = self.__layoutCache.textSize
1109
+ font = self.usedFont(defaultFont)
1110
+ cache = self.__layoutCache
1111
+ font_id = id(font)
1112
+ if cache.textSize is not None and cache.fontId == font_id:
1113
+ sz = QSizeF(cache.textSize)
1114
+ else:
1115
+ fkey = font_key_cached(font)
1116
+ if (
1117
+ cache.textSize is None
1118
+ or not cache.textSize.isValid()
1119
+ or cache.fontKey != fkey
1120
+ ):
1121
+ cache.textSize = self.__data.textEngine.textSize(
1122
+ font, self.__data.renderFlags, self.__data.text
1123
+ )
1124
+ cache.fontKey = fkey
1125
+ cache.fontId = font_id
1126
+ sz = QSizeF(cache.textSize)
1008
1127
  if self.__data.layoutAttributes & self.MinimumLayout:
1009
1128
  (left, right, top, bottom) = self.__data.textEngine.textMargins(font)
1010
1129
  sz -= QSizeF(left + right, top + bottom)
@@ -1072,7 +1191,13 @@ class QwtText(object):
1072
1191
  return self.__map.get(format_)
1073
1192
  elif format_ is not None:
1074
1193
  if format_ == QwtText.AutoText:
1075
- for key, engine in list(self.__map.items()):
1194
+ # Fast path: a string with no ``<`` cannot be rich text, so
1195
+ # we can return the plain engine without iterating the map
1196
+ # and calling Qt.mightBeRichText (which is a hot Qt call
1197
+ # for tick labels like " 1.5").
1198
+ if "<" not in text:
1199
+ return self.__map[QwtText.PlainText]
1200
+ for key, engine in self.__map.items():
1076
1201
  if key != QwtText.PlainText:
1077
1202
  if engine and engine.mightRender(text):
1078
1203
  return engine
@@ -1291,10 +1416,10 @@ class QwtTextLabel(QFrame):
1291
1416
  if indent <= 0:
1292
1417
  indent = self.defaultIndent()
1293
1418
  if indent > 0:
1294
- align = self.__data.text.renderFlags()
1295
- if align & Qt.AlignLeft or align & Qt.AlignRight:
1419
+ align = _flag_int(self.__data.text.renderFlags())
1420
+ if align & (_ALIGN_LEFT | _ALIGN_RIGHT):
1296
1421
  mw += self.__data.indent
1297
- elif align & Qt.AlignTop or align & Qt.AlignBottom:
1422
+ elif align & (_ALIGN_TOP | _ALIGN_BOTTOM):
1298
1423
  mh += self.__data.indent
1299
1424
  sz += QSizeF(mw, mh)
1300
1425
  return QSize(math.ceil(sz.width()), math.ceil(sz.height()))
@@ -1304,15 +1429,15 @@ class QwtTextLabel(QFrame):
1304
1429
  :param int width: Width
1305
1430
  :return: Preferred height for this widget, given the width.
1306
1431
  """
1307
- renderFlags = self.__data.text.renderFlags()
1432
+ renderFlags = _flag_int(self.__data.text.renderFlags())
1308
1433
  indent = self.__data.indent
1309
1434
  if indent <= 0:
1310
1435
  indent = self.defaultIndent()
1311
1436
  width -= 2 * self.frameWidth()
1312
- if renderFlags & Qt.AlignLeft or renderFlags & Qt.AlignRight:
1437
+ if renderFlags & (_ALIGN_LEFT | _ALIGN_RIGHT):
1313
1438
  width -= indent
1314
1439
  height = math.ceil(self.__data.text.heightForWidth(width, self.font()))
1315
- if renderFlags & Qt.AlignTop or renderFlags & Qt.AlignBottom:
1440
+ if renderFlags & (_ALIGN_TOP | _ALIGN_BOTTOM):
1316
1441
  height += indent
1317
1442
  height += 2 * self.frameWidth()
1318
1443
  return height
@@ -1372,14 +1497,14 @@ class QwtTextLabel(QFrame):
1372
1497
  if indent <= 0:
1373
1498
  indent = self.defaultIndent()
1374
1499
  if indent > 0:
1375
- renderFlags = self.__data.text.renderFlags()
1376
- if renderFlags & Qt.AlignLeft:
1500
+ renderFlags = _flag_int(self.__data.text.renderFlags())
1501
+ if renderFlags & _ALIGN_LEFT:
1377
1502
  r.setX(r.x() + indent)
1378
- elif renderFlags & Qt.AlignRight:
1503
+ elif renderFlags & _ALIGN_RIGHT:
1379
1504
  r.setWidth(r.width() - indent)
1380
- elif renderFlags & Qt.AlignTop:
1505
+ elif renderFlags & _ALIGN_TOP:
1381
1506
  r.setY(r.y() + indent)
1382
- elif renderFlags & Qt.AlignBottom:
1507
+ elif renderFlags & _ALIGN_BOTTOM:
1383
1508
  r.setHeight(r.height() - indent)
1384
1509
  return r
1385
1510