initial commit

This commit is contained in:
John Lancaster
2026-02-20 23:04:24 -06:00
commit 0108b97698
7 changed files with 219 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

0
README.md Normal file
View File

17
pyproject.toml Normal file
View File

@@ -0,0 +1,17 @@
[project]
name = "daglib"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "John Lancaster", email = "32917998+jsl12@users.noreply.github.com" }
]
requires-python = ">=3.12"
dependencies = []
[project.scripts]
daglib = "daglib:main"
[build-system]
requires = ["uv_build>=0.10.2,<0.11.0"]
build-backend = "uv_build"

2
src/daglib/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
def main() -> None:
print("Hello from daglib!")

80
src/daglib/dag.py Normal file
View File

@@ -0,0 +1,80 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterable, Iterator, MutableMapping, MutableSet
from typing import DefaultDict, Generic, Set, TypeVar
from .set import _AdjView
T = TypeVar("T")
class DagAdj(Generic[T], MutableMapping[T, MutableSet[T]]):
"""
DAG adjacency map:
- g[u] -> mutable set-like view of successors of u
- g.reverse[v] -> predecessors of v
Wrapper approach: adjacency sets are not exposed directly; callers mutate
a guarded view that keeps reverse in sync.
"""
def __init__(self) -> None:
self._succ: DefaultDict[T, Set[T]] = defaultdict(set)
self.reverse: DefaultDict[T, Set[T]] = defaultdict(set)
# --- MutableMapping interface ---
def __getitem__(self, u: T) -> MutableSet[T]:
# defaultdict semantics: touch key
_ = self._succ[u]
return _AdjView(self, u)
def __setitem__(self, u: T, vs: Iterable[T]) -> None:
view = _AdjView(self, u)
view.clear()
view |= vs # uses our in-place operator
def __delitem__(self, u: T) -> None:
self.discard_node(u)
def __iter__(self) -> Iterator[T]:
return iter(self._succ)
def __len__(self) -> int:
return len(self._succ)
def __repr__(self) -> str:
return f"DagAdj({dict(self._succ)!r})"
# --- core edge/node operations (single source of truth) ---
def add_edge(self, u: T, v: T) -> None:
if u == v:
raise ValueError("Self-loops are not allowed in a DAG")
if v not in self._succ[u]:
self._succ[u].add(v)
self.reverse[v].add(u)
# Touch keys so nodes appear if accessed later (optional but nice)
_ = self._succ[v]
_ = self.reverse[u]
def remove_edge(self, u: T, v: T, *, missing_ok: bool = True) -> None:
if v in self._succ.get(u, ()):
self._succ[u].remove(v)
self.reverse[v].discard(u)
elif not missing_ok:
raise KeyError((u, v))
def discard_node(self, u: T) -> None:
# remove outgoing
for v in list(self._succ.get(u, ())):
self.remove_edge(u, v, missing_ok=True)
# remove incoming
for p in list(self.reverse.get(u, ())):
self.remove_edge(p, u, missing_ok=True)
self._succ.pop(u, None)
self.reverse.pop(u, None)

109
src/daglib/set.py Normal file
View File

@@ -0,0 +1,109 @@
from __future__ import annotations
from collections.abc import Iterable, Iterator, MutableSet, Set
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from .dag import DagAdj
T = TypeVar("T")
class _AdjView(MutableSet[T]):
"""
A mutable set-like view onto DagAdj._succ[u], with reverse-index maintenance.
Supports all set update operators:
- |= (union_update)
- &= (intersection_update)
- -= (difference_update)
- ^= (symmetric_difference_update)
Also supports += as an alias for update for ergonomic batching.
"""
__slots__ = ("_g", "_u")
def __init__(self, g: DagAdj[T], u: T) -> None:
self._g = g
self._u = u
def _raw(self) -> Set[T]:
return self._g._succ[self._u]
# --- required MutableSet methods ---
def __contains__(self, x: object) -> bool:
return x in self._raw()
def __iter__(self) -> Iterator[T]:
return iter(self._raw())
def __len__(self) -> int:
return len(self._raw())
def add(self, v: T) -> None:
self._g.add_edge(self._u, v)
def discard(self, v: T) -> None:
self._g.remove_edge(self._u, v, missing_ok=True)
# --- convenience / correctness helpers ---
def remove(self, v: T) -> None:
self._g.remove_edge(self._u, v, missing_ok=False)
def clear(self) -> None:
for v in list(self._raw()):
self._g.remove_edge(self._u, v, missing_ok=True)
def update(self, it: Iterable[T]) -> None:
for v in it:
self._g.add_edge(self._u, v)
# --- in-place operator support ---
def __ior__(self, other: Iterable[T]) -> _AdjView[T]:
# a |= b => add everything in other
self.update(other)
return self
def __iand__(self, other: Iterable[T]) -> _AdjView[T]:
# a &= b => keep only those also in other
other_set = set(other)
for v in list(self._raw()):
if v not in other_set:
self._g.remove_edge(self._u, v, missing_ok=True)
return self
def __isub__(self, other: Iterable[T]) -> _AdjView[T]:
# a -= b => remove those in other
for v in other:
self._g.remove_edge(self._u, v, missing_ok=True)
return self
def __ixor__(self, other: Iterable[T]) -> _AdjView[T]:
# a ^= b => symmetric difference update
other_set = set(other)
raw = self._raw()
to_remove = raw & other_set
to_add = other_set - raw
for v in to_remove:
self._g.remove_edge(self._u, v, missing_ok=True)
for v in to_add:
self._g.add_edge(self._u, v)
return self
def __iadd__(self, other: Iterable[T]) -> _AdjView[T]:
"""
Built-in set doesn't define +=, but many folks expect it.
Treat it as 'update' (extend).
"""
self.update(other)
return self
# --- nice repr for debugging ---
def __repr__(self) -> str:
return repr(self._raw())