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

Commitc756301

Browse files
committed
Fix support for Ctrl-C on the macosx backend.
Support and tests are largely copy-pasted from the qt implementation(qt_compat._maybe_allow_interrupt), the main difference being thatwhat we need from QSocketNotifier, as well as the equivalent forQApplication.quit(), are reimplemented in ObjC.qt_compat._maybe_allow_interrupt is also slightly cleaned up by movingout the "do-nothing" case (`old_sigint_handler in (None, SIG_IGN, SIG_DFL)`)and dedenting the rest, instead of keeping track of whether signals wereactually manipulated via a `skip` variable.Factoring out the common parts of _maybe_allow_interrupt and of thetests is left as a follow-up.(Test e.g. with`MPLBACKEND=macosx python -c "from pylab import *; plot(); show()"`followed by Ctrl-C.)
1 parentffd3b12 commitc756301

File tree

6 files changed

+266
-69
lines changed

6 files changed

+266
-69
lines changed

‎lib/matplotlib/backends/backend_macosx.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
importos
2+
importsignal
3+
importsocket
24

35
importmatplotlibasmpl
46
frommatplotlibimport_api,cbook
@@ -164,7 +166,37 @@ def destroy(self):
164166

165167
@classmethod
166168
defstart_main_loop(cls):
167-
_macosx.show()
169+
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
170+
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
171+
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
172+
173+
old_sigint_handler=signal.getsignal(signal.SIGINT)
174+
ifold_sigint_handlerin (None,signal.SIG_IGN,signal.SIG_DFL):
175+
_macosx.show()
176+
return
177+
178+
handler_args=None
179+
wsock,rsock=socket.socketpair()
180+
wsock.setblocking(False)
181+
rsock.setblocking(False)
182+
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
183+
_macosx.wake_on_fd_write(rsock.fileno())
184+
185+
defhandle(*args):
186+
nonlocalhandler_args
187+
handler_args=args
188+
_macosx.stop()
189+
190+
signal.signal(signal.SIGINT,handle)
191+
try:
192+
_macosx.show()
193+
finally:
194+
wsock.close()
195+
rsock.close()
196+
signal.set_wakeup_fd(old_wakeup_fd)
197+
signal.signal(signal.SIGINT,old_sigint_handler)
198+
ifhandler_argsisnotNone:
199+
old_sigint_handler(*handler_args)
168200

169201
defshow(self):
170202
ifnotself._shown:

