started README
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user