Skip to content

Commit 1ef9f67

Browse files
authored
Use thread lock to support comms via subshells (#603)
1 parent 5e068ed commit 1ef9f67

File tree

1 file changed

+94
-83
lines changed

1 file changed

+94
-83
lines changed

ipympl/backend_nbagg.py

Lines changed: 94 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io
2020
import json
2121
from base64 import b64encode
22+
from threading import Lock
2223

2324
try:
2425
from collections.abc import Iterable
@@ -67,6 +68,10 @@
6768
cursors.WAIT: 'wait',
6869
}
6970

71+
# threading.Lock to prevent multiple threads from accessing globals such as Gcf
72+
_lock = Lock()
73+
74+
7075

7176
def connection_info():
7277
"""
@@ -75,18 +80,19 @@ def connection_info():
7580
use.
7681
7782
"""
78-
result = []
79-
for manager in Gcf.get_all_fig_managers():
80-
fig = manager.canvas.figure
81-
result.append(
82-
'{} - {}'.format(
83-
(fig.get_label() or f"Figure {manager.num}"),
84-
manager.web_sockets,
83+
with _lock:
84+
result = []
85+
for manager in Gcf.get_all_fig_managers():
86+
fig = manager.canvas.figure
87+
result.append(
88+
'{} - {}'.format(
89+
(fig.get_label() or f"Figure {manager.num}"),
90+
manager.web_sockets,
91+
)
8592
)
86-
)
87-
if not is_interactive():
88-
result.append(f'Figures pending show: {len(Gcf._activeQue)}')
89-
return '\n'.join(result)
93+
if not is_interactive():
94+
result.append(f'Figures pending show: {len(Gcf._activeQue)}')
95+
return '\n'.join(result)
9096

9197

9298
class Toolbar(DOMWidget, NavigationToolbar2WebAgg):
@@ -134,7 +140,8 @@ def export(self):
134140
width = pwidth / self.canvas._dpi_ratio
135141
data = "<img src='data:image/png;base64,{0}' width={1}/>"
136142
data = data.format(b64encode(buf.getvalue()).decode('utf-8'), width)
137-
display(HTML(data))
143+
with _lock:
144+
display(HTML(data))
138145

139146
@default('toolitems')
140147
def _default_toolitems(self):
@@ -397,7 +404,8 @@ def __init__(self, canvas, num):
397404
def show(self):
398405
if self.canvas._closed:
399406
self.canvas._closed = False
400-
display(self.canvas)
407+
with _lock:
408+
display(self.canvas)
401409
else:
402410
self.canvas.draw_idle()
403411

@@ -415,83 +423,86 @@ class _Backend_ipympl(_Backend):
415423

416424
@staticmethod
417425
def new_figure_manager_given_figure(num, figure):
418-
canvas = Canvas(figure)
419-
if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']:
420-
figure.patch.set_alpha(0)
421-
manager = FigureManager(canvas, num)
422-
423-
if is_interactive():
424-
_Backend_ipympl._to_show.append(figure)
425-
figure.canvas.draw_idle()
426-
427-
def destroy(event):
428-
canvas.mpl_disconnect(cid)
429-
Gcf.destroy(manager)
430-
431-
cid = canvas.mpl_connect('close_event', destroy)
432-
433-
# Only register figure for showing when in interactive mode (otherwise
434-
# we'll generate duplicate plots, since a user who set ioff() manually
435-
# expects to make separate draw/show calls).
436-
if is_interactive():
437-
# ensure current figure will be drawn.
438-
try:
439-
_Backend_ipympl._to_show.remove(figure)
440-
except ValueError:
441-
# ensure it only appears in the draw list once
442-
pass
443-
# Queue up the figure for drawing in next show() call
444-
_Backend_ipympl._to_show.append(figure)
445-
_Backend_ipympl._draw_called = True
446-
447-
return manager
426+
with _lock:
427+
canvas = Canvas(figure)
428+
if 'nbagg.transparent' in rcParams and rcParams['nbagg.transparent']:
429+
figure.patch.set_alpha(0)
430+
manager = FigureManager(canvas, num)
431+
432+
if is_interactive():
433+
_Backend_ipympl._to_show.append(figure)
434+
figure.canvas.draw_idle()
435+
436+
def destroy(event):
437+
canvas.mpl_disconnect(cid)
438+
Gcf.destroy(manager)
439+
440+
cid = canvas.mpl_connect('close_event', destroy)
441+
442+
# Only register figure for showing when in interactive mode (otherwise
443+
# we'll generate duplicate plots, since a user who set ioff() manually
444+
# expects to make separate draw/show calls).
445+
if is_interactive():
446+
# ensure current figure will be drawn.
447+
try:
448+
_Backend_ipympl._to_show.remove(figure)
449+
except ValueError:
450+
# ensure it only appears in the draw list once
451+
pass
452+
# Queue up the figure for drawing in next show() call
453+
_Backend_ipympl._to_show.append(figure)
454+
_Backend_ipympl._draw_called = True
455+
456+
return manager
448457

449458
@staticmethod
450459
def show(block=None):
451-
# # TODO: something to do when keyword block==False ?
452-
interactive = is_interactive()
460+
with _lock:
461+
# # TODO: something to do when keyword block==False ?
462+
interactive = is_interactive()
453463

454-
manager = Gcf.get_active()
455-
if manager is None:
456-
return
464+
manager = Gcf.get_active()
465+
if manager is None:
466+
return
457467

458-
try:
459-
display(manager.canvas)
460-
# metadata=_fetch_figure_metadata(manager.canvas.figure)
468+
try:
469+
display(manager.canvas)
470+
# metadata=_fetch_figure_metadata(manager.canvas.figure)
461471

462-
# plt.figure adds an event which makes the figure in focus the
463-
# active one. Disable this behaviour, as it results in
464-
# figures being put as the active figure after they have been
465-
# shown, even in non-interactive mode.
466-
if hasattr(manager, '_cidgcf'):
467-
manager.canvas.mpl_disconnect(manager._cidgcf)
472+
# plt.figure adds an event which makes the figure in focus the
473+
# active one. Disable this behaviour, as it results in
474+
# figures being put as the active figure after they have been
475+
# shown, even in non-interactive mode.
476+
if hasattr(manager, '_cidgcf'):
477+
manager.canvas.mpl_disconnect(manager._cidgcf)
468478

469-
if not interactive:
470-
Gcf.figs.pop(manager.num, None)
471-
finally:
472-
if manager.canvas.figure in _Backend_ipympl._to_show:
473-
_Backend_ipympl._to_show.remove(manager.canvas.figure)
479+
if not interactive:
480+
Gcf.figs.pop(manager.num, None)
481+
finally:
482+
if manager.canvas.figure in _Backend_ipympl._to_show:
483+
_Backend_ipympl._to_show.remove(manager.canvas.figure)
474484

475485

476486
def flush_figures():
477-
backend = matplotlib.get_backend()
478-
if backend in ('widget', 'ipympl', 'module://ipympl.backend_nbagg'):
479-
if not _Backend_ipympl._draw_called:
480-
return
481-
482-
try:
483-
# exclude any figures that were closed:
484-
active = {fm.canvas.figure for fm in Gcf.get_all_fig_managers()}
485-
486-
for fig in [fig for fig in _Backend_ipympl._to_show if fig in active]:
487-
# display(fig.canvas, metadata=_fetch_figure_metadata(fig))
488-
display(fig.canvas)
489-
finally:
490-
# clear flags for next round
491-
_Backend_ipympl._to_show = []
492-
_Backend_ipympl._draw_called = False
493-
494-
495-
ip = get_ipython()
496-
if ip is not None:
497-
ip.events.register('post_execute', flush_figures)
487+
with _lock:
488+
backend = matplotlib.get_backend()
489+
if backend in ('widget', 'ipympl', 'module://ipympl.backend_nbagg'):
490+
if not _Backend_ipympl._draw_called:
491+
return
492+
493+
try:
494+
# exclude any figures that were closed:
495+
active = {fm.canvas.figure for fm in Gcf.get_all_fig_managers()}
496+
for fig in [fig for fig in _Backend_ipympl._to_show if fig in active]:
497+
# display(fig.canvas, metadata=_fetch_figure_metadata(fig))
498+
display(fig.canvas)
499+
finally:
500+
# clear flags for next round
501+
_Backend_ipympl._to_show = []
502+
_Backend_ipympl._draw_called = False
503+
504+
505+
with _lock:
506+
ip = get_ipython()
507+
if ip is not None:
508+
ip.events.register('post_execute', flush_figures)

0 commit comments

Comments
 (0)