Skip to content

Commit b1430e9

Browse files
author
Test User
committed
Modernize for Python 3.14 and Django 5-6
1 parent 92124eb commit b1430e9

20 files changed

Lines changed: 818 additions & 423 deletions

.github/workflows/publish.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
permissions:
9+
contents: read
10+
id-token: write
11+
12+
jobs:
13+
publish:
14+
name: Build and Publish to PyPI
15+
runs-on: ubuntu-latest
16+
environment: pypi
17+
18+
steps:
19+
- name: Check out code
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.14"
26+
27+
- name: Install build tools
28+
run: python -m pip install --upgrade pip build
29+
30+
- name: Build package
31+
run: python -m build
32+
33+
- name: Publish to PyPI
34+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.venv/
2+
__pycache__/
3+
*.egg-info/
4+
*.pyc
5+
.coverage
6+
.ruff_cache/
7+
.pyright/
8+
dist/
9+
build/

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.14

README.rst

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,36 @@
22
django-model-changes
33
====================
44

5-
Please note: django-model-changes does not support Python3.0+. A fork is maintained at https://github.com/iansprice/django-model-changes-py3 for Python3.3+ and can be installed via `pip install django-model-changes-py3`.
5+
django-model-changes allows you to track the state and changes of a model instance.
66

7-
django-model-changes allows you to track the state and changes of a model instance:
7+
**Requirements:** Python 3.14+, Django 5.0-6.x
88

99
Quick start
1010
-----------
1111

1212
1. Install django-model-changes::
1313

14-
pip install django-model-changes
14+
uv add django-model-changes
1515

16-
2. Add "django_model_changes" to your INSTALLED_APPS setting like this::
16+
Or with pip::
1717

18-
INSTALLED_APPS = (
19-
...
20-
'django_model_changes',
21-
)
18+
pip install django-model-changes
2219

23-
3. Add the `ChangesMixin` to your model::
20+
2. Add the ``ChangesMixin`` to your model::
2421

2522
>>> from django.db import models
2623
>>> from django_model_changes import ChangesMixin
2724

2825
>>> class User(ChangesMixin, models.Model):
2926
>>> name = models.CharField(max_length=100)
3027

31-
4. Get instance changes::
28+
3. Get instance changes::
3229

3330
>>> user = User()
3431
>>> user.name = 'Foo Bar'
3532
>>> user.save()
3633

37-
>>> user.name 'I got a new name'
34+
>>> user.name = 'I got a new name'
3835

3936
>>> # Get current state
4037
>>> user.current_state()
@@ -64,10 +61,10 @@ Quick start
6461
>>> user.is_persisted()
6562
True
6663

67-
5. Listen for changes::
68-
64+
4. Listen for changes::
65+
6966
>>> from django_model_changes import post_change
70-
67+
7168
>>> def my_callback(sender, instance, **kwargs):
7269
>>> # Do something with previous and current state
7370
>>> instance.old_state()

django_model_changes/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
from .changes import ChangesMixin
22
from .signals import post_change
3+
from ._version import __version__
4+
5+
__all__ = ["ChangesMixin", "post_change", "__version__"]

django_model_changes/_version.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Version information for django-model-changes.
2+
3+
This file is automatically updated by `djb publish`.
4+
"""
5+
6+
__version__ = "1.0.0"

django_model_changes/changes.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from django.db import models
16
from django.db.models import signals
27

38
from .signals import post_change
49

10+
if TYPE_CHECKING:
11+
from django.db.models.options import Options
12+
513
SAVE = 0
614
DELETE = 1
715

816

9-
class ChangesMixin(object):
10-
"""
17+
class ChangesMixin(models.Model):
18+
r"""
1119
ChangesMixin keeps track of changes for model instances.
1220
1321
It allows you to retrieve the following states from an instance:
@@ -61,22 +69,29 @@ class ChangesMixin(object):
6169
6270
"""
6371

64-
def __init__(self, *args, **kwargs):
65-
super(ChangesMixin, self).__init__(*args, **kwargs)
72+
_states: list[dict[str, Any]]
73+
74+
class Meta:
75+
abstract = True
76+
77+
def __init__(self, *args: Any, **kwargs: Any) -> None:
78+
super().__init__(*args, **kwargs)
6679

6780
self._states = []
6881
self._save_state(new_instance=True)
6982

7083
signals.post_save.connect(
71-
_post_save, sender=self.__class__,
72-
dispatch_uid='django-changes-%s' % self.__class__.__name__
84+
_post_save,
85+
sender=self.__class__,
86+
dispatch_uid=f"django-changes-{self.__class__.__name__}",
7387
)
7488
signals.post_delete.connect(
75-
_post_delete, sender=self.__class__,
76-
dispatch_uid='django-changes-%s' % self.__class__.__name__
89+
_post_delete,
90+
sender=self.__class__,
91+
dispatch_uid=f"django-changes-{self.__class__.__name__}",
7792
)
7893

79-
def _save_state(self, new_instance=False, event_type='save'):
94+
def _save_state(self, new_instance: bool = False, event_type: str | int = "save") -> None:
8095
# Pipe the pk on deletes so that a correct snapshot of the current
8196
# state can be taken.
8297
if event_type == DELETE:
@@ -95,58 +110,60 @@ def _save_state(self, new_instance=False, event_type='save'):
95110
if not new_instance:
96111
post_change.send(sender=self.__class__, instance=self)
97112

98-
def current_state(self):
113+
def current_state(self) -> dict[str, Any]:
99114
"""
100115
Returns a ``field -> value`` dict of the current state of the instance.
101116
"""
102-
field_names = set()
103-
[field_names.add(f.name) for f in self._meta.local_fields]
104-
[field_names.add(f.attname) for f in self._meta.local_fields]
105-
return dict([(field_name, getattr(self, field_name)) for field_name in field_names])
117+
field_names: set[str] = set()
118+
for f in self._meta.local_fields:
119+
field_names.add(f.name)
120+
field_names.add(f.attname)
121+
return {field_name: getattr(self, field_name) for field_name in field_names}
106122

107-
def previous_state(self):
123+
def previous_state(self) -> dict[str, Any]:
108124
"""
109125
Returns a ``field -> value`` dict of the state of the instance after it
110126
was created, saved or deleted the previous time.
111127
"""
112128
if len(self._states) > 1:
113129
return self._states[1]
114-
else:
115-
return self._states[0]
130+
return self._states[0]
116131

117-
def old_state(self):
132+
def old_state(self) -> dict[str, Any]:
118133
"""
119134
Returns a ``field -> value`` dict of the state of the instance after
120135
it was created, saved or deleted the previous previous time. Returns
121136
the previous state if there is no previous previous state.
122137
"""
123138
return self._states[0]
124139

125-
def _changes(self, other, current):
126-
return dict([(key, (was, current[key])) for key, was in other.iteritems() if was != current[key]])
140+
def _changes(
141+
self, other: dict[str, Any], current: dict[str, Any]
142+
) -> dict[str, tuple[Any, Any]]:
143+
return {key: (was, current[key]) for key, was in other.items() if was != current[key]}
127144

128-
def changes(self):
145+
def changes(self) -> dict[str, tuple[Any, Any]]:
129146
"""
130147
Returns a ``field -> (previous value, current value)`` dict of changes
131148
from the previous state to the current state.
132149
"""
133150
return self._changes(self.previous_state(), self.current_state())
134151

135-
def old_changes(self):
152+
def old_changes(self) -> dict[str, tuple[Any, Any]]:
136153
"""
137154
Returns a ``field -> (previous value, current value)`` dict of changes
138155
from the old state to the current state.
139156
"""
140157
return self._changes(self.old_state(), self.current_state())
141158

142-
def previous_changes(self):
159+
def previous_changes(self) -> dict[str, tuple[Any, Any]]:
143160
"""
144161
Returns a ``field -> (previous value, current value)`` dict of changes
145162
from the old state to the previous state.
146163
"""
147164
return self._changes(self.old_state(), self.previous_state())
148165

149-
def was_persisted(self):
166+
def was_persisted(self) -> bool:
150167
"""
151168
Returns true if the instance was persisted (saved) in its old
152169
state.
@@ -163,10 +180,12 @@ def was_persisted(self):
163180
>>> user.was_persisted()
164181
True
165182
"""
166-
pk_name = self._meta.pk.name
167-
return bool(self.old_state()[pk_name])
183+
pk_field = self._meta.pk
184+
if pk_field is None:
185+
return False
186+
return bool(self.old_state()[pk_field.name])
168187

169-
def is_persisted(self):
188+
def is_persisted(self) -> bool:
170189
"""
171190
Returns true if the instance is persisted (saved) in its current
172191
state.
@@ -185,22 +204,22 @@ def is_persisted(self):
185204
"""
186205
return bool(self.pk)
187206

188-
def old_instance(self):
207+
def old_instance(self) -> ChangesMixin:
189208
"""
190209
Returns an instance of this model in its old state.
191210
"""
192211
return self.__class__(**self.old_state())
193212

194-
def previous_instance(self):
213+
def previous_instance(self) -> ChangesMixin:
195214
"""
196215
Returns an instance of this model in its previous state.
197216
"""
198217
return self.__class__(**self.previous_state())
199218

200219

201-
def _post_save(sender, instance, **kwargs):
220+
def _post_save(sender: type[models.Model], instance: ChangesMixin, **kwargs: Any) -> None:
202221
instance._save_state(new_instance=False, event_type=SAVE)
203222

204223

205-
def _post_delete(sender, instance, **kwargs):
224+
def _post_delete(sender: type[models.Model], instance: ChangesMixin, **kwargs: Any) -> None:
206225
instance._save_state(new_instance=False, event_type=DELETE)

0 commit comments

Comments
 (0)