Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb23b010

Browse files
committed
Factor out common parts of qt and macos interrupt handling.
Note that we don't actually need to disable the QSocketNotifier at theend, just letting it go out of scope should be sufficient as itsdestructor also does that (see qsocketnotifier.cpp).
1 parentea66786 commitb23b010

File tree

4 files changed

+115
-137
lines changed

4 files changed

+115
-137
lines changed

‎lib/matplotlib/backends/backend_macosx.py

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
importcontextlib
21
importos
3-
importsignal
4-
importsocket
52

63
importmatplotlibasmpl
74
frommatplotlibimport_api,cbook
@@ -18,6 +15,12 @@ class TimerMac(_macosx.Timer, TimerBase):
1815
# completely implemented at the C-level (in _macosx.Timer)
1916

2017

18+
def_allow_interrupt_macos():
19+
"""A context manager that allows terminating a plot by sending a SIGINT."""
20+
returncbook._allow_interrupt(
21+
lambdarsock:_macosx.wake_on_fd_write(rsock.fileno()),_macosx.stop)
22+
23+
2124
classFigureCanvasMac(FigureCanvasAgg,_macosx.FigureCanvas,FigureCanvasBase):
2225
# docstring inherited
2326

@@ -109,10 +112,9 @@ def resize(self, width, height):
109112

110113
defstart_event_loop(self,timeout=0):
111114
# docstring inherited
112-
with_maybe_allow_interrupt():
113-
# Call the objc implementation of the event loop after
114-
# setting up the interrupt handling
115-
self._start_event_loop(timeout=timeout)
115+
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
116+
with_allow_interrupt_macos():
117+
self._start_event_loop(timeout=timeout)# Forward to ObjC implementation.
116118

117119

118120
classNavigationToolbar2Mac(_macosx.NavigationToolbar2,NavigationToolbar2):
@@ -177,9 +179,7 @@ def destroy(self):
177179
@classmethod
178180
defstart_main_loop(cls):
179181
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
180-
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
181-
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
182-
with_maybe_allow_interrupt():
182+
with_allow_interrupt_macos():
183183
_macosx.show()
184184

185185
defshow(self):
@@ -190,45 +190,6 @@ def show(self):
190190
self._raise()
191191

192192

193-
@contextlib.contextmanager
194-
def_maybe_allow_interrupt():
195-
"""
196-
This manager allows to terminate a plot by sending a SIGINT. It is
197-
necessary because the running backend prevents Python interpreter to
198-
run and process signals (i.e., to raise KeyboardInterrupt exception). To
199-
solve this one needs to somehow wake up the interpreter and make it close
200-
the plot window. The implementation is taken from qt_compat, see that
201-
docstring for a more detailed description.
202-
"""
203-
old_sigint_handler=signal.getsignal(signal.SIGINT)
204-
ifold_sigint_handlerin (None,signal.SIG_IGN,signal.SIG_DFL):
205-
yield
206-
return
207-
208-
handler_args=None
209-
wsock,rsock=socket.socketpair()
210-
wsock.setblocking(False)
211-
rsock.setblocking(False)
212-
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
213-
_macosx.wake_on_fd_write(rsock.fileno())
214-
215-
defhandle(*args):
216-
nonlocalhandler_args
217-
handler_args=args
218-
_macosx.stop()
219-
220-
signal.signal(signal.SIGINT,handle)
221-
try:
222-
yield
223-
finally:
224-
wsock.close()
225-
rsock.close()
226-
signal.set_wakeup_fd(old_wakeup_fd)
227-
signal.signal(signal.SIGINT,old_sigint_handler)
228-
ifhandler_argsisnotNone:
229-
old_sigint_handler(*handler_args)
230-
231-
232193
@_Backend.export
233194
class_BackendMac(_Backend):
234195
FigureCanvas=FigureCanvasMac

‎lib/matplotlib/backends/backend_qt.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
importmatplotlib.backends.qt_editor.figureoptionsasfigureoptions
1414
from .importqt_compat
1515
from .qt_compatimport (
16-
QtCore,QtGui,QtWidgets,__version__,QT_API,
17-
_to_int,_isdeleted,_maybe_allow_interrupt
18-
)
16+
QtCore,QtGui,QtWidgets,__version__,QT_API,_to_int,_isdeleted)
1917

2018

2119
# SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name
@@ -148,6 +146,38 @@ def _create_qApp():
148146
returnapp
149147

150148

149+
def_allow_interrupt_qt(qapp_or_eventloop):
150+
"""A context manager that allows terminating a plot by sending a SIGINT."""
151+
152+
# Use QSocketNotifier to read the socketpair while the Qt event loop runs.
153+
154+
defprepare_notifier(rsock):
155+
sn=QtCore.QSocketNotifier(rsock.fileno(),QtCore.QSocketNotifier.Type.Read)
156+
157+
@sn.activated.connect
158+
def_may_clear_sock():
159+
# Running a Python function on socket activation gives the interpreter a
160+
# chance to handle the signal in Python land. We also need to drain the
161+
# socket with recv() to re-arm it, because it will be written to as part of
162+
# the wakeup. (We need this in case set_wakeup_fd catches a signal other
163+
# than SIGINT and we shall continue waiting.)
164+
try:
165+
rsock.recv(1)
166+
exceptBlockingIOError:
167+
# This may occasionally fire too soon or more than once on Windows, so
168+
# be forgiving about reading an empty socket.
169+
pass
170+
171+
returnsn# Actually keep the notifier alive.
172+
173+
defhandle_sigint():
174+
ifhasattr(qapp_or_eventloop,'closeAllWindows'):
175+
qapp_or_eventloop.closeAllWindows()
176+
qapp_or_eventloop.quit()
177+
178+
returnmpl.cbook._allow_interrupt(prepare_notifier,handle_sigint)
179+
180+
151181
classTimerQT(TimerBase):
152182
"""Subclass of `.TimerBase` using QTimer events."""
153183

@@ -417,7 +447,7 @@ def start_event_loop(self, timeout=0):
417447
iftimeout>0:
418448
_=QtCore.QTimer.singleShot(int(timeout*1000),event_loop.quit)
419449

420-
with_maybe_allow_interrupt(event_loop):
450+
with_allow_interrupt_qt(event_loop):
421451
qt_compat._exec(event_loop)
422452

423453
defstop_event_loop(self,event=None):
@@ -598,7 +628,7 @@ def resize(self, width, height):
598628
defstart_main_loop(cls):
599629
qapp=QtWidgets.QApplication.instance()
600630
ifqapp:
601-
with_maybe_allow_interrupt(qapp):
631+
with_allow_interrupt_qt(qapp):
602632
qt_compat._exec(qapp)
603633

604634
defshow(self):

‎lib/matplotlib/backends/qt_compat.py

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
importos
1414
importplatform
1515
importsys
16-
importsignal
17-
importsocket
18-
importcontextlib
1916

2017
frompackaging.versionimportparseasparse_version
2118

@@ -160,73 +157,3 @@ def _isdeleted(obj):
160157
def_exec(obj):
161158
# exec on PyQt6, exec_ elsewhere.
162159
obj.exec()ifhasattr(obj,"exec")elseobj.exec_()
163-
164-
165-
@contextlib.contextmanager
166-
def_maybe_allow_interrupt(qapp_or_eventloop):
167-
"""
168-
This manager allows to terminate a plot by sending a SIGINT. It is
169-
necessary because the running Qt backend prevents Python interpreter to
170-
run and process signals (i.e., to raise KeyboardInterrupt exception). To
171-
solve this one needs to somehow wake up the interpreter and make it close
172-
the plot window. We do this by using the signal.set_wakeup_fd() function
173-
which organizes a write of the signal number into a socketpair connected
174-
to the QSocketNotifier (since it is part of the Qt backend, it can react
175-
to that write event). Afterwards, the Qt handler empties the socketpair
176-
by a recv() command to re-arm it (we need this if a signal different from
177-
SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If
178-
the SIGINT was caught indeed, after exiting the on_signal() function the
179-
interpreter reacts to the SIGINT according to the handle() function which
180-
had been set up by a signal.signal() call: it causes the qt_object to
181-
exit by calling its quit() method. Finally, we call the old SIGINT
182-
handler with the same arguments that were given to our custom handle()
183-
handler.
184-
185-
We do this only if the old handler for SIGINT was not None, which means
186-
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
187-
which means we should ignore the interrupts.
188-
"""
189-
190-
old_sigint_handler=signal.getsignal(signal.SIGINT)
191-
ifold_sigint_handlerin (None,signal.SIG_IGN,signal.SIG_DFL):
192-
yield
193-
return
194-
195-
handler_args=None
196-
wsock,rsock=socket.socketpair()
197-
wsock.setblocking(False)
198-
rsock.setblocking(False)
199-
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
200-
sn=QtCore.QSocketNotifier(rsock.fileno(),QtCore.QSocketNotifier.Type.Read)
201-
202-
# We do not actually care about this value other than running some Python code to
203-
# ensure that the interpreter has a chance to handle the signal in Python land. We
204-
# also need to drain the socket because it will be written to as part of the wakeup!
205-
# There are some cases where this may fire too soon / more than once on Windows so
206-
# we should be forgiving about reading an empty socket.
207-
# Clear the socket to re-arm the notifier.
208-
@sn.activated.connect
209-
def_may_clear_sock(*args):
210-
try:
211-
rsock.recv(1)
212-
exceptBlockingIOError:
213-
pass
214-
215-
defhandle(*args):
216-
nonlocalhandler_args
217-
handler_args=args
218-
ifhasattr(qapp_or_eventloop,'closeAllWindows'):
219-
qapp_or_eventloop.closeAllWindows()
220-
qapp_or_eventloop.quit()
221-
222-
signal.signal(signal.SIGINT,handle)
223-
try:
224-
yield
225-
finally:
226-
wsock.close()
227-
rsock.close()
228-
sn.setEnabled(False)
229-
signal.set_wakeup_fd(old_wakeup_fd)
230-
signal.signal(signal.SIGINT,old_sigint_handler)
231-
ifhandler_argsisnotNone:
232-
old_sigint_handler(*handler_args)

