ToolManager/Tools adding methods to set figure after initialization #6405

Merged
merged 1 commit into from Jun 9, 2016

Conversation

Projects
None yet
6 participants
Member

fariza commented May 11, 2016

Wanting to cross control figures (tool from one FigureManger affects a figure from other FigureManager) the setting of the figure is separated from the initialization

The changes are :

  • Removing the automatic setting of figure in ToolManager.__init__ and ToolBase.__init__
  • canvas becomes a property that is accessed strictly from figure
  • set_figure is called explicitly on the ToolManager and ToolBase objects

mdboom added the needs_review label May 11, 2016

Member

fariza commented May 12, 2016

Test fail is not related

fariza changed the title from adding methods to set figure after initialization to ToolManager/Tools adding methods to set figure after initialization May 12, 2016

Member

fariza commented May 12, 2016

@OceanWolf can you take a look at this?

Contributor

OceanWolf commented May 15, 2016

I will do my best to find time to look at these two PRs soon

Member

fariza commented May 20, 2016

@OceanWolf just in case you're going to rebase #4143 please take a look at this one first. Just to prevent you having to do two rebases

Member

fariza commented May 20, 2016

@tacaswell I would like to have this to have reviewed by I really don't know who else to ask, maybe you or @efiring?

@OceanWolf OceanWolf and 1 other commented on an outdated diff May 20, 2016

lib/matplotlib/backend_managers.py
+ @property
+ def manager(self):
+ """FigureManager that owns this ToolManager"""
+ return self._manager
+
+ @property
+ def canvas(self):
+ """Canvas managed by FigureManager"""
+ return self._figure.canvas
+
+ @property
+ def figure(self):
+ """Figure that holds the canvas"""
+ return self._figure
+
+ def set_figure(self, figure):
@OceanWolf

OceanWolf May 20, 2016

Contributor

Why not a property setter?

@fariza

fariza May 20, 2016

Member

if I put it as a setter, then when you want to override it you will have to redeclare the property and the setter with it's decorators, if not, the behavior is not the same between the base and the subclass (I can see the problems falling from the sky).

The other option (the one that I normally use) is the base to the declare the property and its setter with private methods that actually do the set and get. And if you need to override this methods, you override the _set_x or _get_x and the behavior is the same because you don't touch the property

@property
def x(self):
   return self._get_x()

@x.setter(self, value):
     self._set_x(value)

def _get_x(self):
    ....

def _set_x(self, value):
    ....
@OceanWolf

OceanWolf May 20, 2016

Contributor

Ahh, that problem, I recall running into it before... I just dislike the asymmetry of a = foo and set_foo(a), rather than foo = a, I find asymmetry makes the API more complicated to learn.

@fariza

fariza May 20, 2016

Member

I agree, and this is a problem that we are going to have everywhere. So we can decide to have set properties defined in the base that call the setters and getters and those are the ones that are specific to each subclass.

@fariza

fariza May 20, 2016

Member

If we agree, it is matter of just adding

@figure.setter
def figure(self, figure):
   self.set_figure(figure)

Is this an acceptable solution?

@OceanWolf

OceanWolf May 20, 2016

Contributor

yes :)

@fariza

fariza May 20, 2016

Member

Ok, I do this as soon as I get home, I have to leave now.

Thanks for the feedback, keep it coming, I want this merged really bad

@OceanWolf OceanWolf and 1 other commented on an outdated diff May 20, 2016

