Skip to content

ENH: Switch to a private, simpler AxesStack. #7377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2017

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Nov 2, 2016

attn @NelleV

The current implementation of AxesStack subclasses cbook.Stack, which
requires hashable keys, which leads to additional complexity on the
caller's side (_make_key).

Instead, switch to using two lists (keys and axes) and relying on
list.index, which makes the implementation much simpler.

Also make the new class private and deprecate the previous one.

(The original idea was to make AxesStack private to avoid having to deal with issues such as #7194. I originally thought that we could just replace it by an OrderedDict, which is still an option, but you need a really need a two-way dict to implement remove-by-value (instead of by key), so I ended up dropping that idea -- the benefit of dropping _make_key looked more enticing.)

@NelleV NelleV self-assigned this Nov 2, 2016
@NelleV
Copy link
Member

NelleV commented Nov 2, 2016

If you want a simpler PR at first, you can make one that deprecates the public AxesStack and "just" creates the private one. We can go from there in simplifing the API.
(else you'll have to fix the failing tests :) )

I'd wait on having the deprecation of the AxesStack vetted by others before moving forward. I think make AxesStack private is a good idea: it is only used in the figure object right now, in a private attribute, and probably aimed at being private from the start.
attn @efiring @tacaswell

@@ -157,6 +158,60 @@ def __contains__(self, a):
return a in self.as_list()


class _AxesStack:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to inherit from (object) (for now).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@tacaswell
Copy link
Member

👍 in principle but we should try to support as much of the old API as possible.

I know I have written SO answers which reached in and touched the AxesStack....

The balance against making everything private is that if you do not give users enough public knobs to get done what they need to get done, they will find the private knobs and just do it. Once enough user are using a private API it become defacto public and we are as bad off or worse than we would have been other wise.

@tacaswell tacaswell added this to the 2.1 (next point release) milestone Nov 2, 2016
@anntzer anntzer force-pushed the private-axesstack branch 2 times, most recently from 9f227e3 to acd4bb2 Compare November 2, 2016 05:29
@anntzer
Copy link
Contributor Author

anntzer commented Nov 2, 2016

@NelleV Hopefully everything should be fixed now (I didn't realize that I need to keep track both of the axes stack and of the order in which the axes were created).

@tacaswell The only reference to AxesStack I could find on SO is http://stackoverflow.com/questions/24104990/how-to-get-a-list-of-axes-for-a-figure-in-pyplot which somehow ironically ends with your comment

fig is a Figure object. I am not sure where AxesStack gets used, but you should probably not be using it.

In any case this PR literally doesn't change any public API -- the old AxesStack class is still there, it's just used nowhere :-) (and 1. the AxesStack object instantiated by the Figure class was a private attribute, so it's fair game to change it and 2. in fact, the new private AxesStack implements (nearly) the same API as the old one...).

Honestly if we start having to worry about modifying private APIs then I don't know what's left to do other than adding features over features and never fixing old stuff because, well, it may always break someone else who is playing with the internals (that person should have asked for a public API, or known better). Again, I'm not removing AxesStack for now; if someone complains that they were actually using it, we can always consider undeprecating it.

@anntzer anntzer force-pushed the private-axesstack branch 3 times, most recently from 7eb590d to a288d93 Compare November 2, 2016 08:42
@NelleV
Copy link
Member

NelleV commented Nov 10, 2016

The code looks good to me.
I don't think we loose any functionality with this switch and the code is much cleaner.

return next((ax for _, k, ax in self._items if k == key), None)

def current_key_axes(self):
"""Return the topmost key, axes pair, or `None, None` if empty.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topmost? Is that a word I don't know?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add some parentheses because right now it can read as either "a key" or "an axes pair" or None, None, but it's really "(key, axes) pair" or None, None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, fixed

@efiring
Copy link
Member

efiring commented Nov 10, 2016

Also related to #7335.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a silly question, but is the only difference between this and an OrderedDict that it can do operations based on the value instead of the key? A comment/doc to that effect would be useful for the future.

return key, ax

def add(self, key, ax):
"""Append a key, axes pair, unless the axes are already present.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add parentheses on this one too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

def as_list(self):
"""Copy of the list of axes, in the order of insertion.
"""
return [ax for _, _, ax in sorted(self._items)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's originally sorted this way, but I wonder if that's even necessary...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "keep creation order" behavior is mandated by the tests right now, and I don't see a reason to get rid of it.

@anntzer anntzer force-pushed the private-axesstack branch 2 times, most recently from 4c0ce14 to 1143621 Compare November 11, 2016 01:34
@anntzer
Copy link
Contributor Author

anntzer commented Nov 11, 2016

@QuLogic also because keys may not be hashable. Added a comment to that effect.

@tacaswell
Copy link
Member

@anntzer It as always fun when 2.5 year old comments come back at me ;)

@NelleV
Copy link
Member

NelleV commented Nov 11, 2016

This PR clearly supersedes #7335, thought I think the latter is 2.0 safe while this one isn't.

def get(self, key):
"""Find the axes corresponding to a key; defaults to `None`.
"""
return next((ax for _, k, ax in self._items if k == key), None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anntzer I consistently learn new python details from your code.

"""
# Skipping existing Axes is needed to support calling `add_axes` with
# an already existing Axes.
if not any(a == ax for _, _, a in self._items):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to handle the duplicate key case?

Copy link
Contributor Author

@anntzer anntzer Nov 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each call to add is actually preceded by a call to get, so technically we can rely on the caller code to never try to add in a duplicate key. Also I worded the docstring carefully: the pair is not appended if the axes is already present but there's no check on whether the key is :-)

In practice I could make the behavior (no checking for duplicate key) clearer in the docstring, or add a check and error out if the key is already there; either is fine with me. Please let me know your preference.

@tacaswell
Copy link
Member

👍 One small concern about the duplicate key, other wise LGTM

I strongly prefer to not backport this for 2.0.

@NelleV
Copy link
Member

NelleV commented Nov 11, 2016

We should never have duplicate keys, so what should be the expected behaviour? Raise an error?

@tacaswell
Copy link
Member

Lets go with raise (and keep the raise / API change in #7335).

@QuLogic
Copy link
Member

QuLogic commented Nov 11, 2016

Hmm, so I just thought of something that the key function actually handles that isn't here, namely, that elements of args and kwargs may be mutable, which may not be something you want in the key. For example:

rect = np.array([0.2, 0.2, 0.2, 0.2])
ax1 = fig.add_axes(rect, ...)
rect[:2] += 0.2
ax2 = fig.add_axes(rect, ...)

ax2 should be new, but I think it will return ax1 with this class.

@anntzer
Copy link
Contributor Author

anntzer commented Nov 11, 2016

In fact I just realized there's a much bigger issue here: you can't use == to compare arrays for full equality, so something as simple as

plt.axes(np.array(...)); plt.axes(np.array(...))

will fail with

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

I guess there's no way around having some variant of make_key :-(

@tacaswell
Copy link
Member

do we need to support mutable/non-hashable keys?

@efiring
Copy link
Member

efiring commented Nov 11, 2016

On 2016/11/11 8:23 AM, Antony Lee wrote:

I would like to claim no, of course... in fact I would even like to
claim that the whole

|If the figure already has an axes with the same parameters, then it will
simply make that axes current and return it. |

behavior is seriously misguided.

I suspect we all agree on this. I think it is the result of starting
off following Matlab long ago for the sake of compatibility. Then our
code and documentation get ever more tied in knots as we go beyond
Matlab while trying to maintain compatibility with a bad API. Maybe we
need to be a little more willing to shed such relics. How much user
code is likely to rely on this?

@anntzer
Copy link
Contributor Author

anntzer commented Nov 11, 2016

If you're willing to break backcompatibility here, the good thing is that's there's an obvious deprecation path:

  1. merge fix the stack remove error #7335 as a stopgap measure and 2) raise a deprecation warning whenever a key collision occurs
    A few (cough... one... cough) releases later,
  2. entirely drop that behavior (i.e. create a new axes every time).

By the way I assume(?) it is possible to change the rectangle where an axes sit, in which case that provides another way to break the current implementation (create an axes, move it somewhere else, and either try to create another axes where the first one was, or try to create another axes where the first one is currently).

In fact even MATLAB doesn't provide that behavior:

>> axes('Position', [.1 .1 .7 .7]); axes('Position', [.1, .1, .7, .7]); get(gcf, 'children')

ans = 

  2×1 Axes array:

  Axes
  Axes

return self._axstack.as_list()

axes = property(fget=_get_axes, doc="Read-only: list of axes in Figure")
axes = property(lambda self: self._axstack.as_list(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be

    @property
    def axes(self):
        """Read-only: list of axes in Figure"""
        return self._axstack.as_list()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think one is more readable that the other?

@NelleV
Copy link
Member

NelleV commented Jan 17, 2017

Is there any interest in pushing this forward?

@anntzer anntzer force-pushed the private-axesstack branch 2 times, most recently from f21c036 to 9b7bfdc Compare January 17, 2017 18:12
@anntzer
Copy link
Contributor Author

anntzer commented Jan 17, 2017

Why not :-)
Rebased, and updated the deprecation warning to 2.1.

@codecov-io
Copy link

codecov-io commented Jan 17, 2017

Current coverage is 62.01% (diff: 97.05%)

Merging #7377 into master will decrease coverage by 0.09%

@@             master      #7377   diff @@
==========================================
  Files           174        174          
  Lines         56065      56068     +3   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
- Hits          34820      34770    -50   
- Misses        21245      21298    +53   
  Partials          0          0          

Powered by Codecov. Last update fa95b58...9b7bfdc

Copy link
Member

@NelleV NelleV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good. I'd rather merge this sooner than later, if something is terribly wrong, we might pick it up before the 2.1 release.

@NelleV NelleV changed the title Switch to a private, simpler AxesStack. [MRG+1] Switch to a private, simpler AxesStack. Jan 20, 2017
@anntzer
Copy link
Contributor Author

anntzer commented Jan 20, 2017

Actually, see the discussion starting at #7377 (comment). I think the issue mentioned by @QuLogic cannot really be fixed with the current implementation so perhaps we should instead follow the path mentioned in #7377 (comment) (see comment by @efiring re: backcompatibility, too).

@tacaswell
Copy link
Member

This needs a rebase.

The current implementation of AxesStack subclasses cbook.Stack, which
requires hashable keys, which leads to additional complexity on the
caller's side (`_make_key`).

Instead, switch to using two lists (keys and axes) and relying on
`list.index`, which makes the implementation much simpler.

Also make the new class private and deprecate the previous one.
@anntzer anntzer force-pushed the private-axesstack branch from 9b7bfdc to 0022611 Compare August 10, 2017 13:31
@anntzer
Copy link
Contributor Author

anntzer commented Aug 10, 2017

done

@tacaswell tacaswell changed the title [MRG+1] Switch to a private, simpler AxesStack. ENH: Switch to a private, simpler AxesStack. Aug 13, 2017
@tacaswell tacaswell merged commit 4c33d97 into matplotlib:master Aug 13, 2017
@QuLogic
Copy link
Member

QuLogic commented Aug 13, 2017

Are we not worried about #7377 (comment)?

@anntzer anntzer deleted the private-axesstack branch August 13, 2017 14:07
@tacaswell
Copy link
Member

@QuLogic I missed that when I was re-reading the comments last night, sorry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants