|
30 | 30 | QT_API_PYSIDE2 = "PySide2" |
31 | 31 | QT_API_PYQTv2 = "PyQt4v2" |
32 | 32 | QT_API_PYSIDE = "PySide" |
33 | | -QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2). |
| 33 | +QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2). |
34 | 34 | QT_API_ENV = os.environ.get("QT_API") |
35 | 35 | if QT_API_ENV is not None: |
36 | 36 | QT_API_ENV = QT_API_ENV.lower() |
@@ -101,7 +101,8 @@ def _isdeleted(obj): return not shiboken6.isValid(obj) |
101 | 101 | elif QT_API == QT_API_PYSIDE2: |
102 | 102 | from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork, __version__ |
103 | 103 | import shiboken2 |
104 | | - def _isdeleted(obj): return not shiboken2.isValid(obj) |
| 104 | + def _isdeleted(obj): |
| 105 | + return not shiboken2.isValid(obj) |
105 | 106 | else: |
106 | 107 | raise AssertionError(f"Unexpected QT_API: {QT_API}") |
107 | 108 | _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName |
@@ -136,7 +137,6 @@ def _isdeleted(obj): return not shiboken2.isValid(obj) |
136 | 137 | "QT_MAC_WANTS_LAYER" not in os.environ): |
137 | 138 | os.environ["QT_MAC_WANTS_LAYER"] = "1" |
138 | 139 |
|
139 | | - |
140 | 140 | # These globals are only defined for backcompatibility purposes. |
141 | 141 | ETS = dict(pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) |
142 | 142 |
|
@@ -195,47 +195,116 @@ def _setDevicePixelRatio(obj, val): |
195 | 195 | obj.setDevicePixelRatio(val) |
196 | 196 |
|
197 | 197 |
|
198 | | -class _allow_interrupt: |
199 | | - def __init__(self, qApp, old_sigint_handler): |
200 | | - self.interrupted_qobject = qApp |
201 | | - self.old_fd = None |
202 | | - if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): |
203 | | - raise ValueError(f"Old SIGINT handler {old_sigint_handler}" |
204 | | - f" will not be overridden") |
205 | | - self.old_sigint_handler = old_sigint_handler |
206 | | - self.caught_args = None |
207 | | - |
208 | | - QAS = QtNetwork.QAbstractSocket |
209 | | - self.qt_socket = QAS(QAS.TcpSocket, qApp) |
210 | | - # Create a socket pair |
211 | | - self.wsock, self.rsock = socket.socketpair() |
212 | | - # Let Qt listen on the one end |
213 | | - self.qt_socket.setSocketDescriptor(self.rsock.fileno()) |
214 | | - self.wsock.setblocking(False) |
215 | | - self.qt_socket.readyRead.connect(self._readSignal) |
216 | | - |
217 | | - def __enter__(self): |
218 | | - signal.signal(signal.SIGINT, self._handle) |
219 | | - # And let Python write on the other end |
220 | | - self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) |
221 | | - |
222 | | - def __exit__(self, type, val, traceback): |
223 | | - signal.set_wakeup_fd(self.old_fd) |
224 | | - signal.signal(signal.SIGINT, self.old_sigint_handler) |
225 | | - |
226 | | - self.wsock.close() |
227 | | - self.rsock.close() |
228 | | - self.qt_socket.abort() |
229 | | - if self.caught_args is not None: |
230 | | - self.old_sigint_handler(*self.caught_args) |
231 | | - |
232 | | - def _readSignal(self): |
233 | | - # Read the written byte. |
234 | | - # Note: readyRead is blocked from |
235 | | - # occurring again until readData() was called, so call it, |
236 | | - # even if you don't need the value. |
237 | | - self.qt_socket.readData(1) |
238 | | - |
239 | | - def _handle(self, *args): |
240 | | - self.caught_args = args |
241 | | - self.interrupted_qobject.quit() |
| 198 | +@contextlib.contextmanager |
| 199 | +def _maybe_allow_interrupt(qapp): |
| 200 | + ''' |
| 201 | + This manager allows to terminate a plot by sending a SIGINT. It is |
| 202 | + necessary because the running Qt backend prevents Python interpreter to |
| 203 | + run and process signals (i.e., to raise KeyboardInterrupt exception). To |
| 204 | + solve this one needs to somehow wake up the interpreter and make it close |
| 205 | + the plot window. We do this by using the signal.set_wakeup_fd() function |
| 206 | + which organizes a write of the signal number into a socketpair connected |
| 207 | + to the QSocketNotifier (since it is part of the Qt backend, it can react |
| 208 | + to that write event). Afterwards, the Qt handler empties the socketpair |
| 209 | + by a recv() command to re-arm it (we need this if a signal different from |
| 210 | + SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If |
| 211 | + the SIGINT was caught indeed, after exiting the on_signal() function the |
| 212 | + interpreter reacts to the SIGINT according to the handle() function which |
| 213 | + had been set up by a signal.signal() call: it causes the qt_object to |
| 214 | + exit by calling its quit() method. Finally, we call the old SIGINT |
| 215 | + handler with the same arguments that were given to our custom handle() |
| 216 | + handler. |
| 217 | +
|
| 218 | + We do this only if the old handler for SIGINT was not None, which means |
| 219 | + that a non-python handler was installed, i.e. in Julia, and not SIG_IGN |
| 220 | + which means we should ignore the interrupts. |
| 221 | + ''' |
| 222 | + old_sigint_handler = signal.getsignal(signal.SIGINT) |
| 223 | + handler_args = None |
| 224 | + skip = False |
| 225 | + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): |
| 226 | + skip = True |
| 227 | + else: |
| 228 | + wsock, rsock = socket.socketpair() |
| 229 | + wsock.setblocking(False) |
| 230 | + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) |
| 231 | + sn = QtCore.QSocketNotifier(rsock.fileno(), |
| 232 | + QtCore.QSocketNotifier.Read) |
| 233 | + |
| 234 | + @sn.activated.connect |
| 235 | + def on_signal(*args): |
| 236 | + rsock.recv( |
| 237 | + sys.getsizeof(int)) # clear the socket to re-arm the notifier |
| 238 | + |
| 239 | + def handle(*args): |
| 240 | + nonlocal handler_args |
| 241 | + handler_args = args |
| 242 | + qapp.quit() |
| 243 | + |
| 244 | + signal.signal(signal.SIGINT, handle) |
| 245 | + try: |
| 246 | + yield |
| 247 | + finally: |
| 248 | + if not skip: |
| 249 | + wsock.close() |
| 250 | + rsock.close() |
| 251 | + sn.setEnabled(False) |
| 252 | + signal.set_wakeup_fd(old_wakeup_fd) |
| 253 | + signal.signal(signal.SIGINT, old_sigint_handler) |
| 254 | + if handler_args is not None: |
| 255 | + old_sigint_handler(handler_args) |
| 256 | + |
| 257 | +# class _maybe_allow_interrupt: |
| 258 | +# |
| 259 | +# def __init__(self, qt_object): |
| 260 | +# self.interrupted_qobject = qt_object |
| 261 | +# self.old_fd = None |
| 262 | +# self.caught_args = None |
| 263 | +# |
| 264 | +# self.skip = False |
| 265 | +# self.old_sigint_handler = signal.getsignal(signal.SIGINT) |
| 266 | +# if self.old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): |
| 267 | +# self.skip = True |
| 268 | +# return |
| 269 | +# |
| 270 | +# QAS = QtNetwork.QAbstractSocket |
| 271 | +# self.qt_socket = QAS(QAS.TcpSocket, qt_object) |
| 272 | +# # Create a socket pair |
| 273 | +# self.wsock, self.rsock = socket.socketpair() |
| 274 | +# # Let Qt listen on the one end |
| 275 | +# self.qt_socket.setSocketDescriptor(self.rsock.fileno()) |
| 276 | +# self.wsock.setblocking(False) |
| 277 | +# self.qt_socket.readyRead.connect(self._readSignal) |
| 278 | +# |
| 279 | +# def __enter__(self): |
| 280 | +# if self.skip: |
| 281 | +# return |
| 282 | +# |
| 283 | +# signal.signal(signal.SIGINT, self._handle) |
| 284 | +# # And let Python write on the other end |
| 285 | +# self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) |
| 286 | +# |
| 287 | +# def __exit__(self, type, val, traceback): |
| 288 | +# if self.skip: |
| 289 | +# return |
| 290 | +# |
| 291 | +# signal.set_wakeup_fd(self.old_fd) |
| 292 | +# signal.signal(signal.SIGINT, self.old_sigint_handler) |
| 293 | +# |
| 294 | +# self.wsock.close() |
| 295 | +# self.rsock.close() |
| 296 | +# self.qt_socket.abort() |
| 297 | +# if self.caught_args is not None: |
| 298 | +# self.old_sigint_handler(*self.caught_args) |
| 299 | +# |
| 300 | +# def _readSignal(self): |
| 301 | +# # Read the written byte to re-arm the socket if a signal different |
| 302 | +# # from SIGINT was caught. |
| 303 | +# # Since a custom handler was installed for SIGINT, KeyboardInterrupt |
| 304 | +# # is not raised here. |
| 305 | +# self.qt_socket.readData(1) |
| 306 | +# |
| 307 | +# def _handle(self, *args): |
| 308 | +# self.caught_args = args # save the caught args to call the old |
| 309 | +# # SIGINT handler afterwards |
| 310 | +# self.interrupted_qobject.quit() |
0 commit comments