def test_window(): v = Value(1) ws = Value(3) win = window(v, ws) # Should start with the current value assert win.value == [1] # Values should accumulate v.value = 2 assert win.value == [1, 2] v.value = 3 assert win.value == [1, 2, 3] # Should truncate to only most recent values v.value = 4 assert win.value == [2, 3, 4] # Increasing the window size should be possible ws.value = 4 v.value = 5 assert win.value == [2, 3, 4, 5] v.value = 6 assert win.value == [3, 4, 5, 6] # Decreasing it should be too ws.value = 2 assert win.value == [5, 6] v.value = 7 assert win.value == [6, 7]
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_list_instantaneous(): # A mix of instantaneous and continuous a = Value("a") b = Value() c = Value() lst = value_list([a, b, c]) # Initial value should have passed through assert lst.value == ["a", NoValue, NoValue] m = Mock() lst.on_value_changed(m) # Changes should propagate through a.value = "A" assert lst.value == ["A", NoValue, NoValue] m.assert_called_once_with(["A", NoValue, NoValue]) # Instantaneous values should propagate only into the callback m.reset_mock() b.set_instantaneous_value("b") assert lst.value == ["A", NoValue, NoValue] m.assert_called_once_with(["A", "b", NoValue]) m.reset_mock() c.set_instantaneous_value("c") assert lst.value == ["A", NoValue, NoValue] m.assert_called_once_with(["A", NoValue, "c"])
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_value_tuple_instantaneous(): # A mix of instantaneous and continuous a = Value("a") b = Value() c = Value() tup = value_tuple([a, b, c]) # Initial value should have passed through assert tup.value == ("a", NoValue, NoValue) m = Mock() tup.on_value_changed(m) # Changes should propagate through a.value = "A" assert tup.value == ("A", NoValue, NoValue) m.assert_called_once_with(("A", NoValue, NoValue)) # Instantaneous values should propagate only into the callback m.reset_mock() b.set_instantaneous_value("b") assert tup.value == ("A", NoValue, NoValue) m.assert_called_once_with(("A", "b", NoValue)) m.reset_mock() c.set_instantaneous_value("c") assert tup.value == ("A", NoValue, NoValue) m.assert_called_once_with(("A", NoValue, "c"))
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
def test_value_dict_instantaneous(): # A mix of instantaneous and continuous a = Value("a") b = Value() c = Value() dct = value_dict({"a": a, "b": b, "c": c}) # Initial value should have passed through assert dct.value == {"a": "a", "b": NoValue, "c": NoValue} m = Mock() dct.on_value_changed(m) # Changes should propagate through a.value = "A" assert dct.value == {"a": "A", "b": NoValue, "c": NoValue} m.assert_called_once_with({"a": "A", "b": NoValue, "c": NoValue}) # Instantaneous values should propagate only into the callback m.reset_mock() b.set_instantaneous_value("b") assert dct.value == {"a": "A", "b": NoValue, "c": NoValue} m.assert_called_once_with({"a": "A", "b": "b", "c": NoValue}) m.reset_mock() c.set_instantaneous_value("c") assert dct.value == {"a": "A", "b": NoValue, "c": NoValue} m.assert_called_once_with({"a": "A", "b": NoValue, "c": "c"})
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
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_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_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_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_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
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)
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)
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_callback(): m = Mock() v = Value() v.on_value_changed(m) v.value = 123 m.assert_called_once_with(123)
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 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_replace_novalue(): a = Value() replacement = Value(123) ar = replace_novalue(a, replacement) assert ar.value == 123 a.value = "hi" assert ar.value == "hi" a.value = NoValue assert ar.value == 123 replacement.value = 321 assert ar.value == 321 a.value = "bye" assert ar.value == "bye"
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_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 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_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_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_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_rate_limit_instantaneous(event_loop): v = Value() # No initial value to speak of rlv = rate_limit(v, 0.1, event_loop) assert rlv.value is NoValue log = [] sem = asyncio.Semaphore(0, loop=event_loop) def on_change(new_value): log.append(new_value) sem.release() rlv.on_value_changed(on_change) # First change should make it through immediately v.set_instantaneous_value(1) assert rlv.value is NoValue assert len(log) == 1 assert log[-1] == 1 await sem.acquire() # Another change made immediately after should be delayed v.set_instantaneous_value(2) assert rlv.value is NoValue assert len(log) == 1 # Change should come through after a delay before = time.time() await sem.acquire() assert time.time() - before >= 0.1 assert rlv.value is NoValue assert len(log) == 2 assert log[-1] == 2 # After a suitable delay, the next change should come through immediately await asyncio.sleep(0.15, loop=event_loop) v.set_instantaneous_value(3) assert rlv.value is NoValue assert len(log) == 3 assert log[-1] == 3 await sem.acquire() # A rapid succession of calls should result in only the last value # comming out, and then only after a delay v.set_instantaneous_value(4) v.set_instantaneous_value(5) v.set_instantaneous_value(6) assert rlv.value is NoValue assert len(log) == 3 before = time.time() await sem.acquire() assert time.time() - before >= 0.1 assert rlv.value is NoValue assert len(log) == 4 assert log[-1] == 6
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 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