Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1d661ea
There's no need to hard-code the store and alias, they're available as
Feb 10, 2026
be99e6d
Do some left-hand-side calculations.
Feb 10, 2026
710d19d
Rename the test number to 'number' instead of 'integer'.
Feb 10, 2026
53ebbad
Use lower-case for the item keys.
Feb 10, 2026
2db16df
Explicitly test floating point math.
Feb 10, 2026
a7133fe
Use lower-case item keys.
Feb 10, 2026
dedc6c1
Explicit tests for string behavior.
Feb 10, 2026
d77bf77
Add an item that has no type.
Feb 10, 2026
b773056
Explicitly test a typeless item.
Feb 10, 2026
8e4423a
Put them back in alphabetical order.
Feb 10, 2026
6ce32f1
Fixed the handling of boolean enumerators, a bug highlighted by unit …
Feb 10, 2026
4cd6a6a
Add boolean items.
Feb 10, 2026
39a7c54
Explicit tests for boolean items.
Feb 10, 2026
8693d0a
Remove the blanket exception catch for failed formatting.
Feb 11, 2026
591a6c8
Further revisions to the initial massaging of enumerators in the
Feb 11, 2026
4325c42
Add an enumerated item for testing.
Feb 11, 2026
c80e50c
Add tests for the enumerated type.
Feb 11, 2026
90af90e
Handle the case where mask enumerators are provided as integers.
Feb 11, 2026
ebc5d87
Add a mask item for testing.
Feb 11, 2026
27cfcf9
Raise a KeyError if a new mask value contains unknown bits.
Feb 11, 2026
8c193c3
Remove debug information from raised exception.
Feb 11, 2026
d824cc1
Add tests for the mask type.
Feb 11, 2026
49786af
Make the 'angle' item use sexagesimal formatting, and add an 'hourangle'
Feb 11, 2026
5717319
Initial values for the angular items.
Feb 11, 2026
9107b50
Add tests for sexagesimal formatting.
Feb 12, 2026
32ec28d
Added a readme file with simple instructions.
Feb 12, 2026
116d153
Include one additional poll.period() call to get to full coverage.
Feb 12, 2026
b73df26
The sexagesimal tests are only possible if pint is installed.
Feb 12, 2026
ae16731
Add tests for the Item.quantity property.
Feb 12, 2026
c039931
Change the output of repr() to be more descriptive.
Feb 12, 2026
b5ad4b6
Revise the __contains__() and __getitem__() methods to properly handle
Feb 12, 2026
eca3580
Exercise retrieving an Item from a Store using an Item instance.
Feb 12, 2026
deb3a83
Clean up some of the lock handling around raising an exception when
Feb 12, 2026
7703337
Test the Payload class.
Feb 12, 2026
fc27d63
More payload testing.
Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions src/mktl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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':
Expand All @@ -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:
Expand Down Expand Up @@ -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():
Expand All @@ -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.

Expand Down
6 changes: 1 addition & 5 deletions src/mktl/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
24 changes: 15 additions & 9 deletions src/mktl/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,26 @@ 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):
raise NotImplementedError("you cannot delete a Store's key directly")


def __getitem__(self, key):
key = key.lower()
if isinstance(key, Item):
key = key.key
else:
key = key.lower()

try:
item = self._items[key]
Expand All @@ -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
Expand All @@ -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):
Expand Down
29 changes: 29 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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.
82 changes: 82 additions & 0 deletions tests/protocol/test_payload.py
Original file line number Diff line number Diff line change
@@ -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:
Loading
Loading