rewrite property engine

Property class is gone; logic is now done with Layers, Stack and Filter
This commit is contained in:
Jakob Ketterl 2020-03-23 23:56:05 +01:00
parent 7562dc8ecb
commit c83d8580ba
12 changed files with 298 additions and 176 deletions

View File

@ -1,4 +1,4 @@
from owrx.property import PropertyManager from owrx.property import PropertyLayer
import importlib.util import importlib.util
import os import os
import logging import logging
@ -24,7 +24,7 @@ class Config:
@staticmethod @staticmethod
def _loadConfig(): def _loadConfig():
pm = PropertyManager() pm = PropertyLayer()
for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]: for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
try: try:
spec = importlib.util.spec_from_file_location("config_webrx", file) spec = importlib.util.spec_from_file_location("config_webrx", file)
@ -55,7 +55,7 @@ class Config:
return [e for e in errors if e is not None] return [e for e in errors if e is not None]
@staticmethod @staticmethod
def checkTempDirectory(pm: PropertyManager): def checkTempDirectory(pm: PropertyLayer):
key = "temporary_directory" key = "temporary_directory"
if key not in pm or pm[key] is None: if key not in pm or pm[key] is None:
return ConfigError(key, "temporary directory is not set") return ConfigError(key, "temporary directory is not set")

View File

@ -10,6 +10,7 @@ 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.locator import Locator from owrx.locator import Locator
from owrx.property import PropertyStack
from multiprocessing import Queue from multiprocessing import Queue
from queue import Full from queue import Full
import json import json
@ -195,14 +196,14 @@ class OpenWebRxReceiverClient(Client):
# send initial config # send initial config
self.setDspProperties(self.connectionProperties) self.setDspProperties(self.connectionProperties)
configProps = ( stack = PropertyStack()
self.sdr.getProps() stack.addLayer(0, self.sdr.getProps())
.collect(*OpenWebRxReceiverClient.config_keys) stack.addLayer(1, Config.get())
.defaults(Config.get()) configProps = stack.collect(*OpenWebRxReceiverClient.config_keys)
)
def sendConfig(key, value): def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) #config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
config = configProps.__dict__()
# TODO mathematical properties? hmmmm # TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack to support multiple sdrs # TODO this is a hack to support multiple sdrs
@ -253,11 +254,10 @@ class OpenWebRxReceiverClient(Client):
if not keys: if not keys:
return return
# only the keys in the protected property manager can be overridden from the web # only the keys in the protected property manager can be overridden from the web
protected = ( stack = PropertyStack()
self.sdr.getProps() stack.addLayer(0, self.sdr.getProps())
.collect(*keys) stack.addLayer(1, config)
.defaults(config) protected = stack.collect(*keys)
)
for key, value in params.items(): for key, value in params.items():
protected[key] = value protected[key] = value

View File

@ -4,6 +4,7 @@ from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser from owrx.pocsag import PocsagParser
from owrx.source import SdrSource from owrx.source import SdrSource
from owrx.property import PropertyStack
from csdr import csdr from csdr import csdr
import threading import threading
@ -23,9 +24,10 @@ class DspManager(csdr.output):
"pocsag_demod": PocsagParser(self.handler), "pocsag_demod": PocsagParser(self.handler),
} }
self.localProps = ( stack = PropertyStack()
self.sdrSource.getProps() stack.addLayer(0, self.sdrSource.getProps())
.collect( stack.addLayer(1, Config.get())
self.localProps = stack.collect(
"audio_compression", "audio_compression",
"fft_compression", "fft_compression",
"digimodes_fft_size", "digimodes_fft_size",
@ -38,8 +40,14 @@ class DspManager(csdr.output):
"dmr_filter", "dmr_filter",
"temporary_directory", "temporary_directory",
"center_freq", "center_freq",
)
.defaults(Config.get()) # TODO: following properties are set from the client
"output_rate",
"squelch_level",
"secondary_mod",
"low_cut",
"high_cut",
"offset_freq",
) )
self.dsp = csdr.dsp(self) self.dsp = csdr.dsp(self)
@ -61,19 +69,19 @@ class DspManager(csdr.output):
parser.setDialFrequency(freq) parser.setDialFrequency(freq)
self.subscriptions = [ self.subscriptions = [
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), self.localProps.wireProperty("audio_compression", self.dsp.set_audio_compression),
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression), self.localProps.wireProperty("fft_compression", self.dsp.set_fft_compression),
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size), self.localProps.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate), self.localProps.wireProperty("samp_rate", self.dsp.set_samp_rate),
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate), self.localProps.wireProperty("output_rate", self.dsp.set_output_rate),
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq), self.localProps.wireProperty("offset_freq", self.dsp.set_offset_freq),
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level), self.localProps.wireProperty("squelch_level", self.dsp.set_squelch_level),
self.localProps.getProperty("low_cut").wire(set_low_cut), self.localProps.wireProperty("low_cut", set_low_cut),
self.localProps.getProperty("high_cut").wire(set_high_cut), self.localProps.wireProperty("high_cut", set_high_cut),
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator), self.localProps.wireProperty("mod", self.dsp.set_demodulator),
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality), self.localProps.wireProperty("digital_voice_unvoiced_quality", self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter), self.localProps.wireProperty("dmr_filter", self.dsp.set_dmr_filter),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory), self.localProps.wireProperty("temporary_directory", self.dsp.set_temporary_directory),
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq), self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
] ]
@ -99,8 +107,8 @@ class DspManager(csdr.output):
) )
self.subscriptions += [ self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod), self.localProps.collect("secondary_mod").wire(set_secondary_mod),
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq), self.localProps.collect("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
] ]
self.sdrSource.addClient(self) self.sdrSource.addClient(self)
@ -134,7 +142,7 @@ class DspManager(csdr.output):
self.subscriptions = [] self.subscriptions = []
def setProperty(self, prop, value): def setProperty(self, prop, value):
self.localProps.getProperty(prop).setValue(value) self.localProps[prop] = value
def getClientClass(self): def getClientClass(self):
return SdrSource.CLIENT_USER return SdrSource.CLIENT_USER

View File

@ -2,6 +2,7 @@ from owrx.config import Config
from csdr import csdr from csdr import csdr
import threading import threading
from owrx.source import SdrSource from owrx.source import SdrSource
from owrx.property import PropertyStack
import logging import logging
@ -13,7 +14,10 @@ class SpectrumThread(csdr.output):
self.sdrSource = sdrSource self.sdrSource = sdrSource
super().__init__() super().__init__()
self.props = props = self.sdrSource.props.collect( stack = PropertyStack()
stack.addLayer(0, self.sdrSource.props)
stack.addLayer(1, Config.get())
self.props = props = stack.collect(
"samp_rate", "samp_rate",
"fft_size", "fft_size",
"fft_fps", "fft_fps",
@ -23,7 +27,7 @@ class SpectrumThread(csdr.output):
"csdr_print_bufsizes", "csdr_print_bufsizes",
"csdr_through", "csdr_through",
"temporary_directory", "temporary_directory",
).defaults(Config.get()) )
self.dsp = dsp = csdr.dsp(self) self.dsp = dsp = csdr.dsp(self)
dsp.nc_port = self.sdrSource.getPort() dsp.nc_port = self.sdrSource.getPort()
@ -42,11 +46,11 @@ class SpectrumThread(csdr.output):
) )
self.subscriptions = [ self.subscriptions = [
props.getProperty("samp_rate").wire(dsp.set_samp_rate), props.wireProperty("samp_rate", dsp.set_samp_rate),
props.getProperty("fft_size").wire(dsp.set_fft_size), props.wireProperty("fft_size", dsp.set_fft_size),
props.getProperty("fft_fps").wire(dsp.set_fft_fps), props.wireProperty("fft_fps", dsp.set_fft_fps),
props.getProperty("fft_compression").wire(dsp.set_fft_compression), props.wireProperty("fft_compression", dsp.set_fft_compression),
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory), props.wireProperty("temporary_directory", dsp.set_temporary_directory),
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages), props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages),
] ]

View File

