Repository: tqdm/tqdm (Python)

Issue:
Colored Progress Bars
With a few tweaks in the code I managed to add a color argument to tqdm.

![newgif1](https://user-images.githubusercontent.com/17583740/30978247-5925ad1e-a450-11e7-9f05-7ea07a46413b.gif)

Is this already an implemented feature ? Anyone likes it ?




notebook: fix description width for unknown `total`
- split bar description into separate widget (fixes #1031)
- improve `reset()`
  + reset bar width if needed
  + reset bar colour
- related #1015

Unfortunately this may break code of a handful of people parsing `tqdm.notebook.tqdm.container.children` since it's changed from `[IProgress(), HTML()]` to `[HTML(), IProgress(), HTML()]`. Technically a breaking change but don't think it affects the vast majority of users.


Hints:
colours seem a bit distracting. If you do want them:

```python
from tqdm import trange
from colorama import Fore


for i in trange(int(7e7),
                bar_format="{l_bar}%s{bar}%s{r_bar}" % (Fore.BLUE, Fore.RESET)):
    pass
```


Patch:
diff --git a/tqdm/notebook.py b/tqdm/notebook.py
--- a/tqdm/notebook.py
+++ b/tqdm/notebook.py
@@ -106,14 +106,11 @@ def status_printer(_, total=None, desc=None, ncols=None):
             if ncols is None:
                 pbar.layout.width = "20px"
 
+        ltext = HTML()
+        rtext = HTML()
         if desc:
-            pbar.description = desc
-            if IPYW >= 7:
-                pbar.style.description_width = 'initial'
-        # Prepare status text
-        ptext = HTML()
-        # Only way to place text to the right of the bar is to use a container
-        container = HBox(children=[pbar, ptext])
+            ltext.value = desc
+        container = HBox(children=[ltext, pbar, rtext])
         # Prepare layout
         if ncols is not None:  # use default style of ipywidgets
             # ncols could be 100, "100px", "100%"
@@ -151,7 +148,7 @@ def display(self, msg=None, pos=None,
         if not msg and not close:
             msg = self.__repr__()
 
-        pbar, ptext = self.container.children
+        ltext, pbar, rtext = self.container.children
         pbar.value = self.n
 
         if msg:
@@ -168,13 +165,10 @@ def display(self, msg=None, pos=None,
                 right = right[1:]
 
             # Update description
-            pbar.description = left
-            if IPYW >= 7:
-                pbar.style.description_width = 'initial'
-
+            ltext.value = left
             # never clear the bar (signal: msg='')
             if right:
-                ptext.value = right
+                rtext.value = right
 
         # Change bar style
         if bar_style:
@@ -190,6 +184,16 @@ def display(self, msg=None, pos=None,
             except AttributeError:
                 self.container.visible = False
 
+    @property
+    def colour(self):
+        if hasattr(self, 'container'):
+            return self.container.children[-2].style.bar_color
+
+    @colour.setter
+    def colour(self, bar_color):
+        if hasattr(self, 'container'):
+            self.container.children[-2].style.bar_color = bar_color
+
     def __init__(self, *args, **kwargs):
         kwargs = kwargs.copy()
         # Setup default output
@@ -204,6 +208,7 @@ def __init__(self, *args, **kwargs):
                 '{bar}', '<bar/>')
         # convert disable = None to False
         kwargs['disable'] = bool(kwargs.get('disable', False))
+        colour = kwargs.pop('colour', None)
         super(tqdm_notebook, self).__init__(*args, **kwargs)
         if self.disable or not kwargs['gui']:
             self.sp = lambda *_, **__: None
@@ -218,6 +223,7 @@ def __init__(self, *args, **kwargs):
         self.container = self.status_printer(
             self.fp, total, self.desc, self.ncols)
         self.sp = self.display
+        self.colour = colour
 
         # Print initial bar state
         if not self.disable:
@@ -273,9 +279,12 @@ def reset(self, total=None):
         ----------
         total  : int or float, optional. Total to use for the new bar.
         """
+        _, pbar, _ = self.container.children
+        pbar.bar_style = ''
         if total is not None:
-            pbar, _ = self.container.children
             pbar.max = total
+            if not self.total and self.ncols is None:  # no longer unknown total
+                pbar.layout.width = None  # reset width
         return super(tqdm_notebook, self).reset(total=total)
 
 
diff --git a/tqdm/std.py b/tqdm/std.py
--- a/tqdm/std.py
+++ b/tqdm/std.py
@@ -12,7 +12,7 @@
 from .utils import _supports_unicode, _screen_shape_wrapper, _range, _unich, \
     _term_move_up, _unicode, WeakSet, _basestring, _OrderedDict, \
     Comparable, _is_ascii, FormatReplace, disp_len, disp_trim, \
-    SimpleTextIOWrapper, CallbackIOWrapper
+    SimpleTextIOWrapper, DisableOnWriteError, CallbackIOWrapper
 from ._monitor import TMonitor
 # native libraries
 from contextlib import contextmanager
@@ -140,8 +140,13 @@ class Bar(object):
     ASCII = " 123456789#"
     UTF = u" " + u''.join(map(_unich, range(0x258F, 0x2587, -1)))
     BLANK = "  "
+    COLOUR_RESET = '\x1b[0m'
+    COLOUR_RGB = '\x1b[38;2;%d;%d;%dm'
+    COLOURS = dict(BLACK='\x1b[30m', RED='\x1b[31m', GREEN='\x1b[32m',
+                   YELLOW='\x1b[33m', BLUE='\x1b[34m', MAGENTA='\x1b[35m',
+                   CYAN='\x1b[36m', WHITE='\x1b[37m')
 
-    def __init__(self, frac, default_len=10, charset=UTF):
+    def __init__(self, frac, default_len=10, charset=UTF, colour=None):
         if not (0 <= frac <= 1):
             warn("clamping frac to range [0, 1]", TqdmWarning, stacklevel=2)
             frac = max(0, min(1, frac))
@@ -149,6 +154,30 @@ def __init__(self, frac, default_len=10, charset=UTF):
         self.frac = frac
         self.default_len = default_len
         self.charset = charset
+        self.colour = colour
+
+    @property
+    def colour(self):
+        return self._colour
+
+    @colour.setter
+    def colour(self, value):
+        if not value:
+            self._colour = None
+            return
+        try:
+            if value.upper() in self.COLOURS:
+                self._colour = self.COLOURS[value.upper()]
+            elif value[0] == '#' and len(value) == 7:
+                self._colour = self.COLOUR_RGB % tuple(
+                    int(i, 16) for i in (value[1:3], value[3:5], value[5:7]))
+            else:
+                raise KeyError
+        except (KeyError, AttributeError):
+            warn("Unknown colour (%s); valid choices: [hex (#00ff00), %s]" % (
+                 value, ", ".join(self.COLOURS)),
+                 TqdmWarning, stacklevel=2)
+            self._colour = None
 
     def __format__(self, format_spec):
         if format_spec:
@@ -178,8 +207,10 @@ def __format__(self, format_spec):
 
         # whitespace padding
         if bar_length < N_BARS:
-            return bar + frac_bar + \
+            bar = bar + frac_bar + \
                 charset[0] * (N_BARS - bar_length - 1)
+        if self.colour:
+            return self.colour + bar + self.COLOUR_RESET
         return bar
 
 
@@ -309,7 +340,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,
-                     postfix=None, unit_divisor=1000, initial=0,
+                     postfix=None, unit_divisor=1000, initial=0, colour=None,
                      **extra_kwargs):
         """
         Return a string-based progress bar given some parameters
@@ -368,6 +399,8 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False,
             [default: 1000], ignored unless `unit_scale` is True.
         initial  : int or float, optional
             The initial counter value [default: 0].
+        colour  : str, optional
+            Bar colour (e.g. `'green'`, `'#00ff00'`).
 
         Returns
         -------
@@ -442,6 +475,7 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False,
             rate_noinv_fmt=rate_noinv_fmt, rate_inv=inv_rate,
             rate_inv_fmt=rate_inv_fmt,
             postfix=postfix, unit_divisor=unit_divisor,
+            colour=colour,
             # plus more useful definitions
             remaining=remaining_str, remaining_s=remaining,
             l_bar=l_bar, r_bar=r_bar,
@@ -483,7 +517,8 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False,
                 frac,
                 max(1, ncols - disp_len(nobar))
                 if ncols else 10,
-                charset=Bar.ASCII if ascii is True else ascii or Bar.UTF)
+                charset=Bar.ASCII if ascii is True else ascii or Bar.UTF,
+                colour=colour)
             if not _is_ascii(full_bar.charset) and _is_ascii(bar_format):
                 bar_format = _unicode(bar_format)
             res = bar_format.format(bar=full_bar, **format_dict)
@@ -501,7 +536,8 @@ def format_meter(n, total, elapsed, ncols=None, prefix='', ascii=False,
                 0,
                 max(1, ncols - disp_len(nobar))
                 if ncols else 10,
-                charset=Bar.BLANK)
+                charset=Bar.BLANK,
+                colour=colour)
             res = bar_format.format(bar=full_bar, **format_dict)
             return disp_trim(res, ncols) if ncols else res
         else:
@@ -802,7 +838,7 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
                  unit_scale=False, dynamic_ncols=False, smoothing=0.3,
                  bar_format=None, initial=0, position=None, postfix=None,
                  unit_divisor=1000, write_bytes=None, lock_args=None,
-                 nrows=None,
+                 nrows=None, colour=None,
                  gui=False, **kwargs):
         """
         Parameters
@@ -908,6 +944,8 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
             The screen height. If specified, hides nested bars outside this
             bound. If unspecified, attempts to use environment height.
             The fallback is 20.
+        colour  : str, optional
+            Bar colour (e.g. `'green'`, `'#00ff00'`).
         gui  : bool, optional
             WARNING: internal parameter - do not use.
             Use tqdm.gui.tqdm(...) instead. If set, will attempt to use
@@ -929,6 +967,8 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
             file = SimpleTextIOWrapper(
                 file, encoding=getattr(file, 'encoding', None) or 'utf-8')
 
+        file = DisableOnWriteError(file, tqdm_instance=self)
+
         if disable is None and hasattr(file, "isatty") and not file.isatty():
             disable = True
 
@@ -1027,9 +1067,10 @@ def __init__(self, iterable=None, desc=None, total=None, leave=True,
         self.dynamic_ncols = dynamic_ncols
         self.smoothing = smoothing
         self.avg_time = None
-        self._time = time
         self.bar_format = bar_format
         self.postfix = None
+        self.colour = colour
+        self._time = time
         if postfix:
             try:
                 self.set_postfix(refresh=False, **postfix)
@@ -1451,7 +1492,8 @@ def format_dict(self):
             unit_scale=self.unit_scale,
             rate=1 / self.avg_time if self.avg_time else None,
             bar_format=self.bar_format, postfix=self.postfix,
-            unit_divisor=self.unit_divisor, initial=self.initial)
+            unit_divisor=self.unit_divisor, initial=self.initial,
+            colour=self.colour)
 
     def display(self, msg=None, pos=None):
         """
