Possible bug in matplotlib.cbook.CallbackRegistry

Here is a bit more detail and a simple example.

The example below places red squares in an axes. When the user clicks on an existing red square - another square is created and added. When the user hits any key a square is deleted from the axes. The error is triggered by clicking on the red square and then hitting any key, and then clicking a red square again.

Diagnosis:
By monitoring cbook.py line 235 and cbook.py line 263 it can be seen that after the second mouse click (following one of the squares being deleted), that the process() function builds a loop and begins handling the button press callbacks. Note that there is a dead reference coming later in this list. The first callback involves another square being created and the connect() method being called. In the connect() call - the dead reference is deleted from the callback list. Now upon returning to the process() callback this dead reference is no longer in the callback list and a Key Exception is triggered once it gets to it in the loop.

Problem:
There are two locations where dead references are cleared from the callback list. When these loops get intermingled - as the case with a callback leading to another connect mid-loop - the exception occurs when both loops attempt to delete the dead reference.

Possible Solutions:
1. Trap the KeyException at the point of attempting to delete it from the list in both places.

2. Place the dead reference check and deletion within a single method ... and perform this check at beginning of the process() and connect() methods before processing callbacks.

Sample Code

···

--------------------

import matplotlib
matplotlib.use('WXAGG')

from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.pyplot import Figure, Axes, Rectangle

import wx
import random

class SquareManager(object):
    def __init__(self, axes):
        self.axes = axes
        self.canvas = axes.figure.canvas
        self.squares = []
        self.last_x = 0
        
        self.canvas.mpl_connect('key_press_event', self.on_key_press)
        
    def add_square(self):
        self.last_x += .1
        s = Square(self, self.axes, [self.last_x, .4],
                   .05, .05, facecolor='red', edgecolor='black')
        self.squares.append(s)
        self._refresh()
        
    def on_key_press(self, evt):
        if len(self.squares) == 0: return
        
        # delete the first square - results in no error
        # self.squares[0].remove()
        # del self.squares[0]
        
        # delete the last square - results in the error
        self.squares[-1].remove()
        del self.squares[-1]
        
        self._refresh()
        
    def _refresh(self):
        self.canvas.draw()

class Square(Rectangle):
    def __init__(self, manager, axes, *args, **kwds):
        Rectangle.__init__(self, *args, **kwds)
        axes.add_patch(self)
        
        self.manager = manager
        axes.figure.canvas.mpl_connect('button_press_event', self.selected)
        
    def selected(self, evt):
        within, _ = self.contains(evt)
        if within:
            self.manager.add_square()

app = wx.PySimpleApp()
frame = wx.Frame(None)
fig = Figure()
canvas = FigureCanvasWxAgg(frame, -1, fig)
a = Axes(fig, [.1, .1, .8, .8])
fig.add_axes(a)

sm = SquareManager(a)
sm.add_square()
    
frame.Show()
app.MainLoop()

# To demonstrate the error:
#
# 1. click on red sqaure
# 2. press any key
# 3. click on red sqaure again

I've run into this same issue in the past, and have it "fixed" in my
own local copy of matplotlib. I just placed a check to make sure that
cid actually was in the callbacks for s before deleting it.

That is quite possibly a band-aid; I never looked far enough in to
see. But it is possibly another way of fixing the problem.

    def process(self, s, *args, **kwargs):
        """
        process signal *s*. All of the functions registered to receive
        callbacks on *s* will be called with *\*args* and *\*\*kwargs*
        """
        if s in self.callbacks:
            for cid, proxy in self.callbacks[s].items():
                # Clean out dead references
                if proxy.inst is not None and proxy.inst() is None:
                   if cid in self.callbacks[s]: #<------- here
                    del self.callbacks[s][cid]
                else:
                    proxy(*args, **kwargs)

···

On Tue, Oct 25, 2011 at 1:09 PM, <tobin@...1028...> wrote:

Here is a bit more detail and a simple example.

The example below places red squares in an axes. When the user clicks on an existing red square - another square is created and added. When the user hits any key a square is deleted from the axes. The error is triggered by clicking on the red square and then hitting any key, and then clicking a red square again.

Diagnosis:
By monitoring cbook.py line 235 and cbook.py line 263 it can be seen that after the second mouse click (following one of the squares being deleted), that the process() function builds a loop and begins handling the button press callbacks. Note that there is a dead reference coming later in this list. The first callback involves another square being created and the connect() method being called. In the connect() call - the dead reference is deleted from the callback list. Now upon returning to the process() callback this dead reference is no longer in the callback list and a Key Exception is triggered once it gets to it in the loop.

Problem:
There are two locations where dead references are cleared from the callback list. When these loops get intermingled - as the case with a callback leading to another connect mid-loop - the exception occurs when both loops attempt to delete the dead reference.

Possible Solutions:
1. Trap the KeyException at the point of attempting to delete it from the list in both places.

2. Place the dead reference check and deletion within a single method ... and perform this check at beginning of the process() and connect() methods before processing callbacks.

Sample Code
--------------------

import matplotlib
matplotlib.use('WXAGG')

from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg
from matplotlib.pyplot import Figure, Axes, Rectangle

import wx
import random

class SquareManager(object):
def __init__(self, axes):
self.axes = axes
self.canvas = axes.figure.canvas
self.squares = []
self.last_x = 0

   self\.canvas\.mpl\_connect\(&#39;key\_press\_event&#39;, self\.on\_key\_press\)

def add_square(self):
self.last_x += .1
s = Square(self, self.axes, [self.last_x, .4],
.05, .05, facecolor='red', edgecolor='black')
self.squares.append(s)
self._refresh()

def on_key_press(self, evt):
if len(self.squares) == 0: return

   \# delete the first square \- results in no error
   \# self\.squares\[0\]\.remove\(\)
   \# del self\.squares\[0\]

   \# delete the last square \- results in the error
   self\.squares\[\-1\]\.remove\(\)
   del self\.squares\[\-1\]

   self\.\_refresh\(\)

def _refresh(self):
self.canvas.draw()

class Square(Rectangle):
def __init__(self, manager, axes, *args, **kwds):
Rectangle.__init__(self, *args, **kwds)
axes.add_patch(self)

   self\.manager = manager
   axes\.figure\.canvas\.mpl\_connect\(&#39;button\_press\_event&#39;, self\.selected\)

def selected(self, evt):
within, _ = self.contains(evt)
if within:
self.manager.add_square()

app = wx.PySimpleApp()
frame = wx.Frame(None)
fig = Figure()
canvas = FigureCanvasWxAgg(frame, -1, fig)
a = Axes(fig, [.1, .1, .8, .8])
fig.add_axes(a)

sm = SquareManager(a)
sm.add_square()

frame.Show()
app.MainLoop()

# To demonstrate the error:
#
# 1. click on red sqaure
# 2. press any key
# 3. click on red sqaure again

------------------------------------------------------------------------------
The demand for IT networking professionals continues to grow, and the
demand for specialized networking skills is growing even more rapidly.
Take a complimentary Learning@...1030... Self-Assessment and learn
about Cisco certifications, training, and career opportunities.
http://p.sf.net/sfu/cisco-dev2dev
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

--
Daniel Hyams
dhyams@...149...