initial commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal 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
2
src/daglib/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def main() -> None:
|
||||
print("Hello from daglib!")
|
||||
80
src/daglib/dag.py
Normal file
80
src/daglib/dag.py
Normal 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
109
src/daglib/set.py
Normal 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())
|
||||
Reference in New Issue
Block a user