Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.9k
Declare property aliases in a single place#9475
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
cc7b870
to5d04619
Compare
eric-wieser left a comment• edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Looks like nice cleanup, but I'd go with
deftry_make_alias(local_d,old_name,new_name):try:old_func=local_d[old_name]exceptKeyError:returnFalse@functools.wraps(old_func)# sets __signature__ and __wrapped__defmethod(*args,**kwargs):returnold_func(*args,**kwargs)method.__name__=new_namemethod.__doc__="alias for `{}`".format(old_name)# backticks for a sphinx linklocal_d[new_name]=methodreturnTrueforprop,aliasesinalias_d.items():found_prop=any(try_make_alias(local_d,prefix+prop,prefix+alias)forprefixin ["get_","set_"] )ifnotfound_prop:raiseValueError("property {} does not exist".format(prop))
lib/matplotlib/cbook/__init__.py Outdated
exists = True | ||
for alias in aliases: | ||
method = make_alias(prefix + prop) | ||
method.__name__ = str(prefix + alias) # Py2 compat. |
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 this line should be handled bymake_alias(orig_name, alias_name)
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 rather make make_alias not know what the name of the resulting function is. Really its only goal is to make a closure over the name (so I'm even going to move the__doc__
out, in fact).
eric-wieserDec 15, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 you should close over the value, not the name
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.
Can you explain what the py2 compat issue is? Seems that this doesn't matter for anything other than printing the__name__
, which py3 does too.
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.
py2 does not want unicode strings as function names, which we do end up passing because of the unicode_literals future alias. There's a similar issue with the first argument to subprocess on Windows + Py2, again we would pass in unicode strings because of unicode_literals, so we have to wrap them in a call to str().
lib/matplotlib/cbook/__init__.py Outdated
def method(self, *args, **kwargs): | ||
return getattr(self, name)(*args, **kwargs) | ||
method.__doc__ = "alias for {}".format(name) | ||
return method |
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.
Why not do thegetattr
call outside ofmethod
?
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.
So that patching the base method gets propagated to the alias method.
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.
Ah, I see. That makes sense to me now
lib/matplotlib/cbook/__init__.py Outdated
method = make_alias(prefix + prop) | ||
method.__name__ = str(prefix + alias) # Py2 compat. | ||
local_d[prefix + alias] = method | ||
if not exists: |
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 this is more explicit than theany(...)
you propose.
lib/matplotlib/cbook/__init__.py Outdated
for alias in aliases: | ||
method = make_alias(prefix + prop) | ||
method.__name__ = str(prefix + alias) # Py2 compat. | ||
method.__doc__ = "alias for `{}`".format(prefix + prop) |
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 suppose setting__signature__
throughfunctools.wraps
also does the wrong thing for overriden properties...
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 mostly too lazy to add a dep on funcsigs for this PR (I actually do add such a dependency for the pyplot PR, so that'd also mean merge conflicts and sadness).
eric-wieser commentedDec 15, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
A novel idea for future: have a global |
eric-wieser commentedDec 15, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Alright, here's the solution that preserves classAliasedMethod(object):def__init__(self,base_name):self.base_name=base_namedef__get__(self,instance,cls):base_method=getattr(cls,self.base_name)@functools.wraps(base_method)defmethod(obj,*args,**kwargs):returnbase_method(obj,*args,**kwargs)method.__doc__="alias of `{}`".format(self.base_name)returnmethod.__get__(instance,cls) More expensive though since it constructs a function upon attribute lookup |
I think that's a reasonable option, but will let other reviewers chime in first. |
To be clear, I think what you have now is fine, and am just exploring alternatives out of curiosity :) |
In general I like this. The clutter-reduction is great, and I agree with favoring the singular forms. My only discomfort with the PR is that in ipython, the single-question-mark works perfectly but the double-question-mark returns the wrapper-method code (and no docstring) instead of the relevant code:
That's not a blocker, but it's not ideal, either. |
fwiw the relevant code in IPython is in IPython.core.oinspect (_info); specificallyhttps://github.com/ipython/ipython/blob/3c53804581bd51347eca577950752e1eac1d5e9b/IPython/core/oinspect.py#L823: "Add docstring only if no source is to be shown (avoid repetitions)." @Carreau any hooks in IPython that we could rely on for this? thanks :-) |
Could this be done as a class decorator? I think that would be clearer than hacking on |
eric-wieser commentedDec 28, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
A class decorator will be slower, since it would invoke the descriptor protocol on each attribute lookup. (Rather than the direct dict lookup that is happening here). I suppose it could use |
I don't think it even needs to be a decorator - just a free function with signature |
Not that I can think of from the top of my head. This is some old code that would highly need refactor, and I am thinking about an ability to make that more configurable with a way for a package to provide a custom object->doc mapping for |
@tacaswell It could be a decorator or a free function but I think it would actually make things less clear. A decorator would either need to declare the aliases at the beginning of the class definition, before the properties themselves are defined (or read the alias table from a table defined in the body of the function, but that doesn't play well with inheritance). A free function would modify the class definition post-hoc, which is not a model of clarity either. @Carreau Perhaps the line athttps://github.com/ipython/ipython/blob/3c53804581bd51347eca577950752e1eac1d5e9b/IPython/core/oinspect.py#L823 ("Add docstring only if no source is to be shown (avoid repetitions).") could try to parse the source tree and see whether the docstring is textually present in the source, and decide accordingly whether to display the docstring? This would be a fairly generic solution for dynamically generated functions. It sounds a bit scary but I don't think it's that hard to write (happy to push the item somewhere on my todo list if that sounds good to you). |
I have to think about it and like feedback from other dev about that. I don't see any drawbacks, so feel free to send a PR or open an issue. |
eric-wieser commentedDec 29, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Works fine if you name the table Edit: Wait, in what way does this mess up inheritance? The decorator should only be applying to the top-level class anyway, shouldn't it? |
Good point that because the decorator is only applied once (which is actually what you want) there are no problems with inheritance. Still, per my above comment, I don't really like splitting the aliasing-relevant code into two places (one table in the body that lists the aliases and one decorator at the top). If there's a strong pushback against the current design I can revisit it, so let me know what you prefer. |
@efiringipython/ipython#10966 should at least help for future IPython versions. |
@anntzer I don't understand your comments. I was imagining something like @alias_manager({'linewidth': ['lw','linewidths'], ...})classFooBar: ... Everything is still in one place, it uses a language provided tools for mucking with class dictionaries between when they are defined and when the user gets the classes, and makes it very clear from looking at the class definition that we are doing so. That said, do we need to do something fancier to deal with the case where a sub-class overides the thing that is aliased so the aliases point back to the sub-classed version of method? |
anntzer commentedJan 1, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Basically I didn't like having a huge decorator before the class itself is defined, but it's not a big deal; changed implementation accordingly. |
lib/matplotlib/cbook/__init__.py Outdated
if not exists: | ||
raise ValueError("property {} does not exist".format(prop)) | ||
cls._alias_map = alias_d |
eric-wieserJan 1, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 might be tempted to have_define_aliases
read this directly, rather than include it in the decorator argument and have it set it anyway - that means the aliases come after the definitions too, and that it becomes more obvious where_alias_map
comes from when callingnormalize_kwargs
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 really would prefer the aliases to stay close to the decorator, at least.
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 prefer it this way, to keep the decorator and the alias map close together.
Would it make sense to have the decorator also add a_normalize_kw
method as
defnormalize_kvwargs(self,kw):returnnormalize_kwargs(kw,self._alias_map)
?
Might also be better to check if the class already has an_alias_map
and if so recursively update it so that the decorator can be used on sub-classes.
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.
if so recursively update it
What you really want here is acollections.ChainMap
, but it's not there in python 2.7.
eric-wieserJan 2, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Or maybe
@staticmethoddef_get_alias_map(self):d= {}forclsinself.mro():fork,aliasesincls.__dict__.get('_alias_map',{}).items()d.setdefault(k, []).extend(aliases)
edit: updated
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 don't think ChainMap helps here, ex
@_define_aliases({'linewidth': ['lw']})classA: ...@_define_aliases({'linewidth': ['linewidths','lws']})classB(A): ...
You would wantB._alias_map['linewidth'] == ['lw', 'linewidths', 'lws']
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.
Good point. Updated my other comment to reflect thatupdate
doesn't work either
lib/matplotlib/cbook/__init__.py Outdated
method.__doc__ = "alias for `{}`".format(prefix + prop) | ||
setattr(cls, prefix + alias, method) | ||
if not exists: | ||
raise ValueError("property {} does not exist".format(prop)) |
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.
Can we use a different word than 'property' here? They are not python@property
instances which may lead to confusion.
eric-wieserJan 1, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
How about"getter or setter methods for {!r} do not exist"
?
edit: or"neither getter nor setter methods for {!r} exist"
"edgecolor": ["edgecolors"], | ||
"facecolor": ["facecolors"], | ||
"linestyle": ["linestyles", "dashes"], | ||
"linewidth": ["linewidths", "lw"], |
eric-wieserJan 2, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Why not sets, instead of lists? Duplicates wouldn't make any sense here. (edit: becausenormalize_kwargs
requires it to be so)
eric-wieser commentedJan 2, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Ping onthis outdated comment on inheritance which I think still applies. Edit: Now resolved |
lib/matplotlib/cbook/__init__.py Outdated
"Neither getter nor setter exists for {!r}".format(prop)) | ||
if hasattr(cls, "_alias_map"): | ||
raise NotImplementedError("Parent class already defines aliases") |
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.
A comment here along the lines of# supporting inheritance requires us to have a plan for conflicting aliases
would be good, so that someone doesn't just come along and add a naive implementation.
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.
done
lib/matplotlib/cbook/__init__.py Outdated
if cls is None: | ||
return functools.partial(_define_aliases, alias_d) | ||
def make_alias(name): # Enfore a closure over *name*. |
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.
typoenforce
?
btw@efiring the next IPython release will show the docstring in this case too. |
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.
It looks to me like this has converged via discussion to the point where it should go in so we can see whether any real-world problems turn up.
The consolidation of the alias declarations is a big gain.
bump. |
OK, seems reasonable - aremany of the aliases tested? I guess the common ones are. |
Well, looks like codecov is happy for once :-) |
🎉 I am happy this made it in! |
PR Summary
Define property aliases in a more systematic manner. Note that there were some "funny" aliases that I needed to work around, e.g. Text.set_name is an alias for Text.set_family but Text.get_name is different from Text.get_family.
Also use the aliases for kwarg normalization.
For collections, prefer normalizing get_facecolors, etc. to thesingular form, as this is more consistent with the Artist interface.
This will add some setters/getters that were not defined before, e.g. there was a Patch.set_aa (set_antialiased) but no Patch.get_aa; this PR adds the missing method.
PR Checklist