urwidtrees-1.0.3.dev0/0000755000076500000240000000000013030713502014427 5ustar basstaff00000000000000urwidtrees-1.0.3.dev0/PKG-INFO0000644000076500000240000000042613030713502015526 0ustar basstaff00000000000000Metadata-Version: 1.0 Name: urwidtrees Version: 1.0.3.dev0 Summary: Tree widgets for urwid Home-page: https://github.com/pazz/urwidtrees Author: Patrick Totzke Author-email: patricktotzke@gmail.com License: Licensed under the GNU GPL v3+. Description: UNKNOWN Platform: UNKNOWN urwidtrees-1.0.3.dev0/setup.cfg0000644000076500000240000000007313030713502016250 0ustar basstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 urwidtrees-1.0.3.dev0/setup.py0000755000076500000240000000110413030713354016145 0ustar basstaff00000000000000#!/usr/bin/env python from setuptools import setup # this loads the version string into __version__ with open('urwidtrees/version.py') as f: exec(f.read()) setup( name='urwidtrees', version=__version__, description="Tree widgets for urwid", author="Patrick Totzke", author_email="patricktotzke@gmail.com", url="https://github.com/pazz/urwidtrees", license="Licensed under the GNU GPL v3+.", packages=['urwidtrees'], install_requires=['urwid>=1.1.0', 'mock'], extras_require={ 'docs': [ 'mock', ], }, ) urwidtrees-1.0.3.dev0/urwidtrees/0000755000076500000240000000000013030713502016624 5ustar basstaff00000000000000urwidtrees-1.0.3.dev0/urwidtrees/__init__.py0000644000076500000240000000045613030713367020753 0ustar basstaff00000000000000from .tree import Tree, SimpleTree from .decoration import DecoratedTree, CollapsibleTree from .decoration import IndentedTree, CollapsibleIndentedTree from .decoration import ArrowTree, CollapsibleArrowTree from .nested import NestedTree from .widgets import TreeBox from .version import __version__ urwidtrees-1.0.3.dev0/urwidtrees/decoration.py0000644000076500000240000004663513030713243021345 0ustar basstaff00000000000000# Copyright (C) 2013 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. import urwid from .tree import Tree, SimpleTree class NoSpaceError(Exception): """too little space for requested decoration""" class DecoratedTree(Tree): """ :class:`Tree` that wraps around another :class:`Tree` and allows to read original content as well as decorated versions thereof. """ def __init__(self, content): if not isinstance(content, Tree): # do we need this? content = SimpleTree(content) self._tree = content self.root = self._tree.root def get_decorated(self, pos): """ return widget that consists of the content of original tree at given position plus its decoration. """ return self.decorate(pos, self[pos]) def decorate(self, pos, widget, is_first=True): """ decorate `widget` according to a position `pos` in the original tree. setting `is_first` to False indicates that we are decorating a line that is *part* of the (multi-line) content at this position, but not the first part. This allows to omit incoming arrow heads for example. """ return widget # pass on everything else to the original tree. def parent_position(self, pos): return self._tree.parent_position(pos) def first_child_position(self, pos): return self._tree.first_child_position(pos) def last_child_position(self, pos): return self._tree.last_child_position(pos) def next_sibling_position(self, pos): return self._tree.next_sibling_position(pos) def prev_sibling_position(self, pos): return self._tree.prev_sibling_position(pos) def __getitem__(self, pos): return self._tree[pos] class CollapseMixin(object): """ Mixin for :class:`Tree` that allows to collapse subtrees. This works by overwriting :meth:`[first|last]_child_position `, forcing them to return `None` if the given position is considered collapsed. We use a (given) callable `is_collapsed` that accepts positions and returns a boolean to determine which node is considered collapsed. """ def __init__(self, is_collapsed=lambda pos: False, **kwargs): self._initially_collapsed = is_collapsed self._divergent_positions = [] def is_collapsed(self, pos): """checks if given position is currently collapsed""" collapsed = self._initially_collapsed(pos) if pos in self._divergent_positions: collapsed = not collapsed return collapsed # implement functionality by overwriting local position transformations # TODO: ATM this assumes we are in a wrapper: it uses self._tree. # This is not necessarily true, for example for subclasses of SimpleTree! # maybe define this whole class as a wrapper? def last_child_position(self, pos): if self.is_collapsed(pos): return None return self._tree.last_child_position(pos) def first_child_position(self, pos): if self.is_collapsed(pos): return None return self._tree.first_child_position(pos) def collapsible(self, pos): return not self._tree.is_leaf(pos) def set_position_collapsed(self, pos, is_collapsed): if self.collapsible(pos): if self._initially_collapsed(pos) == is_collapsed: if pos in self._divergent_positions: self._divergent_positions.remove(pos) else: if pos not in self._divergent_positions: self._divergent_positions.append(pos) def toggle_collapsed(self, pos): self.set_position_collapsed(pos, not self.is_collapsed(pos)) def collapse(self, pos): self.set_position_collapsed(pos, True) def collapse_all(self): self.set_collapsed_all(True) def expand_all(self): self.set_collapsed_all(False) def set_collapsed_all(self, is_collapsed): self._initially_collapsed = lambda x: is_collapsed self._divergent_positions = [] def expand(self, pos): self.set_position_collapsed(pos, False) class CollapseIconMixin(CollapseMixin): """ Mixin for :class:`Tree` that allows to collapse subtrees and use an indicator icon in line decorations. This Mixin adds the ability to construct collapse-icon for a position, indicating its collapse status to :class:`CollapseMixin`. """ def __init__(self, is_collapsed=lambda pos: False, icon_collapsed_char='+', icon_expanded_char='-', icon_collapsed_att=None, icon_expanded_att=None, icon_frame_left_char='[', icon_frame_right_char=']', icon_frame_att=None, icon_focussed_att=None, **kwargs): """TODO: docstrings""" CollapseMixin.__init__(self, is_collapsed, **kwargs) self._icon_collapsed_char = icon_collapsed_char self._icon_expanded_char = icon_expanded_char self._icon_collapsed_att = icon_collapsed_att self._icon_expanded_att = icon_expanded_att self._icon_frame_left_char = icon_frame_left_char self._icon_frame_right_char = icon_frame_right_char self._icon_frame_att = icon_frame_att self._icon_focussed_att = icon_focussed_att def _construct_collapse_icon(self, pos): width = 0 widget = None char = self._icon_expanded_char charatt = self._icon_expanded_att if self.is_collapsed(pos): char = self._icon_collapsed_char charatt = self._icon_collapsed_att if char is not None: columns = [] if self._icon_frame_left_char is not None: lchar = self._icon_frame_left_char charlen = len(lchar) leftframe = urwid.Text((self._icon_frame_att, lchar)) columns.append((charlen, leftframe)) width += charlen # next we build out icon widget: we feed all markups to a Text, # make it selectable (to toggle collapse) if requested markup = (charatt, char) widget = urwid.Text(markup) charlen = len(char) columns.append((charlen, widget)) width += charlen if self._icon_frame_right_char is not None: rchar = self._icon_frame_right_char charlen = len(rchar) rightframe = urwid.Text((self._icon_frame_att, rchar)) columns.append((charlen, rightframe)) width += charlen widget = urwid.Columns(columns) return width, widget class CollapsibleTree(CollapseMixin, DecoratedTree): """Undecorated Tree that allows to collapse subtrees""" def __init__(self, tree, **kwargs): DecoratedTree.__init__(self, tree) CollapseMixin.__init__(self, **kwargs) class IndentedTree(DecoratedTree): """Indent tree nodes according to their depth in the tree""" def __init__(self, tree, indent=2): """ :param tree: tree of widgets to be displayed :type tree: Tree :param indent: indentation width :type indent: int """ self._indent = indent DecoratedTree.__init__(self, tree) def decorate(self, pos, widget, is_first=True): indent = self._tree.depth(pos) * self._indent cols = [(indent, urwid.SolidFill(' ')), widget] # construct a Columns, defining all spacer as Box widgets line = urwid.Columns(cols, box_columns=range(len(cols))[:-1]) return line class CollapsibleIndentedTree(CollapseIconMixin, IndentedTree): """ Indent collapsible tree nodes according to their depth in the tree and display icons indicating collapse-status in the gaps. """ def __init__(self, walker, icon_offset=1, indent=4, **kwargs): """ :param walker: tree of widgets to be displayed :type walker: Tree :param indent: indentation width :type indent: int :param icon_offset: distance from icon to the eginning of the tree node. :type icon_offset: int """ self._icon_offset = icon_offset IndentedTree.__init__(self, walker, indent=indent) CollapseIconMixin.__init__(self, **kwargs) def decorate(self, pos, widget, is_first=True): """ builds a list element for given position in the tree. It consists of the original widget taken from the Tree and some decoration columns depending on the existence of parent and sibling positions. The result is a urwid.Columns widget. """ void = urwid.SolidFill(' ') line = None cols = [] depth = self._tree.depth(pos) # add spacer filling all but the last indent if depth > 0: cols.append((depth * self._indent, void)), # spacer # construct last indent # TODO iwidth, icon = self._construct_collapse_icon(pos) available_space = self._indent firstindent_width = self._icon_offset + iwidth # stop if indent is too small for this decoration if firstindent_width > available_space: raise NoSpaceError() # add icon only for non-leafs is_leaf = self._tree.is_leaf(pos) if not is_leaf: if icon is not None: # space to the left cols.append((available_space - firstindent_width, urwid.SolidFill(' '))) # icon icon_pile = urwid.Pile([('pack', icon), void]) cols.append((iwidth, icon_pile)) # spacer until original widget available_space = self._icon_offset cols.append((available_space, urwid.SolidFill(' '))) else: # otherwise just add another spacer cols.append((self._indent, urwid.SolidFill(' '))) cols.append(widget) # original widget ] # construct a Columns, defining all spacer as Box widgets line = urwid.Columns(cols, box_columns=range(len(cols))[:-1]) return line class ArrowTree(IndentedTree): """ Decorates the tree by indenting nodes according to their depth and using the gaps to draw arrows indicate the tree structure. """ def __init__(self, walker, indent=3, childbar_offset=0, arrow_hbar_char=u'\u2500', arrow_hbar_att=None, arrow_vbar_char=u'\u2502', arrow_vbar_att=None, arrow_tip_char=u'\u27a4', arrow_tip_att=None, arrow_att=None, arrow_connector_tchar=u'\u251c', arrow_connector_lchar=u'\u2514', arrow_connector_att=None, **kwargs): """ :param walker: tree of widgets to be displayed :type walker: Tree :param indent: indentation width :type indent: int """ IndentedTree.__init__(self, walker, indent) self._childbar_offset = childbar_offset self._arrow_hbar_char = arrow_hbar_char self._arrow_hbar_att = arrow_hbar_att self._arrow_vbar_char = arrow_vbar_char self._arrow_vbar_att = arrow_vbar_att self._arrow_connector_lchar = arrow_connector_lchar self._arrow_connector_tchar = arrow_connector_tchar self._arrow_connector_att = arrow_connector_att self._arrow_tip_char = arrow_tip_char self._arrow_tip_att = arrow_tip_att self._arrow_att = arrow_att def _construct_spacer(self, pos, acc): """ build a spacer that occupies the horizontally indented space between pos's parent and the root node. It will return a list of tuples to be fed into a Columns widget. """ parent = self._tree.parent_position(pos) if parent is not None: grandparent = self._tree.parent_position(parent) if self._indent > 0 and grandparent is not None: parent_sib = self._tree.next_sibling_position(parent) draw_vbar = parent_sib is not None and \ self._arrow_vbar_char is not None space_width = self._indent - 1 * (draw_vbar) - self._childbar_offset if space_width > 0: void = urwid.AttrMap(urwid.SolidFill(' '), self._arrow_att) acc.insert(0, ((space_width, void))) if draw_vbar: barw = urwid.SolidFill(self._arrow_vbar_char) bar = urwid.AttrMap(barw, self._arrow_vbar_att or self._arrow_att) acc.insert(0, ((1, bar))) return self._construct_spacer(parent, acc) else: return acc def _construct_connector(self, pos): """ build widget to be used as "connector" bit between the vertical bar between siblings and their respective horizontal bars leading to the arrow tip """ # connector symbol, either L or |- shaped. connectorw = None connector = None if self._tree.next_sibling_position(pos) is not None: # |- shaped if self._arrow_connector_tchar is not None: connectorw = urwid.Text(self._arrow_connector_tchar) else: # L shaped if self._arrow_connector_lchar is not None: connectorw = urwid.Text(self._arrow_connector_lchar) if connectorw is not None: att = self._arrow_connector_att or self._arrow_att connector = urwid.AttrMap(connectorw, att) return connector def _construct_arrow_tip(self, pos): """returns arrow tip as (width, widget)""" arrow_tip = None width = 0 if self._arrow_tip_char: txt = urwid.Text(self._arrow_tip_char) arrow_tip = urwid.AttrMap( txt, self._arrow_tip_att or self._arrow_att) width = len(self._arrow_tip_char) return width, arrow_tip def _construct_first_indent(self, pos): """ build spacer to occupy the first indentation level from pos to the left. This is separate as it adds arrowtip and sibling connector. """ cols = [] void = urwid.AttrMap(urwid.SolidFill(' '), self._arrow_att) available_width = self._indent if self._tree.depth(pos) > 0: connector = self._construct_connector(pos) if connector is not None: width = connector.pack()[0] if width > available_width: raise NoSpaceError() available_width -= width if self._tree.next_sibling_position(pos) is not None: barw = urwid.SolidFill(self._arrow_vbar_char) below = urwid.AttrMap(barw, self._arrow_vbar_att or self._arrow_att) else: below = void # pile up connector and bar spacer = urwid.Pile([('pack', connector), below]) cols.append((width, spacer)) #arrow tip awidth, at = self._construct_arrow_tip(pos) if at is not None: if awidth > available_width: raise NoSpaceError() available_width -= awidth at_spacer = urwid.Pile([('pack', at), void]) cols.append((awidth, at_spacer)) # bar between connector and arrow tip if available_width > 0: barw = urwid.SolidFill(self._arrow_hbar_char) bar = urwid.AttrMap( barw, self._arrow_hbar_att or self._arrow_att) hb_spacer = urwid.Pile([(1, bar), void]) cols.insert(1, (available_width, hb_spacer)) return cols def decorate(self, pos, widget, is_first=True): """ builds a list element for given position in the tree. It consists of the original widget taken from the Tree and some decoration columns depending on the existence of parent and sibling positions. The result is a urwid.Columns widget. """ line = None if pos is not None: original_widget = widget cols = self._construct_spacer(pos, []) # Construct arrow leading from parent here, # if we have a parent and indentation is turned on if self._indent > 0: if is_first: indent = self._construct_first_indent(pos) if indent is not None: cols = cols + indent else: parent = self._tree.parent_position(pos) if self._indent > 0 and parent is not None: parent_sib = self._tree.next_sibling_position(pos) draw_vbar = parent_sib is not None void = urwid.AttrMap(urwid.SolidFill(' '), self._arrow_att) if self._childbar_offset > 0: cols.append((self._childbar_offset, void)) if draw_vbar: barw = urwid.SolidFill(self._arrow_vbar_char) bar = urwid.AttrMap( barw, self._arrow_vbar_att or self._arrow_att) rspace_width = self._indent - \ 1 - self._childbar_offset cols.append((1, bar)) cols.append((rspace_width, void)) else: cols.append((self._indent, void)) # add the original widget for this line cols.append(original_widget) # construct a Columns, defining all spacer as Box widgets line = urwid.Columns(cols, box_columns=range(len(cols))[:-1]) return line class CollapsibleArrowTree(CollapseIconMixin, ArrowTree): """Arrow-decoration that allows collapsing subtrees""" def __init__(self, treelistwalker, icon_offset=0, indent=5, **kwargs): self._icon_offset = icon_offset ArrowTree.__init__(self, treelistwalker, indent, **kwargs) CollapseIconMixin.__init__(self, **kwargs) def _construct_arrow_tip(self, pos): cols = [] overall_width = self._icon_offset if self._icon_offset > 0: # how often we repeat the hbar_char until width icon_offset is # reached hbar_char_count = len(self._arrow_hbar_char) / self._icon_offset barw = urwid.Text(self._arrow_hbar_char * hbar_char_count) bar = urwid.AttrMap(barw, self._arrow_hbar_att or self._arrow_att) cols.insert(1, (self._icon_offset, bar)) # add icon only for non-leafs if self.collapsible(pos): iwidth, icon = self._construct_collapse_icon(pos) if icon is not None: cols.insert(0, (iwidth, icon)) overall_width += iwidth # get arrow tip awidth, tip = ArrowTree._construct_arrow_tip(self, pos) if tip is not None: cols.append((awidth, tip)) overall_width += awidth return overall_width, urwid.Columns(cols) urwidtrees-1.0.3.dev0/urwidtrees/lru_cache.py0000644000076500000240000001365612547243556021161 0ustar basstaff00000000000000# This is a backport of functools.lru_cache, which is part of the stdlib =>v3.3. # http://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/ from collections import namedtuple from functools import update_wrapper from threading import Lock _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) def lru_cache(maxsize=100, typed=False): """Least-recently-used cache decorator. If *maxsize* is set to None, the LRU features are disabled and the cache can grow without bound. If *typed* is True, arguments of different types will be cached separately. For example, f(3.0) and f(3) will be treated as distinct calls with distinct results. Arguments to the cached function must be hashable. View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.__wrapped__. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used """ # Users should only access the lru_cache through its public API: # cache_info, cache_clear, and f.__wrapped__ # The internals of the lru_cache are encapsulated for thread safety and # to allow the implementation to change (including a possible C version). def decorating_function(user_function): cache = dict() stats = [0, 0] # make statistics updateable non-locally HITS, MISSES = 0, 1 # names for the stats fields kwd_mark = (object(),) # separate positional and keyword args cache_get = cache.get # bound method to lookup key or return None _len = len # localize the global len() function lock = Lock() # because linkedlist updates aren't threadsafe root = [] # root of the circular doubly linked list nonlocal_root = [root] # make updateable non-locally root[:] = [root, root, None, None] # initialize by pointing to self PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields def make_key(args, kwds, typed, tuple=tuple, sorted=sorted, type=type): # helper function to build a cache key from positional and keyword args key = args if kwds: sorted_items = tuple(sorted(kwds.items())) key += kwd_mark + sorted_items if typed: key += tuple(type(v) for v in args) if kwds: key += tuple(type(v) for k, v in sorted_items) return key if maxsize == 0: def wrapper(*args, **kwds): # no caching, just do a statistics update after a successful call result = user_function(*args, **kwds) stats[MISSES] += 1 return result elif maxsize is None: def wrapper(*args, **kwds): # simple caching without ordering or size limit key = make_key(args, kwds, typed) if kwds or typed else args result = cache_get(key, root) # root used here as a unique not-found sentinel if result is not root: stats[HITS] += 1 return result result = user_function(*args, **kwds) cache[key] = result stats[MISSES] += 1 return result else: def wrapper(*args, **kwds): # size limited caching that tracks accesses by recency key = make_key(args, kwds, typed) if kwds or typed else args with lock: link = cache_get(key) if link is not None: # record recent use of the key by moving it to the front of the list root, = nonlocal_root link_prev, link_next, key, result = link link_prev[NEXT] = link_next link_next[PREV] = link_prev last = root[PREV] last[NEXT] = root[PREV] = link link[PREV] = last link[NEXT] = root stats[HITS] += 1 return result result = user_function(*args, **kwds) with lock: root = nonlocal_root[0] if _len(cache) < maxsize: # put result in a new link at the front of the list last = root[PREV] link = [last, root, key, result] cache[key] = last[NEXT] = root[PREV] = link else: # use root to store the new key and result root[KEY] = key root[RESULT] = result cache[key] = root # empty the oldest link and make it the new root root = nonlocal_root[0] = root[NEXT] del cache[root[KEY]] root[KEY] = None root[RESULT] = None stats[MISSES] += 1 return result def cache_info(): """Report cache statistics""" with lock: return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) def cache_clear(): """Clear the cache and cache statistics""" with lock: cache.clear() root = nonlocal_root[0] root[:] = [root, root, None, None] stats[:] = [0, 0] wrapper.__wrapped__ = user_function wrapper.cache_info = cache_info wrapper.cache_clear = cache_clear return update_wrapper(wrapper, user_function) return decorating_function urwidtrees-1.0.3.dev0/urwidtrees/nested.py0000644000076500000240000003705713030713243020476 0ustar basstaff00000000000000# Copyright (C) 2013 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. from .tree import Tree from .decoration import DecoratedTree, CollapseMixin class NestedTree(Tree): """ A Tree that wraps around Trees that may contain list walkers or other trees. The wrapped tree may contain normal widgets as well. List walkers and subtree contents will be expanded into the tree presented by this wrapper. This wrapper's positions are tuples of positions of the original and subtrees: For example, `(X,Y,Z)` points at position Z in tree/list at position Y in tree/list at position X in the original tree. NestedTree transparently behaves like a collapsible DecoratedTree. """ @property def root(self): root = (self._tree.root,) rcontent = self._tree[self._tree.root] if isinstance(rcontent, Tree): root = root + (rcontent.root,) return root def _sanitize_position(self, pos, tree=None): """ Ensure a position tuple until the result does not point to a :class:`Tree` any more. """ if pos is not None: tree = tree or self._tree entry = self._lookup_entry(tree, pos) if isinstance(entry, Tree): pos = pos + self._sanitize_position((entry.root,), tree=entry) return pos def __init__(self, tree, interpret_covered=False): self._tree = tree self._interpret_covered = interpret_covered def _lookup_entry(self, tree, pos): if len(pos) == 0: entry = tree[tree.root] else: entry = tree[pos[0]] if len(pos) > 1 and isinstance(entry, Tree): subtree = entry entry = self._lookup_entry(subtree, pos[1:]) return entry def _depth(self, tree, pos, outmost_only=True): depth = self._tree.depth(pos[1:]) if not outmost_only: entry = self._tree[pos[0]] if isinstance(entry, Tree) and len(pos) > 1: depth += self._depth(entry, pos[1:], outmost_only=False) return depth def depth(self, pos, outmost=True): return self._depth(self._tree, pos) def __getitem__(self, pos): return self._lookup_entry(self._tree, pos) # DecoratedTree API def _get_decorated_entry(self, tree, pos, widget=None, is_first=True): entry = tree[pos[0]] if entry is None: return None if len(pos) > 1 and isinstance(entry, Tree): subtree = entry entry = self._get_decorated_entry( subtree, pos[1:], widget, is_first) else: entry = widget or entry if isinstance(tree, (DecoratedTree, NestedTree)): # has decorate-API isf = len(pos) < 2 if not isf and isinstance(tree[pos[0]], Tree): isf = (tree[pos[0]].parent_position(pos[1]) is None) or not is_first entry = tree.decorate(pos[0], entry, is_first=isf) return entry def get_decorated(self, pos): return self._get_decorated_entry(self._tree, pos) def decorate(self, pos, widget, is_first=True): return self._get_decorated_entry(self._tree, pos, widget, is_first) # Collapse API def _get_subtree_for(self, pos): """returns Tree that manages pos[-1]""" res = self._tree candidate = self._lookup_entry(self._tree, pos[:-1]) if isinstance(candidate, Tree): res = candidate return res def collapsible(self, pos): res = False subtree = self._get_subtree_for(pos) if isinstance(subtree, (CollapseMixin, NestedTree)): res = subtree.collapsible(pos[-1]) return res def is_collapsed(self, pos): res = False subtree = self._get_subtree_for(pos) if isinstance(subtree, (CollapseMixin, NestedTree)): res = subtree.is_collapsed(pos[-1]) return res def toggle_collapsed(self, pos): subtree = self._get_subtree_for(pos) if isinstance(subtree, (CollapseMixin, NestedTree)): subtree.toggle_collapsed(pos) def collapse(self, pos): subtree = self._get_subtree_for(pos) if isinstance(subtree, (CollapseMixin, NestedTree)): subtree.collapse(pos[-1]) def collapse_all(self): self._collapse_all(self._tree, self.root) def _collapse_all(self, tree, pos=None): if pos is not None: if isinstance(tree, (CollapseMixin, NestedTree)): tree.expand_all() if len(pos) > 1: self._collapse_all(tree[pos[0]], pos[1:]) nextpos = tree.next_position(pos[0]) if nextpos is not None: nentry = tree[nextpos] if isinstance(nentry, Tree): self._collapse_all(nentry, (nentry.root,)) self._collapse_all(tree, (nextpos,)) if isinstance(tree, (CollapseMixin, NestedTree)): tree.collapse_all() def expand(self, pos): subtree = self._get_subtree_for(pos) if isinstance(subtree, (CollapseMixin, NestedTree)): subtree.expand(pos[-1]) def expand_all(self): self._expand_all(self._tree, self.root) def _expand_all(self, tree, pos=None): if pos is not None: if isinstance(tree, (CollapseMixin, NestedTree)): tree.expand_all() if len(pos) > 1: self._expand_all(tree[pos[0]], pos[1:]) nextpos = tree.next_position(pos[0]) if nextpos is not None: nentry = tree[nextpos] if isinstance(nentry, Tree): self._expand_all(nentry, (nentry.root,)) self._expand_all(tree, (nextpos,)) if isinstance(tree, (CollapseMixin, NestedTree)): tree.expand_all() def is_leaf(self, pos, outmost_only=False): return self.first_child_position(pos, outmost_only) is None ################################################ # Tree API ################################################ def parent_position(self, pos): candidate_pos = self._parent_position(self._tree, pos) # return sanitized path (ensure it points to content, not a subtree) return self._sanitize_position(candidate_pos) def _parent_position(self, tree, pos): candidate_pos = None if len(pos) > 1: # get the deepest subtree subtree_pos = pos[:-1] subtree = self._lookup_entry(tree, subtree_pos) # get parent for our position in this subtree least_pos = pos[-1] subparent_pos = subtree.parent_position(least_pos) if subparent_pos is not None: # in case there is one, we are done, the position we look for # is the path up to the subtree plus the local parent position. candidate_pos = subtree_pos + (subparent_pos,) else: # otherwise we recur and look for subtree's parent in the next # outer tree candidate_pos = self._parent_position(self._tree, subtree_pos) else: # there is only one position in the path, we return its parent in # the outmost tree outer_parent = self._tree.parent_position(pos[0]) if outer_parent is not None: # result needs to be valid position (tuple of local positions) candidate_pos = outer_parent, return candidate_pos def first_child_position(self, pos, outmost_only=False): childpos = self._first_child_position(self._tree, pos, outmost_only) return self._sanitize_position(childpos, self._tree) def _first_child_position(self, tree, pos, outmost_only=False): childpos = None # get content at first path element in outmost tree entry = tree[pos[0]] if isinstance(entry, Tree) and not outmost_only and len(pos) > 1: # this points to a tree and we don't check the outmost tree only # recur: get first child in the subtree for remaining path subchild = self._first_child_position(entry, pos[1:]) if subchild is not None: # found a childposition, re-append the path up to this subtree childpos = (pos[0],) + subchild return childpos else: # continue in the next outer tree only if we do not drop # "covered" parts and the position path points to a parent-less # position in the subtree. if (entry.parent_position(pos[1]) is not None or not self._interpret_covered): return None # return the first child of the outmost tree outerchild = tree.first_child_position(pos[0]) if outerchild is not None: childpos = outerchild, return childpos def last_child_position(self, pos, outmost_only=False): childpos = self._last_child_position(self._tree, pos, outmost_only) return self._sanitize_position(childpos, self._tree) def _last_child_position(self, tree, pos, outmost_only=False): childpos = None # get content at first path element in outmost tree entry = tree[pos[0]] if isinstance(entry, Tree) and not outmost_only and len(pos) > 1: # this points to a tree and we don't check the outmost tree only # get last child in the outmost tree if we do not drop "covered" # parts and the position path points to a root of the subtree. if self._interpret_covered: if entry.parent_position(pos[1]) is None: # return the last child of the outmost tree outerchild = tree.last_child_position(pos[0]) if outerchild is not None: childpos = outerchild, # continue as if we have not found anything yet if childpos is None: # recur: get last child in the subtree for remaining path subchild = self._last_child_position(entry, pos[1:]) if subchild is not None: # found a childposition, re-prepend path up to this subtree childpos = (pos[0],) + subchild else: # outmost position element does not point to a tree: # return the last child of the outmost tree outerchild = tree.last_child_position(pos[0]) if outerchild is not None: childpos = outerchild, return childpos def _next_sibling_position(self, tree, pos): candidate = None if len(pos) > 1: # if position path does not point to position in outmost tree, # first get the subtree as pointed out by first dimension, recur # and check if some inner tree already returns a sibling subtree = tree[pos[0]] subsibling_pos = self._next_sibling_position(subtree, pos[1:]) if subsibling_pos is not None: # we found our sibling, prepend the path up to the subtree candidate = pos[:1] + subsibling_pos else: # no deeper tree has sibling. If inner position is root node # the sibling in the outer tree is a valid candidate subparent = subtree.parent_position(pos[1]) if subparent is None: # check if outer tree defines sibling next_sib = tree.next_sibling_position(pos[0]) if next_sib is not None: # it has, we found our candidate candidate = next_sib, # if the inner position has depth 1, then the first child # of its parent in the outer tree can be seen as candidate for # this position next sibling. Those live in the shadow of the # inner tree and are hidden unless requested otherwise elif subtree.parent_position(subparent) is None and \ self._interpret_covered: # we respect "covered" stuff and inner position has depth 1 # get (possibly nested) first child in outer tree candidate = self._first_child_position(tree, pos[:1]) else: # the position path points to the outmost tree # just return its next sibling in the outmost tree next_sib = tree.next_sibling_position(pos[0]) if next_sib is not None: candidate = next_sib, return candidate def next_sibling_position(self, pos): candidate = self._next_sibling_position(self._tree, pos) return self._sanitize_position(candidate, self._tree) def _prev_sibling_position(self, tree, pos): candidate = None if len(pos) > 1: # if position path does not point to position in outmost tree, # first get the subtree as pointed out by first dimension, recur # and check if some inner tree already returns a sibling subtree = tree[pos[0]] subsibling_pos = self._prev_sibling_position(subtree, pos[1:]) if subsibling_pos is not None: # we found our sibling, prepend the path up to the subtree candidate = pos[:1] + subsibling_pos else: # no deeper tree has sibling. If inner position is root node # the sibling in the outer tree is a valid candidate subparent = subtree.parent_position(pos[1]) if subparent is None: prev_sib = tree.prev_sibling_position(pos[0]) if prev_sib is not None: candidate = prev_sib, return candidate # my position could be "hidden" by being child of a # position pointing to a Tree object (which is then unfolded). if self._interpret_covered: # we respect "covered" stuff: # if parent is Tree, return last child of its (last) root parent_pos = self._parent_position(tree, pos) if parent_pos is not None: parent = self._lookup_entry(self._tree, parent_pos) if isinstance(parent, Tree): sib = parent.last_sibling_position(parent.root) candidate = parent.last_child_position(sib) if candidate is not None: candidate = parent_pos + (candidate,) else: # pos points to position in outmost tree prev_sib = tree.prev_sibling_position(pos[0]) if prev_sib is not None: candidate = prev_sib, # In case our new candidate points to a Tree, pick its last root node if candidate is not None: entry = self._lookup_entry(tree, candidate) if isinstance(entry, Tree): candidate = (candidate) + (entry.last_sibling_position(entry.root),) return candidate def prev_sibling_position(self, pos): candidate = self._prev_sibling_position(self._tree, pos) return self._sanitize_position(candidate, self._tree) def last_decendant(self, pos): def lastd(pos): c = self.last_child_position(pos) if c is not None: c = self.last_sibling_position(c) return c return self._last_in_direction(pos, lastd) urwidtrees-1.0.3.dev0/urwidtrees/tree.py0000644000076500000240000002107013030713243020137 0ustar basstaff00000000000000# Copyright (C) 2013 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. class Tree(object): """ Base class for a tree strucures that can be displayed by :class:`TreeBox` widgets. An instance defines a structure by defining local transformations on positions. That is, by overwriting * `next_sibling_position` * `prev_sibling_position` * `parent_position` * `first_child_position` * `last_child_position` that compute the next position in the respective direction. Also, they need to implement method `__getitem__` that returns a :class:`Widget` for a given position. The type of objects used as positions may vary in subclasses and is deliberately unspecified for the base class. This base class already implements methods based on the local transformations above. These include :meth:`depth`, :meth:`last_decendant` and :meth:`[next|prev]_position ` that computes next/previous positions in depth-first order. """ root = None # local helper def _get(self, pos): """loads widget at given position; handling invalid arguments""" res = None, None if pos is not None: try: res = self[pos], pos except (IndexError, KeyError): pass return res def _next_of_kin(self, pos): """ looks up the next sibling of the closest ancestor with not-None next siblings. """ candidate = None parent = self.parent_position(pos) if parent is not None: candidate = self.next_sibling_position(parent) if candidate is None: candidate = self._next_of_kin(parent) return candidate @staticmethod def _last_in_direction(starting_pos, direction): """ move in the tree in given direction and return the last position. :param starting_pos: position to start at :param direction: callable that transforms a position into a position. """ cur_pos = None next_pos = starting_pos while next_pos is not None: cur_pos = next_pos next_pos = direction(cur_pos) return cur_pos def depth(self, pos): """determine depth of node at pos""" parent = self.parent_position(pos) if parent is None: return 0 else: return self.depth(parent) + 1 def is_leaf(self, pos): """checks if given position has no children""" return self.first_child_position(pos) is None def first_ancestor(self, pos): """ position of pos's ancestor with depth 0. Usually, this should return the root node, but a :class:`Tree` might represent a forrest - have multiple nodes without parent. """ return self._last_in_direction(pos, self.parent_position) def last_decendant(self, pos): """position of last (in DFO) decendant of pos""" return self._last_in_direction(pos, self.last_child_position) def last_sibling_position(self, pos): """position of last sibling of pos""" return self._last_in_direction(pos, self.next_sibling_position) def first_sibling_position(self, pos): """position of first sibling of pos""" return self._last_in_direction(pos, self.prev_sibling_position) def next_position(self, pos): """returns the next position in depth-first order""" candidate = None if pos is not None: candidate = self.first_child_position(pos) if candidate is None: candidate = self.next_sibling_position(pos) if candidate is None: candidate = self._next_of_kin(pos) return candidate def prev_position(self, pos): """returns the previous position in depth-first order""" candidate = None if pos is not None: prevsib = self.prev_sibling_position(pos) # is None if first if prevsib is not None: candidate = self.last_decendant(prevsib) else: parent = self.parent_position(pos) if parent is not None: candidate = parent return candidate def positions(self, reverse=False): """returns a generator that walks the positions of this tree in DFO""" def Posgen(reverse): if reverse: lastrootsib = self.last_sibling_position(self.root) current = self.last_decendant(lastrootsib) while current is not None: yield current current = self.prev_position(current) else: current = self.root while current is not None: yield current current = self.next_position(current) return Posgen(reverse) #################################################################### # End of high-level helper implementation. The following need to be # overwritten by subclasses #################################################################### def parent_position(pos): """returns the position of the parent node of the node at `pos` or `None` if none exists.""" return None def first_child_position(self, pos): """returns the position of the first child of the node at `pos`, or `None` if none exists.""" return None def last_child_position(self, pos): """returns the position of the last child of the node at `pos`, or `None` if none exists.""" return None def next_sibling_position(self, pos): """returns the position of the next sibling of the node at `pos`, or `None` if none exists.""" return None def prev_sibling_position(self, pos): """returns the position of the previous sibling of the node at `pos`, or `None` if none exists.""" return None class SimpleTree(Tree): """ Walks on a given fixed acyclic structure given as a list of nodes; every node is a tuple `(content, children)`, where `content` is a `urwid.Widget` to be displayed at that position and `children` is either `None` or a list of nodes. Positions are lists of integers determining a path from the root node with position `(0,)`. """ def __init__(self, treelist): self._treelist = treelist self.root = (0,) if treelist else None Tree.__init__(self) # a few local helper methods def _get_substructure(self, treelist, pos): """recursive helper to look up node-tuple for `pos` in `treelist`""" subtree = None if len(pos) > 1: subtree = self._get_substructure(treelist[pos[0]][1], pos[1:]) else: try: subtree = treelist[pos[0]] except (IndexError, TypeError): pass return subtree def _get_node(self, treelist, pos): """ look up widget at `pos` of `treelist`; default to None if nonexistent. """ node = None if pos is not None: subtree = self._get_substructure(treelist, pos) if subtree is not None: node = subtree[0] return node def _confirm_pos(self, pos): """look up widget for pos and default to None""" candidate = None if self._get_node(self._treelist, pos) is not None: candidate = pos return candidate # Tree API def __getitem__(self, pos): return self._get_node(self._treelist, pos) @staticmethod def parent_position(pos): parent = None if pos is not None and len(pos) > 1: parent = pos[:-1] return parent def first_child_position(self, pos): return self._confirm_pos(pos + (0,)) def last_child_position(self, pos): candidate = None subtree = self._get_substructure(self._treelist, pos) if subtree is not None: children = subtree[1] if children is not None: candidate = pos + (len(children) - 1,) return candidate def next_sibling_position(self, pos): return self._confirm_pos(pos[:-1] + (pos[-1] + 1,)) @staticmethod def prev_sibling_position(pos): return pos[:-1] + (pos[-1] - 1,) if (pos[-1] > 0) else None # optimizations @staticmethod def depth(pos): """more performant implementation due to specific structure of pos""" return len(pos) - 1 urwidtrees-1.0.3.dev0/urwidtrees/version.py0000644000076500000240000000003213030713257020665 0ustar basstaff00000000000000__version__ = '1.0.3.dev' urwidtrees-1.0.3.dev0/urwidtrees/widgets.py0000644000076500000240000002000013030713243020636 0ustar basstaff00000000000000# Copyright (C) 2013 Patrick Totzke # This file is released under the GNU GPL, version 3 or a later revision. import urwid from urwid import WidgetWrap, ListBox from urwid import signals from .decoration import DecoratedTree, CollapseMixin from .nested import NestedTree try: # lru_cache is part of the stdlib from v3.2 onwards from functools import lru_cache except ImportError: # on older versions we use a backport from .lru_cache import lru_cache # The following functions are used to check dynamically if a tree offers sub-APIs def implementsDecorateAPI(tree): """determines if given tree offers line decoration""" return isinstance(tree, (DecoratedTree, NestedTree)) def implementsCollapseAPI(tree): """determines if given tree can collapse positions""" return isinstance(tree, (CollapseMixin, NestedTree)) class TreeListWalker(urwid.ListWalker): """ ListWalker to walk through a class:`Tree`. This translates a :class:`Tree` into a :class:`urwid.ListWalker` that is digestible by :class:`urwid.ListBox`. It uses :meth:`Tree.[next|prev]_position ` to determine the next/previous position in depth first order. """ def __init__(self, tree, focus=None): """ :param tree: the tree to be displayed :type tree: Tree :param focus: position of node to be focussed initially. This has to be a valid position in the Tree. It defaults to the value of `Tree.root`. """ self._tree = tree self._focus = focus or tree.root self.root = tree.root @lru_cache() def __getitem__(self, pos): """gets (possibly decorated) line widget at given position""" if implementsDecorateAPI(self._tree): entry = self._tree.get_decorated(pos) else: entry = self._tree[pos] return entry def clear_cache(self): """removes all cached lines""" self.__getitem__.cache_clear() def _get(self, pos): """looks up widget for given position; handling invalid arguments""" res = None, None if pos is not None: try: res = self[pos], pos except (IndexError, KeyError): pass return res # List Walker API. def get_focus(self): return self._get(self._focus) def set_focus(self, pos): self._focus = pos self._modified() def get_next(self, pos): return self._get(self._tree.next_position(pos)) def get_prev(self, pos): return self._get(self._tree.prev_position(pos)) def positions(self, reverse=False): """returns a generator that walks the tree's positions""" return self._tree.positions(reverse) # end of List Walker API class TreeBox(WidgetWrap): """ A widget that displays a given :class:`Tree`. This is essentially a :class:`ListBox` with the ability to move the focus based on directions in the Tree and to collapse/expand subtrees if possible. TreeBox interprets `left/right` as well as `page up/`page down` to move the focus to parent/first child and next/previous sibling respectively. All other keys are passed to the underlying ListBox. """ def __init__(self, tree, focus=None): """ :param tree: tree of widgets to be displayed. :type tree: Tree :param focus: initially focussed position """ self._tree = tree self._walker = TreeListWalker(tree) self._outer_list = ListBox(self._walker) if focus is not None: self._outer_list.set_focus(focus) self.__super.__init__(self._outer_list) # Widget API def get_focus(self): return self._outer_list.get_focus() def set_focus(self, pos): return self._outer_list.set_focus(pos) def refresh(self): self._walker.clear_cache() signals.emit_signal(self._walker, "modified") def keypress(self, size, key): key = self._outer_list.keypress(size, key) if key in ['left', 'right', '[', ']', '-', '+', 'C', 'E', ]: if key == 'left': self.focus_parent() elif key == 'right': self.focus_first_child() elif key == '[': self.focus_prev_sibling() elif key == ']': self.focus_next_sibling() elif key == '-': self.collapse_focussed() elif key == '+': self.expand_focussed() elif key == 'C': self.collapse_all() elif key == 'E': self.expand_all() # This is a hack around ListBox misbehaving: # it seems impossible to set the focus without calling keypress as # otherwise the change becomes visible only after the next render() return self._outer_list.keypress(size, None) else: return self._outer_list.keypress(size, key) # Collapse operations def collapse_focussed(self): """ Collapse currently focussed position; works only if the underlying tree allows it. """ if implementsCollapseAPI(self._tree): w, focuspos = self.get_focus() self._tree.collapse(focuspos) self._walker.clear_cache() self.refresh() def expand_focussed(self): """ Expand currently focussed position; works only if the underlying tree allows it. """ if implementsCollapseAPI(self._tree): w, focuspos = self.get_focus() self._tree.expand(focuspos) self._walker.clear_cache() self.refresh() def collapse_all(self): """ Collapse all positions; works only if the underlying tree allows it. """ if implementsCollapseAPI(self._tree): self._tree.collapse_all() self.set_focus(self._tree.root) self._walker.clear_cache() self.refresh() def expand_all(self): """ Expand all positions; works only if the underlying tree allows it. """ if implementsCollapseAPI(self._tree): self._tree.expand_all() self._walker.clear_cache() self.refresh() # Tree based focus movement def focus_parent(self): """move focus to parent node of currently focussed one""" w, focuspos = self.get_focus() parent = self._tree.parent_position(focuspos) if parent is not None: self.set_focus(parent) def focus_first_child(self): """move focus to first child of currently focussed one""" w, focuspos = self.get_focus() child = self._tree.first_child_position(focuspos) if child is not None: self.set_focus(child) def focus_last_child(self): """move focus to last child of currently focussed one""" w, focuspos = self.get_focus() child = self._tree.last_child_position(focuspos) if child is not None: self.set_focus(child) def focus_next_sibling(self): """move focus to next sibling of currently focussed one""" w, focuspos = self.get_focus() sib = self._tree.next_sibling_position(focuspos) if sib is not None: self.set_focus(sib) def focus_prev_sibling(self): """move focus to previous sibling of currently focussed one""" w, focuspos = self.get_focus() sib = self._tree.prev_sibling_position(focuspos) if sib is not None: self.set_focus(sib) def focus_next(self): """move focus to next position (DFO)""" w, focuspos = self.get_focus() next = self._tree.next_position(focuspos) if next is not None: self.set_focus(next) def focus_prev(self): """move focus to previous position (DFO)""" w, focuspos = self.get_focus() prev = self._tree.prev_position(focuspos) if prev is not None: self.set_focus(prev) urwidtrees-1.0.3.dev0/urwidtrees.egg-info/0000755000076500000240000000000013030713502020316 5ustar basstaff00000000000000urwidtrees-1.0.3.dev0/urwidtrees.egg-info/dependency_links.txt0000644000076500000240000000000113030713502024364 0ustar basstaff00000000000000 urwidtrees-1.0.3.dev0/urwidtrees.egg-info/PKG-INFO0000644000076500000240000000042613030713502021415 0ustar basstaff00000000000000Metadata-Version: 1.0 Name: urwidtrees Version: 1.0.3.dev0 Summary: Tree widgets for urwid Home-page: https://github.com/pazz/urwidtrees Author: Patrick Totzke Author-email: patricktotzke@gmail.com License: Licensed under the GNU GPL v3+. Description: UNKNOWN Platform: UNKNOWN urwidtrees-1.0.3.dev0/urwidtrees.egg-info/requires.txt0000644000076500000240000000003713030713502022716 0ustar basstaff00000000000000urwid>=1.1.0 mock [docs] mock urwidtrees-1.0.3.dev0/urwidtrees.egg-info/SOURCES.txt0000644000076500000240000000051513030713502022203 0ustar basstaff00000000000000setup.py urwidtrees/__init__.py urwidtrees/decoration.py urwidtrees/lru_cache.py urwidtrees/nested.py urwidtrees/tree.py urwidtrees/version.py urwidtrees/widgets.py urwidtrees.egg-info/PKG-INFO urwidtrees.egg-info/SOURCES.txt urwidtrees.egg-info/dependency_links.txt urwidtrees.egg-info/requires.txt urwidtrees.egg-info/top_level.txturwidtrees-1.0.3.dev0/urwidtrees.egg-info/top_level.txt0000644000076500000240000000001313030713502023042 0ustar basstaff00000000000000urwidtrees