Skip to content

Mark artist property aliases explicitly #22749

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

timhoffm
Copy link
Member

@timhoffm timhoffm commented Mar 31, 2022

PR Summary

This is a first step for making artist properties explicit.

Overall goal

We call the variables associated with getters and setters of Artists artist properties. e.g. linestyle is an artist property of Line2D. Artist properties can have aliases and are often used as keyword arguments to artist factory functions.

Issue: We don't don't declare artist properties explicitly, but instead try to infer them based on heuristic rules, mainly in ArtistInspector. This is cumbersome and fragile because people might not be aware of these rules when they add new properties or change existing ones.

Solution: The setters and getters assoicated with artist properties should be explicitly marked as such by adding an attribute _artist_property to these methods, usually via a decorator.

This PR

Later PRs

will replace more and more of the ArtistInspector logic, such as get_setters(), get_valid_values() / kwdocs; properties(). But for that we have to massively introduce the decorator and therefore it's better to do it step by step.

Comment on lines +1493 to +1498
result = ds.startswith('Alias for ')
alt_result = (o._artist_property.is_alias
if hasattr(o, '_artist_property')
else False)
assert result == alt_result
return result
Copy link
Member Author

Choose a reason for hiding this comment

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

This code segment is temporary. I've written it to easily verify that the way of obtaining is_alias via the property is equivalent to getting that information from the docstring. Note that this is already run at import time on all artists via ArtistInspector.__init__ -> ArtistInspector.get_aliases() because we inspect all artists for kwdocs. So if the assert holds in the tests the equivalence is proven.

I propose the following review pattern:

After two people have seen that the tests pass and have approved the PR, I'll change this whole function to only using the new way:

def def is_alias(self, o):
    return (o._artist_property.is_alias
            if hasattr(o, '_artist_property')
            else False)

PLEASE REVIEW, BUT DO NOT MERGE after the second approval.

Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to add that "Alias for..." part of the doc-string based on this? So not only provide an is_alias boolean, but rather a string for which artist property it is an alias for?

@anntzer
Copy link
Contributor

anntzer commented Apr 6, 2022

Can you clarify what you're trying to achieve here? (Perhaps I just missed a background discussion...) Yes, it's a bit inelegant that ArtistInspector currently checks the docstring for "Alias for", but that could reasonably easily be fixed as all but one aliases are defined via define_aliases (which sets up the appropriate docstring), and we could perhaps just adjust ArtistInspector to lookup cls._alias_map. Or define_aliases could attach the _artist_property attribute instead of creating _alias_map (then we should just dump _alias_map); the point is that we don't really need to have that info twice.

set_fontname isn't really an alias for set_family as get_fontname is not the same as get_family, unless you decide that getters don't matter here.

@timhoffm
Copy link
Member Author

timhoffm commented Apr 6, 2022

This has to be viewed in a larger context: We should make artist properties and all their aspects explicit instead of relying on heuristics like "docstring starts with " or "get_* methods with one argument". What info exactly will be needed is to be seen, but we want to at least: 1) store the simple fact that it's an artist property. 2) An optional short description for kwdoc 3a) whether a property is an alias and 3b) what is aliases. We can choose where that information is declared and stored.

Declaration: For declaration, I find it most convenient to add decorators to the methods.

@_api.artist_property(...)
def get/set_something(...)
     ...

A separate declaration runs the risk of getting out of sync and is less clear in the code.

Storage: I've here chosen to store the the information directly on the methods. It seems practical and good enough to me, but I haven't thought this through completely. We could alternatively have some sort of PropertyRegistry directly attached to the artist classes (or even standalone). It shouldn't be difficult to switch between those realizations later if needed.


I think the main challenge is to add all decorators while making sure that they have the same as effect the other mechanisms like the heuristics and alias map before. We'll have a transition phase (maybe even only within a PR) in which both mechanisms are present and we can compare them. - The alternative would be to dump that information e.g. in a file and test against that before and after the changes. The second part of the challnage is that we need a lot of decorators, so I started only with the info 3a) (whether the property is an alias) to make sure the basic infrastructure works. We can then add more and more info in subsequent PRs.

For itself, this PR may seem incomplete, but you should see it as a reviewable step in the right direction. I could do all the planned changes in a single PR, but I'm afraid that'd be completely not reviewable because I add several hundred decorators with at least information on 1), 2), 3a), 3b) and at the same time remove all the logic we currently use for artist properties.

Maybe we can get rid of _alias_map, maybe we need it but populate it through the decorator. That's all something to be decided later.

Whether set_fontname should be an alias is also to be looked into later. For now, I'm just replicating the existing logic.

