Source code for progressive.bar

# -*- coding: utf-8 -*-

from __future__ import division
from __future__ import unicode_literals

from progressive.cursor import Cursor
from progressive.util import floor, ensure, u
from progressive.exceptions import ColorUnsupportedError, WidthOverflowError


[docs]class Bar(object): """Progress Bar with blessings Several parts of this class are thanks to Erik Rose's implementation of ``ProgressBar`` in ``nose-progressive``, licensed under The MIT License. `MIT <http://opensource.org/licenses/MIT>`_ `nose-progressive/noseprogressive/bar.py <https://github.com/erikrose/nose-progressive/blob/master/noseprogressive/bar.py>`_ Terminal with 256 colors is recommended. See `this <http://pastelinux.wordpress.com/2010/12/01/upgrading-linux-terminal-to-256-colors/>`_ for Ubuntu installation as an example. :type term: blessings.Terminal|NoneType :param term: blessings.Terminal instance for the terminal of display :type max_value: int :param max_value: The capacity of the bar, i.e., ``value/max_value`` :type width: str :param width: Must be of format {num: int}{unit: c|%}. Unit "c" can be used to specify number of maximum columns; unit "%". to specify percentage of the total terminal width to use. e.g., "20c", "25%", etc. :type title_pos: str :param title_pos: Position of title relative to the progress bar; can be any one of ["left", "right", "above", "below"] :type title: str :param title: Title of the progress bar :type num_rep: str :param num_rep: Numeric representation of completion; can be one of ["fraction", "percentage"] :type indent: int :param indent: Spaces to indent the bar from the left-hand side :type filled_color: str|int :param filled_color: color of the ``filled_char``; can be a string of the color's name or number representing the color; see the ``blessings`` documentation for details :type empty_color: str|int :param empty_color: color of the ``empty_char`` :type back_color: str|NoneType :param back_color: Background color of the progress bar; must be a string of the color name, unused if numbers used for ``filled_color`` and ``empty_color``. If set to None, will not be used. :type filled_char: unicode :param filled_char: Character representing completeness on the progress bar :type empty_char: unicode :param empty_char: The complement to ``filled_char`` :type start_char: unicode :param start_char: Character at the start of the progress bar :type end_char: unicode :param end_char: Character at the end of the progress bar :type fallback: bool :param fallback: If this is set, if the terminal does not support provided colors, this will fall back to plain formatting that works on terminals with no color support, using the provided ``fallback_empty_char` and ``fallback_filled_char`` :type force_color: bool|NoneType :param force_color: ``True`` forces color to be used even if it may not be supported by the terminal; ``False`` forces use of the fallback formatting; ``None`` does not force anything and allows automatic detection as usual. """ def __init__( self, term=None, max_value=100, width="25%", title_pos="left", title="Progress", num_rep="fraction", indent=0, filled_color=2, empty_color=7, back_color=None, filled_char=u' ', empty_char=u' ', start_char=u'', end_char=u'', fallback=True, fallback_empty_char=u'◯', fallback_filled_char=u'◉', force_color=None ): self.cursor = Cursor(term) self.term = self.cursor.term self._measure_terminal() self._width_str = width self._max_value = max_value ensure(title_pos in ["left", "right", "above", "below"], ValueError, "Invalid choice for title position.") self._title_pos = title_pos self._title = title ensure(num_rep in ["fraction", "percentage"], ValueError, "num_rep must be either 'fraction' or 'percentage'.") self._num_rep = num_rep ensure(indent < self.columns, ValueError, "Indent must be smaller than terminal width.") self._indent = indent self._start_char = start_char self._end_char = end_char # Setup callables and characters depending on if terminal has # has color support if force_color is not None: supports_colors = force_color else: supports_colors = self._supports_colors( term=self.term, raise_err=not fallback, colors=(filled_color, empty_color) ) if supports_colors: self._filled_char = filled_char self._empty_char = empty_char self._filled = self._get_format_callable( term=self.term, color=filled_color, back_color=back_color ) self._empty = self._get_format_callable( term=self.term, color=empty_color, back_color=back_color ) else: self._empty_char = fallback_empty_char self._filled_char = fallback_filled_char self._filled = self._empty = lambda s: s ensure(self.full_line_width <= self.columns, WidthOverflowError, "Attempting to initialize Bar with full_line_width {}; " "terminal has width of only {}.".format( self.full_line_width, self.columns)) ###################### # Public Attributes # ###################### @property def max_width(self): """Get maximum width of progress bar :rtype: int :returns: Maximum column width of progress bar """ value, unit = float(self._width_str[:-1]), self._width_str[-1] ensure(unit in ["c", "%"], ValueError, "Width unit must be either 'c' or '%'") if unit == "c": ensure(value <= self.columns, ValueError, "Terminal only has {} columns, cannot draw " "bar of size {}.".format(self.columns, value)) retval = value else: # unit == "%" ensure(0 < value <= 100, ValueError, "value=={} does not satisfy 0 < value <= 100".format(value)) dec = value / 100 retval = dec * self.columns return floor(retval) @property def full_line_width(self): """Find actual length of bar_str e.g., Progress [ | ] 10/10 """ bar_str_len = sum([ self._indent, ((len(self.title) + 1) if self._title_pos in ["left", "right"] else 0), # Title if present len(self.start_char), self.max_width, # Progress bar len(self.end_char), 1, # Space between end_char and amount_complete_str len(str(self.max_value)) * 2 + 1 # 100/100 ]) return bar_str_len @property def filled(self): """Callable for drawing filled portion of progress bar :rtype: callable """ return self._filled @property def empty(self): """Callable for drawing empty portion of progress bar :rtype: callable """ return self._empty @property def max_value(self): """The capacity of the bar, i.e., ``value/max_value``""" return self._max_value @max_value.setter def max_value(self, val): self._max_value = val @property def title(self): """Title of the progress bar""" return self._title @title.setter def title(self, t): self._title = t @property def start_char(self): """Character at the start of the progress bar""" return self._start_char @start_char.setter def start_char(self, c): self._start_char = c @property def end_char(self): """Character at the end of the progress bar""" return self._end_char @end_char.setter def end_char(self, c): self._end_char = c ################### # Private Methods # ################### @staticmethod def _supports_colors(term, raise_err, colors): """Check if ``term`` supports ``colors`` :raises ColorUnsupportedError: This is raised if ``raise_err`` is ``False`` and a color in ``colors`` is unsupported by ``term`` :type raise_err: bool :param raise_err: Set to ``False`` to return a ``bool`` indicating color support rather than raising ColorUnsupportedError :type colors: [str, ...] """ for color in colors: try: if isinstance(color, str): req_colors = 16 if "bright" in color else 8 ensure(term.number_of_colors >= req_colors, ColorUnsupportedError, "{} is unsupported by your terminal.".format(color)) elif isinstance(color, int): ensure(term.number_of_colors >= color, ColorUnsupportedError, "{} is unsupported by your terminal.".format(color)) except ColorUnsupportedError as e: if raise_err: raise e else: return False else: return True @staticmethod def _get_format_callable(term, color, back_color): """Get string-coloring callable Get callable for string output using ``color`` on ``back_color`` on ``term`` :param term: blessings.Terminal instance :param color: Color that callable will color the string it's passed :param back_color: Back color for the string :returns: callable(s: str) -> str """ if isinstance(color, str): ensure( any(isinstance(back_color, t) for t in [str, type(None)]), TypeError, "back_color must be a str or NoneType" ) if back_color: return getattr(term, "_".join( [color, "on", back_color] )) elif back_color is None: return getattr(term, color) elif isinstance(color, int): return term.on_color(color) else: raise TypeError("Invalid type {} for color".format( type(color) )) def _measure_terminal(self): self.lines, self.columns = ( self.term.height or 24, self.term.width or 80 ) def _write(self, s, s_length=None, flush=False, ignore_overflow=False, err_msg=None): """Write ``s`` :type s: str|unicode :param s: String to write :param s_length: Custom length of ``s`` :param flush: Set this to flush the terminal stream after writing :param ignore_overflow: Set this to ignore if s will exceed the terminal's width :param err_msg: The error message given to WidthOverflowError if it is triggered """ if not ignore_overflow: s_length = len(s) if s_length is None else s_length if err_msg is None: err_msg = ( "Terminal has {} columns; attempted to write " "a string {} of length {}.".format( self.columns, repr(s), s_length) ) ensure(s_length <= self.columns, WidthOverflowError, err_msg) self.cursor.write(s) if flush: self.cursor.flush() ################## # Public Methods # ##################
[docs] def draw(self, value, newline=True, flush=True): """Draw the progress bar :type value: int :param value: Progress value relative to ``self.max_value`` :type newline: bool :param newline: If this is set, a newline will be written after drawing """ # This is essentially winch-handling without having # to do winch-handling; cleanly redrawing on winch is difficult # and out of the intended scope of this class; we *can* # however, adjust the next draw to be proper by re-measuring # the terminal since the code is mostly written dynamically # and many attributes and dynamically calculated properties. self._measure_terminal() # To avoid zero division, set amount_complete to 100% if max_value has been stupidly set to 0 amount_complete = 1.0 if self.max_value == 0 else value / self.max_value fill_amount = int(floor(amount_complete * self.max_width)) empty_amount = self.max_width - fill_amount # e.g., '10/20' if 'fraction' or '50%' if 'percentage' amount_complete_str = ( u"{}/{}".format(value, self.max_value) if self._num_rep == "fraction" else u"{}%".format(int(floor(amount_complete * 100))) ) # Write title if supposed to be above if self._title_pos == "above": title_str = u"{}{}\n".format( " " * self._indent, self.title, ) self._write(title_str, ignore_overflow=True) # Construct just the progress bar bar_str = u''.join([ u(self.filled(self._filled_char * fill_amount)), u(self.empty(self._empty_char * empty_amount)), ]) # Wrap with start and end character bar_str = u"{}{}{}".format(self.start_char, bar_str, self.end_char) # Add on title if supposed to be on left or right if self._title_pos == "left": bar_str = u"{} {}".format(self.title, bar_str) elif self._title_pos == "right": bar_str = u"{} {}".format(bar_str, self.title) # Add indent bar_str = u''.join([" " * self._indent, bar_str]) # Add complete percentage or fraction bar_str = u"{} {}".format(bar_str, amount_complete_str) # Set back to normal after printing bar_str = u"{}{}".format(bar_str, self.term.normal) # Finally, write the completed bar_str self._write(bar_str, s_length=self.full_line_width) # Write title if supposed to be below if self._title_pos == "below": title_str = u"\n{}{}".format( " " * self._indent, self.title, ) self._write(title_str, ignore_overflow=True) # Newline to wrap up if newline: self.cursor.newline() if flush: self.cursor.flush()