Immutable-by-default tree manipulation library with functional programming patterns and composable transformations
Project description
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:
TreeBuilder now takes root name in constructor, no .root() method
No ``.sibling()`` method - use .up() then .child()
All operations are immutable - return new trees instead of mutating
Node is immutable - use .with_*() methods instead of property setters
No FlatForest, FlatForestNode, TreeNode - removed entirely
Tree wrapper provides functional API (map, filter, reduce, fold)
Selectors replace string-based pattern matching
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:
Fork the repository
Create a feature branch
Add tests for new functionality
Ensure all tests pass
Submit a pull request
License
MIT License - see LICENSE file for details.
Links
Documentation: https://queelius.github.io/AlgoTree/
Source Code: https://github.com/queelius/AlgoTree
Issue Tracker: https://github.com/queelius/AlgoTree/issues
Changelog: https://github.com/queelius/AlgoTree/blob/master/CHANGELOG.md
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d509374d77ca22a5ad6f0f6763cf70792c127fb091029b50a2f96885cbe0fb2f
|
|
| MD5 |
6ff19c8dff485f6a9b180a391858cec2
|
|
| BLAKE2b-256 |
7506dfa18a022b8377aac11d002dbfe63bcd3f114327d26de5abb6433197e675
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1f6102648718367324df01c87a8ddfff7239bcb3edb2151b58ed28fa239c6ac
|
|
| MD5 |
625fcf83de550d49f81095299833ab3d
|
|
| BLAKE2b-256 |
0d31d5ff2981737a2fe1558921da0c482b01b49986222872a37f7733fdf65a41
|