‎lib/matplotlib/backends/qt_compat.py

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -198,48 +198,46 @@ def _maybe_allow_interrupt(qapp):
198198
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
199199
which means we should ignore the interrupts.
200200
"""
201+
201202
old_sigint_handler=signal.getsignal(signal.SIGINT)
202-
handler_args=None
203-
skip=False
204203
ifold_sigint_handlerin (None,signal.SIG_IGN,signal.SIG_DFL):
205-
skip=True
206-
else:
207-
wsock,rsock=socket.socketpair()
208-
wsock.setblocking(False)
209-
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
210-
sn=QtCore.QSocketNotifier(
211-
rsock.fileno(),_enum('QtCore.QSocketNotifier.Type').Read
212-
)
204+
yield
205+
return
206+
207+
handler_args=None
208+
wsock,rsock=socket.socketpair()
209+
wsock.setblocking(False)
210+
rsock.setblocking(False)
211+
old_wakeup_fd=signal.set_wakeup_fd(wsock.fileno())
212+
sn=QtCore.QSocketNotifier(
213+
rsock.fileno(),_enum('QtCore.QSocketNotifier.Type').Read)
214+
215+
# We do not actually care about this value other than running some Python code to
216+
# ensure that the interpreter has a chance to handle the signal in Python land. We
217+
# also need to drain the socket because it will be written to as part of the wakeup!
218+
# There are some cases where this may fire too soon / more than once on Windows so
219+
# we should be forgiving about reading an empty socket.
220+
# Clear the socket to re-arm the notifier.
221+
@sn.activated.connect
222+
def_may_clear_sock(*args):
223+
try:
224+
rsock.recv(1)
225+
exceptBlockingIOError:
226+
pass
227+
228+
defhandle(*args):
229+
nonlocalhandler_args
230+
handler_args=args
231+
qapp.quit()
213232

214-
# We do not actually care about this value other than running some
215-
# Python code to ensure that the interpreter has a chance to handle the
216-
# signal in Python land. We also need to drain the socket because it
217-
# will be written to as part of the wakeup! There are some cases where
218-
# this may fire too soon / more than once on Windows so we should be
219-
# forgiving about reading an empty socket.
220-
rsock.setblocking(False)
221-
# Clear the socket to re-arm the notifier.
222-
@sn.activated.connect
223-
def_may_clear_sock(*args):
224-
try:
225-
rsock.recv(1)
226-
exceptBlockingIOError:
227-
pass
228-
229-
defhandle(*args):
230-
nonlocalhandler_args
231-
handler_args=args
232-
qapp.quit()
233-
234-
signal.signal(signal.SIGINT,handle)
233+
signal.signal(signal.SIGINT,handle)
235234
try:
236235
yield
237236
finally:
238-
ifnotskip:
239-
wsock.close()
240-
rsock.close()
241-
sn.setEnabled(False)
242-
signal.set_wakeup_fd(old_wakeup_fd)
243-
signal.signal(signal.SIGINT,old_sigint_handler)
244-
ifhandler_argsisnotNone:
245-
old_sigint_handler(*handler_args)
237+
wsock.close()
238+
rsock.close()
239+
sn.setEnabled(False)
240+
signal.set_wakeup_fd(old_wakeup_fd)
241+
signal.signal(signal.SIGINT,old_sigint_handler)
242+
ifhandler_argsisnotNone:
243+
old_sigint_handler(*handler_args)

‎lib/matplotlib/testing/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ def _check_for_pgf(texsystem):
173173
returnTrue
174174

175175

176+
class_WaitForStringPopen(subprocess.Popen):
177+
"""
178+
A Popen that passes flags that allow triggering KeyboardInterrupt.
179+
"""
180+
181+
def__init__(self,*args,**kwargs):
182+
ifsys.platform=='win32':
183+
kwargs['creationflags']=subprocess.CREATE_NEW_CONSOLE
184+
super().__init__(
185+
*args,**kwargs,
186+
# Force Agg so that each test can switch to its desired Qt backend.
187+
env={**os.environ,"MPLBACKEND":"Agg","SOURCE_DATE_EPOCH":"0"},
188+
stdout=subprocess.PIPE,universal_newlines=True)
189+
190+
defwait_for(self,terminator):
191+
"""Read until the terminator is reached."""
192+
buf=''
193+
whileTrue:
194+
c=self.stdout.read(1)
195+
ifnotc:
196+
raiseRuntimeError(
197+
f'Subprocess died before emitting expected{terminator!r}')
198+
buf+=c
199+
ifbuf.endswith(terminator):
200+
return
201+
202+
176203
def_has_tex_package(package):
177204
try:
178205
mpl.dviread.find_tex_file(f"{package}.sty")

‎lib/matplotlib/tests/test_backend_macosx.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
importinspect
12
importos
3+
importsignal
4+
importsys
25

36
importpytest
47

58
importmatplotlibasmpl
69
importmatplotlib.pyplotasplt
10+
frommatplotlib.testingimport_WaitForStringPopen
711
try:
812
frommatplotlib.backendsimport_macosx
913
exceptImportError:
1014
pytest.skip("These are mac only tests",allow_module_level=True)
1115

1216

17+
_test_timeout=60# A reasonably safe value for slower architectures.
18+
19+
1320
@pytest.mark.backend('macosx')
1421
deftest_cached_renderer():
1522
# Make sure that figures have an associated renderer after
@@ -44,3 +51,112 @@ def new_choose_save_file(title, directory, filename):
4451
# Check the savefig.directory rcParam got updated because
4552
# we added a subdirectory "test"
4653
assertmpl.rcParams["savefig.directory"]==f"{tmp_path}/test"
54+
55+
56+
def_test_sigint_impl(backend,target_name,kwargs):
57+
importsys
58+
importmatplotlib.pyplotasplt
59+
importos
60+
importthreading
61+
62+
plt.switch_backend(backend)
63+
64+
definterrupter():
65+
ifsys.platform=='win32':
66+
importwin32api
67+
win32api.GenerateConsoleCtrlEvent(0,0)
68+
else:
69+
importsignal
70+
os.kill(os.getpid(),signal.SIGINT)
71+
72+
target=getattr(plt,target_name)
73+
timer=threading.Timer(1,interrupter)
74+
fig=plt.figure()
75+
fig.canvas.mpl_connect(
76+
'draw_event',
77+
lambda*args:print('DRAW',flush=True)
78+
)
79+
fig.canvas.mpl_connect(
80+
'draw_event',
81+
lambda*args:timer.start()
82+
)
83+
try:
84+
target(**kwargs)
85+
exceptKeyboardInterrupt:
86+
print('SUCCESS',flush=True)
87+
88+
89+
@pytest.mark.backend('macosx',skip_on_importerror=True)
90+
@pytest.mark.parametrize("target, kwargs", [
91+
('show', {'block':True}),
92+
('pause', {'interval':10})
93+
])
94+
deftest_sigint(target,kwargs):
95+
backend=plt.get_backend()
96+
proc=_WaitForStringPopen(
97+
[sys.executable,"-c",
98+
inspect.getsource(_test_sigint_impl)+
99+
f"\n_test_sigint_impl({backend!r},{target!r},{kwargs!r})"])
100+
try:
101+
proc.wait_for('DRAW')
102+
stdout,_=proc.communicate(timeout=_test_timeout)
103+
exceptException:
104+
proc.kill()
105+
stdout,_=proc.communicate()
106+
raise
107+
print(stdout)
108+
assert'SUCCESS'instdout
109+
110+
111+
def_test_other_signal_before_sigint_impl(backend,target_name,kwargs):
112+
importsignal
113+
importmatplotlib.pyplotasplt
114+
plt.switch_backend(backend)
115+
116+
target=getattr(plt,target_name)
117+
118+
fig=plt.figure()
119+
fig.canvas.mpl_connect('draw_event',
120+
lambda*args:print('DRAW',flush=True))
121+
122+
timer=fig.canvas.new_timer(interval=1)
123+
timer.single_shot=True
124+
timer.add_callback(print,'SIGUSR1',flush=True)
125+
126+
defcustom_signal_handler(signum,frame):
127+
timer.start()
128+
signal.signal(signal.SIGUSR1,custom_signal_handler)
129+
130+
try:
131+
target(**kwargs)
132+
exceptKeyboardInterrupt:
133+
print('SUCCESS',flush=True)
134+
135+
136+
@pytest.mark.skipif(sys.platform=='win32',
137+
reason='No other signal available to send on Windows')
138+
@pytest.mark.backend('macosx',skip_on_importerror=True)
139+
@pytest.mark.parametrize("target, kwargs", [
140+
('show', {'block':True}),
141+
('pause', {'interval':10})
142+
])
143+
deftest_other_signal_before_sigint(target,kwargs):
144+
backend=plt.get_backend()
145+
proc=_WaitForStringPopen(
146+
[sys.executable,"-c",
147+
inspect.getsource(_test_other_signal_before_sigint_impl)+
148+
"\n_test_other_signal_before_sigint_impl("
149+
f"{backend!r},{target!r},{kwargs!r})"])
150+
try:
151+
proc.wait_for('DRAW')
152+
os.kill(proc.pid,signal.SIGUSR1)
153+
proc.wait_for('SIGUSR1')
154+
os.kill(proc.pid,signal.SIGINT)
155+
stdout,_=proc.communicate(timeout=_test_timeout)
156+
exceptException:
157+
proc.kill()
158+
stdout,_=proc.communicate()
159+
raise
160+
print(stdout)
161+
assert'SUCCESS'instdout
162+
plt.figure()

‎lib/matplotlib/tests/test_backend_qt.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
frommatplotlibimportpyplotasplt
1616
frommatplotlib._pylab_helpersimportGcf
1717
frommatplotlibimport_c_internal_utils
18+
frommatplotlib.testingimport_WaitForStringPopen
1819

1920

2021
try:
@@ -53,33 +54,6 @@ def test_fig_close():
5354
assertinit_figs==Gcf.figs
5455

5556

56-
classWaitForStringPopen(subprocess.Popen):
57-
"""
58-
A Popen that passes flags that allow triggering KeyboardInterrupt.
59-
"""
60-
61-
def__init__(self,*args,**kwargs):
62-
ifsys.platform=='win32':
63-
kwargs['creationflags']=subprocess.CREATE_NEW_CONSOLE
64-
super().__init__(
65-
*args,**kwargs,
66-
# Force Agg so that each test can switch to its desired Qt backend.
67-
env={**os.environ,"MPLBACKEND":"Agg","SOURCE_DATE_EPOCH":"0"},
68-
stdout=subprocess.PIPE,universal_newlines=True)
69-
70-
defwait_for(self,terminator):
71-
"""Read until the terminator is reached."""
72-
buf=''
73-
whileTrue:
74-
c=self.stdout.read(1)
75-
ifnotc:
76-
raiseRuntimeError(
77-
f'Subprocess died before emitting expected{terminator!r}')
78-
buf+=c
79-
ifbuf.endswith(terminator):
80-
return
81-
82-
8357
def_test_sigint_impl(backend,target_name,kwargs):
8458
importsys
8559
importmatplotlib.pyplotasplt
@@ -121,7 +95,7 @@ def interrupter():
12195
])
12296
deftest_sigint(target,kwargs):
12397
backend=plt.get_backend()
124-
proc=WaitForStringPopen(
98+
proc=_WaitForStringPopen(
12599
[sys.executable,"-c",
126100
inspect.getsource(_test_sigint_impl)+
127101
f"\n_test_sigint_impl({backend!r},{target!r},{kwargs!r})"])
@@ -171,7 +145,7 @@ def custom_signal_handler(signum, frame):
171145
])
172146
deftest_other_signal_before_sigint(target,kwargs):
173147
backend=plt.get_backend()
174-
proc=WaitForStringPopen(
148+
proc=_WaitForStringPopen(
175149
[sys.executable,"-c",
176150
inspect.getsource(_test_other_signal_before_sigint_impl)+
177151
"\n_test_other_signal_before_sigint_impl("

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp