Skip to main content

Immutable-by-default tree manipulation library with functional programming patterns and composable transformations

Project description

https://img.shields.io/pypi/v/AlgoTree.svg https://img.shields.io/pypi/l/AlgoTree.svg https://img.shields.io/badge/coverage-86%25-brightgreen.svg

A powerful, immutable-by-default tree manipulation library for Python with functional programming patterns, composable transformations, and advanced pattern matching.

⚠️ BREAKING CHANGES in v2.0.0 ⚠️

Version 2.0.0 is a complete redesign with NO backward compatibility.

  • Immutable-by-default API - All operations return new trees

  • Functional programming style - map, filter, reduce, fold operations

  • Composable transformations - Selectors and transformers compose with operators

  • Type-safe with full type hints - Complete IDE support

  • 86% test coverage - 197 passing tests across comprehensive test suite

Upgrading from v1.x:

The v1.x API has been completely removed. See the Migration Guide below.

If you need the old API:

pip install "AlgoTree<2.0.0"

Introduction

AlgoTree v2.0 provides a modern, functional approach to working with tree structures in Python. Built on immutable-by-default principles, it offers:

  • Immutable trees - Safe, predictable transformations without side effects

  • Functional operations - map, filter, reduce, fold, and more

  • Pattern matching - Powerful selectors with wildcards and composition

  • Composable transformers - Build complex pipelines with simple operators

  • Multiple construction styles - Fluent builders, DSL parsing, factory methods

  • Rich export formats - JSON, GraphViz, Mermaid, paths, and more

  • Interactive shell - Terminal-based exploration and quick operations

For Scripting & Automation: Use the fluent Python API (recommended)

For Interactive Exploration: Use the shell/CLI tools

Installation

pip install AlgoTree

Quick Start

Building Trees

from AlgoTree import Node, node, Tree, TreeBuilder

# Simple construction with Node
tree = Node("root",
    Node("child1", attrs={"value": 1}),
    Node("child2", attrs={"value": 2})
)

# Convenience function (auto-converts strings)
tree = node("root",
    node("child1", value=1),
    "child2",  # Strings auto-convert to nodes
    node("child3",
        "grandchild1",
        "grandchild2"
    )
)

# Fluent builder API
tree = (TreeBuilder("root", type="container")
    .child("src")
        .child("main.py", type="file", size=1024)
        .child("utils.py", type="file", size=512)
        .up()
    .child("docs")
        .child("README.md", type="file")
    .build())

Immutable Transformations

# All operations return new trees
tree2 = tree.with_name("new_root")
tree3 = tree.with_attrs(status="active")
tree4 = tree.with_child(Node("new_child"))

# Functional tree-wide operations
doubled = tree.map(lambda n: n.with_attrs(
    value=n.get("value", 0) * 2
))

filtered = tree.filter(lambda n: n.get("value", 0) > 5)
nodes = tree.find_all(lambda n: n.is_leaf)

Composable Selectors

from AlgoTree import name, attrs, leaf, type_

# Pattern matching with wildcards
selector = name("*.txt")

# Attribute matching with predicates
selector = attrs(size=lambda s: s > 1000)

# Logical composition with operators
selector = type_("file") & ~leaf()

# Structural selectors
selector = type_("file").child_of(name("src"))
selector = leaf().at_depth(2)

# Use selectors with trees
matching_nodes = list(selector.select(tree))

Composable Transformers

from AlgoTree import map_, filter_, prune, normalize, extract

# Build transformation pipelines with >> operator
pipeline = (
    map_(lambda n: {"processed": True}) >>
    filter_(lambda n: n.get("active")) >>
    normalize(sort_children=True) >>
    extract(lambda n: n.name)
)

# Apply to tree
result = pipeline(tree)

# Or use Tree's fluent API
result = (Tree(tree)
    .map(lambda n: {"processed": True})
    .filter(lambda n: n.get("active"))
    .prune(lambda n: n.name == "temp"))

DSL Parsing

from AlgoTree import parse_tree

# Visual ASCII format
tree = parse_tree("""
root
├── child1
│   ├── grandchild1
│   └── grandchild2
└── child2
""")

# Indented format
tree = parse_tree("""
root
  child1
    grandchild1
    grandchild2
  child2
""")

# S-expression format
tree = parse_tree("(root (child1 (grandchild1) (grandchild2)) (child2))")

Export & Serialization

from AlgoTree import export_tree, save, load

# Export to various formats
json_str = export_tree(tree, "json")
dot_str = export_tree(tree, "graphviz")
mermaid_str = export_tree(tree, "mermaid")

# File operations
save(tree, "tree.json")
loaded = load("tree.json")

# Export to dictionary
data = Tree(tree).to_dict()

# Export to paths
paths = Tree(tree).to_paths()  # ['root/child1', 'root/child2', ...]

Core API

Node (Immutable)

The Node class is the foundation of AlgoTree v2.0. It’s immutable by default, making trees safe to share and transform.

Properties:

  • name - Node name (immutable)

  • attrs - Node attributes dictionary (returns copy)

  • children - Tuple of child nodes (immutable)

  • parent - Parent node (weak reference)

  • is_root - True if node has no parent

  • is_leaf - True if node has no children

  • depth - Distance from root (root has depth 0)

  • height - Height of subtree rooted at this node

  • size - Total number of nodes in subtree

  • path - Path from root as string (e.g., “root/child/grandchild”)

Immutable transformations:

new_node = node.with_name("new_name")
new_node = node.with_attrs(key="value")
new_node = node.without_attrs("key1", "key2")
new_node = node.with_child(Node("child"))
new_node = node.with_children(Node("a"), Node("b"))
new_node = node.without_child("child_name")
new_node = node.map_children(lambda c: c.with_attrs(tagged=True))
new_node = node.filter_children(lambda c: c.get("active"))

Tree-wide transformations:

new_tree = node.map(lambda n: n.with_attrs(processed=True))
filtered = node.filter(lambda n: n.get("value") > 0)
found = node.find(lambda n: n.name == "target")
all_matches = node.find_all(lambda n: n.is_leaf)

Iteration:

for n in node.walk("preorder"):  # or "postorder", "levelorder"
    print(n.name)

for descendant in node.descendants():
    print(descendant.name)

for ancestor in node.ancestors():
    print(ancestor.name)

for leaf in node.leaves():
    print(leaf.name)

Tree (Wrapper)

The Tree class wraps a root node and provides a consistent fluent API.

Factory methods:

tree = Tree.from_dict({
    "name": "root",
    "value": 1,
    "children": [
        {"name": "child1", "value": 2},
        {"name": "child2", "value": 3}
    ]
})

tree = Tree.from_paths([
    "root/a/b",
    "root/a/c",
    "root/d"
])

Functional operations:

# Map over all nodes
result = tree.map(lambda n: {"processed": True})

# Filter nodes (preserves structure)
result = tree.filter(lambda n: n.get("active"))

# Reduce to single value
total = tree.reduce(
    lambda acc, n: acc + n.get("value", 0),
    0
)

# Fold bottom-up
result = tree.fold(
    lambda node, child_results: sum(child_results) + 1
)

Structure operations:

pruned = tree.prune(lambda n: n.name == "temp")
grafted = tree.graft(lambda n: n.is_leaf, Node("new_child"))
flattened = tree.flatten(max_depth=2)

Query operations:

node = tree.find(lambda n: n.name == "target")
nodes = tree.find_all(lambda n: n.is_leaf)
exists = tree.exists(lambda n: n.get("error"))
count = tree.count(lambda n: n.get("active"))

Properties:

  • root - Root node

  • size - Total number of nodes

  • height - Height of tree

  • leaves - List of all leaf nodes

  • is_empty - True if tree is empty

Selectors

Selectors provide composable pattern matching for tree nodes.

Basic selectors:

from AlgoTree import name, attrs, type_, predicate, depth, leaf, root

# Name matching with wildcards
sel = name("*.txt")
sel = name("file_*")

# Attribute matching
sel = attrs(type="file")
sel = attrs(size=lambda s: s > 1000)  # With predicate
sel = attrs(type="file", active=True)  # Multiple attrs

# Type matching
sel = type_("directory")

# Custom predicate
sel = predicate(lambda n: n.name.startswith("test_"))

# Depth matching
sel = depth(2)  # Nodes at depth 2

# Leaf/root selectors
sel = leaf()
sel = root()

Logical composition:

# AND
sel = name("*.py") & type_("file")

# OR
sel = name("*.py") | name("*.txt")

# NOT
sel = ~leaf()

# XOR
sel = type_("file") ^ leaf()

Structural composition:

# Child of
sel = name("*.py").child_of(name("src"))

# Parent of
sel = type_("directory").parent_of(name("main.py"))

# Descendant of
sel = name("*.txt").descendant_of(name("docs"))

# Ancestor of
sel = type_("directory").ancestor_of(leaf())

# Sibling of
sel = name("config.json").sibling_of(name("main.py"))

# At specific depth
sel = type_("file").at_depth(3)

Using selectors:

# Select all matching nodes
for node in selector.select(tree):
    print(node.name)

# Get first match
first = selector.first(tree)

# Count matches
count = selector.count(tree)

# Check existence
exists = selector.exists(tree)

Transformers

Transformers provide composable tree transformations.

Tree -> Tree transformers:

from AlgoTree import map_, filter_, prune, graft, flatten, normalize, annotate

# Map over nodes
t = map_(lambda n: {"processed": True})
t = map_(lambda n: n.with_attrs(value=n.get("value", 0) * 2))

# Filter nodes
t = filter_(lambda n: n.get("active"))
t = filter_(name("*.txt"))  # Can use selectors

# Prune (remove) nodes
t = prune(lambda n: n.name == "temp")
t = prune(name("test_*"))

# Graft (add) subtrees
t = graft(leaf(), Node("new_child"))

# Flatten to depth
t = flatten(max_depth=2)

# Normalize (sort, deduplicate)
t = normalize(sort_children=True, dedup=True)

# Annotate (add metadata)
t = annotate(lambda n: {"depth": n.depth, "size": n.size})

Tree -> Any transformers (shapers):

from AlgoTree import reduce_, fold, extract, to_dict, to_paths

# Reduce to single value
t = reduce_(lambda acc, n: acc + n.get("value", 0), initial=0)

# Fold bottom-up
t = fold(lambda node, child_results: sum(child_results) + 1)

# Extract values
t = extract(lambda n: n.name)

# Convert to dictionary
t = to_dict(children_key="children")

# Convert to paths
t = to_paths(to_leaves_only=True)

Composition:

# Sequential composition with >>
pipeline = map_(fn1) >> filter_(pred) >> prune(sel)

# Parallel composition with &
both = map_(fn1) & annotate(fn2)

# Conditional application
conditional = map_(fn).when(lambda t: t.size > 10)

# Repeated application
repeated = normalize().repeat(3)

# Debug intermediate results
debug = pipeline.debug("after_filter")

Builders

Builders provide fluent APIs for tree construction.

TreeBuilder:

from AlgoTree import TreeBuilder

tree = (TreeBuilder("root", type="container")
    .attr(version="1.0")
    .child("src", type="directory")
        .child("main.py", type="file", size=1024)
        .child("utils.py", type="file", size=512)
        .up()
    .child("tests", type="directory")
        .child("test_main.py", type="file")
    .build())

DSL-style functions:

from AlgoTree import tree, branch, leaf

my_tree = tree("root",
    tree("child1",
        leaf("grandchild1", value=1),
        leaf("grandchild2", value=2)
    ),
    tree("child2",
        leaf("grandchild3", value=3)
    )
).build()

QuickBuilder (path-based):

from AlgoTree import QuickBuilder

tree = (QuickBuilder()
    .root("project")
    .add("src/main.py", type="file", size=1024)
    .add("src/utils.py", type="file", size=512)
    .add("tests/test_main.py", type="file")
    .add("docs/README.md", type="file")
    .build())

Examples

File System Tree

from AlgoTree import TreeBuilder, name, type_, export_tree

# Build file system tree
fs = (TreeBuilder("home")
    .child("user")
        .child("documents", type="dir")
            .child("report.pdf", type="file", size=1024)
            .child("notes.txt", type="file", size=256)
            .up()
        .child("code", type="dir")
            .child("main.py", type="file", size=512)
            .child("test.py", type="file", size=128)
            .up()
        .up()
    .build())

