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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy