def file_backed_value(filename, initial_value=NoValue): """ 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. """ try: with open(filename, "rb") as f: source_value = Value(pickle.load(f)) except FileNotFoundError: # If the file doesn't exist, use the initial value source_value = Value(initial_value) except Exception: # If there's a pickling error or similar, show the error but continue # with the provided initial value. traceback.print_exc() source_value = Value(initial_value) # Store changes to disk @source_value.on_value_changed def on_value_changed(new_value): with open(filename, "wb") as f: pickle.dump(new_value, f) # Immediately trigger a store (incase the file did not exist yet) on_value_changed(source_value.value) return source_value
async def test_set_property_register(qth_client, event_loop): a = Value(123) qth_yarp.set_property("foo/bar", a, register=True, description="Something", qth_client=qth_client) # Allow asyncio functions to run... await asyncio.sleep(0.1) # Initial should have been sent to Qth qth_client.set_property.assert_called_once_with("foo/bar", 123) # Registration should have been sent qth_client.register.assert_called_once_with( "foo/bar", qth.PROPERTY_ONE_TO_MANY, "Something", delete_on_unregister=True, ) # Setting the qth value should update Qth a.value = 321 await asyncio.sleep(0.1) qth_client.set_property.assert_called_with("foo/bar", 321) # And again... a.value = 1234 await asyncio.sleep(0.1) qth_client.set_property.assert_called_with("foo/bar", 1234)
def test_change_callback(): m = Mock() v = Value() v.on_value_changed(m) v.value = 123 m.assert_called_once_with(123)
def test_operator_wrapper_continous(): # Only test addition since others are defined in exactly the same way a = Value(1) b = Value(2) a_add_b = add(a, b) assert a_add_b.value == 3 a.value = 10 assert a_add_b.value == 12
def test_ensure_value_dict(): a = 123 b = Value(456) v = ensure_value({"a": a, "b": b}) assert isinstance(v, Value) assert v.value == {"a": 123, "b": 456} b.value = 789 assert v.value == {"a": 123, "b": 789}
def test_ensure_value_tuple(): a = 123 b = Value(456) v = ensure_value((a, b)) assert isinstance(v, Value) assert v.value == (123, 456) b.value = 789 assert v.value == (123, 789)
def test_ensure_value_list(): a = 123 b = Value(456) v = ensure_value([a, b]) assert isinstance(v, Value) assert v.value == [123, 456] b.value = 789 assert v.value == [123, 789]
def test_value_dict_persistent(): a = Value("a") b = Value("b") c = Value("c") dct = value_dict({"a": a, "b": b, "c": c}) # Initial value should have passed through assert dct.value == {"a": "a", "b": "b", "c": "c"} m = Mock() dct.on_value_changed(m) # Changes should propagate through a.value = "A" assert dct.value == {"a": "A", "b": "b", "c": "c"} m.assert_called_once_with({"a": "A", "b": "b", "c": "c"}) m.reset_mock() b.value = "B" assert dct.value == {"a": "A", "b": "B", "c": "c"} m.assert_called_once_with({"a": "A", "b": "B", "c": "c"}) m.reset_mock() c.value = "C" assert dct.value == {"a": "A", "b": "B", "c": "C"} m.assert_called_once_with({"a": "A", "b": "B", "c": "C"})
def test_value_list_persistent(): a = Value("a") b = Value("b") c = Value("c") lst = value_list([a, b, c]) # Initial value should have passed through assert lst.value == ["a", "b", "c"] m = Mock() lst.on_value_changed(m) # Changes should propagate through a.value = "A" assert lst.value == ["A", "b", "c"] m.assert_called_once_with(["A", "b", "c"]) m.reset_mock() b.value = "B" assert lst.value == ["A", "B", "c"] m.assert_called_once_with(["A", "B", "c"]) m.reset_mock() c.value = "C" assert lst.value == ["A", "B", "C"] m.assert_called_once_with(["A", "B", "C"])
def test_value_tuple_persistent(): a = Value("a") b = Value("b") c = Value("c") tup = value_tuple((a, b, c)) # Initial value should have passed through assert tup.value == ("a", "b", "c") m = Mock() tup.on_value_changed(m) # Changes should propagate through a.value = "A" assert tup.value == ("A", "b", "c") m.assert_called_once_with(("A", "b", "c")) m.reset_mock() b.value = "B" assert tup.value == ("A", "B", "c") m.assert_called_once_with(("A", "B", "c")) m.reset_mock() c.value = "C" assert tup.value == ("A", "B", "C") m.assert_called_once_with(("A", "B", "C"))
def test_check_initial_value(): # Initial value should also be filtered rule = lambda x: x == 123 v = Value(123) fl = filter(v, rule) assert fl.value == 123 v = Value(321) fl = filter(v, rule) assert fl.value is NoValue
def test_ensure_value_nested(): a = Value(123) b = Value(456) c = Value(789) v = ensure_value({"a": a, "bc": [b, c]}) assert isinstance(v, Value) assert v.value == {"a": 123, "bc": [456, 789]} b.value = 654 assert v.value == {"a": 123, "bc": [654, 789]}
def test_make_instantaneous(): v = Value(1) iv = make_instantaneous(v) m = Mock() iv.on_value_changed(m) assert iv.value is NoValue v.value = 2 assert iv.value is NoValue m.assert_called_once_with(2)
def test_inst_positional_kwargs(): m = Mock() @instantaneous_fn def example(*args, **kwargs): return (args, kwargs) a_value = Value() b_value = Value() # No value should be assigned result = example(a=a_value, b=b_value) result.on_value_changed(m) assert result.value is NoValue # Changes should propagate, callbacks should fire but no value should be # stored m.reset_mock() a_value.set_instantaneous_value(123) m.assert_called_once_with(((), {"a": 123, "b": NoValue})) assert result.value is NoValue m.reset_mock() b_value.set_instantaneous_value(123) m.assert_called_once_with(((), {"a": NoValue, "b": 123})) assert result.value is NoValue
def test_make_persistent(): v = Value() # Initially no value to be found... pv = make_persistent(v) assert pv.value is NoValue m = Mock() pv.on_value_changed(m) assert pv.value is NoValue v.set_instantaneous_value(2) assert pv.value == 2 m.assert_called_once_with(2)
def filter(source_value, rule=NoValue): """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 persistent, the persistent value will remain unchanged when a value change is not passed on. 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. """ source_value = ensure_value(source_value) output_value = Value(source_value.value if ( source_value.value is not NoValue and _check_value(source_value.value, rule)) else NoValue) @source_value.on_value_changed def on_source_value_changed(new_value): if _check_value(new_value, rule): output_value._value = source_value.value output_value.set_instantaneous_value(new_value) return output_value
def window(source_value, num_values): """Produce a moving window over a :py:class:`Value`'s historical values. This function treats the Value it is passed as a persistent Value, even if it is instantaneous (since a window function doesn't really have any meaning for a instantaneous values). The ``num_values`` argument may be a (persistent) Value or a constant indicating the number of entries in the window. If this value later reduced, the contents of the window will be truncated immediately. If it is increaesd, any previously dropped values will not return. ``num_values`` is always assumed to be an integer greater than zero and never ``NoValue``. """ source_value = ensure_value(source_value) output_value = Value([source_value.value]) num_values = ensure_value(num_values) assert num_values.value >= 1 @source_value.on_value_changed def on_source_value_changed(new_value): """Internal. Insert incoming Value into the window.""" output_value.value = (output_value.value + [new_value])[-num_values.value:] @num_values.on_value_changed def on_num_values_changed(_instantaneous_new_num_values): """Internal. Handle window size changes.""" # Truncate the window data if required new_num_values = num_values.value assert new_num_values >= 1 if len(output_value.value) > new_num_values: output_value.value = output_value.value[-new_num_values:] return output_value
def test_change_persistent(): rule = lambda x: x < 10 m = Mock() v = Value(1) fl = filter(v, rule) fl.on_value_changed(m) assert fl.value == 1 v.value = 2 assert fl.value == 2 m.assert_called_once_with(2) # Above ten, shouldn't get through v.value = 100 assert fl.value == 2 m.assert_called_once_with(2)
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
def test_change_persistent_initial_value_filtered(): rule = lambda x: x < 10 v = Value(123) fl = filter(v, rule) # Initial value should be rejected by the filter and thus not passed # through assert fl.value is NoValue
def test_no_repeat_persistent(): v = Value(1) # Initial value should come through nrv = no_repeat(v) assert nrv.value == 1 m = Mock() nrv.on_value_changed(m) # Same value doesn't pass through v.value = 1 assert not m.called # New values do v.value = 2 assert nrv.value == 2 m.assert_called_once_with(2)
def test_getattr_value_name(): # Special attention since this is important m = Mock() m.foo = "FOO!" m.bar = "BAR!" v = Value(m) name_v = Value("foo") attr_v = getattr(v, name_v) assert attr_v.value == "FOO!" log = [] attr_v.on_value_changed(log.append) name_v.value = "bar" assert attr_v.value == "BAR!" assert log == ["BAR!"]
async def test_instantaneous(self, event_loop): value = Value() delayed_value = delay(value, 0.1, loop=event_loop) assert delayed_value.value is NoValue # Monitor changes evt = asyncio.Event(loop=event_loop) m = Mock(side_effect=lambda *_: evt.set()) delayed_value.on_value_changed(m) # Trigger a change for later... before = time.time() value.set_instantaneous_value(123) assert delayed_value.value is NoValue assert not m.mock_calls await evt.wait() assert time.time() - before >= 0.1 m.assert_called_once_with(123) assert delayed_value.value is NoValue
def test_getattr_string_name(): # Special attention since this is important m = Mock() m.foo = "bar" v = Value(m) foo_v = getattr(v, "foo") assert isinstance(foo_v, Value) assert foo_v.value == "bar" log = [] foo_v.on_value_changed(log.append) m2 = Mock() m2.foo = "baz" v.value = m2 assert foo_v.value == "baz" assert log == ["baz"]
def test_str_format_operator(): # Special attention due for str_format as its an almost custom function(!) a = Value(0xAB) b = Value("hi") fmt = Value("{}, {}") stringified = str_format(fmt, a, b) assert stringified.value == "171, hi" a.value = 0xBC assert stringified.value == "188, hi" b.value = "foo" assert stringified.value == "188, foo" fmt.value = "0x{:04X}: {!r}" assert stringified.value == "0x00BC: 'foo'"
def test_positional_kwargs(): m = Mock() @fn def example(a, b): return a - b a_value = Value(10) b_value = Value(5) # Initial value should pass through result = example(a=a_value, b=b_value) result.on_value_changed(m) assert result.value == 5 # Changes should propagate, callbacks should fire m.reset_mock() a_value.value = 20 m.assert_called_once_with(15) assert result.value == 15 m.reset_mock() b_value.value = -5 m.assert_called_once_with(25) assert result.value == 25
async def test_send_event_register(qth_client, event_loop): a = Value() qth_yarp.send_event("foo/bar", a, register=True, description="Something", qth_client=qth_client) # Allow asyncio functions to run... await asyncio.sleep(0.1) # No initial value should have been sent to Qth assert len(qth_client.send_event.mock_calls) == 0 # No registration should be made # Registration should have been sent qth_client.register.assert_called_once_with( "foo/bar", qth.EVENT_ONE_TO_MANY, "Something", ) # Setting the qth value should update Qth a.set_instantaneous_value(321) await asyncio.sleep(0.1) qth_client.send_event.assert_called_with("foo/bar", 321) # And again... a.set_instantaneous_value(1234) await asyncio.sleep(0.1) qth_client.send_event.assert_called_with("foo/bar", 1234)
async def test_set_property(qth_client, event_loop): a = Value(123) qth_yarp.set_property("foo/bar", a, qth_client=qth_client) # Allow asyncio functions to run... await asyncio.sleep(0.1) # Initial should have been sent to Qth qth_client.set_property.assert_called_once_with("foo/bar", 123) # No registration should be made assert len(qth_client.register.mock_calls) == 0 # Setting the qth value should update Qth a.value = 321 await asyncio.sleep(0.1) qth_client.set_property.assert_called_with("foo/bar", 321) # And again... a.value = 1234 await asyncio.sleep(0.1) qth_client.set_property.assert_called_with("foo/bar", 1234)
def test_change_callback_only(): m = Mock() v = Value() v.on_value_changed(m) v.set_instantaneous_value(123) m.assert_called_once_with(123) assert v.value is NoValue
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