feat: add basic support for progress indicators
This commit is contained in:
parent
110ff69d6d
commit
4b9db13b8f
|
@ -20,5 +20,6 @@
|
|||
enum
|
||||
exc
|
||||
people
|
||||
progress
|
||||
testing
|
||||
util
|
||||
|
|
6
docs/api/wuttjamaican/progress.rst
Normal file
6
docs/api/wuttjamaican/progress.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttjamaican.progress``
|
||||
=========================
|
||||
|
||||
.. automodule:: wuttjamaican.progress
|
||||
:members:
|
|
@ -27,6 +27,7 @@ classifiers = [
|
|||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
'importlib-metadata; python_version < "3.10"',
|
||||
"progress",
|
||||
"python-configuration",
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
113
src/wuttjamaican/progress.py
Normal file
113
src/wuttjamaican/progress.py
Normal 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()
|
|
@ -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()
|
||||
|
|
|
@ -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
27
tests/test_progress.py
Normal 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()
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue