Skip to content

_AnnotationBase keeps copy of mutable parameter (issue #17566) #17567

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

Closed
wants to merge 5 commits into from

Conversation

azelcer
Copy link

@azelcer azelcer commented Jun 4, 2020

Fixes #17566

PR Summary

_AnnotationBase now keeps a copy of the data passed as the xy parameter, instead of the same object. Otherwise, passing an ndarray as xy and changing its elements later unexpectedly changes the annotation.
It is a one-line change, so I have performed no extensive tests beside basic functionality yet

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@tacaswell
Copy link
Member

Thanks for working on this! Unfortunately the fix is not this simple :(

We can work with user data passed in with units attached and casting through an array like this can drop that data. I think we should ether do all of the unit work up-front (and the in the set_ method) or do a copy.copy(xy) and leave the rest of the machinery alone.

@azelcer
Copy link
Author

azelcer commented Jun 4, 2020

I though something like this could happen (the "point" class I mentioned in #17566).

The fast way is copy.copy(xy), but I did not want to add an import. Doing all the unit work up-front and in the set_ command is cleaner only if it's done by a helper function that sanitizes and "normalizes" the input to a canonical type. I am not familiar with the codebase but I can check.

If this helper function uses a canonical type, some code could be moved away from the part which uses self.xy to the helper function (and maybe to any get_ command). I can try to spot this in text.py and any classes defined there.

So I think we have a quick way (copy.copy, 1 import, 1 line changed) and a long way (take care early, helper function, documentation, extensive testing, etc). None of this is in a critical execution path and neither a lot of memory is involved, so it depends on what is expected for this interface in future.

Please tell me which way to go and I try to take care of this (in text.py only)

Note that a similar issue is present in the helper class OffsetFrom

@tacaswell
Copy link
Member

Adding an import is not a problem.

@azelcer
Copy link
Author

azelcer commented Jun 4, 2020

I updated my annotate_bug branch using copy.copy both in _AnnotationBase and OffsetFrom __init__ methods.

I will see if I find something similar in other files, but it's a huge codebase (I think ConnectionPatch in patches.py has the same issue). Maybe is better to put the PR on hold and sweep many files

@tacaswell
Copy link
Member

Could you also add a test? This seems like a good case for https://matplotlib.org/api/testing_api.html#matplotlib.testing.decorators.check_figures_equal (make two figures that you do the same thing to and then mutate one of the inputs).

I am happy to have many small PRs rather than 1 big PR. Smaller PRs are easier to review and merge. If any of the things you fix get more complicated (which is a thing that happens often when you start fixing things in the mpl codebase...) we don't want to hold up the easy / non-controversial improvements while we sort out the harder stuff.

@tacaswell tacaswell added this to the v3.4.0 milestone Jun 5, 2020
@azelcer
Copy link
Author

azelcer commented Jun 6, 2020

OK. I'll take care of this in the next week (I have to learn how to implement the tests). I've already found the same issue on other modules, so maybe I'll do one PRs for each file.

@azelcer
Copy link
Author

azelcer commented Jul 24, 2020

I think this is ready for review. I will move on to another file, but I have already noted non-uniform API behavior in the sense that some interfaces are ready for data with units while others are not. I do not know if I should discuss this here in the PR on the associated bug.

Coerce the received parameter to tuple instead of making a copy

Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@azelcer
Copy link
Author

azelcer commented May 25, 2021

I have tested that the coercion to tuple approach works, and I do have strong objections against the copy approach because:

  • The copied object can be anything. It can take an unknown amount of resources, and we just make a copy of it (uncomment the line in the code below).
  • I think the expected behavioris to take the values at the moment of the annotation creation, but in the case of OffsetFrom, the values might be reevaluated each time the figure is redrawn. It is a strange case, but it is my opinion that it should be avoided, and just for coherence the same approach should be used everywhere.

To make this last point clear, I think we should avoid that code like the following render a different image each time the figure is redrawn:

import matplotlib.pyplot as plt
from matplotlib.text import OffsetFrom
import numpy as np
import datetime

class mock_xy:
    def __init__(self, seconds_per_trip=60):
        #self._bigarray = np.linspace(0, 1000000, 300000000, dtype=np.complex256)
        self._spt = int(seconds_per_trip)
        pass
    def __getitem__(self, index):
        now = datetime.datetime.now()# we could connect to a time server here
        angulo = 2.*np.pi*(now.second % self._spt + now.microsecond*1E-6)/self._spt
        if index == 0:
            return np.cos(angulo)+.5
        elif index == 1:
            return np.sin(angulo)+.5
        else:
            raise IndexError

fig = plt.figure("Aw[ful, esome]")

ax = fig.add_axes([0.13, 0.15, .8, .8])
ax.set_xlim(-5, 5)
ax.set_ylim(-3, 3)

clock_face = plt.Circle((0.0, 0.0), 1, color='blue')
ax.add_patch(clock_face)
xy_0 = np.array((-0, 0))
offset_from = OffsetFrom(clock_face, mock_xy(5))
ann = ax.annotate("around\nthe clock", xy=(0, 0), xycoords="data",
                  xytext=(0, 0), textcoords=offset_from,
                  va="top", ha="center",
                  bbox=dict(boxstyle="round", fc="w"),
                  arrowprops=dict(arrowstyle="<-"))

With the copy approach, it works as a timer that moves it is redrawn (try resizing the window and watch the timer tick). I think it should instead show the annotation at the same place every time.

It's my last attempt to avoid the copy approach. If that's the path the main developers prefer, let me know and I'll adjust the code accordingly.

@QuLogic
Copy link
Member

QuLogic commented May 27, 2021

This probably needs a rebase to get tests working again.

@timhoffm
Copy link
Member

It'd be nicer to coerce to tuple if possible. However, I'm not an expert in units handling, so I cannot judge if coercing to tuple can cause issues there.

OTOH, I don't see a fundamental problem with copying. If we want to persist state, we need to make a copy of the object. It's highly unlikely that an object representing xy will consume significant resources. I consider the example somewhat artificial. By doing a copy, we isolate from future changes of the original object passed in. Here, the constructed object changes without user interaction, so it's not clear to me what the expected behavior should be.

@anntzer
Copy link
Contributor

anntzer commented May 30, 2021

Actually I believe that casting to tuple is just fine even for unit support (and much simpler), because this simply assumes that we can iterate over xy, and we already assume that iteration is safe (that's how errorbar used to handle units for example (something like [z + zerr for z, zerr in zip(y, yerr)]); we also already do x, y = xytext further down the AnnotationBase code which likewise assumes that iteration is safe).

@QuLogic QuLogic modified the milestones: v3.5.0, v3.6.0 Aug 23, 2021
@timhoffm timhoffm modified the milestones: v3.6.0, unassigned Apr 30, 2022
@story645 story645 removed this from the unassigned milestone Oct 6, 2022
@story645 story645 added this to the needs sorting milestone Oct 6, 2022
@github-actions
Copy link

github-actions bot commented Aug 4, 2023

Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Aug 4, 2023
@melissawm
Copy link
Member

It seems that #26466 supersedes this PR, so I am closing.

@azelcer I am sorry this didn't work out - please feel free to take a look at other open issues and let us know if you need help with your contributions. Cheers!

@melissawm melissawm closed this Aug 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Waiting for author
Development

Successfully merging this pull request may close these issues.

Updating an array passed as the xy parameter to annotate updates the anottation
8 participants