8000 FIX: ensure that qt5agg and qt5cairo backends actually use qt5 · matplotlib/matplotlib@8a0483d · GitHub
[go: up one dir, main page]

Skip to content

Commit 8a0483d

Browse files
tacaswellQuLogic
andcommitted
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!). Additional changes to qt_compat that only matters if 1. rcparams['backend'] is set to qt5agg or qt5agg 2. QT_API env is not set 3. the user directly import matplotlib.backends.qt_compat This will likely only affect users who are using Matplotlib as an qt-shim implementation. closes #21998 Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
1 parent fc8863b commit 8a0483d

File tree

7 files changed

+102
-15
lines changed

7 files changed

+102
-15
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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from .backend_qt import (
1+
from .. import backends
2+
3+
backends._QT_MODE = 5
4+
5+
6+
from .backend_qt import ( # noqa
27
backend_version, SPECIAL_KEYS,
38
# Public API
49
cursord, _create_qApp, _BackendQT, TimerQT, MainWindow, FigureCanvasQT,

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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
from .backend_qtcairo import (
2-
_BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT)
1+
from .. import backends
2+
3+
backends._QT_MODE = 5
4+
from .backend_qtcairo import ( # noqa
5+
_BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT
6+
)
37

48

59
@_BackendQTCairo.export

lib/matplotlib/backends/qt_compat.py

Lines changed: 16 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"
@@ -66,6 +67,7 @@
6667
if QT_API_ENV in ["pyqt5", "pyside2"]:
6768
QT_API = _ETS[QT_API_ENV]
6869
else:
70+
_QT_MODE = 5 # noqa
6971
QT_API = None
7072
# A non-Qt backend was selected but we still got there (possible, e.g., when
7173
# fully manually embedding Matplotlib in a Qt app without using pyplot).
@@ -117,12 +119,20 @@ def _isdeleted(obj):
117119
if QT_API in [QT_API_PYQT6, QT_API_PYQT5, QT_API_PYSIDE6, QT_API_PYSIDE2]:
118120
_setup_pyqt5plus()
119121
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-
]
122+
if _QT_MODE is None:
123+
_candidates = [
124+
(_setup_pyqt5plus, QT_API_PYQT6),
125+
(_setup_pyqt5plus, QT_API_PYSIDE6),
126+
(_setup_pyqt5plus, QT_API_PYQT5),
127+
(_setup_pyqt5plus, QT_API_PYSIDE2),
128+
]
129+
elif _QT_MODE == 5:
130+
_candidates = [
131+
(_setup_pyqt5plus, QT_API_PYQT5),
132+
(_setup_pyqt5plus, QT_API_PYSIDE2),
133+
]
134+
else:
135+
raise ValueError("Should never hit here")
126136
for _setup, QT_API in _candidates:
127137
try:
128138
_setup()

lib/matplotlib/pyplot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ def _initial_switch_backend():
109109
# Just to be safe. Interactive mode can be turned on without
110110
# calling `plt.ion()` so register it again here.
111111
# This is safe because multiple calls to `install_repl_displayhook`
112-
# are no-ops and the registered function respect `mpl.is_interactive()`
113-
# to determine if they should trigger a draw.
112+
# are no-ops and the registered function respects `mpl.is_interactive()`
113+
# to determine if it should trigger a draw.
114114
install_repl_displayhook()
115115

116116

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77
import signal
88
import subprocess
99
import sys
10+
import textwrap
1011
import time
1112
import urllib.request
12-
import textwrap
1313

1414
import pytest
1515

1616
import matplotlib as mpl
1717
from matplotlib import _c_internal_utils
1818

1919

20+
def _run_function_in_subprocess(func):
21+
func_source = textwrap.dedent(inspect.getsource(func))
22+
func_source = func_source[func_source.index('\n')+1:] # Remove decorator
23+
return f"{func_source}\n{func.__name__}()"
24+
25+
2026
# Minimal smoke-testing of the backends for which the dependencies are
2127
# PyPI-installable on CI. They are not available for all tested Python
2228
# versions so we don't fail on missing backends.
@@ -258,6 +264,7 @@ def test_interactive_thread_safety(env):
258264

259265
def test_lazy_auto_backend_selection():
260266

267+
@_run_function_in_subprocess
261268
def _impl():
262269
import matplotlib
263270
import matplotlib.pyplot as plt
@@ -272,8 +279,66 @@ def _impl():
272279
assert isinstance(bk, str)
273280

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

0 commit comments

Comments
 (0)
0