Full Code of spandanb/ascii_tree for AI

master c8a902e4e839 cached
18 files
42.2 KB
9.4k tokens
41 symbols
1 requests
Download .txt
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()
)
Download .txt
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
Download .txt
SYMBOL INDEX (41 symbols across 8 files)

FILE: ascii_tree/ascii_tree.py
  function get_margin (line 16) | def get_margin(node_count: int, margin: int=MARGIN)->float:
  function recomp_node_width (line 23) | def recomp_node_width(root: Node, add_on: Node=None, margin: int=MARGIN)...
  function get_node_widths (line 46) | def get_node_widths(root: Node, margin: int=MARGIN):
  function get_tree_height (line 73) | def get_tree_height(root: Node, margin: int=MARGIN)->int:
  function position_nodes (line 88) | def position_nodes(root: Node, left_offset: int, top_offset: int, margin...
  function update_page_nums (line 126) | def update_page_nums(page_map: dict, splits: List[Node]):
  function split_tree (line 136) | def split_tree(root: Node, max_width: int=SCREEN_WIDTH, first_max_width:...
  function draw_tree (line 218) | def draw_tree(root, screen_width: int=SCREEN_WIDTH, margin: int=MARGIN, ...
  function print_screen (line 247) | def print_screen(screen):
  function print_tree (line 255) | def print_tree(root: Node, screen_width: int=SCREEN_WIDTH, margin: int=M...

FILE: ascii_tree/charsets.py
  class Charset (line 5) | class Charset:
    method __init__ (line 6) | def __init__(self, name=None, xside=None, yside=None, top=None, bottom...
    method __str__ (line 50) | def __str__(self):

FILE: ascii_tree/custom_types.py
  class Node (line 10) | class Node:
    method __init__ (line 14) | def __init__(self, val):
    method init_with_box (line 21) | def init_with_box(cls, val, **kwargs):
    method __repr__ (line 30) | def __repr__(self):
    method __str__ (line 33) | def __str__(self):
    method is_leaf (line 37) | def is_leaf(self):
    method __copy__ (line 40) | def __copy__(self):
  class AsciiBox (line 50) | class AsciiBox:
    method __init__ (line 55) | def __init__(self, text: str, box_max_width: int=BOX_MAX_WIDTH, paddin...
    method __repr__ (line 77) | def __repr__(self):
    method width (line 81) | def width(self):
    method box_dims (line 84) | def box_dims(self, text: str, box_max_width: int, padding: int):

FILE: ascii_tree/draw.py
  function draw_node (line 10) | def draw_node(screen: list, text: str, left_bound: int, top_bound: int, ...
  function draw_edge (line 78) | def draw_edge(screen, src: Node, dest: Node, charset=charsets.Ascii):
  function draw (line 134) | def draw(screen: List[List[str]], root: Node, padding: int=PADDING, char...
  function draw_line (line 155) | def draw_line(width, charset=charsets.Unicode)->str:

FILE: ascii_tree/external.py
  function transformed_tree (line 18) | def transformed_tree(root: Any, get_val: Callable[[Any], Any], get_child...
  function make_and_print_tree (line 32) | def make_and_print_tree(root: Any, get_val: Callable[[Any], Any], get_ch...
  function transform_param (line 39) | def transform_param(param, val):
  function update_param (line 51) | def update_param(param: str, new_val):

FILE: examples/change_config.py
  class NTreeNode (line 9) | class NTreeNode:
    method __init__ (line 10) | def __init__(self, val):

FILE: examples/node_types.py
  class BinaryTreeNode (line 2) | class BinaryTreeNode:
    method __init__ (line 3) | def __init__(self, value):
  class NTreeNode (line 9) | class NTreeNode:
    method __init__ (line 10) | def __init__(self, val):

FILE: examples/small_tree.py
  function get_val (line 30) | def get_val(node):
  function get_children (line 37) | def get_children(node):
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (46K chars).
[
  {
    "path": "LICENSE",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) [2019] [Spandan Bemby]\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "README.md",
    "chars": 12317,
    "preview": "Ascii Tree\n==========\n\nGenerate beautiful ascii trees.\n```\nI think that I shall never see\nA graph more lovely than a tre"
  },
  {
    "path": "ascii_tree/__init__.py",
    "chars": 110,
    "preview": "from .ascii_tree import print_tree\nfrom .external import transformed_tree, make_and_print_tree, update_param\n\n"
  },
  {
    "path": "ascii_tree/ascii_tree.py",
    "chars": 9679,
    "preview": "'''\nConstruct ascii tree and determine placement of tree\nonto page or possibly onto multiple pages if it's too\nbig to fi"
  },
  {
    "path": "ascii_tree/charsets.py",
    "chars": 2280,
    "preview": "'''\ndifferent characters to draw the ascii tree with\n'''\n\nclass Charset:\n    def __init__(self, name=None, xside=None, y"
  },
  {
    "path": "ascii_tree/custom_types.py",
    "chars": 3308,
    "preview": "import math\nfrom collections import namedtuple, deque\nfrom copy import copy\nfrom .params import PADDING, BOX_MAX_WIDTH\n\n"
  },
  {
    "path": "ascii_tree/draw.py",
    "chars": 5596,
    "preview": "'''\nFunctions for drawing\n'''\nfrom . import charsets\nfrom typing import List\nfrom .params import PADDING\nfrom .custom_ty"
  },
  {
    "path": "ascii_tree/external.py",
    "chars": 1846,
    "preview": "'''\nUtilities for external interactions\n'''\nfrom . import charsets\nfrom typing import Callable, Any, List\nfrom .ascii_tr"
  },
  {
    "path": "ascii_tree/params.py",
    "chars": 180,
    "preview": "SCREEN_WIDTH = 180\nMARGIN = 5  # space between nodes\nPADDING = 1  # space between node body and node walls\nBOX_MAX_WIDTH"
  },
  {
    "path": "examples/big_tree.py",
    "chars": 820,
    "preview": "'''\nIf the tree can't fit on one screen, it will be split into\npages and printed over multiple pages.\nFirst let's constr"
  },
  {
    "path": "examples/big_tree1.py",
    "chars": 629,
    "preview": "from ascii_tree import make_and_print_tree\nfrom node_types import  NTreeNode\n\n'''\nNow make it ever wider, such that it c"
  },
  {
    "path": "examples/big_tree2.py",
    "chars": 768,
    "preview": "from ascii_tree import make_and_print_tree\nfrom node_types import NTreeNode\n\nroot2 = NTreeNode('onomatopaie'*10)\nroot2.c"
  },
  {
    "path": "examples/change_config.py",
    "chars": 968,
    "preview": "'''\nIf the tree can't fit on one screen, it will be split into\npages and printed over multiple pages.\nFirst let's constr"
  },
  {
    "path": "examples/node_types.py",
    "chars": 231,
    "preview": "\nclass BinaryTreeNode:\n    def __init__(self, value):\n        self.value = value\n        self.left = None\n        self.r"
  },
  {
    "path": "examples/small_tree.py",
    "chars": 1996,
    "preview": "'''\nAscii Tree is library for printing beautiful ascii trees.\nThe first step is to convert the tree into another\ntree th"
  },
  {
    "path": "examples/small_tree1.py",
    "chars": 600,
    "preview": "from ascii_tree import make_and_print_tree, update_param\nfrom node_types import NTreeNode\n\nroot = NTreeNode('Lorem ipsum"
  },
  {
    "path": "examples/small_tree2.py",
    "chars": 645,
    "preview": "from ascii_tree import make_and_print_tree, update_param\nfrom node_types import NTreeNode\n\nroot = NTreeNode('Lorem ipsum"
  },
  {
    "path": "setup.py",
    "chars": 204,
    "preview": "from setuptools import setup, find_packages\n\n\nsetup(name='ascii_tree',\n      version='0.1',\n      description='create be"
  }
]

About this extraction

This page contains the full source code of the spandanb/ascii_tree GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (42.2 KB), approximately 9.4k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!