@ -1,13 +1,18 @@
from abc import ABC, abstractmethod
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Subscription(object): class Subscription(object):
def __init__(self, subscriptee, subscriber): def __init__(self, subscriptee, name, subscriber):
self.subscriptee = subscriptee self.subscriptee = subscriptee
self.name = name
self.subscriber = subscriber self.subscriber = subscriber
def getName(self):
return self.name
def call(self, *args, **kwargs): def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs) self.subscriber(*args, **kwargs)
@ -15,30 +20,39 @@ class Subscription(object):
self.subscriptee.unwire(self) self.subscriptee.unwire(self)
class Property(object): class PropertyManager(ABC):
def __init__(self, value=None): def __init__(self):
self.value = value
self.subscribers = [] self.subscribers = []
def getValue(self): @abstractmethod
return self.value def __getitem__(self, item):
pass
def setValue(self, value): @abstractmethod
if self.value == value: def __setitem__(self, key, value):
return self pass
self.value = value
for c in self.subscribers: @abstractmethod
try: def __contains__(self, item):
c.call(self.value) pass
except Exception as e:
logger.exception(e) @abstractmethod
return self def __dict__(self):
pass
def collect(self, *props):
return PropertyFilter(self, *props)
def wire(self, callback): def wire(self, callback):
sub = Subscription(self, callback) sub = Subscription(self, None, callback)
self.subscribers.append(sub) self.subscribers.append(sub)
if self.value is not None: return sub
sub.call(self.value)
def wireProperty(self, name, callback):
sub = Subscription(self, name, callback)
self.subscribers.append(sub)
if name in self:
sub.call(self[name])
return sub return sub
def unwire(self, sub): def unwire(self, sub):
@ -49,80 +63,79 @@ class Property(object):
pass pass
return self return self
def _fireCallbacks(self, name, value):
class PropertyManager(object):
def collect(self, *props):
return PropertyManager(
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
)
def __init__(self, properties=None):
self.properties = {}
self.subscribers = []
if properties is not None:
for (name, prop) in properties.items():
self.add(name, prop)
def add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers: for c in self.subscribers:
try: try:
if c.getName() is None:
c.call(name, value) c.call(name, value)
elif c.getName() == name:
c.call(value)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
prop.wire(fireCallbacks)
class PropertyLayer(PropertyManager):
def __init__(self, properties=None):
super().__init__()
self.properties = {}
if properties is not None:
for (name, prop) in properties.items():
self._add(name, prop)
def _add(self, name, prop):
self.properties[name] = prop
self._fireCallbacks(name, prop.getValue())
return self return self
def __contains__(self, name): def __contains__(self, name):
return self.hasProperty(name)
def __getitem__(self, name):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def __dict__(self):
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties return name in self.properties
def getProperty(self, name): def __getitem__(self, name):
if not self.hasProperty(name):
self.add(name, Property())
return self.properties[name] return self.properties[name]
def getPropertyValue(self, name): def __setitem__(self, name, value):
return self.getProperty(name).getValue() logger.debug("property change: %s => %s", name, value)
self.properties[name] = value
self._fireCallbacks(name, value)
def wire(self, callback): def __dict__(self):
sub = Subscription(self, callback) return {k: v for k, v in self.properties.items()}
self.subscribers.append(sub)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
class PropertyLayers(object): class PropertyFilter(PropertyManager):
def __init__(self, pm: PropertyManager, *props: str):
super().__init__()
self.pm = pm
self.props = props
self.pm.wire(self.receiveEvent)
def receiveEvent(self, name, value):
if name not in self.props:
return
self._fireCallbacks(name, value)
def __getitem__(self, item):
if item not in self.props:
raise KeyError(item)
return self.pm.__getitem__(item)
def __setitem__(self, key, value):
if key not in self.props:
raise KeyError(key)
return self.pm.__setitem__(key, value)
def __contains__(self, item):
if item not in self.props:
return False
return self.pm.__contains__(item)
def __dict__(self):
return {k: v for k, v in self.pm.__dict__().items() if k in self.props}
class PropertyStack(PropertyManager):
def __init__(self): def __init__(self):
super().__init__()
self.layers = [] self.layers = []
def addLayer(self, priority: int, pm: PropertyManager): def addLayer(self, priority: int, pm: PropertyManager):
@ -136,8 +149,26 @@ class PropertyLayers(object):
if layer["props"] == pm: if layer["props"] == pm:
self.layers.remove(layer) self.layers.remove(layer)
def __getitem__(self, item): def _getLayer(self, item):
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[item] return m
# return top layer by default
return layers[0]
def __getitem__(self, item):
layer = self._getLayer(item)
return layer.__getitem__(item)
def __setitem__(self, key, value):
layer = self._getLayer(key)
return layer.__setitem__(key, value)
def __contains__(self, item):
layer = self._getLayer(item)
return layer.__contains__(item)
def __dict__(self):
keys = [key for l in self.layers for key in l["props"].__dict__().keys()]
return {k: self.__getitem__(k) for k in keys}

