Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/tracksdata/_test/test_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tracksdata.attrs import (
Attr,
AttrComparison,
AttrFilter,
EdgeAttr,
NodeAttr,
attr_comps_to_strs,
Expand Down Expand Up @@ -650,3 +651,116 @@ def test_attr_is_in_accepts_numpy_arrays() -> None:

evaluated = comp.to_attr().evaluate(df)
assert evaluated.to_list() == [False, True, False]


# ---------------------------------------------------------------------------
# AttrFilter (compound boolean filters built from AttrComparisons)
# ---------------------------------------------------------------------------


def test_attr_filter_or_operator_returns_filter() -> None:
comp1 = NodeAttr("t") == 1
comp2 = NodeAttr("t") == 2
f = comp1 | comp2

assert isinstance(f, AttrFilter)
assert f.op == "or"
assert f.operands == [comp1, comp2]
assert f.columns == ["t"]


def test_attr_filter_xor_and_invert_operators() -> None:
comp1 = NodeAttr("a") > 0
comp2 = NodeAttr("b") > 0
xor_f = comp1 ^ comp2
assert isinstance(xor_f, AttrFilter)
assert xor_f.op == "xor"

not_f = ~comp1
assert isinstance(not_f, AttrFilter)
assert not_f.op == "not"
assert not_f.operands == [comp1]


def test_attr_filter_and_operator_between_comparisons() -> None:
comp1 = NodeAttr("a") > 0
comp2 = NodeAttr("b") < 1
and_f = comp1 & comp2
assert isinstance(and_f, AttrFilter)
assert and_f.op == "and"


def test_attr_filter_nested_composition() -> None:
f = (NodeAttr("a") > 0) & ((NodeAttr("b") == 1) | (NodeAttr("b") == 2))
assert isinstance(f, AttrFilter)
assert f.op == "and"
assert isinstance(f.operands[1], AttrFilter)
assert f.operands[1].op == "or"
assert sorted({leaf.column for leaf in f.leaves()}) == ["a", "b"]


def test_attr_filter_mixed_node_and_edge_raises_on_split() -> None:
"""A single compound that mixes node and edge attributes must error in split."""
f = (NodeAttr("t") == 1) | (EdgeAttr("weight") > 0.5)
with pytest.raises(ValueError, match="cannot mix NodeAttr and EdgeAttr"):
split_attr_comps([f])


def test_attr_filter_split_attr_comps_with_compounds() -> None:
node_f = (NodeAttr("t") == 1) | (NodeAttr("t") == 2)
edge_f = (EdgeAttr("w") > 0.5) | (EdgeAttr("w") < -0.5)
node_only = NodeAttr("label") == "A"

nodes, edges = split_attr_comps([node_f, edge_f, node_only])
assert nodes == [node_f, node_only]
assert edges == [edge_f]


def test_attr_filter_invalid_op_raises() -> None:
with pytest.raises(ValueError, match="Unknown logical operator"):
AttrFilter("nor", [NodeAttr("a") == 1, NodeAttr("a") == 2])


def test_attr_filter_not_with_multiple_operands_raises() -> None:
with pytest.raises(ValueError, match="'not' filter requires exactly one operand"):
AttrFilter("not", [NodeAttr("a") == 1, NodeAttr("a") == 2])


def test_attr_filter_or_with_single_operand_raises() -> None:
with pytest.raises(ValueError, match="'or' filter requires at least two operands"):
AttrFilter("or", [NodeAttr("a") == 1])


def test_attr_filter_rejects_non_filter_operands() -> None:
with pytest.raises(TypeError, match="must be AttrComparison or AttrFilter"):
AttrFilter("or", [NodeAttr("a") == 1, 5])


def test_attr_filter_polars_reduce_or() -> None:
df = pl.DataFrame({"t": [0, 1, 2, 3, 4]})
f = (NodeAttr("t") == 1) | (NodeAttr("t") == 3)
expr = polars_reduce_attr_comps(df, [f], operator.and_)
result = df.select(expr).to_series()
assert result.to_list() == [False, True, False, True, False]


def test_attr_filter_polars_reduce_xor() -> None:
df = pl.DataFrame({"a": [0, 1, 0, 1], "b": [0, 0, 1, 1]})
f = (NodeAttr("a") == 1) ^ (NodeAttr("b") == 1)
expr = polars_reduce_attr_comps(df, [f], operator.and_)
result = df.select(expr).to_series()
assert result.to_list() == [False, True, True, False]


def test_attr_filter_polars_reduce_not() -> None:
df = pl.DataFrame({"t": [0, 1, 2]})
f = ~(NodeAttr("t") == 1)
expr = polars_reduce_attr_comps(df, [f], operator.and_)
result = df.select(expr).to_series()
assert result.to_list() == [True, False, True]


def test_attr_filter_attr_comps_to_strs_with_compound() -> None:
f = (NodeAttr("a") == 1) | (NodeAttr("b") == 2)
plain = NodeAttr("c") == 3
assert attr_comps_to_strs([f, plain]) == ["a", "b", "c"]
Loading
Loading