@anntzer
Copy link
Contributor

anntzer commented Apr 6, 2022

We should make artist properties and all their aspects explicit instead of relying on heuristics like "docstring starts with " or "get_* methods with one argument".

It is actually not so clear to me why this is necessary. I am absolutely not opposed to doing so, but what would be the benefits?
I think(?) right now artist properties really only have one important role in the API: they can be passed as kwargs to Artist.__init__/Artist.set/Artist.update, which individually calls set_foo() for each property. But that actually exactly matches the definition "it's the name of a set_foo" method (possibly checking for the setter's signature, as suggested elsewhere). In fact, even the fact that there's normally a get_foo associated is rather secondary. But again, I may be missing something.

I've here chosen to store the the information directly on the methods. It seems practical and good enough to me, but I haven't thought this through completely. We could alternatively have some sort of PropertyRegistry directly attached to the artist classes (or even standalone). It shouldn't be difficult to switch between those realizations later if needed.

I guess one way to decide this is how this info should propagate to subclasses, e.g. if a subclass overrides a method it will (by default) lose the attribute info -- do we want that? OTOH if it's stored in a dict(-like) in the class then the subclass should probably copy it before updating it with its own info.

For itself, this PR may seem incomplete, but you should see it as a reviewable step in the right direction.

I certainly understand that changing the hundreds of properties we have would not be a good idea before deciding on a final design; I'm just trying to understand what the desired endpoint is.

@timhoffm
Copy link
Member Author

timhoffm commented Apr 7, 2022

what would be the benefits?

Clarity and simplicity.

One could discuss whether one needs the concept at all, but we're using this heavily e.g. in **kwargs : `~matplotlib.text.Text` properties, but using properties without a technical definition is deeply confusing because Python has a technical definition, which we don't want to imply. So IMHO, yes we should have artist properties.

What I don't like about the current solution:

  • We don't define what artist properties are exactly neither for the user nor for the developer.
  • One needs an ArtistInspector to get property-related information. This feels cumbersome and indirect. Apart from @define_aliases the artist does not know about its properties.
  • There's redundancy/implicit assumtions, which are brittle. ArtistInspector.is_alias() returns whether the docstring starts with "Alias for ". @define_aliases creates such a docstring, but someone might decide to change that docstring, which would subtly break ArtistInspector.is_alias(). We also have set_fontname() as only explicit function whose docstring starts with "Alias for ". I'm not clear whether that's been done, knowing it has an effect on ArtistInspector.is_alias().
  • Naming is imprecise: get_setters() does not return setters, but a list of "attribute strings", which in fact are determined from set_* methods with at least one parameter (not counting self) that is not considered an alias.
  • Operational definitions have the danger of not supporting edge-cases. - e.g. aboves "set_* methods with at least one parameter (not counting self" would match a set_xlim(min, max) signature but cannot be used as a kwarg as of now. OTOH one could imagine a set_something(foo) method that we may not want to regard as a property conceptually.
  • Similar for get_valid_values(): It tries to extract the type from the first parameter docstring (which is reasonable some times but not always). So somebody invented the .. ACCEPTS: comment. But that's rather unconventional. It looks alien in the docstring and we have to document its existing and intended usage in the documentation guide. Defining an explicit value via a decorator is much cleaner (we can still have a default of scraping the docstring if no value is given to avoid duplicating the definition when not necessary).

I'd like to get rid of ArtistInspector completely finally. I think most of the above can be addressed using a declarative approach with a decorator.

@anntzer
Copy link
Contributor

anntzer commented Apr 8, 2022

So IMHO, yes we should have artist properties.

Note that we could relatively easily rename "artist properties" to something else -- the name "property" hardly appears in the API, except as initials in getp and setp (I think).

We don't define what artist properties are exactly neither for the user nor for the developer.

Sure, but that's something that can be done independently of the rest ("it's a name such that there is a set_thisname method that can be called with a single argument").

One needs an ArtistInspector to get property-related information. This feels cumbersome and indirect. Apart from @define_aliases the artist does not know about its properties.

We could move most of the implementation of ArtistInspector to a private class and make Artist.__init_subclass__ call that implementation so that Artist subclasses have their own mapping of the properties they define. (And then the public ArtistInspector would just read that mapping.)

There's redundancy/implicit assumtions, which are brittle. ArtistInspector.is_alias() returns whether the docstring starts with "Alias for ". @define_aliases creates such a docstring, but someone might decide to change that docstring, which would subtly break ArtistInspector.is_alias(). We also have set_fontname() as only explicit function whose docstring starts with "Alias for ". I'm not clear whether that's been done, knowing it has an effect on ArtistInspector.is_alias().

This was almost certainly done intentionally, given that before I introduced @define_aliases, aliases were all written manually with a manually written docstring stating "Alias for ...". Moving this docstring prefix to become an internal global constant, instead of duplicating it in @define_aliases and in is_alias, is certainly possible and would prevent any loss of sync.

Naming is imprecise: get_setters() does not return setters, but a list of "attribute strings", which in fact are determined from set_* methods with at least one parameter (not counting self) that is not considered an alias.

Sure, that could be renamed. If AI gets deprecated as a public class (see below), the name doesn't really matter anyways though.

Operational definitions have the danger of not supporting edge-cases. - e.g. aboves "set_* methods with at least one parameter (not counting self" would match a set_xlim(min, max) signature but cannot be used as a kwarg as of now. OTOH one could imagine a set_something(foo) method that we may not want to regard as a property conceptually.

In practice it would seem simpler to have a decorator to exclude such cases (perhaps less pure, but avoids having to add hundreds and hundreds of decorators...)

Similar for get_valid_values(): It tries to extract the type from the first parameter docstring (which is reasonable some times but not always). So somebody invented the .. ACCEPTS: comment. But that's rather unconventional. It looks alien in the docstring and we have to document its existing and intended usage in the documentation guide. Defining an explicit value via a decorator is much cleaner (we can still have a default of scraping the docstring if no value is given to avoid duplicating the definition when not necessary).

As for "Alias for", "ACCEPTS:" was here waaaay before parameter docstring scraping (I wrote that scraping and removed ACCEPTS strings which were present in nearly all setters before that). I do agree that this specific case may be better served by a decorator, though.

I'd like to get rid of ArtistInspector completely finally. I think most of the above can be addressed using a declarative approach with a decorator.

No strong opinion here, but do you want to maintain a user-facing API similar to ArtistInspector? If so it may be not worth breaking the backcompat.


So I guess my suggestion would be: keep the implicit "setters with one arg are properties" (perhaps with Artist.__init_subclass__ handling the enumeration); add a decorator that replaces ACCEPTS; add a decorator that allows a set_foo to opt-out.

@timhoffm
Copy link
Member Author

So IMHO, yes we should have artist properties.

Note that we could relatively easily rename "artist properties" to something else -- the name "property" hardly appears in the API, except as initials in getp and setp (I think).

Yes, it's mostly in docs. If you have a good name, that would certainly be a consideration to reduce confusion with python properties.

Operational definitions have the danger of not supporting edge-cases. - e.g. aboves "set_* methods with at least one parameter (not counting self" would match a set_xlim(min, max) signature but cannot be used as a kwarg as of now. OTOH one could imagine a set_something(foo) method that we may not want to regard as a property conceptually.

In practice it would seem simpler to have a decorator to exclude such cases (perhaps less pure, but avoids having to add hundreds and hundreds of decorators...)

I'm afraid we'll have to add decorators in many cases anyway to get better docs for kwargs, c.f. #22699
But ok, let's see how far we have to go there. I'll reactivate #22699 and put this here on hold.

As for "Alias for", "ACCEPTS:" was here waaaay before parameter docstring scraping (I wrote that scraping and removed ACCEPTS strings which were present in nearly all setters before that). I do agree that this specific case may be better served by a decorator, though.

I'd like to get rid of ArtistInspector completely finally. I think most of the above can be addressed using a declarative approach with a decorator.

No strong opinion here, but do you want to maintain a user-facing API similar to ArtistInspector? If so it may be not worth breaking the backcompat.

Not yet decided. IMHO users should not need this and less public API is less maintenance burden. If it stays, I'd rather have it only look up data that has previously been set/declared. I don't like the complex logic it does.

So I guess my suggestion would be: keep the implicit "setters with one arg are properties" (perhaps with Artist.__init_subclass__ handling the enumeration); add a decorator that replaces ACCEPTS; add a decorator that allows a set_foo to opt-out.

Let's take it step by step. I'll put this on hold, and reactivate #22699 for "a decorator that replaces ACCEPTS" everything else can come later and we can then decide how to approach the individual parts.

@timhoffm timhoffm marked this pull request as draft May 21, 2022 22:22
@anntzer
Copy link
Contributor

anntzer commented May 21, 2022

If you have a good name

Probably a better question for the native speakers :) I can't think of anything right now.

I guess after #22699 goes in first, we can always decide whether to also decorate everything else or manually mark exclusions. So that sounds good to me.

@jklymak
Copy link
Member

jklymak commented May 22, 2022

"Artist setting"? Since it's something you set?

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

Successfully merging this pull request may close these issues.

4 participants