lib/matplotlib/backend_managers.py
@@ -56,14 +56,13 @@ class ToolManager(object):
`LockDraw` object to know if the message is available to write
"""
- def __init__(self, canvas):
+ def __init__(self, manager):
@OceanWolf

OceanWolf May 20, 2016

Contributor

I feel a bit odd about this, we create the ToolManager based on a FigureManager, yet we use getters/setters work directly on the figure, in fact I don't ever see us use the manager passed in here, so it seems pointless as far as I can see, and makes the API more convoluted and difficult to learn. Especially for embedded canvas widgets. Does this bear relation to your multi-figure manager? In which case can you document the use case of passing in a FigureManager here instead of a figure or canvas?

@fariza

fariza May 20, 2016

Member

Nothing stops you from creating a tool that interacts with the FigureManager instead of a Figure and I like that idea wide open.
Think about adding/removing one toolbar from another, this has to go throught the FigureManager
Or for example once we get rid of Gcf ToolQuit and ToolQuitAll will have to talk to manager instead of Gcf

@OceanWolf

OceanWolf May 20, 2016

Contributor

but you can still do that, right, canvas.manager gives you the FigureManager...

@fariza

fariza May 20, 2016

Member

Yes and no.
If we want to be able to control figures from other figure manager, then the canvas.manager will point to another figure manager.

@fariza

fariza May 20, 2016

Member

And it is part of the fun thing, the tools are not tied to the figure in their figuremanager it can be somewhere else.

@OceanWolf

OceanWolf May 20, 2016 edited

Contributor

Not convinced yet, at the moment we can do that anyway, you created the example for #4143 which does exactly that, we have a toolbar in a different window, still very cool btw and shows the power and flexibility of MEP22 and MEP27 combined :D.

The only difference with this, as I see it, means we have two different tools, one that work on a set manager, and one that work on a set figure... and they can work unrelated, so you have tools that work on a specific manager unrelated to the main figure (and of course, you can have tools in the same toolbar pointing to different figures), in which case ToolManager.figure should really read ToolManager.default_figure as we can change the figure that individual tools work with later, as I see it (or do I go too far here)

However the tool selection mechanism will have to come from a specific FigureCanvas source, the keys come from the canvas, right?

I also wonder if this all becomes too complicated. I don't know... will leave it here for now until I hear your response.

@fariza

fariza May 20, 2016

Member

OK. You convinced me

@OceanWolf

OceanWolf May 20, 2016

Contributor

I did? What did I convince you of and how did I do it?

@fariza

fariza May 21, 2016

Member

You are right it becomes too complicated for little benefit.

If latter we need it, it is no big deal to add it

tacaswell added this to the 2.1 (next point release) milestone May 21, 2016

Member

fariza commented May 22, 2016

@OceanWolf i got rid of the manager reference for toolmanager and added the setters for for the figures.

Contributor

OceanWolf commented May 23, 2016 edited

Looks much better. One idea I had, but again perhaps too complicated (and can easily get added later), making it

ToolManager.set_figure(self, figure, update_tools=False):
  # snip

  if update_tools:
    for tool in self.tools:
      tool.figure = figure

the most complicated part here comes from deciding whether the kw should default to True, or to False, and thus how toolmgr.figure = my_figure would work.

Member

fariza commented May 24, 2016

With the second argument the property setter stops working.
Let's leave it for later

Contributor

OceanWolf commented May 24, 2016

It doesn't stop working, because it gets passed as kwarg.

Member

fariza commented May 24, 2016

What I mean is that it is impossible to change the value by calling the property setter

Contributor

OceanWolf commented May 24, 2016

Sure, but you have declared set_figure as public. We just need to decide the default behaviour. I guess default to True.

Member

fariza commented May 24, 2016

Done, I tried and it looks better than I thought.

Contributor

OceanWolf commented May 24, 2016

Yay, fabulous! I have no further comments. Anyone else?

@efiring efiring commented on an outdated diff May 24, 2016

lib/matplotlib/backend_managers.py
@@ -74,6 +73,43 @@ def __init__(self, canvas):
self.keypresslock = widgets.LockDraw()
self.messagelock = widgets.LockDraw()
+ if figure:
+ self.figure = figure
@efiring

efiring May 24, 2016

Owner

This may be nit-picking, but I would put the initialization of self._figure right above this, and instead of using the property I would use the setter. To me, eliminating that indirection would make the code more readable with no cost.

@efiring efiring and 2 others commented on an outdated diff May 24, 2016

lib/matplotlib/backend_managers.py
+ def canvas(self):
+ """Canvas managed by FigureManager"""
+ return self._figure.canvas
+
+ @property
+ def figure(self):
+ """Figure that holds the canvas"""
+ return self._figure
+
+ @figure.setter
+ def figure(self, figure):
+ self.set_figure(figure)
+
+ def set_figure(self, figure, update_tools=True):
+ """
+ Set the figure that interact with tools
@efiring

