|
1 |
| -# pdm-nox-dev-template |
2 |
| -Template for python projects managed with pdm and tested with nox |
| 1 | +# ascii-tree |
| 2 | + |
| 3 | +(Yet another) ascii tree rendering utility! |
| 4 | + |
| 5 | +There are a bunch existing solutions out there for drawing directory structures in ascii, but not many with the ability to draw non-filesystem object hierarchies. |
| 6 | + |
| 7 | +`ascii-tree` provides an easy-to-use interface to display any tree-like data as a string. |
| 8 | + |
| 9 | +## Drawing a Directory Tree |
| 10 | +One batteries-included feature of `ascii-tree` is the ability to draw directory trees. |
| 11 | + |
| 12 | +This also provides a good example of how you might customize rendering for custom tree data. |
| 13 | + |
| 14 | +```python |
| 15 | +import ascii_tree |
| 16 | +tree = ascii_tree.renderable_dir_tree( |
| 17 | + "./tests/fixtures" |
| 18 | +) |
| 19 | +print(ascii_tree.render(tree)) |
| 20 | +``` |
| 21 | +Output: |
| 22 | +``` |
| 23 | +fixtures / |
| 24 | +└─ root_dir / |
| 25 | + ├─ child_dir_one / |
| 26 | + │ ├─ grandchild_dir_one / |
| 27 | + │ │ └─ great_grandchild_file_one.txt |
| 28 | + │ └─ grandchild_file_one.txt |
| 29 | + └─ child_dir_two / |
| 30 | + ├─ grandchild_dir_two / |
| 31 | + │ └─ great_grandchild_file_two.txt |
| 32 | + └─ grandchild_file_two.txt |
| 33 | +``` |
| 34 | +By default, this will recurse until all directories and files are visited. |
| 35 | +### Setting Traversal Depth |
| 36 | +By providing a `max_depth` argument, you can only search directories up to a certain level of nesting: |
| 37 | +```python |
| 38 | +import ascii_tree |
| 39 | +tree = ascii_tree.renderable_dir_tree( |
| 40 | + "./tests/fixtures", |
| 41 | + max_depth=2 |
| 42 | +) |
| 43 | +print(ascii_tree.render(tree)) |
| 44 | +``` |
| 45 | +Output: |
| 46 | +``` |
| 47 | +fixtures / |
| 48 | +└─ root_dir / |
| 49 | + ├─ child_dir_one / ... |
| 50 | + └─ child_dir_two / ... |
| 51 | +``` |
| 52 | +The slashes after directories and the ellipses (`...`) after max depth can be controlled with parameters on `renderable_dir_tree`. |
| 53 | +```python |
| 54 | +import ascii_tree |
| 55 | +tree = ascii_tree.renderable_dir_tree( |
| 56 | + "./tests/fixtures", |
| 57 | + max_depth=2, |
| 58 | + slash_after_dir=False, |
| 59 | + ellipsis_after_max_depth=False |
| 60 | +) |
| 61 | +``` |
| 62 | +Output: |
| 63 | +``` |
| 64 | +fixtures |
| 65 | +└─ root_dir |
| 66 | + ├─ child_dir_one |
| 67 | + └─ child_dir_two |
| 68 | +``` |
| 69 | +### Dealing with Permission Errors |
| 70 | +Finally, if you are traversing files with mixed permission levels, it can be useful to skip files that would otherwise cause a `PermissionError` when access isn't allowed. Use the |
| 71 | +`skip_if_no_permission` parameter: |
| 72 | +```python |
| 73 | +import ascii_tree |
| 74 | +tree = ascii_tree.renderable_dir_tree( |
| 75 | + "here/be/dragons", |
| 76 | + skip_if_no_permission=True |
| 77 | +) |
| 78 | +print(ascii_tree.render(tree)) |
| 79 | +``` |
| 80 | +Output: |
| 81 | +``` |
| 82 | +dragons / |
| 83 | +├─ pendor / |
| 84 | +│ └─ yevaud.txt |
| 85 | +├─ lonely_mountain [Permission Denied] |
| 86 | +└─ westeros / |
| 87 | + └─ balereon.txt |
| 88 | +``` |
| 89 | +## Building a Tree from Scratch |
| 90 | + |
| 91 | +Trees can be built up from a chain of `TextRenderNode` instances. `TextRenderNode` is extremely simple; just a `display` and a `children` attribute. |
| 92 | + |
| 93 | +```python |
| 94 | +import ascii_tree |
| 95 | + |
| 96 | +grandchild = ascii_tree.TextRenderNode("grandchild") |
| 97 | +grandchild_two = ascii_tree.TextRenderNode("grandchild_two") |
| 98 | +child = ascii_tree.TextRenderNode("child") |
| 99 | +root = ascii_tree.TextRenderNode("root") |
| 100 | + |
| 101 | +print(ascii_tree.render(root)) |
| 102 | +``` |
| 103 | + |
| 104 | +Output: |
| 105 | +``` |
| 106 | +root |
| 107 | +└─ child |
| 108 | + ├─ grandchild |
| 109 | + └─ grandchild_two |
| 110 | +``` |
| 111 | + |
| 112 | +## Making a Custom ASCII-Tree Interface |
| 113 | + |
| 114 | +Unless you're very lucky, your data *probably* doesn't have `display` and `children` attributes. So we'll need to use an interface! |
| 115 | + |
| 116 | +Let's assume you've got your business data in a class like so: |
| 117 | +```python |
| 118 | +class YourClass: |
| 119 | + def __init__(self, name, child_names=None): |
| 120 | + self.name = name |
| 121 | + self.child_names = child_names or [] |
| 122 | + |
| 123 | + def get_children(self): |
| 124 | + return [YourClass(name) for name in self.child_names] |
| 125 | +``` |
| 126 | +We'll want to map the `name` attribute to `display`, and the `get_children` method to `children`. |
| 127 | +```python |
| 128 | +import ascii_tree |
| 129 | + |
| 130 | +obj = YourClass(name="root", children=["one", "two"]) |
| 131 | + |
| 132 | +node = ascii_tree.renderable( |
| 133 | + obj, display_attr="name", children_method=obj.get_children |
| 134 | +) |
| 135 | + |
| 136 | +print(ascii_tree.render(node)) |
| 137 | +``` |
| 138 | +Output: |
| 139 | +``` |
| 140 | +root |
| 141 | +├─ one |
| 142 | +└─ two |
| 143 | +``` |
| 144 | + |
| 145 | +The `renderable` function allows you to specify an attribute or a method for both `display` and `children` |
| 146 | + |
| 147 | +## Parent-Only Interface |
| 148 | + |
| 149 | +Oftentimes, hierarchically-organized objects only contain references to their parents instead of their children. In that case, we need to do a little more transformation in order to build the renderable tree. |
| 150 | + |
| 151 | +```python |
| 152 | +class MyObjectType: |
| 153 | + def __init__(self, name, parent): |
| 154 | + self.name = name |
| 155 | + self.parent = parent |
| 156 | + |
| 157 | +root = MyObjectType("root", parent=None) |
| 158 | +child = MyObjectType("child_one", parent=root) |
| 159 | +grandchild = MyObjectType("grandchild_one", parent=child) |
| 160 | +``` |
| 161 | +In this case, we need to build the tree *upwards* from the leaf nodes (the grandchildren). |
| 162 | +To do so, pass the leaf-level objects in the tree to the `renderable_from_parents` function: |
| 163 | + |
| 164 | +```python |
| 165 | +import ascii_tree |
| 166 | +tree = ascii_tree.renderable_from_parents( |
| 167 | + [grandchild], |
| 168 | + parent_attr="parent" |
| 169 | +) |
| 170 | +print(ascii_tree.render(tree[0])) |
| 171 | +``` |
| 172 | +Root-level nodes in your hierarchy *must* return `None` from either `parent_attr` or `parent_method`. |
| 173 | + |
| 174 | +Note that `renderable_from_parents` always **returns a list** -- this is to ensure that |
| 175 | +data with multiple root nodes are supported. |
| 176 | + |
| 177 | +Output: |
| 178 | +``` |
| 179 | +root |
| 180 | +└─ child |
| 181 | + └─ grandchild |
| 182 | +``` |
| 183 | +## Rendering Styles |
| 184 | + |
| 185 | +`ascii-tree` currently provides three styles of rendering: |
| 186 | +### Solid Line Style |
| 187 | +```python |
| 188 | +render(tree, style=styles.solid_line_style) |
| 189 | +``` |
| 190 | +``` |
| 191 | +root |
| 192 | +├─ child_one |
| 193 | +│ ├─ grandchild_one |
| 194 | +│ │ ├─ great_grandchild_one |
| 195 | +│ │ └─ great_grandchild_two |
| 196 | +│ └─ grandchild_two |
| 197 | +│ ├─ great_grandchild_three |
| 198 | +│ └─ great_grandchild_four |
| 199 | +└─ child_two |
| 200 | +``` |
| 201 | +### Basic Style |
| 202 | +```python |
| 203 | +render(tree, style=styles.basic_style) |
| 204 | +``` |
| 205 | +``` |
| 206 | +root |
| 207 | ++- child_one |
| 208 | +| +- grandchild_one |
| 209 | +| | +- great_grandchild_one |
| 210 | +| | +- great_grandchild_two |
| 211 | +| +- grandchild_two |
| 212 | +| +- great_grandchild_three |
| 213 | +| +- great_grandchild_four |
| 214 | ++- child_two |
| 215 | +``` |
| 216 | +### Clean Style |
| 217 | +```python |
| 218 | +render(tree, style=styles.clean_style) |
| 219 | +``` |
| 220 | +``` |
| 221 | +root |
| 222 | +· child_one |
| 223 | + · grandchild_one |
| 224 | + · great_grandchild_one |
| 225 | + · great_grandchild_two |
| 226 | + · grandchild_two |
| 227 | + · great_grandchild_three |
| 228 | + · great_grandchild_four |
| 229 | +· child_two |
| 230 | +``` |
0 commit comments