diff --git a/build.py b/build.py index f4f6b45..b3c042c 100644 --- a/build.py +++ b/build.py @@ -59,7 +59,7 @@ ] summary = 'A framework that exposes a simple set of APIs enabling multi-process integration with the curses screen painting library' url = 'https://github.com/soda480/mpcurses' -version = '0.1.0' +version = '0.1.1' default_task = [ 'clean', 'analyze', diff --git a/src/main/python/mpcurses/mpcurses.py b/src/main/python/mpcurses/mpcurses.py index 961f3cf..926af24 100644 --- a/src/main/python/mpcurses/mpcurses.py +++ b/src/main/python/mpcurses/mpcurses.py @@ -40,10 +40,41 @@ class NoActiveProcesses(Exception): pass +class OnDict(dict): + """ subclass dict to execute method when items are added or removed changes + """ + def __init__(self, on_change=None): + """ override constructor + """ + if on_change is None: + raise ValueError('on_change method must be specified') + super(OnDict, self).__init__() + self.on_change = on_change + + def __setitem__(self, *args): + """ override setitem + """ + super(OnDict, self).__setitem__(*args) + self.on_change(False) + + def __delitem__(self, *args): + """ override delitem + """ + super(OnDict, self).__delitem__(*args) + self.on_change(True) + + def pop(self, *args): + """ override pop + """ + value = super(OnDict, self).pop(*args) + if value is not None: + self.on_change(True) + return value + + class MPcurses(): """ mpcurses process pool """ - def __init__(self, function, *, process_data=None, shared_data=None, processes_to_start=None, screen_layout=None, init_messages=None, setup_process_queue=True): """ MPCstate constructor """ @@ -61,7 +92,7 @@ def __init__(self, function, *, process_data=None, shared_data=None, processes_t processes_to_start = len(process_data) self.processes_to_start = processes_to_start - self.active_processes = {} + self.active_processes = OnDict(on_change=self.on_state_change) self.process_data_offset = [(self.process_data.index(item), item) for item in self.process_data] @@ -123,9 +154,6 @@ def start_next_process(self): # update active_processes dictionary with process meta-data for the process offset self.active_processes[str(offset)] = process - # consider better way of implementing on_state_change - self.on_state_change(process_completed=False) - def terminate_processes(self): """ terminate all active processes """ @@ -147,9 +175,6 @@ def remove_active_process(self, offset): process_id = process.pid if process else '-' logger.info(f'process at offset {offset} process id {process_id} has completed') - # consider better way of implementing on_state_change - self.on_state_change(process_completed=True) - def update_result(self): """ update process data with result """ diff --git a/src/unittest/python/test_mpcurses.py b/src/unittest/python/test_mpcurses.py index e1c158d..7aee23f 100644 --- a/src/unittest/python/test_mpcurses.py +++ b/src/unittest/python/test_mpcurses.py @@ -23,6 +23,7 @@ from mpcurses.mpcurses import MPcurses from mpcurses.mpcurses import NoActiveProcesses +from mpcurses.mpcurses import OnDict import sys import logging @@ -83,9 +84,8 @@ def test__start_processes_Should_CallStartNextProcess_When_ProcessesToStartGreat client.start_processes() self.assertEqual(len(start_next_process_patch.mock_calls), 0) - @patch('mpcurses.MPcurses.on_state_change') @patch('mpcurses.mpcurses.Process') - def test__start_next_process_Should_CallExpected_When_Called(self, process_patch, on_state_change_patch, *patches): + def test__start_next_process_Should_CallExpected_When_Called(self, process_patch, *patches): process_mock = Mock() process_patch.return_value = process_mock @@ -102,7 +102,6 @@ def test__start_next_process_Should_CallExpected_When_Called(self, process_patch 'offset': 0, 'result_queue': client.result_queue }) - on_state_change_patch.assert_called_once_with(process_completed=False) def test__terminate_processes_Should_CallExpected_When_Called(self, *patches): function_mock = Mock() @@ -124,8 +123,7 @@ def test__purge_process_queue_Should_PurgeProcessQueue_When_Called(self, *patche self.assertTrue(client.process_queue.empty()) @patch('mpcurses.mpcurses.logger') - @patch('mpcurses.MPcurses.on_state_change') - def test__remove_active_process_Should_CallExpected_When_Called(self, on_state_change_patch, logger_patch, *patches): + def test__remove_active_process_Should_CallExpected_When_Called(self, logger_patch, *patches): function_mock = Mock() process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}] client = MPcurses(function=function_mock, process_data=process_data) @@ -133,7 +131,6 @@ def test__remove_active_process_Should_CallExpected_When_Called(self, on_state_c client.active_processes['0'] = process_mock client.remove_active_process('0') logger_patch.info.assert_called_once_with('process at offset 0 process id 121372 has completed') - on_state_change_patch.assert_called_once_with(process_completed=True) def test__update_result_Should_CallExpected_When_Called(self, *patches): result_queue_mock = Mock() @@ -416,3 +413,51 @@ def test__execute_Should_CallExpected_When_NoScreenLayout(self, run_patch, updat client.execute() run_patch.assert_called_once_with() update_result_patch.assert_called_once_with() + + +class TestOnDict(unittest.TestCase): + + def setUp(self): + """ + """ + pass + + def tearDown(self): + """ + """ + pass + + def test__init_Should_SetOnChange_When_Called(self, *patches): + on_change_mock = Mock() + onDict = OnDict(on_change=on_change_mock) + self.assertEqual(onDict.on_change, on_change_mock) + + def test__init_Should_RaiseValueError_When_OnChangeNotSpecified(self, *patches): + with self.assertRaises(ValueError): + OnDict() + + def test__setitem_Should_CallOnChange_When_Called(self, *patches): + on_change_mock = Mock() + onDict = OnDict(on_change=on_change_mock) + onDict['key1'] = 'value1' + on_change_mock.assert_called_once_with(False) + + def test__deltitem_Should_CallOnChange_When_Called(self, *patches): + on_change_mock = Mock() + onDict = OnDict(on_change=on_change_mock) + onDict['key1'] = 'value1' + del onDict['key1'] + self.assertTrue(call(True) in on_change_mock.mock_calls) + + def test__pop_Should_CallOnChange_When_Called(self, *patches): + on_change_mock = Mock() + onDict = OnDict(on_change=on_change_mock) + onDict['key1'] = 'value1' + onDict.pop('key1', None) + self.assertTrue(call(True) in on_change_mock.mock_calls) + + def test__pop_Should_NotCallOnChange_When_NoValue(self, *patches): + on_change_mock = Mock() + onDict = OnDict(on_change=on_change_mock) + onDict.pop('key1', None) + on_change_mock.assert_not_called()