Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
jklymak merged 7 commits intomatplotlib:masterfromanntzer:unify-aliases
Mar 15, 2018

Conversation

anntzer
Copy link
Contributor

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

  • Has Pytest style unit tests... in the sense that tests still pass.
  • Code is PEP 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

Copy link
Contributor

@eric-wiesereric-wieser left a comment
edited
Loading

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))

exists = True
for alias in aliases:
method = make_alias(prefix + prop)
method.__name__ = str(prefix + alias) # Py2 compat.
Copy link
Contributor

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)

Copy link
ContributorAuthor

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).

Copy link
Contributor

@eric-wiesereric-wieserDec 15, 2017
edited
Loading

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

Copy link
Contributor

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.

Copy link
ContributorAuthor

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().

def method(self, *args, **kwargs):
return getattr(self, name)(*args, **kwargs)
method.__doc__ = "alias for {}".format(name)
return method
Copy link
Contributor

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?

Copy link
ContributorAuthor

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.

Copy link
Contributor

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

method = make_alias(prefix + prop)
method.__name__ = str(prefix + alias) # Py2 compat.
local_d[prefix + alias] = method
if not exists:
Copy link
ContributorAuthor

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.

for alias in aliases:
method = make_alias(prefix + prop)
method.__name__ = str(prefix + alias) # Py2 compat.
method.__doc__ = "alias for `{}`".format(prefix + prop)
Copy link
Contributor

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...

Copy link
ContributorAuthor

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
Copy link
Contributor

eric-wieser commentedDec 15, 2017
edited
Loading

A novel idea for future: have a globalMATPLOTLIB_WARN_ALIASES environment variable, and when set emit warnings when aliases are used. Follow up: ensure that internally matplotlib never uses the aliases, but always the full names.

@eric-wieser
Copy link
Contributor

eric-wieser commentedDec 15, 2017
edited
Loading

Alright, here's the solution that preserves__signature__ in python 3:

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

@anntzer
Copy link
ContributorAuthor

I think that's a reasonable option, but will let other reviewers chime in first.

@eric-wieser
Copy link
Contributor

To be clear, I think what you have now is fine, and am just exploring alternatives out of curiosity :)

@efiring
Copy link
Member

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:

In [5]: l.get_mec?Signature: l.get_mec(*args, **kwargs)Docstring: alias for `get_markeredgecolor`File:      ~/work/programs/py/mpl/matplotlib/lib/matplotlib/cbook/__init__.pyType:      methodIn [6]: l.get_mec??Signature: l.get_mec(*args, **kwargs)Source:        def method(self, *args, **kwargs):            return getattr(self, name)(*args, **kwargs)File:      ~/work/programs/py/mpl/matplotlib/lib/matplotlib/cbook/__init__.pyType:      method

That's not a blocker, but it's not ideal, either.

@anntzer
Copy link
ContributorAuthor

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 :-)

@tacaswell
Copy link
Member

Could this be done as a class decorator? I think that would be clearer than hacking onlocals() in the class body.

@tacaswelltacaswell added this to thev2.2 milestoneDec 28, 2017
@eric-wieser
Copy link
Contributor

eric-wieser commentedDec 28, 2017
edited
Loading

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 usecls.__dict__[name] instead ofgetattr(cls, name) to avoid that though.

@eric-wieser
Copy link
Contributor

I don't think it even needs to be a decorator - just a free function with signatureadd_aliases(cls: Type, aliases: Dict) -> None

@Carreau
Copy link
Contributor

@Carreau any hooks in IPython that we could rely on for this? thanks :-)

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?/?? but that will take time.

@anntzer
Copy link
ContributorAuthor

@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).

@Carreau
Copy link
Contributor

@Carreau Perhaps the line at ipython/ipython:IPython/core/oinspect.py@3c53804#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
Copy link
Contributor

eric-wieser commentedDec 29, 2017
edited
Loading

or read the alias table from a table defined in the body of the function, but that doesn't play well with inheritance

Works fine if you name the table__aliases, and within your helper usegetattr(cls, '_{}__aliases'.format(cls.__name__))

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?

@anntzer
Copy link
ContributorAuthor

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.

@anntzer
Copy link
ContributorAuthor

@efiringipython/ipython#10966 should at least help for future IPython versions.

@tacaswell
Copy link
Member

@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
Copy link
ContributorAuthor

anntzer commentedJan 1, 2018
edited
Loading

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.
Subclassing should not be an issue because the aliases call getattr at call time and not at function-definition time.

if not exists:
raise ValueError("property {} does not exist".format(prop))

cls._alias_map = alias_d
Copy link
Contributor

@eric-wiesereric-wieserJan 1, 2018
edited
Loading

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

Copy link
ContributorAuthor

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.

Copy link
Member

@tacaswelltacaswellJan 1, 2018
edited
Loading

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.

Copy link
Contributor

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.

Copy link
Contributor

@eric-wiesereric-wieserJan 2, 2018
edited
Loading

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

Copy link
Member

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']

Copy link
Contributor

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

method.__doc__ = "alias for `{}`".format(prefix + prop)
setattr(cls, prefix + alias, method)
if not exists:
raise ValueError("property {} does not exist".format(prop))
Copy link
Member

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.

Copy link
Contributor

@eric-wiesereric-wieserJan 1, 2018
edited
Loading

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"

tacaswell and anntzer reacted with thumbs up emoji
"edgecolor": ["edgecolors"],
"facecolor": ["facecolors"],
"linestyle": ["linestyles", "dashes"],
"linewidth": ["linewidths", "lw"],
Copy link
Contributor

@eric-wiesereric-wieserJan 2, 2018
edited
Loading

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
Copy link
Contributor

eric-wieser commentedJan 2, 2018
edited
Loading

Ping onthis outdated comment on inheritance which I think still applies.

Edit: Now resolved

"Neither getter nor setter exists for {!r}".format(prop))

if hasattr(cls, "_alias_map"):
raise NotImplementedError("Parent class already defines aliases")
Copy link
Contributor

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.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

done

if cls is None:
return functools.partial(_define_aliases, alias_d)

def make_alias(name): # Enfore a closure over *name*.

Choose a reason for hiding this comment

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

typoenforce?

@anntzer
Copy link
ContributorAuthor

btw@efiring the next IPython release will show the docstring in this case too.

Copy link
Member

@efiringefiring left a 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.

@anntzer
Copy link
ContributorAuthor

bump.

@jklymak
Copy link
Member

OK, seems reasonable - aremany of the aliases tested? I guess the common ones are.

@anntzer
Copy link
ContributorAuthor

Well, looks like codecov is happy for once :-)

@jklymakjklymak modified the milestones:needs sorting,v3.0Mar 15, 2018
@jklymakjklymak merged commitb083c2a intomatplotlib:masterMar 15, 2018
@anntzeranntzer deleted the unify-aliases branchMarch 15, 2018 19:36
@tacaswell
Copy link
Member

🎉 I am happy this made it in!

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@tacaswelltacaswelltacaswell left review comments

@eric-wiesereric-wiesereric-wieser left review comments

@PeterPabloPeterPabloPeterPablo left review comments

@efiringefiringefiring approved these changes

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
v3.0.0
Development

Successfully merging this pull request may close these issues.

7 participants
@anntzer@eric-wieser@efiring@tacaswell@Carreau@jklymak@PeterPablo

[8]ページ先頭

©2009-2025 Movatter.jp