implement property deletion handling; activate scheduler deletion
This commit is contained in:
parent
91c4d6f568
commit
412e0a51c7
@ -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)]
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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})
|
||||||
|
@ -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()
|
||||||
|
@ -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})
|
||||||
|
Loading…
Reference in New Issue
Block a user