Skip to content

Commit

Permalink
feat: release 0.5.0 (#37)
Browse files Browse the repository at this point in the history
* feat: use on methods for screen process update
* fix: warnings on Dockerfile
* build: update version and classifiers
* fix: examples
* ci: update build pipeline
* feat: add support for Python v12
---------
Signed-off-by: Emilio Reyes <soda480@gmail.com>
  • Loading branch information
soda480 authored Sep 23, 2024
1 parent d17afa6 commit 1ca223b
Show file tree
Hide file tree
Showing 12 changed files with 39 additions and 174 deletions.
40 changes: 3 additions & 37 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,11 @@ jobs:
build-images:
strategy:
matrix:
version: ['3.8', '3.9', '3.10', '3.11']
version: ['3.8', '3.9', '3.10', '3.11', '3.12']
name: Build Python Docker images
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: build mpcurses ${{ matrix.version }} image
run:
docker image build --build-arg PYTHON_VERSION=${{ matrix.version }} -t mpcurses:${{ matrix.version }} .
- name: save mpcurses ${{ matrix.version }} image
if: ${{ matrix.version == '3.9' }}
run: |
mkdir -p images
docker save --output images/mpcurses-${{ matrix.version }}.tar mpcurses:${{ matrix.version }}
- name: upload mpcurses ${{ matrix.version }} image artifact
if: ${{ matrix.version == '3.9' }}
uses: actions/upload-artifact@v3
with:
name: image
path: images/mpcurses-${{ matrix.version }}.tar
coverage:
name: Publish Code Coverage Report
needs: build-images
runs-on: ubuntu-20.04
steps:
- name: download image artifact
uses: actions/download-artifact@v3
with:
name: image
path: images/
- name: load image
run:
docker load --input images/mpcurses-3.9.tar
- name: prepare report
run: |
ID=$(docker create mpcurses:3.9)
docker cp $ID:/code/target/reports/mpcurses_coverage.xml mpcurses_coverage.xml
sed -i -e 's,filename="mpcurses/,filename="src/main/python/mpcurses/,g' mpcurses_coverage.xml
- name: upload report
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: mpcurses_coverage.xml
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ARG PYTHON_VERSION=3.9
FROM python:${PYTHON_VERSION}-slim
ENV PYTHONDONTWRITEBYTECODE 1
ENV TERM xterm-256color
ENV PYTHONDONTWRITEBYTECODE=1
ENV TERM=xterm-256color
WORKDIR /code
COPY . /code/
RUN pip install --upgrade pip && \
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# mpcurses
[![build+test](https://github.com/soda480/mpcurses/actions/workflows/main.yml/badge.svg)](https://github.com/soda480/mpcurses/actions/workflows/main.yml)
[![Code Coverage](https://codecov.io/gh/soda480/mpcurses/branch/master/graph/badge.svg)](https://codecov.io/gh/soda480/mpcurses)
[![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://pybuilder.io/)
[![complexity](https://img.shields.io/badge/complexity-A-brightgreen)](https://radon.readthedocs.io/en/latest/api.html#module-radon.complexity)
[![vulnerabilities](https://img.shields.io/badge/vulnerabilities-None-brightgreen)](https://pypi.org/project/bandit/)
[![PyPI version](https://badge.fury.io/py/mpcurses.svg)](https://badge.fury.io/py/mpcurses)
[![python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-teal)](https://www.python.org/downloads/)
[![python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-teal)](https://www.python.org/downloads/)

Mpcurses is an abstraction of the Python curses and multiprocessing libraries providing function execution and runtime visualization capabilities at scale. It contains a simple API to enable any Python function to be executed across one or more background processes and includes built-in directives to visualize the functions execution on a terminal screen.

Expand Down
16 changes: 3 additions & 13 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
authors = [Author('Emilio Reyes', 'emilio.reyes@intel.com')]
summary = 'Mpcurses is an abstraction of the Python curses and multiprocessing libraries providing function execution and runtime visualization capabilities'
url = 'https://github.com/soda480/mpcurses'
version = '0.4.3'
version = '0.5.0'
default_task = [
'clean',
'analyze',
Expand Down Expand Up @@ -58,24 +58,14 @@ def set_properties(project):
project.set_property('distutils_description_overwrite', True)
project.set_property('distutils_upload_skip_existing', True)
project.set_property('distutils_classifiers', [
'Development Status :: 4 - Beta',
'Environment :: Console',
'Environment :: Console :: Curses',
'Environment :: Other Environment',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: System :: Systems Administration'])
'Programming Language :: Python :: 3.12'])
project.set_property('radon_break_build_average_complexity_threshold', 4)
project.set_property('radon_break_build_complexity_threshold', 14)
project.set_property('bandit_break_build', True)
project.set_property('anybadge_exclude', 'coverage, complexity')
project.set_property('anybadge_complexity_use_average', True)
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
versions=( '3.8' '3.9' '3.10' '3.11' )
versions=( '3.8' '3.9' '3.10' '3.11' '3.12' )
for version in "${versions[@]}";
do
docker image build --build-arg PYTHON_VERSION=$version -t mpcurses:$version .
Expand Down
13 changes: 5 additions & 8 deletions examples/colors.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# Sample - Prime Number Counter

from mpcurses import queue_handler
from mpcurses import execute
# from mpcurses import queue_handler
from mpcurses import MPcurses

from time import sleep
import logging
logger = logging.getLogger(__name__)


@queue_handler
def noop(*args):
pass

Expand All @@ -34,12 +33,10 @@ def get_screen_layout():


def main():
execute(
MPcurses(
function=noop,
process_data=[
{}
],
number_of_processes=1,
process_data=[{}],
processes_to_start=1,
screen_layout=get_screen_layout())


Expand Down
5 changes: 2 additions & 3 deletions examples/example5.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,8 @@ def main():
processes_to_start=5,
screen_layout=get_screen_layout(),
shared_data={'bays': range(1, 17)})
mpcurses.execute()

if any([process['result'] for process in mpcurses.process_data]):
results = mpcurses.execute()
if any([result for result in results]):
sys.exit(-1)


Expand Down
9 changes: 5 additions & 4 deletions examples/example8.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ def main():
{'range': '10001-20000'},
{'range': '20001-30000'}
]
MPcurses(
mpc = MPcurses(
function=check_primes,
process_data=process_data,
processes_to_start=len(process_data)).execute()
processes_to_start=len(process_data))
results = mpc.execute()

for process in process_data:
print(f"the range {process['range']} has {len(process['result'])} primes")
for index, process in enumerate(process_data):
print(f"the range {process['range']} has {len(results[index])} primes")


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion examples/screen_layouts/example4_sl.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def get_screen_layout():
'text_color': 7,
'color': 27,
'clear': True,
'regex': '^checking (?P<value>\d+)$'
'regex': r'^checking (?P<value>\d+)$'
},
'prime': {
'position': (2, 4),
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
mpmq~=0.3.1
mpmq~=0.4.0
54 changes: 12 additions & 42 deletions src/main/python/mpcurses/mpcurses.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,38 +37,6 @@
logger = logging.getLogger(__name__)


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(MPmq):
""" a subclass of MPmq providing multi-processing (MP) capabilities for a curses screen
"""
Expand Down Expand Up @@ -102,8 +70,6 @@ def __init__(self, *args, **kwargs):
# respect processes_to_start if initially passed in
self.processes_to_start = processes_to_start if processes_to_start else None

self.active_processes = OnDict(on_change=self.on_state_change)

self.init_messages = [] if init_messages is None else init_messages
start_time = datetime.now().strftime('%m/%d/%Y %H:%M:%S')
self.init_messages.append(f'mpcurses: Started:{start_time}')
Expand All @@ -120,8 +86,6 @@ def __init__(self, *args, **kwargs):
if self.blink_screen:
self.blink_queue = Queue()

self.completed_processes = 0

def start_blink_process(self):
""" start blink process
"""
Expand All @@ -140,20 +104,26 @@ def stop_blink_process(self):
self.blink_process.terminate()
logger.debug('terminated blink process')

def on_state_change(self, process_completed=True):
def on_start_process(self):
""" override base class method - call on_state_change
"""
self.on_state_change()

def on_complete_process(self):
""" override base class method - call on_state_change
"""
self.on_state_change()

def on_state_change(self):
""" update screen on state change
"""
if not self.screen:
return

if process_completed:
self.completed_processes += 1

update_screen_status(
self.screen,
'process-update',
self.screen_layout['_screen'],
running=len(self.active_processes),
running=self.active_processes,
queued=self.process_queue.qsize(),
completed=self.completed_processes)

Expand Down
61 changes: 1 addition & 60 deletions src/unittest/python/test_mpcurses.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

from mpcurses.mpcurses import MPcurses
from mpmq.mpmq import NoActiveProcesses
from mpcurses.mpcurses import OnDict
from mpmq.handler import queue_handler

import sys
Expand Down Expand Up @@ -129,22 +128,12 @@ def test__on_state_change_Should_CallExpected_When_Called(self, update_screen_st
client.on_state_change()
update_screen_status_patch.assert_called()

@patch('mpcurses.mpcurses.update_screen_status')
def test__on_state_change_Should_CallExpected_When_NoProcessCompleted(self, update_screen_status_patch, *patches):
screen_mock = Mock()
function_mock = Mock(__name__='mockfunc')
process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}]
client = MPcurses(function=function_mock, process_data=process_data, screen_layout={'_screen': {}})
client.screen = screen_mock
client.on_state_change(process_completed=False)
update_screen_status_patch.assert_called()

@patch('mpcurses.mpcurses.update_screen')
def test__on_state_change_Should_CallExpected_When_NoScreen(self, update_screen_patch, *patches):
function_mock = Mock(__name__='mockfunc')
process_data = [{'range': '0-1'}, {'range': '2-3'}, {'range': '4-5'}]
client = MPcurses(function=function_mock, process_data=process_data)
client.on_state_change(process_completed=False)
client.on_state_change()
update_screen_patch.assert_not_called()

@patch('mpcurses.mpcurses.update_screen_status')
Expand Down Expand Up @@ -393,51 +382,3 @@ def test__execute_run_Should_CallExpected_When_ScreenLayout(self, wrapper_patch,
client = MPcurses(function=function_mock, process_data=process_data, screen_layout={'_screen': {'blink': False}})
client.execute_run()
wrapper_patch.assert_called_once_with(run_screen_patch)


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()

0 comments on commit 1ca223b

Please sign in to comment.