From 08f2140e2f632084f1c87d7fb2bfcb1ee03b20da Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sun, 17 May 2026 10:32:58 -0500 Subject: [PATCH] started README --- README.md | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/README.md b/README.md index e69de29..ff82d1c 100644 --- a/README.md +++ b/README.md @@ -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 +```