From 0dcdd21abacbd04e540220e04b1507244e4ddc1d Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Thu, 27 Aug 2020 12:39:54 -0400 Subject: [PATCH 01/19] WIP: Try to make tkagg blitting threadsafe current status: doesn't crash but also doesn't draw when called from a thread --- lib/matplotlib/backends/_backend_tk.py | 32 ++++++++++++ lib/matplotlib/backends/backend_tkagg.py | 7 +-- src/_tkagg.cpp | 63 ++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 69b57fc15d2a..42c92aca36b6 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -123,6 +123,7 @@ def __init__(self, figure, master=None, resize_callback=None): width=w, height=h, borderwidth=0, highlightthickness=0) self._tkphoto = tk.PhotoImage( master=self._tkcanvas, width=w, height=h) + self._findphoto_name = self._tkcanvas.register(self._findphoto) self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) self._resize_callback = resize_callback self._tkcanvas.bind("", self.resize) @@ -178,6 +179,37 @@ def resize(self, event): int(width / 2), int(height / 2), image=self._tkphoto) self.resize_event() + def _findphoto(self): + return _tkagg.findphoto(self._tkphoto.tk.interpaddr(), str(self._tkphoto)) + + def blit(self, bbox=None): + """ + Blit *aggimage* to *photoimage*. + + *offsets* is a tuple describing how to fill the ``offset`` field of the + ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data, + (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) + for big-endian ARGB32 (i.e. ARGB8888) data. + + If *bbox* is passed, it defines the region that gets blitted. + """ + data = np.asarray(self.renderer._renderer) + height, width = data.shape[:2] + dataptr = (height, width, data.ctypes.data) + if bbox is not None: + (x1, y1), (x2, y2) = bbox.__array__() + x1 = max(math.floor(x1), 0) + x2 = min(math.ceil(x2), width) + y1 = max(math.floor(y1), 0) + y2 = min(math.ceil(y2), height) + bboxptr = (x1, x2, y1, y2) + else: + self._tkphoto.blank() + bboxptr = (0, width, 0, height) + # photoptr = self._findphoto() # Thread unsafe + photoptr = self._master.tk.call(self._findphoto_name) # Thread safe + _tkagg.photoputblock(photoptr, dataptr, (0, 1, 2, 3), bboxptr) # ??? + def draw_idle(self): # docstring inherited if not self._idle: diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 484d5a40edbe..da01a5c4c58e 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1,4 +1,3 @@ -from . import _backend_tk from .backend_agg import FigureCanvasAgg from ._backend_tk import ( _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk) @@ -7,11 +6,7 @@ class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk): def draw(self): super().draw() - _backend_tk.blit(self._tkphoto, self.renderer._renderer, (0, 1, 2, 3)) - - def blit(self, bbox=None): - _backend_tk.blit( - self._tkphoto, self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) + self.blit() @_BackendTk.export diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index d74800d5241d..3cfc68bcf38f 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -89,8 +89,71 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) } } +static PyObject *mpl_tk_findphoto(PyObject *self, PyObject *args) +{ + Tcl_Interp *interp; + char const *photo_name; + Tk_PhotoHandle photo; + if (!PyArg_ParseTuple(args, "O&s:findphoto",convert_voidptr, &interp, &photo_name)) { + goto exit; + } + if (!(photo = TK_FIND_PHOTO(interp, photo_name))) { + PyErr_SetString(PyExc_ValueError, "Failed to extract Tk_PhotoHandle"); + goto exit; + } +exit: + if (PyErr_Occurred()) { + return NULL; + } else { + return PyLong_FromVoidPtr((void *)photo); + } +} + +static PyObject *mpl_tk_photoputblock(PyObject *self, PyObject *args) +{ + int height, width; + unsigned char *data_ptr; + int o0, o1, o2, o3; + int x1, x2, y1, y2; + Tk_PhotoHandle photo; + Tk_PhotoImageBlock block; + if (!PyArg_ParseTuple(args, "O&(iiO&)(iiii)(iiii):photoputblock", + convert_voidptr, &photo, + &height, &width, convert_voidptr, &data_ptr, + &o0, &o1, &o2, &o3, + &x1, &x2, &y1, &y2)) { + goto exit; + } + if (0 > y1 || y1 > y2 || y2 > height || 0 > x1 || x1 > x2 || x2 > width) { + PyErr_SetString(PyExc_ValueError, "Attempting to draw out of bounds"); + goto exit; + } + + Py_BEGIN_ALLOW_THREADS + block.pixelPtr = data_ptr + 4 * ((height - y2) * width + x1); + block.width = x2 - x1; + block.height = y2 - y1; + block.pitch = 4 * width; + block.pixelSize = 4; + block.offset[0] = o0; + block.offset[1] = o1; + block.offset[2] = o2; + block.offset[3] = o3; + TK_PHOTO_PUT_BLOCK_NO_COMPOSITE( + photo, &block, x1, height - y2, x2 - x1, y2 - y1); + Py_END_ALLOW_THREADS +exit: + if (PyErr_Occurred()) { + return NULL; + } else { + Py_RETURN_NONE; + } +} + static PyMethodDef functions[] = { { "blit", (PyCFunction)mpl_tk_blit, METH_VARARGS }, + { "findphoto", (PyCFunction)mpl_tk_findphoto, METH_VARARGS }, + { "photoputblock", (PyCFunction)mpl_tk_photoputblock, METH_VARARGS }, { NULL, NULL } /* sentinel */ }; From 2ad422a78c3794ac5139364c0ea3d6a8d6de6030 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Thu, 24 Sep 2020 16:51:36 -0400 Subject: [PATCH 02/19] make tkagg blitting threadsafe current status: Prone to races and hard-to-diagnose timeouts (see https://github.com/python/cpython/pull/21299) --- lib/matplotlib/backends/_backend_tk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 42c92aca36b6..fe5a8c6c28b9 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -124,6 +124,8 @@ def __init__(self, figure, master=None, resize_callback=None): self._tkphoto = tk.PhotoImage( master=self._tkcanvas, width=w, height=h) self._findphoto_name = self._tkcanvas.register(self._findphoto) + self._photoputblock_name = self._tkcanvas.register(self._photoputblock) + self._tkpointers = None self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) self._resize_callback = resize_callback self._tkcanvas.bind("", self.resize) @@ -182,6 +184,10 @@ def resize(self, event): def _findphoto(self): return _tkagg.findphoto(self._tkphoto.tk.interpaddr(), str(self._tkphoto)) + def _photoputblock(self): + photoptr, dataptr, bboxptr = self._tkpointers + _tkagg.photoputblock(photoptr, dataptr, (0, 1, 2, 3), bboxptr) + def blit(self, bbox=None): """ Blit *aggimage* to *photoimage*. @@ -206,9 +212,9 @@ def blit(self, bbox=None): else: self._tkphoto.blank() bboxptr = (0, width, 0, height) - # photoptr = self._findphoto() # Thread unsafe - photoptr = self._master.tk.call(self._findphoto_name) # Thread safe - _tkagg.photoputblock(photoptr, dataptr, (0, 1, 2, 3), bboxptr) # ??? + photoptr = self._master.tk.call(self._findphoto_name) + self._tkpointers = photoptr, dataptr, bboxptr + self._master.tk.call(self._photoputblock_name) def draw_idle(self): # docstring inherited From a4a920edf87f3a1399949f198934e1a50f4af6a8 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 10:13:43 -0400 Subject: [PATCH 03/19] revert to unified blit code and prevent some races current status: Relies on tkinter arcana, untested --- lib/matplotlib/backends/_backend_tk.py | 71 ++++++++++------------ lib/matplotlib/backends/backend_tkagg.py | 6 +- lib/matplotlib/backends/backend_tkcairo.py | 2 +- src/_tkagg.cpp | 63 ------------------- 4 files changed, 37 insertions(+), 105 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index fe5a8c6c28b9..4d3e862b9861 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -44,10 +44,25 @@ def _restore_foreground_window_at_end(): _c_internal_utils.Win32_SetForegroundWindow(foreground) -def blit(photoimage, aggimage, offsets, bbox=None): +_blit_args = {} +_blit_tcl_name = None + + +def _blit(argsid): + """ + Thin wrapper to pass arguments to blit via tkapp.call + """ + args = _blit_args.pop(argsid) + _tkagg.blit(*args) + + +def blit(tk_instance, photoimage, aggimage, offsets, bbox=None): """ Blit *aggimage* to *photoimage*. + A *tk_instance* such as a canvas is required execute the blit. + Tcl events must be dispatched to trigger a blit from a non-Tcl thread. + *offsets* is a tuple describing how to fill the ``offset`` field of the ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data, (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) @@ -68,8 +83,22 @@ def blit(photoimage, aggimage, offsets, bbox=None): else: photoimage.blank() bboxptr = (0, width, 0, height) - _tkagg.blit( + + args = ( photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) + argsid = str(id) + _blit_args[argsid] = args + + global _blit_tcl_name + # tkapp.call coerces all arguments to strings + try: + tk_instance.tk.call(_blit_tcl_name, argsid) + except tk.TclError: + # need to register with the tk root or constantly re-register + # each time tk_instance is destroyed + root = tk_instance._root() + _blit_tcl_name = root.register(_blit) + tk_instance.tk.call(_blit_tcl_name, argsid) class TimerTk(TimerBase): @@ -123,9 +152,6 @@ def __init__(self, figure, master=None, resize_callback=None): width=w, height=h, borderwidth=0, highlightthickness=0) self._tkphoto = tk.PhotoImage( master=self._tkcanvas, width=w, height=h) - self._findphoto_name = self._tkcanvas.register(self._findphoto) - self._photoputblock_name = self._tkcanvas.register(self._photoputblock) - self._tkpointers = None self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) self._resize_callback = resize_callback self._tkcanvas.bind("", self.resize) @@ -181,41 +207,6 @@ def resize(self, event): int(width / 2), int(height / 2), image=self._tkphoto) self.resize_event() - def _findphoto(self): - return _tkagg.findphoto(self._tkphoto.tk.interpaddr(), str(self._tkphoto)) - - def _photoputblock(self): - photoptr, dataptr, bboxptr = self._tkpointers - _tkagg.photoputblock(photoptr, dataptr, (0, 1, 2, 3), bboxptr) - - def blit(self, bbox=None): - """ - Blit *aggimage* to *photoimage*. - - *offsets* is a tuple describing how to fill the ``offset`` field of the - ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data, - (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) - for big-endian ARGB32 (i.e. ARGB8888) data. - - If *bbox* is passed, it defines the region that gets blitted. - """ - data = np.asarray(self.renderer._renderer) - height, width = data.shape[:2] - dataptr = (height, width, data.ctypes.data) - if bbox is not None: - (x1, y1), (x2, y2) = bbox.__array__() - x1 = max(math.floor(x1), 0) - x2 = min(math.ceil(x2), width) - y1 = max(math.floor(y1), 0) - y2 = min(math.ceil(y2), height) - bboxptr = (x1, x2, y1, y2) - else: - self._tkphoto.blank() - bboxptr = (0, width, 0, height) - photoptr = self._master.tk.call(self._findphoto_name) - self._tkpointers = photoptr, dataptr, bboxptr - self._master.tk.call(self._photoputblock_name) - def draw_idle(self): # docstring inherited if not self._idle: diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index da01a5c4c58e..c652262ebd88 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1,6 +1,6 @@ from .backend_agg import FigureCanvasAgg from ._backend_tk import ( - _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk) + _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk, blit) class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk): @@ -8,6 +8,10 @@ def draw(self): super().draw() self.blit() + def blit(self, bbox=None): + blit(self._tkcanvas, self._tkphoto, + self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) + @_BackendTk.export class _BackendTkAgg(_BackendTk): diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py index a81fd0d92bb8..e7c8dd02480c 100644 --- a/lib/matplotlib/backends/backend_tkcairo.py +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -21,7 +21,7 @@ def draw(self): self.figure.draw(self._renderer) buf = np.reshape(surface.get_data(), (height, width, 4)) _backend_tk.blit( - self._tkphoto, buf, + self._tkcanvas, self._tkphoto, buf, (2, 1, 0, 3) if sys.byteorder == "little" else (1, 2, 3, 0)) diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index 3cfc68bcf38f..d74800d5241d 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -89,71 +89,8 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) } } -static PyObject *mpl_tk_findphoto(PyObject *self, PyObject *args) -{ - Tcl_Interp *interp; - char const *photo_name; - Tk_PhotoHandle photo; - if (!PyArg_ParseTuple(args, "O&s:findphoto",convert_voidptr, &interp, &photo_name)) { - goto exit; - } - if (!(photo = TK_FIND_PHOTO(interp, photo_name))) { - PyErr_SetString(PyExc_ValueError, "Failed to extract Tk_PhotoHandle"); - goto exit; - } -exit: - if (PyErr_Occurred()) { - return NULL; - } else { - return PyLong_FromVoidPtr((void *)photo); - } -} - -static PyObject *mpl_tk_photoputblock(PyObject *self, PyObject *args) -{ - int height, width; - unsigned char *data_ptr; - int o0, o1, o2, o3; - int x1, x2, y1, y2; - Tk_PhotoHandle photo; - Tk_PhotoImageBlock block; - if (!PyArg_ParseTuple(args, "O&(iiO&)(iiii)(iiii):photoputblock", - convert_voidptr, &photo, - &height, &width, convert_voidptr, &data_ptr, - &o0, &o1, &o2, &o3, - &x1, &x2, &y1, &y2)) { - goto exit; - } - if (0 > y1 || y1 > y2 || y2 > height || 0 > x1 || x1 > x2 || x2 > width) { - PyErr_SetString(PyExc_ValueError, "Attempting to draw out of bounds"); - goto exit; - } - - Py_BEGIN_ALLOW_THREADS - block.pixelPtr = data_ptr + 4 * ((height - y2) * width + x1); - block.width = x2 - x1; - block.height = y2 - y1; - block.pitch = 4 * width; - block.pixelSize = 4; - block.offset[0] = o0; - block.offset[1] = o1; - block.offset[2] = o2; - block.offset[3] = o3; - TK_PHOTO_PUT_BLOCK_NO_COMPOSITE( - photo, &block, x1, height - y2, x2 - x1, y2 - y1); - Py_END_ALLOW_THREADS -exit: - if (PyErr_Occurred()) { - return NULL; - } else { - Py_RETURN_NONE; - } -} - static PyMethodDef functions[] = { { "blit", (PyCFunction)mpl_tk_blit, METH_VARARGS }, - { "findphoto", (PyCFunction)mpl_tk_findphoto, METH_VARARGS }, - { "photoputblock", (PyCFunction)mpl_tk_photoputblock, METH_VARARGS }, { NULL, NULL } /* sentinel */ }; From 321b7a0990297a8785e6f1e4cafc167a9e5a0746 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 10:39:01 -0400 Subject: [PATCH 04/19] register _blit with Tcl without an extra widget current status: Relies on tkinter arcana, untested --- lib/matplotlib/backends/_backend_tk.py | 16 ++++++++-------- lib/matplotlib/backends/backend_tkagg.py | 3 +-- lib/matplotlib/backends/backend_tkcairo.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 4d3e862b9861..fc36904a37c8 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -45,7 +45,8 @@ def _restore_foreground_window_at_end(): _blit_args = {} -_blit_tcl_name = None +# Initialized to non-empty string that is not a current Tcl command +_blit_tcl_name = "29345091836409813" def _blit(argsid): @@ -56,7 +57,7 @@ def _blit(argsid): _tkagg.blit(*args) -def blit(tk_instance, photoimage, aggimage, offsets, bbox=None): +def blit(photoimage, aggimage, offsets, bbox=None): """ Blit *aggimage* to *photoimage*. @@ -92,13 +93,12 @@ def blit(tk_instance, photoimage, aggimage, offsets, bbox=None): global _blit_tcl_name # tkapp.call coerces all arguments to strings try: - tk_instance.tk.call(_blit_tcl_name, argsid) + photoimage.tk.call(_blit_tcl_name, argsid) except tk.TclError: - # need to register with the tk root or constantly re-register - # each time tk_instance is destroyed - root = tk_instance._root() - _blit_tcl_name = root.register(_blit) - tk_instance.tk.call(_blit_tcl_name, argsid) + # register _blit by copying code from tkinter.Misc._register + _blit_tcl_name = repr(id(_blit)) + _blit.__name__ + photoimage.tk.createcommand(_blit_tcl_name, _blit) + photoimage.tk.call(_blit_tcl_name, argsid) class TimerTk(TimerBase): diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index c652262ebd88..8768a729c6d0 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -9,8 +9,7 @@ def draw(self): self.blit() def blit(self, bbox=None): - blit(self._tkcanvas, self._tkphoto, - self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) + blit(self._tkphoto, self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) @_BackendTk.export diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py index e7c8dd02480c..a81fd0d92bb8 100644 --- a/lib/matplotlib/backends/backend_tkcairo.py +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -21,7 +21,7 @@ def draw(self): self.figure.draw(self._renderer) buf = np.reshape(surface.get_data(), (height, width, 4)) _backend_tk.blit( - self._tkcanvas, self._tkphoto, buf, + self._tkphoto, buf, (2, 1, 0, 3) if sys.byteorder == "little" else (1, 2, 3, 0)) From 24d25456ccb37bf2be22f63e6d0ac65eeeea48ed Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 10:40:34 -0400 Subject: [PATCH 05/19] docstring --- lib/matplotlib/backends/_backend_tk.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index fc36904a37c8..5d9a62bd5004 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -61,9 +61,6 @@ def blit(photoimage, aggimage, offsets, bbox=None): """ Blit *aggimage* to *photoimage*. - A *tk_instance* such as a canvas is required execute the blit. - Tcl events must be dispatched to trigger a blit from a non-Tcl thread. - *offsets* is a tuple describing how to fill the ``offset`` field of the ``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data, (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) From 7196d37bf44bfa7ce2ae0bf3af7bdd37bbef910d Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 10:41:24 -0400 Subject: [PATCH 06/19] missing id argument --- lib/matplotlib/backends/_backend_tk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 5d9a62bd5004..4d076d251c95 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -84,7 +84,7 @@ def blit(photoimage, aggimage, offsets, bbox=None): args = ( photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) - argsid = str(id) + argsid = repr(id(args)) _blit_args[argsid] = args global _blit_tcl_name From b63950e9aef2d580c0ffb1bcc2b5b9176e6371c3 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 11:15:04 -0400 Subject: [PATCH 07/19] make blank occur in same event as blit drawing from a thread produces an intolerable flicker unless blank and blit are called from the same event in the main thread --- lib/matplotlib/backends/_backend_tk.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 4d076d251c95..9e9dd7ae72e3 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -53,7 +53,11 @@ def _blit(argsid): """ Thin wrapper to pass arguments to blit via tkapp.call """ - args = _blit_args.pop(argsid) + photoimage, dataptr, offsets, bboxptr, blank = _blit_args.pop(argsid) + args = ( + photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) + if blank: + photoimage.blank() _tkagg.blit(*args) @@ -78,12 +82,12 @@ def blit(photoimage, aggimage, offsets, bbox=None): y1 = max(math.floor(y1), 0) y2 = min(math.ceil(y2), height) bboxptr = (x1, x2, y1, y2) + blank = False else: - photoimage.blank() bboxptr = (0, width, 0, height) + blank = True - args = ( - photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) + args = photoimage, dataptr, offsets, bboxptr, blank argsid = repr(id(args)) _blit_args[argsid] = args From 9180607cbe2dab31132023d80c6772a8b1955972 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Fri, 25 Sep 2020 16:04:00 -0400 Subject: [PATCH 08/19] test for thread safety --- lib/matplotlib/backends/_backend_tk.py | 5 ++-- .../tests/test_backends_interactive.py | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 9e9dd7ae72e3..26e5227bf032 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -54,11 +54,10 @@ def _blit(argsid): Thin wrapper to pass arguments to blit via tkapp.call """ photoimage, dataptr, offsets, bboxptr, blank = _blit_args.pop(argsid) - args = ( - photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) if blank: photoimage.blank() - _tkagg.blit(*args) + _tkagg.blit( + photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) def blit(photoimage, aggimage, offsets, bbox=None): diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 000ff971bc97..aee0f37d3aaa 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -72,6 +72,7 @@ def _get_testable_interactive_backends(): import io import json import sys +import threading from unittest import TestCase import matplotlib as mpl @@ -146,6 +147,28 @@ def check_alt_backend(alt_backend): # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS to # not resize incorrectly. assert_equal(result.getvalue(), result_after.getvalue()) + +# Test artists and drawing does not crash from thread (no other guarantees) +fig, ax = plt.subplots() +# plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg +plt.pause(0.1) + +def thread_artist_work(): + ax.plot([1,3,6]) + +def thread_draw_work(): + fig.canvas.draw() + fig.canvas.stop_event_loop() + +t = threading.Thread(target=thread_artist_work) +t.start() +# artists never wait for the event loop to run, so just join +t.join() + +t = threading.Thread(target=thread_draw_work) +t.start() +fig.canvas.start_event_loop() +t.join() """ _test_timeout = 10 # Empirically, 1s is not enough on Travis. From e692b625bd290b92eaf71ebf9304b6944ccc67a7 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Mon, 28 Sep 2020 06:38:55 -0400 Subject: [PATCH 09/19] improve thread safety test --- .../tests/test_backends_interactive.py | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index aee0f37d3aaa..644dead74543 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -67,7 +67,6 @@ def _get_testable_interactive_backends(): # early. Also, gtk3 redefines key_press_event with a different signature, so # we directly invoke it from the superclass instead. _test_script = """\ -import importlib import importlib.util import io import json @@ -147,28 +146,49 @@ def check_alt_backend(alt_backend): # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS to # not resize incorrectly. assert_equal(result.getvalue(), result_after.getvalue()) - -# Test artists and drawing does not crash from thread (no other guarantees) + +# Test artists and drawing from thread does not crash (no other guarantees) fig, ax = plt.subplots() # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg -plt.pause(0.1) +plt.pause(0.5) + +exc_info = None def thread_artist_work(): - ax.plot([1,3,6]) + try: + ax.plot([1,3,6]) + except Exception as e: + # Propagate error to main thread + global exc_info + exc_info = sys.exc_info() def thread_draw_work(): - fig.canvas.draw() - fig.canvas.stop_event_loop() + try: + fig.canvas.draw() + except Exception as e: + # Propagate error to main thread + global exc_info + exc_info = sys.exc_info() t = threading.Thread(target=thread_artist_work) t.start() # artists never wait for the event loop to run, so just join t.join() +if exc_info is not None: # Raise thread error + raise exc_info[1].with_traceback(exc_info[2]) + t = threading.Thread(target=thread_draw_work) +timer = fig.canvas.new_timer(1.) +timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q") +fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) +fig.canvas.mpl_connect("close_event", print) t.start() fig.canvas.start_event_loop() t.join() + +if exc_info is not None: # Raise thread error + raise exc_info[1].with_traceback(exc_info[2]) """ _test_timeout = 10 # Empirically, 1s is not enough on Travis. @@ -193,7 +213,7 @@ def test_interactive_backend(backend, toolbar): if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.") - assert proc.stdout.count("CloseEvent") == 1 + assert proc.stdout.count("CloseEvent") == 2 @pytest.mark.skipif('TF_BUILD' in os.environ, From e20118963a222b9d373f3a9e3f06835063b47f3a Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Mon, 28 Sep 2020 07:00:46 -0400 Subject: [PATCH 10/19] fix tcl errors by delaying the widget destruction --- lib/matplotlib/backends/_backend_tk.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 26e5227bf032..2fc3be07bebb 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -431,10 +431,18 @@ def destroy(self, *args): if self.canvas._idle_callback: self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) - self.window.destroy() + # NOTE: events need to be flushed before issuing destroy (GH #9956), + # however, self.window.update() can break user code. This is the + # safest way to achieve a complete draining of the event queue, + # but it may require users to update() on their own to execute the + # completion in obscure corner cases. + def delayed_destroy(): + self.window.destroy() + + if self._owns_mainloop and not Gcf.get_num_fig_managers(): + self.window.quit() - if self._owns_mainloop and not Gcf.get_num_fig_managers(): - self.window.quit() + self.window.after_idle(delayed_destroy) def get_window_title(self): return self.window.wm_title() From 8c979cc117fbc82955dd96ed14d7f8b7be51064b Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Mon, 28 Sep 2020 07:39:45 -0400 Subject: [PATCH 11/19] blit docs & comments --- lib/matplotlib/backends/_backend_tk.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 2fc3be07bebb..d97f294bc5b9 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -45,13 +45,19 @@ def _restore_foreground_window_at_end(): _blit_args = {} -# Initialized to non-empty string that is not a current Tcl command +# Initialize to a non-empty string that is not a Tcl command _blit_tcl_name = "29345091836409813" def _blit(argsid): """ - Thin wrapper to pass arguments to blit via tkapp.call + Thin wrapper to blit via tkapp.call + + *argsid* is a unique string identifier to fetch the correct arguments from + the _blit_args dict, since args cannot be passed directly + + photoimage blanking must occur in the same event and thread as blitting + to avoid flickering. """ photoimage, dataptr, offsets, bboxptr, blank = _blit_args.pop(argsid) if blank: @@ -69,7 +75,10 @@ def blit(photoimage, aggimage, offsets, bbox=None): (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0) for big-endian ARGB32 (i.e. ARGB8888) data. - If *bbox* is passed, it defines the region that gets blitted. + If *bbox* is passed, it defines the region that gets blitted. That region + will NOT be blanked before blitting. + + Tcl events must be dispatched to trigger a blit from a non-Tcl thread. """ data = np.asarray(aggimage) height, width = data.shape[:2] @@ -86,16 +95,24 @@ def blit(photoimage, aggimage, offsets, bbox=None): bboxptr = (0, width, 0, height) blank = True + # NOTE: _tkagg.blit is thread unsafe and will crash the process if called + # from a thread (GH#13293). Instead of blanking and blitting here, + # use tkapp.call to post a cross-thread event if this function is called + # from a non-Tcl thread. + + # tkapp.call coerces all arguments to strings, so to avoid string parsing + # within _blit, pack up the arguments into a global data structure. args = photoimage, dataptr, offsets, bboxptr, blank + # Need a unique key to avoid thread races. + # Again, make the key a string to avoid string parsing in _blit. argsid = repr(id(args)) _blit_args[argsid] = args global _blit_tcl_name - # tkapp.call coerces all arguments to strings try: photoimage.tk.call(_blit_tcl_name, argsid) except tk.TclError: - # register _blit by copying code from tkinter.Misc._register + # register _blit with code copied from tkinter.Misc._register _blit_tcl_name = repr(id(_blit)) + _blit.__name__ photoimage.tk.createcommand(_blit_tcl_name, _blit) photoimage.tk.call(_blit_tcl_name, argsid) From 346314808050ccda74966a545b1fd170b9e1f679 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Mon, 28 Sep 2020 11:41:29 -0400 Subject: [PATCH 12/19] appease CI --- .../tests/test_backends_interactive.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 644dead74543..55b1096ec3a7 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -147,17 +147,17 @@ def check_alt_backend(alt_backend): # not resize incorrectly. assert_equal(result.getvalue(), result_after.getvalue()) -# Test artists and drawing from thread does not crash (no other guarantees) +# Test artists and drawing does not crash from thread (no other guarantees) fig, ax = plt.subplots() # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg -plt.pause(0.5) +plt.pause(0.1) exc_info = None def thread_artist_work(): try: ax.plot([1,3,6]) - except Exception as e: + except: # Propagate error to main thread global exc_info exc_info = sys.exc_info() @@ -165,7 +165,7 @@ def thread_artist_work(): def thread_draw_work(): try: fig.canvas.draw() - except Exception as e: + except: # Propagate error to main thread global exc_info exc_info = sys.exc_info() @@ -175,19 +175,18 @@ def thread_draw_work(): # artists never wait for the event loop to run, so just join t.join() -if exc_info is not None: # Raise thread error +if exc_info: # Raise thread error raise exc_info[1].with_traceback(exc_info[2]) t = threading.Thread(target=thread_draw_work) -timer = fig.canvas.new_timer(1.) -timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q") -fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) fig.canvas.mpl_connect("close_event", print) t.start() -fig.canvas.start_event_loop() +plt.pause(0.1) # flush_events fails on at least Tkagg (bpo-41176) t.join() +plt.close() +fig.canvas.flush_events() # pause doesn't process events after close -if exc_info is not None: # Raise thread error +if exc_info: # Raise thread error raise exc_info[1].with_traceback(exc_info[2]) """ _test_timeout = 10 # Empirically, 1s is not enough on Travis. From fd97233b0581c8062a704788a64539e151dbfdee Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Sat, 24 Oct 2020 09:15:32 -0400 Subject: [PATCH 13/19] Split thread tests from other interactive tests ignore wx --- .../tests/test_backends_interactive.py | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 55b1096ec3a7..0d631799b1a0 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -146,8 +146,51 @@ def check_alt_backend(alt_backend): # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS to # not resize incorrectly. assert_equal(result.getvalue(), result_after.getvalue()) +""" +_test_timeout = 10 # Empirically, 1s is not enough on Travis. + + +@pytest.mark.parametrize("backend", _get_testable_interactive_backends()) +@pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) +@pytest.mark.flaky(reruns=3) +def test_interactive_backend(backend, toolbar): + if backend == "macosx": + if toolbar == "toolmanager": + pytest.skip("toolmanager is not implemented for macosx.") + if toolbar == "toolbar2" and os.environ.get('TRAVIS'): + # See https://github.com/matplotlib/matplotlib/issues/18213 + pytest.skip("toolbar2 for macosx is buggy on Travis.") + + proc = subprocess.run( + [sys.executable, "-c", _test_script, + json.dumps({"toolbar": toolbar})], + env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, + timeout=_test_timeout, + stdout=subprocess.PIPE, universal_newlines=True) + if proc.returncode: + pytest.fail("The subprocess returned with non-zero exit status " + f"{proc.returncode}.") + assert proc.stdout.count("CloseEvent") == 1 + + +_thread_test_script = """\ +import json +import sys +import threading +from unittest import TestCase + +from matplotlib import pyplot as plt, rcParams +rcParams.update({ + "webagg.open_in_browser": False, + "webagg.port_retries": 1, +}) +if len(sys.argv) >= 2: # Second argument is json-encoded rcParams. + rcParams.update(json.loads(sys.argv[1])) +assert_equal = TestCase().assertEqual +assert_raises = TestCase().assertRaises -# Test artists and drawing does not crash from thread (no other guarantees) +# Test artist creation and drawing does not crash from thread +# No other guarantees! fig, ax = plt.subplots() # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg plt.pause(0.1) @@ -159,6 +202,7 @@ def thread_artist_work(): ax.plot([1,3,6]) except: # Propagate error to main thread + import sys global exc_info exc_info = sys.exc_info() @@ -167,6 +211,7 @@ def thread_draw_work(): fig.canvas.draw() except: # Propagate error to main thread + import sys global exc_info exc_info = sys.exc_info() @@ -181,7 +226,7 @@ def thread_draw_work(): t = threading.Thread(target=thread_draw_work) fig.canvas.mpl_connect("close_event", print) t.start() -plt.pause(0.1) # flush_events fails on at least Tkagg (bpo-41176) +plt.pause(0.1) # flush_events fails here on at least Tkagg (bpo-41176) t.join() plt.close() fig.canvas.flush_events() # pause doesn't process events after close @@ -189,30 +234,22 @@ def thread_draw_work(): if exc_info: # Raise thread error raise exc_info[1].with_traceback(exc_info[2]) """ -_test_timeout = 10 # Empirically, 1s is not enough on Travis. @pytest.mark.parametrize("backend", _get_testable_interactive_backends()) -@pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) @pytest.mark.flaky(reruns=3) -def test_interactive_backend(backend, toolbar): - if backend == "macosx": - if toolbar == "toolmanager": - pytest.skip("toolmanager is not implemented for macosx.") - if toolbar == "toolbar2" and os.environ.get('TRAVIS'): - # See https://github.com/matplotlib/matplotlib/issues/18213 - pytest.skip("toolbar2 for macosx is buggy on Travis.") - +def test_interactive_thread_safety(backend): proc = subprocess.run( - [sys.executable, "-c", _test_script, - json.dumps({"toolbar": toolbar})], + [sys.executable, "-c", _thread_test_script], env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, timeout=_test_timeout, stdout=subprocess.PIPE, universal_newlines=True) if proc.returncode: + if backend == "wx": + pytest.xfail("Ignoring deprecated wx failure. Use wxagg.") pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.") - assert proc.stdout.count("CloseEvent") == 2 + assert proc.stdout.count("CloseEvent") == 1 @pytest.mark.skipif('TF_BUILD' in os.environ, From bbb82ac438589f5e01b99bc8e70850b48e032272 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Sat, 24 Oct 2020 10:59:50 -0400 Subject: [PATCH 14/19] ignore -> xfail for wx and macosx --- .../tests/test_backends_interactive.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 0d631799b1a0..0fc682102b4b 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -235,20 +235,28 @@ def thread_draw_work(): raise exc_info[1].with_traceback(exc_info[2]) """ - -@pytest.mark.parametrize("backend", _get_testable_interactive_backends()) +_thread_safe_backends = _get_testable_interactive_backends() +# Known unsafe backends. Remove the xfails if they start to pass! +if "wx" in _thread_safe_backends: + _thread_safe_backends.remove("wx") + _thread_safe_backends.append( + pytest.param("wx", marks=pytest.mark.xfail( + raises=subprocess.CalledProcessError, strict=True))) +if "macosx" in _thread_safe_backends: + _thread_safe_backends.remove("macosx") + _thread_safe_backends.append( + pytest.param("macosx", marks=pytest.mark.xfail( + raises=subprocess.TimeoutExpired, strict=True))) + + +@pytest.mark.parametrize("backend", _thread_safe_backends) @pytest.mark.flaky(reruns=3) def test_interactive_thread_safety(backend): proc = subprocess.run( [sys.executable, "-c", _thread_test_script], env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, + timeout=_test_timeout, check=True, stdout=subprocess.PIPE, universal_newlines=True) - if proc.returncode: - if backend == "wx": - pytest.xfail("Ignoring deprecated wx failure. Use wxagg.") - pytest.fail("The subprocess returned with non-zero exit status " - f"{proc.returncode}.") assert proc.stdout.count("CloseEvent") == 1 From 59d5c0635b75a49e72c37298041753bc12641cb9 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Sat, 24 Oct 2020 12:01:13 -0400 Subject: [PATCH 15/19] increase event loop time for CI, cleanup imports --- lib/matplotlib/tests/test_backends_interactive.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 0fc682102b4b..6ccdcd921d63 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -177,7 +177,6 @@ def test_interactive_backend(backend, toolbar): import json import sys import threading -from unittest import TestCase from matplotlib import pyplot as plt, rcParams rcParams.update({ @@ -186,14 +185,12 @@ def test_interactive_backend(backend, toolbar): }) if len(sys.argv) >= 2: # Second argument is json-encoded rcParams. rcParams.update(json.loads(sys.argv[1])) -assert_equal = TestCase().assertEqual -assert_raises = TestCase().assertRaises # Test artist creation and drawing does not crash from thread # No other guarantees! fig, ax = plt.subplots() # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg -plt.pause(0.1) +plt.pause(0.5) exc_info = None @@ -226,7 +223,7 @@ def thread_draw_work(): t = threading.Thread(target=thread_draw_work) fig.canvas.mpl_connect("close_event", print) t.start() -plt.pause(0.1) # flush_events fails here on at least Tkagg (bpo-41176) +plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176) t.join() plt.close() fig.canvas.flush_events() # pause doesn't process events after close From 9255fb6af0958d55b08a3d78636e46a9b430dc60 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Tue, 27 Oct 2020 08:44:32 -0400 Subject: [PATCH 16/19] respond to comments --- lib/matplotlib/backends/_backend_tk.py | 5 ++--- lib/matplotlib/backends/backend_tkagg.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index d97f294bc5b9..326df873214e 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -1,3 +1,4 @@ +import uuid from contextlib import contextmanager import logging import math @@ -46,7 +47,7 @@ def _restore_foreground_window_at_end(): _blit_args = {} # Initialize to a non-empty string that is not a Tcl command -_blit_tcl_name = "29345091836409813" +_blit_tcl_name = "mpl_blit_" + uuid.uuid4().hex def _blit(argsid): @@ -108,12 +109,10 @@ def blit(photoimage, aggimage, offsets, bbox=None): argsid = repr(id(args)) _blit_args[argsid] = args - global _blit_tcl_name try: photoimage.tk.call(_blit_tcl_name, argsid) except tk.TclError: # register _blit with code copied from tkinter.Misc._register - _blit_tcl_name = repr(id(_blit)) + _blit.__name__ photoimage.tk.createcommand(_blit_tcl_name, _blit) photoimage.tk.call(_blit_tcl_name, argsid) diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 8768a729c6d0..ca15bf818142 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1,6 +1,7 @@ +from . import _backend_tk from .backend_agg import FigureCanvasAgg from ._backend_tk import ( - _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk, blit) + _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk) class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk): @@ -9,7 +10,8 @@ def draw(self): self.blit() def blit(self, bbox=None): - blit(self._tkphoto, self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) + _backend_tk.blit( + self._tkphoto, self.renderer._renderer, (0, 1, 2, 3), bbox=bbox) @_BackendTk.export From d1984c5b1ca963710aa82d930e2b75e67ab5bcd1 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Tue, 27 Oct 2020 09:41:46 -0400 Subject: [PATCH 17/19] remove lingering threading import --- lib/matplotlib/tests/test_backends_interactive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 6ccdcd921d63..ecf9c513bd1b 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -71,7 +71,6 @@ def _get_testable_interactive_backends(): import io import json import sys -import threading from unittest import TestCase import matplotlib as mpl From 78a1e43b1016c3a4871e6b6f447d08fdba4f1589 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Tue, 27 Oct 2020 09:55:52 -0400 Subject: [PATCH 18/19] Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/backends/_backend_tk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 326df873214e..afe51e7c5c26 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -52,10 +52,10 @@ def _restore_foreground_window_at_end(): def _blit(argsid): """ - Thin wrapper to blit via tkapp.call + Thin wrapper to blit called via tkapp.call. *argsid* is a unique string identifier to fetch the correct arguments from - the _blit_args dict, since args cannot be passed directly + the ``_blit_args`` dict, since arguments cannot be passed directly. photoimage blanking must occur in the same event and thread as blitting to avoid flickering. @@ -106,7 +106,7 @@ def blit(photoimage, aggimage, offsets, bbox=None): args = photoimage, dataptr, offsets, bboxptr, blank # Need a unique key to avoid thread races. # Again, make the key a string to avoid string parsing in _blit. - argsid = repr(id(args)) + argsid = str(id(args)) _blit_args[argsid] = args try: From 0b1660bfbdf742d795edb1e742c0ee66b40bdef0 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Tue, 27 Oct 2020 10:00:52 -0400 Subject: [PATCH 19/19] narrow TclError handling --- lib/matplotlib/backends/_backend_tk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index afe51e7c5c26..a6389944f3cf 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -111,8 +111,9 @@ def blit(photoimage, aggimage, offsets, bbox=None): try: photoimage.tk.call(_blit_tcl_name, argsid) - except tk.TclError: - # register _blit with code copied from tkinter.Misc._register + except tk.TclError as e: + if "invalid command name" not in str(e): + raise photoimage.tk.createcommand(_blit_tcl_name, _blit) photoimage.tk.call(_blit_tcl_name, argsid)