Skip to content

Commit f7292ad

Browse files
simplify test
1 parent a63e229 commit f7292ad

2 files changed

Lines changed: 78 additions & 245 deletions

File tree

diffly/summary.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ def _validate_primary_key_hidden_columns() -> None:
220220
rows_equal = comp._equal_rows()
221221
else:
222222
rows_equal = comp.equal_num_rows()
223+
# NOTE: In slim mode, we only print the section if there are differences.
223224
if not slim or not rows_equal:
224225
if comp.primary_key is not None:
225226
rows = SummaryDataRows(
@@ -231,6 +232,8 @@ def _validate_primary_key_hidden_columns() -> None:
231232
n_right_only=comp.num_rows_right_only(),
232233
_equal_rows=comp._equal_rows(),
233234
_equal_num_rows=comp.equal_num_rows(),
235+
# NOTE: In slim mode, we omit the row counts section and only show the
236+
# row matches section.
234237
_show_row_counts=not (comp.equal_num_rows() and slim),
235238
)
236239
else:
@@ -248,11 +251,15 @@ def _validate_primary_key_hidden_columns() -> None:
248251

249252
# --- Columns ---
250253
columns: list[SummaryDataColumn] | None = None
254+
# NOTE: We can only compute column matches if there are primary key columns and at
255+
# least one joined row.
251256
match_rates_can_be_computed = (
252257
comp.primary_key is not None and comp.num_rows_joined() > 0
253258
)
254259
if match_rates_can_be_computed:
255260
match_rates = comp.fraction_same()
261+
# NOTE: In slim mode, we only print the columns section if there are
262+
# non-primary key columns and at least one column has a match rate < 1.
256263
if not slim or (comp._other_common_columns and min(match_rates.values()) < 1):
257264
columns = []
258265
for col_name in sorted(match_rates):
@@ -453,6 +460,8 @@ def _print_primary_key(self, console: Console) -> None:
453460
" computed.",
454461
style="italic",
455462
)
463+
# NOTE: The primary key is only displayed in the default mode. If a primary key
464+
# was not supplied, the warning is displayed in both modes.
456465
if not self.slim or primary_key is None:
457466
console.print(Padding(content, pad=(0, 3)))
458467
console.print("")

tests/test_summary_data.py

Lines changed: 69 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,32 @@
33

44
import itertools
55
import json
6-
from datetime import date, datetime, timedelta
7-
from decimal import Decimal
86

97
import polars as pl
108
import pytest
119

1210
from diffly import compare_frames
1311
from diffly.comparison import DataFrameComparison
14-
from diffly.summary import (
15-
SummaryData,
16-
SummaryDataColumn,
17-
SummaryDataColumnChange,
18-
SummaryDataRows,
19-
SummaryDataSchemas,
20-
_to_python,
21-
)
2212

2313

