type modernization

This commit is contained in:
John Lancaster
2026-10-10 10:20:22 -05:00
parent 08f2140e2f
commit 308c06c31d
9 changed files with 1461 additions and 445 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ readme = "README.md"
authors = [ authors = [
{ name = "John Lancaster", email = "[email protected]" } { name = "John Lancaster", email = "[email protected]" }
] ]
requires-python = ">=3.10" requires-python = ">=3.12"
dependencies = [] dependencies = []
[project.scripts] [project.scripts]
@@ -19,6 +19,7 @@ build-backend = "uv_build"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"ipykernel>=7.2.0", "ipykernel>=7.2.0",
"notebook>=7.6.3",
"pre-commit>=4.5.1", "pre-commit>=4.5.1",
"rich>=14.3.3", "rich>=14.3.3",
"ruff>=0.15.2", "ruff>=0.15.2",
@@ -54,7 +55,6 @@ select = [
"SIM", # flake8-simplify "SIM", # flake8-simplify
] ]
extend-fixable = ["ALL"] extend-fixable = ["ALL"]
ignore = ["UP046", "UP047"]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["daglib"] known-first-party = ["daglib"]
+5 -7
View File
@@ -4,17 +4,15 @@ from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator, MutableMapping from collections.abc import Callable, Iterable, Iterator, MutableMapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from graphlib import TopologicalSorter from graphlib import TopologicalSorter
from typing import Generic, TypeVar from typing import Self
from .operations import transitive_closure from .operations import transitive_closure
from .set import DAGSetView from .set import DAGSetView
from .typing import dictset from .typing import dictset
T = TypeVar("T")
@dataclass(repr=False) @dataclass(repr=False)
class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]): class DAG[T](MutableMapping[T, DAGSetView[T]]):
""" """
DAG adjacency map: DAG adjacency map:
- g[u] -> mutable set-like view of successors of u - g[u] -> mutable set-like view of successors of u
@@ -31,7 +29,7 @@ class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
on_remove: Callable[[T, T], None] | None = None on_remove: Callable[[T, T], None] | None = None
@property @property
def reverse(self) -> DAG[T]: def reverse(self) -> Self:
return type(self)( return type(self)(
_succ=self._pred, _succ=self._pred,
_pred=self._succ, _pred=self._succ,
@@ -57,7 +55,7 @@ class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
# --- MutableMapping interface --- # --- MutableMapping interface ---
def __setitem__(self, u: T, value: Iterable[T]) -> None: def __setitem__(self, u: T, value: T | Iterable[T]) -> None:
match value: match value:
case set() as vs: case set() as vs:
self.__dagsetmulti__(u, vs) self.__dagsetmulti__(u, vs)
@@ -120,7 +118,7 @@ class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
self._succ.pop(u, None) self._succ.pop(u, None)
self._pred.pop(u, None) self._pred.pop(u, None)
def subgraph(self, sub: Iterable[T]) -> DAG[T]: def subgraph(self, sub: T | Iterable[T]) -> Self:
closure = transitive_closure(self, sub) closure = transitive_closure(self, sub)
subgraph = type(self)( subgraph = type(self)(
_succ=defaultdict(set, {k: self._succ[k] for k in set(self._succ.keys()) & closure}), _succ=defaultdict(set, {k: self._succ[k] for k in set(self._succ.keys()) & closure}),
+3 -5
View File
@@ -1,21 +1,19 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING
from .util import ensure_set from .util import ensure_set
if TYPE_CHECKING: if TYPE_CHECKING:
from .dag import DAG from .dag import DAG
T = TypeVar("T")
def all_nodes[T](graph: DAG[T]) -> set[T]:
def all_nodes(graph: DAG) -> set:
return set(graph.keys()) | set(graph.reverse.keys()) return set(graph.keys()) | set(graph.reverse.keys())
def transitive_closure(graph: DAG[T], sub: T | Iterable[T], *, include_self: bool = False): def transitive_closure[T](graph: DAG[T], sub: T | Iterable[T], *, include_self: bool = False) -> set[T]:
sub = ensure_set(sub) sub = ensure_set(sub)
stack = list(sub) stack = list(sub)
seen = set() seen = set()
View File
+10 -12
View File
@@ -1,22 +1,20 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator, MutableSet from collections.abc import Callable, Iterable, Iterator, MutableSet
from typing import Any, TypeVar from typing import Self
from .util import ensure_set from .util import ensure_set
T = TypeVar("T")
class DAGSetView[T](MutableSet[T]):
class DAGSetView(MutableSet[T]):
"""A mutable set-like view onto DAG._succ[u], with reverse-index maintenance.""" """A mutable set-like view onto DAG._succ[u], with reverse-index maintenance."""
_data: set[T] _data: set[T]
on_add: Callable[[Any], None] | None = None on_add: Callable[[T], None] | None = None
on_remove: Callable[[Any], None] | None = None on_remove: Callable[[T], None] | None = None
def __init__(self, v: Iterable[T] | None = None) -> None: def __init__(self, v: T | Iterable[T] | None = None) -> None:
self._data: set[T] = ensure_set(v) self._data: set[T] = ensure_set(v)
# --- required MutableSet methods --- # --- required MutableSet methods ---
@@ -43,23 +41,23 @@ class DAGSetView(MutableSet[T]):
# --- in-place operator support --- # --- in-place operator support ---
def __iadd__(self, other: Iterable[T]) -> DAGSetView[T]: def __iadd__(self, other: T | Iterable[T]) -> Self:
# a += b => update/extend # a += b => update/extend
return super().__ior__(ensure_set(other)) return super().__ior__(ensure_set(other))
def __ior__(self, other: Iterable[T]) -> DAGSetView[T]: def __ior__(self, other: T | Iterable[T]) -> Self:
# a |= b => update/extend # a |= b => update/extend
return super().__ior__(ensure_set(other)) return super().__ior__(ensure_set(other))
def __isub__(self, other: Iterable[T]) -> DAGSetView[T]: def __isub__(self, other: T | Iterable[T]) -> Self:
# a -= b => remove those in other # a -= b => remove those in other
return super().__isub__(ensure_set(other)) return super().__isub__(ensure_set(other))
def __iand__(self, other: Iterable[T]) -> DAGSetView[T]: def __iand__(self, other: T | Iterable[T]) -> Self:
# a &= b => keep only those also in other # a &= b => keep only those also in other
return super().__iand__(ensure_set(other)) return super().__iand__(ensure_set(other))
def __ixor__(self, other: Iterable[T]) -> DAGSetView[T]: def __ixor__(self, other: T | Iterable[T]) -> Self:
# a ^= b => symmetric difference update # a ^= b => symmetric difference update
return super().__ixor__(ensure_set(other)) return super().__ixor__(ensure_set(other))
+2 -5
View File
@@ -2,9 +2,6 @@ from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from collections.abc import MutableMapping, MutableSet from collections.abc import MutableMapping, MutableSet
from typing import TypeVar
T = TypeVar("T") type dictset[T] = defaultdict[T, set[T]]
type DAGType[T] = MutableMapping[T, MutableSet[T]]
type dictset = defaultdict[T, set[T]]
type DAGType = MutableMapping[T, MutableSet[T]]
+20
View File
@@ -3,11 +3,17 @@ from __future__ import annotations
import pytest import pytest
from daglib.dag import DAG from daglib.dag import DAG
from daglib.operations import all_nodes, transitive_closure
from daglib.typing import DAGType, dictset
class TestDAGInit: class TestDAGInit:
"""Test DAG initialization.""" """Test DAG initialization."""
def test_generic_alias_parameters(self) -> None:
assert len(dictset.__type_params__) == 1
assert len(DAGType.__type_params__) == 1
def test_init_empty(self) -> None: def test_init_empty(self) -> None:
g = DAG() g = DAG()
assert len(g) == 0 assert len(g) == 0
@@ -281,6 +287,20 @@ class TestDAGCallbacks:
class TestDAGComplexScenarios: class TestDAGComplexScenarios:
"""Test complex usage patterns.""" """Test complex usage patterns."""
def test_typed_traversal_and_subclass_returns(self) -> None:
class IntDAG(DAG[int]):
pass
graph = IntDAG()
graph.add_edge(1, 2)
graph.add_edge(2, 3)
assert all_nodes(graph) == {1, 2, 3}
assert transitive_closure(graph, 1) == {1, 2, 3}
assert isinstance(graph.reverse, IntDAG)
assert isinstance(graph.subgraph(1), IntDAG)
graph.remove_edge(1, 2)
assert 2 not in graph[1]
def test_chain_graph(self) -> None: def test_chain_graph(self) -> None:
g = DAG[str]() g = DAG[str]()
g["foo"] = {"bar"} g["foo"] = {"bar"}
+18
View File
@@ -39,6 +39,10 @@ class TestDAGSetInit:
s = DAGSetView(None) s = DAGSetView(None)
assert len(s) == 0 assert len(s) == 0
def test_from_scalar(self) -> None:
s = DAGSetView[int](2)
assert set(s) == {2}
class TestDAGSetBasicOps: class TestDAGSetBasicOps:
"""Test basic set operations.""" """Test basic set operations."""
@@ -169,6 +173,20 @@ class TestDAGSetCallbacks:
class TestDAGSetInPlaceOperators: class TestDAGSetInPlaceOperators:
"""Test in-place set operators.""" """Test in-place set operators."""
def test_scalar_operators_preserve_instance(self) -> None:
class IntSetView(DAGSetView[int]):
pass
view = IntSetView({1, 2})
original = view
view += 3
view |= 4
view -= 1
view &= 2
view ^= 5
assert view is original
assert set(view) == {2, 5}
class TestIOR: class TestIOR:
def test_set(self) -> None: def test_set(self) -> None:
s = DAGSetView({"foo", 2}) s = DAGSetView({"foo", 2})
Generated
+1401 -414
View File
File diff suppressed because it is too large Load Diff