-
Notifications
You must be signed in to change notification settings - Fork 14
expects decorator #143
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
Open
TomNicholas
wants to merge
55
commits into
xarray-contrib:main
Choose a base branch
from
TomNicholas:expects_decorator
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
expects decorator #143
Changes from 2 commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
044d59a
draft implementation of @expects
TomNicholas 0754f22
sketch of different tests needed
TomNicholas e879ef9
idea for test
TomNicholas aad7936
upgrade check then convert function to optionally take magnitude
TomNicholas e354f4e
removed magnitude option
TomNicholas 7727d8e
works for single return value
TomNicholas 1379779
works for single kwarg
TomNicholas 77f5d02
works for multiple return values
TomNicholas 71f4200
allow passing through arguments unchecked
TomNicholas a710741
check types of units
TomNicholas 497e97f
remove uneeded option to specify a lack of return value
TomNicholas 00219bc
check number of inputs and return values
TomNicholas 9e92f21
removed nonlocal keyword
TomNicholas 86f7e58
generalised to handle specifying dicts of units
TomNicholas 2141c6c
type hint for func
TomNicholas a94a6ae
type hint for args_units
TomNicholas a2cc63f
Merge branch 'expects_decorator' of https://github.com/TomNicholas/pi…
TomNicholas 7103483
numpy-style type hints for all arguments
TomNicholas 59ddf86
whats new
TomNicholas a5a2493
add to API docs
TomNicholas e5e84fb
use always_iterable
TomNicholas 3f59414
hashable
TomNicholas b281674
hashable
TomNicholas c669105
hashable
TomNicholas 9ac8887
dict comprehension
TomNicholas 0a6447d
list comprehension
TomNicholas c29f935
unindent if/else
TomNicholas 81913a6
missing parenthesis
TomNicholas 4de6f4d
simplify if/else logic for checking there were actually results
TomNicholas 83e422f
return results immediately if a tuple
TomNicholas 37c3fbc
allow for returning Datasets from wrapped funciton
TomNicholas 9c19af0
Update docs/api.rst
TomNicholas 0b5c7c0
correct indentation of docstring
TomNicholas 0f50305
use inspects to check number of arguments passed to decorated function
TomNicholas 57d341e
reformat the docstring
keewis 8845b77
update the definition of unit-like
keewis bc41425
simplify if/else statement
TomNicholas aba2d11
Merge branch 'expects_decorator' of https://github.com/TomNicholas/pi…
TomNicholas 0350308
check units in .to instead
TomNicholas 3a24a73
remove extra xfailed test
TomNicholas 19fd6e0
test raises on unquantified input
TomNicholas d2d74e4
add example of function which optionally accepts dimensionless weights
TomNicholas 1c4feb4
Merge branch 'main' into expects_decorator
keewis 7a6f2cb
Merge branch 'main' into expects_decorator
keewis 85b982c
rewrite using inspect.Signature's bind and bind_partial
keewis 5ea484b
also allow converting and stripping Variable objects
keewis b7c71c1
implement the conversion functions
keewis b39fff3
simplify the return construct
keewis 61e0299
code reorganization
keewis 63d8aeb
black
keewis 32a57b2
fix a test
keewis 91b4826
remove the note about coordinates not being checked [skip-ci]
keewis a43dd13
reword the error message raised when there's no units for some parame…
keewis 7ea921c
move the changelog to a new section
keewis b92087a
Merge branch 'main' into expects_decorator
keewis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import functools | ||
|
||
from pint import Quantity | ||
from xarray import DataArray | ||
|
||
from .accessors import PintDataArrayAccessor | ||
|
||
|
||
def expects(*args_units, return_units=None, **kwargs_units): | ||
""" | ||
Decorator which checks the inputs and outputs of the decorated function have certain units. | ||
|
||
Arguments | ||
|
||
Note that the coordinates of input DataArrays are not checked, only the data. | ||
So if your decorated function uses coordinates and you wish to check their units, | ||
you should pass the coordinates of interest as separate arguments. | ||
|
||
Parameters | ||
---------- | ||
func: function | ||
TomNicholas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Decorated function, which accepts zero or more xarray.DataArrays or pint.Quantitys as inputs, | ||
and may optionally return one or more xarray.DataArrays or pint.Quantitys. | ||
args_units : Union[str, pint.Unit, None] | ||
TomNicholas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Unit to expect for each positional argument given to func. | ||
|
||
A value of None indicates not to check that argument for units (suitable for flags | ||
and other non-data arguments). | ||
return_units : Union[Union[str, pint.Unit, None, False], Sequence[Union[str, pint.Unit, None]], Optional | ||
The expected units of the returned value(s), either as a single unit or as an iterable of units. | ||
|
||
A value of None indicates not to check that return value for units (suitable for flags and other | ||
non-data arguments). Passing False means that no return value is expected from the function at all, | ||
and an error will be raised if a return value is found. | ||
kwargs_units : Dict[str, Union[str, pint.Unit, None]], Optional | ||
Unit to expect for each keyword argument given to func. | ||
|
||
A value of None indicates not to check that argument for units (suitable for flags | ||
and other non-data arguments). | ||
|
||
Returns | ||
------- | ||
return_values | ||
Return values of the wrapped function, either a single value or a tuple of values. | ||
|
||
Raises | ||
------ | ||
TypeError | ||
If an argument or return value has a specified unit, but is not an xarray.DataArray. | ||
|
||
|
||
Examples | ||
-------- | ||
|
||
Decorating a function which takes one quantified input, but returns a non-data value (in this case a boolean). | ||
|
||
>>> @expects('deg C') | ||
... def above_freezing(temp): | ||
... return temp > 0 | ||
... | ||
|
||
|
||
TODO: example where we check units of an optional weighted kwarg | ||
""" | ||
|
||
# TODO: Check args_units, kwargs_units, and return_units types | ||
# TODO: Check number of arguments line up | ||
|
||
def _expects_decorator(func): | ||
|
||
@functools.wraps(func) | ||
def _unit_checking_wrapper(*args, **kwargs): | ||
|
||
converted_args = [] | ||
for arg, arg_unit in zip(args, args_units): | ||
converted_arg = _check_then_convert_to(arg, arg_unit) | ||
converted_args.append(converted_arg) | ||
|
||
converted_kwargs = {} | ||
for key, val in kwargs.items(): | ||
kwarg_unit = kwargs_units[key] | ||
converted_kwargs[key] = _check_then_convert_to(val, kwarg_unit) | ||
|
||
results = func(*converted_args, **converted_kwargs) | ||
|
||
if results is not None: | ||
if return_units is False: | ||
raise ValueError("Did not expect function to return anything") | ||
elif return_units is not None: | ||
# TODO check something was actually returned | ||
# TODO check same number of things were returned as expected | ||
# TODO handle single return value vs tuple of return values | ||
|
||
converted_results = [] | ||
for return_unit, return_value in zip(return_units, results): | ||
converted_result = _check_then_convert_to(return_value, return_unit) | ||
converted_results.append(converted_result) | ||
|
||
return tuple(converted_results) | ||
else: | ||
return results | ||
else: | ||
if return_units: | ||
raise ValueError("Expected function to return something") | ||
|
||
return _unit_checking_wrapper | ||
|
||
return _expects_decorator | ||
|
||
|
||
def _check_then_convert_to(obj, units): | ||
if isinstance(obj, Quantity): | ||
return obj.to(units) | ||
elif isinstance(obj, DataArray): | ||
return obj.pint.to(units) | ||
else: | ||
raise TypeError("Can only expect units for arguments of type xarray.DataArray or pint.Quantity," | ||
f"not {type(obj)}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import pytest | ||
import pint | ||
import xarray as xr | ||
|
||
from pint import UnitRegistry | ||
|
||
from ..checking import expects | ||
|
||
ureg = UnitRegistry() | ||
|
||
|
||
class TestExpects: | ||
def test_single_arg(self): | ||
|
||
@expects('degC') | ||
def above_freezing(temp : pint.Quantity): | ||
return temp.magnitude > 0 | ||
|
||
f_q = pint.Quantity(20, units='degF') | ||
assert above_freezing(f_q) == False | ||
|
||
c_q = pint.Quantity(-2, units='degC') | ||
assert above_freezing(c_q) == False | ||
|
||
@expects('degC') | ||
def above_freezing(temp : xr.DataArray): | ||
return temp.pint.magnitude > 0 | ||
|
||
f_da = xr.DataArray(20).pint.quantify(units='degF') | ||
assert above_freezing(f_da) == False | ||
|
||
c_da = xr.DataArray(-2).pint.quantify(units='degC') | ||
assert above_freezing(c_da) == False | ||
|
||
def test_single_kwarg(self): | ||
|
||
@expects('meters', c='meters / second') | ||
def freq(wavelength, c=None): | ||
if c is None: | ||
c = ureg.speed_of_light | ||
|
||
return c / wavelength | ||
|
||
def test_single_return_value(self): | ||
|
||
@expects('Hz') | ||
def period(freq): | ||
return 1 / freq | ||
|
||
f = pint.Quantity(10, units='Hz') | ||
|
||
# test conversion | ||
T = period(f) | ||
assert f.units == 'seconds' | ||
|
||
# test wrong dimensions for conversion | ||
... | ||
|
||
@pytest.mark.xfail | ||
def test_multiple_return_values(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_mixed_args_kwargs_return_values(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_invalid_input_types(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_invalid_return_types(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_unquantified_arrays(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_wrong_number_of_args(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_nonexistent_kwarg(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_unexpected_return_value(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_expected_return_value(self): | ||
raise NotImplementedError | ||
|
||
@pytest.mark.xfail | ||
def test_wrong_number_of_return_values(self): | ||
raise NotImplementedError |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.