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))
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))
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 # ################## 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() amount_complete = 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()
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 # ################## 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() amount_complete = 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()
def __init__(self, term=None, indent=4): self.cursor = Cursor(term) self.indent = indent
class ProgressTree(object): """Progress display for trees For drawing a hierarchical progress view from a tree :type term: NoneType|blessings.Terminal :param term: Terminal instance; if not given, will be created by the class :type indent: int :param indent: The amount of indentation between each level in hierarchy """ def __init__(self, term=None, indent=4): self.cursor = Cursor(term) self.indent = indent ################## # Public Methods # ################## def draw(self, tree, bar_desc=None, save_cursor=True, flush=True): """Draw ``tree`` to the terminal :type tree: dict :param tree: ``tree`` should be a tree representing a hierarchy; each key should be a string describing that hierarchy level and value should also be ``dict`` except for leaves which should be ``BarDescriptors``. See ``BarDescriptor`` for a tree example. :type bar_desc: BarDescriptor|NoneType :param bar_desc: For describing non-leaf bars in that will be drawn from ``tree``; certain attributes such as ``value`` and ``kwargs["max_value"]`` will of course be overridden if provided. :type flush: bool :param flush: If this is set, output written will be flushed :type save_cursor: bool :param save_cursor: If this is set, cursor location will be saved before drawing; this will OVERWRITE a previous save, so be sure to set this accordingly (to your needs). """ if save_cursor: self.cursor.save() tree = deepcopy(tree) # TODO: Automatically collapse hierarchy so something # will always be displayable (well, unless the top-level) # contains too many to display lines_required = self.lines_required(tree) ensure(lines_required <= self.cursor.term.height, LengthOverflowError, "Terminal is not long ({} rows) enough to fit all bars " "({} rows).".format(self.cursor.term.height, lines_required)) bar_desc = BarDescriptor(type=Bar) if not bar_desc else bar_desc self._calculate_values(tree, bar_desc) self._draw(tree) if flush: self.cursor.flush() def make_room(self, tree): """Clear lines in terminal below current cursor position as required This is important to do before drawing to ensure sufficient room at the bottom of your terminal. :type tree: dict :param tree: tree as described in ``BarDescriptor`` """ lines_req = self.lines_required(tree) self.cursor.clear_lines(lines_req) def lines_required(self, tree, count=0): """Calculate number of lines required to draw ``tree``""" if all([ isinstance(tree, dict), type(tree) != BarDescriptor ]): return sum(self.lines_required(v, count=count) for v in tree.values()) + 2 elif isinstance(tree, BarDescriptor): if tree.get("kwargs", {}).get("title_pos") in ["left", "right"]: return 1 else: return 2 ################### # Private Methods # ################### def _calculate_values(self, tree, bar_d): """Calculate values for drawing bars of non-leafs in ``tree`` Recurses through ``tree``, replaces ``dict``s with ``(BarDescriptor, dict)`` so ``ProgressTree._draw`` can use the ``BarDescriptor``s to draw the tree """ if all([ isinstance(tree, dict), type(tree) != BarDescriptor ]): # Calculate value and max_value max_val = 0 value = 0 for k in tree: # Get descriptor by recursing bar_desc = self._calculate_values(tree[k], bar_d) # Reassign to tuple of (new descriptor, tree below) tree[k] = (bar_desc, tree[k]) value += bar_desc["value"].value max_val += bar_desc.get("kwargs", {}).get("max_value", 100) # Merge in values from ``bar_d`` before returning descriptor kwargs = merge_dicts( [bar_d.get("kwargs", {}), dict(max_value=max_val)], deepcopy=True ) ret_d = merge_dicts( [bar_d, dict(value=Value(floor(value)), kwargs=kwargs)], deepcopy=True ) return BarDescriptor(ret_d) elif isinstance(tree, BarDescriptor): return tree else: raise TypeError("Unexpected type {}".format(type(tree))) def _draw(self, tree, indent=0): """Recurse through ``tree`` and draw all nodes""" if all([ isinstance(tree, dict), type(tree) != BarDescriptor ]): for k, v in sorted(tree.items()): bar_desc, subdict = v[0], v[1] args = [self.cursor.term] + bar_desc.get("args", []) kwargs = dict(title_pos="above", indent=indent, title=k) kwargs.update(bar_desc.get("kwargs", {})) b = Bar(*args, **kwargs) b.draw(value=bar_desc["value"].value, flush=False) self._draw(subdict, indent=indent + self.indent)
class ProgressTree(object): """Progress display for trees For drawing a hierarchical progress view from a tree :type term: NoneType|blessings.Terminal :param term: Terminal instance; if not given, will be created by the class :type indent: int :param indent: The amount of indentation between each level in hierarchy """ def __init__(self, term=None, indent=4): self.cursor = Cursor(term) self.indent = indent ################## # Public Methods # ################## def draw(self, tree, bar_desc=None, save_cursor=True, flush=True): """Draw ``tree`` to the terminal :type tree: dict :param tree: ``tree`` should be a tree representing a hierarchy; each key should be a string describing that hierarchy level and value should also be ``dict`` except for leaves which should be ``BarDescriptors``. See ``BarDescriptor`` for a tree example. :type bar_desc: BarDescriptor|NoneType :param bar_desc: For describing non-leaf bars in that will be drawn from ``tree``; certain attributes such as ``value`` and ``kwargs["max_value"]`` will of course be overridden if provided. :type flush: bool :param flush: If this is set, output written will be flushed :type save_cursor: bool :param save_cursor: If this is set, cursor location will be saved before drawing; this will OVERWRITE a previous save, so be sure to set this accordingly (to your needs). """ if save_cursor: self.cursor.save() tree = deepcopy(tree) # TODO: Automatically collapse hierarchy so something # will always be displayable (well, unless the top-level) # contains too many to display lines_required = self.lines_required(tree) ensure( lines_required <= self.cursor.term.height, LengthOverflowError, "Terminal is not long ({} rows) enough to fit all bars " "({} rows).".format(self.cursor.term.height, lines_required)) bar_desc = BarDescriptor(type=Bar) if not bar_desc else bar_desc self._calculate_values(tree, bar_desc) self._draw(tree) if flush: self.cursor.flush() def make_room(self, tree): """Clear lines in terminal below current cursor position as required This is important to do before drawing to ensure sufficient room at the bottom of your terminal. :type tree: dict :param tree: tree as described in ``BarDescriptor`` """ lines_req = self.lines_required(tree) self.cursor.clear_lines(lines_req) def lines_required(self, tree, count=0): """Calculate number of lines required to draw ``tree``""" if all([isinstance(tree, dict), type(tree) != BarDescriptor]): return sum( self.lines_required(v, count=count) for v in tree.values()) + 2 elif isinstance(tree, BarDescriptor): if tree.get("kwargs", {}).get("title_pos") in ["left", "right"]: return 1 else: return 2 ################### # Private Methods # ################### def _calculate_values(self, tree, bar_d): """Calculate values for drawing bars of non-leafs in ``tree`` Recurses through ``tree``, replaces ``dict``s with ``(BarDescriptor, dict)`` so ``ProgressTree._draw`` can use the ``BarDescriptor``s to draw the tree """ if all([isinstance(tree, dict), type(tree) != BarDescriptor]): # Calculate value and max_value max_val = 0 value = 0 for k in tree: # Get descriptor by recursing bar_desc = self._calculate_values(tree[k], bar_d) # Reassign to tuple of (new descriptor, tree below) tree[k] = (bar_desc, tree[k]) value += bar_desc["value"].value max_val += bar_desc.get("kwargs", {}).get("max_value", 100) # Merge in values from ``bar_d`` before returning descriptor kwargs = merge_dicts( [bar_d.get("kwargs", {}), dict(max_value=max_val)], deepcopy=True) ret_d = merge_dicts( [bar_d, dict(value=Value(floor(value)), kwargs=kwargs)], deepcopy=True) return BarDescriptor(ret_d) elif isinstance(tree, BarDescriptor): return tree else: raise TypeError("Unexpected type {}".format(type(tree))) def _draw(self, tree, indent=0): """Recurse through ``tree`` and draw all nodes""" if all([isinstance(tree, dict), type(tree) != BarDescriptor]): for k, v in sorted(tree.items()): bar_desc, subdict = v[0], v[1] args = [self.cursor.term] + bar_desc.get("args", []) kwargs = dict(title_pos="above", indent=indent, title=k) kwargs.update(bar_desc.get("kwargs", {})) b = Bar(*args, **kwargs) b.draw(value=bar_desc["value"].value, flush=False) self._draw(subdict, indent=indent + self.indent)