started tests
This commit is contained in:
@@ -20,8 +20,20 @@ build-backend = "uv_build"
|
|||||||
dev = [
|
dev = [
|
||||||
"ipykernel>=7.2.0",
|
"ipykernel>=7.2.0",
|
||||||
"pre-commit>=4.5.1",
|
"pre-commit>=4.5.1",
|
||||||
|
"rich>=14.3.3",
|
||||||
"ruff>=0.15.2",
|
"ruff>=0.15.2",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest>=9.0.2",
|
"pytest>=9.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-v",
|
||||||
|
"--strict-markers",
|
||||||
|
"--strict-config",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections import defaultdict
|
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 typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
from .set import DAGSet
|
from .set import DAGSet
|
||||||
@@ -10,7 +11,7 @@ type dictset = defaultdict[T, set[T]]
|
|||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
class DAG(Generic[T], MutableMapping[T, DAGSet[T]]):
|
class DAG(Generic[T], MutableMapping[T, DAGSet[T]]):
|
||||||
"""
|
"""
|
||||||
DAG adjacency map:
|
DAG adjacency map:
|
||||||
@@ -21,16 +22,12 @@ class DAG(Generic[T], MutableMapping[T, DAGSet[T]]):
|
|||||||
a guarded view that keeps reverse in sync.
|
a guarded view that keeps reverse in sync.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_succ: dictset[T]
|
_succ: dictset[T] = field(default_factory=lambda: defaultdict(set))
|
||||||
_pred: dictset[T]
|
_pred: dictset[T] = field(default_factory=lambda: defaultdict(set))
|
||||||
|
|
||||||
on_add: Callable[[T, T], None] | None = None
|
on_add: Callable[[T, T], None] | None = None
|
||||||
on_remove: Callable[[T, T], None] | None = None
|
on_remove: Callable[[T, T], None] | None = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._succ: dictset[T] = defaultdict(set)
|
|
||||||
self._pred: dictset[T] = defaultdict(set)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reverse(self) -> dictset[T]:
|
def reverse(self) -> dictset[T]:
|
||||||
return self._pred
|
return self._pred
|
||||||
@@ -70,7 +67,8 @@ class DAG(Generic[T], MutableMapping[T, DAGSet[T]]):
|
|||||||
for v in vs:
|
for v in vs:
|
||||||
self._pred[v] |= {u}
|
self._pred[v] |= {u}
|
||||||
case _:
|
case _:
|
||||||
raise TypeError(f"Expected set or iterable, got {type(vs).__name__}")
|
self._succ[u] |= {vs}
|
||||||
|
self._pred[vs] |= {u}
|
||||||
|
|
||||||
def __delitem__(self, u: T) -> None:
|
def __delitem__(self, u: T) -> None:
|
||||||
self.discard_node(u)
|
self.discard_node(u)
|
||||||
|
|||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# tests package
|
||||||
307
tests/test_dag.py
Normal file
307
tests/test_dag.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from daglib.dag import DAG
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGInit:
|
||||||
|
"""Test DAG initialization."""
|
||||||
|
|
||||||
|
def test_init_empty(self) -> None:
|
||||||
|
g = DAG()
|
||||||
|
assert len(g) == 0
|
||||||
|
|
||||||
|
def test_default_callbacks_none(self) -> None:
|
||||||
|
g = DAG()
|
||||||
|
assert g.on_add is None
|
||||||
|
assert g.on_remove is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGAddEdge:
|
||||||
|
"""Test adding edges."""
|
||||||
|
|
||||||
|
def test_add_single_edge(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
|
||||||
|
def test_add_multiple_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "c" in g["a"]
|
||||||
|
assert "c" in g["b"]
|
||||||
|
|
||||||
|
def test_add_edge_creates_nodes(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
assert "a" in g._succ
|
||||||
|
assert "b" in g._pred
|
||||||
|
|
||||||
|
def test_self_loop_raises_error(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
with pytest.raises(ValueError, match="Self-loops are not allowed"):
|
||||||
|
g.add_edge("a", "a")
|
||||||
|
|
||||||
|
def test_add_duplicate_edge_idempotent(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
assert len(g["a"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGRemoveEdge:
|
||||||
|
"""Test removing edges."""
|
||||||
|
|
||||||
|
def test_remove_existing_edge(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.remove_edge("a", "b")
|
||||||
|
assert "b" not in g["a"]
|
||||||
|
assert "a" not in g.reverse["b"]
|
||||||
|
|
||||||
|
def test_remove_nonexistent_edge_missing_ok(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.remove_edge("a", "b", missing_ok=True) # should not raise
|
||||||
|
|
||||||
|
def test_remove_nonexistent_edge_error(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
g.remove_edge("a", "b", missing_ok=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGDiscardNode:
|
||||||
|
"""Test node removal."""
|
||||||
|
|
||||||
|
def test_discard_node_removes_outgoing_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.discard_node("a")
|
||||||
|
assert "a" not in g._succ
|
||||||
|
assert "a" not in g.reverse["b"]
|
||||||
|
assert "a" not in g.reverse["c"]
|
||||||
|
|
||||||
|
def test_discard_node_removes_incoming_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
g.discard_node("c")
|
||||||
|
assert "c" not in g._pred
|
||||||
|
assert "c" not in g["a"]
|
||||||
|
assert "c" not in g["b"]
|
||||||
|
|
||||||
|
def test_discard_nonexistent_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.discard_node("z") # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGGetItem:
|
||||||
|
"""Test dictionary-style access."""
|
||||||
|
|
||||||
|
def test_getitem_returns_dagset(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
dagset = g["a"]
|
||||||
|
assert "b" in dagset
|
||||||
|
|
||||||
|
def test_getitem_empty_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
dagset = g["nonexistent"]
|
||||||
|
assert len(dagset) == 0
|
||||||
|
|
||||||
|
def test_getitem_mutation_updates_graph(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"].add("b")
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
|
||||||
|
def test_getitem_mutation_triggers_callbacks(self) -> None:
|
||||||
|
added: list[tuple[str, str]] = []
|
||||||
|
g = DAG[str]()
|
||||||
|
g.on_add = lambda u, v: added.append((u, v))
|
||||||
|
g["a"].add("b")
|
||||||
|
assert ("a", "b") in added
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetItem:
|
||||||
|
"""Test dictionary-style assignment."""
|
||||||
|
|
||||||
|
def test_setitem_with_set(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = {"b", "c"}
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "c" in g["a"]
|
||||||
|
|
||||||
|
def test_setitem_with_list(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = ["b", "c"]
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "c" in g["a"]
|
||||||
|
|
||||||
|
def test_setitem_with_string(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = "b"
|
||||||
|
assert "b" in g["a"]
|
||||||
|
|
||||||
|
def test_setitem_with_single_item(self) -> None:
|
||||||
|
g = DAG[int]()
|
||||||
|
g[1] = 2
|
||||||
|
assert 2 in g[1]
|
||||||
|
|
||||||
|
def test_setitem_updates_reverse(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = {"b", "c"}
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
assert "a" in g.reverse["c"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGDelItem:
|
||||||
|
"""Test del operation."""
|
||||||
|
|
||||||
|
def test_delitem_removes_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
del g["a"]
|
||||||
|
assert "a" not in g._succ
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGIter:
|
||||||
|
"""Test iteration."""
|
||||||
|
|
||||||
|
def test_iter_empty(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
assert list(g) == []
|
||||||
|
|
||||||
|
def test_iter_returns_nodes(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
nodes = set(g)
|
||||||
|
assert "a" in nodes
|
||||||
|
assert "b" in nodes
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGLen:
|
||||||
|
"""Test length (edge count)."""
|
||||||
|
|
||||||
|
def test_len_empty(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
assert len(g) == 0
|
||||||
|
|
||||||
|
def test_len_counts_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
assert len(g) == 3
|
||||||
|
|
||||||
|
def test_len_after_removal(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.remove_edge("a", "b")
|
||||||
|
assert len(g) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGReverse:
|
||||||
|
"""Test reverse (predecessor) access."""
|
||||||
|
|
||||||
|
def test_reverse_property(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
assert len(g.reverse["a"]) == 0
|
||||||
|
|
||||||
|
def test_reverse_multiple_predecessors(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
preds = g.reverse["c"]
|
||||||
|
assert "a" in preds
|
||||||
|
assert "b" in preds
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGCallbacks:
|
||||||
|
"""Test callback mechanisms."""
|
||||||
|
|
||||||
|
def test_on_add_callback_via_setitem(self) -> None:
|
||||||
|
added: list[tuple[str, str]] = []
|
||||||
|
g = DAG[str]()
|
||||||
|
g.on_add = lambda u, v: added.append((u, v))
|
||||||
|
g["a"] = {"b", "c"}
|
||||||
|
# Note: setitem doesn't trigger callbacks in current implementation
|
||||||
|
|
||||||
|
def test_on_add_callback_via_getitem_mutation(self) -> None:
|
||||||
|
added: list[tuple[str, str]] = []
|
||||||
|
g = DAG[str]()
|
||||||
|
g.on_add = lambda u, v: added.append((u, v))
|
||||||
|
g["a"].add("b")
|
||||||
|
assert ("a", "b") in added
|
||||||
|
|
||||||
|
def test_on_remove_callback(self) -> None:
|
||||||
|
removed: list[tuple[str, str]] = []
|
||||||
|
g = DAG[str]()
|
||||||
|
g.on_remove = lambda u, v: removed.append((u, v))
|
||||||
|
g["a"].add("b")
|
||||||
|
g["a"].discard("b")
|
||||||
|
assert ("a", "b") in removed
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGRepr:
|
||||||
|
"""Test string representation."""
|
||||||
|
|
||||||
|
def test_repr_empty(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
r = repr(g)
|
||||||
|
assert "DAG" in r
|
||||||
|
assert "{}" in r
|
||||||
|
|
||||||
|
def test_repr_with_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
r = repr(g)
|
||||||
|
assert "DAG" in r
|
||||||
|
assert "a" in r
|
||||||
|
assert "b" in r
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGComplexScenarios:
|
||||||
|
"""Test complex usage patterns."""
|
||||||
|
|
||||||
|
def test_chain_graph(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = {"b"}
|
||||||
|
g["b"] = {"c"}
|
||||||
|
g["c"] = {"d"}
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "c" in g["b"]
|
||||||
|
assert "d" in g["c"]
|
||||||
|
assert len(g) == 3
|
||||||
|
|
||||||
|
def test_diamond_graph(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = {"b", "c"}
|
||||||
|
g["b"] = {"d"}
|
||||||
|
g["c"] = {"d"}
|
||||||
|
assert len(g.reverse["d"]) == 2
|
||||||
|
|
||||||
|
def test_batch_operations(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] += {"b", "c", "d"}
|
||||||
|
assert len(g["a"]) == 3
|
||||||
|
g["a"] -= {"b"}
|
||||||
|
assert len(g["a"]) == 2
|
||||||
|
assert "b" not in g["a"]
|
||||||
|
|
||||||
|
def test_type_hints_with_ints(self) -> None:
|
||||||
|
g = DAG[int]()
|
||||||
|
g[1] = {2, 3}
|
||||||
|
g[2] = {4}
|
||||||
|
assert 2 in g[1]
|
||||||
|
assert 4 in g[2]
|
||||||
|
assert len(g) == 3
|
||||||
222
tests/test_dagset.py
Normal file
222
tests/test_dagset.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from daglib.set import DAGSet
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetInit:
|
||||||
|
"""Test DAGSet initialization."""
|
||||||
|
|
||||||
|
def test_init_empty(self) -> None:
|
||||||
|
s = DAGSet()
|
||||||
|
assert len(s) == 0
|
||||||
|
assert list(s) == []
|
||||||
|
|
||||||
|
def test_init_from_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
assert len(s) == 3
|
||||||
|
assert set(s) == {1, 2, 3}
|
||||||
|
|
||||||
|
def test_init_from_list(self) -> None:
|
||||||
|
s = DAGSet([1, 2, 3, 2])
|
||||||
|
assert len(s) == 3
|
||||||
|
assert set(s) == {1, 2, 3}
|
||||||
|
|
||||||
|
def test_init_from_tuple(self) -> None:
|
||||||
|
s = DAGSet((4, 5, 6))
|
||||||
|
assert len(s) == 3
|
||||||
|
assert 4 in s and 5 in s and 6 in s
|
||||||
|
|
||||||
|
def test_init_none(self) -> None:
|
||||||
|
s = DAGSet(None)
|
||||||
|
assert len(s) == 0
|
||||||
|
|
||||||
|
def test_init_invalid_type(self) -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
DAGSet(42) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetBasicOps:
|
||||||
|
"""Test basic set operations."""
|
||||||
|
|
||||||
|
def test_contains(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
assert 1 in s
|
||||||
|
assert 4 not in s
|
||||||
|
|
||||||
|
def test_add(self) -> None:
|
||||||
|
s = DAGSet()
|
||||||
|
s.add(1)
|
||||||
|
assert 1 in s
|
||||||
|
assert len(s) == 1
|
||||||
|
|
||||||
|
def test_discard(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s.discard(2)
|
||||||
|
assert 2 not in s
|
||||||
|
assert len(s) == 2
|
||||||
|
|
||||||
|
def test_discard_missing(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s.discard(99) # should not raise
|
||||||
|
assert len(s) == 3
|
||||||
|
|
||||||
|
def test_remove(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s.remove(2)
|
||||||
|
assert 2 not in s
|
||||||
|
|
||||||
|
def test_remove_missing(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
s.remove(99)
|
||||||
|
|
||||||
|
def test_iter(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
items = set(s)
|
||||||
|
assert items == {1, 2, 3}
|
||||||
|
|
||||||
|
def test_len(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
assert len(s) == 3
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetCallbacks:
|
||||||
|
"""Test callback mechanisms."""
|
||||||
|
|
||||||
|
def test_on_add_callback(self) -> None:
|
||||||
|
added: list[int] = []
|
||||||
|
s = DAGSet({1, 2})
|
||||||
|
s.on_add = lambda v: added.append(v)
|
||||||
|
s.add(3)
|
||||||
|
assert 3 in added
|
||||||
|
assert len(added) == 1
|
||||||
|
|
||||||
|
def test_on_remove_callback(self) -> None:
|
||||||
|
removed: list[int] = []
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s.on_remove = lambda v: removed.append(v)
|
||||||
|
s.discard(2)
|
||||||
|
assert 2 in removed
|
||||||
|
assert len(removed) == 1
|
||||||
|
|
||||||
|
def test_callbacks_none_by_default(self) -> None:
|
||||||
|
s = DAGSet()
|
||||||
|
assert s.on_add is None
|
||||||
|
assert s.on_remove is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetInPlaceOperators:
|
||||||
|
"""Test in-place set operators."""
|
||||||
|
|
||||||
|
def test_ior_with_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2})
|
||||||
|
s |= {3, 4}
|
||||||
|
assert set(s) == {1, 2, 3, 4}
|
||||||
|
|
||||||
|
def test_ior_with_list(self) -> None:
|
||||||
|
s = DAGSet({1})
|
||||||
|
s |= [2, 3]
|
||||||
|
assert set(s) == {1, 2, 3}
|
||||||
|
|
||||||
|
def test_ior_with_string(self) -> None:
|
||||||
|
s = DAGSet({"a", "b"})
|
||||||
|
s |= "c"
|
||||||
|
assert "c" in s
|
||||||
|
|
||||||
|
def test_iadd_with_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2})
|
||||||
|
s += {3, 4}
|
||||||
|
assert set(s) == {1, 2, 3, 4}
|
||||||
|
|
||||||
|
def test_iadd_with_list(self) -> None:
|
||||||
|
s = DAGSet({1})
|
||||||
|
s += [2, 3]
|
||||||
|
assert set(s) == {1, 2, 3}
|
||||||
|
|
||||||
|
def test_iadd_with_string(self) -> None:
|
||||||
|
s = DAGSet({"a"})
|
||||||
|
s += "b"
|
||||||
|
assert "b" in s
|
||||||
|
|
||||||
|
def test_isub_with_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3, 4})
|
||||||
|
s -= {2, 3}
|
||||||
|
assert set(s) == {1, 4}
|
||||||
|
|
||||||
|
def test_isub_with_list(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s -= [2]
|
||||||
|
assert set(s) == {1, 3}
|
||||||
|
|
||||||
|
def test_isub_with_string(self) -> None:
|
||||||
|
s = DAGSet({"a", "b", "c"})
|
||||||
|
s -= "b"
|
||||||
|
assert set(s) == {"a", "c"}
|
||||||
|
|
||||||
|
def test_iand_with_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s &= {2, 3, 4}
|
||||||
|
assert set(s) == {2, 3}
|
||||||
|
|
||||||
|
def test_iand_with_list(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s &= [2, 3]
|
||||||
|
assert set(s) == {2, 3}
|
||||||
|
|
||||||
|
def test_ixor_with_set(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s ^= {3, 4, 5}
|
||||||
|
assert set(s) == {1, 2, 4, 5}
|
||||||
|
|
||||||
|
def test_ixor_with_list(self) -> None:
|
||||||
|
s = DAGSet({1, 2})
|
||||||
|
s ^= [2, 3]
|
||||||
|
assert set(s) == {1, 3}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetCallbacksWithOperators:
|
||||||
|
"""Test that callbacks fire with in-place operators."""
|
||||||
|
|
||||||
|
def test_ior_triggers_callbacks(self) -> None:
|
||||||
|
added: list[int] = []
|
||||||
|
s = DAGSet({1})
|
||||||
|
s.on_add = lambda v: added.append(v)
|
||||||
|
s |= {2, 3}
|
||||||
|
assert 2 in added
|
||||||
|
assert 3 in added
|
||||||
|
|
||||||
|
def test_iadd_triggers_callbacks(self) -> None:
|
||||||
|
added: list[int] = []
|
||||||
|
s = DAGSet({1})
|
||||||
|
s.on_add = lambda v: added.append(v)
|
||||||
|
s += [2, 3]
|
||||||
|
assert 2 in added
|
||||||
|
assert 3 in added
|
||||||
|
|
||||||
|
def test_isub_triggers_callbacks(self) -> None:
|
||||||
|
removed: list[int] = []
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
s.on_remove = lambda v: removed.append(v)
|
||||||
|
s -= {2, 3}
|
||||||
|
assert 2 in removed
|
||||||
|
assert 3 in removed
|
||||||
|
|
||||||
|
|
||||||
|
class TestDAGSetRepr:
|
||||||
|
"""Test string representation."""
|
||||||
|
|
||||||
|
def test_repr_empty(self) -> None:
|
||||||
|
s = DAGSet()
|
||||||
|
assert repr(s) == "{}"
|
||||||
|
|
||||||
|
def test_repr_with_items(self) -> None:
|
||||||
|
s = DAGSet({1, 2, 3})
|
||||||
|
r = repr(s)
|
||||||
|
assert r.startswith("{")
|
||||||
|
assert r.endswith("}")
|
||||||
|
# items may be in any order
|
||||||
|
assert "1" in r
|
||||||
|
assert "2" in r
|
||||||
|
assert "3" in r
|
||||||
36
uv.lock
generated
36
uv.lock
generated
@@ -113,6 +113,7 @@ source = { editable = "." }
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "ipykernel" },
|
{ name = "ipykernel" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
@@ -125,6 +126,7 @@ test = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "ipykernel", specifier = ">=7.2.0" },
|
{ name = "ipykernel", specifier = ">=7.2.0" },
|
||||||
{ name = "pre-commit", specifier = ">=4.5.1" },
|
{ name = "pre-commit", specifier = ">=4.5.1" },
|
||||||
|
{ name = "rich", specifier = ">=14.3.3" },
|
||||||
{ name = "ruff", specifier = ">=0.15.2" },
|
{ name = "ruff", specifier = ">=0.15.2" },
|
||||||
]
|
]
|
||||||
test = [{ name = "pytest", specifier = ">=9.0.2" }]
|
test = [{ name = "pytest", specifier = ">=9.0.2" }]
|
||||||
@@ -302,6 +304,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
|
{ url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matplotlib-inline"
|
name = "matplotlib-inline"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -314,6 +328,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nest-asyncio"
|
name = "nest-asyncio"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -589,6 +612,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user