Source code for yarp.function_wrappers

"""
Wrappers for making reactive Python functions which accept and produce
:py:class:`Value` objects.
"""

import functools

from yarp import NoValue, Value, ensure_value

__names__ = [
    "fn",
    "instantaneous_fn",
]

def _function_call_on_argument_value_change(call_immediately, callback,
                                            *value_args, **value_kwargs):
    """
    Internal use. Call a regular Python function whenever a :py:class:`Value`
    in the arguments change.
    
    Parameters
    ----------
    call_immediately: bool
        If True, calls 'callback' immediately with the current value of the
        argument :py:class:`Values`.
    callback: callable
        Call this function with value-substituted arguments whenever an
        argument value changes.
    *value_args, **value_kwargs
        The arguments given to this function. These may contain
        :py:class:`Value` objects. When these values change, ``callback`` will
        be called with the latest underlying values from the arguments.
    """
    args = []
    kwargs = {}
    
    def get_args_kwargs():
        """
        Return an (args, kwargs) tuple containing the current underlying values
        of the arg/kwarg :py:class:`Value` objects.
        """
        a = [a.value for a in args]
        k = {k: a.value for k, a in kwargs.items()}
        
        return (a, k)
    
    def on_arg_changed(index, value):
        """Callback on an argument :py:class:`Value` changing."""
        args, kwargs = get_args_kwargs()
        args[index] = value
        
        callback(*args, **kwargs)
    
    def on_kwarg_changed(key, value):
        """Callback on a keyword argument :py:class:`Value` changing."""
        args, kwargs = get_args_kwargs()
        kwargs[key] = value
        
        callback(*args, **kwargs)
    
    # Wrap all args/kwargs in Value objects, if not already, and subscribe
    # to changes
    for i, arg in enumerate(map(ensure_value, value_args)):
        args.append(arg)
        arg.on_value_changed(functools.partial(on_arg_changed, i))
    
    for key, arg in value_kwargs.items():
        arg = ensure_value(arg)
        kwargs[key] = arg
        arg.on_value_changed(functools.partial(on_kwarg_changed, key))
    
    if call_immediately:
        a, k = get_args_kwargs()
        callback(*a, **k)
    


[docs]def fn(f): """ Decorator. Wraps a function so that it may be called with :py:class:`Value` objects and itself return a persistent :py:class:`Value`. Say a function is defined and wrapped with :py:func:`fn` like so:: >>> @yarp.fn ... def add(a, b): ... return a + b The function can now be called with :py:class:`Value` objects like so:: >>> a = yarp.Value(1) >>> b = yarp.Value(2) >>> c = add(a, b) The returned value will itself be a :py:class:`Value` object which will be updated whenever any of the arguments change. >>> c.value 3 The wrapped function doesn't need to know anything about :py:class:`Value` objects: the wrapper unpacks the :py:class:`Value`\ s of each argument before passing it on and automatically wrapps the return value in a :py:class:`Value`. (Non-:py:class:`Value` arguments passed to the function are automatically passed through without modification). The wrapped function is called once immediately when it is called and then again as required when its arguments change. The output :py:class:`Value` will be persistent. See also: :py:func:`instantaneous_fn`. """ @functools.wraps(f) def instance_maker(*args, **kwargs): output_value = Value() first_call = True def callback(*args, **kwargs): nonlocal first_call if first_call: first_call = False output_value._value = f(*args, **kwargs) else: output_value.value = f(*args, **kwargs) _function_call_on_argument_value_change( True, callback, *args, **kwargs) return output_value return instance_maker
[docs]def instantaneous_fn(f): """ Decorator. Like :py:func:`fn` but the function output will be wrapped as an instantaneous :py:class:`Value`. The only other difference is that the function will not be called immediately and instead will only be called later when its inputs change. """ @functools.wraps(f) def instance_maker(*args, **kwargs): output_value = Value() def callback(*args, **kwargs): output_value.set_instantaneous_value(f(*args, **kwargs)) _function_call_on_argument_value_change( False, callback, *args, **kwargs) return output_value return instance_maker