efiring May 24, 2016

Owner

"interact" -> "interacts"

@OceanWolf

OceanWolf May 24, 2016

Contributor

"Sets the figure to interact with the tools"?

@efiring

efiring May 24, 2016

Owner

On 2016/05/24 12:20 PM, OceanWolf wrote:

"Sets the figure to interact with the tools"?

Fine.

@QuLogic

QuLogic May 25, 2016

Member

Isn't this statement a bit backwards? That is, the tools interact with the figure, and not the other way around, as specified here?

@efiring efiring commented on an outdated diff May 24, 2016

lib/matplotlib/backend_tools.py
@property
def figure(self):
return self._figure
+ @figure.setter
+ def figure(self, figure):
+ self.set_figure(figure)
+
+ @property
+ def canvas(self):
+ return self.figure.canvas
+
+ @property
+ def toolmanager(self):
+ return self._toolmanager
+
+ def set_figure(self, figure):
+ """
+ Called when the figure is setted
@efiring

efiring May 24, 2016

Owner

Maybe "Assign a figure to the tool"? Or omit; the function is obvious from the name.

@efiring efiring and 1 other commented on an outdated diff May 24, 2016

lib/matplotlib/backend_tools.py
+ self.set_figure(figure)
+
+ @property
+ def canvas(self):
+ return self.figure.canvas
+
+ @property
+ def toolmanager(self):
+ return self._toolmanager
+
+ def set_figure(self, figure):
+ """
+ Called when the figure is setted
+
+ This method allows to perform special settings for tools with special
+ needs
@efiring

efiring May 24, 2016

Owner

Maybe replace with "Some tools may need to override this."

@OceanWolf

OceanWolf May 24, 2016

Contributor

Do we need any of this? If a user needs to override a method, shouldn't it become obvious that they need to do so? Basic OOPython, right?

@efiring

efiring May 24, 2016

Owner

Fine with me to eliminate the whole docstring.

Member

fariza commented May 24, 2016

@efiring I updated the docstrings

Member

fariza commented May 25, 2016

@QuLogic the interaction is bidirectional

Member

fariza commented Jun 2, 2016

@efiring @OceanWolf can we merge?

Contributor

OceanWolf commented Jun 2, 2016

Fine by me!

@efiring efiring commented on an outdated diff Jun 2, 2016

lib/matplotlib/backend_tools.py
@property
def figure(self):
return self._figure
+ @figure.setter
+ def figure(self, figure):
+ self.set_figure(figure)
+
+ @property
+ def canvas(self):
+ return self.figure.canvas
@efiring

efiring Jun 2, 2016

Owner

Rather than have the extra indirection via the property, this could be self._figure.canvas.

@efiring

efiring Jun 2, 2016

Owner

Here, too, it seems like we have a situation where accessing a property can lead to an obscure exception.

@efiring efiring and 2 others commented on an outdated diff Jun 2, 2016

lib/matplotlib/backend_managers.py
@@ -74,6 +72,44 @@ def __init__(self, canvas):
self.keypresslock = widgets.LockDraw()
self.messagelock = widgets.LockDraw()
+ self._figure = None
+ if figure:
+ self.set_figure(figure)
+
+ @property
+ def canvas(self):
+ """Canvas managed by FigureManager"""
+ return self._figure.canvas
@efiring

efiring Jun 2, 2016

Owner

This looks odd to me: if a default ToolManager is created and its first property, canvas, is accessed, it will raise AttributeError because self._figure will be None. Presumably this won't happen ordinarily, but it just doesn't look right. Maybe canvas should trap it and raise a more informative exception.

@OceanWolf

OceanWolf Jun 2, 2016

