Repository: spandanb/ascii_tree Branch: master Commit: c8a902e4e839 Files: 18 Total size: 42.2 KB Directory structure: gitextract_z82j96jg/ ├── LICENSE ├── README.md ├── ascii_tree/ │ ├── __init__.py │ ├── ascii_tree.py │ ├── charsets.py │ ├── custom_types.py │ ├── draw.py │ ├── external.py │ └── params.py ├── examples/ │ ├── big_tree.py │ ├── big_tree1.py │ ├── big_tree2.py │ ├── change_config.py │ ├── node_types.py │ ├── small_tree.py │ ├── small_tree1.py │ └── small_tree2.py └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) [2019] [Spandan Bemby] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Ascii Tree ========== Generate beautiful ascii trees. ``` I think that I shall never see A graph more lovely than a tree. A tree whose crucial property Is loop-free connectivity. A tree that must be sure to span So packets can reach every LAN. First, the root must be selected. By ID, it is elected. Least-cost paths from root are traced. In the tree, these paths are placed. A mesh is made by folks like me, Then bridges find a spanning tree. -Radia Perlman ``` Installation ============ `git clone ...` `cd ...` `python3 setup.py install` Usage ===== This library can print **arbitrary** trees. This requires you to specify how the value of a node, and list of it's children can be extracted from the node object. For example, consider the following n-ary tree node class ``` >>> class NTreeNode: ... def __init__(self, val): ... self.val = val ... self.children = [] ``` We can extract it's value and children like ``` >>> def get_value(node): ... return node.value ... >>> def get_children(node): ... return node.children ... ``` First, we'll construct a dummy tree. ``` >>> root = NTreeNode('Lorem ipsum dolor sit amet') >>> root.children = [NTreeNode('consectetur adipiscing elit.'), NTreeNode('Etiam laoreet congue')] >>> root.children[0].children = [NTreeNode('Pellentesque finibus metus eget'), NTreeNode('ante aliquet ullamcorper')] >>> root.children[1].children = [NTreeNode('Morbi porta, diam at imperdiet venenatis'), NTreeNode('neque eros bibendum tortor, quis')] ``` Now we can print this as an ascii (the library originally only used ascii characters) tree like ``` >>> from ascii_tree import make_and_print_tree >>> make_and_print_tree(root, get_value, get_children) page: 0 ┌───────────────────┐ │ │ │ Lorem ipsum dolo │ │ r sit amet ├────────────────────────────────────┐ │ │ │ └┬──────────────────┘ │ │ │ │ │ │ │ │ │ │ │ ┌─────────┴─────────┐ ┌─────────┴─────────┐ │ │ │ │ │ consectetur adip │ │ Etiam laoreet co │ │ iscing elit. ├────────────────────┐ │ ngue │ │ │ │ │ │ └──┬────────────────┘ │ └───────────────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────┴─────────┐ ┌─────────┴─────────┐ │ │ │ │ │ Pellentesque fin │ │ ante aliquet ull │ ┌─┤ ibus metus eget ├──┐ │ amcorper │ │ │ │ │ │ │ │ └───────────────────┘ │ └───────────────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────┴─────────┐ ┌─────────┴─────────┐ │ │ │ │ │ Sed nec nibh cur │ │ volutpat lacus a │ │ sus │ │ t, euismod mi │ │ │ │ │ └───────────────────┘ └───────────────────┘ ``` We can also print the tree using only ascii characters like: ``` >>> update_param('charset', 'ascii') >>> make_and_print_tree(root, lambda n: n.val, lambda n: n.children) page: 0 --------------------- | | | Lorem ipsum dolo | | r sit amet |------------------------------------- | | | --------------------- | | | | | | | | | | | --------------------- --------------------- | | | | | consectetur adip | | Etiam laoreet co | | iscing elit. |--------------------- | ngue | | | | | | --------------------- | --------------------- | | | | | | | | | | --------------------- --------------------- | | | | | Pellentesque fin | | ante aliquet ull | --| ibus metus eget |--- | amcorper | | | | | | | | --------------------- | --------------------- | | | | | | | | | | --------------------- --------------------- | | | | | Sed nec nibh cur | | volutpat lacus a | | sus | | t, euismod mi | | | | | --------------------- --------------------- ``` Currently the screen width (the maximum character width assumed by the tree) is `180`. This can be updated like: ``` >>> from ascii_tree import update_param >>> update_param('screen_width', 120) >>> make_and_print_tree(root, get_value, get_children) ``` These params can be updated thus: `screen_width`, `margin` (distance between nodes), `padding` (distance between box contents and border) and `box_max_width`. See more examples [here](./examples). The [call_graph](https://github.com/spandanb/call_graph) library uses `ascii_tree` to print call graphs. Notes ===== - Only supports `python3` - Boxes/nodes can have be upto max width, beyond which their contents are wrapped. - If a tree is too wide, then it is split at a node. Practically, this means that some children go one side of the split, and the rest on the other side. When a node is split, an information box is added as a child to the node, which shows the page number where tree is continued. ================================================ FILE: ascii_tree/__init__.py ================================================ from .ascii_tree import print_tree from .external import transformed_tree, make_and_print_tree, update_param ================================================ FILE: ascii_tree/ascii_tree.py ================================================ ''' Construct ascii tree and determine placement of tree onto page or possibly onto multiple pages if it's too big to fit on a single page. ''' import math from copy import copy from typing import List, Callable from . import charsets from .params import SCREEN_WIDTH, MARGIN, PADDING, SHOW_CONT_DIALOG from .custom_types import Node, Offset, AsciiBox from .draw import draw, draw_line def get_margin(node_count: int, margin: int=MARGIN)->float: ''' determine the amount of margin for node_count ''' return margin*(node_count-1) def recomp_node_width(root: Node, add_on: Node=None, margin: int=MARGIN)->int: ''' Similar to get_node_widths, but only computes the width of a single node. Assumes children widths are computed for non-leaf nodes. Optionally accepts an `add_on`, i.e. a child Node, not actually added to parent ''' if root.is_leaf and add_on is None: return root.box.width if add_on: total_width = sum(child.box.tree_width for child in root.children) + \ + get_margin(len(root.children) + 1, margin) + add_on.box.tree_width else: total_width = sum(child.box.tree_width for child in root.children) + \ + get_margin(len(root.children), margin) total_width = max(total_width, root.box.width) return total_width def get_node_widths(root: Node, margin: int=MARGIN): ''' - compute width of each node - for a leaf node, this is width of encapsulating AsciiBox - for a non-leaf node this is the width of the tree rooted at node. this includes width between children but not around them Args: margin: space between two nodes Returns: dict: map of width of each node. ''' total_width = 0 if root.is_leaf: total_width = root.box.width else: for child in root.children: total_width += get_node_widths(child, margin) # n children need n-1 spaces between them total_width += get_margin(len(root.children), margin) # in case parent is wider than all children total_width = max(total_width, root.box.width) root.box.tree_width = total_width return total_width def get_tree_height(root: Node, margin: int=MARGIN)->int: ''' compute height of tree rooted at `root`. this assumes nodes are not staggered ''' stack = [(root, root.box.box_height)] tree_height = 0 while stack: node, height = stack.pop() tree_height = max(tree_height, height) for child in node.children: stack.append((child, child.box.box_height + height + margin)) return tree_height def position_nodes(root: Node, left_offset: int, top_offset: int, margin: int=MARGIN)-> Offset: ''' compute top-left coordinate for each AsciiBox update node.box.position in place Args: left_offset: (inclusive) left position; for root this is 0 top_offset ''' if root.is_leaf: root.box.position = Offset(left_offset, top_offset) else: # compute positions for children # compute offsets for children child_top_offset = top_offset + root.box.box_height + margin for i, child in enumerate(root.children): if i == 0: child_left_offset = left_offset else: prev_child = root.children[i-1] # using the offset of previous sibling is wrong since # if the sibling has children, the sibling will be positioned # in the middle of its children and its offset will shifted to right # want the left offset of the leftmost descendent of the prev # sibling. This can be achieved by directly storing that value child_left_offset = prev_child.box.tree_left_offset + margin + prev_child.box.tree_width position_nodes(child, child_left_offset, child_top_offset, margin) # compute self positions # place between first and last child first = root.children[0].box.position.left last = root.children[-1].box.position.left node_left_offset = left_offset + (last - first) // 2 root.box.position = Offset(node_left_offset, top_offset) # store the left-offset of the space for the box and it's descendents root.box.tree_left_offset = left_offset return root.box.position def update_page_nums(page_map: dict, splits: List[Node]): ''' page_map: maps root to msg_node, referencing it ''' for i, split in enumerate(splits): if split in page_map: # relies on template string value page_map[split].val = page_map[split].val.format(str(i)) def split_tree(root: Node, max_width: int=SCREEN_WIDTH, first_max_width: int=None, margin: int=MARGIN, show_page: bool=SHOW_CONT_DIALOG): ''' split a tree that is too wide/tall. allows specifying a different first_max_width, e.g. if node needs to be attached to parent, in some limited space. Whether to display page number is configurable, i.e. when the parent node is distinct enough to visually track. Splitting algorithm: - keep list of splits - clone root - iteratively add a child (to this split) and determine width (try with pagenum box if enabled) - if tree gets too wide, split root - if child is too wide, iteratively split child - track page number NB: typically have to fully copy a root at a split since location and width info is stored directly on the objects. TODO: implement turning off cont. on ... dialog ''' if first_max_width is None: first_max_width = max_width # a child call will update a clone # we may want to update actual, especially when slicing the tree sroot = copy(root) splits = [] child_splits = [] # inserted after splits on current level # leave space for 3 digit page numbers msg_node = Node.init_with_box('Cont. on page {} ') msg_node_copy = None # maximum width a single child can consume max_child_width = max_width - margin - msg_node.box.width # map root to msg_node page_map = {} for i, child in enumerate(root.children): if child.box.tree_width > max_child_width: # recursively split child csplits, cpage_map = split_tree(child, max_width=max_width, first_max_width=max_child_width, margin=margin, show_page=show_page) # will get attached to sroot child = csplits[0] # partitioned child # keep separated so these splits can be inserted after siblings child_splits.extend(csplits[1:]) page_map.update(cpage_map) sroot.children.append(child) is_last_child = i == len(root.children) - 1 if is_last_child: new_width = recomp_node_width(sroot, margin=margin) else: new_width = recomp_node_width(sroot, add_on=msg_node, margin=margin) # handle different first_max_width and max_width if i == 0: alloc_width = first_max_width else: alloc_width = max_width if new_width > alloc_width: sroot.children.pop() msg_node_copy = copy(msg_node) sroot.children.append(msg_node_copy) # recompute widths get_node_widths(sroot, margin) position_nodes(sroot, 0, 0, margin) splits.append(sroot) # handle new childs sroot = copy(sroot) sroot.children.append(child) page_map[sroot] = msg_node_copy page_map[sroot] = msg_node_copy get_node_widths(sroot) position_nodes(sroot, 0, 0, margin) splits.append(sroot) splits.extend(child_splits) return splits, page_map def draw_tree(root, screen_width: int=SCREEN_WIDTH, margin: int=MARGIN, padding: int=PADDING, charset=charsets.Unicode)->List[List[List[str]]]: ''' Draw Ascii tree repr of root. Return a list of screen objects with chunks of tree. ''' screens = [] get_node_widths(root) if root.box.tree_width <= screen_width: # set screen height to be height of tree screen_height = get_tree_height(root, margin) # construct screen buffer screen = [[' ']*screen_width for _ in range(screen_height)] position_nodes(root, 0, 0, margin) draw(screen, root, padding, charset) screens.append(screen) else: # if tree is too wide, split the tree splits, page_map = split_tree(root, max_width=screen_width, margin=margin) update_page_nums(page_map, splits) for sroot in splits: screen_height = get_tree_height(sroot, margin) screen = [[' ']*screen_width for _ in range(screen_height)] draw(screen, sroot, padding, charset) screens.append(screen) return screens def print_screen(screen): ''' print each row of the screen ''' for row in screen: print(''.join(row).rstrip()) def print_tree(root: Node, screen_width: int=SCREEN_WIDTH, margin: int=MARGIN, padding: int=PADDING, charset=charsets.Unicode): ''' Output tree to stdout ''' screens = draw_tree(root, screen_width, margin, padding, charset) for i, screen in enumerate(screens): print('page: {}'.format(i)) print_screen(screen) # print page separator if i != len(screens)-1: print(draw_line(screen_width, charset)) ================================================ FILE: ascii_tree/charsets.py ================================================ ''' different characters to draw the ascii tree with ''' class Charset: def __init__(self, name=None, xside=None, yside=None, top=None, bottom=None, left=None, right=None, top_left=None, top_right=None, bottom_left=None, bottom_right=None, top_out=None, bottom_out=None, left_out=None, right_out=None): ''' Accepts characters for different elements of the box. Minimally, (xside, yside) or (top, left, bottom, right) must be specified. The rest are intended to allow finer control. xside: character to use for top and bottom, i.e. along x-axis yside: character to use for left and right, i.e. along y-axis top: character to use for box top. defaults to xside bottom: left: character to use for box left. defaults to yside right: top_left: character to use for top-left corner. defaults to xside top_right: bottom_left: bottom_right: left_out: character where an edge connects on the left. defaults to yside right_out: top_out: character where an edge connects on the left. defaults to xside bottom_out: ''' if (xside is None or yside is None) and \ (left is None or right is None or top is None or bottom is None): raise ValueError('Underdefined character set') self.name = str(name) self.xside = xside or top self.yside = yside or left self.top = top or xside self.bottom = bottom or xside self.left = left or yside self.right = right or yside self.top_left = top_left or xside self.top_right = top_right or xside self.bottom_left = bottom_left or xside self.bottom_right = bottom_right or xside self.top_out = top_out or xside self.bottom_out = bottom_out or xside self.left_out = left_out or yside self.right_out = right_out or yside def __str__(self): return self.name Ascii = Charset(name='ascii', xside='-', yside='|') Unicode = Charset(name='unicode', xside='─', yside='│', top_left='┌', top_right='┐', bottom_left='└', bottom_right='┘', left_out='┤', right_out='├', top_out='┴', bottom_out='┬') ================================================ FILE: ascii_tree/custom_types.py ================================================ import math from collections import namedtuple, deque from copy import copy from .params import PADDING, BOX_MAX_WIDTH # position offset Offset = namedtuple('Offset', 'left top') class Node: ''' Representing a node of the user tree ''' def __init__(self, val): self.val = str(val) self.children = deque() # reference to its AsciiBox self.box = None @classmethod def init_with_box(cls, val, **kwargs): ''' construct Node with associated AsciiBox accepts kwargs to be passed to AsciiBox constructor ''' node = cls(val) node.box = AsciiBox(node.val, **kwargs) return node def __repr__(self): return self.val def __str__(self): return self.val @property def is_leaf(self): return len(self.children) == 0 def __copy__(self): ''' utility to make a copy with box copied and empty children ''' clone = Node(self.val) clone.box = copy(self.box) return clone class AsciiBox: ''' Holds parameters associated with drawing ascii box containing some text. ''' def __init__(self, text: str, box_max_width: int=BOX_MAX_WIDTH, padding: int=PADDING): self.text = text # width of encapsulating box (includes border chars) self.box_width = None # numbers of chars line of text self.line_width = None # height of box self.box_height = None # height of contents self.content_height = None self.box_width, self.line_width, self.box_height, self.content_height = \ self.box_dims(text, box_max_width, padding) # width of tree rooted at node # this must be updated for non-leaf nodes self.tree_width = self.box_width # top-left corner of this box's position self.position = None # left-offset of tree rooted at this node # i.e. the left offset of rectangular box # consumed by this node and it's descendents. self.tree_left_offset = None def __repr__(self): return f'[{self.text}](bw:{self.box_width}, bh:{self.box_height}, cw:{self.line_width}, ch:{self.content_height})' @property def width(self): return self.box_width def box_dims(self, text: str, box_max_width: int, padding: int): ''' determine the box dimensions, i.e. box_width (width including border chars) line_width (number of text chars) box_height (height including border chars) content_height (lines of wrapped text) ''' # max text per line; each line has 2 paddings and 2 border chars max_line_width = box_max_width - 2*padding - 2 if max_line_width >= len(text): box_width = len(text) + 2*padding + 2 line_width = len(text) else: # entire text won't fit on one line; wrap box_width = box_max_width line_width = max_line_width content_height = math.ceil(len(text) / line_width) # box height is height of content + 2 paddings + 2 border chars box_height = content_height + 2*padding + 2 return box_width, line_width, box_height, content_height ================================================ FILE: ascii_tree/draw.py ================================================ ''' Functions for drawing ''' from . import charsets from typing import List from .params import PADDING from .custom_types import Node, Offset def draw_node(screen: list, text: str, left_bound: int, top_bound: int, box_width: int, line_width: int, padding: int=PADDING, charset=charsets.Ascii): ''' Update screen buffer with node ascii repr ''' # inclusive left and right boundaries right_bound = left_bound + box_width # top border for i in range(left_bound+1, right_bound): screen[top_bound][i] = charset.top # top-left corner screen[top_bound][left_bound] = charset.top_left # top-right corner screen[top_bound][right_bound] = charset.top_right # top padding row_idx = top_bound + 1 for p in range(padding): for i in range(left_bound, right_bound+1): if i == left_bound: screen[row_idx + p][i] = charset.left elif i == right_bound: screen[row_idx + p][i] = charset.right # create body of box # each row will contain box_width total characters # and line_width text chars row_idx += padding idx = 0 # iterates over chars in text while idx < len(text): # left boundary screen[row_idx][left_bound] = charset.left # left padding for i in range(padding): screen[row_idx][left_bound + i + 1] = ' ' # content subtext = text[idx:idx + line_width] text_lbound = left_bound + padding + 1 screen[row_idx][text_lbound: text_lbound+len(subtext)] = subtext # right padding for i in range(padding): screen[row_idx][right_bound - i - 1] = ' ' # right boundary screen[row_idx][right_bound] = charset.right idx += line_width row_idx += 1 # bottom padding for p in range(padding): for i in range(left_bound, right_bound+1): if i == left_bound: screen[row_idx + p][i] = charset.left elif i == right_bound: screen[row_idx + p][i] = charset.right row_idx += padding bottom_bound = row_idx # bottom-left corner screen[bottom_bound][left_bound] = charset.bottom_left # bottom-right corner screen[bottom_bound][right_bound] = charset.bottom_right # bottom border for i in range(left_bound+1, right_bound): screen[bottom_bound][i] = charset.bottom def draw_edge(screen, src: Node, dest: Node, charset=charsets.Ascii): ''' draw an edge between src and destination nodes. an edge always extends from the side/bottom of src node and goes to the middle of the of dest node ''' ascii_src = src.box ascii_dest = dest.box # for dest, edge will connect in middle of top dest_x = ascii_dest.position.left + ascii_dest.box_width // 2 dest_y = ascii_dest.position.top src_lbound = ascii_src.position.left src_rbound = ascii_src.position.left + ascii_src.box_width src_ymiddle = ascii_src.position.top + ascii_src.box_height // 2 # based on entry point on dest, determine whether # the line will go from left, right, or center # of source if src_lbound > dest_x: # edge will go from left of source box # y-axis of where to extend from source # draw horizontal leg for i in range(src_lbound, dest_x-1, -1): screen[src_ymiddle][i] = charset.xside # draw left out going portrusion screen[src_ymiddle][src_lbound] = charset.left_out # draw elbow of edge screen[src_ymiddle][dest_x] = charset.top_left # draw vertical leg for i in range(src_ymiddle+1, dest_y): screen[i][dest_x] = charset.yside elif src_rbound < dest_x: # edge will go from right of source box for i in range(src_rbound, dest_x+1): screen[src_ymiddle][i] = charset.xside # right protrusion screen[src_ymiddle][src_rbound] = charset.right_out # elbow screen[src_ymiddle][dest_x] = charset.top_right # then draw vertical leg for i in range(src_ymiddle+1, dest_y): screen[i][dest_x] = charset.yside else: # edge will go from underneath source box src_ybottom = ascii_src.box_height + ascii_src.position.top for i in range(src_ybottom, dest_y): screen[i][dest_x] = charset.yside # bottom protrusion - goes on box which is one unit # higher hence -1 screen[src_ybottom-1][dest_x] = charset.bottom_out # every dest box will have a top out screen[dest_y][dest_x] = charset.top_out def draw(screen: List[List[str]], root: Node, padding: int=PADDING, charset=charsets.Unicode): ''' traverse tree and draw nodes and edges ''' stack = [root] edges = [] while stack: node = stack.pop() left_bound, top_bound = node.box.position box_width, line_width = node.box.box_width, node.box.line_width draw_node(screen, node.val, left_bound, top_bound, box_width, line_width, padding, charset) for child in node.children: # order doesn't matter since each node is drawn independently stack.append(child) edges.append((node, child)) # draw edges at the end since draw_edge handles # adding outgoing protrusion chars on box for node, child in edges: draw_edge(screen, node, child, charset) def draw_line(width, charset=charsets.Unicode)->str: ''' draw line ''' return charset.xside*width ================================================ FILE: ascii_tree/external.py ================================================ ''' Utilities for external interactions ''' from . import charsets from typing import Callable, Any, List from .ascii_tree import print_tree from .custom_types import Node # params may overlap SCREEN_PARAM_NAMES = ['screen_width', 'margin', 'padding', 'charset'] BOX_PARAM_NAMES = ['padding', 'box_max_width'] # updated param values _screen_params = {} _box_params = {} def transformed_tree(root: Any, get_val: Callable[[Any], Any], get_children: Callable[[Any], List])->Node: ''' Utility func transform tree from arbitrary node type to tree of `Node`. This is needed since functions here assume the Node structure. `get_val` and `get_children` are callables that when called on source node, return val and children (list) respectively. ''' troot = Node.init_with_box(get_val(root), **_box_params) troot.children = [transformed_tree(child, get_val, get_children) for child in get_children(root)] return troot def make_and_print_tree(root: Any, get_val: Callable[[Any], Any], get_children: Callable[[Any], List]): ''' Utility method that transforms and prints tree(s) ''' print_tree(transformed_tree(root, get_val, get_children), **_screen_params) def transform_param(param, val): ''' some param value need to be transformed ''' if param == 'charset': if val.lower() == 'unicode': return charsets.Unicode return charsets.Ascii return val def update_param(param: str, new_val): ''' updates param dict ''' param = param.lower() new_val = transform_param(param, new_val) if param in SCREEN_PARAM_NAMES: _screen_params[param] = new_val print(f'{param} updated to {new_val}') if param in BOX_PARAM_NAMES: _box_params[param] = new_val print(f'{param} updated to {new_val}') ================================================ FILE: ascii_tree/params.py ================================================ SCREEN_WIDTH = 180 MARGIN = 5 # space between nodes PADDING = 1 # space between node body and node walls BOX_MAX_WIDTH = 30 # maximum width of ascii box SHOW_CONT_DIALOG = True ================================================ FILE: examples/big_tree.py ================================================ ''' If the tree can't fit on one screen, it will be split into pages and printed over multiple pages. First let's construct a big tree. ''' from ascii_tree import make_and_print_tree from node_types import NTreeNode ''' First generate a really wide tree ''' root0 = NTreeNode('onomatopaie'*10) root0.children = [NTreeNode('boobar'), NTreeNode('car'), NTreeNode('scuba'), NTreeNode('tarzanman'), NTreeNode('smartcar')] root0.children[-1].children = [NTreeNode('Kathmandu'), NTreeNode('smartcat '*20), NTreeNode('lorem ipsum dolor...')] root0.children[-1].children[0].children = [NTreeNode('fooball '*10)] root0.children[-1].children[1].children = [NTreeNode('foosbag')] root0.children.append(NTreeNode('carbar')) print("Printing root0...") make_and_print_tree(root0, lambda node: node.val, lambda node:node.children) ================================================ FILE: examples/big_tree1.py ================================================ from ascii_tree import make_and_print_tree from node_types import NTreeNode ''' Now make it ever wider, such that it can't fit on the screen. This will print multiple pages now ''' root1 = NTreeNode('onomatopaie'*10) root1.children = [NTreeNode(f'lorem ipsum dolor...{i}') for i in range(10)] print("Printing root1...") make_and_print_tree(root1, lambda node: node.val, lambda node:node.children) ''' Notice how the object is split over multiple pages. Also observer that the parent is duplicated in both pages. This is done to make the graph more readable. The splitting algorithm will split along any level of the tree. ''' ================================================ FILE: examples/big_tree2.py ================================================ from ascii_tree import make_and_print_tree from node_types import NTreeNode root2 = NTreeNode('onomatopaie'*10) root2.children = [NTreeNode('boobar'), NTreeNode('car'), NTreeNode('scuba'), NTreeNode('tarzanman'), NTreeNode('smartcar')] root2.children[-1].children = [NTreeNode('Kathmandu'*5), NTreeNode('smartcat '*20), NTreeNode('lorem ipsum dolor...'*20)] root2.children[-1].children.extend([NTreeNode('Kathmandu'*5), NTreeNode('smartcat '*20), NTreeNode('lorem ipsum dolor...'*20)]) root2.children[-1].children[0].children = [NTreeNode('fooball '*10)] root2.children[-1].children[1].children = [NTreeNode('foosbag')] root2.children.append(NTreeNode('carbar')) print("Printing root2...") make_and_print_tree(root2, lambda node: node.val, lambda node:node.children) ================================================ FILE: examples/change_config.py ================================================ ''' If the tree can't fit on one screen, it will be split into pages and printed over multiple pages. First let's construct a big tree. ''' from ascii_tree import make_and_print_tree, update_param class NTreeNode: def __init__(self, val): self.val = val self.children = [] ''' First generate a really wide tree ''' root0 = NTreeNode('onomatopaie'*10) root0.children = [NTreeNode('boobar'), NTreeNode('car'), NTreeNode('scuba'), NTreeNode('tarzanman'), NTreeNode('smartcar')] root0.children[-1].children = [NTreeNode('Kathmandu'), NTreeNode('smartcat '*20), NTreeNode('lorem ipsum dolor...')] root0.children[-1].children[0].children = [NTreeNode('fooball '*10)] root0.children[-1].children[1].children = [NTreeNode('foosbag')] root0.children.append(NTreeNode('carbar')) update_param('box_max_width', 20) update_param('screen_width', 100) print("Printing root0...") make_and_print_tree(root0, lambda node: node.val, lambda node:node.children) ================================================ FILE: examples/node_types.py ================================================ class BinaryTreeNode: def __init__(self, value): self.value = value self.left = None self.right = None class NTreeNode: def __init__(self, val): self.val = val self.children = [] ================================================ FILE: examples/small_tree.py ================================================ ''' Ascii Tree is library for printing beautiful ascii trees. The first step is to convert the tree into another tree that is comrehensible by the library. For this we'll use the following. ''' from ascii_tree import transformed_tree, print_tree from node_types import BinaryTreeNode ''' construct a dummy tre ''' root = BinaryTreeNode(1) root.left = BinaryTreeNode(2) root.right = BinaryTreeNode(3) root.left.left = BinaryTreeNode(4) root.left.right = BinaryTreeNode(5) root.right.left = BinaryTreeNode(6) root.right.right = BinaryTreeNode(7) ''' transformed_tree(root, get_val: Callable, get_children: Callable): transformed_tree accepts the root node of some arbitrary tree class, a callable, called with the node as the argument to get its value and another callable to get the current node's children ''' def get_val(node): ''' get value from node ''' return node.value def get_children(node): ''' get list of children from node ''' children = [] if node.left: children.append(node.left) if node.right: children.append(node.right) return children troot = transformed_tree(root, get_val, get_children) print_tree(troot) ''' page: 0 ------ | | ---- 1 ------------- | | | | | ------ | | | | | | | | | | | ------ ------ | | | | ---- 2 --- ---- 3 --- | | | | | | | | | ------ | | ------ | | | | | | | | | | | | | | | | | | | | | ------ ------ ------ ------ | | | | | | | | | 4 | | 5 | | 6 | | 7 | | | | | | | | | ------ ------ ------ ------ ''' ================================================ FILE: examples/small_tree1.py ================================================ from ascii_tree import make_and_print_tree, update_param from node_types import NTreeNode root = NTreeNode('Lorem ipsum dolor sit amet') root.children = [NTreeNode('consectetur adipiscing elit.'), NTreeNode('Etiam laoreet congue')] root.children[0].children = [NTreeNode('Pellentesque finibus metus eget'), NTreeNode('ante aliquet ullamcorper')] root.children[0].children[0].children = [NTreeNode('Sed nec nibh cursus'), NTreeNode('volutpat lacus at, euismod mi')] update_param('screen_width', 120) update_param('box_max_width', 20) make_and_print_tree(root, lambda n: n.val, lambda n:n.children) ================================================ FILE: examples/small_tree2.py ================================================ from ascii_tree import make_and_print_tree, update_param from node_types import NTreeNode root = NTreeNode('Lorem ipsum dolor sit amet') root.children = [NTreeNode('consectetur adipiscing elit.'), NTreeNode('Etiam laoreet congue')] root.children[0].children = [NTreeNode('Pellentesque finibus metus eget'), NTreeNode('ante aliquet ullamcorper')] root.children[1].children = [NTreeNode('Morbi porta, diam at imperdiet venenatis'), NTreeNode('neque eros bibendum tortor, quis')] update_param('screen_width', 120) update_param('box_max_width', 20) update_param('charset', 'ascii') make_and_print_tree(root, lambda n: n.val, lambda n:n.children) ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages setup(name='ascii_tree', version='0.1', description='create beautiful ascii trees', python_requires='>=3', packages=find_packages() )