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 enum
exc exc
people people
progress
testing testing
util util

View file

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

View file

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

View file

@ -28,7 +28,9 @@ import importlib
import os import os
import warnings 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: class AppHandler:
@ -417,6 +419,18 @@ class AppHandler:
""" """
return make_uuid() 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): def get_session(self, obj):
""" """
Returns the SQLAlchemy session with which the given object is 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("'"): elif value.startswith("'") and value.endswith("'"):
values[i] = value[1:-1] values[i] = value[1:-1]
return values 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 import wuttjamaican.enum
from wuttjamaican import app from wuttjamaican import app
from wuttjamaican.progress import ProgressBase
from wuttjamaican.conf import WuttaConfig from wuttjamaican.conf import WuttaConfig
from wuttjamaican.util import UNSPECIFIED from wuttjamaican.util import UNSPECIFIED
@ -315,6 +316,19 @@ class TestAppHandler(TestCase):
uuid = self.app.make_uuid() uuid = self.app.make_uuid()
self.assertEqual(len(uuid), 32) 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): def test_get_session(self):
try: try:
import sqlalchemy as sa 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 import pytest
from wuttjamaican import util as mod from wuttjamaican import util as mod
from wuttjamaican.progress import ProgressBase
class A: pass class A: pass
@ -260,3 +261,19 @@ class TestMakeTitle(TestCase):
def test_basic(self): def test_basic(self):
text = mod.make_title('foo_bar') text = mod.make_title('foo_bar')
self.assertEqual(text, "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")