diff --git a/tqdm/utils.py b/tqdm/utils.py
--- a/tqdm/utils.py
+++ b/tqdm/utils.py
@@ -212,6 +212,41 @@ def __eq__(self, other):
         return self._wrapped == getattr(other, '_wrapped', other)
 
 
+class DisableOnWriteError(ObjectWrapper):
+    """
+    Disable the given `tqdm_instance` upon `write()` or `flush()` errors.
+    """
+    @staticmethod
+    def disable_on_exception(tqdm_instance, func):
+        """
+        Quietly set `tqdm_instance.miniters=inf` if `func` raises `errno=5`.
+        """
+        def inner(*args, **kwargs):
+            try:
+                return func(*args, **kwargs)
+            except (IOError, OSError) as e:
+                if e.errno != 5:
+                    raise
+                tqdm_instance.miniters = float('inf')
+            except ValueError as e:
+                if 'closed' not in str(e):
+                    raise
+                tqdm_instance.miniters = float('inf')
+        return inner
+
+    def __init__(self, wrapped, tqdm_instance):
+        super(DisableOnWriteError, self).__init__(wrapped)
+        if hasattr(wrapped, 'write'):
+            self.wrapper_setattr('write', self.disable_on_exception(
+                tqdm_instance, wrapped.write))
+        if hasattr(wrapped, 'flush'):
+            self.wrapper_setattr('flush', self.disable_on_exception(
+                tqdm_instance, wrapped.flush))
+
+    def __eq__(self, other):
+        return self._wrapped == getattr(other, '_wrapped', other)
+
+
 class CallbackIOWrapper(ObjectWrapper):
     def __init__(self, callback, stream, method="read"):
         """


Test patch:
diff --git a/tqdm/tests/tests_perf.py b/tqdm/tests/tests_perf.py
--- a/tqdm/tests/tests_perf.py
+++ b/tqdm/tests/tests_perf.py
@@ -293,13 +293,13 @@ def test_manual_overhead_hard():
     total = int(1e5)
 
     with closing(MockIO()) as our_file:
-        t = tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
-                 mininterval=0, maxinterval=0)
-        a = 0
-        with relative_timer() as time_tqdm:
-            for i in _range(total):
-                a += i
-                t.update(10)
+        with tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
+                  mininterval=0, maxinterval=0) as t:
+            a = 0
+            with relative_timer() as time_tqdm:
+                for i in _range(total):
+                    a += i
+                    t.update(10)
 
         a = 0
         with relative_timer() as time_bench:
@@ -334,7 +334,7 @@ def test_iter_overhead_simplebar_hard():
                 a += i
 
     assert_performance(
-        7.5, 'trange', time_tqdm(), 'simple_progress', time_bench())
+        8, 'trange', time_tqdm(), 'simple_progress', time_bench())
 
 
 @with_setup(pretest, posttest)
@@ -345,13 +345,13 @@ def test_manual_overhead_simplebar_hard():
     total = int(1e4)
 
     with closing(MockIO()) as our_file:
-        t = tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
-                 mininterval=0, maxinterval=0)
-        a = 0
-        with relative_timer() as time_tqdm:
-            for i in _range(total):
-                a += i
-                t.update(10)
+        with tqdm(total=total * 10, file=our_file, leave=True, miniters=1,
+                  mininterval=0, maxinterval=0) as t:
+            a = 0
+            with relative_timer() as time_tqdm:
+                for i in _range(total):
+                    a += i
+                    t.update(10)
 
         simplebar_update = simple_progress(
             total=total * 10, file=our_file, leave=True, miniters=1,
@@ -363,4 +363,4 @@ def test_manual_overhead_simplebar_hard():
                 simplebar_update(10)
 
     assert_performance(
-        7.5, 'tqdm', time_tqdm(), 'simple_progress', time_bench())
+        8, 'tqdm', time_tqdm(), 'simple_progress', time_bench())
diff --git a/tqdm/tests/tests_tqdm.py b/tqdm/tests/tests_tqdm.py
--- a/tqdm/tests/tests_tqdm.py
+++ b/tqdm/tests/tests_tqdm.py
@@ -15,7 +15,7 @@
 
 from tqdm import tqdm
 from tqdm import trange
-from tqdm import TqdmDeprecationWarning
+from tqdm import TqdmDeprecationWarning, TqdmWarning
 from tqdm.std import Bar
 from tqdm.contrib import DummyTqdmFile
 
@@ -1895,7 +1895,7 @@ def test_float_progress():
     with closing(StringIO()) as our_file:
         with trange(10, total=9.6, file=our_file) as t:
             with catch_warnings(record=True) as w:
-                simplefilter("always")
+                simplefilter("always", category=TqdmWarning)
                 for i in t:
                     if i < 9:
                         assert not w
@@ -1987,3 +1987,35 @@ def test_initial():
         out = our_file.getvalue()
         assert '10/19' in out
         assert '19/19' in out
+
+
+@with_setup(pretest, posttest)
+def test_colour():
+    """Test `colour`"""
+    with closing(StringIO()) as our_file:
+        for _ in tqdm(_range(9), file=our_file, colour="#beefed"):
+            pass
+        out = our_file.getvalue()
+        assert '\x1b[38;2;%d;%d;%dm' % (0xbe, 0xef, 0xed) in out
+
+        with catch_warnings(record=True) as w:
+            simplefilter("always", category=TqdmWarning)
+            with tqdm(total=1, file=our_file, colour="charm") as t:
+                assert w
+                t.update()
+            assert "Unknown colour" in str(w[-1].message)
+
+    with closing(StringIO()) as our_file2:
+        for _ in tqdm(_range(9), file=our_file2, colour="blue"):
+            pass
+        out = our_file2.getvalue()
+        assert '\x1b[34m' in out
+
+
+@with_setup(pretest, posttest)
+def test_closed():
+    """Test writing to closed file"""
+    with closing(StringIO()) as our_file:
+        for i in trange(9, file=our_file, miniters=1, mininterval=0):
+            if i == 5:
+                our_file.close()