implement property deletion handling; activate scheduler deletion

This commit is contained in:
Jakob Ketterl 2021-02-26 01:12:03 +01:00
parent 91c4d6f568
commit 412e0a51c7
8 changed files with 102 additions and 25 deletions

View File

@ -1,13 +1,11 @@
from owrx.config.core import CoreConfig from owrx.config.core import CoreConfig
from owrx.config.migration import Migrator from owrx.config.migration import Migrator
from owrx.property import PropertyLayer from owrx.property import PropertyLayer, PropertyDeleted
from owrx.jsons import Encoder from owrx.jsons import Encoder
import json import json
class DynamicConfig(PropertyLayer): class DynamicConfig(PropertyLayer):
_deleted = object()
def __init__(self): def __init__(self):
super().__init__() super().__init__()
try: try:
@ -43,12 +41,12 @@ class DynamicConfig(PropertyLayer):
file.write(jsonContent) file.write(jsonContent)
def __delitem__(self, key): def __delitem__(self, key):
self.__setitem__(key, DynamicConfig._deleted) self.__setitem__(key, PropertyDeleted)
def __contains__(self, item): def __contains__(self, item):
if not super().__contains__(item): if not super().__contains__(item):
return False return False
if super().__getitem__(item) is DynamicConfig._deleted: if super().__getitem__(item) is PropertyDeleted:
return False return False
return True return True
@ -58,7 +56,7 @@ class DynamicConfig(PropertyLayer):
raise KeyError('Key "{key}" does not exist'.format(key=item)) raise KeyError('Key "{key}" does not exist'.format(key=item))
def __dict__(self): 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): def keys(self):
return [k for k in super().keys() if self.__contains__(k)] return [k for k in super().keys() if self.__contains__(k)]

View File

@ -9,7 +9,7 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks from owrx.bookmarks import Bookmarks
from owrx.map import Map from owrx.map import Map
from owrx.property import PropertyStack from owrx.property import PropertyStack, PropertyDeleted
from owrx.modes import Modes, DigitalMode from owrx.modes import Modes, DigitalMode
from owrx.config import Config from owrx.config import Config
from owrx.waterfall import WaterfallOptions from owrx.waterfall import WaterfallOptions
@ -176,7 +176,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
if changes is None: if changes is None:
config = configProps.__dict__() config = configProps.__dict__()
else: else:
config = changes # transform deletions into Nones
config = {k: v if v is not PropertyDeleted else None for k, v in changes.items()}
if ( if (
(changes is None or "start_freq" in changes or "center_freq" in changes) (changes is None or "start_freq" in changes or "center_freq" in changes)
and "start_freq" in configProps and "start_freq" in configProps

View File

@ -10,6 +10,15 @@ class PropertyError(Exception):
pass 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): class Subscription(object):
def __init__(self, subscriptee, name, subscriber): def __init__(self, subscriptee, name, subscriber):
self.subscriptee = subscriptee self.subscriptee = subscriptee
@ -126,7 +135,8 @@ class PropertyLayer(PropertyManager):
return {k: v for k, v in self.properties.items()} return {k: v for k, v in self.properties.items()}
def __delitem__(self, key): def __delitem__(self, key):
return self.properties.__delitem__(key) self.properties.__delitem__(key)
self._fireCallbacks({key: PropertyDeleted})
def keys(self): def keys(self):
return self.properties.keys() return self.properties.keys()
@ -273,7 +283,7 @@ class PropertyStack(PropertyManager):
if self[key] != pm[key]: if self[key] != pm[key]:
changes[key] = self[key] changes[key] = self[key]
else: else:
changes[key] = None changes[key] = PropertyDeleted
return changes return changes
def replaceLayer(self, priority: int, pm: PropertyManager): def replaceLayer(self, priority: int, pm: PropertyManager):
@ -289,15 +299,21 @@ class PropertyStack(PropertyManager):
def receiveEvent(self, layer, changes): def receiveEvent(self, layer, changes):
changesToForward = {name: value for name, value in changes.items() if layer == self._getTopLayer(name)} 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"])] layers = [la["props"] for la in sorted(self.layers, key=lambda l: l["priority"])]
for m in layers: for m in layers:
if item in m: if item in m:
return m return m
# return top layer by default # return top layer as fallback
if layers: if fallback and layers:
return layers[0] return layers[0]
def __getitem__(self, item): def __getitem__(self, item):

View File

@ -213,7 +213,8 @@ class ServiceScheduler(SdrSourceEventClient):
props = self.source.getProps() props = self.source.getProps()
props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange) props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange)
props.wireProperty("scheduler", self.parseSchedule) props.wireProperty("scheduler", self.parseSchedule)
self.parseSchedule() # wireProperty calls parseSchedule with the initial value
# self.parseSchedule()
def parseSchedule(self, *args): def parseSchedule(self, *args):
props = self.source.getProps() props = self.source.getProps()
@ -259,6 +260,12 @@ class ServiceScheduler(SdrSourceEventClient):
if self.source.hasClients(SdrClientClass.USER): if self.source.hasClients(SdrClientClass.USER):
logger.debug("source has active users; not touching") logger.debug("source has active users; not touching")
return 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") logger.debug("source seems to be idle, selecting profile for background services")
entry = self.schedule.getCurrentEntry() entry = self.schedule.getCurrentEntry()
@ -269,6 +276,7 @@ class ServiceScheduler(SdrSourceEventClient):
nextEntry = self.schedule.getNextEntry() nextEntry = self.schedule.getNextEntry()
if nextEntry is not None: if nextEntry is not None:
self.scheduleSelection(nextEntry.getNextActivation()) self.scheduleSelection(nextEntry.getNextActivation())
else:
logger.debug("no next entry available, scheduler standing by for external events.") logger.debug("no next entry available, scheduler standing by for external events.")
return return

