Skip to content

New "accepts units" decorator #10411

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 4 commits into from

Conversation

dstansby
Copy link
Member

This PR implements a decorator that marks a function as accepting units, and also does the unit conversion. The decorator takes a list of parameters that can accept unitised data, so for example both x and xlim could be converted as part of the same function.

The decorator itself is currently not very neat code, but it works as a proof of concept.

This would also make the creation of "methods that accept units" registry really easy, as anything that is decorated could be automatically added to it, and a page in the docs with methods that support units could be automatically generated.

@dstansby dstansby added this to the v3.0 milestone Feb 10, 2018
.travis.yml Outdated
@@ -65,19 +65,6 @@ env:

matrix:
include:
- python: 2.7
Copy link
Member Author

Choose a reason for hiding this comment

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

I've got rid of the py27 build to avoid having to do funny things with importing signature.

@anntzer
Copy link
Contributor

anntzer commented Feb 10, 2018

+1 for the general idea, but may be easier to move the decorator under preprocess_data so that you don't have to fiddle with the data kwarg yourself?

@dstansby
Copy link
Member Author

I'm not sure that would work, because eventually it would be good to apply this decorator to e.g. ax.set_xlim() which doesn't have the preprocess_data decorator.

@tacaswell
Copy link
Member

I am not sure if it is better to pull the conversion as early as possible (as proposed here) or to push it as late as possible (into the draw methods)

@efiring
Copy link
Member

efiring commented Feb 11, 2018

It seems to me that early conversion is necessary, so that you can proceed to work with numbers.

@anntzer
Copy link
Contributor

anntzer commented Feb 11, 2018

re: ax.set_xlim: that one can't take a data kwarg anyways, so I don't see how things are related?

@dstansby
Copy link
Member Author

@anntzer Ah, I see what you mean now. I tried it the other way round, but the data decorator seems to do funny things with the function signature and I couldn't get it to work.

@anntzer
Copy link
Contributor

anntzer commented Feb 14, 2018

May be worth (but not necessarily easy) to make the data decorator set the correct signature so that the units decorator can be made to work? (just throwing ideas around)

@dstansby dstansby changed the title [WIP] New "accepts units" decorator New "accepts units" decorator Mar 1, 2018
@dstansby dstansby force-pushed the unit-decorator branch 2 times, most recently from 3fdb8d7 to f44ee44 Compare April 13, 2018 15:55
@dstansby
Copy link
Member Author

I think this is ready - if everyone is still keen I will try and roll it out to a few other functions that do the unit conversion in house near the beginning of their definition?

@dopplershift
Copy link
Contributor

It seems like an improvement to me.

has_data = (('data' in arguments) and
(arguments['data'] is not None))
if has_data:
data = arguments['data']
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps move the decorator under _preprocess_data, so that you don't have to reproduce its logic here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried that a while ago, and from what I remember it became quite complicated because _preprocess_data alters functions signatures in a funny way.

Copy link
Member

@timhoffm timhoffm Apr 18, 2018

Choose a reason for hiding this comment

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

I'm not quite sure, that it's a good idea for _preprocess_data to alter the function signature anyway. I would be happy explicitly writing a data=None in the signatures and only let _preprocess_data work on that. Feels less magical and simplifies the _preprocess_data code.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that would work, since the idea is that if I do ax.plot(dataframe['Distance']) it will automatically get the index of the dataframe as the x-values. Anyway, if anyone wants to alter the _preprocess_data decorator feel free to in another PR, but this one is for units 😉

Copy link
Member

Choose a reason for hiding this comment

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

Don't think that's done in _preprocess_data. If I understand the code correctly, this is from cbook.index_of() via Axes._process_plot_var_args(), which is independent of _preprocess_data. 😇

Anyway, I'm +/-0 on using the copied code from _preprocess_data in the new decorator vs. including the unit stuff there. Just saying that altering the function signature can (and maybe) should be removed from _preprocess_data, if that was the only reason preventing inclusion.

One final remark. If the decision is, that this functionality should be kept in separate decorators, which would be a vailid point, maybe it's worth to factor out common functionality.

Copy link
Member

Choose a reason for hiding this comment

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

Separate topic: If I'm correct, the four previous lines can be simplified to
data = arguments.get('data')

and use data is not None instead of has_data in the following.

@dstansby dstansby force-pushed the unit-decorator branch 2 times, most recently from 8a7f1a9 to 07bf0d6 Compare April 18, 2018 12:47
@dstansby
Copy link
Member Author

I think that's all the easy methods taken care of, feel free to review!

convert_x, convert_y : list
A list of integers or strings, indicating the arguments to be converted
"""
def decorator(func):
Copy link
Member

Choose a reason for hiding this comment

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

The current implementation hides the docstring of the decorated functions. Not an expert on decorators, but it might be sufficient to decorate decorator(func) with @wraps.

Copy link
Member Author

Choose a reason for hiding this comment

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

Eeek good catch, will see what I can do.

Copy link
Member Author

Choose a reason for hiding this comment

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

Doing the @wraps seems to work

@dstansby
Copy link
Member Author

Reviews needed bump!

@tacaswell tacaswell requested a review from dopplershift May 1, 2018 01:40
Copy link
Contributor

@dopplershift dopplershift left a comment

Choose a reason for hiding this comment

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

Just found one docstring that needs updating. Otherwise, this is 👍 from me.

@@ -3004,14 +3005,13 @@ def _validate_converted_limits(self, limit, convert):
The limit value after call to convert(), or None if limit is None.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this function returns anything now, no?

@@ -3356,6 +3350,7 @@ def get_ylim(self):
"""
return tuple(self.viewLim.intervaly)

@munits._accepts_units(convert_y=['bottom', 'top'])
Copy link
Contributor

Choose a reason for hiding this comment

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

I just wanted to note that this fixes a bug for us where we ran into problems setting limits using units. (Note the lack of a call to _process_unit_info() in the original set_ylim() code.)

@dopplershift
Copy link
Contributor

Need another sign off to merge

@dstansby dstansby added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jun 24, 2018
@jklymak
Copy link
Member

jklymak commented Jul 4, 2018

ping @anntzer, @tacaswell - this seems good to me, but is too technical for me to know all the implications.

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
axes = args[0]
Copy link
Member

Choose a reason for hiding this comment

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

It should be noted in the docstring that the first parameter must be an Axes.

has_data = (('data' in arguments) and
(arguments['data'] is not None))
if has_data:
data = arguments['data']
Copy link
Member

Choose a reason for hiding this comment

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

Separate topic: If I'm correct, the four previous lines can be simplified to
data = arguments.get('data')

and use data is not None instead of has_data in the following.

# unit info, convert argument, and replace in *arguments* with
# converted values
for arg in convert_x:
if has_data and arguments[arg] in data:
Copy link
Member

@timhoffm timhoffm Jul 4, 2018

Choose a reason for hiding this comment

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

This does not work for structured numpy arrays:

>>> a = np.array([(1, 2), (3, 4)], dtype=[('x', float), ('y', float)])
>>> 'x' in a
False
>>> print(a['x'])
[1, 3]

See also #10872 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you know what works instead? Do we officially support structured numpy arrays?

Copy link
Member

@timhoffm timhoffm Jul 5, 2018

Choose a reason for hiding this comment

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

See the _has_item() function in #10872 for a working check.

https://matplotlib.org/api/_as_gen/matplotlib.axes.Axes.plot.html says:

All indexable objects are supported. This could e.g. be a dict, a pandas. DataFame or a structured numpy array.

Not sure if I have written it in there with the last changes. But at least nobody has vetoed. Anyway, I think it's desirable to support structured numpy arrays.


import numpy as np

from matplotlib.cbook import iterable, safe_first_element


def _accepts_units(convert_x=[], convert_y=[]):
Copy link
Member

Choose a reason for hiding this comment

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

no mutable defaults please!

@tacaswell
Copy link
Member

I am a bit concerned about this being half applied to the code base but also concerned about trying to touch everything all at once.

Can you also add a section to the documentation a "how to handle uints" section do the docs?

@dstansby
Copy link
Member Author

dstansby commented Jul 9, 2018

I'm happy to let this slip to 3.1, and expand it properly and give everyone a bit more time to think about it, especially since I don't have much time to work on it at the moment.

@tacaswell tacaswell modified the milestones: v3.0, v3.1 Jul 9, 2018
@jklymak jklymak removed status: needs review Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. labels Aug 17, 2018
Proof of concept "accepts units" decorator

Add helper function

Mssing commas

Fix docstrings disappearing

Ignore None when converting data

Remove returns section in doc
Add some more unit decorators

Add unit decorator to mplot3d
@dstansby
Copy link
Member Author

After some more thinking, it strikes me that this might not be a possible way to approach units, because in theory a user could change the plotting units (e.g. from seconds to hours) at any point before draw time, so we really do need to keep copies of the input "quantities" in order to do this conversion at some point down the line.

@jklymak
Copy link
Member

jklymak commented Sep 16, 2018

Agreed. I sent an email around with a units MEP some of which was misguided, but this is what I think would make the whole thing work. If a data variable can be converted it gets packed in a dict that has the original data object so we can always rerun the conversion on the original object. That way we can convert to np float right away without worrying that we can’t undo it.

@jklymak
Copy link
Member

jklymak commented Feb 11, 2019

@dstansby is this still active? Closing, but feel free to re-open!

@jklymak jklymak closed this Feb 11, 2019
@dstansby
Copy link
Member Author

🤷‍♂️ don't really know any more. This is at lest a consistent approach, which is arguably better than the current mess, but whether it's the correct one or not I am unsure of.

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.

7 participants