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 = [
{ name = "John Lancaster", email = "[email protected]" }
]
requires-python = ">=3.10"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
@@ -19,6 +19,7 @@ build-backend = "uv_build"
[dependency-groups]
dev = [
"ipykernel>=7.2.0",
"notebook>=7.6.3",
"pre-commit>=4.5.1",
"rich>=14.3.3",
"ruff>=0.15.2",
@@ -54,7 +55,6 @@ select = [
"SIM", # flake8-simplify
]
extend-fixable = ["ALL"]
ignore = ["UP046", "UP047"]
[tool.ruff.lint.isort]
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 dataclasses import dataclass, field
from graphlib import TopologicalSorter
from typing import Generic, TypeVar
from typing import Self
from .operations import transitive_closure
from .set import DAGSetView
from .typing import dictset
T = TypeVar("T")
@dataclass(repr=False)
class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
class DAG[T](MutableMapping[T, DAGSetView[T]]):
"""
DAG adjacency map:
- 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
@property
def reverse(self) -> DAG[T]:
def reverse(self) -> Self:
return type(self)(
_succ=self._pred,
_pred=self._succ,
@@ -57,7 +55,7 @@ class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
# --- MutableMapping interface ---
def __setitem__(self, u: T, value: Iterable[T]) -> None:
def __setitem__(self, u: T, value: T | Iterable[T]) -> None:
match value:
case set() as vs:
self.__dagsetmulti__(u, vs)
@@ -120,7 +118,7 @@ class DAG(Generic[T], MutableMapping[T, DAGSetView[T]]):
self._succ.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)
subgraph = type(self)(
_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 collections.abc import Iterable
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING
from .util import ensure_set
if TYPE_CHECKING:
from .dag import DAG
T = TypeVar("T")
def all_nodes(graph: DAG) -> set:
def all_nodes[T](graph: DAG[T]) -> set[T]:
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)
stack = list(sub)
seen = set()
View File
+10 -12
View File
@@ -1,22 +1,20 @@
from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator, MutableSet
from typing import Any, TypeVar
from typing import Self
from .util import ensure_set
T = TypeVar("T")
class DAGSetView(MutableSet[T]):
class DAGSetView[T](MutableSet[T]):
"""A mutable set-like view onto DAG._succ[u], with reverse-index maintenance."""
_data: set[T]
on_add: Callable[[Any], None] | None = None
on_remove: Callable[[Any], None] | None = None
on_add: Callable[[T], 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)
# --- required MutableSet methods ---
@@ -43,23 +41,23 @@ class DAGSetView(MutableSet[T]):
# --- in-place operator support ---
def __iadd__(self, other: Iterable[T]) -> DAGSetView[T]:
def __iadd__(self, other: T | Iterable[T]) -> Self:
# a += b => update/extend
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
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
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
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
return super().__ixor__(ensure_set(other))
+2 -5
View File
@@ -2,9 +2,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import MutableMapping, MutableSet
from typing import TypeVar
T = TypeVar("T")
type dictset = defaultdict[T, set[T]]
type DAGType = MutableMapping[T, MutableSet[T]]
type dictset[T] = defaultdict[T, set[T]]
type DAGType[T] = MutableMapping[T, MutableSet[T]]
+20
View File
@@ -3,11 +3,17 @@ from __future__ import annotations
import pytest
from daglib.dag import DAG
from daglib.operations import all_nodes, transitive_closure
from daglib.typing import DAGType, dictset
class TestDAGInit:
"""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:
g = DAG()
assert len(g) == 0
@@ -281,6 +287,20 @@ class TestDAGCallbacks:
class TestDAGComplexScenarios:
"""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:
g = DAG[str]()
g["foo"] = {"bar"}
+18
View File
@@ -39,6 +39,10 @@ class TestDAGSetInit:
s = DAGSetView(None)
assert len(s) == 0
def test_from_scalar(self) -> None:
s = DAGSetView[int](2)
assert set(s) == {2}
class TestDAGSetBasicOps:
"""Test basic set operations."""
@@ -169,6 +173,20 @@ class TestDAGSetCallbacks:
class TestDAGSetInPlaceOperators:
"""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:
def test_set(self) -> None:
s = DAGSetView({"foo", 2})
Generated
+1401 -414
View File
File diff suppressed because it is too large Load Diff