Files
daglib/README.md
T
John Lancaster 08f2140e2f started README
2026-05-17 10:32:58 -05:00

203 lines
3.4 KiB
Markdown

# daglib
Lightweight directed acyclic graph (DAG) primitives with a mutable mapping-style API.
## Install
For local development in this repository:
```bash
uv sync --all-groups
```
## Core Concept
`DAG` behaves like an adjacency map:
- `g[u]` gives a mutable set-like view of successors of `u`
- `g.reverse[v]` gives predecessors of `v`
- `len(g)` returns edge count (not node count)
```python
from daglib.dag import DAG
g = DAG[str]()
g.add_edge("build", "test")
g.add_edge("test", "deploy")
assert "test" in g["build"]
assert "build" in g.reverse["test"]
assert len(g) == 2
```
## Adding Edges
### Explicit edge API
```python
g = DAG[str]()
g.add_edge("foo", "bar")
g.add_edge("foo", "baz")
```
Self-loops are rejected:
```python
g = DAG[str]()
g.add_edge("foo", "foo") # raises ValueError
```
### Mapping and set-like API
```python
g = DAG[str]()
g["foo"] = "bar" # add single edge
g["foo"] = {"baz", "qux"} # add multiple edges
g["foo"] += {"zap", "zip"} # in-place add
assert set(g["foo"]) >= {"bar", "baz", "qux", "zap", "zip"}
```
Note: assignment is additive in current behavior. It does not clear existing outgoing edges.
## Removing Edges and Nodes
```python
g = DAG[str]()
g["foo"] += {"bar", "baz"}
g.remove_edge("foo", "bar")
g["foo"] -= {"baz"}
g.discard_node("foo")
```
`remove_edge` supports strict/non-strict behavior:
```python
g = DAG[str]()
g.remove_edge("x", "y", missing_ok=True) # no error
g.remove_edge("x", "y", missing_ok=False) # raises KeyError
```
## Iteration and Size
```python
g = DAG[str]()
g.add_edge("foo", "bar")
g.add_edge("bar", "baz")
nodes_with_outgoing = set(g) # {"foo", "bar"}
edge_count = len(g) # 2
```
## Reverse View
```python
g = DAG[str]()
g["foo"] = {"baz"}
g["bar"] = {"baz"}
assert set(g.reverse["baz"]) == {"foo", "bar"}
```
## Callbacks
You can hook edge additions/removals done through the mutable set views.
```python
g = DAG[str]()
added = []
removed = []
g.on_add = lambda u, v: added.append((u, v))
g.on_remove = lambda u, v: removed.append((u, v))
g["foo"].add("bar")
g["foo"].discard("bar")
assert ("foo", "bar") in added
assert ("foo", "bar") in removed
```
Current tested behavior: `g["foo"] = {...}` does not trigger `on_add` callbacks.
## Topological Sort
```python
g = DAG[str]()
g["build"] = "test"
g["test"] = "deploy"
order = g.topo_sort()
reverse_order = g.topo_sort(reverse=True)
```
## Subgraphs (Transitive Closure from Seeds)
```python
g = DAG[str]()
g["B"] += "C"
g["B"] += "D"
g["D"] += "E"
g["E"] += "F"
sub = g.subgraph("D")
assert set(sub["D"]) == {"E"}
assert set(sub["E"]) == {"F"}
```
## `DAGSetView` as a Standalone Type
`DAGSetView` can also be used directly as a mutable set wrapper.
```python
from daglib.set import DAGSetView
s = DAGSetView({"foo", 2, 3})
s.add(4)
s -= {2}
s ^= {3, 5}
assert set(s) == {"foo", 4, 5}
```
It supports callbacks:
```python
s = DAGSetView()
events = []
events_removed = []
s.on_add = lambda v: events.append(v)
s.on_remove = lambda v: events_removed.append(v)
s |= {1, 2}
s -= {1}
```
## Performance Test Scaffolding
The test suite includes graph generators and a benchmark helper in `tests/test_performance.py`:
```python
from tests.test_performance import DAGGenerator, benchmark_operation
g = DAGGenerator.chain(10_000)
stats = benchmark_operation(lambda: g.topo_sort(), n_runs=5)
print(stats["avg"])
```
Run performance tests:
```bash
pytest tests/test_performance.py -v -s
```
## Run Tests
```bash
pytest
```