diff --git a/src/mktl/config.py b/src/mktl/config.py index cab1c59..c66b8fa 100644 --- a/src/mktl/config.py +++ b/src/mktl/config.py @@ -200,7 +200,7 @@ def from_format_enumerated(self, key, value): break if unformatted is None: - raise KeyError('invalid enumerator: ' + repr(value)) + raise KeyError('invalid enumeration value: ' + repr(value)) return unformatted @@ -682,12 +682,18 @@ def to_format_enumerated(self, key, value): # {"0": "No", "1": "Yes", "2": "Unknown"} + if value is None: + return '' + + if isinstance(value, bool): + value = int(value) + value = str(value) try: formatted = enumerators[value] except KeyError: - formatted = value + raise KeyError('invalid enumerator: ' + repr(value)) return formatted @@ -711,6 +717,7 @@ def to_format_mask(self, key, value): value = int(value) formatted = list() + verified = 0 for bit,name in enumerators.items(): if bit == 'None': @@ -720,6 +727,10 @@ def to_format_mask(self, key, value): bit_value = 1 << bit if value & bit_value: formatted.append(name) + verified += bit_value + + if verified != value: + raise KeyError('value contains unknown active bits: ' + str(value)) if len(formatted) == 0: try: @@ -972,9 +983,8 @@ def update(self, block, save=True): del items[key] items[lower] = item - # Allow for the possibility that a boolean item does not include - # enumerators in its description. This check is only necessary - # for authoritative blocks. + # Normalize the formatting of enumerators for any relevant items. + # This check is only necessary for authoritative blocks. if uuid == self.authoritative_uuid: for key in items.keys(): @@ -985,24 +995,50 @@ def update(self, block, save=True): except KeyError: continue - if type == 'boolean': + # First pass: make sure the enumerators are in with + # strings as keys instead of integers. + + if type == 'boolean' or type == 'enumerated' or type == 'mask': + additions = dict() + deletions = list() + try: enumerators = item_config['enumerators'] except KeyError: enumerators = dict() item_config['enumerators'] = enumerators + for enumerator in enumerators.keys(): + # This would be a nice place to handle the enumerator + # being None for a mask, but if that were the case an + # exception would already be thrown when trying to + # convert the configuration block to JSON. + + if isinstance(enumerator, int): + additions[str(enumerator)] = enumerators[enumerator] + deletions.append(enumerator) + + enumerators.update(additions) + + for deletion in deletions: + del enumerators[deletion] + + # Second pass: fill in default boolean values if they + # are not specified. + + if type == 'boolean': try: enumerators['0'] - except: + except KeyError: enumerators['0'] = 'False' try: enumerators['1'] - except: + except KeyError: enumerators['1'] = 'True' + # It's possible the contents of the local authoritative block changed. # Update the hash and configuration timestamp if that is the case. diff --git a/src/mktl/item.py b/src/mktl/item.py index 2b4c0d9..c81b59e 100644 --- a/src/mktl/item.py +++ b/src/mktl/item.py @@ -833,11 +833,7 @@ def to_format(self, value): This is the inverse of :func:`from_format`. """ - try: - formatted = self.store.config.to_format(self.key, value) - except: - formatted = str(self.value) - + formatted = self.store.config.to_format(self.key, value) return formatted diff --git a/src/mktl/store.py b/src/mktl/store.py index ac1a362..25e4876 100644 --- a/src/mktl/store.py +++ b/src/mktl/store.py @@ -30,11 +30,15 @@ def __init__(self, name): def __contains__(self, key): if isinstance(key, Item): - key = key.key + item = key + key = item.key + + my_item = self._items[key] + return item is my_item + else: key = key.lower() - - return key in self._items + return key in self._items def __delitem__(self, key): @@ -42,7 +46,10 @@ def __delitem__(self, key): def __getitem__(self, key): - key = key.lower() + if isinstance(key, Item): + key = key.key + else: + key = key.lower() try: item = self._items[key] @@ -60,11 +67,10 @@ def __getitem__(self, key): if item is None: try: item = Item(self, key) - except: + finally: self._items_lock.release() - raise - - self._items_lock.release() + else: + self._items_lock.release() # The Item assigns itself to our self._items dictionary as an early # step in its initialization process, there is no need to manipulate @@ -87,7 +93,7 @@ def __len__(self): def __repr__(self): - return 'store.Store: ' + repr(self._items) + return "mktl.Store(%s): %s" % (self.name, repr(self._items)) def __setitem__(self, name, value): diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b1e90e1 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,29 @@ +The tests here are expecting to be invoked via `pytest`. This will inspect +all test_*.py files in the local directory, including any subdirectories, +and invoke all defined tests discovered in this fashion. The simplest +invocation requires no arguments:: + + pytest + +If you only want to specify a subset of tests to run, you can do so; one +straightforward path is to select a file containing tests of interest:: + + pytest ./test_items.py + +pytest has a long list of ways it can be invoked. One report that may be +of particular interest is test coverage; the simplest invocation with a +summary of coverage would be:: + + pytest --cov=mktl + +You can also request a complete breakdown listing which sections of +code are not covered:: + + pytest --cov=mktl --cov-report term-missing + +The other `--cov-report` options, such as the HTML-formatted report, can +provide additional context beyond just the line numbers. It's worth remembering +that 100% test coverage doesn't mean all the corner cases have been exercised-- +it's important that the tests cover as many cases as possible, which generally +means any given line of code will be exercised many times, in many different +ways, depending on the inputs provided. diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py new file mode 100644 index 0000000..3e9abec --- /dev/null +++ b/tests/protocol/test_payload.py @@ -0,0 +1,82 @@ +import mktl +import pytest +import time + +def test_basics(): + + start = time.time() + + for test_value in (44, True, None, 35.5, (1,2,3), {1: 'one'}, 'string'): + payload = mktl.protocol.message.Payload(test_value) + assert payload.value is test_value + assert payload.time > start + assert payload.bulk == None + assert payload.dtype == None + assert payload.error == None + assert payload.refresh == None + assert payload.shape == None + ## Pull request is pending + ##assert payload.reply == True + payload.encapsulate() + + +def test_encapsulate(): + + test_value = 44 + for test_value in (44, True, None, 35.5, [1,2,3], 'string'): + payload = mktl.protocol.message.Payload(test_value) + + encapsulated = payload.encapsulate() + assert isinstance(encapsulated, bytes) + + decoded = mktl.json.loads(encapsulated) + assert isinstance(decoded, dict) + + assert 'time' in decoded + assert 'value' in decoded + assert decoded['value'] == test_value + + + bad_payload = mktl.protocol.message.Payload({None: 'none'}) + + with pytest.raises(TypeError): + bad_payload.encapsulate() + + +def test_kwargs(): + + payload = mktl.protocol.message.Payload('something', testing='testing') + assert payload.testing == 'testing' + + encapsulated = payload.encapsulate() + decoded = mktl.json.loads(encapsulated) + + assert 'time' in decoded + assert 'value' in decoded + assert 'testing' in decoded + + payload.omit.add('testing') + + encapsulated = payload.encapsulate() + decoded = mktl.json.loads(encapsulated) + + assert not 'testing' in decoded + + +def test_origin(): + + payload = mktl.protocol.message.Payload('something') + payload.add_origin() + + encapsulated = payload.encapsulate() + decoded = mktl.json.loads(encapsulated) + + assert '_user' in decoded + assert '_hostname' in decoded + assert '_pid' in decoded + assert '_ppid' in decoded + assert '_executable' in decoded + assert '_argv' in decoded + + +# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 autoindent: diff --git a/tests/test_item.py b/tests/test_item.py index 1d18473..7eaf018 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -2,24 +2,30 @@ import pytest import time +try: + import pint +except ImportError: + pint = None + + def test_get(run_mkbrokerd, run_mkd): - integer = mktl.get('unittest.INTEGER') - integer.get() + number = mktl.get('unittest.number') + number.get() def test_set(run_mkbrokerd, run_mkd): - integer = mktl.get('unittest.INTEGER') - integer.set(-1) - integer.set(23) - integer.set(44) + number = mktl.get('unittest.number') + number.set(-1) + number.set(23) + number.set(44) - assert integer.get() == 44 - assert integer.value == 44 - assert integer == 44 + assert number.get() == 44 + assert number.value == 44 + assert number == 44 - string = mktl.get('unittest.STRING') + string = mktl.get('unittest.string') string.set('testing') assert string.get() == 'testing' @@ -28,15 +34,98 @@ def test_set(run_mkbrokerd, run_mkd): string.set('') - readonly = mktl.get('unittest.READONLY') + readonly = mktl.get('unittest.readonly') with pytest.raises(RuntimeError): readonly.set(44) +def test_boolean(run_mkbrokerd, run_mkd): + + boolean = mktl.get('unittest.boolean') + + boolean.value = False + assert boolean == 0 + assert boolean == False + + boolean.value = True + assert boolean == 1 + assert boolean == True + + boolean.value = 0 + assert boolean == 0 + assert boolean == False + + boolean.value = 1 + assert boolean == 1 + assert boolean == True + + boolean.formatted = 'fALSE' + boolean.formatted = 'False' + assert boolean == 0 + assert boolean == False + + boolean.formatted = 'tRUE' + boolean.formatted = 'True' + assert boolean == 1 + assert boolean == True + + with pytest.raises(KeyError): + boolean.formatted = 'No' + + with pytest.raises(KeyError): + boolean.formatted = 'Yes' + + noyes = mktl.get('unittest.noyes') + + with pytest.raises(KeyError): + noyes.formatted = 'False' + + with pytest.raises(KeyError): + noyes.formatted = 'True' + + noyes.formatted = 'nO' + noyes.formatted = 'No' + assert noyes == 0 + assert noyes == False + + noyes.formatted = 'yES' + noyes.formatted = 'Yes' + assert noyes == 1 + assert noyes == True + + +def test_enumerated(run_mkbrokerd, run_mkd): + + enumerated = mktl.get('unittest.enumerated') + + enumerated.value = 0 + assert enumerated == 0 + assert enumerated.formatted == 'Zero' + + enumerated.value = 1 + assert enumerated == 1 + assert enumerated.formatted == 'One' + + enumerated.formatted = 'zERO' + assert enumerated == 0 + + enumerated.formatted = 'oNE' + assert enumerated == 1 + + with pytest.raises(KeyError): + enumerated.formatted = 'invalid' + + with pytest.raises(RuntimeError): + enumerated.value = 234 + + with pytest.raises(RuntimeError): + enumerated.value = 'invalid' + + def test_logic(run_mkbrokerd, run_mkd): - string = mktl.get('unittest.STRING') + string = mktl.get('unittest.string') string.set('testing') assert bool(string) == True @@ -45,84 +134,306 @@ def test_logic(run_mkbrokerd, run_mkd): assert bool(string) == False - integer = mktl.get('unittest.INTEGER') + number = mktl.get('unittest.number') + + number.value = 0 + assert bool(number) == False + assert number | 0 == 0 + assert number | 1 == 1 + assert number & 0 == 0 + assert number & 1 == 0 + + number.value = 1 + assert bool(number) == True + + number.value = 2 + assert bool(number) == True + assert number | 0 == 2 + assert number | 1 == 3 + assert number & 0 == 0 + assert number & 1 == 0 + assert number & 2 == 2 + assert number ^ 2 == 0 + assert number ^ 1 == 3 + + assert 0 | number == 2 + assert 1 | number == 3 + assert 0 & number == 0 + assert 1 & number == 0 + assert 2 & number == 2 + assert 2 ^ number == 0 + assert 1 ^ number == 3 - integer.value = 0 - assert bool(integer) == False - assert integer | 0 == 0 - assert integer | 1 == 1 - assert integer & 0 == 0 - assert integer & 1 == 0 - integer.value = 1 - assert bool(integer) == True +def test_mask(run_mkbrokerd, run_mkd): - integer.value = 2 - assert bool(integer) == True - assert integer | 0 == 2 - assert integer | 1 == 3 - assert integer & 0 == 0 - assert integer & 1 == 0 - assert integer & 2 == 2 - assert integer ^ 2 == 0 - assert integer ^ 1 == 3 + mask = mktl.get('unittest.mask') - assert 0 | integer == 2 - assert 1 | integer == 3 - assert 0 & integer == 0 - assert 1 & integer == 0 - assert 2 & integer == 2 - assert 2 ^ integer == 0 - assert 1 ^ integer == 3 + mask.value = 0 + assert mask == 0 + assert mask.formatted == 'none set' + + mask.value = 1 + assert mask == 1 + assert mask.formatted == 'A' + + mask.formatted = 'B' + assert mask == 2 + + mask.formatted = 'c' + assert mask == 4 + + mask.value = 3 + assert mask.formatted == 'A, B' + + mask.formatted = 'B, C' + assert mask == 6 + + with pytest.raises(KeyError): + mask.formatted = 'invalid' + + with pytest.raises(RuntimeError): + mask.value = 234 + + with pytest.raises(RuntimeError): + mask.value = 'invalid' def test_math(run_mkbrokerd, run_mkd): - integer = mktl.get('unittest.INTEGER') - - integer.value = 50 - - assert integer == 50 - assert integer <= 50 - assert integer <= 51 - assert integer < 51 - assert integer >= 50 - assert integer >= 49 - assert integer > 49 - assert integer != 49 - - assert +integer == 50 - assert -integer == -50 - assert ~integer == ~50 - assert integer + 1 == 51 - assert integer - 1 == 49 - assert integer * 2 == 100 - assert integer / 2 == 25 - assert integer ** 2 == 2500 - assert integer % 25 == 0 - assert integer % 12 == 2 - - assert 1 + integer == 51 - assert 1 - integer == -49 - assert 2 * integer == 100 - assert 2 / integer == 0.04 - assert 2 ** integer == 1125899906842624 - assert 100 % integer == 0 - assert 52 % integer == 2 - - integer += 1 - assert integer == 51 - integer -= 1 - assert integer == 50 - integer /= 2 - assert integer == 25 - integer *= 2 - assert integer == 50 + number = mktl.get('unittest.number') + + # The ~ operator works on integers and not floating point numbers. + + number.value = 25 + assert ~number == ~25 + + number.value = 25.1 + with pytest.raises(TypeError): + ~number + + # The remainder of the operations are expected to work for both integer + # and floating point numbers. + + testing = (50, 50.1) + for test_value in testing: + number.value = test_value + + assert number == test_value + assert number <= test_value + assert number <= test_value + 1 + assert number < test_value + 1 + assert number >= test_value + assert number >= test_value - 1 + assert number > test_value - 1 + assert number != test_value - 1 + + assert +number == test_value + assert -number == -test_value + assert number + 1 == test_value + 1 + assert number - 1 == test_value - 1 + assert number * 2 == test_value * 2 + assert number / 2 == test_value / 2 + assert number ** 2 == test_value ** 2 + assert number % 25 == test_value % 25 + assert number % 12 == test_value % 12 + + assert 1 + number == 1 + test_value + assert 1 - number == 1 - test_value + assert 2 * number == 2 * test_value + assert 2 / number == 2 / test_value + assert 2 ** number == 2 ** test_value + assert 100 % number == 100 % test_value + assert 52 % number == 52 % test_value + + assert number + 1 == test_value + 1 + assert number - 1 == test_value - 1 + assert number * 2 == test_value * 2 + assert number / 2 == test_value / 2 + assert number ** 2 == test_value ** 2 + assert number % 25 == test_value % 25 + assert number % 48 == test_value % 48 + + number += 1 + assert number == test_value + 1 + number -= 1 + assert number == test_value + number /= 2 + assert number == test_value / 2 + number *= 2 + assert number == test_value + + +def test_quantity(run_mkbrokerd, run_mkd): + + if pint is None: + return + + angle = mktl.get('unittest', 'angle') + + angle.value = 0.5 + original_value = angle.value + + # The base units are radians. The formatted units require degrees to be + # available, but any rational angular unit should be usable. + + radians = angle.quantity + degrees = angle.quantity.to('degrees') + microradians = angle.quantity.to('microradians') + + # Comparing microradians runs into floating point imprecision. + + original_microradians = original_value * 1000000 + assert microradians.magnitude >= original_microradians - 0.0000001 + assert microradians.magnitude <= original_microradians + 0.0000001 + + # The quantity property should accept any units that can be translated to + # the base units. Multiply some other units by a factor of ten, and set the + # item value to that scaled value; the item-provided quantity should reflect + # the multiplied value. + + microradians *= 10 + angle.quantity = microradians + + original_scaled = original_value * 10 + assert angle.value >= original_scaled - 0.0000001 + assert angle.value <= original_scaled + 0.0000001 + + +def test_sexagesimal(run_mkbrokerd, run_mkd): + + if pint is None: + return + + # The regular 'angle' is D:M:S. + + angle = mktl.get('unittest.angle') + + angle.formatted = '1:2:3' + assert angle.formatted == ' 1:02:03.0' + + angle *= 2 + assert angle.formatted == ' 2:04:06.0' + + angle *= 2 + assert angle.formatted == ' 4:08:12.0' + + angle *= 10 + assert angle.formatted == '41:22:00.0' + + angle *= 10 + assert angle.formatted == '413:40:00.0' + assert angle.value == 7.21984533908321 + + angle.formatted = '-1:2:3' + assert angle.formatted == '-1:02:03.0' + + angle.formatted = '1:-2:3' + assert angle.formatted == ' 0:58:03.0' + + angle.formatted = '1:-2:-3' + assert angle.formatted == ' 0:57:57.0' + + angle.formatted = '1:2:-3' + assert angle.formatted == ' 1:01:57.0' + + # The 'hourangle' item is H:M:S. + + hourangle = mktl.get('unittest.hourangle') + + hourangle.formatted = '1:2:3' + assert hourangle.formatted == ' 1:02:03.0' + + hourangle *= 2 + assert hourangle.formatted == ' 2:04:06.0' + + hourangle *= 2 + assert hourangle.formatted == ' 4:08:12.0' + + hourangle *= 10 + assert hourangle.formatted == '41:22:00.0' + + hourangle *= 10 + assert hourangle.formatted == '413:40:00.0' + assert hourangle.value == 108.29768008624816 + + +def test_string(run_mkbrokerd, run_mkd): + + string = mktl.get('unittest.string') + + test_value = 'test' + string.value = test_value + + with pytest.raises(TypeError): + ~string + + with pytest.raises(TypeError): + string - 't' + + with pytest.raises(TypeError): + string - 1 + + with pytest.raises(TypeError): + +string + + with pytest.raises(TypeError): + -string + + with pytest.raises(ValueError): + string / 't' + + with pytest.raises(ValueError): + string / 2 + + with pytest.raises(TypeError): + string ** 2 + + with pytest.raises(TypeError): + string % 2 + + assert string == test_value + assert string <= test_value + assert string <= test_value + 'z' + assert string < test_value + 'z' + assert string >= test_value + assert string >= 'a' + test_value + assert string > 'a' + test_value + assert string != 'a' + test_value + + assert string + 'z' == test_value + 'z' + assert string * 2 == test_value * 2 + assert 'z' + string == 'z' + test_value + assert 2 * string == 2 * test_value + + string += 'z' + assert string == test_value + 'z' + string.value = test_value + string *= 2 + assert string == test_value * 2 + + +def test_typeless(run_mkbrokerd, run_mkd): + + typeless = mktl.get('unittest.typeless') + + typeless.value = 24 + typeless.formatted + typeless.value = True + typeless.formatted + typeless.value = 'test' + typeless.formatted + typeless.value = {1: 'one', 2: 'two'} + typeless.formatted + typeless.value = (1, 2, 3) + typeless.formatted + typeless.value = None + typeless.formatted def test_callback(run_mkbrokerd, run_mkd): - string = mktl.get('unittest.STRING') + string = mktl.get('unittest.string') test_callback.called = False test_callback.item = None diff --git a/tests/test_poll.py b/tests/test_poll.py index d103bfd..3ef2083 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -22,6 +22,9 @@ def test_references(): def callback(): pass + period = mktl.poll.period(callback) + assert period is None + mktl.poll.start(callback, period=1) period = mktl.poll.period(callback) assert period is not None diff --git a/tests/test_store.py b/tests/test_store.py index 9fff612..22897fa 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -11,13 +11,13 @@ def test_store(run_mkbrokerd, run_mkd): assert store.has_key('angle') assert store.has_key('Angle') assert store.has_key('ANGLE') - assert store.has_key('integer') + assert store.has_key('number') assert store.has_key('string') assert 'angle' in store assert 'Angle' in store assert 'ANGLE' in store - assert 'integer' in store + assert 'number' in store assert 'string' in store with pytest.raises(NotImplementedError): @@ -44,15 +44,19 @@ def test_store(run_mkbrokerd, run_mkd): with pytest.raises(NotImplementedError): store.update() - integer1 = store['integer'] + number1 = store['number'] string1 = store['string'] - integer2 = store['INTEGER'] + number2 = store['NUMBER'] string2 = store['STRING'] - assert integer1 is integer2 + assert number1 is number2 assert string1 is string2 + assert number1 in store + number3 = store[number1] + assert number1 is number3 + for item in store: assert isinstance(item, mktl.Item) @@ -67,8 +71,8 @@ def test_store(run_mkbrokerd, run_mkd): for key in store.keys(): assert key in store.config - # The actual result from str() and repr() is not enforced, just want to - # invoke the statement(s) for completeness's sake. + # The actual result from str() and repr() is not inspected, it's only + # used for debug purposes. str(store) repr(store) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 9eb0607..fcc2661 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -9,11 +9,11 @@ class Daemon(mktl.Daemon): - def __init__(self, *args, **kwargs): + def __init__(self, store, alias, *args, **kwargs): items = generate_config() - mktl.config.authoritative('unittest', 'unittest', items) - mktl.Daemon.__init__(self, *args, **kwargs) + mktl.config.authoritative(store, alias, items) + mktl.Daemon.__init__(self, store, alias, *args, **kwargs) # end of class Daemon @@ -24,26 +24,57 @@ def generate_config(): items = dict() - items['INTEGER'] = dict() - items['INTEGER']['description'] = 'A numeric item.' - items['INTEGER']['units'] = 'meaningless units' - items['INTEGER']['type'] = 'numeric' - - items['STRING'] = dict() - items['STRING']['description'] = 'A string item.' - items['STRING']['type'] = 'string' - - items['ANGLE'] = dict() - items['ANGLE']['description'] = 'An angular numeric item.' - items['ANGLE']['units'] = 'radians' - items['ANGLE']['type'] = 'numeric' - - items['READONLY'] = dict() - items['READONLY']['description'] = 'A read-only numeric item.' - items['READONLY']['units'] = 'meaningless units' - items['READONLY']['type'] = 'numeric' - items['READONLY']['initial'] = 13 - items['READONLY']['settable'] = False + items['angle'] = dict() + items['angle']['description'] = 'An angular numeric item.' + items['angle']['type'] = 'numeric' + items['angle']['format'] = '%2d:%2.2d:%04.1f' + items['angle']['units'] = {'': 'radians', 'formatted': 'degrees'} + items['angle']['initial'] = 0.018049613347708025 + + items['boolean'] = dict() + items['boolean']['description'] = 'A boolean item without enumerators.' + items['boolean']['type'] = 'boolean' + + items['enumerated'] = dict() + items['enumerated']['description'] = 'An enumerated item.' + items['enumerated']['type'] = 'enumerated' + items['enumerated']['enumerators'] = {0: 'Zero', 1: 'One', 4: 'Four'} + + items['hourangle'] = dict() + items['hourangle']['description'] = 'An angular numeric item, in h:m:s.' + items['hourangle']['type'] = 'numeric' + items['hourangle']['format'] = '%2d:%2.2d:%04.1f' + items['hourangle']['units'] = {'': 'radians', 'formatted': 'hours'} + items['hourangle']['initial'] = 0.2707442002156204 + + items['mask'] = dict() + items['mask']['description'] = 'A mask item.' + items['mask']['type'] = 'mask' + items['mask']['enumerators'] = {'None': 'none set', 0: 'A', 1: 'B', 2: 'C'} + + items['noyes'] = dict() + items['noyes']['description'] = 'A boolean item with enumerators.' + items['noyes']['type'] = 'boolean' + items['noyes']['enumerators'] = {0: 'No', 1: 'Yes'} + + items['number'] = dict() + items['number']['description'] = 'A numeric item.' + items['number']['type'] = 'numeric' + items['number']['units'] = 'meaningless units' + + items['readonly'] = dict() + items['readonly']['description'] = 'A read-only numeric item.' + items['readonly']['type'] = 'numeric' + items['readonly']['units'] = 'meaningless units' + items['readonly']['initial'] = 13 + items['readonly']['settable'] = False + + items['string'] = dict() + items['string']['description'] = 'A string item.' + items['string']['type'] = 'string' + + items['typeless'] = dict() + items['typeless']['description'] = 'A typeless item.' return items