Skip to content

Commit

Permalink
bump version, Merge branch 'postfix'
Browse files Browse the repository at this point in the history
  • Loading branch information
casperdcl committed Jan 12, 2017
2 parents 56d1518 + cdf7ab7 commit 1047ac2
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 16 deletions.
37 changes: 36 additions & 1 deletion README.rst
Expand Up @@ -251,7 +251,8 @@ Documentation
file=sys.stderr, ncols=None, mininterval=0.1,
maxinterval=10.0, miniters=None, ascii=None, disable=False,
unit='it', unit_scale=False, dynamic_ncols=False,
smoothing=0.3, bar_format=None, initial=0, position=None):
smoothing=0.3, bar_format=None, initial=0, position=None,
postfix=None):
Parameters
~~~~~~~~~~
Expand Down Expand Up @@ -331,6 +332,8 @@ Parameters
Specify the line offset to print this bar (starting from 0)
Automatic if unspecified.
Useful to manage multiple bars at once (eg, from threads).
* postfix : dict, optional
Specify additional stats to display at the end of the bar.

Extra CLI Options
~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -392,6 +395,17 @@ Returns
Print a message via tqdm (without overlap with bars)
"""
def set_description(self, desc=None):
"""
Set/modify description of the progress bar.
"""
def set_postfix(self, **kwargs):
"""
Set/modify postfix (additional stats)
with automatic formatting based on datatype.
"""
def trange(*args, **kwargs):
"""
A shortcut for tqdm(xrange(*args), **kwargs).
Expand Down Expand Up @@ -428,6 +442,27 @@ Examples and Advanced Usage
- consult the `wiki <https://github.com/tqdm/tqdm/wiki>`__.
- this has an `excellent article <https://github.com/tqdm/tqdm/wiki/How-to-make-a-great-Progress-Bar>`__ on how to make a **great** progressbar.

Description and additional stats
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Custom information can be displayed and updated dynamically on ``tqdm`` bars
with the ``desc`` and ``postfix`` arguments:

.. code:: python
from tqdm import trange
from random import random, randint
from time import sleep
t = trange(100)
for i in t:
# Description will be displayed on the left
t.set_description('GEN %i' % i)
# Postfix will be displayed on the right, and will format automatically
# based on argument's datatype
t.set_postfix(loss=random(), gen=randint(1,999), str='h', lst=[1, 2])
sleep(0.1)
Nested progress bars
~~~~~~~~~~~~~~~~~~~~