View File

@ -1,5 +1,5 @@
from owrx.config import Config from owrx.config import Config
from owrx.property import PropertyManager from owrx.property import PropertyLayer
from owrx.feature import FeatureDetector, UnknownFeatureException from owrx.feature import FeatureDetector, UnknownFeatureException
import logging import logging
@ -19,7 +19,7 @@ class SdrService(object):
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
def loadIntoPropertyManager(dict: dict): def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager() propertyManager = PropertyLayer()
for (name, value) in dict.items(): for (name, value) in dict.items():
propertyManager[name] = value propertyManager[name] = value
return propertyManager return propertyManager

View File

@ -8,7 +8,7 @@ from owrx.aprs import AprsParser
from owrx.config import Config from owrx.config import Config
from owrx.source.resampler import Resampler from owrx.source.resampler import Resampler
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from owrx.property import PropertyManager from owrx.property import PropertyLayer
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from .schedule import ServiceScheduler from .schedule import ServiceScheduler
@ -164,7 +164,7 @@ class ServiceHandler(object):
cf = (min + max) / 2 cf = (min + max) / 2
bw = max - min bw = max - min
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw)) logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
resampler_props = PropertyManager() resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf resampler_props["center_freq"] = cf
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths # TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
resampler_props["samp_rate"] = bw + 24000 resampler_props["samp_rate"] = bw + 24000

View File

@ -9,6 +9,7 @@ import signal
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from owrx.command import CommandMapper from owrx.command import CommandMapper
from owrx.socket import getAvailablePort from owrx.socket import getAvailablePort
from owrx.property import PropertyStack
import logging import logging
@ -35,7 +36,10 @@ class SdrSource(ABC):
self.props = props self.props = props
self.profile_id = None self.profile_id = None
self.activateProfile() self.activateProfile()
self.rtlProps = self.props.collect(*self.getEventNames()).defaults(Config.get()) stack = PropertyStack()
stack.addLayer(0, self.props)
stack.addLayer(1, Config.get())
self.rtlProps = stack.collect(*self.getEventNames())
self.wireEvents() self.wireEvents()
self.commandMapper = None self.commandMapper = None

View File

@ -1,28 +0,0 @@
import unittest
from unittest.mock import Mock
from owrx.property import Property
class PropertyTest(unittest.TestCase):
def testValue(self):
prop = Property("testvalue")
self.assertEqual(prop.getValue(), "testvalue")
def testChangeValue(self):
prop = Property("before")
prop.setValue("after")
self.assertEqual(prop.getValue(), "after")
def testInitialValueOnCallback(self):
prop = Property("before")
m = Mock()
prop.wire(m.method)
m.method.assert_called_once_with("before")
def testChangedValueOnCallback(self):
prop = Property("before")
m = Mock()
prop.wire(m.method)
m.reset_mock()
prop.setValue("after")
m.method.assert_called_with("after")

View File

