from __future__ import division
from copy import deepcopy
from progressive.bar import Bar
from progressive.cursor import Cursor
from progressive.util import floor, ensure, merge_dicts
from progressive.exceptions import LengthOverflowError
[docs]class Value(object):
"""Container class for use with ``BarDescriptor``
Should be used for ``value`` argument when initializing
``BarDescriptor``, e.g., ``BarDescriptor(type=..., value=Value(10))``
"""
def __init__(self, val=0):
self.value = val
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = floor(val)
[docs]class BarDescriptor(dict):
"""Bar descriptor
To be used in leaf of a tree describing a hierarchy for ``ProgressTree``,
e.g.,:
tree = {"Job":
{"Task1": BarDescriptor(...)},
{"Task2":
{"Subtask1": BarDescriptor(...)},
},
}
:type type: Bar|subclass of Bar
:param type: The type of Bar to use to display that leaf
:type value: Value
:param value: Amount to fill the progress bar vs. its max value
:type args: list
:param args: A list of args to instantiate ``type`` with
:type kwargs: dict
:param kwargs: A dict of kwargs to instantiate ``type`` with
"""
[docs]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 #
##################
[docs] 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()
[docs] 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)
[docs] 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)