type modernization
This commit is contained in:
+2
-2
@@ -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
@@ -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}),
|
||||
|
||||
@@ -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()
|
||||
|
||||
+10
-12
@@ -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,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]]
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user