Contributor

Nice catch, but perhaps we could just return None.

return self._figure.canvas if self._figure else None
@fariza

fariza Jun 2, 2016

Member

I like the idea of returning None

@OceanWolf OceanWolf commented on the diff Jun 2, 2016

lib/matplotlib/backend_managers.py
+
+ @property
+ def canvas(self):
+ """Canvas managed by FigureManager"""
+ return self._figure.canvas
+
+ @property
+ def figure(self):
+ """Figure that holds the canvas"""
+ return self._figure
+
+ @figure.setter
+ def figure(self, figure):
+ self.set_figure(figure)
+
+ def set_figure(self, figure, update_tools=True):
@OceanWolf

OceanWolf Jun 2, 2016

Contributor

figure=None to match the class init method? Not 100% sure on this though, thinking of someone doing an array iteration calling this method and would want a value like this to mean no-change, but I err towards the feeling that they should handle that themselves.

@fariza

fariza Jun 3, 2016 edited

Member

Internally None is a valid value, so it is possible to set the figure to None, but it has to be done explicitly

@OceanWolf OceanWolf commented on an outdated diff Jun 2, 2016

lib/matplotlib/backend_managers.py
+ def set_figure(self, figure, update_tools=True):
+ """
+ Sets the figure to interact with the tools
+
+ Parameters
+ ==========
+ figure: `Figure`
+ update_tools: bool
+ Force tools to update figure
+ """
+
+ if self._key_press_handler_id:
+ self.canvas.mpl_disconnect(self._key_press_handler_id)
+ self._figure = figure
+ self._key_press_handler_id = self.canvas.mpl_connect(
+ 'key_press_event', self._key_press)
@OceanWolf

OceanWolf Jun 2, 2016

Contributor

But I think we should definitly allow the user to reset to None by explicitly passing in None, otherwise, we cannot return to the default original state. Thus we should match the if statement from the init here. Maybe a function is_bound or something to make this test more changeable in the future.

Member

fariza commented Jun 2, 2016

@efiring that was a good catch, I modified the code so, it returns None instead of rising an error.
Also, I do a check for existence before connecting again when figure is setted

@OceanWolf OceanWolf and 1 other commented on an outdated diff Jun 3, 2016

lib/matplotlib/backend_managers.py
@@ -245,6 +283,8 @@ def add_tool(self, name, tool, *args, **kwargs):
else:
self._toggled.setdefault(tool_obj.radio_group, None)
+ if self.figure:
+ tool_obj.set_figure(self.figure)
@OceanWolf

OceanWolf Jun 3, 2016

Contributor

Do we need these two lines? The tool inits with ToolBase._figure = None

@fariza

fariza Jun 3, 2016

Member

Yes the tools initialize with None, but when addding a tool, it is better to set the figure to the current figure of the Toolmanager, we could add a kwarg to add_tool to bypass the tool.set_figure but I find it a little bit overkill

@OceanWolf

OceanWolf Jun 3, 2016

Contributor

Sorry, I said that awkwardly, I meant to say "do we really need the if?", i.e. keep the second line in there. If self.figure == None, then the call to tool_obj.set_figure(self.figure) just does a no-op...

@fariza

fariza Jun 3, 2016

Member

you're right, Done

Member

fariza commented Jun 6, 2016

I just removed the last check for figure that I forgot in the __init__ method
Anything else?

Federico Ariza ToolManager and Tools can be initialized without tools
5d73e9f
Member

fariza commented Jun 8, 2016

I think this is ready to merge

@efiring efiring merged commit e69e565 into matplotlib:master Jun 9, 2016

2 of 3 checks passed

coverage/coveralls Coverage decreased (-0.03%) to 69.611%
Details
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

efiring removed the needs_review label Jun 9, 2016

Contributor

OceanWolf commented Jun 9, 2016

Ahh, I was going to say squash the commits first, but okay.

@fariza which other PR did you want me to look at?

fariza deleted the fariza:tools-set-figure-by-event branch Jun 11, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment