Repository navigation
Expand file tree
/
Copy pathqt_compat.py
More file actions
354 lines (318 loc) · 12.9 KB
/
Copy pathqt_compat.py
File metadata and controls
354 lines (318 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
"""
Qt binding and backend selector.
The selection logic is as follows:
- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
(checked in that order), use it;
- otherwise, if the QT_API environment variable (used by Enthought) is set, use
it to determine which binding to use (but do not change the backend based on
it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4",
then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
- otherwise, use whatever the rcParams indicate.
Support for PyQt4 is deprecated.
"""
import contextlib
from distutils.version import LooseVersion
import os
import platform
import sys
import signal
import socket
import matplotlib as mpl
from matplotlib import _api
QT_API_PYQT5 = "PyQt5"
QT_API_PYSIDE2 = "PySide2"
QT_API_PYQTv2 = "PyQt4v2"
QT_API_PYSIDE = "PySide"
QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2).
QT_API_ENV = os.environ.get("QT_API")
if QT_API_ENV is not None:
QT_API_ENV = QT_API_ENV.lower()
# Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1.
# (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
_ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
"pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
None: None}
# First, check if anything is already imported. Use ``sys.modules.get(name)``
# rather than ``name in sys.modules`` as entries can also have been explicitly
# set to None.
if sys.modules.get("PyQt5.QtCore"):
QT_API = QT_API_PYQT5
elif sys.modules.get("PySide2.QtCore"):
QT_API = QT_API_PYSIDE2
elif sys.modules.get("PyQt4.QtCore"):
QT_API = QT_API_PYQTv2
elif sys.modules.get("PySide.QtCore"):
QT_API = QT_API_PYSIDE
# Otherwise, check the QT_API environment variable (from Enthought). This can
# only override the binding, not the backend (in other words, we check that the
# requested backend actually matches). Use dict.__getitem__ to avoid
# triggering backend resolution (which can result in a partially but
# incompletely imported backend_qt5).
elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]:
if QT_API_ENV in ["pyqt5", "pyside2"]:
QT_API = _ETS[QT_API_ENV]
else:
QT_API = None
elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt4Agg", "Qt4Cairo"]:
if QT_API_ENV in ["pyqt4", "pyside"]:
QT_API = _ETS[QT_API_ENV]
else:
QT_API = None
# A non-Qt backend was selected but we still got there (possible, e.g., when
# fully manually embedding Matplotlib in a Qt app without using pyplot).
else:
try:
QT_API = _ETS[QT_API_ENV]
except KeyError as err:
raise RuntimeError(
"The environment variable QT_API has the unrecognized value {!r};"
"valid values are 'pyqt5', 'pyside2', 'pyqt', and "
"'pyside'") from err
def _setup_pyqt5():
global QtCore, QtGui, QtWidgets, QtNetwork, __version__, is_pyqt5, \
_isdeleted, _getSaveFileName
if QT_API == QT_API_PYQT5:
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork
import sip
__version__ = QtCore.PYQT_VERSION_STR
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
QtCore.Property = QtCore.pyqtProperty
_isdeleted = sip.isdeleted
elif QT_API == QT_API_PYSIDE2:
from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork, __version__
import shiboken2
def _isdeleted(obj):
return not shiboken2.isValid(obj)
else:
raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
_getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
@_api.deprecated("3.3", alternative="QtCore.qVersion()")
def is_pyqt5():
return True
def _setup_pyqt4():
global QtCore, QtGui, QtWidgets, QtNetwork, __version__, is_pyqt5, \
_isdeleted, _getSaveFileName
def _setup_pyqt4_internal(api):
global QtCore, QtGui, QtWidgets, QtNetwork, \
__version__, is_pyqt5, _isdeleted, _getSaveFileName
# List of incompatible APIs:
# http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
_sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
"QUrl", "QVariant"]
try:
import sip
except ImportError:
pass
else:
for _sip_api in _sip_apis:
try:
sip.setapi(_sip_api, api)
except (AttributeError, ValueError):
pass
from PyQt4 import QtCore, QtGui, QtNetwork
import sip # Always succeeds *after* importing PyQt4.
__version__ = QtCore.PYQT_VERSION_STR
# PyQt 4.6 introduced getSaveFileNameAndFilter:
# https://riverbankcomputing.com/news/pyqt-46
if __version__ < LooseVersion("4.6"):
raise ImportError("PyQt<4.6 is not supported")
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
QtCore.Property = QtCore.pyqtProperty
_isdeleted = sip.isdeleted
_getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter
if QT_API == QT_API_PYQTv2:
_setup_pyqt4_internal(api=2)
elif QT_API == QT_API_PYSIDE:
from PySide import (
QtCore, QtGui, QtNetwork, __version__, __version_info__
)
import shiboken
# PySide 1.0.3 fixed the following:
# https://srinikom.github.io/pyside-bz-archive/809.html
if __version_info__ < (1, 0, 3):
raise ImportError("PySide<1.0.3 is not supported")
def _isdeleted(obj):
return not shiboken.isValid(obj)
_getSaveFileName = QtGui.QFileDialog.getSaveFileName
elif QT_API == QT_API_PYQT:
_setup_pyqt4_internal(api=1)
else:
raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
QtWidgets = QtGui
@_api.deprecated("3.3", alternative="QtCore.qVersion()")
def is_pyqt5():
return False
if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
_setup_pyqt5()
elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
_setup_pyqt4()
elif QT_API is None: # See above re: dict.__getitem__.
if dict.__getitem__(mpl.rcParams, "backend") == "Qt4Agg":
_candidates = [(_setup_pyqt4, QT_API_PYQTv2),
(_setup_pyqt4, QT_API_PYSIDE),
(_setup_pyqt4, QT_API_PYQT),
(_setup_pyqt5, QT_API_PYQT5),
(_setup_pyqt5, QT_API_PYSIDE2)]
else:
_candidates = [(_setup_pyqt5, QT_API_PYQT5),
(_setup_pyqt5, QT_API_PYSIDE2),
(_setup_pyqt4, QT_API_PYQTv2),
(_setup_pyqt4, QT_API_PYSIDE),
(_setup_pyqt4, QT_API_PYQT)]
for _setup, QT_API in _candidates:
try:
_setup()
except ImportError:
continue
break
else:
raise ImportError("Failed to import any qt binding")
else: # We should not get there.
raise AssertionError("Unexpected QT_API: {}".format(QT_API))
# Fixes issues with Big Sur
# https://bugreports.qt.io/browse/QTBUG-87014, fixed in qt 5.15.2
if (sys.platform == 'darwin' and
LooseVersion(platform.mac_ver()[0]) >= LooseVersion("10.16") and
LooseVersion(QtCore.qVersion()) < LooseVersion("5.15.2") and
"QT_MAC_WANTS_LAYER" not in os.environ):
os.environ["QT_MAC_WANTS_LAYER"] = "1"
# These globals are only defined for backcompatibility purposes.
ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
QT_RC_MAJOR_VERSION = int(QtCore.qVersion().split(".")[0])
if QT_RC_MAJOR_VERSION == 4:
_api.warn_deprecated("3.3", name="support for Qt4")
def _devicePixelRatioF(obj):
"""
Return obj.devicePixelRatioF() with graceful fallback for older Qt.
This can be replaced by the direct call when we require Qt>=5.6.
"""
try:
# Not available on Qt<5.6
return obj.devicePixelRatioF() or 1
except AttributeError:
pass
try:
# Not available on Qt4 or some older Qt5.
# self.devicePixelRatio() returns 0 in rare cases
return obj.devicePixelRatio() or 1
except AttributeError:
return 1
def _setDevicePixelRatio(obj, val):
"""
Call obj.setDevicePixelRatio(val) with graceful fallback for older Qt.
This can be replaced by the direct call when we require Qt>=5.6.
"""
if hasattr(obj, 'setDevicePixelRatio'):
# Not available on Qt4 or some older Qt5.
obj.setDevicePixelRatio(val)
@contextlib.contextmanager
def _maybe_allow_interrupt(qapp):
"""
This manager allows to terminate a plot by sending a SIGINT. It is
necessary because the running Qt backend prevents Python interpreter to
run and process signals (i.e., to raise KeyboardInterrupt exception). To
solve this one needs to somehow wake up the interpreter and make it close
the plot window. We do this by using the signal.set_wakeup_fd() function
which organizes a write of the signal number into a socketpair connected
to the QSocketNotifier (since it is part of the Qt backend, it can react
to that write event). Afterwards, the Qt handler empties the socketpair
by a recv() command to re-arm it (we need this if a signal different from
SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If
the SIGINT was caught indeed, after exiting the on_signal() function the
interpreter reacts to the SIGINT according to the handle() function which
had been set up by a signal.signal() call: it causes the qt_object to
exit by calling its quit() method. Finally, we call the old SIGINT
handler with the same arguments that were given to our custom handle()
handler.
We do this only if the old handler for SIGINT was not None, which means
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
which means we should ignore the interrupts.
"""
old_sigint_handler = signal.getsignal(signal.SIGINT)
handler_args = None
skip = False
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
skip = True
else:
wsock, rsock = socket.socketpair()
wsock.setblocking(False)
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
sn = QtCore.QSocketNotifier(rsock.fileno(),
QtCore.QSocketNotifier.Read)
@sn.activated.connect
def on_signal(*args):
rsock.recv(
sys.getsizeof(int)) # clear the socket to re-arm the notifier
def handle(*args):
nonlocal handler_args
handler_args = args
qapp.quit()
signal.signal(signal.SIGINT, handle)
try:
yield
finally:
if not skip:
wsock.close()
rsock.close()
sn.setEnabled(False)
signal.set_wakeup_fd(old_wakeup_fd)
signal.signal(signal.SIGINT, old_sigint_handler)
if handler_args is not None:
old_sigint_handler(handler_args)
# class _maybe_allow_interrupt:
#
# def __init__(self, qt_object):
# self.interrupted_qobject = qt_object
# self.old_fd = None
# self.caught_args = None
#
# self.skip = False
# self.old_sigint_handler = signal.getsignal(signal.SIGINT)
# if self.old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
# self.skip = True
# return
#
# QAS = QtNetwork.QAbstractSocket
# self.qt_socket = QAS(QAS.TcpSocket, qt_object)
# # Create a socket pair
# self.wsock, self.rsock = socket.socketpair()
# # Let Qt listen on the one end
# self.qt_socket.setSocketDescriptor(self.rsock.fileno())
# self.wsock.setblocking(False)
# self.qt_socket.readyRead.connect(self._readSignal)
#
# def __enter__(self):
# if self.skip:
# return
#
# signal.signal(signal.SIGINT, self._handle)
# # And let Python write on the other end
# self.old_fd = signal.set_wakeup_fd(self.wsock.fileno())
#
# def __exit__(self, type, val, traceback):
# if self.skip:
# return
#
# signal.set_wakeup_fd(self.old_fd)
# signal.signal(signal.SIGINT, self.old_sigint_handler)
#
# self.wsock.close()
# self.rsock.close()
# self.qt_socket.abort()
# if self.caught_args is not None:
# self.old_sigint_handler(*self.caught_args)
#
# def _readSignal(self):
# # Read the written byte to re-arm the socket if a signal different
# # from SIGINT was caught.
# # Since a custom handler was installed for SIGINT, KeyboardInterrupt
# # is not raised here.
# self.qt_socket.readData(1)
#
# def _handle(self, *args):
# self.caught_args = args # save the caught args to call the old
# # SIGINT handler afterwards
# self.interrupted_qobject.quit()