3
0
Fork 0

feat: add basic support for progress indicators

This commit is contained in:
Lance Edgar 2024-08-24 17:19:50 -05:00
parent 110ff69d6d
commit 4b9db13b8f
9 changed files with 248 additions and 1 deletions

View file

@ -20,5 +20,6 @@
enum
exc
people
progress
testing
util

View file

@ -0,0 +1,6 @@
``wuttjamaican.progress``
=========================
.. automodule:: wuttjamaican.progress
:members:

View file

@ -27,6 +27,7 @@ classifiers = [
requires-python = ">= 3.8"
dependencies = [
'importlib-metadata; python_version < "3.10"',
"progress",
"python-configuration",
]

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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()

View file

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

View file

@ -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

27
tests/test_progress.py Normal file
View file

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

View file

@ -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")