From 412e0a51c70549adf81cf71d4de6fed7030324a1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 26 Feb 2021 01:12:03 +0100 Subject: [PATCH] implement property deletion handling; activate scheduler deletion --- owrx/config/dynamic.py | 10 ++++------ owrx/connection.py | 5 +++-- owrx/property/__init__.py | 28 +++++++++++++++++++++------ owrx/service/schedule.py | 12 ++++++++++-- owrx/source/__init__.py | 8 ++++---- test/property/test_property_filter.py | 12 +++++++++++- test/property/test_property_layer.py | 26 ++++++++++++++++++++++++- test/property/test_property_stack.py | 26 ++++++++++++++++++++++--- 8 files changed, 102 insertions(+), 25 deletions(-) diff --git a/owrx/config/dynamic.py b/owrx/config/dynamic.py index 97ecd44..9357e05 100644 --- a/owrx/config/dynamic.py +++ b/owrx/config/dynamic.py @@ -1,13 +1,11 @@ from owrx.config.core import CoreConfig from owrx.config.migration import Migrator -from owrx.property import PropertyLayer +from owrx.property import PropertyLayer, PropertyDeleted from owrx.jsons import Encoder import json class DynamicConfig(PropertyLayer): - _deleted = object() - def __init__(self): super().__init__() try: @@ -43,12 +41,12 @@ class DynamicConfig(PropertyLayer): file.write(jsonContent) def __delitem__(self, key): - self.__setitem__(key, DynamicConfig._deleted) + self.__setitem__(key, PropertyDeleted) def __contains__(self, item): if not super().__contains__(item): return False - if super().__getitem__(item) is DynamicConfig._deleted: + if super().__getitem__(item) is PropertyDeleted: return False return True @@ -58,7 +56,7 @@ class DynamicConfig(PropertyLayer): raise KeyError('Key "{key}" does not exist'.format(key=item)) def __dict__(self): - return {k: v for k, v in super().__dict__().items() if v is not DynamicConfig._deleted} + return {k: v for k, v in super().__dict__().items() if v is not PropertyDeleted} def keys(self): return [k for k in super().keys() if self.__contains__(k)] diff --git a/owrx/connection.py b/owrx/connection.py index d215b90..93ccdba 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -9,7 +9,7 @@ from owrx.version import openwebrx_version from owrx.bands import Bandplan from owrx.bookmarks import Bookmarks from owrx.map import Map -from owrx.property import PropertyStack +from owrx.property import PropertyStack, PropertyDeleted from owrx.modes import Modes, DigitalMode from owrx.config import Config from owrx.waterfall import WaterfallOptions @@ -176,7 +176,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): if changes is None: config = configProps.__dict__() else: - config = changes + # transform deletions into Nones + config = {k: v if v is not PropertyDeleted else None for k, v in changes.items()} if ( (changes is None or "start_freq" in changes or "center_freq" in changes) and "start_freq" in configProps diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py index 302ecc1..41b922c 100644 --- a/owrx/property/__init__.py +++ b/owrx/property/__init__.py @@ -10,6 +10,15 @@ class PropertyError(Exception): pass +class PropertyDeletion(object): + pass + + +# a special object that will be sent in events when a deletion occured +# it can also represent deletion of a key in internal storage, but should not be return from standard dict apis +PropertyDeleted = PropertyDeletion() + + class Subscription(object): def __init__(self, subscriptee, name, subscriber): self.subscriptee = subscriptee @@ -126,7 +135,8 @@ class PropertyLayer(PropertyManager): return {k: v for k, v in self.properties.items()} def __delitem__(self, key): - return self.properties.__delitem__(key) + self.properties.__delitem__(key) + self._fireCallbacks({key: PropertyDeleted}) def keys(self): return self.properties.keys() @@ -273,7 +283,7 @@ class PropertyStack(PropertyManager): if self[key] != pm[key]: changes[key] = self[key] else: - changes[key] = None + changes[key] = PropertyDeleted return changes def replaceLayer(self, priority: int, pm: PropertyManager): @@ -289,15 +299,21 @@ class PropertyStack(PropertyManager): def receiveEvent(self, layer, changes): changesToForward = {name: value for name, value in changes.items() if layer == self._getTopLayer(name)} - self._fireCallbacks(changesToForward) + # deletions need to be handled separately: only send them if deleted in all layers + deletionsToForward = { + name: value + for name, value in changes.items() + if value is PropertyDeleted and self._getTopLayer(name, False) is None + } + self._fireCallbacks({**changesToForward, **deletionsToForward}) - def _getTopLayer(self, item): + def _getTopLayer(self, item, fallback=True): layers = [la["props"] for la in sorted(self.layers, key=lambda l: l["priority"])] for m in layers: if item in m: return m - # return top layer by default - if layers: + # return top layer as fallback + if fallback and layers: return layers[0] def __getitem__(self, item): diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 53032ed..7aeb41d 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -213,7 +213,8 @@ class ServiceScheduler(SdrSourceEventClient): props = self.source.getProps() props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange) props.wireProperty("scheduler", self.parseSchedule) - self.parseSchedule() + # wireProperty calls parseSchedule with the initial value + # self.parseSchedule() def parseSchedule(self, *args): props = self.source.getProps() @@ -259,6 +260,12 @@ class ServiceScheduler(SdrSourceEventClient): if self.source.hasClients(SdrClientClass.USER): logger.debug("source has active users; not touching") return + + if self.schedule is None: + logger.debug("no active schedule. releasing source...") + self.source.removeClient(self) + return + logger.debug("source seems to be idle, selecting profile for background services") entry = self.schedule.getCurrentEntry() @@ -269,7 +276,8 @@ class ServiceScheduler(SdrSourceEventClient): nextEntry = self.schedule.getNextEntry() if nextEntry is not None: self.scheduleSelection(nextEntry.getNextActivation()) - logger.debug("no next entry available, scheduler standing by for external events.") + else: + logger.debug("no next entry available, scheduler standing by for external events.") return self.source.addClient(self) diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index 078441e..22f563b 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -314,10 +314,10 @@ class SdrSource(ABC): self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) def removeClient(self, c: SdrSourceEventClient): - try: - self.clients.remove(c) - except ValueError: - pass + if c not in self.clients: + return + + self.clients.remove(c) hasUsers = self.hasClients(SdrClientClass.USER) self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) diff --git a/test/property/test_property_filter.py b/test/property/test_property_filter.py index bdea738..82f5961 100644 --- a/test/property/test_property_filter.py +++ b/test/property/test_property_filter.py @@ -1,6 +1,6 @@ from unittest import TestCase from unittest.mock import Mock -from owrx.property import PropertyLayer, PropertyFilter +from owrx.property import PropertyLayer, PropertyFilter, PropertyDeleted class PropertyFilterTest(TestCase): @@ -70,3 +70,13 @@ class PropertyFilterTest(TestCase): with self.assertRaises(KeyError): pf["testkey"] = "new value" self.assertEqual(pm["testkey"], "old value") + + def testPropagatesDeletion(self): + pm = PropertyLayer(testkey="somevalue") + filter_mock = Mock() + filter_mock.apply.return_value = True + pf = PropertyFilter(pm, filter_mock) + mock = Mock() + pf.wire(mock.method) + del pf["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) diff --git a/test/property/test_property_layer.py b/test/property/test_property_layer.py index 2d57fd9..f07ae1b 100644 --- a/test/property/test_property_layer.py +++ b/test/property/test_property_layer.py @@ -1,4 +1,4 @@ -from owrx.property import PropertyLayer +from owrx.property import PropertyLayer, PropertyDeleted from unittest import TestCase from unittest.mock import Mock @@ -67,3 +67,27 @@ class PropertyLayerTest(TestCase): pm.wire(mock.method) pm["testkey"] = "testvalue" mock.method.assert_not_called() + + def testDeletionIsSent(self): + pm = PropertyLayer(testkey="somevalue") + mock = Mock() + pm.wireProperty("testkey", mock.method) + mock.method.reset_mock() + del pm["testkey"] + mock.method.assert_called_once_with(PropertyDeleted) + + def testDeletionInGeneralWiring(self): + pm = PropertyLayer(testkey="somevalue") + mock = Mock() + pm.wire(mock.method) + del pm["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + + def testNoDeletionEventWhenPropertyDoesntExist(self): + pm = PropertyLayer(otherkey="somevalue") + mock = Mock() + pm.wireProperty("testkey", mock.method) + mock.method.reset_mock() + with self.assertRaises(KeyError): + del pm["testkey"] + mock.method.assert_not_called() diff --git a/test/property/test_property_stack.py b/test/property/test_property_stack.py index 8eb5382..0efeac4 100644 --- a/test/property/test_property_stack.py +++ b/test/property/test_property_stack.py @@ -1,6 +1,6 @@ from unittest import TestCase from unittest.mock import Mock -from owrx.property import PropertyLayer, PropertyStack +from owrx.property import PropertyLayer, PropertyStack, PropertyDeleted class PropertyStackTest(TestCase): @@ -135,7 +135,7 @@ class PropertyStackTest(TestCase): mock.method.assert_called_once_with("unique value") mock.reset_mock() stack.removeLayer(high_layer) - mock.method.assert_called_once_with(None) + mock.method.assert_called_once_with(PropertyDeleted) def testReplaceLayer(self): first_layer = PropertyLayer() @@ -162,7 +162,7 @@ class PropertyStackTest(TestCase): mock = Mock() stack.wire(mock.method) stack.removeLayer(layer) - mock.method.assert_called_once_with({"testkey": None}) + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) mock.reset_mock() layer["testkey"] = "after" @@ -195,3 +195,23 @@ class PropertyStackTest(TestCase): om.addLayer(0, high_pm) om["testkey"] = "new value" self.assertEqual(low_pm["testkey"], "new value") + + def testDeletionEvent(self): + ps = PropertyStack() + pm = PropertyLayer(testkey="testvalue") + ps.addLayer(0, pm) + mock = Mock() + ps.wire(mock.method) + del ps["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + + def testDeletionWithSecondLayer(self): + ps = PropertyStack() + low_pm = PropertyLayer(testkey="testvalue") + high_pm = PropertyLayer() + ps.addLayer(0, high_pm) + ps.addLayer(1, low_pm) + mock = Mock() + ps.wire(mock.method) + del low_pm["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted})