yarp API

Value and Event types

At the core of the yarp API are the Value and Event types, defined below.

class yarp.Value(initial_value=NoValue, inputs=(), get_value=None)[source]

represents a value that changes over time

Parameters:
initial_value

the value of this object on construction

inputsIterable[Reactive]

other reactive objects (Value or Event) whose changes cause the value of this object to be updated, either through get_value, or assignment to value inside their on_value_changed or Event.on_event callbacks

get_valueCallable[[], Any]

a callback which when called with no arguments returns a new value for this object

If provided, this is called during construction (overriding initial_value), and once after all objects specified in inputs have finished updating in a transaction.

This may return NoChange, in which case value will not be updated.

property value

the current value of this object

If not yet set (either in the constructor or by assigning to this property), this will be NoValue.

Setting this property will call the on_value_changed callbacks.

on_value_changed(cb)[source]

Registers cb as a callback function to be called when this value changes.

The callback function will be called with a single argument: the value now held by this object.

Note

There is no way to remove callbacks. For the moment this is an intentional restriction: if this causes you difficulties this is a good sign what you’re doing is ‘serious’ enough that yarp is not for you.

This function returns the callback passed to it making it possible to use it as a decorator if desired.

class yarp.Event(inputs=(), on_inputs_done=None)[source]

represents a value that is only known at some points in time

this is really just a list of callbacks (added with on_event), which can be called by calling emit.

inputsIterable[Reactive]

other reactive objects (Value or Event) whose changes (value changes or events) cause events to be emitted from this object, either through on_inputs_done, or calling emit inside their on_value_changed or Event.on_event callbacks

on_inputs_doneCallable[[Callable[[Any]], None], Any]

a callback which when called with a single callable argument, calls that argument with a single parameter to emit values from this object

If provided, this is called once after all objects specified in inputs have finished updating in a transaction.

on_event(cb)[source]

register a callback, which will be called whenever emit is called, with the same value

the callback is returned, so this can be used as a decorator

emit(value)[source]

emit a value, calling all callbacks registered with on_event

class yarp.Reactive(inputs)[source]

base class for reactive types (Value and Event)

this only exists to handle dependency tracking and transactions

add_input(input: Reactive)[source]

register a new input to this value

Normally inputs should be specified in the constructor, but this is needed when the logical structure of dependencies (but not the actual dependencies) is circular. This can happen when using asyncio (which breaks loops by running callbacks asynchronously) – see for example the implementation of functions like rate_limit.

yarp.NoValue = NoValue

A special value indicating that a yarp value has not been assigned a value.

yarp.NoChange = NoChange

A value returned by callbacks indicating that no change should be made to a value, or no event should be emitted.

Aggregate Values

The yarp API provides a limited set of convenience functions which which turn certain native Python data structures into Values which update whenever the underlying Values do.

yarp.value_list(list_of_values)[source]

Returns a Value consisting of a fixed list of other Values. The returned Value will change whenever one of its members does.

Parameters:
list_of_values: [:py:class:`Value`, …]

A fixed list of Values. The value of this object will be an array of the underlying values. Callbacks will be raised whenever a value in the list changes.

It is not possible to modify the list or set the contained values directly from this object.

For instantaneous list members, the instantaneous value will be present in the version of this list passed to registered callbacks but otherwise not retained. (Typically the instantaneous values will be represented by NoValue in value or in callbacks resulting from other Values changing.

yarp.value_tuple(tuple_of_values)[source]

A Value consisting of a tuple of other Values.

Parameters:
tuple_of_values: (:py:class:`Value`, …)

A fixed tuple of Values. The value of this object will be a tuple of the underlying values. Callbacks will be raised whenever a value in the tuple changes.

It is not possible to modify the tuple or set the contained values directly from this object.

For instantaneous tuple members, the instantaneous value will be present in the version of this tuple passed to registered callbacks but otherwise not retained. (Typically the instantaneous values will be represented by NoValue in value or in callbacks resulting from other Values changing.

yarp.value_dict(dict_of_values)[source]

A Value consisting of a dictionary where the values (but not keys) are Values.

Parameters:
dict_of_values: {key: :py:class:`Value`, …}

A fixed dictionary of Values. The value of this object will be a dictionary of the underlying values. Callbacks will be raised whenever a value in the dictionary changes.

It is not possible to modify the set of keys in the dictionary nor directly change the values of its elements from this object.

For instantaneous dictionary members, the instantaneous value will be present in the version of this dict passed to registered callbacks but otherwise not retained. (Typically the instantaneous values will be represented by NoValue in value or in callbacks resulting from other Values changing.

Value casting

The following low-level functions are provided for creating and casting Value and Event objects.

yarp.ensure_value(value)[source]

Ensure a variable is a Value object, wrapping it accordingly if not.

  • If already a Value, returns unmodified.

  • If a list, tuple or dict, applies ensure_value() to all contained values and returns a value_list, value_tuple or value_dict respectively.

  • If any other type, wraps the variable in a continous Value with the initial value set to the defined value.

yarp.value_to_event(source_value)[source]

make an Event which emits the new value of source_value whenever it changes

yarp.event_to_value(source_event, initial_value=NoValue)[source]

make a Value which takes its value from events of source_event

Value Operators

The Value and Event classes also supports many (but not all) of the native Python operations, producing corresponding Value or Event objects. These operations support the mixing of Value, Event and other suitable Python objects, following the same rules as fn. The following operators are supported:

  • Arithmetic
    • a + b

    • a - b

    • a * b

    • a @ b

    • a / b

    • a // b

    • a % b

    • divmod(a, b)

    • a ** b

  • Bit-wise
    • a << b

    • a >> b

    • a & b

    • a | b

    • a ^ b

  • Unary
    • -a

    • +a

    • abs(a)

    • ~a

  • Comparison
    • a < b

    • a <= b

    • a == b

    • a != b

    • a >= b

    • a > b

  • Container operators
    • a[key]

  • Numerical conversions
    • complex(a)

    • int(a)

    • float(a)

    • round(a)

  • Python object/function usage
    • a(...) will call the value as a function and return a Value or Event containing the result. This will be updated by re-calling the function whenever the input changes. Like fn(), arguments may be Value or Event objects and these will be unwrapped before the function is called and will also cause the function to be re-evaluated whenever they change. Do not use this to call functions with side effects.

    • a.name equivalent to yarp.getattr(a, "name")

Unfortunately this list doesn’t include boolean operators (i.e. not, and, or and bool). This is due to a limitation of the Python data model which means that bool may only return an actual boolean value, not some other type of object. As a workaround you can substitute:

  • bool(a) for a == True (works in most cases)

  • a and b for a & b (works for boolean values but produces numbers)

  • a or b for a | b (works for boolean values but produces numbers)

For a similar reasons, the len and in operators are also not supported.

This list also doesn’t include mutating operators, for example a[key] = b. This is because the Python objects within a Value are treated as being immutable.

Python builtins

The yarp API provides versions of a number of Python builtins and functions from the standard library which work with Value and Event:

  • Builtins
    • bool(a)

    • any(a)

    • all(a)

    • min(a)

    • max(a)

    • sum(a)

    • map(a)

    • sorted(a)

    • str(a)

    • repr(a)

    • str_format(a, ...) (equivalent to a.format(...))

    • oct(a)

    • hex(a)

    • zip(a)

    • len(a)

    • getattr(object, name[, default])

  • Most non-mutating, non-underscore prefixed functions from the operator module.

As above, these follow the same rules as fn: the result will be an Event if any of the inputs are Events, otherwise Value.

Function wrappers

The primary mode of interaction with yarp Values and Events is intended to be via simple Python functions wrapped with fn, defined below.

yarp.fn(f)[source]

Wrap a function operating on plain values so that it can accept Value/Event arguments and produces a Value/Event result.

If the function is called with only Value (or non-reactive) arguments, the result will be a Value, the result of calling the function, which updates whenever any of the inputs change. See the README example.

If the function is called with any Event values, then the result will be an Event, which emits once each time any input Event emits, with the result of calling the wrapped function with the value emitted by the event and the latest version of any Value inputs. Value changes do not cause the resulting Event to emit. For example:

>>> @fn
... def passthrough(*args):
...     return args
>>> a = Value(1)
>>> b = Event()
>>> res = passthrough(a, b)
>>> res.on_event(print)
<...>
>>> b.emit(2)
(1, 2)
>>> a.value = 3 # nothing
>>> b.emit(4)
(3, 4)

If multiple Events are passed to the wrapped function, the “one output per input event” rule still holds, and the non-firing event inputs are replaced with NoValue. For example:

>>> @fn
... def passthrough(*args):
...     return args
>>> a = Event()
>>> b = Event()
>>> res = passthrough(a, b)
>>> res.on_event(print)
<...>
>>> a.emit(1)
(1, NoValue)
>>> b.emit(2)
(NoValue, 2)

This happens even if two events occur at the same time (within one transaction):

>>> res = passthrough(a, a)
>>> res.on_event(print)
<...>
>>> a.emit(1)
(1, NoValue)
(NoValue, 1)

If the function returns NoChange, then the resulting Event will not emit, or the Value will not change.

Notes

event behaviour: It would be possible instead to only call the function once for events which occur in the same transaction, and only produce one result.

This isn’t done because it’s possible (though maybe it shouldn’t be) for an Event to emit more than once in a transaction. This isn’t a niche issue – think about something that turns high-level commands into multiple lower-level ones. What should be done then?

We could turn the values into a list, but that’s either inconsistent (if singular events are not wrapped) or messy (if they are always wrapped). It seems bad to force users to think about this annoyance in the transaction mechanism.

We could call it once per event, but what if multiple input events emit more than once? I don’t think there’s a good answer to this.

Or, we could ignore all but the last event, but that’s definitely bad.

This behaviour is chosen, even if it isn’t perfect for all uses, because it’s consistent and doesn’t force the user to understand how it interacts with the transaction processing.

The major downside is the inability to use some overloads on events (e.g. e ** 2 + e) – just use a fn for that kind of thing.

If a different behaviour is needed, either write it manually, write something which merges events in the way you need (then process the result with fn), or add options to this to pick a different behaviour.

General Value manipulation

The following utility functions are defined.

yarp.replace_novalue(source_value, replacement_if_novalue=None)[source]

If the source_value is NoValue, return replacement_if_novalue instead.

Parameters:
source_valueValue or Event
replacement_if_novaluePython object or Value

Replacement value to use if source_value has the value NoValue.

Returns:
A Value or Event matching source_value.
yarp.window(source_value: Value | Event, num_values: int | Value) Value[source]

make a Value with the num_values most recent values or events from source_value

If num_values is decreased then the return value will be cropped. If it is increased, then the return value will lengthen gradually with new events or changes.

yarp.no_repeat(source)[source]

don’t repeat the previous event or value of source

yarp.filter(source, rule=NoValue)[source]

Filter change events.

The filter rule should be a function which takes the new value as an argument and returns a boolean indicating if the value should be passed on or not.

If the source value is a Value, the old value will remain unchanged when a value change is not passed on. If the initial value does not pass the test, the initial value of the result will be NoValue.

If the filter rule is None, non-truthy values and NoValue will be filtered out. If the filter rule is NoValue (the default) only NoValue will be filtered out.

Temporal Value manipulation

The following utility functions are used to modify or observe how Values and Events change over time. These all use asyncio internally and require that a asyncio.BaseEventLoop be running.

yarp.delay(source, delay_seconds)[source]

Produce a time-delayed version of a Value or Event.

For Values, the initial value is set immediately.

The delay_seconds argument may be a constant or a Value giving the number of seconds to delay value changes. If it is increased, previously delayed values will be delayed further. If it is decreased, values which should already have been output will be output rapidly one after another.

yarp.time_window(source, duration_seconds)[source]

Produce a moving window over the historical values of a Value or the events of an Event within a given time period.

duration_seconds may be a constant or a Value giving the window duration as a number of seconds. The duration should be a number of seconds greater than zero and never be NoValue. If the value is reduced, previously inserted values will be expired earlier, possibly immediately if they should already have expired. If the value is increased, previously inserted values will have an increased timeout.

yarp.rate_limit(source, min_interval=0.1)[source]

Prevent changes occurring above a particular rate, dropping or postponing changes if necessary.

source may be a Value or Event.

The min_interval argument may be a constant or a Value. If this value is decreased, currently delayed values will be output early (or immediately if the value would have been output previously). If increased, the current delay will be increased.

yarp.emit_at(time: Value | float | int | None) Event[source]

emit an event at the times given in time

Time can be None (no events), a time in seconds as given by loop.time() (emit None at the given time), or a tuple containing the time and the value to emit.

Whenever time changes the timer is reloaded, so if the timer for the previous value has not fired, it never will. This also means that if the time changes to be before the current time, there will be one event per change.

This is mostly useful in cases where you can calculate the next time that something should happen from some Value.

The resulting Event does not depend on time, because it is always emitted in an asyncio callback (never synchronously on time changes, even if time is in the past). This makes it safe to modify time (or one of its inputs) in a callback attached to the result.

File-backed Values

The following function can be used to make very persistent Values

yarp.file_backed_value(filename, initial_value=NoValue) Value[source]

A persistent, file-backed value.

Upon creation, the value will be loaded from the specified filename. Whenever the value is changed it will be rewritten to disk. Changes made to the file while your program is running will be ignored.

If the file does not exist, it will be created and the value set to the value given by initial_value.

The value must be pickleable.

Time Values

The following function can be used to get the (continously changing) date and time:

yarp.now(interval=1.0, tz=None, loop=None) Value[source]

Returns a Value containing a datetime.datetime object holding the current time, refreshed every interval seconds.

The interval argument may be a constant or a Value giving the number of seconds to wait between updates. If the Value changes, the time until the next update will be reset starting from that moment in time.

The tz argument is passed on to datetime.datetime.now(). This must be a constant.

The loop argument should be an asyncio.BaseEventLoop in which the delays will be scheduled. If None, the default loop is used.