# vim: set et sw=4 sts=4 fileencoding=utf-8:
#
# Copyright (c) 2013-2017 Dave Jones <dave@waveform.org.uk>
# Copyright (c) 2013 Mime Consulting Ltd. <info@mimeconsulting.co.uk>
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
This module provides a wrapper that outputs simple progress meters to the
command line based on source file positions, or an arbitrary counter. The
:class:`ProgressMeter` class is the major element that this module provides.
Classes
=======
.. autoclass:: ProgressMeter(fileobj=None, value=0, total=None, max_wait=0.1, \
stream=sys.stderr, mode='w', style=BarStyle, hide_on_finish=True)
:members:
.. autoclass:: SpinnerStyle
.. autoclass:: PercentageStyle
.. autoclass:: EllipsisStyle
.. autoclass:: BarStyle
.. autoclass:: HashStyle
Examples
========
The most basic usage of this class is as follows::
import io
from lars import iis, csv, progress
with io.open('logs\\iis.txt', 'rb') as infile, \\
io.open('iis.csv', 'wb') as outfile, \\
progress.ProgressMeter(infile) as meter, \\
iis.IISSource(infile) as source, \\
csv.CSVTarget(outfile) as target:
for row in source:
target.write(row)
meter.update()
Note that you do not need to worry about the detrimental performance effects of
calling :meth:`~ProgressMeter.update` too often; the class ensures that
repeated calls are ignored until :attr:`~ProgressMeter.max_wait` seconds have
elapsed since the last update.
Alternatively, if you wish to update according to, say, the number of files to
process you could use something like the following example (which also
demonstrates temporarily hiding the progress meter in order to show the current
filename)::
import os
import io
from lars import iis, csv, progress
from pathlib import Path
files = list(Path('.').iterdir())
with progress.ProgressMeter(total=len(files),
style=progress.BarStyle) as meter:
for file_num, file_name in enumerate(files):
meter.hide()
print "Processing %s" % file_name
meter.show()
with file_name.open('rb') as infile, \\
file_name.with_suffix('.csv').open('wb') as outfile, \\
iis.IISSource(infile) as source, \\
csv.CSVTarget(outfile) as target:
for row in source:
target.write(row)
meter.update(file_num)
"""
from __future__ import (
unicode_literals,
absolute_import,
print_function,
division,
)
import io
import sys
import time
str = type('') # pylint: disable=redefined-builtin,invalid-name
class ProgressStyle(object):
"""
Abstract base class for :class:`ProgressMeter` styles.
"""
# pylint: disable=too-few-public-methods
def __init__(self, meter):
"""
Perform initialization for the style, and set hide_on_finish
"""
pass
def render(self, value, total):
"""
Return a string showing value/total progress
"""
raise NotImplementedError
[docs]class SpinnerStyle(ProgressStyle):
"""
A :class:`ProgressMeter` style that renders a simple spinning line.
"""
# pylint: disable=too-few-public-methods
def __init__(self, meter):
super(SpinnerStyle, self).__init__(meter)
self.index = 0
def render(self, value, total):
self.index += 1
return ['/', '-', '\\', '|'][self.index % 4]
[docs]class EllipsisStyle(ProgressStyle):
"""
A :class:`ProgressMeter` style that renders an looping series of dots.
"""
# pylint: disable=too-few-public-methods
def __init__(self, meter):
super(EllipsisStyle, self).__init__(meter)
self.count = 0
self.max = 8
def render(self, value, total):
self.count += 1
self.count %= self.max
return '.' * self.count
[docs]class PercentageStyle(ProgressStyle):
"""
A :class:`ProgressMeter` style that renders a simple percentage counter.
"""
# pylint: disable=too-few-public-methods
def render(self, value, total):
return '%3d%%' % (100 * value // total)
[docs]class BarStyle(ProgressStyle):
"""
A :class:`ProgressMeter` style that renders a full progress bar and
percentage.
"""
# pylint: disable=too-few-public-methods
def __init__(self, meter):
super(BarStyle, self).__init__(meter)
self.width = 60
self.fill_char = '='
self.back_char = ' '
def render(self, value, total):
x = (self.width - 8) * value // total
return '[%s>%s] %3d%%' % (
self.fill_char * x,
self.back_char * (self.width - 8 - x),
100 * value // total,
)
[docs]class HashStyle(ProgressStyle):
"""
A :class:`ProgressMeter` style for those that remember FTP's ``hash``
command!
"""
# pylint: disable=too-few-public-methods
def __init__(self, meter):
super(HashStyle, self).__init__(meter)
self.count = 0
self.char = '#'
def render(self, value, total):
self.count += 1
return self.char * self.count
[docs]class ProgressMeter(object):
"""
This class provides a simple means of rendering a progress meter at the
command line. It can be driven either with a file object (in which case the
current position of the file is used) or with an arbitrary value (which
your code must provide). In the case of a file-object, the file must be
seekable (so that the class can determine the overall length of the file).
If *fileobj* is not specified, then *total* must be specified.
The class is intended to be used as a context manager. Upon entry it will
render an initial progress meter, and will update it at reasonable
intervals (dictated by the max_wait parameter) in response to calls to the
:meth:`update` method. When you leave the context, the progress meter will
be automatically erased if *hide_on_finish* is True (which it is by
default).
Within the context, the :meth:`hide` and :meth:`show` methods can be used
to temporarily hide and show the progress meter (in order to display some
status text, for example).
:param file fileobj:
A file-like object from which to determine progress
:param int value:
An arbitrary value from which to determine progress
:param int total:
In the case that *value* is set, this must be set to the maximum value
that *value* will take
:param float max_wait:
The minimum length of time that must elapse before a screen update is
permitted
:param file stream:
The stream object that output should be written to, defaults to stderr
:param style:
A reference to a class which will be used to render the progress meter,
defaults to :class:`BarStyle`
:param bool hide_on_finish:
If True (the default), the progress meter will be erased when the
context exits
"""
# pylint: disable=too-many-instance-attributes
def __init__(
self, fileobj=None, value=0, total=None, max_wait=0.1,
stream=sys.stderr, style=BarStyle, hide_on_finish=True):
# pylint: disable=too-many-arguments
if fileobj is None and total is None:
raise ValueError('One of fileobj or total must be specified')
if fileobj is not None and total is not None:
raise ValueError('Only one of fileobj or total can be specified')
self.max_wait = max_wait
self.stream = stream
self.hide_on_finish = hide_on_finish
self.fileobj = fileobj
if fileobj is None:
self.value = value
self.total = total
else:
self.value = self.fileobj.tell()
try:
self.total = self.fileobj.seek(0, io.SEEK_END)
finally:
self.fileobj.seek(self.value, io.SEEK_SET)
self.style = style(self)
self._last_value = self.value
self._last_output = ''
self._last_update = None
[docs] def hide(self):
"""
Hide the progress bar from the console (or whatever the output stream
is connected to).
"""
if self._last_output:
self.stream.write('\b' * len(self._last_output))
self.stream.write(' ' * len(self._last_output))
self.stream.write('\b' * len(self._last_output))
self.stream.flush()
self._last_output = ''
self._last_update = None
[docs] def show(self):
"""
Show the progress bar on the console (or whatever the output stream
is connected to).
"""
self._render()
self._last_update = time.time()
[docs] def update(self, value=None):
"""
Update the progress bar to position *value* (which must be less than
the *total* value passed to the constructor).
"""
if value is None:
value = self.fileobj.tell()
self.value = value
if value != self._last_value:
now = time.time()
if self._last_update is None or now > (self._last_update +
self.max_wait):
self.hide()
self._last_value = value
self._render()
self._last_update = now
def _render(self):
self._last_output = self.style.render(self._last_value, self.total)
self.stream.write(self._last_output)
self.stream.flush()
def __enter__(self):
self.show()
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.hide()
if not self.hide_on_finish:
self._last_value = self.value
self._render()
self.stream.write('\n')