2414
def _make_comparison() -> DataFrameComparison:
25-
"""A rich comparison with schema diffs, row diffs, and column diffs."""
15+
# Designed so every parametrized flag affects the expected JSON output:
16+
# - Same columns in both frames → schemas equal → slim suppresses schemas section
17+
# - status matches perfectly for joined rows → show_perfect_column_matches matters
18+
# - value differs for id=2 → always has a non-perfect column
19+
# - id=4 left-only, id=5 right-only → sample rows matter
2620
left = pl.DataFrame(
2721
{
2822
"id": [1, 2, 3, 4],
2923
"status": ["a", "b", "c", "d"],
3024
"value": [10.0, 20.0, 30.0, 40.0],
31-
"left_col": ["x", "y", "z", "w"],
3225
}
3326
)
3427
right = pl.DataFrame(
3528
{
3629
"id": [1, 2, 3, 5],
37-
"status": ["a", "x", "x", "e"],
30+
"status": ["a", "b", "c", "e"],
3831
"value": [10.0, 25.0, 30.0, 50.0],
39-
"right_col": ["p", "q", "r", "s"],
4032
}
4133
)
4234
return compare_frames(left, right, primary_key="id")
@@ -57,241 +49,73 @@ def test_summary_data_parametrized(
5749
sample_pk: bool,
5850
) -> None:
5951
comp = _make_comparison()
52+
top_k = 3 if show_top_column_changes else 0
6053
summary = comp.summary(
6154
show_perfect_column_matches=show_perfect_column_matches,
62-
top_k_column_changes=3 if show_top_column_changes else 0,
63-
slim=slim,
55+
top_k_column_changes=top_k,
6456
sample_k_rows_only=3 if sample_rows else 0,
6557
show_sample_primary_key_per_change=sample_pk,
58+
slim=slim,
6659
)
67-
data = summary._data
68-
69-
assert isinstance(data, SummaryData)
70-
assert data.equal is False
71-
assert data.primary_key == ["id"]
72-
73-
# --- Schemas ---
74-
schemas_equal = comp.schemas.equal()
75-
if slim and schemas_equal:
76-
assert data.schemas is None
77-
else:
78-
assert isinstance(data.schemas, SummaryDataSchemas)
79-
assert len(data.schemas.left_only_names) > 0 # left_col
80-
assert len(data.schemas.right_only_names) > 0 # right_col
81-
82-
# --- Rows ---
83-
rows_equal = comp._equal_rows()
84-
if slim and rows_equal:
85-
assert data.rows is None
86-
else:
87-
assert isinstance(data.rows, SummaryDataRows)
88-
assert data.rows.n_left == 4
89-
assert data.rows.n_right == 4
90-
assert data.rows.n_left_only is not None
91-
assert data.rows.n_right_only is not None
92-
93-
# --- Columns ---
94-
assert data.columns is not None
95-
match_rates = comp.fraction_same()
96-
for col in data.columns:
97-
assert isinstance(col, SummaryDataColumn)
98-
rate = match_rates[col.name]
99-
assert col.match_rate == rate
100-
if show_top_column_changes and rate < 1:
101-
assert col.changes is not None
102-
for change in col.changes:
103-
assert isinstance(change, SummaryDataColumnChange)
104-
if sample_pk:
105-
assert isinstance(change.sample_pk, tuple)
106-
assert len(change.sample_pk) == 1
107-
else:
108-
assert change.sample_pk is None
109-
else:
110-
assert col.changes is None
111-
112-
# --- Sample rows ---
113-
if sample_rows:
114-
assert data.sample_rows_left_only is not None
115-
assert data.sample_rows_right_only is not None
116-
assert len(data.sample_rows_left_only) > 0
117-
assert len(data.sample_rows_right_only) > 0
118-
for row in data.sample_rows_left_only:
119-
assert isinstance(row, tuple)
120-
for row in data.sample_rows_right_only:
121-
assert isinstance(row, tuple)
122-
else:
123-
assert data.sample_rows_left_only is None
124-
assert data.sample_rows_right_only is None
125-
126-
# JSON roundtrip
127-
parsed = json.loads(summary.to_json())
128-
assert isinstance(parsed, dict)
129-
assert parsed["equal"] is False
130-
131-
132-
def test_summary_data_equal_frames() -> None:
133-
df = pl.DataFrame({"id": [1, 2], "value": [10.0, 20.0]})
134-
comp = compare_frames(df, df, primary_key="id")
135-
data = comp.summary()._data
136-
assert data.equal is True
137-
assert data.schemas is None
138-
assert data.rows is None
139-
assert data.columns is None
140-
assert data.sample_rows_left_only is None
141-
assert data.sample_rows_right_only is None
142-
143-
144-
def test_summary_data_no_primary_key() -> None:
145-
left = pl.DataFrame({"a": [1, 2], "b": [3.0, 4.0]})
146-
right = pl.DataFrame({"a": [1, 2], "b": [3.0, 5.0]})
147-
comp = compare_frames(left, right)
148-
data = comp.summary()._data
149-
assert data.equal is False
150-
assert data.primary_key is None
151-
assert data.rows is not None
152-
assert data.rows.n_left_only is None
153-
assert data.rows.n_joined_equal is None
154-
assert data.columns is None
155-
assert data.sample_rows_left_only is None
156-
assert data.sample_rows_right_only is None
157-
158-
159-
def test_summary_data_hidden_columns() -> None:
160-
left = pl.DataFrame({"id": [1, 2], "secret": ["a", "b"], "value": [10.0, 20.0]})
161-
right = pl.DataFrame({"id": [1, 2], "secret": ["a", "x"], "value": [10.0, 25.0]})
162-
comp = compare_frames(left, right, primary_key="id")
163-
data = comp.summary(
164-
top_k_column_changes=3,
165-
hidden_columns=["secret"],
166-
)._data
167-
assert data.columns is not None
168-
for col in data.columns:
169-
if col.name == "secret":
170-
assert col.changes is None
171-
elif col.match_rate < 1:
172-
assert col.changes is not None
173-
174-
175-
def test_summary_data_validate_hidden_pk_sample_rows() -> None:
176-
df = pl.DataFrame({"id": ["a", "b", "c"]})
177-
comp = compare_frames(df, df.filter(pl.col("id") == "a"), primary_key=["id"])
178-
with pytest.raises(ValueError, match="Cannot show sample rows only"):
179-
comp.summary(sample_k_rows_only=3, hidden_columns=["id"])
180-
60+
result = json.loads(summary.to_json())
61+
62+
# --- Build expected dictionary ---
63+
# Schemas: equal (same columns, same dtypes) → suppressed in slim mode
64+
expected_schemas: dict | None = None
65+
if not slim:
66+
expected_schemas = {
67+
"left_only_names": [],
68+
"in_common": [
69+
["id", "Int64", "Int64"],
70+
["status", "String", "String"],
71+
["value", "Float64", "Float64"],
72+
],
73+
"right_only_names": [],
74+
}
18175

182-
def test_summary_data_validate_hidden_pk_sample_pk() -> None:
183-
df = pl.DataFrame({"id": ["a", "b", "c"], "value": [1.0, 2.0, 3.0]})
184-
comp = compare_frames(df, df.with_columns(pl.col("value") + 1), primary_key=["id"])
185-
with pytest.raises(ValueError, match="Cannot show sample primary key"):
186-
comp.summary(
187-
top_k_column_changes=3,
188-
show_sample_primary_key_per_change=True,
189-
hidden_columns=["id"],
76+
# Columns: status has 100% match rate, value has 2/3
77+
# show_perfect_column_matches controls whether the perfect status column appears
78+
value_col = {
79+
"name": "value",
80+
"match_rate": pytest.approx(2 / 3),
81+
"n_total_changes": 1 if show_top_column_changes else 0,
82+
"changes": (
83+
[
84+
{
85+
"old": 20.0,
86+
"new": 25.0,
87+
"count": 1,
88+
"sample_pk": [2] if sample_pk else None,
89+
}
90+
]
91+
if show_top_column_changes
92+
else None
93+
),
94+
}
95+
expected_columns = []
96+
if show_perfect_column_matches:
97+
expected_columns.append(
98+
{"name": "status", "match_rate": 1.0, "n_total_changes": 0, "changes": None}
19099
)
191-
192-
193-
def test_summary_data_validate_zero_top_k_with_sample_pk() -> None:
194-
df = pl.DataFrame({"id": ["a", "b"], "value": [1.0, 2.0]})
195-
comp = compare_frames(df, df.with_columns(pl.col("value") + 1), primary_key=["id"])
196-
with pytest.raises(
197-
ValueError,
198-
match="Cannot show sample primary key per change when top_k_column_changes is 0",
199-
):
200-
comp.summary(top_k_column_changes=0, show_sample_primary_key_per_change=True)
201-
202-
203-
def test_summary_data_multiple_pk_columns() -> None:
204-
left = pl.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"], "val": [10, 20, 30]})
205-
right = pl.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"], "val": [10, 99, 30]})
206-
comp = compare_frames(left, right, primary_key=["a", "b"])
207-
data = comp.summary(
208-
top_k_column_changes=3,
209-
show_sample_primary_key_per_change=True,
210-
sample_k_rows_only=3,
211-
)._data
212-
assert data.primary_key == ["a", "b"]
213-
assert data.columns is not None
214-
for col in data.columns:
215-
if col.changes:
216-
for change in col.changes:
217-
assert isinstance(change.sample_pk, tuple)
218-
assert len(change.sample_pk) == 2
219-
220-
221-
def test_summary_data_to_dict() -> None:
222-
df = pl.DataFrame({"id": [1, 2], "value": [10.0, 20.0]})
223-
comp = compare_frames(df, df, primary_key="id")
224-
d = comp.summary()._data.to_dict()
225-
assert isinstance(d, dict)
226-
assert d["equal"] is True
227-
228-
229-
def test_summary_data_slim_suppresses_matching_sections() -> None:
230-
left = pl.DataFrame({"id": [1, 2, 3], "value": [10.0, 20.0, 30.0]})
231-
right = pl.DataFrame({"id": [1, 2, 3], "value": [10.0, 25.0, 30.0]})
232-
comp = compare_frames(left, right, primary_key="id")
233-
data = comp.summary(slim=True)._data
234-
235-
# Schemas match -> None in slim mode
236-
assert data.schemas is None
237-
# Rows have differences (joined unequal) -> shown
238-
assert data.rows is not None
239-
# Columns have differences -> shown
240-
assert data.columns is not None
241-
242-
243-
@pytest.mark.parametrize(
244-
"value, expected",
245-
[
246-
(datetime(2024, 1, 15, 12, 30), "2024-01-15T12:30:00"),
247-
(date(2024, 1, 15), "2024-01-15"),
248-
(timedelta(seconds=5), 5.0),
249-
(Decimal("1.5"), 1.5),
250-
(42, 42),
251-
("hello", "hello"),
252-
(None, None),
253-
],
254-
)
255-
def test_to_python(value: object, expected: object) -> None:
256-
assert _to_python(value) == expected
257-
258-
259-
def test_to_dict_with_typed_values() -> None:
260-
comp = _make_comparison()
261-
summary = comp.summary(top_k_column_changes=3, sample_k_rows_only=3)
262-
d = summary._data.to_dict()
263-
264-
assert isinstance(d, dict)
265-
assert d["equal"] is False
266-
assert isinstance(d["columns"], list)
267-
assert isinstance(d["sample_rows_left_only"], list)
268-
# Verify roundtrip through JSON works
269-
json_str = json.dumps(d)
270-
parsed = json.loads(json_str)
271-
assert parsed["equal"] is False
272-
assert len(parsed["columns"]) > 0
273-
274-
275-
def test_to_json_with_date_values() -> None:
276-
left = pl.DataFrame({"id": [1, 2], "d": [date(2024, 1, 1), date(2024, 6, 1)]})
277-
right = pl.DataFrame({"id": [1, 2], "d": [date(2024, 1, 1), date(2024, 12, 1)]})
278-
comp = compare_frames(left, right, primary_key="id")
279-
summary = comp.summary(top_k_column_changes=3)
280-
parsed = json.loads(summary.to_json())
281-
assert parsed["equal"] is False
282-
col = next(c for c in parsed["columns"] if c["name"] == "d")
283-
assert col["changes"] is not None
284-
assert col["changes"][0]["old"] == "2024-06-01"
285-
assert col["changes"][0]["new"] == "2024-12-01"
286-
287-
288-
def test_summary_data_n_total_changes() -> None:
289-
left = pl.DataFrame({"id": list(range(10)), "val": list(range(10))})
290-
right = pl.DataFrame({"id": list(range(10)), "val": list(range(10, 20))})
291-
comp = compare_frames(left, right, primary_key="id")
292-
data = comp.summary(top_k_column_changes=3)._data
293-
assert data.columns is not None
294-
col = next(c for c in data.columns if c.name == "val")
295-
assert col.changes is not None
296-
assert len(col.changes) == 3
297-
assert col.n_total_changes == 10
100+
expected_columns.append(value_col)
101+
102+
expected = {
103+
"equal": False,
104+
"left_name": "left",
105+
"right_name": "right",
106+
"primary_key": ["id"],
107+
"schemas": expected_schemas,
108+
"rows": {
109+
"n_left": 4,
110+
"n_right": 4,
111+
"n_left_only": 1,
112+
"n_joined_equal": 2,
113+
"n_joined_unequal": 1,
114+
"n_right_only": 1,
115+
},
116+
"columns": expected_columns,
117+
"sample_rows_left_only": [[4]] if sample_rows else None,
118+
"sample_rows_right_only": [[5]] if sample_rows else None,
119+
}
120+
121+
assert result == expected

0 commit comments

Comments
 (0)