do we want to use decorators to "hide" compatibility checks? #310
Replies: 6 comments
-
Yeah I think I mentioned this, and that's what I meant! Indeed I think it would be cool. |
Beta Was this translation helpful? Give feedback.
-
will leave some relevant info here: so, what not so trivial is to extract values of some specific arguments that were passed to one of our functions, i.e.: @check_cooler_balanced
def expected_cis(
clr,
view_df=None,
intra_only=True,
smooth=True,
aggregate_smoothed=True,
smooth_sigma=0.1,
clr_weight_name="weight",
ignore_diags=2
...
) in this case hypothetical
some relevant bit for introspecting functions: import inspect
argspec = inspect.getfullargspec(func)
assert "clr" in argspec.args
assert "clr_weight_name" in argspec.args
# catching strictly positional argument
if "clr" not in kwargs:
_arg_index = argspec.args.index("clr")
_val_passed = args[_arg_index]
clr = _val_passed
else:
clr = kwargs["clr"]
# last n-defaults !
argspec.defaults
... there is also that https://www.python.org/dev/peps/pep-3102/ - that @mimakaev brought up some time ago (enforcing kwargs form for some arguments) |
Beta Was this translation helpful? Give feedback.
-
this is mega https://stackoverflow.com/questions/57314496/how-to-intercept-and-preproccess-arguments-with-decorators |
Beta Was this translation helpful? Give feedback.
-
from functools import wraps
import inspect
# helper functions to intercept values of specific arguments
# passed to func
def _intercept_positional_arg(name, args, kwargs, argspec):
"""
intercept passed value of a positional argument {name}
positional argument can be passed in:
1. kwargs
2. args
"""
if name in kwargs: # passed as kwarg
return kwargs[name]
else: # passed as arg or not at all
_index = argspec.args.index(name)
if len(args) > _index:
return args[_index]
else: # not passed at all
raise ValueError(f"{name} argument was not passed")
def _get_default(name, argspec):
"""
get default of an argument {name}, given its {argspec}
"""
_n_defaults = len(argspec.defaults) # last n defaults
_n_args = len(argspec.args) # total number of args
_n_pos_args = _n_args - _n_defaults # number of positional args
# positional args without defaults
# argspec.args[:-_n_defaults]
# {name} should be among args with defaults
assert name in argspec.args[-_n_defaults:]
_index = argspec.args.index(name)
_index_of_default = _index - _n_pos_args
return argspec.defaults[_index_of_default]
def _intercept_default_arg(name, args, kwargs, argspec):
"""
intercept passed value of an argument {name} with default
argument with default can be passed in:
1. kwargs
2. args
3. not passed at all (use its default)
"""
if name in kwargs:
# it was passed as kwarg
return kwargs[name]
else:
# either in args or nowhere
_index = argspec.args.index(name)
if len(args) > _index:
# was passed as arg
return args[_index]
else:
# not passed at all - default
return _get_default(name, argspec)
def check_cooler_balanced(func):
"""
this decorator validates some special relationship between
arguments of {func}
"""
@wraps(func)
def wrapper(*args, **kwargs):
argspec = inspect.getfullargspec(func)
# intercept relevant arguments
_clr = _intercept_positional_arg("clr", args, kwargs, argspec)
_weight = _intercept_default_arg("clr_weight_name", args, kwargs, argspec)
# check them for compatibility
if not is_cooler_balanced(
clr=_clr,
clr_weight_name=_weight,
):
raise ValueError("cooler ain't no balanced, yo!")
else:
result = func(*args, **kwargs)
return result
return wrapper
@check_cooler_balanced
def xxx(clr, clr_weight_name="weight"):
"""useless func that works for balanced coolers only !"""
return clr.bins()[:10] |
Beta Was this translation helpful? Give feedback.
-
# this does not work with kwonlyargs yet !!!
@check_cooler_balanced
def xxx(clr, *, clr_weight_name="weight"):
"""useless func that works for balanced coolers only !"""
return clr.bins()[:10] |
Beta Was this translation helpful? Give feedback.
-
here is what looks like a robust solution using only built-in Python stuff: import inspect
from functools import wraps
def check_cooler_balanced2(func):
sig = inspect.signature(func) # get func's signature
@wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs) # bind passed args, kwargs
bound.apply_defaults() # apply defaults - make all args filled in
# bound.arguments is a dict holding every argument name and every passed value
# use bound.arguments to intercept relevant arguments, e.g. clr and clr_weight_name:
_clr = bound.arguments["clr"]
_weight = bound.arguments["clr_weight_name"]
# check them for compatibility
if not is_cooler_balanced(
clr=_clr,
clr_weight_name=_weight,
):
raise ValueError("cooler ain't no balanced, yo!")
else:
result = func(*args, **kwargs)
return result
return wrapper
# this works !
@check_cooler_balanced2
def xxx(clr, clr_weight_name="weight"):
"""useless func that works for balanced coolers only !"""
return clr.bins()[:10]
# this also works now!
@check_cooler_balanced2
def xxx(clr, *, clr_weight_name="weight"):
"""useless func that works for balanced coolers only !"""
return clr.bins()[:10] |
Beta Was this translation helpful? Give feedback.
-
from one of the discussions about our new "checks" someone mentioned "decorators" ...
Are we looking to do something like below:
I think this would be very very cool and improve readability of the code
Beta Was this translation helpful? Give feedback.
All reactions