@ -0,0 +1,51 @@
from unittest import TestCase
from unittest.mock import Mock
from owrx.property import PropertyLayer, PropertyFilter
class PropertyFilterTest(TestCase):
def testPassesProperty(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "testkey")
self.assertEqual(pf["testkey"], "testvalue")
def testMissesPropert(self):
pm = PropertyLayer()
pm["testkey"] = "testvalue"
pf = PropertyFilter(pm, "other_key")
self.assertFalse("testkey" in pf)
with self.assertRaises(KeyError):
x = pf["testkey"]
def testForwardsEvent(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
mock = Mock()
pf.wire(mock.method)
pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testkey", "testvalue")
def testForwardsPropertyEvent(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
mock = Mock()
pf.wireProperty("testkey", mock.method)
pm["testkey"] = "testvalue"
mock.method.assert_called_once_with("testvalue")
def testForwardsWrite(self):
pm = PropertyLayer()
pf = PropertyFilter(pm, "testkey")
pf["testkey"] = "testvalue"
self.assertTrue("testkey" in pm)
self.assertEqual(pm["testkey"], "testvalue")
def testOverwrite(self):
pm = PropertyLayer()
pm["testkey"] = "old value"
pf = PropertyFilter(pm, "testkey")
pf["testkey"] = "new value"
self.assertEqual(pm["testkey"], "new value")
self.assertEqual(pf["testkey"], "new value")

View File

@ -0,0 +1,52 @@
from owrx.property import PropertyLayer
from unittest import TestCase
from unittest.mock import Mock
class PropertyLayerTest(TestCase):
def testKeyIsset(self):
pm = PropertyLayer()
self.assertFalse("some_key" in pm)
def testKeyError(self):
pm = PropertyLayer()
with self.assertRaises(KeyError):
x = pm["some_key"]
def testSubscription(self):
pm = PropertyLayer()
pm["testkey"] = "before"
mock = Mock()
pm.wire(mock.method)
pm["testkey"] = "after"
mock.method.assert_called_once_with("testkey", "after")
def testUnsubscribe(self):
pm = PropertyLayer()
pm["testkey"] = "before"
mock = Mock()
sub = pm.wire(mock.method)
pm["testkey"] = "between"
mock.method.assert_called_once_with("testkey", "between")
mock.reset_mock()
pm.unwire(sub)
pm["testkey"] = "after"
mock.method.assert_not_called()
def testContains(self):
pm = PropertyLayer()
pm["testkey"] = "value"
self.assertTrue("testkey" in pm)
def testDoesNotContain(self):
pm = PropertyLayer()
self.assertFalse("testkey" in pm)
def testSubscribeBeforeSet(self):
pm = PropertyLayer()
mock = Mock()
pm.wireProperty("testkey", mock.method)
mock.method.assert_not_called()
pm["testkey"] = "newvalue"
mock.method.assert_called_once_with("newvalue")

View File

@ -1,20 +1,20 @@
from unittest import TestCase from unittest import TestCase
from unittest.mock import Mock from unittest.mock import Mock
from owrx.property import PropertyManager, PropertyLayers from owrx.property import PropertyLayer, PropertyStack
class TestPropertyLayers(TestCase): class PropertyStackTest(TestCase):
def testLayer(self): def testLayer(self):
om = PropertyLayers() om = PropertyStack()
pm = PropertyManager() pm = PropertyLayer()
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"
om.addLayer(1, pm) om.addLayer(1, pm)
self.assertEqual(om["testkey"], "testvalue") self.assertEqual(om["testkey"], "testvalue")
def testHighPriority(self): def testHighPriority(self):
om = PropertyLayers() om = PropertyStack()
low_pm = PropertyManager() low_pm = PropertyLayer()
high_pm = PropertyManager() high_pm = PropertyLayer()
low_pm["testkey"] = "low value" low_pm["testkey"] = "low value"
high_pm["testkey"] = "high value" high_pm["testkey"] = "high value"
om.addLayer(1, low_pm) om.addLayer(1, low_pm)
@ -22,18 +22,18 @@ class TestPropertyLayers(TestCase):
self.assertEqual(om["testkey"], "high value") self.assertEqual(om["testkey"], "high value")
def testPriorityFallback(self): def testPriorityFallback(self):
om = PropertyLayers() om = PropertyStack()
low_pm = PropertyManager() low_pm = PropertyLayer()
high_pm = PropertyManager() high_pm = PropertyLayer()
low_pm["testkey"] = "low value" low_pm["testkey"] = "low value"
om.addLayer(1, low_pm) om.addLayer(1, low_pm)
om.addLayer(0, high_pm) om.addLayer(0, high_pm)
self.assertEqual(om["testkey"], "low value") self.assertEqual(om["testkey"], "low value")
def testLayerRemoval(self): def testLayerRemoval(self):
om = PropertyLayers() om = PropertyStack()
low_pm = PropertyManager() low_pm = PropertyLayer()
high_pm = PropertyManager() high_pm = PropertyLayer()
low_pm["testkey"] = "low value" low_pm["testkey"] = "low value"
high_pm["testkey"] = "high value" high_pm["testkey"] = "high value"
om.addLayer(1, low_pm) om.addLayer(1, low_pm)