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.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)]

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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})

View File

@ -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()

View File

@ -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})