Skip to content

Commit 28afb4d

Browse files
committed
Refactored ModelDelta + ModelChange as dataclasses
This gives them useful methods like `__eq__()`, `__repr__()` and `__hash__()` for free :) Note that they have `frozen=True` (mainly to allow safe `__hash__()` implementations), as I think the vast majority of users are in practice treating these classes as read-only; modifying them serve no purpose within this library, and should therefore be an incredibly niche use case (except when comparing against expected mock objects while testing, but then `dataclasses.replace()` can be used).
1 parent 9d232a6 commit 28afb4d

File tree

3 files changed

+80
-52
lines changed

3 files changed

+80
-52
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Unreleased
1515
is used to list the historical records (gh-1128)
1616
- Added ``SimpleHistoryAdmin.get_history_list_display()`` which returns
1717
``history_list_display`` by default, and made the latter into an actual field (gh-1128)
18+
- ``ModelDelta`` and ``ModelChange`` (in ``simple_history.models``) are now immutable
19+
dataclasses; their signatures remain unchanged (gh-1128)
1820

1921
3.5.0 (2024-02-19)
2022
------------------

simple_history/models.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import importlib
33
import uuid
44
import warnings
5+
from dataclasses import dataclass
56
from functools import partial
7+
from typing import Any, Dict, List, Sequence, Union
68

79
import django
810
from django.apps import apps
@@ -1006,16 +1008,16 @@ def diff_against(self, old_history, excluded_fields=None, included_fields=None):
10061008
return ModelDelta(changes, changed_fields, old_history, self)
10071009

10081010

1011+
@dataclass(frozen=True)
10091012
class ModelChange:
1010-
def __init__(self, field_name, old_value, new_value):
1011-
self.field = field_name
1012-
self.old = old_value
1013-
self.new = new_value
1013+
field: str
1014+
old: Union[Any, List[Dict[str, Any]]]
1015+
new: Union[Any, List[Dict[str, Any]]]
10141016

10151017

1018+
@dataclass(frozen=True)
10161019
class ModelDelta:
1017-
def __init__(self, changes, changed_fields, old_record, new_record):
1018-
self.changes = changes
1019-
self.changed_fields = changed_fields
1020-
self.old_record = old_record
1021-
self.new_record = new_record
1020+
changes: Sequence[ModelChange]
1021+
changed_fields: Sequence[str]
1022+
old_record: HistoricalChanges
1023+
new_record: HistoricalChanges

simple_history/tests/tests/test_models.py

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dataclasses
12
import unittest
23
import uuid
34
import warnings
@@ -21,6 +22,7 @@
2122
SIMPLE_HISTORY_REVERSE_ATTR_NAME,
2223
HistoricalRecords,
2324
ModelChange,
25+
ModelDelta,
2426
is_historic,
2527
to_historic,
2628
)
@@ -697,11 +699,13 @@ def test_history_diff_includes_changed_fields(self):
697699
new_record, old_record = p.history.all()
698700
with self.assertNumQueries(0):
699701
delta = new_record.diff_against(old_record)
700-
expected_change = ModelChange("question", "what's up?", "what's up, man")
701-
self.assertEqual(delta.changed_fields, ["question"])
702-
self.assertEqual(delta.old_record, old_record)
703-
self.assertEqual(delta.new_record, new_record)
704-
self.assertEqual(expected_change.field, delta.changes[0].field)
702+
expected_delta = ModelDelta(
703+
[ModelChange("question", "what's up?", "what's up, man?")],
704+
["question"],
705+
old_record,
706+
new_record,
707+
)
708+
self.assertEqual(delta, expected_delta)
705709

706710
def test_history_diff_does_not_include_unchanged_fields(self):
707711
p = Poll.objects.create(question="what's up?", pub_date=today)
@@ -720,11 +724,13 @@ def test_history_diff_includes_changed_fields_of_base_model(self):
720724
new_record, old_record = r.history.all()
721725
with self.assertNumQueries(0):
722726
delta = new_record.diff_against(old_record)
723-
expected_change = ModelChange("name", "McDonna", "DonnutsKing")
724-
self.assertEqual(delta.changed_fields, ["name"])
725-
self.assertEqual(delta.old_record, old_record)
726-
self.assertEqual(delta.new_record, new_record)
727-
self.assertEqual(expected_change.field, delta.changes[0].field)
727+
expected_delta = ModelDelta(
728+
[ModelChange("name", "McDonna", "DonnutsKing")],
729+
["name"],
730+
old_record,
731+
new_record,
732+
)
733+
self.assertEqual(delta, expected_delta)
728734

729735
def test_history_table_name_is_not_inherited(self):
730736
def assert_table_name(obj, expected_table_name):
@@ -759,8 +765,8 @@ def test_history_diff_with_excluded_fields(self):
759765
new_record, old_record = p.history.all()
760766
with self.assertNumQueries(0):
761767
delta = new_record.diff_against(old_record, excluded_fields=("question",))
762-
self.assertEqual(delta.changed_fields, [])
763-
self.assertEqual(delta.changes, [])
768+
expected_delta = ModelDelta([], [], old_record, new_record)
769+
self.assertEqual(delta, expected_delta)
764770

765771
def test_history_diff_with_included_fields(self):
766772
p = Poll.objects.create(question="what's up?", pub_date=today)
@@ -769,13 +775,17 @@ def test_history_diff_with_included_fields(self):
769775
new_record, old_record = p.history.all()
770776
with self.assertNumQueries(0):
771777
delta = new_record.diff_against(old_record, included_fields=[])
772-
self.assertEqual(delta.changed_fields, [])
773-
self.assertEqual(delta.changes, [])
778+
expected_delta = ModelDelta([], [], old_record, new_record)
779+
self.assertEqual(delta, expected_delta)
774780

775781
with self.assertNumQueries(0):
776782
delta = new_record.diff_against(old_record, included_fields=["question"])
777-
self.assertEqual(delta.changed_fields, ["question"])
778-
self.assertEqual(len(delta.changes), 1)
783+
expected_delta = dataclasses.replace(
784+
expected_delta,
785+
changes=[ModelChange("question", "what's up?", "what's up, man?")],
786+
changed_fields=["question"],
787+
)
788+
self.assertEqual(delta, expected_delta)
779789

780790
def test_history_diff_with_non_editable_field(self):
781791
p = PollWithNonEditableField.objects.create(
@@ -786,8 +796,13 @@ def test_history_diff_with_non_editable_field(self):
786796
new_record, old_record = p.history.all()
787797
with self.assertNumQueries(0):
788798
delta = new_record.diff_against(old_record)
789-
self.assertEqual(delta.changed_fields, ["question"])
790-
self.assertEqual(len(delta.changes), 1)
799+
expected_delta = ModelDelta(
800+
[ModelChange("question", "what's up?", "what's up, man?")],
801+
["question"],
802+
old_record,
803+
new_record,
804+
)
805+
self.assertEqual(delta, expected_delta)
791806

792807
def test_history_with_unknown_field(self):
793808
p = Poll.objects.create(question="what's up?", pub_date=today)
@@ -1922,7 +1937,6 @@ def test_self_field(self):
19221937
class ManyToManyWithSignalsTest(TestCase):
19231938
def setUp(self):
19241939
self.model = PollWithManyToManyWithIPAddress
1925-
# self.historical_through_model = self.model.history.
19261940
self.places = (
19271941
Place.objects.create(name="London"),
19281942
Place.objects.create(name="Paris"),
@@ -1963,9 +1977,26 @@ def test_diff(self):
19631977
old = new.prev_record
19641978

19651979
delta = new.diff_against(old)
1966-
1967-
self.assertEqual("places", delta.changes[0].field)
1968-
self.assertEqual(2, len(delta.changes[0].new))
1980+
expected_delta = ModelDelta(
1981+
[
1982+
ModelChange(
1983+
"places",
1984+
[],
1985+
[
1986+
{
1987+
"pollwithmanytomanywithipaddress": self.poll.pk,
1988+
"place": place.pk,
1989+
"ip_address": "192.168.0.1",
1990+
}
1991+
for place in self.places
1992+
],
1993+
)
1994+
],
1995+
["places"],
1996+
old,
1997+
new,
1998+
)
1999+
self.assertEqual(delta, expected_delta)
19692000

19702001

19712002
class ManyToManyCustomIDTest(TestCase):
@@ -2246,24 +2277,19 @@ def test_diff_against(self):
22462277
expected_change = ModelChange(
22472278
"places", [], [{"pollwithmanytomany": self.poll.pk, "place": self.place.pk}]
22482279
)
2249-
self.assertEqual(delta.changed_fields, ["places"])
2250-
self.assertEqual(delta.old_record, create_record)
2251-
self.assertEqual(delta.new_record, add_record)
2252-
self.assertEqual(expected_change.field, delta.changes[0].field)
2253-
2254-
self.assertListEqual(expected_change.new, delta.changes[0].new)
2255-
self.assertListEqual(expected_change.old, delta.changes[0].old)
2280+
expected_delta = ModelDelta(
2281+
[expected_change], ["places"], create_record, add_record
2282+
)
2283+
self.assertEqual(delta, expected_delta)
22562284

22572285
delta = add_record.diff_against(create_record, included_fields=["places"])
2258-
self.assertEqual(delta.changed_fields, ["places"])
2259-
self.assertEqual(delta.old_record, create_record)
2260-
self.assertEqual(delta.new_record, add_record)
2261-
self.assertEqual(expected_change.field, delta.changes[0].field)
2286+
self.assertEqual(delta, expected_delta)
22622287

22632288
delta = add_record.diff_against(create_record, excluded_fields=["places"])
2264-
self.assertEqual(delta.changed_fields, [])
2265-
self.assertEqual(delta.old_record, create_record)
2266-
self.assertEqual(delta.new_record, add_record)
2289+
expected_delta = dataclasses.replace(
2290+
expected_delta, changes=[], changed_fields=[]
2291+
)
2292+
self.assertEqual(delta, expected_delta)
22672293

22682294
self.poll.places.clear()
22692295

@@ -2272,18 +2298,16 @@ def test_diff_against(self):
22722298
delta = del_record.diff_against(create_record)
22732299
self.assertNotIn("places", delta.changed_fields)
22742300

2301+
delta = del_record.diff_against(add_record)
22752302
# Second and third should have the same diffs as first and second, but with
22762303
# old and new reversed
22772304
expected_change = ModelChange(
22782305
"places", [{"place": self.place.pk, "pollwithmanytomany": self.poll.pk}], []
22792306
)
2280-
delta = del_record.diff_against(add_record)
2281-
self.assertEqual(delta.changed_fields, ["places"])
2282-
self.assertEqual(delta.old_record, add_record)
2283-
self.assertEqual(delta.new_record, del_record)
2284-
self.assertEqual(expected_change.field, delta.changes[0].field)
2285-
self.assertListEqual(expected_change.new, delta.changes[0].new)
2286-
self.assertListEqual(expected_change.old, delta.changes[0].old)
2307+
expected_delta = ModelDelta(
2308+
[expected_change], ["places"], add_record, del_record
2309+
)
2310+
self.assertEqual(delta, expected_delta)
22872311

22882312

22892313
@override_settings(**database_router_override_settings)

0 commit comments

Comments
 (0)