‎lib/matplotlib/cbook.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
importos
1515
frompathlibimportPath
1616
importshlex
17+
importsignal
18+
importsocket
1719
importsubprocess
1820
importsys
1921
importtime
@@ -82,6 +84,64 @@ def _get_running_interactive_framework():
8284
returnNone
8385

8486

87+
@contextlib.contextmanager
88+
def_allow_interrupt(prepare_notifier,handle_sigint):
89+
"""
90+
A context manager that allows terminating a plot by sending a SIGINT. It
91+
is necessary because the running backend prevents Python interpreter to
92+
run and process signals (i.e., to raise KeyboardInterrupt exception). To
93+
solve this one needs to somehow wake up the interpreter and make it close
94+
the plot window. We do this by using the signal.set_wakeup_fd() function
95+
which organizes a write of the signal number into a socketpair. A
96+
backend-specific function, *prepare_notifier*, arranges to listen to the
97+
pair's read socket while the event loop is running. (If it returns a
98+
notifier object, that object is kept alive while the context manager runs.)
99+
100+
If SIGINT was indeed caught, after exiting the on_signal() function the
101+
interpreter reacts to the signal according to the handler function which
102+
had been set up by a signal.signal() call; here, we arrange to call the
103+
backend-specific *handle_sigint* function. Finally, we call the old SIGINT
104+
handler with the same arguments that were given to our custom handler.
105+
106+
We do this only if the old handler for SIGINT was not None, which means
107+
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
108+
which means we should ignore the interrupts.
109+
110+
Parameters
111+
----------
112+
prepare_notifier : Callable[[socket.socket], object]
113+
handle_sigint : Callable[[], object]
114+
"""
115+
116+
old_sigint_handler=signal.getsignal(signal.SIGINT)
117+
ifold_sigint_handlerin (None,signal.SIG_IGN,signal.SIG_DFL):
118+
yield
119+
return
120+
121+
handler_args=None
122+
wsock,rsock=socket.socketpair()
123+
wsock.setblocking(False)
124+
rsock.setblocking(False)
125+
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
126+
notifier=prepare_notifier(rsock)
127+
128+
defsave_args_and_handle_sigint(*args):
129+
nonlocalhandler_args
130+
handler_args=args
131+
handle_sigint()
132+
133+
signal.signal(signal.SIGINT,save_args_and_handle_sigint)
134+
try:
135+
yield
136+
finally:
137+
wsock.close()
138+
rsock.close()
139+
signal.set_wakeup_fd(old_wakeup_fd)
140+
signal.signal(signal.SIGINT,old_sigint_handler)
141+
ifhandler_argsisnotNone:
142+
old_sigint_handler(*handler_args)
143+
144+
85145
def_exception_printer(exc):
86146
if_get_running_interactive_framework()in ["headless",None]:
87147
raiseexc
@@ -242,19 +302,19 @@ def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing):
242302
if_is_finalizing():
243303
# Weakrefs can't be properly torn down at that point anymore.
244304
return
245-
forsignal,proxy_to_cidinlist(self._func_cid_map.items()):
305+
forsig,proxy_to_cidinlist(self._func_cid_map.items()):
246306
cid=proxy_to_cid.pop(proxy,None)
247307
ifcidisnotNone:
248-
delself.callbacks[signal][cid]
308+
delself.callbacks[sig][cid]
249309
self._pickled_cids.discard(cid)
250310
break
251311
else:
252312
# Not found
253313
return
254314
# Clean up empty dicts
255-
iflen(self.callbacks[signal])==0:
256-
delself.callbacks[signal]
257-
delself._func_cid_map[signal]
315+
iflen(self.callbacks[sig])==0:
316+
delself.callbacks[sig]
317+
delself._func_cid_map[sig]
258318

259319
defdisconnect(self,cid):
260320
"""
@@ -264,23 +324,23 @@ def disconnect(self, cid):
264324
"""
265325
self._pickled_cids.discard(cid)
266326
# Clean up callbacks
267-
forsignal,cid_to_proxyinlist(self.callbacks.items()):
327+
forsig,cid_to_proxyinlist(self.callbacks.items()):
268328
proxy=cid_to_proxy.pop(cid,None)
269329
ifproxyisnotNone:
270330
break
271331
else:
272332
# Not found
273333
return
274334

275-
proxy_to_cid=self._func_cid_map[signal]
335+
proxy_to_cid=self._func_cid_map[sig]
276336
forcurrent_proxy,current_cidinlist(proxy_to_cid.items()):
277337
ifcurrent_cid==cid:
278338
assertproxyiscurrent_proxy
279339
delproxy_to_cid[current_proxy]
280340
# Clean up empty dicts
281-
iflen(self.callbacks[signal])==0:
282-
delself.callbacks[signal]
283-
delself._func_cid_map[signal]
341+
iflen(self.callbacks[sig])==0:
342+
delself.callbacks[sig]
343+
delself._func_cid_map[sig]
284344

285345
defprocess(self,s,*args,**kwargs):
286346
"""

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp