[med-svn] [Git][med-team/enlighten][upstream] New upstream version 1.6.0
Andreas Tille
gitlab at salsa.debian.org
Mon Jul 13 13:40:45 BST 2020
Andreas Tille pushed to branch upstream at Debian Med / enlighten
Commits:
d1545d97 by Andreas Tille at 2020-07-13T14:09:30+02:00
New upstream version 1.6.0
- - - - -
27 changed files:
- + .github/ISSUE_TEMPLATE/bug_report.md
- + .github/ISSUE_TEMPLATE/feature_request.md
- README.rst
- doc/_static/demo.gif
- doc/api.rst
- doc/examples.rst
- doc/index.rst
- doc/patterns.rst
- enlighten/__init__.py
- + enlighten/_basecounter.py
- enlighten/_counter.py
- enlighten/_manager.py
- + enlighten/_statusbar.py
- + enlighten/_util.py
- enlighten/counter.py
- examples/demo.py
- examples/multicolored.py
- examples/multiple_logging.py
- pylintrc
- setup.py
- tests/__init__.py
- + tests/test_basecounter.py
- tests/test_counter.py
- tests/test_manager.py
- + tests/test_statusbar.py
- + tests/test_util.py
- tox.ini
Changes:
=====================================
.github/ISSUE_TEMPLATE/bug_report.md
=====================================
@@ -0,0 +1,23 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+It's best to provide a generic code sample that illustrates the problem. Examples from the documentation are a good starting point.
+
+**Environment (please complete the following information):**
+ - Enlighten Version:
+ - OS and version:
+ - Console application: [e.g. xterm, cmd, VS Code Terminal]
+ - Special Conditions: [e.g. Running under pyinstaller]
+
+**Additional context**
+Add any other context about the problem here.
=====================================
.github/ISSUE_TEMPLATE/feature_request.md
=====================================
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: Feature Request
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
=====================================
README.rst
=====================================
@@ -64,6 +64,12 @@ The main advantage of Enlighten is it allows writing to stdout and stderr withou
redirection.
.. image:: https://raw.githubusercontent.com/Rockhopper-Technologies/enlighten/master/doc/_static/demo.gif
+ :target: http://python-enlighten.readthedocs.io/en/stable/examples.html
+
+The code for this animation can be found in
+`demo.py <https://github.com/Rockhopper-Technologies/enlighten/blob/master/examples/demo.py>`__
+in
+`examples <https://github.com/Rockhopper-Technologies/enlighten/tree/master/examples>`__.
Documentation
=============
=====================================
doc/_static/demo.gif
=====================================
Binary files a/doc/_static/demo.gif and b/doc/_static/demo.gif differ
=====================================
doc/api.rst
=====================================
@@ -24,6 +24,11 @@ Classes
:inherited-members:
:exclude-members: elapsed, position
+.. autoclass:: StatusBar
+ :members:
+ :inherited-members:
+ :exclude-members: elapsed, position
+
.. autoclass:: SubCounter
:members:
@@ -31,3 +36,9 @@ Functions
---------
.. autofunction:: enlighten.get_manager(stream=None, counter_class=Counter, **kwargs)
+
+
+Constants
+---------
+
+.. autoclass:: enlighten.Justify
\ No newline at end of file
=====================================
doc/examples.rst
=====================================
@@ -72,11 +72,55 @@ total. If neither of these conditions are met, the counter format is used:
time.sleep(0.1) # Simulate work
counter.update()
+Status Bars
+-----------
+Status bars are bars that work similarly to progress similarly to progress bars and counters,
+but present relatively static information.
+Status bars are created with :py:meth:`Manager.status_bar <enlighten.Manager.status_bar>`.
+
+.. code-block:: python
+
+ import enlighten
+ import time
+
+ manager = enlighten.get_manager()
+ status_bar = manager.status_bar('Static Message',
+ color='white_on_red',
+ justify=enlighten.Justify.CENTER)
+ time.sleep(1)
+ status_bar.update('Updated static message')
+ time.sleep(1)
+
+Status bars can also use formatting with dynamic variables.
+
+.. code-block:: python
+
+ import enlighten
+ import time
+
+ manager = enlighten.get_manager()
+ status_format = '{program}{fill}Stage: {stage}{fill} Status {status}'
+ status_bar = manager.status_bar(status_format=status_format,
+ color='bold_slategray',
+ program='Demo',
+ stage='Loading',
+ status='OKAY')
+ time.sleep(1)
+ status_bar.update(stage='Initializing', status='OKAY')
+ time.sleep(1)
+ status_bar.update(status='FAIL')
+
+Status bars, like other bars can be pinned. To pin a status bar to the top of all other bars,
+initialize it before any other bars. To pin a bar to the bottom of the screen, use
+``position=1`` when initializing.
+
+
Color
-----
-The bar component of a progress bar can be colored by setting the ``color`` keyword argument.
-See :ref:`Series Color <series_color>` for more information about valid colors.
+Status bars and the bar component of a progress bar can be colored by setting the
+``color`` keyword argument. See :ref:`Series Color <series_color>` for more information
+about valid colors.
.. code-block:: python
@@ -128,12 +172,6 @@ of the underlying `Blessed <https://blessed.readthedocs.io/en/stable>`_
bar_format = manager.term.red(u'{desc}') + u'{desc_pad}' + \
manager.term.blue(u'{percentage:3.0f}%') + u'|{bar}|'
- # Change the background of only the bar
- bar_format = u'{desc}{desc_pad}{percentage:3.0f}%|' + \
- manager.term.on_white(u'{bar}') + \
- u'| {count:{len_total}d}/{total:d} ' + \
- u'[{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
-
# Apply to counter
ticks = manager.counter(total=100, desc='Ticks', unit='ticks', bar_format=bar_format)
=====================================
doc/index.rst
=====================================
@@ -25,3 +25,9 @@ The main advantage of Enlighten is it allows writing to stdout and stderr withou
redirection.
.. image:: _static/demo.gif
+ :target: examples.html
+
+The code for this animation can be found in
+`demo.py <https://github.com/Rockhopper-Technologies/enlighten/blob/master/examples/demo.py>`__
+in
+`examples <https://github.com/Rockhopper-Technologies/enlighten/tree/master/examples>`__.
\ No newline at end of file
=====================================
doc/patterns.rst
=====================================
@@ -84,4 +84,35 @@ the iterables and then updates the count by 1.
for sheep in pbar(flock1, flock2):
time.sleep(0.2)
- print('%s: Baaa' % sheep)
\ No newline at end of file
+ print('%s: Baaa' % sheep)
+
+User-defined fields
+-------------------
+
+Both :py:class:`~enlighten.Counter` and Both :py:class:`~enlighten.StatusBar` accept
+user defined fields as keyword arguments at initialization and during an update.
+These fields are persistent and only need to be specified when they change.
+
+In the following example, ``source`` is a user-defined field that is periodically updated.
+
+.. code-block:: python
+
+ import enlighten
+ import random
+ import time
+
+ bar_format = u'{desc}{desc_pad}{source} {percentage:3.0f}%|{bar}| ' + \
+ u'{count:{len_total}d}/{total:d} ' + \
+ u'[{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
+ manager = enlighten.get_manager(bar_format=bar_format)
+
+ bar = manager.counter(total=100, desc='Loading', unit='files', source='server.a')
+ for num in range(100):
+ time.sleep(0.1) # Simulate work
+ if not num % 5:
+ bar.update(source=random.choice(['server.a', 'server.b', 'server.c']))
+ else:
+ bar.update()
+
+For more information, see the :ref:`Counter Format <counter_format>` and
+:ref:`StatusBar Format <status_format>` sections.
\ No newline at end of file
=====================================
enlighten/__init__.py
=====================================
@@ -11,9 +11,10 @@
Provides progress bars and counters which play nice in a TTY console
"""
-from enlighten.counter import Counter, SubCounter
+from enlighten.counter import Counter, StatusBar, SubCounter
from enlighten._manager import Manager, get_manager
+from enlighten._util import Justify
-__version__ = '1.5.2'
-__all__ = ('Counter', 'Manager', 'SubCounter', 'get_manager')
+__version__ = '1.6.0'
+__all__ = ('Counter', 'Justify', 'Manager', 'StatusBar', 'SubCounter', 'get_manager')
=====================================
enlighten/_basecounter.py
=====================================
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+**Enlighten base counter submodule**
+
+Provides BaseCounter and PrintableCounter classes
+"""
+
+import time
+
+from enlighten._util import BASESTRING
+
+try:
+ from collections.abc import Iterable
+except ImportError: # pragma: no cover(Python 2)
+ from collections import Iterable
+
+
+class BaseCounter(object):
+ """
+ Args:
+ manager(:py:class:`Manager`): Manager instance. Required.
+ color(str): Color as a string or RGB tuple (Default: None)
+
+ Base class for counters
+ """
+
+ __slots__ = ('_color', 'count', 'manager', 'start_count')
+ _repr_attrs = ('count', 'color')
+
+ def __repr__(self):
+
+ params = []
+ for attr in self._repr_attrs:
+ value = getattr(self, attr)
+ if value is not None:
+ params.append('%s=%r' % (attr, value))
+
+ return '%s(%s)' % (self.__class__.__name__, ', '.join(params))
+
+ def __init__(self, **kwargs):
+
+ self.count = self.start_count = kwargs.get('count', 0)
+ self._color = None
+
+ self.manager = kwargs.get('manager', None)
+ if self.manager is None:
+ raise TypeError('manager must be specified')
+
+ self.color = kwargs.get('color', None)
+
+ @property
+ def color(self):
+ """
+ Color property
+
+ Preferred to be a string or iterable of three integers for RGB.
+ Single integer supported for backwards compatibility
+ """
+
+ color = self._color
+ return color if color is None else color[0]
+
+ @color.setter
+ def color(self, value):
+
+ if value is None:
+ self._color = None
+ elif isinstance(value, int) and 0 <= value <= 255:
+ self._color = (value, self.manager.term.color(value))
+ elif isinstance(value, BASESTRING):
+ term = self.manager.term
+ color_cap = self.manager.term.formatter(value)
+ if not color_cap and term.does_styling and term.number_of_colors:
+ raise AttributeError('Invalid color specified: %s' % value)
+ self._color = (value, color_cap)
+ elif isinstance(value, Iterable) and \
+ len(value) == 3 and \
+ all(isinstance(_, int) and 0 <= _ <= 255 for _ in value):
+ self._color = (value, self.manager.term.color_rgb(*value))
+ else:
+ raise AttributeError('Invalid color specified: %s' % repr(value))
+
+ def _colorize(self, content):
+ """
+ Args:
+ content(str): Color as a string or number 0 - 255 (Default: None)
+
+ Returns:
+ :py:class:`str`: content formatted with color
+
+ Format ``content`` with the color specified for this progress bar
+
+ If no color is specified for this instance, the content is returned unmodified
+ """
+
+ # No color specified
+ if self._color is None:
+ return content
+
+ # Used spec cached by color.setter
+ return self._color[1](content)
+
+ def update(self, *args, **kwargs):
+ """
+ Placeholder for update method
+ """
+
+ raise NotImplementedError
+
+ def __call__(self, *args):
+
+ for iterable in args:
+ if not isinstance(iterable, Iterable):
+ raise TypeError('Argument type %s is not iterable' % type(iterable).__name__)
+
+ for element in iterable:
+ yield element
+ self.update()
+
+
+class PrintableCounter(BaseCounter):
+ """
+ Base class for printable counters
+ """
+
+ __slots__ = ('enabled', '_fill', 'last_update', 'leave', 'min_delta', '_pinned', 'start')
+
+ def __init__(self, **kwargs):
+
+ super(PrintableCounter, self).__init__(**kwargs)
+
+ self.enabled = kwargs.get('enabled', True)
+ self._fill = u' '
+ self.fill = kwargs.get('fill', u' ')
+ self.leave = kwargs.get('leave', True)
+ self.min_delta = kwargs.get('min_delta', 0.1)
+ self._pinned = False
+ self.last_update = self.start = time.time()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+ @property
+ def elapsed(self):
+ """
+ Get elapsed time is seconds (float)
+ """
+
+ return time.time() - self.start
+
+ @property
+ def fill(self):
+ """
+ Fill character used in formatting
+ """
+ return self._fill
+
+ @fill.setter
+ def fill(self, value):
+
+ char_len = self.manager.term.length(value)
+ if char_len != 1:
+ raise ValueError('fill character must be a length of 1 '
+ 'when printed. Length: %d, Value given: %r' % (char_len, value))
+
+ self._fill = value
+
+ @property
+ def position(self):
+ """
+ Fetch position from the manager
+ """
+
+ return self.manager.counters.get(self, 0)
+
+ def clear(self, flush=True):
+ """
+ Args:
+ flush(bool): Flush stream after clearing bar (Default:True)
+
+ Clear bar
+ """
+
+ if self.enabled:
+ self.manager.write(flush=flush, counter=self)
+ self.last_update = 0
+
+ def close(self, clear=False):
+ """
+ Do final refresh and remove from manager
+
+ If ``leave`` is True, the default, the effect is the same as :py:meth:`refresh`.
+ """
+
+ if clear and not self.leave:
+ self.clear()
+ else:
+ self.refresh()
+
+ self.manager.remove(self)
+
+ def format(self, width=None, elapsed=None):
+ """
+ Format counter for printing
+ """
+
+ raise NotImplementedError
+
+ def refresh(self, flush=True, elapsed=None):
+ """
+ Args:
+ flush(bool): Flush stream after writing bar (Default:True)
+ elapsed(float): Time since started. Automatically determined if :py:data:`None`
+
+ Redraw bar
+ """
+
+ if self.enabled:
+ self.last_update = time.time()
+ self.manager.write(output=self.format(elapsed=elapsed),
+ flush=flush, counter=self)
+
+ def _fill_text(self, text, width, offset=None):
+ """
+ Args:
+ text (str): String to modify
+ width (int): Width in columns to make progress bar
+ offset(int): Number of non-printable characters to account for when formatting
+
+ Returns:
+ :py:class:`str`: String with ``'{0}'`` replaced with fill characters
+
+ Replace ``'{0}'`` in string with appropriate number of fill characters
+ """
+
+ fill_count = text.count(u'{0}')
+ if not fill_count:
+ return text
+
+ if offset is None:
+ remaining = width - self.manager.term.length(text) + 3 * fill_count
+ else:
+ remaining = width - len(text) + offset + 3 * fill_count
+
+ if fill_count == 1:
+ return text.format(self.fill * remaining)
+
+ fill_size, extra = divmod(remaining, fill_count)
+
+ # Add extra fill evenly starting from the last one
+ text = '{1}'.join(text.rsplit('{0}', extra))
+ return text.format(self.fill * fill_size,
+ self.fill * (fill_size + 1))
=====================================
enlighten/_counter.py
=====================================
@@ -8,19 +8,15 @@
"""
**Enlighten counter submodule**
-Provides Counter base class
+Provides Counter and SubConter classes
"""
import platform
import sys
import time
-try:
- from collections.abc import Iterable
-except ImportError: # pragma: no cover(Python 2)
- from collections import Iterable
-
-from blessed.colorspace import X11_COLORNAMES_TO_RGB
+from enlighten._basecounter import BaseCounter, PrintableCounter
+from enlighten._util import format_time
COUNTER_FMT = u'{desc}{desc_pad}{count:d} {unit}{unit_pad}' + \
u'[{elapsed}, {rate:.2f}{unit_pad}{unit}/s]{fill}'
@@ -28,6 +24,8 @@ COUNTER_FMT = u'{desc}{desc_pad}{count:d} {unit}{unit_pad}' + \
BAR_FMT = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| {count:{len_total}d}/{total:d} ' + \
u'[{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
+STATUS_FMT = u'{message}'
+
# Even with cp65001, Windows doesn't seem to support all unicode characters
if platform.system() == 'Windows': # pragma: no cover(Windows)
SERIES_STD = u' ▌█'
@@ -42,141 +40,6 @@ except UnicodeEncodeError: # pragma: no cover(Non-unicode Terminal)
except (AttributeError, TypeError): # pragma: no cover(Non-standard Terminal)
pass
-COLORS_16 = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
- 'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
- 'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white')
-
-try:
- BASESTRING = basestring
-except NameError:
- BASESTRING = str
-
-
-def _format_time(seconds):
- """
- Args:
- seconds (float): amount of time
-
- Format time string for eta and elapsed
- """
-
- # Always do minutes and seconds in mm:ss format
- minutes = seconds // 60
- hours = minutes // 60
- rtn = u'{0:02.0f}:{1:02.0f}'.format(minutes % 60, seconds % 60)
-
- # Add hours if there are any
- if hours:
-
- rtn = u'{0:d}h {1}'.format(int(hours % 24), rtn)
-
- # Add days if there are any
- days = int(hours // 24)
- if days:
- rtn = u'{0:d}d {1}'.format(days, rtn)
-
- return rtn
-
-
-class BaseCounter(object):
- """
- Args:
- manager(:py:class:`Manager`): Manager instance. Required.
- color(str): Color as a string or RGB tuple (Default: None)
-
- Base class for counters
- """
-
- __slots__ = ('_color', 'count', 'manager', 'start_count')
- _repr_attrs = ('count', 'color')
-
- def __repr__(self):
-
- params = []
- for attr in self._repr_attrs:
- value = getattr(self, attr)
- if value is not None:
- params.append('%s=%r' % (attr, value))
-
- return '%s(%s)' % (self.__class__.__name__, ', '.join(params))
-
- def __init__(self, **kwargs):
-
- self.count = self.start_count = kwargs.get('count', 0)
- self._color = None
-
- self.manager = kwargs.get('manager', None)
- if self.manager is None:
- raise TypeError('manager must be specified')
-
- self.color = kwargs.get('color', None)
-
- @property
- def color(self):
- """
- Color property
- Preferred to be a string or iterable of three integers for RGB
- Single integer supported for backwards compatibility
- """
-
- color = self._color
- return color if color is None else color[0]
-
- @color.setter
- def color(self, value):
-
- if value is None:
- self._color = None
- elif isinstance(value, int) and 0 <= value <= 255:
- self._color = (value, self.manager.term.color(value))
- elif isinstance(value, BASESTRING):
- if value not in COLORS_16 or value not in X11_COLORNAMES_TO_RGB:
- raise AttributeError('Invalid color specified: %s' % value)
- self._color = (value, getattr(self.manager.term, value))
- elif isinstance(value, Iterable) and \
- len(value) == 3 and \
- all(isinstance(_, int) and 0 <= _ <= 255 for _ in value):
- self._color = (value, self.manager.term.color_rgb(*value))
- else:
- raise AttributeError('Invalid color specified: %s' % repr(value))
-
- def _colorize(self, content):
- """
- Args:
- content(str): Color as a string or number 0 - 255 (Default: None)
-
- Returns:
- :py:class:`str`: content formatted with color
-
- Format ``content`` with the color specified for this progress bar
-
- If no color is specified for this instance, the content is returned unmodified
- """
-
- # No color specified
- if self._color is None:
- return content
-
- # Used spec cached by color.setter
- return self._color[1](content)
-
- def update(self, incr=1, force=False):
- """
- Placeholder for update method
- """
-
- raise NotImplementedError
-
- def __call__(self, *args):
-
- for iterable in args:
- if not isinstance(iterable, Iterable):
- raise TypeError('Argument type %s is not iterable' % type(iterable).__name__)
-
- for element in iterable:
- yield element
- self.update()
-
class SubCounter(BaseCounter):
"""
@@ -216,7 +79,7 @@ class SubCounter(BaseCounter):
self.parent = parent
self.all_fields = all_fields
- def update(self, incr=1, force=False):
+ def update(self, incr=1, force=False): # pylint: disable=arguments-differ
"""
Args:
incr(int): Amount to increment ``count`` (Default: 1)
@@ -267,20 +130,23 @@ class SubCounter(BaseCounter):
raise ValueError('source must be parent or peer')
-class Counter(BaseCounter):
+class Counter(PrintableCounter):
"""
.. spelling::
desc
len
+ seagreen
+ peru
Args:
- additional_fields(dict): Additional fields used for :ref:`formating <counter_format>`
bar_format(str): Progress bar format, see :ref:`Format <counter_format>` below
count(int): Initial count (Default: 0)
counter_format(str): Counter format, see :ref:`Format <counter_format>` below
color(str): Series color as a string or RGB tuple see :ref:`Series Color <series_color>`
desc(str): Description
enabled(bool): Status (Default: :py:data:`True`)
+ fill(str): Fill character used for ``counter_format`` (Default: ' ')
+ fields(dict): Additional fields used for :ref:`formatting <counter_format>`
leave(True): Leave progress bar after closing (Default: :py:data:`True`)
manager(:py:class:`Manager`): Manager instance. Creates instance if not specified.
min_delta(float): Minimum time, in seconds, between refreshes (Default: 0.1)
@@ -350,6 +216,9 @@ class Counter(BaseCounter):
For backward compatibility, a color can be expressed as an integer 0 - 255, but this
is deprecated in favor of named or RGB colors.
+ Compound colors, such as 'white_on_seagreen', 'bold_red', or 'underline_on_peru' are
+ also supported.
+
If a terminal is not capable of 24-bit color, and is given a color outside of its
range, the color will be downconverted to a supported color.
@@ -427,9 +296,10 @@ class Counter(BaseCounter):
- percentage(:py:class:`float`) - Percentage complete
- total(:py:class:`int`) - Value of ``total``
- Addition fields for ``counter_format`` only:
+ Additional fields for ``counter_format`` only:
- - fill(:py:class:`str`) - blank spaces, number needed to fill line
+ - fill(:py:class:`str`) - Filled with :py:attr:`fill` until line is width of terminal.
+ May be used multiple times. Minimum width is 3.
Additional fields when subcounters are used:
@@ -453,17 +323,22 @@ class Counter(BaseCounter):
User-defined fields:
- The ``additional_fields`` parameter can be used to pass a dictionary of additional
+ Users can define fields in two ways, the ``fields`` parameter and by passing keyword
+ arguments to :py:meth:`Manager.counter` or :py:meth:`Counter.update`
+
+ The ``fields`` parameter can be used to pass a dictionary of additional
user-defined fields. The dictionary values can be updated after initialization to allow
for dynamic fields. Any fields that share names with built-in fields are ignored.
+ If fields are passed as keyword arguments to :py:meth:`Manager.counter` or
+ :py:meth:`Counter.update`, they take precedent over the ``fields`` parameter.
.. _counter_offset:
**Offset**
When ``offset`` is :py:data:`None`, the width of the bar portion of the progress bar and
- the fill characters for counter will be automatically determined,
+ the fill size for counter will be automatically determined,
taking into account terminal escape sequences that may be included in the string.
Under special circumstances, and to permit backward compatibility, ``offset`` may be
@@ -509,9 +384,8 @@ class Counter(BaseCounter):
"""
# pylint: disable=too-many-instance-attributes
- __slots__ = ('additional_fields', 'bar_format', 'counter_format', 'desc', 'enabled',
- 'last_update', 'leave', 'manager', 'min_delta', 'offset', 'series', 'start',
- 'total', 'unit', '_subcounters')
+ __slots__ = ('bar_format', 'counter_format', 'desc', 'fields', 'manager',
+ 'offset', 'series', 'total', 'unit', '_fields', '_subcounters')
_repr_attrs = ('desc', 'total', 'count', 'unit', 'color')
# pylint: disable=too-many-arguments
@@ -519,35 +393,18 @@ class Counter(BaseCounter):
super(Counter, self).__init__(**kwargs)
- self.additional_fields = kwargs.get('additional_fields', {})
- self.bar_format = kwargs.get('bar_format', BAR_FMT)
- self.counter_format = kwargs.get('counter_format', COUNTER_FMT)
- self.desc = kwargs.get('desc', None)
- self.enabled = kwargs.get('enabled', True)
- self.leave = kwargs.get('leave', True)
- self.min_delta = kwargs.get('min_delta', 0.1)
- self.offset = kwargs.get('offset', None)
- self.series = kwargs.get('series', SERIES_STD)
- self.total = kwargs.get('total', None)
- self.unit = kwargs.get('unit', None)
+ # Accept additional_fields for backwards compatibility
+ self.fields = kwargs.pop('fields', kwargs.pop('additional_fields', {}))
+ self.bar_format = kwargs.pop('bar_format', BAR_FMT)
+ self.counter_format = kwargs.pop('counter_format', COUNTER_FMT)
+ self.desc = kwargs.pop('desc', None)
+ self.offset = kwargs.pop('offset', None)
+ self.series = kwargs.pop('series', SERIES_STD)
+ self.total = kwargs.pop('total', None)
+ self.unit = kwargs.pop('unit', None)
+ self._fields = kwargs
self._subcounters = []
- self.last_update = self.start = time.time()
-
- def __enter__(self):
- return self
-
- def __exit__(self, *args):
- self.close()
-
- @property
- def position(self):
- """
- Fetch position from the manager
- """
-
- return self.manager.counters.get(self, 0)
-
@property
def elapsed(self):
"""
@@ -570,31 +427,6 @@ class Counter(BaseCounter):
return sum(subcounter.count for subcounter in self._subcounters)
- def clear(self, flush=True):
- """
- Args:
- flush(bool): Flush stream after clearing progress bar (Default:True)
-
- Clear progress bar
- """
-
- if self.enabled:
- self.manager.write(flush=flush, position=self.position)
-
- def close(self, clear=False):
- """
- Do final refresh and remove from manager
-
- If ``leave`` is True, the default, the effect is the same as :py:meth:`refresh`.
- """
-
- if clear and not self.leave:
- self.clear()
- else:
- self.refresh()
-
- self.manager.remove(self)
-
def _get_subcounters(self, elapsed, bar_fields=True):
"""
Args:
@@ -645,7 +477,7 @@ class Counter(BaseCounter):
if self.total == 0:
fields['eta_{0}'.format(num)] = u'00:00'
elif rate:
- fields['eta_{0}'.format(num)] = _format_time((self.total - interations) / rate)
+ fields['eta_{0}'.format(num)] = format_time((self.total - interations) / rate)
else:
fields['eta_{0}'.format(num)] = u'?'
@@ -668,7 +500,8 @@ class Counter(BaseCounter):
iterations = abs(self.count - self.start_count)
- fields = self.additional_fields.copy()
+ fields = self.fields.copy()
+ fields.update(self._fields)
fields.update({'bar': u'{0}',
'count': self.count,
'desc': self.desc or u'',
@@ -681,7 +514,7 @@ class Counter(BaseCounter):
if elapsed is None:
elapsed = self.elapsed
- fields['elapsed'] = _format_time(elapsed)
+ fields['elapsed'] = format_time(elapsed)
# Get rate. Elapsed could be 0 if counter was not updated and has a zero total.
if elapsed:
@@ -707,7 +540,7 @@ class Counter(BaseCounter):
# Get eta
if fields['rate']:
# Use iterations so a counter running backwards is accurate
- fields['eta'] = _format_time((self.total - iterations) / fields['rate'])
+ fields['eta'] = format_time((self.total - iterations) / fields['rate'])
else:
fields['eta'] = u'?'
@@ -726,8 +559,7 @@ class Counter(BaseCounter):
try:
rtn = self.bar_format.format(**fields)
except KeyError as e:
- raise ValueError('%r specified in format, but not present in additional_fields' %
- e.args[0])
+ raise ValueError('%r specified in format, but not provided' % e.args[0])
# Format the bar
if self.offset is None:
@@ -767,35 +599,16 @@ class Counter(BaseCounter):
try:
rtn = self.counter_format.format(**fields)
except KeyError as e:
- raise ValueError('%r specified in format, but not present in additional_fields' %
- e.args[0])
-
- if self.offset is None:
- ret = rtn.format(u' ' * (width - self.manager.term.length(rtn) + 3))
- else:
- # Offset was explicitly given
- ret = rtn.format(u' ' * (width - len(rtn) + self.offset + 3))
+ raise ValueError('%r specified in format, but not provided' % e.args[0])
- return ret
-
- def refresh(self, flush=True, elapsed=None):
- """
- Args:
- flush(bool): Flush stream after writing progress bar (Default:True)
- elapsed(float): Time since started. Automatically determined if :py:data:`None`
-
- Redraw progress bar
- """
-
- if self.enabled:
- self.manager.write(output=self.format(elapsed=elapsed),
- flush=flush, position=self.position)
+ return self._fill_text(rtn, width, offset=self.offset)
- def update(self, incr=1, force=False):
+ def update(self, incr=1, force=False, **fields): # pylint: disable=arguments-differ
"""
Args:
incr(int): Amount to increment ``count`` (Default: 1)
force(bool): Force refresh even if ``min_delta`` has not been reached
+ fields(dict): Fields for for :ref:`formatting <counter_format>`
Increment progress bar and redraw
@@ -803,12 +616,12 @@ class Counter(BaseCounter):
"""
self.count += incr
+ self._fields.update(fields)
if self.enabled:
currentTime = time.time()
# Update if force, 100%, or minimum delta has been reached
if force or self.count == self.total or \
currentTime - self.last_update >= self.min_delta:
- self.last_update = currentTime
self.refresh(elapsed=currentTime - self.start)
def add_subcounter(self, color, count=0, all_fields=False):
=====================================
enlighten/_manager.py
=====================================
@@ -23,6 +23,7 @@ except ImportError: # pragma: no cover (Python 2.6)
from enlighten._counter import Counter
+from enlighten._statusbar import StatusBar
from enlighten._terminal import Terminal
@@ -72,9 +73,12 @@ class Manager(object):
self.stream = sys.stdout if stream is None else stream
self.counter_class = counter_class
+ self.status_bar_class = StatusBar
+
self.counters = OrderedDict()
self.enabled = kwargs.get('enabled', True) # Double duty for counters
- self.no_resize = kwargs.get('no_resize', False)
+ self.no_resize = kwargs.pop('no_resize', False)
+ self.set_scroll = kwargs.pop('set_scroll', True)
self.term = Terminal(stream=self.stream)
# Set up companion stream
@@ -99,12 +103,14 @@ class Manager(object):
else:
self.companion_term = None
- self.scroll_offset = 1
- self.process_exit = False
+ self.autorefresh = []
self.height = self.term.height
- self.width = self.term.width
- self.set_scroll = kwargs.pop('set_scroll', True)
+ self.process_exit = False
+ self.refresh_lock = False
self.resize_lock = False
+ self.scroll_offset = 1
+ self.width = self.term.width
+
if not self.no_resize and RESIZE_SUPPORTED:
self.sigwinch_orig = signal.getsignal(signal.SIGWINCH)
@@ -123,6 +129,7 @@ class Manager(object):
"""
Args:
position(int): Line number counting from the bottom of the screen
+ autorefresh(bool): Refresh this counter when other bars are drawn
kwargs(dict): Any additional :py:term:`keyword arguments<keyword argument>`
are passed to :py:class:`Counter`
@@ -131,43 +138,130 @@ class Manager(object):
Get a new progress bar instance
+ If ``position`` is specified, the counter's position will be pinned.
+ A :py:exc:`ValueError` will be raised if ``position`` exceeds the screen height or
+ has already been pinned by another counter.
+
+ If ``autorefresh`` is :py:data:`True`, this bar will be redrawn whenever another bar is
+ drawn assuming it had been ``min_delta`` seconds since the last update. This is usually
+ unnecessary.
+
+ .. note:: Counters are not automatically drawn when created because fields may be missing
+ if subcounters are used. To force the counter to draw before updating,
+ call :py:meth:`~Counter.refresh`.
+
+ """
+
+ return self._add_counter(self.counter_class, position=position, **kwargs)
+
+ def status_bar(self, *args, **kwargs):
+ """
+ Args:
+ position(int): Line number counting from the bottom of the screen
+ autorefresh(bool): Refresh this counter when other bars are drawn
+ kwargs(dict): Any additional :py:term:`keyword arguments<keyword argument>`
+ are passed to :py:class:`StatusBar`
+
+ Returns:
+ :py:class:`StatusBar`: Instance of status bar class
+
+ Get a new status bar instance
+
+ If ``position`` is specified, the counter's position can change dynamically if
+ additional counters are called without a ``position`` argument.
+
+ If ``autorefresh`` is :py:data:`True`, this bar will be redrawn whenever another bar is
+ drawn assuming it had been ``min_delta`` seconds since the last update. Generally,
+ only need when ``elapsed`` is used in :ref:`status_format <status_format>`.
+
+ """
+
+ position = kwargs.pop('position', None)
+
+ return self._add_counter(self.status_bar_class, *args, position=position, **kwargs)
+
+ def _add_counter(self, counter_class, *args, **kwargs):
+ """
+ Args:
+ counter_class(:py:class:`PrintableCounter`): Class to instantiate
+ position(int): Line number counting from the bottom of the screen
+ kwargs(dict): Any additional :py:term:`keyword arguments<keyword argument>`
+ are passed to :py:class:`Counter`
+
+ Returns:
+ :py:class:`Counter`: Instance of counter class
+
+ Get a new instance of the given class and add it to the manager
+
If ``position`` is specified, the counter's position can change dynamically if
additional counters are called without a ``position`` argument.
"""
+ position = kwargs.pop('position', None)
+
+ # List of counters to refresh due to new position
+ toRefresh = []
+
+ # Add default values to kwargs
for key, val in self.defaults.items():
if key not in kwargs:
kwargs[key] = val
kwargs['manager'] = self
- counter = self.counter_class(**kwargs)
-
- if position is None:
- toRefresh = []
- if self.counters:
- pos = 2
- for cter in reversed(self.counters):
- if self.counters[cter] < pos:
- toRefresh.append(cter)
- cter.clear(flush=False)
- self.counters[cter] = pos
- pos += 1
-
- self.counters[counter] = 1
- self._set_scroll_area()
- for cter in reversed(toRefresh):
- cter.refresh()
- self.stream.flush()
-
- elif position in self.counters.values():
- raise ValueError('Counter position %d is already occupied.' % position)
- elif position > self.height:
- raise ValueError('Counter position %d is greater than terminal height.' % position)
+ # Create counter
+ new = counter_class(*args, **kwargs)
+ if kwargs.pop('autorefresh', False):
+ self.autorefresh.append(new)
+
+ # Get pinned counters
+ # pylint: disable=protected-access
+ pinned = dict((pos, ctr) for ctr, pos in self.counters.items() if ctr._pinned)
+
+ # Check position
+ if position is not None:
+ if position in pinned:
+ raise ValueError('Counter position %d is already occupied.' % position)
+ if position > self.height:
+ raise ValueError('Counter position %d is greater than terminal height.' % position)
+ new._pinned = True # pylint: disable=protected-access
+ self.counters[new] = position
+ pinned[position] = new
+ if counter_class is self.status_bar_class:
+ toRefresh.append(new)
else:
- self.counters[counter] = position
+ # Set for now, but will change
+ self.counters[new] = 0
+
+ # Iterate through all counters in reverse order
+ pos = 1
+ for ctr in reversed(self.counters):
+
+ if ctr in pinned.values():
+ continue
+
+ old_pos = self.counters[ctr]
+
+ while pos in pinned:
+ pos += 1
+
+ if pos != old_pos:
- return counter
+ # Don't refresh new counter in case it will have subcounters
+ if ctr is not new or counter_class is self.status_bar_class:
+ ctr.clear(flush=False)
+ toRefresh.append(ctr)
+
+ self.counters[ctr] = pos
+
+ pos += 1
+
+ self._set_scroll_area()
+ for ctr in reversed(toRefresh):
+ ctr.refresh()
+ self.stream.flush()
+
+ return new
def _resize_handler(self, *args, **kwarg): # pylint: disable=unused-argument
"""
@@ -283,20 +377,21 @@ class Manager(object):
def remove(self, counter):
"""
Args:
- counter(:py:class:`Counter`): Progress bar instance
+ counter(:py:class:`Counter`): Progress bar or status bar instance
- Remove progress bar instance from manager
+ Remove bar instance from manager
Does not error if instance is not managed by this manager
Generally this method should not be called directly,
- instead used :py:meth:`remove`.
+ instead used :py:meth:`Counter.close`.
"""
if not counter.leave:
try:
del self.counters[counter]
- except KeyError:
+ self.autorefresh.remove(counter)
+ except (KeyError, ValueError):
pass
def stop(self):
@@ -350,16 +445,18 @@ class Manager(object):
if 1 in positions:
term.feed()
- def write(self, output='', flush=True, position=0):
+ def write(self, output='', flush=True, counter=None):
"""
Args:
- output(str: Output string
+ output(str): Output string
flush(bool): Flush the output stream after writing
- position(int): Position relative to the bottom of the screen to write output
+ counter(:py:class:`Counter`): Bar being written (for position and auto-refresh)
Write to stream at a given position
"""
+ position = self.counters[counter] if counter else 0
+
if self.enabled:
term = self.term
@@ -376,9 +473,37 @@ class Manager(object):
finally:
# Reset position and scrolling
- self._set_scroll_area()
- if flush:
- stream.flush()
+ if not self.refresh_lock:
+ if self.autorefresh:
+ self._autorefresh(exclude=(counter,))
+ self._set_scroll_area()
+ if flush:
+ stream.flush()
+
+ def _autorefresh(self, exclude):
+ """
+ Args:
+ exclude(list): Iterable of bars to ignore when auto-refreshing
+
+ Refresh any bars specified for auto-refresh
+ """
+
+ # Make sure this is only running once
+ try:
+ assert self.refresh_lock
+ except AssertionError:
+
+ self.refresh_lock = True
+ current_time = time.time()
+
+ for counter in self.autorefresh:
+
+ if counter in exclude or counter.min_delta > current_time - counter.last_update:
+ continue
+
+ counter.refresh()
+
+ self.refresh_lock = False
def get_manager(stream=None, counterclass=Counter, **kwargs):
=====================================
enlighten/_statusbar.py
=====================================
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+**Enlighten status bar submodule**
+
+Provides StatusBar class
+"""
+
+import time
+
+from enlighten._basecounter import PrintableCounter
+from enlighten._util import format_time, Justify
+
+
+class StatusBar(PrintableCounter):
+ """
+ Args:
+ enabled(bool): Status (Default: :py:data:`True`)
+ color(str): Color as a string or RGB tuple see :ref:`Status Color <status_color>`
+ fields(dict): Additional fields used for :ref:`formating <status_format>`
+ fill(str): Fill character used in formatting and justifying text (Default: ' ')
+ justify(str):
+ One of :py:attr:`Justify.CENTER`, :py:attr:`Justify.LEFT`, :py:attr:`Justify.RIGHT`
+ leave(True): Leave status bar after closing (Default: :py:data:`True`)
+ min_delta(float): Minimum time, in seconds, between refreshes (Default: 0.1)
+ status_format(str): Status bar format, see :ref:`Format <status_format>`
+
+ Status bar class
+
+ A :py:class:`StatusBar` instance should be created with the :py:meth:`Manager.status_bar`
+ method.
+
+ .. _status_color:
+
+ **Status Color**
+
+ Color works similarly to color on :py:class:`Counter`, except it affects the entire status bar.
+ See :ref:`Series Color <series_color>` for more information.
+
+ .. _status_format:
+
+ **Format**
+
+ There are two ways to populate the status bar, direct and formatted. Direct takes
+ precedence over formatted.
+
+ .. _status_format_direct:
+
+ **Direct Status**
+
+ Direct status is used when arguments are passed to :py:meth:`Manager.status_bar` or
+ :py:meth:`StatusBar.update`. Any arguments are coerced to strings and joined with a space.
+ For example:
+
+ .. code-block:: python
+
+
+ status_bar.update('Hello', 'World!')
+ # Example output: Hello World!
+
+ status_bar.update('Hello World!')
+ # Example output: Hello World!
+
+ count = [1, 2, 3, 4]
+ status_bar.update(*count)
+ # Example output: 1 2 3 4
+
+ .. _status_format_formatted:
+
+ **Formatted Status**
+
+ Formatted status uses the format specified in the ``status_format`` parameter to populate
+ the status bar.
+
+ .. code-block:: python
+
+ 'Current Stage: {stage}'
+
+ # Example output
+ 'Current Stage: Testing'
+
+ Available fields:
+
+ - elapsed(:py:class:`str`) - Time elapsed since instance was created
+ - fill(:py:class:`str`) - Filled with :py:attr:`fill` until line is width of terminal.
+ May be used multiple times. Minimum width is 3.
+
+ .. note::
+
+ The status bar is only updated when :py:meth:`StatusBar.update` or
+ :py:meth:`StatusBar.refresh` is called, so fields like ``elapsed``
+ will need additional calls to appear dynamic.
+
+ User-defined fields:
+
+ Users can define fields in two ways, the ``fields`` parameter and by passing keyword
+ arguments to :py:meth:`Manager.status_bar` or :py:meth:`StatusBar.update`
+
+ The ``fields`` parameter can be used to pass a dictionary of additional
+ user-defined fields. The dictionary values can be updated after initialization to allow
+ for dynamic fields. Any fields that share names with available fields are ignored.
+
+ If fields are passed as keyword arguments to :py:meth:`Manager.status_bar` or
+ :py:meth:`StatusBar.update`, they take precedent over the ``fields`` parameter.
+
+
+ **Instance Attributes**
+
+ .. py:attribute:: elapsed
+
+ :py:class:`float` - Time since start
+
+ .. py:attribute:: enabled
+
+ :py:class:`bool` - Current status
+
+ .. py:attribute:: manager
+
+ :py:class:`Manager` - Manager Instance
+
+ .. py:attribute:: position
+
+ :py:class:`int` - Current position
+
+ """
+
+ __slots__ = ('fields', '_justify', 'status_format', '_static', '_fields')
+
+ def __init__(self, *args, **kwargs):
+ super(StatusBar, self).__init__(**kwargs)
+
+ self.fields = kwargs.pop('fields', {})
+ self._justify = None
+ self.justify = kwargs.pop('justify', Justify.LEFT)
+ self.status_format = kwargs.pop('status_format', None)
+ self._fields = kwargs
+ self._static = ' '.join(str(arg) for arg in args) if args else None
+
+ @property
+ def justify(self):
+ """
+ Maps to justify method determined by ``justify`` parameter
+ """
+ return self._justify
+
+ @justify.setter
+ def justify(self, value):
+
+ if value in (Justify.LEFT, Justify.CENTER, Justify.RIGHT):
+ self._justify = getattr(self.manager.term, value)
+
+ else:
+ raise ValueError("justify must be one of Justify.LEFT, Justify.CENTER, ",
+ "Justify.RIGHT, not: '%r'" % value)
+
+ def format(self, width=None, elapsed=None):
+ """
+ Args:
+ width (int): Width in columns to make progress bar
+ elapsed(float): Time since started. Automatically determined if :py:data:`None`
+
+ Returns:
+ :py:class:`str`: Formatted status bar
+
+ Format status bar
+ """
+
+ width = width or self.manager.width
+ justify = self.justify
+
+ # If static message was given, just return it
+ if self._static is not None:
+ rtn = self._static
+
+ # If there is no format, return empty
+ elif self.status_format is None:
+ rtn = ''
+
+ # Generate from format
+ else:
+ fields = self.fields.copy()
+ fields.update(self._fields)
+ elapsed = elapsed if elapsed is not None else self.elapsed
+ fields['elapsed'] = format_time(elapsed)
+ fields['fill'] = u'{0}'
+
+ # Format
+ try:
+ rtn = self.status_format.format(**fields)
+ except KeyError as e:
+ raise ValueError('%r specified in format, but not provided' % e.args[0])
+
+ rtn = self._fill_text(rtn, width)
+
+ return self._colorize(justify(rtn, width=width, fillchar=self.fill))
+
+ def update(self, *objects, **fields): # pylint: disable=arguments-differ
+ """
+ Args:
+ objects(list): Values for :ref:`Direct Status <status_format_direct>`
+ force(bool): Force refresh even if ``min_delta`` has not been reached
+ fields(dict): Fields for for :ref:`Formatted Status <status_format_formatted>`
+
+ Update status and redraw
+
+ Status bar is only redrawn if ``min_delta`` seconds past since the last update
+ """
+
+ force = fields.pop('force', False)
+
+ self._static = ' '.join(str(obj) for obj in objects) if objects else None
+ self._fields.update(fields)
+
+ if self.enabled:
+ currentTime = time.time()
+ if force or currentTime - self.last_update >= self.min_delta:
+ self.refresh(elapsed=currentTime - self.start)
=====================================
enlighten/_util.py
=====================================
@@ -0,0 +1,67 @@
+
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+**Enlighten utility submodule**
+
+Provides utility functions and objects
+"""
+
+try:
+ BASESTRING = basestring
+except NameError:
+ BASESTRING = str
+
+
+def format_time(seconds):
+ """
+ Args:
+ seconds (float): amount of time
+
+ Format time string for eta and elapsed
+ """
+
+ # Always do minutes and seconds in mm:ss format
+ minutes = seconds // 60
+ hours = minutes // 60
+ rtn = u'{0:02.0f}:{1:02.0f}'.format(minutes % 60, seconds % 60)
+
+ # Add hours if there are any
+ if hours:
+
+ rtn = u'{0:d}h {1}'.format(int(hours % 24), rtn)
+
+ # Add days if there are any
+ days = int(hours // 24)
+ if days:
+ rtn = u'{0:d}d {1}'.format(days, rtn)
+
+ return rtn
+
+
+class Justify(object):
+ """
+ Enumerated type for justification options
+
+ .. py:attribute:: CENTER
+
+ Justify center
+
+ .. py:attribute:: LEFT
+
+ Justify left
+
+ .. py:attribute:: RIGHT
+
+ Justify right
+
+ """
+
+ CENTER = 'center'
+ LEFT = 'ljust'
+ RIGHT = 'rjust'
=====================================
enlighten/counter.py
=====================================
@@ -15,6 +15,7 @@ import sys
from enlighten._counter import Counter as _Counter
from enlighten._counter import SubCounter # pylint: disable=unused-import # noqa: F401
+from enlighten._statusbar import StatusBar # pylint: disable=unused-import # noqa: F401
from enlighten._manager import get_manager
=====================================
examples/demo.py
=====================================
@@ -26,7 +26,7 @@ def initialize(manager, initials=15):
# Simulated preparation
pbar = manager.counter(total=initials, desc='Initializing:', unit='initials')
for num in range(initials): # pylint: disable=unused-variable
- time.sleep(random.uniform(0.1, 0.5)) # Random processing time
+ time.sleep(random.uniform(0.05, 0.25)) # Random processing time
pbar.update()
pbar.close()
@@ -37,9 +37,21 @@ def main():
"""
with enlighten.get_manager() as manager:
+ status = manager.status_bar(status_format=u'Enlighten{fill}Stage: {demo}{fill}{elapsed}',
+ color='bold_underline_bright_white_on_lightslategray',
+ justify=enlighten.Justify.CENTER, demo='Initializing',
+ autorefresh=True, min_delta=0.5)
+ docs = manager.term.link('https://python-enlighten.readthedocs.io/en/stable/examples.html',
+ 'Read the Docs')
+ manager.status_bar(' More examples on %s! ' % docs, position=1, fill='-',
+ justify=enlighten.Justify.CENTER)
+
initialize(manager, 15)
- load(manager, 80)
- run_tests(manager, 40)
+ status.update(demo='Loading')
+ load(manager, 40)
+ status.update(demo='Testing')
+ run_tests(manager, 20)
+ status.update(demo='File Processing')
process_files(manager)
=====================================
examples/multicolored.py
=====================================
@@ -89,15 +89,15 @@ def run_tests(manager, tests=100):
terminal = manager.term
bar_format = u'{desc}{desc_pad}{percentage:3.0f}%|{bar}| ' + \
- u'S:' + terminal.green(u'{count_0:{len_total}d}') + u' ' + \
- u'F:' + terminal.red(u'{count_2:{len_total}d}') + u' ' + \
- u'E:' + terminal.yellow(u'{count_1:{len_total}d}') + u' ' + \
+ u'S:' + terminal.green3(u'{count_0:{len_total}d}') + u' ' + \
+ u'F:' + terminal.red2(u'{count_2:{len_total}d}') + u' ' + \
+ u'E:' + terminal.yellow2(u'{count_1:{len_total}d}') + u' ' + \
u'[{elapsed}<{eta}, {rate:.2f}{unit_pad}{unit}/s]'
- with manager.counter(total=tests, desc='Testing', unit='tests', color='green',
+ with manager.counter(total=tests, desc='Testing', unit='tests', color='green3',
bar_format=bar_format) as success:
- errors = success.add_subcounter('yellow')
- failures = success.add_subcounter('red')
+ errors = success.add_subcounter('yellow2')
+ failures = success.add_subcounter('red2')
for num in range(tests):
time.sleep(random.uniform(0.1, 0.3)) # Random processing time
@@ -120,9 +120,9 @@ def load(manager, units=80):
"""
pb_connecting = manager.counter(total=units, desc='Loading', unit='services',
- color='red', bar_format=BAR_FMT)
- pb_loading = pb_connecting.add_subcounter('yellow')
- pb_loaded = pb_connecting.add_subcounter('green', all_fields=True)
+ color='red2', bar_format=BAR_FMT)
+ pb_loading = pb_connecting.add_subcounter('yellow2')
+ pb_loaded = pb_connecting.add_subcounter('green3', all_fields=True)
connecting = []
loading = []
=====================================
examples/multiple_logging.py
=====================================
@@ -21,7 +21,7 @@ logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger('enlighten')
DATACENTERS = 5
-SYSTEMS = (10, 20) # Range
+SYSTEMS = (5, 10) # Range
FILES = (10, 100) # Range
=====================================
pylintrc
=====================================
@@ -6,6 +6,10 @@ output-format=colorized
# Use max line length of 100
max-line-length=100
+[DESIGN]
+# Maximum number of branch for function / method body.
+max-branches=15
+
[BASIC]
# As far as I can tell, PEP-8 (Nov 1, 2013) does not specify
# a specific naming convention for variables and arguments
@@ -32,7 +36,7 @@ spelling-dict=en_US
# List of comma separated words that should not be checked.
spelling-ignore-words=
Avram, ansicon, Args, assertRaisesRegexp, assertRegexpMatches, assertNotRegexpMatches, attr,
- AttributeError,
+ AttributeError, autorefresh,
BaseManager, bool,
desc, downconverted, downloader,
Enlighten's, exc,
@@ -42,9 +46,11 @@ spelling-ignore-words=
len, Lubkin,
meth, Mozilla, MPL,
noqa,
- pragma, py,
+ peru, pragma, py,
redirector, resize, resizing, RGB,
- setscroll, sphinxcontrib, ss, stdout, stderr, str, subcounter, subcounters, submodule,
+ seagreen, setscroll, sphinxcontrib, ss, StatusBar, stdout,
+ stderr, str, subcounter, subcounters, submodule,
subprocesses, sys,
TestCase, tty, TTY, tuple,
- unicode, unittest
+ unicode, unittest,
+ ValueError
=====================================
setup.py
=====================================
@@ -17,7 +17,7 @@ from setuptools import setup, find_packages
from setup_helpers import get_version, readme
-INSTALL_REQUIRES = ['blessed>=1.17.2']
+INSTALL_REQUIRES = ['blessed>=1.17.7']
TESTS_REQUIRE = ['mock; python_version < "3.3"',
'unittest2; python_version < "2.7"']
=====================================
tests/__init__.py
=====================================
@@ -19,7 +19,9 @@ import sys
import termios
from enlighten import Manager
-from enlighten._counter import Counter, BaseCounter
+from enlighten._basecounter import BaseCounter
+from enlighten._counter import Counter
+from enlighten._statusbar import StatusBar
# pylint: disable=import-error
@@ -140,6 +142,9 @@ class MockTTY(object):
self.stdout.close()
self.stdread.close()
+ def clear(self):
+ termios.tcflush(self.stdread, termios.TCIFLUSH)
+
def resize(self, height, width):
fcntl.ioctl(self.slave, termios.TIOCSWINSZ, struct.pack('hhhh', height, width, 0, 0))
@@ -149,7 +154,7 @@ class MockBaseCounter(BaseCounter):
Mock version of base counter for testing
"""
- def update(self, incr=1, force=False):
+ def update(self, *args, **kwargs):
"""
Simple update that updates the count. We know it's called based on the count.
"""
@@ -174,12 +179,32 @@ class MockCounter(Counter):
self.calls.append('clear(flush=%s)' % flush)
+class MockStatusBar(StatusBar):
+
+ __slots__ = ('called', 'calls')
+
+ def __init__(self, *args, **kwargs):
+ super(MockStatusBar, self).__init__(*args, **kwargs)
+ self.called = 0
+ self.calls = []
+
+ def refresh(self, flush=True, elapsed=None):
+ self.called += 1
+ self.calls.append('refresh(flush=%s, elapsed=%s)' % (flush, elapsed))
+
+
class MockManager(Manager):
# pylint: disable=super-init-not-called
def __init__(self, counter_class=Counter, **kwargs):
super(MockManager, self).__init__(counter_class=counter_class, **kwargs)
self.width = 80
self.output = []
+ self.remove_calls = 0
+
+ def write(self, output='', flush=True, counter=None):
+ self.output.append('write(output=%s, flush=%s, position=%s)' %
+ (output, flush, counter.position))
- def write(self, output='', flush=True, position=0):
- self.output.append('write(output=%s, flush=%s, position=%s)' % (output, flush, position))
+ def remove(self, counter):
+ self.remove_calls += 1
+ super(MockManager, self).remove(counter)
=====================================
tests/test_basecounter.py
=====================================
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Test module for enlighten._counter and enlighten.counter
+"""
+
+from types import GeneratorType
+
+from enlighten._basecounter import BaseCounter
+
+from tests import TestCase, MockManager, MockTTY, MockBaseCounter
+
+
+# pylint: disable=protected-access
+class TestBaseCounter(TestCase):
+ """
+ Test the BaseCounter class
+ """
+
+ def setUp(self):
+ self.tty = MockTTY()
+ self.manager = MockManager(stream=self.tty.stdout)
+
+ def tearDown(self):
+ self.tty.close()
+
+ def test_init_default(self):
+ """Ensure default values are set"""
+ counter = BaseCounter(manager=self.manager)
+ self.assertIsNone(counter.color)
+ self.assertIsNone(counter.color)
+ self.assertIs(counter.manager, self.manager)
+ self.assertEqual(counter.count, 0)
+ self.assertEqual(counter.start_count, 0)
+
+ def test_no_manager(self):
+ """Raise an error if there is no manager specified"""
+ with self.assertRaisesRegex(TypeError, 'manager must be specified'):
+ BaseCounter()
+
+ def test_color_invalid(self):
+ """Color must be a valid string, RGB, or int 0 - 255"""
+ # Unsupported type
+ with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 1.0'):
+ BaseCounter(manager=self.manager, color=1.0)
+
+ # Invalid String
+ with self.assertRaisesRegex(AttributeError, 'Invalid color specified: buggersnot'):
+ BaseCounter(manager=self.manager, color='buggersnot')
+
+ # Invalid integer
+ with self.assertRaisesRegex(AttributeError, 'Invalid color specified: -1'):
+ BaseCounter(manager=self.manager, color=-1)
+ with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 256'):
+ BaseCounter(manager=self.manager, color=256)
+
+ # Invalid iterable
+ with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[\]'):
+ BaseCounter(manager=self.manager, color=[])
+ with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[1\]'):
+ BaseCounter(manager=self.manager, color=[1])
+ with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2\)'):
+ BaseCounter(manager=self.manager, color=(1, 2))
+ with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2, 3, 4\)'):
+ BaseCounter(manager=self.manager, color=(1, 2, 3, 4))
+
+ def test_colorize_none(self):
+ """If color is None, return content unchanged"""
+ counter = BaseCounter(manager=self.manager)
+ self.assertEqual(counter._colorize('test'), 'test')
+
+ def test_colorize_string(self):
+ """Return string formatted with color (string)"""
+ counter = BaseCounter(manager=self.manager, color='red')
+ self.assertEqual(counter.color, 'red')
+ self.assertEqual(counter._color, ('red', self.manager.term.red))
+ self.assertNotEqual(counter._colorize('test'), 'test')
+ self.assertEqual(counter._colorize('test'), self.manager.term.red('test'))
+
+ def test_colorize_string_compound(self):
+ """Return string formatted with compound color (string)"""
+ counter = BaseCounter(manager=self.manager, color='bold_red_on_blue')
+ self.assertEqual(counter.color, 'bold_red_on_blue')
+ self.assertEqual(counter._color, ('bold_red_on_blue', self.manager.term.bold_red_on_blue))
+ self.assertNotEqual(counter._colorize('test'), 'test')
+ self.assertEqual(counter._colorize('test'), self.manager.term.bold_red_on_blue('test'))
+
+ def test_colorize_int(self):
+ """Return string formatted with color (int)"""
+ counter = BaseCounter(manager=self.manager, color=40)
+ self.assertEqual(counter.color, 40)
+ self.assertEqual(counter._color, (40, self.manager.term.color(40)))
+ self.assertNotEqual(counter._colorize('test'), 'test')
+ self.assertEqual(counter._colorize('test'), self.manager.term.color(40)('test'))
+
+ def test_colorize_rgb(self):
+ """Return string formatted with color (RGB)"""
+ counter = BaseCounter(manager=self.manager, color=(50, 40, 60))
+ self.assertEqual(counter.color, (50, 40, 60))
+ self.assertEqual(counter._color, ((50, 40, 60), self.manager.term.color_rgb(50, 40, 60)))
+ self.assertNotEqual(counter._colorize('test'), 'test')
+ self.assertEqual(counter._colorize('test'), self.manager.term.color_rgb(50, 40, 60)('test'))
+
+ def test_call(self):
+ """Returns generator when used as a function"""
+
+ # Bad arguments
+ counter = MockBaseCounter(manager=self.manager)
+ with self.assertRaisesRegex(TypeError, 'Argument type int is not iterable'):
+ list(counter(1))
+ with self.assertRaisesRegex(TypeError, 'Argument type bool is not iterable'):
+ list(counter([1, 2, 3], True))
+
+ # Expected
+ counter = MockBaseCounter(manager=self.manager)
+ rtn = counter([1, 2, 3])
+ self.assertIsInstance(rtn, GeneratorType)
+ self.assertEqual(list(rtn), [1, 2, 3])
+ self.assertEqual(counter.count, 3)
+
+ # Multiple arguments
+ counter = MockBaseCounter(manager=self.manager)
+ rtn = counter([1, 2, 3], (3, 2, 1))
+ self.assertIsInstance(rtn, GeneratorType)
+ self.assertEqual(tuple(rtn), (1, 2, 3, 3, 2, 1))
+ self.assertEqual(counter.count, 6)
=====================================
tests/test_counter.py
=====================================
@@ -10,13 +10,12 @@ Test module for enlighten._counter and enlighten.counter
"""
import time
-from types import GeneratorType
from enlighten import Counter, Manager
import enlighten._counter
from enlighten._manager import NEEDS_UNICODE_HELP
-from tests import TestCase, mock, MockManager, MockTTY, MockCounter, MockBaseCounter
+from tests import TestCase, mock, MockManager, MockTTY, MockCounter
# pylint: disable=missing-docstring, protected-access, too-many-public-methods
@@ -24,141 +23,6 @@ SERIES_STD = u' ▏▎▍▌▋▊▉█'
BLOCK = enlighten._counter.SERIES_STD[-1]
-class TestFormatTime(TestCase):
- """
- Test cases for :py:func:`_format_time`
- """
-
- def test_seconds(self):
-
- self.assertEqual(enlighten._counter._format_time(0), '00:00')
- self.assertEqual(enlighten._counter._format_time(6), '00:06')
- self.assertEqual(enlighten._counter._format_time(42), '00:42')
-
- def test_minutes(self):
-
- self.assertEqual(enlighten._counter._format_time(60), '01:00')
- self.assertEqual(enlighten._counter._format_time(128), '02:08')
- self.assertEqual(enlighten._counter._format_time(1684), '28:04')
-
- def test_hours(self):
-
- self.assertEqual(enlighten._counter._format_time(3600), '1h 00:00')
- self.assertEqual(enlighten._counter._format_time(43980), '12h 13:00')
- self.assertEqual(enlighten._counter._format_time(43998), '12h 13:18')
-
- def test_days(self):
-
- self.assertEqual(enlighten._counter._format_time(86400), '1d 0h 00:00')
- self.assertEqual(enlighten._counter._format_time(1447597), '16d 18h 06:37')
-
-
-class TestBaseCounter(TestCase):
- """
- Test the BaseCounter class
- """
-
- def setUp(self):
- self.tty = MockTTY()
- self.manager = MockManager(stream=self.tty.stdout)
-
- def tearDown(self):
- self.tty.close()
-
- def test_init_default(self):
- """Ensure default values are set"""
- counter = enlighten._counter.BaseCounter(manager=self.manager)
- self.assertIsNone(counter.color)
- self.assertIsNone(counter.color)
- self.assertIs(counter.manager, self.manager)
- self.assertEqual(counter.count, 0)
- self.assertEqual(counter.start_count, 0)
-
- def test_no_manager(self):
- """Raise an error if there is no manager specified"""
- with self.assertRaisesRegex(TypeError, 'manager must be specified'):
- enlighten._counter.BaseCounter()
-
- def test_color_invalid(self):
- """Color must be a valid string, RGB, or int 0 - 255"""
- # Unsupported type
- with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 1.0'):
- enlighten._counter.BaseCounter(manager=self.manager, color=1.0)
-
- # Invalid String
- with self.assertRaisesRegex(AttributeError, 'Invalid color specified: buggersnot'):
- enlighten._counter.BaseCounter(manager=self.manager, color='buggersnot')
-
- # Invalid integer
- with self.assertRaisesRegex(AttributeError, 'Invalid color specified: -1'):
- enlighten._counter.BaseCounter(manager=self.manager, color=-1)
- with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 256'):
- enlighten._counter.BaseCounter(manager=self.manager, color=256)
-
- # Invalid iterable
- with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[\]'):
- enlighten._counter.BaseCounter(manager=self.manager, color=[])
- with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[1\]'):
- enlighten._counter.BaseCounter(manager=self.manager, color=[1])
- with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2\)'):
- enlighten._counter.BaseCounter(manager=self.manager, color=(1, 2))
- with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2, 3, 4\)'):
- enlighten._counter.BaseCounter(manager=self.manager, color=(1, 2, 3, 4))
-
- def test_colorize_none(self):
- """If color is None, return content unchanged"""
- counter = enlighten._counter.BaseCounter(manager=self.manager)
- self.assertEqual(counter._colorize('test'), 'test')
-
- def test_colorize_string(self):
- """Return string formatted with color (string)"""
- counter = enlighten._counter.BaseCounter(manager=self.manager, color='red')
- self.assertEqual(counter.color, 'red')
- self.assertEqual(counter._color, ('red', self.manager.term.red))
- self.assertNotEqual(counter._colorize('test'), 'test')
- self.assertEqual(counter._colorize('test'), self.manager.term.red('test'))
-
- def test_colorize_int(self):
- """Return string formatted with color (int)"""
- counter = enlighten._counter.BaseCounter(manager=self.manager, color=40)
- self.assertEqual(counter.color, 40)
- self.assertEqual(counter._color, (40, self.manager.term.color(40)))
- self.assertNotEqual(counter._colorize('test'), 'test')
- self.assertEqual(counter._colorize('test'), self.manager.term.color(40)('test'))
-
- def test_colorize_rgb(self):
- """Return string formatted with color (RGB)"""
- counter = enlighten._counter.BaseCounter(manager=self.manager, color=(50, 40, 60))
- self.assertEqual(counter.color, (50, 40, 60))
- self.assertEqual(counter._color, ((50, 40, 60), self.manager.term.color_rgb(50, 40, 60)))
- self.assertNotEqual(counter._colorize('test'), 'test')
- self.assertEqual(counter._colorize('test'), self.manager.term.color_rgb(50, 40, 60)('test'))
-
- def test_call(self):
- """Returns generator when used as a function"""
-
- # Bad arguments
- counter = MockBaseCounter(manager=self.manager)
- with self.assertRaisesRegex(TypeError, 'Argument type int is not iterable'):
- list(counter(1))
- with self.assertRaisesRegex(TypeError, 'Argument type bool is not iterable'):
- list(counter([1, 2, 3], True))
-
- # Expected
- counter = MockBaseCounter(manager=self.manager)
- rtn = counter([1, 2, 3])
- self.assertIsInstance(rtn, GeneratorType)
- self.assertEqual(list(rtn), [1, 2, 3])
- self.assertEqual(counter.count, 3)
-
- # Multiple arguments
- counter = MockBaseCounter(manager=self.manager)
- rtn = counter([1, 2, 3], (3, 2, 1))
- self.assertIsInstance(rtn, GeneratorType)
- self.assertEqual(tuple(rtn), (1, 2, 3, 3, 2, 1))
- self.assertEqual(counter.count, 6)
-
-
class TestSubCounter(TestCase):
"""
Test the BaseCounter class
@@ -370,9 +234,11 @@ class TestCounter(TestCase):
self.assertEqual(int(ctr.elapsed), 3)
def test_refresh(self):
+ self.ctr.last_update = 0
self.ctr.refresh()
self.assertRegex(self.manager.output[0],
r'write\(output=%s, flush=True, position=3\)' % self.output)
+ self.assertAlmostEqual(self.ctr.last_update, time.time(), delta=0.3)
self.manager.output = []
self.ctr.refresh(flush=False)
@@ -385,8 +251,10 @@ class TestCounter(TestCase):
self.assertEqual(len(self.manager.output), 0)
def test_clear(self):
+ self.ctr.last_update = 100
self.ctr.clear()
self.assertRegex(self.manager.output[0], r'write\(output=, flush=True, position=3\)')
+ self.assertEqual(self.ctr.last_update, 0)
self.manager.output = []
self.ctr.clear(flush=False)
@@ -719,25 +587,25 @@ class TestCounter(TestCase):
self.assertEqual(formatted, u'350 | 50 1.0 | 100')
def test_close(self):
- manager = mock.Mock()
+ manager = MockManager()
# Clear is False
ctr = MockCounter(manager=manager)
ctr.close()
self.assertEqual(ctr.calls, ['refresh(flush=True, elapsed=None)'])
- self.assertEqual(manager.remove.call_count, 1)
+ self.assertEqual(manager.remove_calls, 1)
# Clear is True, leave is True
ctr = MockCounter(manager=manager, leave=True)
ctr.close(clear=True)
self.assertEqual(ctr.calls, ['refresh(flush=True, elapsed=None)'])
- self.assertEqual(manager.remove.call_count, 2)
+ self.assertEqual(manager.remove_calls, 2)
# Clear is True, leave is False
ctr = MockCounter(manager=manager, leave=False)
ctr.close(clear=True)
self.assertEqual(ctr.calls, ['clear(flush=True)'])
- self.assertEqual(manager.remove.call_count, 3)
+ self.assertEqual(manager.remove_calls, 3)
def test_context_manager(self):
mgr = Manager(stream=self.tty.stdout, enabled=False)
@@ -776,11 +644,11 @@ class TestCounter(TestCase):
bar_format = ctr_format = u'{arg1:s} {count:d}'
ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format,
- additional_fields={'arg1': 'hello'})
+ fields={'arg1': 'hello'})
self.assertEqual(ctr.format(), 'hello 1')
ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format,
- additional_fields={'arg1': 'hello'})
+ fields={'arg1': 'hello'})
self.assertEqual(ctr.format(), 'hello 1')
def test_additional_fields_missing(self):
@@ -791,11 +659,11 @@ class TestCounter(TestCase):
bar_format = ctr_format = u'{arg1:s} {count:d}'
ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format)
- with self.assertRaisesRegex(ValueError, "'arg1' specified in format, but not present"):
+ with self.assertRaisesRegex(ValueError, "'arg1' specified in format, but not provided"):
ctr.format()
ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format)
- with self.assertRaisesRegex(ValueError, "'arg1' specified in format, but not present"):
+ with self.assertRaisesRegex(ValueError, "'arg1' specified in format, but not provided"):
ctr.format()
def test_additional_fields_changed(self):
@@ -807,13 +675,13 @@ class TestCounter(TestCase):
additional_fields = {'arg1': 'hello'}
ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format,
- additional_fields=additional_fields)
+ fields=additional_fields)
self.assertEqual(ctr.format(), 'hello 1')
additional_fields['arg1'] = 'goodbye'
self.assertEqual(ctr.format(), 'goodbye 1')
ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format,
- additional_fields=additional_fields)
+ fields=additional_fields)
self.assertEqual(ctr.format(), 'goodbye 1')
additional_fields['arg1'] = 'hello'
self.assertEqual(ctr.format(), 'hello 1')
@@ -827,9 +695,70 @@ class TestCounter(TestCase):
additional_fields = {'arg1': 'hello', 'count': 100000}
ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format,
- additional_fields=additional_fields)
+ fields=additional_fields)
self.assertEqual(ctr.format(), 'hello 1')
ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format,
- additional_fields=additional_fields)
+ fields=additional_fields)
+ self.assertEqual(ctr.format(), 'hello 1')
+
+ def test_kwarg_fields(self):
+ """
+ Additional fields to format via keyword arguments
+ """
+
+ bar_format = ctr_format = u'{arg1:s} {count:d}'
+
+ ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format,
+ arg1='hello')
+ self.assertEqual(ctr.format(), 'hello 1')
+
+ ctr.update(arg1='goodbye')
+ self.assertEqual(ctr.format(), 'goodbye 2')
+
+ ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format,
+ arg1='hello')
+ self.assertEqual(ctr.format(), 'hello 1')
+
+ ctr.update(arg1='goodbye')
+ self.assertEqual(ctr.format(), 'goodbye 2')
+
+ def test_kwarg_fields_precedence(self):
+ """
+ Keyword arguments take precedence over fields
+ """
+
+ bar_format = u'{arg1:s} {count:d}'
+ additional_fields = {'arg1': 'hello'}
+
+ ctr = Counter(stream=self.tty.stdout, total=10, count=1, bar_format=bar_format,
+ fields=additional_fields)
+
self.assertEqual(ctr.format(), 'hello 1')
+
+ ctr.update(arg1='goodbye')
+ self.assertEqual(ctr.format(), 'goodbye 2')
+
+ def test_fill_setter(self):
+ """Fill must be one printable character"""
+
+ ctr = Counter(stream=self.tty.stdout, fill='a')
+
+ with self.assertRaisesRegex(ValueError, 'fill character must be a length of 1'):
+ ctr.fill = 'hello'
+
+ with self.assertRaisesRegex(ValueError, 'fill character must be a length of 1'):
+ ctr.fill = ''
+
+ def test_fill(self):
+ """
+ Fill uses remaining space
+ """
+
+ ctr_format = u'{fill}HI'
+ ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format, fill=u'-')
+ self.assertEqual(ctr.format(), u'-' * 78 + 'HI')
+
+ ctr_format = u'{fill}HI{fill}'
+ ctr = Counter(stream=self.tty.stdout, count=1, counter_format=ctr_format, fill=u'-')
+ self.assertEqual(ctr.format(), u'-' * 39 + 'HI' + u'-' * 39)
=====================================
tests/test_manager.py
=====================================
@@ -11,6 +11,7 @@ Test module for enlighten._manager
import signal
import sys
+import time
from enlighten import _manager
@@ -192,6 +193,35 @@ class TestManager(TestCase):
'Counter position 200 is greater than terminal height'):
manager.counter(position=200)
+ def test_counter_position_pinned(self):
+ """If a position is taken, use next available"""
+
+ manager = _manager.Manager(stream=self.tty.stdout, set_scroll=False)
+ counter1 = manager.counter(position=2)
+ self.assertEqual(manager.counters[counter1], 2)
+
+ counter2 = manager.counter()
+ self.assertEqual(manager.counters[counter1], 2)
+ self.assertEqual(manager.counters[counter2], 1)
+
+ counter3 = manager.counter()
+ self.assertEqual(manager.counters[counter1], 2)
+ self.assertEqual(manager.counters[counter2], 3)
+ self.assertEqual(manager.counters[counter3], 1)
+
+ status1 = manager.status_bar(position=3)
+ self.assertEqual(manager.counters[counter1], 2)
+ self.assertEqual(manager.counters[counter2], 4)
+ self.assertEqual(manager.counters[counter3], 1)
+ self.assertEqual(manager.counters[status1], 3)
+
+ status2 = manager.status_bar()
+ self.assertEqual(manager.counters[counter1], 2)
+ self.assertEqual(manager.counters[counter2], 5)
+ self.assertEqual(manager.counters[counter3], 4)
+ self.assertEqual(manager.counters[status1], 3)
+ self.assertEqual(manager.counters[status2], 1)
+
def test_inherit_kwargs(self):
manager = _manager.Manager(stream=self.tty.stdout, counter_class=MockCounter,
unit='knights', not_real=True, desc='Default')
@@ -212,14 +242,15 @@ class TestManager(TestCase):
with mock.patch('enlighten._manager.Manager._set_scroll_area') as ssa:
manager = _manager.Manager(stream=self.tty.stdout)
+ counter = manager.counter(position=3)
term = manager.term
- manager.write(msg, position=3)
+ manager.write(msg, counter=counter)
self.tty.stdout.write(u'X\n')
# Carriage return is getting converted to newline
self.assertEqual(self.tty.stdread.readline(),
term.move(22, 0) + '\r' + term.clear_eol + msg + 'X\n')
- self.assertEqual(ssa.call_count, 1)
+ self.assertEqual(ssa.call_count, 2)
def test_write_no_flush(self):
"""
@@ -233,14 +264,43 @@ class TestManager(TestCase):
with mock.patch('enlighten._manager.Manager._set_scroll_area') as ssa:
manager = _manager.Manager(stream=self.tty.stdout)
+ counter = manager.counter(position=3)
term = manager.term
- manager.write(msg, position=3, flush=False)
+ manager.write(msg, counter=counter, flush=False)
self.tty.stdout.write(u'X\n')
# Carriage return is getting converted to newline
self.assertEqual(self.tty.stdread.readline(),
term.move(22, 0) + '\r' + term.clear_eol + msg + 'X\n')
- self.assertEqual(ssa.call_count, 1)
+ self.assertEqual(ssa.call_count, 2)
+
+ def test_autorefresh(self):
+ """
+ Ensure auto-refreshed counters are updated when others are
+ """
+
+ manager = _manager.Manager(stream=self.tty.stdout)
+ counter1 = manager.counter(count=1, total=0, counter_format=u'counter1', autorefresh=True)
+ counter2 = manager.counter(count=1, total=0, counter_format=u'counter2')
+ self.tty.clear()
+
+ # Counter 1 in auto-refresh list
+ self.assertIn(counter1, manager.autorefresh)
+
+ # If auto-refreshed counter hasn't been refreshed recently refresh
+ counter1.last_update = 0
+ counter2.refresh()
+ self.tty.stdout.write(u'X\n')
+ output = self.tty.stdread.readline()
+ self.assertRegex(output, 'counter2.+counter1')
+
+ # If auto-refreshed counter has been refreshed recently, skip
+ counter1.last_update = time.time() + 5
+ counter2.refresh()
+ self.tty.stdout.write(u'X\n')
+ output = self.tty.stdread.readline()
+ self.assertRegex(output, 'counter2')
+ self.assertNotRegex(output, 'counter1')
def test_set_scroll_area_disabled(self):
manager = _manager.Manager(stream=self.tty.stdout,
=====================================
tests/test_statusbar.py
=====================================
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Test module for enlighten._statusbar
+"""
+
+from enlighten import Justify
+
+from tests import TestCase, MockManager, MockTTY, MockStatusBar
+
+
+class TestStatusBar(TestCase):
+ """
+ Test the StatusBar class
+ """
+
+ def setUp(self):
+ self.tty = MockTTY()
+ self.manager = MockManager(stream=self.tty.stdout)
+
+ def tearDown(self):
+ self.tty.close()
+
+ def test_static(self):
+ """
+ Basic static status bar
+ """
+
+ sbar = self.manager.status_bar('Hello', 'World!')
+ self.assertEqual(sbar.format(), 'Hello World!' + ' ' * 68)
+
+ sbar.update('Goodbye, World!')
+ self.assertEqual(sbar.format(), 'Goodbye, World!' + ' ' * 65)
+
+ def test_static_justify(self):
+ """
+ Justified static status bar
+ """
+
+ sbar = self.manager.status_bar('Hello', 'World!', justify=Justify.LEFT)
+ self.assertEqual(sbar.format(), 'Hello World!' + ' ' * 68)
+
+ sbar = self.manager.status_bar('Hello', 'World!', justify=Justify.RIGHT)
+ self.assertEqual(sbar.format(), ' ' * 68 + 'Hello World!')
+
+ sbar = self.manager.status_bar('Hello', 'World!', justify=Justify.CENTER)
+ self.assertEqual(sbar.format(), ' ' * 34 + 'Hello World!' + ' ' * 34)
+
+ def test_formatted(self):
+ """
+ Basic formatted status bar
+ """
+
+ sbar = self.manager.status_bar(status_format=u'Stage: {stage}, Status: {status}', stage=1,
+ fields={'status': 'All good!'})
+ self.assertEqual(sbar.format(), 'Stage: 1, Status: All good!' + ' ' * 53)
+ sbar.update(stage=2)
+ self.assertEqual(sbar.format(), 'Stage: 2, Status: All good!' + ' ' * 53)
+ sbar.update(stage=3, status='Meh')
+ self.assertEqual(sbar.format(), 'Stage: 3, Status: Meh' + ' ' * 59)
+
+ def test_formatted_justify(self):
+ """
+ Justified formatted status bar
+ """
+
+ sbar = self.manager.status_bar(status_format=u'Stage: {stage}, Status: {status}', stage=1,
+ fields={'status': 'All good!'}, justify=Justify.LEFT)
+ self.assertEqual(sbar.format(), 'Stage: 1, Status: All good!' + ' ' * 53)
+
+ sbar = self.manager.status_bar(status_format=u'Stage: {stage}, Status: {status}', stage=1,
+ fields={'status': 'All good!'}, justify=Justify.RIGHT)
+ self.assertEqual(sbar.format(), ' ' * 53 + 'Stage: 1, Status: All good!')
+
+ sbar = self.manager.status_bar(status_format=u'Stage: {stage}, Status: {status}', stage=1,
+ fields={'status': 'All good'}, justify=Justify.CENTER)
+ self.assertEqual(sbar.format(), ' ' * 27 + 'Stage: 1, Status: All good' + ' ' * 27)
+
+ def test_formatted_missing_field(self):
+ """
+ ValueError raised when a field is missing when updating status bar
+ """
+
+ fields = {'status': 'All good!'}
+ sbar = self.manager.status_bar(status_format=u'Stage: {stage}, Status: {status}', stage=1,
+ fields=fields)
+ del fields['status']
+
+ sbar.last_update = sbar.start - 5.0
+ with self.assertRaisesRegex(ValueError, "'status' specified in format, but not provided"):
+ sbar.update()
+
+ def test_bad_justify(self):
+ """
+ ValueError raised when justify is given an invalid value
+ """
+
+ with self.assertRaisesRegex(ValueError, 'justify must be one of Justify.LEFT, '):
+ self.manager.status_bar('Hello', 'World!', justify='justice')
+
+ def test_update(self):
+ """
+ update() does not refresh is bar is disabled or min_delta hasn't passed
+ """
+
+ self.manager.status_bar_class = MockStatusBar
+ sbar = self.manager.status_bar('Hello', 'World!')
+
+ self.assertEqual(sbar.called, 1)
+ sbar.last_update = sbar.start - 1.0
+ sbar.update()
+ self.assertEqual(sbar.called, 2)
+
+ sbar.last_update = sbar.start + 5.0
+ sbar.update()
+ self.assertEqual(sbar.called, 2)
+
+ sbar.last_update = sbar.last_update - 10.0
+ sbar.enabled = False
+ sbar.update()
+ self.assertEqual(sbar.called, 2)
+
+ sbar.enabled = True
+ sbar.update()
+ self.assertEqual(sbar.called, 3)
+
+ def test_fill(self):
+ """
+ Fill uses remaining space
+ """
+
+ sbar = self.manager.status_bar(status_format=u'{fill}HI', fill='-')
+ self.assertEqual(sbar.format(), u'-' * 78 + 'HI')
+
+ sbar = self.manager.status_bar(status_format=u'{fill}HI{fill}', fill='-')
+ self.assertEqual(sbar.format(), u'-' * 39 + 'HI' + u'-' * 39)
+
+ def test_fill_uneven(self):
+ """
+ Extra fill should be equal
+ """
+
+ print(self.manager.term.width)
+ sbar = self.manager.status_bar(
+ status_format=u'{fill}Helloooo!{fill}Woooorld!{fill}', fill='-'
+ )
+ self.assertEqual(sbar.format(),
+ u'-' * 20 + 'Helloooo!' + u'-' * 21 + 'Woooorld!' + u'-' * 21)
=====================================
tests/test_util.py
=====================================
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Test module for enlighten._util
+"""
+
+from enlighten._util import format_time
+
+from tests import TestCase
+
+
+class TestFormatTime(TestCase):
+ """
+ Test cases for :py:func:`_format_time`
+ """
+
+ def test_seconds(self):
+ """Verify seconds formatting"""
+
+ self.assertEqual(format_time(0), '00:00')
+ self.assertEqual(format_time(6), '00:06')
+ self.assertEqual(format_time(42), '00:42')
+
+ def test_minutes(self):
+ """Verify minutes formatting"""
+
+ self.assertEqual(format_time(60), '01:00')
+ self.assertEqual(format_time(128), '02:08')
+ self.assertEqual(format_time(1684), '28:04')
+
+ def test_hours(self):
+ """Verify hours formatting"""
+
+ self.assertEqual(format_time(3600), '1h 00:00')
+ self.assertEqual(format_time(43980), '12h 13:00')
+ self.assertEqual(format_time(43998), '12h 13:18')
+
+ def test_days(self):
+ """Verify days formatting"""
+
+ self.assertEqual(format_time(86400), '1d 0h 00:00')
+ self.assertEqual(format_time(1447597), '16d 18h 06:37')
=====================================
tox.ini
=====================================
@@ -24,7 +24,7 @@ commands =
[testenv:el7]
basepython = python2.7
deps =
- blessed == 1.17.2
+ blessed == 1.17.8
mock == 1.0.1
# setuptools == 0.9.8 (Doesn't support PEP 508)
setuptools == 20.2.2
View it on GitLab: https://salsa.debian.org/med-team/enlighten/-/commit/d1545d97e8764050a4c440b8ce8a007e0ff3b461
--
View it on GitLab: https://salsa.debian.org/med-team/enlighten/-/commit/d1545d97e8764050a4c440b8ce8a007e0ff3b461
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20200713/d3e62a82/attachment-0001.html>
More information about the debian-med-commit
mailing list