-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Rationalise artist get_figure methods; make figure attribute a property #28177
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
Conversation
21a68c0 to
02de9fa
Compare
lib/matplotlib/artist.py
Outdated
| return the root Figure for a nested tree of SubFigures. | ||
| """ | ||
| if root: | ||
| return self._figure._figure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this hold in general, i.e. also for deeper subfigure nestings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My test has the axes on the second subfigure down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I now see that this is technically correct: From #28170 (comment)
[for subfigures] I explicitly set
self.figure = parent.figurewhich therefore resolves to the root figure, and I think that is because artists have things likeself.figure.staleandself.figure.suppressCompositewhich would somehow have to magically thread themselves through.
But still needs some explanation, because it looks confusing: self._figure is the enclosing (sub)figure. and the _figure attribute of that is always the root figure. Maybe it helps to rename Artist._figure to Artist._enclosing_figure. Also, the second access is to a private attribute of (sub)figures, which general artists have no business with - we're relying on an implementation detail of a different class. Possibly a better spelling would be
| return self._figure._figure | |
| return self._enclosing_figure.get_figure(root=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go with _root_figure to make the tree structure clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh OK, it never occurred to me that we shouldn't use private attributes on another class in the same package. But that means we shouldn't use self._enclosing_figure._root_figure so @jklymak have I misunderstood your comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jklymak Am I correct here? Currently the _figure attribute semantics is
_root_figurefor subfigures_enclosing_figurefor all other artists- self for Figure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought you meant the _root_figure by _enclosing_figure. Maybe call the artists' subfigure the _parent_figure or _parent_subfigure if we want to be more specific. I'd keep with standard tree terminology? https://en.wikipedia.org/wiki/Tree_(data_structure)#:~:text=A%20node%20that%20has%20a,same%20parent%20are%20sibling%20nodes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think _parent_figure as it may be either a Figure or a SubFigure. I note also that we already have SubFigure.parent, so it's good to be consistent with that.
|
Next question: what should |
|
Yes, the root figure of a Figure is the figure itself. |
Right, so there is no ambiguity if |
|
It should return the (sub)figure one level above, which in this case is the root figure. |
7345058 to
73c0840
Compare
|
OK, I found many and varied ways to break the tests but I think I have things straightened out now. My current problem is that MyPy says "Read-only property cannot override read-write property". So if |
|
Actually there is still more to do here (aside from placating MyPy) as currently |
| def set_figure(self, fig): | ||
| """ | ||
| .. deprecated:: 3.10 | ||
| Currently this method will raise an exception if *fig* is anything other | ||
| than the root `.Figure` this (Sub)Figure is on. In future it will always | ||
| raise an exception. | ||
| """ | ||
| no_switch = ("The parent and root figures of a (Sub)Figure are set at " | ||
| "instantiation and cannot be changed.") | ||
| if fig is self._root_figure: | ||
| _api.deprecation("3.10", | ||
| message=(f"{no_switch} From Matplotlib 3.12 this " | ||
| "operation will raise an exception.")) | ||
| return | ||
|
|
||
| raise ValueError(no_switch) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure this is the best solution here, but it's based on the following considerations:
- Since the parent figure is set at instantiation and
Artist.set_figuredoes not allow switching to a different figure,(Sub)Figure.set_figurewill always either be a no-op or raise an exception. - For consistency with
Artist.set_figure, the no-op should be on the parent figure. - For backwards compatibility and consistency with the
(Sub)Figure.figureproperty, the no-op should be on the root figure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we can get rid of the setter without breaking OO principles (specifically Liskov's Substitution Principle)... And not sure it is worth working around that expectation break.
The parent implementation already raises if a figure has already been set.
It is a reasonable point that we already break the semantics of having a "set_figure" on a "Figure"... but on the other hand, all that has really changed is "a figure is set for you at instantiation", and it is actually consistent to say "set_figure on an artist that already has a figure is an error, here is a method that we just didn't override, it will error, but hey that is expected".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we can get rid of the setter without breaking OO principles (specifically Liskov's Substitution Principle)... And not sure it is worth working around that expectation break.
Liskov is already broken. You can’t use a Figure everywhere where you can use an Artist. If one wanted strict Liskov Substitution Principle, one would need a more refined inheritance architecture, which is not where we are. But I see this pragmatic: As long as nobody expects a functionality (you would not try figure.add_artist(figure)) or the behavior is reasonable (you can’t set another figure if one is already set), the API is good enough.
| class SubFigure(FigureBase): | ||
| figure: Figure | ||
| @property | ||
| def figure(self) -> Figure: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ksunden can you advise what I should do here? It is a fact of the implementation that (Sub)Figure.figure can only ever be a Figure. It is also a fact that the setter can at best be a no-op, so it doesn't really make sense to me to have a type hint encouraging the user to set it. I can add the setter, but MyPy still complains about Figure being stricter than the parent's Union[Figure, SubFigure]. Adding this to the allowlist doesn't seem to make any difference. I'm stumped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firstly, the allow list is only for stubtest, and this is breaking a mypy check, so that is why that is not helping. A # type: ignore comment could be added, but that is a bit more of a last resort. (stubtest requires mypy to pass and runs it first, part of why it takes so long)
Secondly, is there a reason to be more strict here? Subfigures can be nested inside of subfigures, can they not? Type narrowing for a subclass isn't actually the problem here, but also not clear to me why it is narrowed here. (Not fully sure why you get two errors for each... but both are resolved below without adjusting their own type hints...)
Unfortunately, getting rid of the setter is breaking a pretty fundamental Object Orientation principle that all things that are provided by the parent class can be done on instances of child classes... and the method still exists, so I think type hinting it is fine...
That said, I would not be too opposed to making Artist.figure a read-only (type hinted at least) attribute, since I think writing to it (without using Artist.set_figure) is generally a mistake).
If I comment out the two lines from artist.pyi, I get no mypy issues found.
diff --git a/lib/matplotlib/artist.pyi b/lib/matplotlib/artist.pyi
index 08a99c0ed1..730c530dd2 100644
--- a/lib/matplotlib/artist.pyi
+++ b/lib/matplotlib/artist.pyi
@@ -33,8 +33,8 @@ class Artist:
stale_callback: Callable[[Artist, bool], None] | None
@property
def figure(self) -> Figure | SubFigure: ...
- @figure.setter
- def figure(self, fig: Figure | SubFigure): ...
+ #@figure.setter
+ #def figure(self, fig: Figure | SubFigure): ...
clipbox: BboxBase | None
def __init__(self) -> None: ...
def remove(self) -> None: ...In implementation, Artist.figure is a standard instance attribute, made in __init__, so it is writable by anyone... but I'm good with making the type hint stricter than that (even without making the implementation enforce it)
Also, looking back... the type hint is technically wrong, as artist.figure can in fact be None... but that may just be a "useful fib", since 99% of artists that you have will in fact have a figure, and so checking for the none case in all usages is perhaps not something we wish to enforce with type hints...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re type narrowing, I guess actually closely reading the code, you do narrow, so not opposed to encapsulating that meaning if that is what the code does... but do have some slight hesitations there, particularly if standard artists don't get root for art.figure... I'd have leaned towards not special casing Figure there...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason to be more strict here? Subfigures can be nested inside of subfigures, can they not?
Unfortunately we have a mismatch in what this property is: Artist.figure is the parent (Sub)Figure but SubFigure.figure is the root Figure. See #28170 (comment) for why it's difficult to change that.
[slow x-post]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#28170 (comment) suggested discouraging use of the figure attribute. Would it therefore be reasonable to just not type hint it? Looks like that would require a few updates in pyplot to use get_figure instead, but that's simple enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation for moving from figure to get_figure() is clarity - do we want the parent or the root? Internally, we should not be using figure anymore. For now, it should only stay available for backward compatibility. Whether or not we want to deprecate it and force downstream users to migrate can be decided later.
I'm not clear what the effect of not type-hinting is. Would it not show up in completions? Would there be warnings on incomplete typing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm (clearly!) no expert on type hinting but I took out the hint for this property in my branch and the mypy-precommit flagged failures in pyplot.
Internally, we should not be using
figureanymore.
Should internal usage be removed in this PR or could it wait for a follow-up? I lean towards doing it separately as I think the current change stands by itself and should be useful for the subfigure_mosaic work (#28170 (comment)). I have started having a go at removing the internal use but there is, um, rather a lot of it and not every instance is obvious to me whether it should be root or not. So I think separating those concerns off into a separate PR may make the review easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the internal refactoring can be a follow-up.
and not every instance is obvious to me whether it should be root or not
That's part of the motivation to get rid of it 😏. I also won't guarantee that the current usages are always correct. Subfigures (and in particular nested subfigures) are fairly recent and thus not that common.
78a229c to
c1fc4cf
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
🎉 Very happy to have hit merge on this! It was about an order of magnitude more complicated than I would have guessed.... |
|
🚀 @rcomer for digging through the details! |
PR summary
Closes #28170.
get_figureand make the behaviour consistent betweenSubFigureand other artists.figureattribute into a property so that users cannot set the figure without going through the checks of theset_figuremethod.Not included here is my attempt to find/replace all internal use of the
figureattribute with_figure, which would obviously be necessary if we want to deprecate thefigureproperty. I gave up when I realised we have other objects that have the attribute but are not artists (GridSpec,FigureCanvasBase, maybe others) which will make it more complicated to find what does and doesn't need to change. I have my progress so far saved in a commit in case we do decide we want to do that. Is there any performance consideration for making the internal use go through the get and set methods?PR checklist