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