-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #198 from mkelley/dataclass-input-deco
Re-write quantity_to_dataclass; new dataclass_input
- Loading branch information
Showing
5 changed files
with
276 additions
and
119 deletions.
There are no files selected for viewing
This file contains 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 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 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,196 @@ | ||
# Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
"""sbpy Data Decorators | ||
Based on astropy's units decorator: `~astropy.units.quantity_input`. | ||
""" | ||
|
||
__all__ = [ | ||
'quantity_to_dataclass', | ||
'dataclass_input' | ||
] | ||
|
||
import inspect | ||
from functools import wraps | ||
from astropy.table import Table, QTable | ||
from .core import DataClass | ||
|
||
|
||
def quantity_to_dataclass(**kwargs): | ||
"""Decorator that converts astropy quantities to sbpy data classes. | ||
Use this decorator when your function is based on a single field | ||
in an sbpy `~sbpy.data.DataClass`. | ||
Examples | ||
-------- | ||
This function accepts `~sbpy.data.Ephem` objects, but only uses | ||
heliocentric distance: | ||
>>> import astropy.units as u | ||
>>> import sbpy.data as sbd | ||
>>> | ||
>>> @sbd.quantity_to_dataclass(eph=(sbd.Ephem, 'rh')) | ||
... def temperature(eph): | ||
... return 278 * u.K / (eph['rh'] / u.au)**0.5 | ||
>>> | ||
>>> print(temperature(1 * u.au)) # doctest: +FLOAT_CMP | ||
[278.] K | ||
>>> eph = sbd.Ephem.from_dict({'rh': 1 * u.au}) | ||
>>> print(temperature(eph)) # doctest: +FLOAT_CMP | ||
[278.] K | ||
""" | ||
|
||
def decorator(wrapped_function): | ||
decorator_kwargs = kwargs # for clarity | ||
|
||
# Extract the function signature for the function we are wrapping. | ||
wrapped_signature = inspect.signature(wrapped_function) | ||
|
||
@wraps(wrapped_function) | ||
def wrapper(*func_args, **func_kwargs): | ||
# Bind the arguments of our new function to the signature | ||
# of the original. | ||
bound_args = wrapped_signature.bind(*func_args, **func_kwargs) | ||
|
||
for param in wrapped_signature.parameters.values(): | ||
# is this a parameter that we might want to replace? | ||
if param.name not in decorator_kwargs: | ||
# no | ||
continue | ||
|
||
# get requested DataClass and field name | ||
dataclass, field = None, None | ||
for v in decorator_kwargs[param.name]: | ||
if isinstance(v, str): | ||
field = v | ||
elif issubclass(v, DataClass): | ||
dataclass = v | ||
|
||
if any((dataclass is None, field is None)): | ||
raise ValueError( | ||
'quantity_to_dataclass decorator requires a ' | ||
'DataClass object and a field name as a string.') | ||
|
||
# check passed argument, update as needed | ||
arg = bound_args.arguments[param.name] | ||
if not isinstance(arg, dataclass): | ||
# Argument is not a DataClass. Make it so. | ||
new_arg = dataclass.from_dict({field: arg}) | ||
bound_args.arguments[param.name] = new_arg | ||
|
||
return wrapped_function(*bound_args.args, **bound_args.kwargs) | ||
return wrapper | ||
return decorator | ||
|
||
|
||
class DataClassInput: | ||
@classmethod | ||
def as_decorator(cls, func=None, **kwargs): | ||
"""Decorator that converts parameters to `DataClass`. | ||
sbpy methods use ``DataClass`` objects whenever possible. But for | ||
convenience, we may let users pass other objects that are | ||
internally converted: | ||
* dictionary, | ||
* file name, | ||
* `astropy.table.Table` or `~astropy.table.QTable`. | ||
Examples | ||
-------- | ||
>>> import astropy.units as u | ||
>>> import sbpy.data as sbd | ||
>>> | ||
>>> @sbd.dataclass_input(eph=sbd.Ephem) | ||
... def myfunction(eph): | ||
... return eph['rh']**2 * eph['delta']**2 | ||
>>> | ||
>>> dictionary = {'rh': 2 * u.au, 'delta': 1 * u.au} | ||
>>> print(myfunction(dictionary)) # doctest: +FLOAT_CMP | ||
[4.0] AU4 | ||
>>> | ||
>>> from astropy.table import QTable | ||
>>> qtable = QTable([[2] * u.au, [1] * u.au], names=('rh', 'delta')) | ||
>>> print(myfunction(qtable)) # doctest: +FLOAT_CMP | ||
[4.0] AU4 | ||
Data classes may also be specified with function annotations: | ||
>>> import sbpy.data as sbd | ||
>>> | ||
>>> @sbd.dataclass_input | ||
... def myfunction(eph: sbd.Ephem): | ||
... return eph['rh']**2 * eph['delta']**2 | ||
""" | ||
self = cls(**kwargs) | ||
if func is not None and not kwargs: | ||
return self(func) | ||
else: | ||
return self | ||
|
||
def __init__(self, func=None, **kwargs): | ||
self.decorator_kwargs = kwargs | ||
|
||
def __call__(self, wrapped_function): | ||
# Extract the function signature for the function we are | ||
# wrapping. | ||
wrapped_signature = inspect.signature(wrapped_function) | ||
|
||
@wraps(wrapped_function) | ||
def wrapper(*func_args, **func_kwargs): | ||
# Bind the arguments of our new function to the signature | ||
# of the original. | ||
bound_args = wrapped_signature.bind(*func_args, | ||
**func_kwargs) | ||
|
||
for param in wrapped_signature.parameters.values(): | ||
# is this a parameter that we might want to replace? | ||
if param.name in self.decorator_kwargs: | ||
target = self.decorator_kwargs[param.name] | ||
else: | ||
target = param.annotation | ||
|
||
# not in decorator_kwargs and not annotated | ||
if target is inspect.Parameter.empty: | ||
continue | ||
|
||
arg = bound_args.arguments[param.name] | ||
|
||
# argument value is None, and the default value is | ||
# None, pass through the None | ||
if arg is None and param.default is None: | ||
continue | ||
|
||
# not a DataClass? carry on. | ||
try: | ||
if issubclass(target, DataClass): | ||
dataclass = target | ||
else: | ||
continue | ||
except TypeError: | ||
continue | ||
|
||
# check passed argument, update as needed | ||
if isinstance(arg, dict): | ||
new_arg = dataclass.from_dict(arg) | ||
elif isinstance(arg, (Table, QTable)): | ||
new_arg = dataclass.from_table(arg) | ||
elif isinstance(arg, str): | ||
new_arg = dataclass.from_file(arg) | ||
else: | ||
continue | ||
|
||
bound_args.arguments[param.name] = new_arg | ||
|
||
return wrapped_function(*bound_args.args, | ||
**bound_args.kwargs) | ||
return wrapper | ||
|
||
|
||
dataclass_input = DataClassInput.as_decorator |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.