From 4b9db13b8ff8591f889efae395f070719f5cb5d7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 24 Aug 2024 17:19:50 -0500 Subject: [PATCH] feat: add basic support for progress indicators --- docs/api/wuttjamaican/index.rst | 1 + docs/api/wuttjamaican/progress.rst | 6 ++ pyproject.toml | 1 + src/wuttjamaican/app.py | 16 +++- src/wuttjamaican/progress.py | 113 +++++++++++++++++++++++++++++ src/wuttjamaican/util.py | 54 ++++++++++++++ tests/test_app.py | 14 ++++ tests/test_progress.py | 27 +++++++ tests/test_util.py | 17 +++++ 9 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 docs/api/wuttjamaican/progress.rst create mode 100644 src/wuttjamaican/progress.py create mode 100644 tests/test_progress.py diff --git a/docs/api/wuttjamaican/index.rst b/docs/api/wuttjamaican/index.rst index 70de342..91a1cf8 100644 --- a/docs/api/wuttjamaican/index.rst +++ b/docs/api/wuttjamaican/index.rst @@ -20,5 +20,6 @@ enum exc people + progress testing util diff --git a/docs/api/wuttjamaican/progress.rst b/docs/api/wuttjamaican/progress.rst new file mode 100644 index 0000000..7a14cb3 --- /dev/null +++ b/docs/api/wuttjamaican/progress.rst @@ -0,0 +1,6 @@ + +``wuttjamaican.progress`` +========================= + +.. automodule:: wuttjamaican.progress + :members: diff --git a/pyproject.toml b/pyproject.toml index 01fa468..815cac1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ requires-python = ">= 3.8" dependencies = [ 'importlib-metadata; python_version < "3.10"', + "progress", "python-configuration", ] diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 35cb332..f2b6479 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -28,7 +28,9 @@ import importlib import os import warnings -from wuttjamaican.util import load_entry_points, load_object, make_title, make_uuid, parse_bool +from wuttjamaican.util import (load_entry_points, load_object, + make_title, make_uuid, parse_bool, + progress_loop) class AppHandler: @@ -417,6 +419,18 @@ class AppHandler: """ return make_uuid() + def progress_loop(self, *args, **kwargs): + """ + Convenience method to iterate over a set of items, invoking + logic for each, and updating a progress indicator along the + way. + + This is a wrapper around + :func:`wuttjamaican.util.progress_loop()`; see those docs for + param details. + """ + return progress_loop(*args, **kwargs) + def get_session(self, obj): """ Returns the SQLAlchemy session with which the given object is diff --git a/src/wuttjamaican/progress.py b/src/wuttjamaican/progress.py new file mode 100644 index 0000000..712675c --- /dev/null +++ b/src/wuttjamaican/progress.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttJamaican -- Base package for Wutta Framework +# Copyright © 2023-2024 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Progress Indicators +""" + +import sys + +from progress.bar import Bar + + +class ProgressBase: + """ + Base class for progress indicators. + + This is *only* a base class, and should not be used directly. For + simple console use, see :class:`ConsoleProgress`. + + Progress indicators are created via factory from various places in + the code. The factory is called with ``(message, maximum)`` args + and it must return a progress instance with these methods: + + * :meth:`update()` + * :meth:`finish()` + + Code may call ``update()`` several times while its operation + continues; it then ultimately should call ``finish()``. + + See also :func:`wuttjamaican.util.progress_loop()` and + :meth:`wuttjamaican.app.AppHandler.progress_loop()` for a way to + do these things automatically from code. + + :param message: Info message to be displayed along with the + progress bar. + + :param maximum: Max progress value. + """ + + def __init__(self, message, maximum): + self.message = message + self.maximum = maximum + + def update(self, value): + """ + Update the current progress value. + + :param value: New progress value to be displayed. + """ + + def finish(self): + """ + Wrap things up for the progress display etc. + """ + + +class ConsoleProgress(ProgressBase): + """ + Provides a console-based progress bar. + + This is a subclass of :class:`ProgressBase`. + + Simple usage is like:: + + from wuttjamaican.progress import ConsoleProgress + + def action(obj, i): + print(obj) + + items = [1, 2, 3, 4, 5] + + app = config.get_app() + app.progress_loop(action, items, ConsoleProgress, + message="printing items") + + See also :func:`~wuttjamaican.util.progress_loop()`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args) + + self.stderr = kwargs.get('stderr', sys.stderr) + self.stderr.write(f"\n{self.message}...\n") + + self.bar = Bar(message='', max=self.maximum, width=70, + suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds') + + def update(self, value): + """ """ + self.bar.next() + + def finish(self): + """ """ + self.bar.finish() diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index 51bdb03..d64270d 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -210,3 +210,57 @@ def parse_list(value): elif value.startswith("'") and value.endswith("'"): values[i] = value[1:-1] return values + + +def progress_loop(func, items, factory, message=None): + """ + Convenience function to iterate over a set of items, invoking + logic for each, and updating a progress indicator along the way. + + This function may also be called via the :term:`app handler`; see + :meth:`~wuttjamaican.app.AppHandler.progress_loop()`. + + The ``factory`` will be called to create the progress indicator, + which should be an instance of + :class:`~wuttjamaican.progress.ProgressBase`. + + The ``factory`` may also be ``None`` in which case there is no + progress, and this is really just a simple "for loop". + + :param func: Callable to be invoked for each item in the sequence. + See below for more details. + + :param items: Sequence of items over which to iterate. + + :param factory: Callable which creates/returns a progress + indicator, or can be ``None`` for no progress. + + :param message: Message to display along with the progress + indicator. If no message is specified, whether a default is + shown will be up to the progress indicator. + + The ``func`` param should be a callable which accepts 2 positional + args ``(obj, i)`` - meaning for which is as follows: + + :param obj: This will be an item within the sequence. + + :param i: This will be the *one-based* sequence number for the + item. + + See also :class:`~wuttjamaican.progress.ConsoleProgress` for a + usage example. + """ + progress = None + if factory: + count = len(items) + progress = factory(message, count) + + for i, item in enumerate(items, 1): + + func(item, i) + + if progress: + progress.update(i) + + if progress: + progress.finish() diff --git a/tests/test_app.py b/tests/test_app.py index 2d9d716..db9ef32 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -12,6 +12,7 @@ import pytest import wuttjamaican.enum from wuttjamaican import app +from wuttjamaican.progress import ProgressBase from wuttjamaican.conf import WuttaConfig from wuttjamaican.util import UNSPECIFIED @@ -315,6 +316,19 @@ class TestAppHandler(TestCase): uuid = self.app.make_uuid() self.assertEqual(len(uuid), 32) + def test_progress_loop(self): + + def act(obj, i): + pass + + # with progress + self.app.progress_loop(act, [1, 2, 3], ProgressBase, + message="whatever") + + # without progress + self.app.progress_loop(act, [1, 2, 3], None, + message="whatever") + def test_get_session(self): try: import sqlalchemy as sa diff --git a/tests/test_progress.py b/tests/test_progress.py new file mode 100644 index 0000000..16a6787 --- /dev/null +++ b/tests/test_progress.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8; -*- + +from unittest import TestCase + +from wuttjamaican import progress as mod + + +class TestProgressBase(TestCase): + + def test_basic(self): + + # sanity / coverage check + prog = mod.ProgressBase('testing', 2) + prog.update(1) + prog.update(2) + prog.finish() + + +class TestConsoleProgress(TestCase): + + def test_basic(self): + + # sanity / coverage check + prog = mod.ConsoleProgress('testing', 2) + prog.update(1) + prog.update(2) + prog.finish() diff --git a/tests/test_util.py b/tests/test_util.py index 0f2baf4..2e732ca 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -7,6 +7,7 @@ from unittest.mock import patch, MagicMock import pytest from wuttjamaican import util as mod +from wuttjamaican.progress import ProgressBase class A: pass @@ -260,3 +261,19 @@ class TestMakeTitle(TestCase): def test_basic(self): text = mod.make_title('foo_bar') self.assertEqual(text, "Foo Bar") + + +class TestProgressLoop(TestCase): + + def test_basic(self): + + def act(obj, i): + pass + + # with progress + mod.progress_loop(act, [1, 2, 3], ProgressBase, + message="whatever") + + # without progress + mod.progress_loop(act, [1, 2, 3], None, + message="whatever")