# Find all Python files
py_files = fs.find_all(name("*.py"))
for f in py_files:
    print(f.path, f.get("size"))

# Calculate total size of all files
total_size = fs.reduce(
    lambda acc, n: acc + n.get("size", 0),
    0
)
print(f"Total size: {total_size} bytes")

# Export to GraphViz
dot = export_tree(fs, "graphviz")
print(dot)

Organization Hierarchy

from AlgoTree import node, attrs, map_, normalize

# Build organization tree
company = node("TechCorp",
    node("Engineering",
        node("Frontend", team_size=10, budget=500000),
        node("Backend", team_size=15, budget=750000),
        node("DevOps", team_size=5, budget=300000)
    ),
    node("Sales",
        node("Enterprise", team_size=8, budget=400000),
        node("SMB", team_size=12, budget=350000)
    ),
    node("Marketing",
        node("Digital", team_size=6, budget=250000),
        node("Content", team_size=4, budget=150000)
    )
)

# Calculate department budgets
result = company.map(lambda n: {
    "total_budget": sum(
        child.get("budget", 0) for child in n.children
    ) if n.children else n.get("budget", 0)
})

# Find high-budget teams
high_budget = result.find_all(
    lambda n: n.get("budget", 0) > 400000
)

for team in high_budget:
    print(f"{team.path}: ${team.get('budget'):,}")

Data Processing Pipeline

from AlgoTree import Tree, map_, filter_, normalize, extract

# Create data tree
data = Tree.from_dict({
    "name": "dataset",
    "children": [
        {"name": "record_1", "value": 10, "valid": True},
        {"name": "record_2", "value": 5, "valid": False},
        {"name": "record_3", "value": 15, "valid": True},
        {"name": "record_4", "value": 8, "valid": True},
    ]
})

# Build processing pipeline
pipeline = (
    filter_(lambda n: n.get("valid", False)) >>
    map_(lambda n: {"value": n.get("value", 0) * 2}) >>
    normalize(sort_children=True) >>
    extract(lambda n: n.get("value"))
)

# Process data
result = pipeline(data)
print("Processed values:", result)

When to Use Python API vs Shell

AlgoTree provides two interfaces for different use cases:

Use the Python API (Recommended for Scripting):

from AlgoTree import Node, Tree, node
from AlgoTree.transformers import map_, filter_, prune

# Fluent, composable, type-safe
tree = Tree(node('root',
    node('child1', value=1),
    node('child2', value=2)
))

# Powerful transformations with full Python
result = tree.map(lambda n: n.with_attrs(doubled=n.get('value', 0) * 2))
filtered = tree.filter(lambda n: n.get('value', 0) > 1)

# Integration with Python ecosystem
import pandas as pd
df = pd.DataFrame([{'name': n.name, 'value': n.get('value')}
                   for n in tree.root.descendants()])

Use the Shell/CLI (For Interactive Exploration):

# Quick exploration
algotree shell tree.json

# In the shell:
> cd root
> ls
> find "child.*"
> select "n.get('value', 0) > 1"
> tree

# One-off terminal operations
algotree tree tree.json
algotree select 'n.depth > 2' tree.json

Decision Matrix:

Use Case

Python API

Shell/CLI

Automation

Recommended

Not ideal

Scripts/Programs

Recommended

Not ideal

Complex logic

Recommended

Not ideal

Type safety

Recommended

No type checking

IDE support

Recommended

N/A

Testing

Recommended

Limited

Integration

Recommended

Limited

Quick exploration

Possible

Recommended

Terminal workflows

Possible

Recommended

Learning structure

Possible

Recommended

Human interaction

Possible

Recommended

Example: Automation with Python API

# Production-ready script
from AlgoTree import Tree, map_, filter_, save

# Load, transform, save
tree = Tree.from_dict(load_data())
processed = tree.filter(validate).map(enrich).prune(cleanup)
save(processed.root, 'output.json')

# Full error handling, logging, etc.

Example: Quick Exploration with Shell

# Quick terminal session
$ algotree shell data.json
> tree
> cd interesting_node
> stat
> find "pattern.*"
> exit

Migration from v1.x

AlgoTree v2.0 is a complete redesign. Here’s how to migrate:

Old v1.x API:

from AlgoTree import TreeBuilder

# v1.x - mutable operations
tree = (TreeBuilder()
    .root("company")
    .child("engineering")
        .child("frontend")
        .sibling("backend")
        .up()
    .sibling("sales")
    .build())

# Mutable modification
tree.add_child(Node("marketing"))

New v2.0 API:

from AlgoTree import TreeBuilder, Node

# v2.0 - fluent builder
tree = (TreeBuilder("company")
    .child("engineering")
        .child("frontend")
        .child("backend")
        .up()
    .child("sales")
    .build())

# Immutable operations
tree2 = Tree(tree.root.with_child(Node("marketing")))

Key changes:

  1. TreeBuilder now takes root name in constructor, no .root() method

  2. No ``.sibling()`` method - use .up() then .child()

  3. All operations are immutable - return new trees instead of mutating

  4. Node is immutable - use .with_*() methods instead of property setters

  5. No FlatForest, FlatForestNode, TreeNode - removed entirely

  6. Tree wrapper provides functional API (map, filter, reduce, fold)

  7. Selectors replace string-based pattern matching

  8. Transformers provide composable pipelines

Advanced Features

Lazy Evaluation

Transformers support lazy evaluation for large trees:

# Create lazy pipeline
pipeline = map_(expensive_fn) >> filter_(predicate)

# Only evaluated when called
result = pipeline(tree)

Structural Sharing

Immutable operations use structural sharing for efficiency:

tree1 = Node("root", Node("a"), Node("b"))
tree2 = tree1.with_child(Node("c"))

# tree1 and tree2 share "a" and "b" nodes
# Only "c" and new root are created

Custom Selectors

Create custom selectors by subclassing:

from AlgoTree import Selector

class ExtensionSelector(Selector):
    def __init__(self, ext):
        self.ext = ext

    def matches(self, node):
        return node.name.endswith(self.ext)

# Use it
py_files = ExtensionSelector(".py")
for node in py_files.select(tree):
    print(node.name)

Custom Transformers

Create custom transformers:

from AlgoTree import TreeTransformer

class CapitalizeNames(TreeTransformer):
    def __call__(self, tree):
        return tree.map(lambda n: n.with_name(n.name.upper()))

# Use it
uppercase_tree = CapitalizeNames()(tree)

# Or compose it
pipeline = CapitalizeNames() >> normalize()

Testing

AlgoTree v2.0 has 86% test coverage with 197 passing tests:

# Run tests
python -m pytest

# Run with coverage
python -m pytest --cov=AlgoTree --cov-report=html

# Run specific test
python -m pytest test/test_node.py::TestNode::test_immutability

Contributing

Contributions are welcome! Please:

  1. Fork the repository

  2. Create a feature branch

  3. Add tests for new functionality

  4. Ensure all tests pass

  5. Submit a pull request

License

MIT License - see LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

algotree-2.0.1.tar.gz (120.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

algotree-2.0.1-py3-none-any.whl (124.3 kB view details)

Uploaded Python 3

File details

Details for the file algotree-2.0.1.tar.gz.

File metadata

  • Download URL: algotree-2.0.1.tar.gz
  • Upload date:
  • Size: 120.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for algotree-2.0.1.tar.gz
Algorithm Hash digest
SHA256 d509374d77ca22a5ad6f0f6763cf70792c127fb091029b50a2f96885cbe0fb2f
MD5 6ff19c8dff485f6a9b180a391858cec2
BLAKE2b-256 7506dfa18a022b8377aac11d002dbfe63bcd3f114327d26de5abb6433197e675

See more details on using hashes here.

File details

Details for the file algotree-2.0.1-py3-none-any.whl.

File metadata

  • Download URL: algotree-2.0.1-py3-none-any.whl
  • Upload date:
  • Size: 124.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for algotree-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d1f6102648718367324df01c87a8ddfff7239bcb3edb2151b58ed28fa239c6ac
MD5 625fcf83de550d49f81095299833ab3d
BLAKE2b-256 0d31d5ff2981737a2fe1558921da0c482b01b49986222872a37f7733fdf65a41

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page