View File

@ -314,10 +314,10 @@ class SdrSource(ABC):
self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE)
def removeClient(self, c: SdrSourceEventClient): def removeClient(self, c: SdrSourceEventClient):
try: if c not in self.clients:
return
self.clients.remove(c) self.clients.remove(c)
except ValueError:
pass
hasUsers = self.hasClients(SdrClientClass.USER) hasUsers = self.hasClients(SdrClientClass.USER)
self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE)

View File

@ -1,6 +1,6 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import Mock from unittest.mock import Mock
from owrx.property import PropertyLayer, PropertyFilter from owrx.property import PropertyLayer, PropertyFilter, PropertyDeleted
class PropertyFilterTest(TestCase): class PropertyFilterTest(TestCase):
@ -70,3 +70,13 @@ class PropertyFilterTest(TestCase):
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
pf["testkey"] = "new value" pf["testkey"] = "new value"
self.assertEqual(pm["testkey"], "old 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})

View File

@ -1,4 +1,4 @@
from owrx.property import PropertyLayer from owrx.property import PropertyLayer, PropertyDeleted
from unittest import TestCase from unittest import TestCase
from unittest.mock import Mock from unittest.mock import Mock
@ -67,3 +67,27 @@ class PropertyLayerTest(TestCase):
pm.wire(mock.method) pm.wire(mock.method)
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"
mock.method.assert_not_called() 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()

View File

@ -1,6 +1,6 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import Mock from unittest.mock import Mock
from owrx.property import PropertyLayer, PropertyStack from owrx.property import PropertyLayer, PropertyStack, PropertyDeleted
class PropertyStackTest(TestCase): class PropertyStackTest(TestCase):
@ -135,7 +135,7 @@ class PropertyStackTest(TestCase):
mock.method.assert_called_once_with("unique value") mock.method.assert_called_once_with("unique value")
mock.reset_mock() mock.reset_mock()
stack.removeLayer(high_layer) stack.removeLayer(high_layer)
mock.method.assert_called_once_with(None) mock.method.assert_called_once_with(PropertyDeleted)
def testReplaceLayer(self): def testReplaceLayer(self):
first_layer = PropertyLayer() first_layer = PropertyLayer()
@ -162,7 +162,7 @@ class PropertyStackTest(TestCase):
mock = Mock() mock = Mock()
stack.wire(mock.method) stack.wire(mock.method)
stack.removeLayer(layer) stack.removeLayer(layer)
mock.method.assert_called_once_with({"testkey": None}) mock.method.assert_called_once_with({"testkey": PropertyDeleted})
mock.reset_mock() mock.reset_mock()
layer["testkey"] = "after" layer["testkey"] = "after"
@ -195,3 +195,23 @@ class PropertyStackTest(TestCase):
om.addLayer(0, high_pm) om.addLayer(0, high_pm)
om["testkey"] = "new value" om["testkey"] = "new value"
self.assertEqual(low_pm["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})