Expand Down
65 changes: 51 additions & 14 deletions tqdm/_tqdm.py
Expand Up @@ -8,13 +8,14 @@
... ...
"""
from __future__ import absolute_import
# future division is important to divide integers and get as
# a result precise floating numbers (instead of truncated int)
# integer division / : float, // : int
from __future__ import division
# import compatibility functions and utilities
# compatibility functions and utilities
from ._utils import _supports_unicode, _environ_cols_wrapper, _range, _unich, \
_term_move_up, _unicode, WeakSet
_term_move_up, _unicode, WeakSet, _basestring, _OrderedDict
# native libraries
import sys
from numbers import Number
from threading import Thread
from time import time
from time import sleep
Expand Down Expand Up @@ -199,7 +200,7 @@ def print_status(s):
@staticmethod
def format_meter(n, total, elapsed, ncols=None, prefix='',
ascii=False, unit='it', unit_scale=False, rate=None,
bar_format=None):
bar_format=None, postfix=None):
"""
Return a string-based progress bar given some parameters
Expand Down Expand Up @@ -240,6 +241,8 @@ def format_meter(n, total, elapsed, ncols=None, prefix='',
'| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'
Possible vars: bar, n, n_fmt, total, total_fmt, percentage,
rate, rate_fmt, elapsed, remaining, l_bar, r_bar, desc.
postfix : str, optional
Same as prefix but will be placed at the end as additional stats.
Returns
-------
Expand Down Expand Up @@ -284,8 +287,9 @@ def format_meter(n, total, elapsed, ncols=None, prefix='',
# format the stats displayed to the left and right sides of the bar
l_bar = (prefix if prefix else '') + \
'{0:3.0f}%|'.format(percentage)
r_bar = '| {0}/{1} [{2}<{3}, {4}]'.format(
n_fmt, total_fmt, elapsed_str, remaining_str, rate_fmt)
r_bar = '| {0}/{1} [{2}<{3}, {4}{5}]'.format(
n_fmt, total_fmt, elapsed_str, remaining_str, rate_fmt,
', '+postfix if postfix else '')

if ncols == 0:
return l_bar[:-1] + r_bar[1:]
Expand All @@ -310,6 +314,7 @@ def format_meter(n, total, elapsed, ncols=None, prefix='',
'l_bar': l_bar,
'r_bar': r_bar,
'desc': prefix if prefix else '',
'postfix': ', '+postfix if postfix else '',
# 'bar': full_bar # replaced by procedure below
}

Expand Down Expand Up @@ -358,8 +363,9 @@ def format_meter(n, total, elapsed, ncols=None, prefix='',

# no total: no progressbar, ETA, just progress stats
else:
return (prefix if prefix else '') + '{0}{1} [{2}, {3}]'.format(
n_fmt, unit, elapsed_str, rate_fmt)
return (prefix if prefix else '') + '{0}{1} [{2}, {3}{4}]'.format(
n_fmt, unit, elapsed_str, rate_fmt,
', '+postfix if postfix else '')

def __new__(cls, *args, **kwargs):
# Create a new instance
Expand Down Expand Up @@ -540,6 +546,7 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
maxinterval=10.0, miniters=None, ascii=None, disable=False,
unit='it', unit_scale=False, dynamic_ncols=False,
smoothing=0.3, bar_format=None, initial=0, position=None,
postfix=None,
gui=False, **kwargs):
"""
Parameters
Expand Down Expand Up @@ -619,6 +626,8 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
Specify the line offset to print this bar (starting from 0)
Automatic if unspecified.
Useful to manage multiple bars at once (eg, from threads).
postfix : dict, optional
Specify additional stats to display at the end of the bar.
gui : bool, optional
WARNING: internal parameter - do not use.
Use tqdm_gui(...) instead. If set, will attempt to use
Expand Down Expand Up @@ -707,6 +716,9 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
self.avg_time = None
self._time = time
self.bar_format = bar_format
self.postfix = None
if postfix:
self.set_postfix(**postfix)

# Init the iterations counters
self.last_print_n = initial
Expand All @@ -723,7 +735,8 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
self.moveto(self.pos)
self.sp(self.format_meter(self.n, total, 0,
(dynamic_ncols(file) if dynamic_ncols else ncols),
self.desc, ascii, unit, unit_scale, None, bar_format))
self.desc, ascii, unit, unit_scale, None, bar_format,
self.postfix))
if self.pos:
self.moveto(-self.pos)

Expand Down Expand Up @@ -752,7 +765,8 @@ def __repr__(self):
self._time() - self.start_t,
self.ncols, self.desc, self.ascii, self.unit,
self.unit_scale, 1 / self.avg_time
if self.avg_time else None, self.bar_format)
if self.avg_time else None, self.bar_format,
self.postfix)

def __lt__(self, other):
return self.pos < other.pos
Expand Down Expand Up @@ -842,7 +856,8 @@ def __iter__(self):
(dynamic_ncols(self.fp) if dynamic_ncols
else ncols),
self.desc, ascii, unit, unit_scale,
1 / avg_time if avg_time else None, bar_format))
1 / avg_time if avg_time else None, bar_format,
self.postfix))

if self.pos:
self.moveto(-self.pos)
Expand Down Expand Up @@ -938,7 +953,7 @@ def update(self, n=1):
else self.ncols),
self.desc, self.ascii, self.unit, self.unit_scale,
1 / self.avg_time if self.avg_time else None,
self.bar_format))
self.bar_format, self.postfix))

if self.pos:
self.moveto(-self.pos)
Expand Down Expand Up @@ -1010,7 +1025,7 @@ def fp_write(s):
(self.dynamic_ncols(self.fp) if self.dynamic_ncols
else self.ncols),
self.desc, self.ascii, self.unit, self.unit_scale, None,
self.bar_format))
self.bar_format, self.postfix))
if pos:
self.moveto(-pos)
else:
Expand All @@ -1036,6 +1051,28 @@ def set_description(self, desc=None):
"""
self.desc = desc + ': ' if desc else ''

def set_postfix(self, ordered_dict=None, **kwargs):
"""
Set/modify postfix (additional stats)
with automatic formatting based on datatype.
"""
# Sort in alphabetical order to be more deterministic
postfix = _OrderedDict([] if ordered_dict is None else ordered_dict)
for key in sorted(kwargs.keys()):
postfix[key] = kwargs[key]
# Preprocess stats according to datatype
for key in postfix.keys():
# Number: limit the length of the string
if isinstance(postfix[key], Number):
postfix[key] = '{0:2.3g}'.format(postfix[key])
# Else for any other type, try to get the string conversion
elif not isinstance(postfix[key], _basestring):
postfix[key] = str(postfix[key])
# Else if it's a string, don't need to preprocess anything
# Stitch together to get the final postfix
self.postfix = ', '.join(key + '=' + postfix[key].strip()
for key in postfix.keys())

def moveto(self, n):
self.fp.write(_unicode('\n' * n + _term_move_up() * -n))

Expand Down
77 changes: 77 additions & 0 deletions tqdm/_utils.py
Expand Up @@ -8,6 +8,7 @@
['CYGWIN', 'MSYS', 'Linux', 'Darwin', 'SunOS', 'FreeBSD'])


# Py2/3 compat. Empty conditional to avoid coverage
if True: # pragma: no cover
try:
_range = xrange
Expand Down Expand Up @@ -38,6 +39,82 @@
except ImportError:
WeakSet = set

try:
_basestring = basestring
except NameError:
_basestring = str

try: # py>=2.7,>=3.1
from collections import OrderedDict as _OrderedDict
except ImportError:
try: # older Python versions with backported ordereddict lib
from ordereddict import OrderedDict as _OrderedDict
except ImportError: # older Python versions without ordereddict lib
# Py2.6,3.0 compat, from PEP 372
from collections import MutableMapping
class _OrderedDict(dict, MutableMapping):
# Methods with direct access to underlying attributes
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at 1 argument, got %d', len(args))
if not hasattr(self, '_keys'):
self._keys = []
self.update(*args, **kwds)

def clear(self):
del self._keys[:]
dict.clear(self)

def __setitem__(self, key, value):
if key not in self:
self._keys.append(key)
dict.__setitem__(self, key, value)

def __delitem__(self, key):
dict.__delitem__(self, key)
self._keys.remove(key)

def __iter__(self):
return iter(self._keys)

def __reversed__(self):
return reversed(self._keys)

def popitem(self):
if not self:
raise KeyError
key = self._keys.pop()
value = dict.pop(self, key)
return key, value

def __reduce__(self):
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
inst_dict.pop('_keys', None)
return (self.__class__, (items,), inst_dict)

# Methods with indirect access via the above methods
setdefault = MutableMapping.setdefault
update = MutableMapping.update
pop = MutableMapping.pop
keys = MutableMapping.keys
values = MutableMapping.values
items = MutableMapping.items

def __repr__(self):
pairs = ', '.join(map('%r: %r'.__mod__, self.items()))
return '%s({%s})' % (self.__class__.__name__, pairs)

def copy(self):
return self.__class__(self)

@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d


def _is_utf(encoding):
return encoding.lower().startswith('utf-') or ('U8' == encoding)
Expand Down
2 changes: 1 addition & 1 deletion tqdm/_version.py
Expand Up @@ -8,7 +8,7 @@
__all__ = ["__version__"]

# major, minor, patch, -extra
version_info = 4, 10, 0
version_info = 4, 11, 0

# Nice string for the version
__version__ = '.'.join(map(str, version_info))
Expand Down
40 changes: 40 additions & 0 deletions tqdm/tests/tests_tqdm.py
Expand Up @@ -1509,3 +1509,43 @@ class fake_tqdm(object):
sleep(0.000001)
assert t1.miniters == 1 # check that monitor corrected miniters
assert t2.miniters == 500 # check that t2 was not adjusted


@with_setup(pretest, posttest)
def test_postfix():
"""Test postfix"""
postfix = {'float': 0.321034, 'gen': 543, 'str': 'h', 'lst': [2]}
postfix_order = (('w', 'w'), ('a', 0)) # no need for a OrderedDict, set is OK
expected = ['float=0.321', 'gen=543', 'lst=[2]', 'str=h']
expected_order = ['w=w', 'a=0', 'float=0.321', 'gen=543', 'lst=[2]', 'str=h']

# Test postfix set at init
with closing(StringIO()) as our_file:
with tqdm(total=10, file=our_file, desc='pos0 bar',
bar_format='{r_bar}', postfix=postfix) as t1:
t1.refresh()
out = our_file.getvalue()

# Test postfix set after init
with closing(StringIO()) as our_file:
with trange(10, file=our_file, desc='pos1 bar',
bar_format='{r_bar}', postfix=None) as t2:
t2.set_postfix(**postfix)
t2.refresh()
out2 = our_file.getvalue()

# Order of items in dict may change, so need a loop to check per item
for res in expected:
assert res in out
assert res in out2

# Test postfix set after init and with ordered dict
with closing(StringIO()) as our_file:
with trange(10, file=our_file, desc='pos2 bar',
bar_format='{r_bar}', postfix=None) as t3:
t3.set_postfix(postfix_order, **postfix)
t3.refresh()
out3 = our_file.getvalue()

out3 = out3[1:-1].split(', ')[3:]
assert out3 == expected_order

0 comments on commit 1047ac2

Please sign in to comment.