pax_global_header00006660000000000000000000000064152674404230014521gustar00rootroot0000000000000052 comment=fc36148f3616918ed0894aee54bfdb90af98dccf asciitree-1.0.0/000077500000000000000000000000001526744042300134675ustar00rootroot00000000000000asciitree-1.0.0/.envrc000066400000000000000000000000401526744042300145770ustar00rootroot00000000000000use flake dotenv_if_exists .env asciitree-1.0.0/.github/000077500000000000000000000000001526744042300150275ustar00rootroot00000000000000asciitree-1.0.0/.github/workflows/000077500000000000000000000000001526744042300170645ustar00rootroot00000000000000asciitree-1.0.0/.github/workflows/ci.yml000066400000000000000000000010311526744042300201750ustar00rootroot00000000000000name: CI on: push: branches: [main, master] pull_request: permissions: contents: read jobs: ci: runs-on: ubuntu-latest container: image: nixpkgs/nix-flakes:latest steps: - name: Checkout run: | git config --global --add safe.directory "$PWD" git init git remote add origin "https://github.com/$GITHUB_REPOSITORY.git" git fetch --depth=1 origin "$GITHUB_SHA" git checkout --detach FETCH_HEAD - name: Check run: nix flake check -L asciitree-1.0.0/.gitignore000066400000000000000000000001511526744042300154540ustar00rootroot00000000000000/.env /result /result-doc /docs/_build/ /build/ /dist/ *.egg-info/ __pycache__/ *.py[cod] .pytest_cache/ asciitree-1.0.0/.readthedocs.yaml000066400000000000000000000006601526744042300167200ustar00rootroot00000000000000version: 2 # RTD's non-root container does not support normal Nix operation; # use `proot` to map `/nix` to a local user directory. build: os: ubuntu-lts-latest tools: python: "3.13" apt_packages: [nix-bin, build-essential, pkg-config, libtalloc-dev] # see build-proot.sh jobs: install: - ./docs/build-proot.sh - ./docs/configure-nix.sh build: html: - ~/proot-env ./docs/build-docs.sh asciitree-1.0.0/.replit000066400000000000000000000000571526744042300147710ustar00rootroot00000000000000language = "python3" run = "python example.py" asciitree-1.0.0/CHANGELOG.md000066400000000000000000000026731526744042300153100ustar00rootroot00000000000000# Changelog ## [1.0.0] - 2026-10-25 ### Added - Configurable layout spaces through `BoxStyle.gfx["SPACE"]`, with an ordinary-space fallback for existing palettes. ### Changed - Declared the existing public API stable, without breaking changes from 0.4.0. ## [0.4.0] - 2026-10-25 ### Added - Nix builds, tests, docs, and direnv development shell. - Ruff linting, treefmt formatting, and Nix-based GitHub CI. ### Changed - Moved setuptools configuration to `pyproject.toml`. - Switched docs to Furo. - Converted README and docs to Markdown with MyST. ### Removed - Python 2 compatibility; use `0.3.3` for Python 2. ## [0.3.3] - 2016-09-05 ### Changed - Ported examples to Python 3. ### Fixed - Dictionary traversal on Python 3. - Missing `LICENSE` in source distributions. - README formatting on PyPI. ## [0.3.2] - 2016-06-28 ### Changed - Release metadata only. ## [0.3.1] - 2015-10-03 ### Added - MIT license file. ### Fixed - Unicode labels on Python 2. ## [0.3] - 2015-02-27 ### Added - Configurable `LeftAligned` renderer; retained `draw_tree()`. - Dictionary and attribute-based traversal. - Unicode box styles and Sphinx API docs. [0.3]: https://pypi.org/project/asciitree/0.3/ [0.3.1]: https://pypi.org/project/asciitree/0.3.1/ [0.3.2]: https://pypi.org/project/asciitree/0.3.2/ [0.3.3]: https://pypi.org/project/asciitree/0.3.3/ [0.4.0]: https://pypi.org/project/asciitree/0.4.0/ [1.0.0]: https://pypi.org/project/asciitree/1.0.0/ asciitree-1.0.0/LICENSE000066400000000000000000000020471526744042300144770ustar00rootroot00000000000000Copyright (c) 2015-2026 Marc Brinkmann 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. asciitree-1.0.0/README.md000066400000000000000000000014211526744042300147440ustar00rootroot00000000000000# ASCII Trees ```text asciitree +-- sometimes | +-- you +-- just | +-- want | +-- to | +-- draw +-- trees +-- in +-- your +-- terminal ``` ```python from asciitree import LeftAligned tree = { "asciitree": { "sometimes": {"you": {}}, "just": {"want": {"to": {}, "draw": {}}}, "trees": {}, "in": {"your": {"terminal": {}}}, } } tr = LeftAligned() print(tr(tree)) ``` Since Python 3.7, dictionaries preserve insertion order. On older versions, use `collections.OrderedDict` to preserve the tree's display order. For Python 2, use `asciitree<0.4`. Version 0.4.0 preserves the API and rendering behavior but supports Python 3 only. Read the documentation at . asciitree-1.0.0/asciitree/000077500000000000000000000000001526744042300154375ustar00rootroot00000000000000asciitree-1.0.0/asciitree/__init__.py000066400000000000000000000044141526744042300175530ustar00rootroot00000000000000#!/usr/bin/env python from .drawing import BoxStyle, Style from .traversal import DictTraversal, Traversal from .util import KeyArgsConstructor class LeftAligned(KeyArgsConstructor): """Creates a renderer for a left-aligned tree. Any attributes of the resulting class instances can be set using constructor arguments.""" draw = BoxStyle() "The draw style used. See :class:`~asciitree.drawing.Style`." traverse = DictTraversal() "Traversal method. See :class:`~asciitree.traversal.Traversal`." def render(self, node): """Renders a node. This function is used internally, as it returns a list of lines. Use :func:`~asciitree.LeftAligned.__call__` instead. """ lines = [] children = self.traverse.get_children(node) lines.append(self.draw.node_label(self.traverse.get_text(node))) for n, child in enumerate(children): child_tree = self.render(child) if n == len(children) - 1: # last child does not get the line drawn lines.append(self.draw.last_child_head(child_tree.pop(0))) lines.extend(self.draw.last_child_tail(line) for line in child_tree) else: lines.append(self.draw.child_head(child_tree.pop(0))) lines.extend(self.draw.child_tail(line) for line in child_tree) return lines def __call__(self, tree): """Render the tree into string suitable for console output. :param tree: A tree.""" return "\n".join(self.render(self.traverse.get_root(tree))) # legacy support below class LegacyStyle(Style): def node_label(self, text): return text def child_head(self, label): return " +--" + label def child_tail(self, line): return " |" + line def last_child_head(self, label): return " +--" + label def last_child_tail(self, line): return " " + line def draw_tree(node, child_iter=lambda n: n.children, text_str=str): """Support asciitree 0.2 API. This function solely exist to not break old code (using asciitree 0.2). Its use is deprecated.""" return LeftAligned( traverse=Traversal(get_text=text_str, get_children=child_iter), draw=LegacyStyle(), )(node) asciitree-1.0.0/asciitree/drawing.py000066400000000000000000000062321526744042300174470ustar00rootroot00000000000000from .util import KeyArgsConstructor BOX_LIGHT = { "SPACE": " ", "UP_AND_RIGHT": "\u2514", "HORIZONTAL": "\u2500", "VERTICAL": "\u2502", "VERTICAL_AND_RIGHT": "\u251c", } #: Unicode box-drawing glyphs, light style BOX_HEAVY = { "SPACE": " ", "UP_AND_RIGHT": "\u2517", "HORIZONTAL": "\u2501", "VERTICAL": "\u2503", "VERTICAL_AND_RIGHT": "\u2523", } #: Unicode box-drawing glyphs, heavy style BOX_DOUBLE = { "SPACE": " ", "UP_AND_RIGHT": "\u255a", "HORIZONTAL": "\u2550", "VERTICAL": "\u2551", "VERTICAL_AND_RIGHT": "\u2560", } #: Unicode box-drawing glyphs, double-line style BOX_ASCII = { "SPACE": " ", "UP_AND_RIGHT": "+", "HORIZONTAL": "-", "VERTICAL": "|", "VERTICAL_AND_RIGHT": "+", } #: Unicode box-drawing glyphs, using only ascii ``|+-`` characters. BOX_BLANK = { "SPACE": " ", "UP_AND_RIGHT": " ", "HORIZONTAL": " ", "VERTICAL": " ", "VERTICAL_AND_RIGHT": " ", } #: Unicode box-drawing glyphs, using only spaces. class Style(KeyArgsConstructor): """Rendering style for trees.""" label_format = "{}" #: Format for labels. def node_label(self, text): """Render a node text into a label.""" return self.label_format.format(text) def child_head(self, label): """Render a node label into final output.""" return label def child_tail(self, line): """Render a node line that is not a label into final output.""" return line def last_child_head(self, label): """Like :func:`~asciitree.drawing.Style.child_head` but only called for the last child.""" return label def last_child_tail(self, line): """Like :func:`~asciitree.drawing.Style.child_tail` but only called for the last child.""" return line class BoxStyle(Style): """A rendering style that uses box draw characters and a common layout.""" gfx = BOX_ASCII #: Glyhps to use. label_space = 1 #: Space between glyphs and label. horiz_len = 2 #: Length of horizontal lines indent = 1 #: Indent for subtrees @property def space(self): """Layout space, with an ASCII-space fallback for older custom palettes.""" return self.gfx.get("SPACE", " ") def child_head(self, label): return ( self.space * self.indent + self.gfx["VERTICAL_AND_RIGHT"] + self.gfx["HORIZONTAL"] * self.horiz_len + self.space * self.label_space + label ) def child_tail(self, line): return ( self.space * self.indent + self.gfx["VERTICAL"] + self.space * self.horiz_len + line ) def last_child_head(self, label): return ( self.space * self.indent + self.gfx["UP_AND_RIGHT"] + self.gfx["HORIZONTAL"] * self.horiz_len + self.space * self.label_space + label ) def last_child_tail(self, line): return ( self.space * self.indent + self.space * len(self.gfx["VERTICAL"]) + self.space * self.horiz_len + line ) asciitree-1.0.0/asciitree/traversal.py000066400000000000000000000021711526744042300200150ustar00rootroot00000000000000from .util import KeyArgsConstructor class Traversal(KeyArgsConstructor): """Traversal method. Used by the tree rendering functions like :class:`~asciitree.LeftAligned`. """ def get_children(self, node): """Return a list of children of a node.""" raise NotImplementedError def get_root(self, tree): """Return a node representing the tree root from the tree.""" return tree def get_text(self, node): """Return the text associated with a node.""" return str(node) class DictTraversal(Traversal): """Traversal suitable for a dictionary. Keys are tree labels, all values must be dictionaries as well.""" def get_children(self, node): return list(node[1].items()) def get_root(self, tree): return list(tree.items())[0] def get_text(self, node): return node[0] class AttributeTraversal(Traversal): """Attribute traversal. Uses an attribute of a node as its list of children. """ attribute = "children" #: Attribute to use. def get_children(self, node): return getattr(node, self.attribute) asciitree-1.0.0/asciitree/util.py000066400000000000000000000002001526744042300167560ustar00rootroot00000000000000class KeyArgsConstructor: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) asciitree-1.0.0/docs/000077500000000000000000000000001526744042300144175ustar00rootroot00000000000000asciitree-1.0.0/docs/build-docs.sh000077500000000000000000000002131526744042300167770ustar00rootroot00000000000000#!/bin/sh set -eu mkdir -p "$READTHEDOCS_OUTPUT/html" nix build "^doc" -L cp -R result-doc/share/doc/*/html/. "$READTHEDOCS_OUTPUT/html/" asciitree-1.0.0/docs/build-proot.sh000077500000000000000000000012231526744042300172140ustar00rootroot00000000000000#!/bin/sh # This script builds PRoot 5.5.0, which fixes `fchmodat2` handling: # https://github.com/proot-me/proot/issues/387 # Required apt packages: `build-essential`, `pkg-config`, `libtalloc-dev`, # `git`, and `ca-certificates`. set -eu cd "$(dirname "$0")/.." build_dir=$(mktemp -d) source_dir="$build_dir/proot" git clone --depth=1 --revision=25dc6a3134891f98a79f57ce1c2c1b23ff15cad1 \ https://github.com/proot-me/proot.git "$source_dir" make -C "$source_dir/src" WITHOUT_PYTHON=1 VERSION=5.5.0 loader.elf loader-m32.elf build.h make -C "$source_dir/src" -j2 WITHOUT_PYTHON=1 VERSION=5.5.0 proot install -m 755 "$source_dir/src/proot" "$HOME/proot" asciitree-1.0.0/docs/conf.py000066400000000000000000000011011526744042300157070ustar00rootroot00000000000000# Generated by sphinx-quickstart on Fri Feb 27 13:10:00 2015. from pathlib import Path import tomllib project = "asciitree" author = "Marc Brinkmann" copyright = "2015-2026, Marc Brinkmann" with (Path(__file__).resolve().parents[1] / "pyproject.toml").open("rb") as metadata: release = tomllib.load(metadata)["project"]["version"] version = ".".join(release.split(".")[:2]) extensions = ["myst_parser", "sphinx.ext.autodoc"] exclude_patterns = ["_build"] html_theme = "furo" # Preserve explicit years under reproducible builds. html_context = {"copyright": copyright} asciitree-1.0.0/docs/configure-nix.sh000077500000000000000000000005261526744042300175360ustar00rootroot00000000000000#!/bin/sh set -eu mkdir -p "$HOME/.config/nix" "$HOME/.local/share/nix" cat >"$HOME/.config/nix/nix.conf" <<'EOF' experimental-features = nix-command flakes store = local build-users-group = sandbox = false EOF cat >"$HOME/proot-env" <<'EOF' #!/bin/sh exec "$HOME/proot" -b "$HOME/.local/share/nix:/nix" "$@" EOF chmod +x "$HOME/proot-env" asciitree-1.0.0/docs/index.md000066400000000000000000000006031526744042300160470ustar00rootroot00000000000000# asciitree `asciitree` draws tree structures using characters. ```{include} ../README.md :heading-offset: 1 ``` Fancier examples: ```{literalinclude} ../example.py :language: python ``` ## API ```{eval-rst} .. autoclass:: asciitree.LeftAligned :members: :special-members: .. automodule:: asciitree.traversal :members: .. automodule:: asciitree.drawing :members: ``` asciitree-1.0.0/example.py000066400000000000000000000013741526744042300155010ustar00rootroot00000000000000from copy import deepcopy from asciitree import LeftAligned from asciitree.drawing import BoxStyle, BOX_DOUBLE, BOX_BLANK tr = LeftAligned() tree = { "asciitree": { "sometimes": {"you": {}}, "just": {"want": {"to": {}, "draw": {}}}, "trees": {}, "in": {"your": {"terminal": {}}}, } } print(tr(tree)) # construct a more complex tree by copying the tree and grafting it onto itself tree2 = deepcopy(tree) tree2["asciitree"]["trees"] = deepcopy(tree2["asciitree"]) print(tr(tree2)) # use a box style box_tr = LeftAligned(draw=BoxStyle(gfx=BOX_DOUBLE, horiz_len=1)) print(box_tr(tree)) # more airy air_tr = LeftAligned( draw=BoxStyle(gfx=BOX_BLANK, label_space=0, label_format="[{}]", indent=0) ) print(air_tr(tree)) asciitree-1.0.0/flake.lock000066400000000000000000000037561526744042300154360ustar00rootroot00000000000000{ "nodes": { "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1791914995, "narHash": "sha256-Zf/xfQEAasnwnmEfHqW/4hjDE/s44yn2hD4RuVIZRjA=", "owner": "numtide", "repo": "flake-utils", "rev": "b7a79d3fea1e214f6d668d521cecec0c01e26544", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1792615934, "narHash": "sha256-MlvRfa1O5Cqc2/a+gG+l9TSuowEoCKCmrJ1d9oAVm0I=", "owner": "NixOS", "repo": "nixpkgs", "rev": "da6dc0e7b26060f2f71ce3ef7088fb56ee44fa36", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } }, "treefmt-nix": { "inputs": { "nixpkgs": [ "nixpkgs" ] }, "locked": { "lastModified": 1791885023, "narHash": "sha256-C3alQ2MEt0zVrUSho5NUDo3jmhbff72++bfwoqT9Omo=", "owner": "numtide", "repo": "treefmt-nix", "rev": "0d6d739a833ba9a9c93034862e4a7b061a062a1c", "type": "github" }, "original": { "owner": "numtide", "repo": "treefmt-nix", "type": "github" } } }, "root": "root", "version": 7 } asciitree-1.0.0/flake.nix000066400000000000000000000037251526744042300153000ustar00rootroot00000000000000{ description = "Draws ASCII trees"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; flake-utils.url = "github:numtide/flake-utils"; treefmt-nix.url = "github:numtide/treefmt-nix"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, flake-utils, treefmt-nix, ... }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages.${system}; python = pkgs.python3; pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); treefmt = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; programs = { ruff-check.enable = true; ruff-format.enable = true; nixfmt.enable = true; mdformat.enable = true; taplo.enable = true; shfmt.enable = true; }; }; in { packages.default = python.pkgs.buildPythonPackage { pname = pyproject.project.name; version = pyproject.project.version; src = ./.; pyproject = true; outputs = [ "out" "doc" ]; build-system = [ python.pkgs.setuptools ]; nativeBuildInputs = with python.pkgs; [ sphinxHook myst-parser furo ]; nativeCheckInputs = [ python.pkgs.pytestCheckHook ]; pythonImportsCheck = [ "asciitree" ]; }; formatter = treefmt.config.build.wrapper; checks = { formatting = treefmt.config.build.check self; library = self.packages.${system}.default; }; devShells.default = pkgs.mkShell { inputsFrom = [ self.packages.${system}.default ]; packages = [ treefmt.config.build.wrapper pkgs.ruff pkgs.twine python.pkgs.build ]; }; } ); } asciitree-1.0.0/publish.sh000077500000000000000000000003441526744042300154750ustar00rootroot00000000000000#!/bin/sh set -eu cd "$(dirname "$0")" nix flake check -L dist_dir=$(mktemp -d) trap 'rm -rf "$dist_dir"' EXIT pyproject-build --no-isolation --outdir "$dist_dir" twine check --strict "$dist_dir"/* twine upload "$dist_dir"/* asciitree-1.0.0/pyproject.toml000066400000000000000000000007571526744042300164140ustar00rootroot00000000000000[build-system] requires = ["setuptools>=77.0.3"] build-backend = "setuptools.build_meta" [project] name = "asciitree" version = "1.0.0" description = "Draws ASCII trees." readme = "README.md" authors = [{ name = "Marc Brinkmann", email = "git@marcbrinkmann.de" }] license = "MIT" license-files = ["LICENSE"] dependencies = [] [project.urls] Homepage = "https://github.com/mbr/asciitree" Documentation = "https://asciitree.readthedocs.io/en/stable/" [tool.setuptools] packages = ["asciitree"] asciitree-1.0.0/tests/000077500000000000000000000000001526744042300146315ustar00rootroot00000000000000asciitree-1.0.0/tests/__init__.py000066400000000000000000000000001526744042300167300ustar00rootroot00000000000000asciitree-1.0.0/tests/test_backwards_compat.py000066400000000000000000000013341526744042300215470ustar00rootroot00000000000000from asciitree import draw_tree def test_behaves_as_originally_advertised(): expected_output = """root +--sub1 +--sub2 | +--sub2sub1 +--sub3 +--sub3sub1 | +--sub3sub1sub1 +--sub3sub2""" class Node: def __init__(self, name, children): self.name = name self.children = children def __str__(self): return self.name root = Node( "root", [ Node("sub1", []), Node("sub2", [Node("sub2sub1", [])]), Node( "sub3", [Node("sub3sub1", [Node("sub3sub1sub1", [])]), Node("sub3sub2", [])], ), ], ) assert draw_tree(root) == expected_output asciitree-1.0.0/tests/test_layout_space.py000066400000000000000000000011751526744042300207360ustar00rootroot00000000000000from asciitree import LeftAligned from asciitree.drawing import BoxStyle def test_palette_without_space(): """Custom palettes without a `SPACE` key retain ordinary space padding.""" gfx = { "UP_AND_RIGHT": "+", "HORIZONTAL": "-", "VERTICAL": "|", "VERTICAL_AND_RIGHT": "+", } tree = { "root label": { "first label": {"first leaf": {}}, "last label": {"last leaf": {}}, }, } expected = """root label +-- first label | +-- first leaf +-- last label +-- last leaf""" assert LeftAligned(draw=BoxStyle(gfx=gfx))(tree) == expected asciitree-1.0.0/tests/test_unicode.py000066400000000000000000000002061526744042300176660ustar00rootroot00000000000000from asciitree import LeftAligned def test_unicode_doesnt_crash(): tr = LeftAligned() assert tr({"åäö": {}}) == "åäö"