Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0c77b9a

Browse files
committed
FIX: ensure that qt5agg and qt5cairo backends actually use qt5
Because the code in qt_compat tries qt6 bindings first, backend_qt supports both Qt5 and Qt6, and the qt5 named backends are shims to the generic Qt backend, if you imported matplotlib.backends.backend_qt5agg, matplotlib.backends.backend_qt5cairo, or matplotlib.backends.backend_qt5, and 1. had PyQt6 or pyside6 installed 2. had not previously imported a Qt5 binding Then you will end up with a backend that (by name) claims to be Qt5, but will be using Qt6 bindings. If you then subsequently import a Qt6 binding and try to embed the canvas it will fail (due to being Qt6 objects not Qt5 objects!). closes #21998
1 parent 8b3b315 commit 0c77b9a

File tree

6 files changed

+100
-10
lines changed

6 files changed

+100
-10
lines changed

lib/matplotlib/backends/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
22
# attribute here for backcompat.
3+
_QT_MODE = None

lib/matplotlib/backends/backend_qt5.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
from .. import backends
2+
3+
backends._QT_MODE = 5
4+
5+
16
from .backend_qt import (
27
backend_version, SPECIAL_KEYS,
38
# Public API

lib/matplotlib/backends/backend_qt5agg.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""
22
Render to qt from agg
33
"""
4+
from .. import backends
45

5-
from .backend_qtagg import (
6+
backends._QT_MODE = 5
7+
from .backend_qtagg import ( # noqa
68
_BackendQTAgg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT,
79
backend_version, FigureCanvasAgg, FigureCanvasQT
810
)

lib/matplotlib/backends/backend_qt5cairo.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from .. import backends
2+
3+
backends._QT_MODE = 5
14
from .backend_qtcairo import (
2-
_BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT)
5+
_BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT
6+
)
37

48

59
@_BackendQTCairo.export

lib/matplotlib/backends/qt_compat.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import matplotlib as mpl
2626
from matplotlib import _api
2727

28+
from . import _QT_MODE
2829

2930
QT_API_PYQT6 = "PyQt6"
3031
QT_API_PYSIDE6 = "PySide6"
@@ -117,12 +118,20 @@ def _isdeleted(obj):
117118
if QT_API in [QT_API_PYQT6, QT_API_PYQT5, QT_API_PYSIDE6, QT_API_PYSIDE2]:
118119
_setup_pyqt5plus()
119120
elif QT_API is None: # See above re: dict.__getitem__.
120-
_candidates = [
121-
(_setup_pyqt5plus, QT_API_PYQT6),
122-
(_setup_pyqt5plus, QT_API_PYSIDE6),
123-
(_setup_pyqt5plus, QT_API_PYQT5),
124-
(_setup_pyqt5plus, QT_API_PYSIDE2),
125-
]
121+
if _QT_MODE is None:
122+
_candidates = [
123+
(_setup_pyqt5plus, QT_API_PYQT6),
124+
(_setup_pyqt5plus, QT_API_PYSIDE6),
125+
(_setup_pyqt5plus, QT_API_PYQT5),
126+
(_setup_pyqt5plus, QT_API_PYSIDE2),
127+
]
128+
elif _QT_MODE == 5:
129+
_candidates = [
130+
(_setup_pyqt5plus, QT_API_PYQT5),
131+
(_setup_pyqt5plus, QT_API_PYSIDE2),
132+
]
133+
else:
134+
raise ValueError("Should never hit here")
126135
for _setup, QT_API in _candidates:
127136
try:
128137
_setup()

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
from matplotlib import _c_internal_utils
1818

1919

20+
def _yeet_to_subprocess(func):
21+
func_source = '\n'.join(
22+
textwrap.dedent(inspect.getsource(func)).split('\n')[1:]
23+
)
24+
return f"{func_source}\n{func.__name__}()"
25+
26+
2027
# Minimal smoke-testing of the backends for which the dependencies are
2128
# PyPI-installable on CI. They are not available for all tested Python
2229
# versions so we don't fail on missing backends.
@@ -258,6 +265,7 @@ def test_interactive_thread_safety(env):
258265

259266
def test_lazy_auto_backend_selection():
260267

268+
@_yeet_to_subprocess
261269
def _impl():
262270
import matplotlib
263271
import matplotlib.pyplot as plt
@@ -272,8 +280,69 @@ def _impl():
272280
assert isinstance(bk, str)
273281

274282
proc = subprocess.run(
275-
[sys.executable, "-c",
276-
textwrap.dedent(inspect.getsource(_impl)) + "\n_impl()"],
283+
[sys.executable, "-c", _impl],
284+
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
285+
timeout=_test_timeout, check=True,
286+
stdout=subprocess.PIPE, universal_newlines=True)
287+
288+
289+
def test_qt5backends_uses_qt5():
290+
291+
qt5_bindings = [
292+
dep for dep in ['PyQt5', 'pyside2']
293+
if not importlib.util.find_spec(dep)
294+
]
295+
qt6_bindings = [
296+
dep for dep in ['PyQt6', 'pyside6']
297+
if not importlib.util.find_spec(dep)
298+
]
299+
if not (len(qt5_bindings) > 0 and len(qt6_bindings) > 0):
300+
pytest.skip('need both QT6 and QT5 bindings')
301+
302+
@_yeet_to_subprocess
303+
def _implagg():
304+
import matplotlib.backends.backend_qt5agg
305+
import sys
306+
matplotlib.backends.backend_qt5agg
307+
308+
assert 'PyQt6' not in sys.modules
309+
assert 'pyside6' not in sys.modules
310+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
311+
312+
@_yeet_to_subprocess
313+
def _implcairo():
314+
import matplotlib.backends.backend_qt5cairo
315+
import sys
316+
matplotlib.backends.backend_qt5cairo
317+
318+
assert 'PyQt6' not in sys.modules
319+
assert 'pyside6' not in sys.modules
320+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
321+
322+
@_yeet_to_subprocess
323+
def _implcore():
324+
import matplotlib.backends.backend_qt5
325+
import sys
326+
matplotlib.backends.backend_qt5
327+
328+
assert 'PyQt6' not in sys.modules
329+
assert 'pyside6' not in sys.modules
330+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
331+
332+
subprocess.run(
333+
[sys.executable, "-c", _implagg],
334+
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
335+
timeout=_test_timeout, check=True,
336+
stdout=subprocess.PIPE, universal_newlines=True)
337+
338+
subprocess.run(
339+
[sys.executable, "-c", _implcairo],
340+
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
341+
timeout=_test_timeout, check=True,
342+
stdout=subprocess.PIPE, universal_newlines=True)
343+
344+
subprocess.run(
345+
[sys.executable, "-c", _implcore],
277346
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
278347
timeout=_test_timeout, check=True,
279348
stdout=subprocess.PIPE, universal_newlines=True)

0 commit comments

Comments
 (0)