From 1d661ea7003cbd302cb55c559933a5f648f26365 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 09:55:54 -1000 Subject: [PATCH 01/35] There's no need to hard-code the store and alias, they're available as arguments. --- tests/unitdaemon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 9eb0607..2b6a275 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 From be99e6d3ecbbf9c969d4f87b8066506a8f250216 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:16:59 -1000 Subject: [PATCH 02/35] Do some left-hand-side calculations. --- tests/test_item.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 1d18473..539e31b 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -110,6 +110,14 @@ def test_math(run_mkbrokerd, run_mkd): assert 100 % integer == 0 assert 52 % integer == 2 + 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 % 48 == 2 + integer += 1 assert integer == 51 integer -= 1 From 710d19d232b8798f4837088482a53d5277a704ef Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:21:10 -1000 Subject: [PATCH 03/35] Rename the test number to 'number' instead of 'integer'. --- tests/test_item.py | 164 ++++++++++++++++++++++---------------------- tests/test_store.py | 10 +-- tests/unitdaemon.py | 8 +-- 3 files changed, 91 insertions(+), 91 deletions(-) diff --git a/tests/test_item.py b/tests/test_item.py index 539e31b..be8ceef 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -4,20 +4,20 @@ 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') @@ -45,87 +45,87 @@ def test_logic(run_mkbrokerd, run_mkd): assert bool(string) == False - integer = mktl.get('unittest.INTEGER') + number = mktl.get('unittest.number') - integer.value = 0 - assert bool(integer) == False - assert integer | 0 == 0 - assert integer | 1 == 1 - assert integer & 0 == 0 - assert integer & 1 == 0 + number.value = 0 + assert bool(number) == False + assert number | 0 == 0 + assert number | 1 == 1 + assert number & 0 == 0 + assert number & 1 == 0 - integer.value = 1 - assert bool(integer) == True + number.value = 1 + assert bool(number) == True - 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 + 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 | 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 + 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 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 - - 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 % 48 == 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') + + number.value = 50 + + assert number == 50 + assert number <= 50 + assert number <= 51 + assert number < 51 + assert number >= 50 + assert number >= 49 + assert number > 49 + assert number != 49 + + assert +number == 50 + assert -number == -50 + assert ~number == ~50 + assert number + 1 == 51 + assert number - 1 == 49 + assert number * 2 == 100 + assert number / 2 == 25 + assert number ** 2 == 2500 + assert number % 25 == 0 + assert number % 12 == 2 + + assert 1 + number == 51 + assert 1 - number == -49 + assert 2 * number == 100 + assert 2 / number == 0.04 + assert 2 ** number == 1125899906842624 + assert 100 % number == 0 + assert 52 % number == 2 + + assert number + 1 == 51 + assert number - 1 == 49 + assert number * 2 == 100 + assert number / 2 == 25 + assert number ** 2 == 2500 + assert number % 25 == 0 + assert number % 48 == 2 + + number += 1 + assert number == 51 + number -= 1 + assert number == 50 + number /= 2 + assert number == 25 + number *= 2 + assert number == 50 def test_callback(run_mkbrokerd, run_mkd): diff --git a/tests/test_store.py b/tests/test_store.py index 9fff612..8c4a47a 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,13 +44,13 @@ 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 for item in store: diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 2b6a275..fdb6d49 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -24,10 +24,10 @@ def generate_config(): items = dict() - items['INTEGER'] = dict() - items['INTEGER']['description'] = 'A numeric item.' - items['INTEGER']['units'] = 'meaningless units' - items['INTEGER']['type'] = 'numeric' + items['number'] = dict() + items['number']['description'] = 'A numeric item.' + items['number']['units'] = 'meaningless units' + items['number']['type'] = 'numeric' items['STRING'] = dict() items['STRING']['description'] = 'A string item.' From 53ebbadd783f62dd97a60e954b7bb6c2adeb848f Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:22:07 -1000 Subject: [PATCH 04/35] Use lower-case for the item keys. --- tests/unitdaemon.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index fdb6d49..1c20500 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -29,21 +29,21 @@ def generate_config(): items['number']['units'] = 'meaningless units' items['number']['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['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 return items From 2db16df88f5c74378db52a4b508778df3d2ad0d6 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:35:34 -1000 Subject: [PATCH 05/35] Explicitly test floating point math. --- tests/test_item.py | 105 +++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/tests/test_item.py b/tests/test_item.py index be8ceef..964b822 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -80,52 +80,65 @@ def test_math(run_mkbrokerd, run_mkd): number = mktl.get('unittest.number') - number.value = 50 - - assert number == 50 - assert number <= 50 - assert number <= 51 - assert number < 51 - assert number >= 50 - assert number >= 49 - assert number > 49 - assert number != 49 - - assert +number == 50 - assert -number == -50 - assert ~number == ~50 - assert number + 1 == 51 - assert number - 1 == 49 - assert number * 2 == 100 - assert number / 2 == 25 - assert number ** 2 == 2500 - assert number % 25 == 0 - assert number % 12 == 2 - - assert 1 + number == 51 - assert 1 - number == -49 - assert 2 * number == 100 - assert 2 / number == 0.04 - assert 2 ** number == 1125899906842624 - assert 100 % number == 0 - assert 52 % number == 2 - - assert number + 1 == 51 - assert number - 1 == 49 - assert number * 2 == 100 - assert number / 2 == 25 - assert number ** 2 == 2500 - assert number % 25 == 0 - assert number % 48 == 2 - - number += 1 - assert number == 51 - number -= 1 - assert number == 50 - number /= 2 - assert number == 25 - number *= 2 - assert number == 50 + # 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_callback(run_mkbrokerd, run_mkd): From a7133fe4cae792cacde21a693962b1e3075832fa Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:44:40 -1000 Subject: [PATCH 06/35] Use lower-case item keys. --- tests/test_item.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_item.py b/tests/test_item.py index 964b822..9aa9f35 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -19,7 +19,7 @@ def test_set(run_mkbrokerd, run_mkd): 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,7 +28,7 @@ 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) @@ -36,7 +36,7 @@ def test_set(run_mkbrokerd, run_mkd): def test_logic(run_mkbrokerd, run_mkd): - string = mktl.get('unittest.STRING') + string = mktl.get('unittest.string') string.set('testing') assert bool(string) == True @@ -143,7 +143,7 @@ def test_math(run_mkbrokerd, run_mkd): 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 From dedc6c11e8d5e434aa6c8ab6b42553b421103774 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 10:57:17 -1000 Subject: [PATCH 07/35] Explicit tests for string behavior. --- tests/test_item.py | 61 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/tests/test_item.py b/tests/test_item.py index 9aa9f35..747eab4 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -102,13 +102,13 @@ def test_math(run_mkbrokerd, run_mkd): 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 - 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 - 1 == test_value - 1 assert number * 2 == test_value * 2 assert number / 2 == test_value / 2 assert number ** 2 == test_value ** 2 @@ -141,6 +141,61 @@ def test_math(run_mkbrokerd, run_mkd): assert number == test_value +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_callback(run_mkbrokerd, run_mkd): string = mktl.get('unittest.string') From d77bf77e0f3081f8f8e2793aa7932a10d456d5c1 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:00:38 -1000 Subject: [PATCH 08/35] Add an item that has no type. --- tests/unitdaemon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 1c20500..c1ad375 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -45,6 +45,9 @@ def generate_config(): items['readonly']['initial'] = 13 items['readonly']['settable'] = False + items['typeless'] = dict() + items['typeless']['description'] = 'A typeless item.' + return items From b773056db65957e8725aa15074e9dc749037a364 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:06:20 -1000 Subject: [PATCH 09/35] Explicitly test a typeless item. --- tests/test_item.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 747eab4..9af0265 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -196,6 +196,24 @@ def test_string(run_mkbrokerd, run_mkd): 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') From 8e4423a33c8e7d11cb4e37a87f4088684cedf9fe Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:07:11 -1000 Subject: [PATCH 10/35] Put them back in alphabetical order. --- tests/unitdaemon.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index c1ad375..69bd2d6 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -24,20 +24,16 @@ def generate_config(): items = dict() - items['number'] = dict() - items['number']['description'] = 'A numeric item.' - items['number']['units'] = 'meaningless units' - items['number']['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['number'] = dict() + items['number']['description'] = 'A numeric item.' + items['number']['units'] = 'meaningless units' + items['number']['type'] = 'numeric' + items['readonly'] = dict() items['readonly']['description'] = 'A read-only numeric item.' items['readonly']['units'] = 'meaningless units' @@ -45,6 +41,10 @@ def generate_config(): 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.' From 6ce32f1b301822cc2a1ec9d894ce2e6b7445ad82 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:43:42 -1000 Subject: [PATCH 11/35] Fixed the handling of boolean enumerators, a bug highlighted by unit tests. --- src/mktl/config.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/mktl/config.py b/src/mktl/config.py index cab1c59..fcabc03 100644 --- a/src/mktl/config.py +++ b/src/mktl/config.py @@ -682,6 +682,9 @@ def to_format_enumerated(self, key, value): # {"0": "No", "1": "Yes", "2": "Unknown"} + if isinstance(value, bool): + value = int(value) + value = str(value) try: @@ -992,14 +995,14 @@ def update(self, block, save=True): enumerators = dict() item_config['enumerators'] = enumerators - try: - enumerators['0'] - except: + if 0 in enumerators or '0' in enumerators: + pass + else: enumerators['0'] = 'False' - try: - enumerators['1'] - except: + if 1 in enumerators or '1' in enumerators: + pass + else: enumerators['1'] = 'True' From 4cd6a6a69add4950e518f0ae59456dc991646cc9 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:44:57 -1000 Subject: [PATCH 12/35] Add boolean items. --- tests/unitdaemon.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 69bd2d6..def759e 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -29,6 +29,15 @@ def generate_config(): items['angle']['units'] = 'radians' items['angle']['type'] = 'numeric' + items['boolean'] = dict() + items['boolean']['description'] = 'A boolean item without enumerators.' + items['boolean']['type'] = 'boolean' + + 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']['units'] = 'meaningless units' From 39a7c547d7a335affba5295570358beb47b90bc7 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 11:45:12 -1000 Subject: [PATCH 13/35] Explicit tests for boolean items. --- tests/test_item.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 9af0265..49d27bf 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -34,6 +34,61 @@ def test_set(run_mkbrokerd, run_mkd): 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_logic(run_mkbrokerd, run_mkd): string = mktl.get('unittest.string') From 8693d0a8c9dd0695f869b032720e3e2dbe5b62fa Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 15:57:18 -1000 Subject: [PATCH 14/35] Remove the blanket exception catch for failed formatting. --- src/mktl/item.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 From 591a6c8351ea54538753d1dcea2452006f3f02c6 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 15:57:46 -1000 Subject: [PATCH 15/35] Further revisions to the initial massaging of enumerators in the description of items. --- src/mktl/config.py | 47 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/mktl/config.py b/src/mktl/config.py index fcabc03..f38fb2c 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,6 +682,9 @@ 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) @@ -690,7 +693,7 @@ def to_format_enumerated(self, key, value): try: formatted = enumerators[value] except KeyError: - formatted = value + raise KeyError('invalid enumerator: ' + repr(value)) return formatted @@ -975,9 +978,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(): @@ -988,24 +990,45 @@ 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': + additions = dict() + deletions = list() + try: enumerators = item_config['enumerators'] except KeyError: enumerators = dict() item_config['enumerators'] = enumerators - if 0 in enumerators or '0' in enumerators: - pass - else: + for enumerator in enumerators.keys(): + 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 KeyError: enumerators['0'] = 'False' - if 1 in enumerators or '1' in enumerators: - pass - else: + try: + enumerators['1'] + 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. From 4325c4239a758696ff4aa91122bbaea1800746bb Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 15:58:36 -1000 Subject: [PATCH 16/35] Add an enumerated item for testing. --- tests/unitdaemon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index def759e..9a3b248 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -33,6 +33,11 @@ def generate_config(): 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['noyes'] = dict() items['noyes']['description'] = 'A boolean item with enumerators.' items['noyes']['type'] = 'boolean' From c80e50cacadfe769636b113850aa98a9b64c629c Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 15:58:51 -1000 Subject: [PATCH 17/35] Add tests for the enumerated type. --- tests/test_item.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 49d27bf..d7fb41e 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -89,6 +89,34 @@ def test_boolean(run_mkbrokerd, run_mkd): 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') From 90af90ed05051c1abb42f7f0a1fd93a911ca6dd7 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 16:28:01 -1000 Subject: [PATCH 18/35] Handle the case where mask enumerators are provided as integers. --- src/mktl/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mktl/config.py b/src/mktl/config.py index f38fb2c..a262b0c 100644 --- a/src/mktl/config.py +++ b/src/mktl/config.py @@ -993,7 +993,7 @@ def update(self, block, save=True): # First pass: make sure the enumerators are in with # strings as keys instead of integers. - if type == 'boolean' or type == 'enumerated': + if type == 'boolean' or type == 'enumerated' or type == 'mask': additions = dict() deletions = list() @@ -1004,6 +1004,11 @@ def update(self, block, save=True): 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) From ebc5d871d0b491e5251383638f29198040b4ba6b Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 16:28:56 -1000 Subject: [PATCH 19/35] Add a mask item for testing. --- tests/unitdaemon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index 9a3b248..fb96dc7 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -38,6 +38,11 @@ def generate_config(): items['enumerated']['type'] = 'enumerated' items['enumerated']['enumerators'] = {0: 'Zero', 1: 'One', 4: 'Four'} + 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' From 27cfcf98b2109ac4351d51382bbd395a422b1d66 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 16:50:37 -1000 Subject: [PATCH 20/35] Raise a KeyError if a new mask value contains unknown bits. --- src/mktl/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mktl/config.py b/src/mktl/config.py index a262b0c..f935659 100644 --- a/src/mktl/config.py +++ b/src/mktl/config.py @@ -717,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': @@ -726,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('verified %d; value contains unknown active bits: ' % (verified) + str(value)) if len(formatted) == 0: try: From 8c193c35176fb412f3524116f6c3e07e8711dd85 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 16:52:51 -1000 Subject: [PATCH 21/35] Remove debug information from raised exception. --- src/mktl/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mktl/config.py b/src/mktl/config.py index f935659..c66b8fa 100644 --- a/src/mktl/config.py +++ b/src/mktl/config.py @@ -730,7 +730,7 @@ def to_format_mask(self, key, value): verified += bit_value if verified != value: - raise KeyError('verified %d; value contains unknown active bits: ' % (verified) + str(value)) + raise KeyError('value contains unknown active bits: ' + str(value)) if len(formatted) == 0: try: From d824cc17b2428bdbde391ec57a136c85ca21d2ac Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Tue, 10 Feb 2026 16:54:00 -1000 Subject: [PATCH 22/35] Add tests for the mask type. --- tests/test_item.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index d7fb41e..4063f21 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -159,6 +159,40 @@ def test_logic(run_mkbrokerd, run_mkd): assert 1 ^ number == 3 +def test_mask(run_mkbrokerd, run_mkd): + + mask = mktl.get('unittest.mask') + + 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): number = mktl.get('unittest.number') From 49786af201c0eb015f55248fe44af78f725ce329 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 11:38:08 -1000 Subject: [PATCH 23/35] Make the 'angle' item use sexagesimal formatting, and add an 'hourangle' item that uses H:M:S formatting. --- tests/unitdaemon.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index fb96dc7..b403635 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -26,8 +26,9 @@ def generate_config(): items['angle'] = dict() items['angle']['description'] = 'An angular numeric item.' - items['angle']['units'] = 'radians' items['angle']['type'] = 'numeric' + items['angle']['format'] = '%2d:%2.2d:%04.1f' + items['angle']['units'] = {'': 'radians', 'formatted': 'degrees'} items['boolean'] = dict() items['boolean']['description'] = 'A boolean item without enumerators.' @@ -38,6 +39,12 @@ def generate_config(): 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['mask'] = dict() items['mask']['description'] = 'A mask item.' items['mask']['type'] = 'mask' @@ -50,13 +57,13 @@ def generate_config(): items['number'] = dict() items['number']['description'] = 'A numeric item.' - items['number']['units'] = 'meaningless units' items['number']['type'] = 'numeric' + items['number']['units'] = 'meaningless units' items['readonly'] = dict() items['readonly']['description'] = 'A read-only numeric item.' - items['readonly']['units'] = 'meaningless units' items['readonly']['type'] = 'numeric' + items['readonly']['units'] = 'meaningless units' items['readonly']['initial'] = 13 items['readonly']['settable'] = False From 57173196d20105c13c753b74f30b8cc74ca53ae1 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 11:47:45 -1000 Subject: [PATCH 24/35] Initial values for the angular items. --- tests/unitdaemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unitdaemon.py b/tests/unitdaemon.py index b403635..fcc2661 100644 --- a/tests/unitdaemon.py +++ b/tests/unitdaemon.py @@ -29,6 +29,7 @@ def generate_config(): 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.' @@ -44,6 +45,7 @@ def generate_config(): 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.' From 9107b504ff3e1fd62b959ecfc2a445a580d2a5b5 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 14:14:01 -1000 Subject: [PATCH 25/35] Add tests for sexagesimal formatting. --- tests/test_item.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 4063f21..0f35382 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -258,6 +258,61 @@ def test_math(run_mkbrokerd, run_mkd): assert number == test_value +def test_sexagesimal(run_mkbrokerd, run_mkd): + + # 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') From 32ec28d60be8ea7d69ac54bdf6af4a5c99fb2b53 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 15:08:48 -1000 Subject: [PATCH 26/35] Added a readme file with simple instructions. --- tests/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/README.md 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. From 116d153056e4a370d0cb29894fff429608ba2763 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 15:21:45 -1000 Subject: [PATCH 27/35] Include one additional poll.period() call to get to full coverage. --- tests/test_poll.py | 3 +++ 1 file changed, 3 insertions(+) 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 From b73df26b139f56ae096ea43fcfa16d231b14b81e Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 15:31:33 -1000 Subject: [PATCH 28/35] The sexagesimal tests are only possible if pint is installed. --- tests/test_item.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 0f35382..1d66a9e 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -2,6 +2,12 @@ import pytest import time +try: + import pint +except ImportError: + pint = None + + def test_get(run_mkbrokerd, run_mkd): number = mktl.get('unittest.number') @@ -258,8 +264,17 @@ def test_math(run_mkbrokerd, run_mkd): assert number == test_value +def test_quantity(run_mkbrokerd, run_mkd): + + if pint is None: + return + + def test_sexagesimal(run_mkbrokerd, run_mkd): + if pint is None: + return + # The regular 'angle' is D:M:S. angle = mktl.get('unittest.angle') From ae1673172aec39f962752d1aaf6d0ddafa290c9a Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Wed, 11 Feb 2026 16:00:18 -1000 Subject: [PATCH 29/35] Add tests for the Item.quantity property. --- tests/test_item.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_item.py b/tests/test_item.py index 1d66a9e..7eaf018 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -269,6 +269,36 @@ 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): From c0399313dfe7661f4c27461075c0bd2c3e141a3b Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 10:09:59 -1000 Subject: [PATCH 30/35] Change the output of repr() to be more descriptive. --- src/mktl/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mktl/store.py b/src/mktl/store.py index ac1a362..d22431f 100644 --- a/src/mktl/store.py +++ b/src/mktl/store.py @@ -87,7 +87,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): From b5ad4b69145a9cfc927519f8e9c561842bd34acc Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 10:25:10 -1000 Subject: [PATCH 31/35] Revise the __contains__() and __getitem__() methods to properly handle being passed an Item instance as an argument. --- src/mktl/store.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/mktl/store.py b/src/mktl/store.py index d22431f..43e55fa 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] From eca358003f86d554c4223ad1e1c8f64f1e5ff468 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 10:29:51 -1000 Subject: [PATCH 32/35] Exercise retrieving an Item from a Store using an Item instance. --- tests/test_store.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_store.py b/tests/test_store.py index 8c4a47a..22897fa 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -53,6 +53,10 @@ def test_store(run_mkbrokerd, run_mkd): 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) From deb3a83f7f76470720704d8b628bf8794710a926 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 11:12:59 -1000 Subject: [PATCH 33/35] Clean up some of the lock handling around raising an exception when trying to instantiate an Item. --- src/mktl/store.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mktl/store.py b/src/mktl/store.py index 43e55fa..25e4876 100644 --- a/src/mktl/store.py +++ b/src/mktl/store.py @@ -67,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 From 770333711682f1f5f90f68416bcd408742ad8a3c Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 11:37:38 -1000 Subject: [PATCH 34/35] Test the Payload class. --- tests/protocol/test_payload.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/protocol/test_payload.py diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py new file mode 100644 index 0000000..c98f86e --- /dev/null +++ b/tests/protocol/test_payload.py @@ -0,0 +1,29 @@ +import mktl +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 + ##assert payload.reply == True + + + +##def test_encapsulate(): + +##def test_kwargs(): + +##def test_omission(): + +##def test_origin() + +# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 autoindent: From fc27d631678124b718ec7275490c4532f5d6a273 Mon Sep 17 00:00:00 2001 From: Kyle Lanclos Date: Thu, 12 Feb 2026 16:00:38 -1000 Subject: [PATCH 35/35] More payload testing. --- tests/protocol/test_payload.py | 61 +++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/protocol/test_payload.py b/tests/protocol/test_payload.py index c98f86e..3e9abec 100644 --- a/tests/protocol/test_payload.py +++ b/tests/protocol/test_payload.py @@ -1,4 +1,5 @@ import mktl +import pytest import time def test_basics(): @@ -14,16 +15,68 @@ def test_basics(): 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(): -##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() -##def test_kwargs(): + encapsulated = payload.encapsulate() + decoded = mktl.json.loads(encapsulated) -##def test_omission(): + assert '_user' in decoded + assert '_hostname' in decoded + assert '_pid' in decoded + assert '_ppid' in decoded + assert '_executable' in decoded + assert '_argv' in decoded -##def test_origin() # vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 autoindent: