Merge branch 'master' into Fix-django-modeltranslation-compatibility

This commit is contained in:
Benedikt Willi 2026-01-08 10:28:08 +01:00 committed by GitHub
commit 421014da19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 41 additions and 44 deletions

View file

@ -16,17 +16,16 @@ jobs:
max-parallel: 5
matrix:
python-version: [
'3.8',
'3.9',
'3.10',
'3.11',
'3.12',
'3.13',
'3.14',
]
services:
postgres:
image: postgres:14-alpine
image: postgres:15-alpine
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}

View file

@ -1,20 +1,20 @@
repos:
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
rev: 7.0.0
hooks:
- id: isort
args: ['--profile', 'black', '--check-only', '--diff']
files: ^((model_utils|tests)/)|setup.py
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
rev: 7.3.0
hooks:
- id: flake8
args: ['--ignore=E402,E501,E731,W503']
files: ^(model_utils|tests)/
- repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py38-plus]

View file

@ -4,11 +4,13 @@ Changelog
To be released
------------------
- Add support for `Python 3.13` (GH-#628)
- Add support for `Python 3.14`
- Add formal support for `Django 5.2` (GH-#641)
- Drop support for older versions than `Django 4.2`
- Fix compatibility with django-modeltranslation: manager mixins no longer
inherit from `Generic[T]` at runtime, preventing `TypeError` on `__class__`
assignment (GH-#636)
- Drop support for `Python 3.8` and `Python 3.9`
5.0.0 (2024-09-01)
------------------

View file

@ -209,8 +209,7 @@ class InheritanceQuerySetMixin(_GenericMixin):
# Defining the 'model' attribute using a generic type triggers a bug in mypy:
# https://github.com/python/mypy/issues/9031
class InheritanceQuerySet(InheritanceQuerySetMixin[ModelT], QuerySet[ModelT]): # type: ignore[misc]
class InheritanceQuerySet(InheritanceQuerySetMixin[ModelT], QuerySet[ModelT]):
def instance_of(self, *models: type[ModelT]) -> InheritanceQuerySet[ModelT]:
"""
Fetch only objects that are instances of the provided model(s).

View file

@ -1,3 +1,4 @@
mypy==1.10.0
django-stubs==5.0.2
mypy>=1.13.0
django-stubs==5.1.3
pytest
time-machine>=2.8.2

View file

@ -29,8 +29,8 @@ setup(
maintainer='JazzBand',
url='https://github.com/jazzband/django-model-utils',
packages=find_packages(exclude=['tests*']),
python_requires=">=3.8",
install_requires=['Django>=3.2'],
python_requires=">=3.10",
install_requires=['Django>=4.2'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
@ -39,12 +39,11 @@ setup(
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Framework :: Django',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',

View file

@ -110,7 +110,7 @@ class TimeFrameManagerAdded(TimeFramedModel):
class Monitored(models.Model):
name = models.CharField(max_length=25)
name_changed = MonitorField(monitor="name")
name_changed_nullable = MonitorField(monitor="name", null=True)
name_changed_nullable = MonitorField(monitor="name", null=True) # type: ignore[misc]
class MonitorWhen(models.Model):

View file

@ -544,7 +544,7 @@ class FieldTrackerForeignKeyMixin(FieldTrackerMixin):
self.assertChanged()
self.assertPrevious()
self.assertCurrent(id=self.instance.id, fk_id=self.old_fk.id)
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
self.instance.fk = self.fk_class.objects.create(number=8)
self.assertChanged(fk_id=self.old_fk.id)
self.assertPrevious(fk_id=self.old_fk.id)
self.assertCurrent(id=self.instance.id, fk_id=self.instance.fk_id)
@ -554,7 +554,7 @@ class FieldTrackerForeignKeyMixin(FieldTrackerMixin):
self.assertChanged()
self.assertPrevious()
self.assertCurrent(fk_id=self.old_fk.id)
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
self.instance.fk = self.fk_class.objects.create(number=8)
self.assertChanged(fk_id=self.old_fk.id)
self.assertPrevious(fk_id=self.old_fk.id)
self.assertCurrent(fk_id=self.instance.fk_id)
@ -566,7 +566,7 @@ class FieldTrackerForeignKeyMixin(FieldTrackerMixin):
self.assertChanged()
self.assertPrevious()
self.assertCurrent(fk=self.old_fk.id)
self.instance.fk = self.fk_class.objects.create(number=8) # type: ignore[assignment]
self.instance.fk = self.fk_class.objects.create(number=8)
self.assertChanged(fk=self.old_fk.id)
self.assertPrevious(fk=self.old_fk.id)
self.assertCurrent(fk=self.instance.fk_id)

View file

@ -511,7 +511,7 @@ class InheritanceManagerRelatedTests(InheritanceManagerTests):
self.child1)
def test_get_method_with_select_subclasses_check_for_useless_join(self) -> None:
child4 = InheritanceManagerTestChild4.objects.create(related=self.related, other_onetoone=self.child1)
child4 = InheritanceManagerTestChild4.objects.create(related=self.related, other_onetoone=self.child1) # type: ignore[misc]
self.assertEqual(
str(InheritanceManagerTestChild4.objects.select_subclasses().filter(
id=child4.id).query),
@ -521,7 +521,7 @@ class InheritanceManagerRelatedTests(InheritanceManagerTests):
def test_annotate_with_select_subclasses(self) -> None:
qs = InheritanceManagerTestParent.objects.select_subclasses().annotate(
models.Count('id'))
self.assertEqual(qs.get(id=self.child1.id).id__count, 1)
self.assertEqual(qs.get(id=self.child1.id).id__count, 1) # type: ignore[attr-defined]
def test_annotate_with_named_arguments_with_select_subclasses(self) -> None:
qs = InheritanceManagerTestParent.objects.select_subclasses().annotate(
@ -531,7 +531,7 @@ class InheritanceManagerRelatedTests(InheritanceManagerTests):
def test_annotate_before_select_subclasses(self) -> None:
qs = InheritanceManagerTestParent.objects.annotate(
models.Count('id')).select_subclasses()
self.assertEqual(qs.get(id=self.child1.id).id__count, 1)
self.assertEqual(qs.get(id=self.child1.id).id__count, 1) # type: ignore[attr-defined]
def test_annotate_with_named_arguments_before_select_subclasses(self) -> None:
qs = InheritanceManagerTestParent.objects.annotate(

View file

@ -8,16 +8,16 @@ from tests.models import SoftDeletable
class SoftDeletableModelTests(TestCase):
def test_can_only_see_not_removed_entries(self) -> None:
SoftDeletable.available_objects.create(name='a', is_removed=True)
SoftDeletable.available_objects.create(name='b', is_removed=False)
SoftDeletable.available_objects.create(name='a', is_removed=True) # type: ignore[misc]
SoftDeletable.available_objects.create(name='b', is_removed=False) # type: ignore[misc]
queryset = SoftDeletable.available_objects.all()
self.assertEqual(queryset.count(), 1)
self.assertEqual(queryset[0].name, 'b')
self.assertEqual(queryset[0].name, 'b') # type: ignore[attr-defined]
def test_instance_cannot_be_fully_deleted(self) -> None:
instance = SoftDeletable.available_objects.create(name='a')
instance = SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
instance.delete()
@ -25,7 +25,7 @@ class SoftDeletableModelTests(TestCase):
self.assertEqual(SoftDeletable.all_objects.count(), 1)
def test_instance_cannot_be_fully_deleted_via_queryset(self) -> None:
SoftDeletable.available_objects.create(name='a')
SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
SoftDeletable.available_objects.all().delete()
@ -33,12 +33,12 @@ class SoftDeletableModelTests(TestCase):
self.assertEqual(SoftDeletable.all_objects.count(), 1)
def test_delete_instance_no_connection(self) -> None:
obj = SoftDeletable.available_objects.create(name='a')
obj = SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
self.assertRaises(ConnectionDoesNotExist, obj.delete, using='other')
def test_instance_purge(self) -> None:
instance = SoftDeletable.available_objects.create(name='a')
instance = SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
instance.delete(soft=False)
@ -46,7 +46,7 @@ class SoftDeletableModelTests(TestCase):
self.assertEqual(SoftDeletable.all_objects.count(), 0)
def test_instance_purge_no_connection(self) -> None:
instance = SoftDeletable.available_objects.create(name='a')
instance = SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
self.assertRaises(ConnectionDoesNotExist, instance.delete,
using='other', soft=False)
@ -55,10 +55,10 @@ class SoftDeletableModelTests(TestCase):
self.assertWarns(DeprecationWarning, SoftDeletable.objects.all)
def test_delete_queryset_return(self) -> None:
SoftDeletable.available_objects.create(name='a')
SoftDeletable.available_objects.create(name='b')
SoftDeletable.available_objects.create(name='a') # type: ignore[misc]
SoftDeletable.available_objects.create(name='b') # type: ignore[misc]
result = SoftDeletable.available_objects.filter(name="a").delete()
result = SoftDeletable.available_objects.filter(name="a").delete() # type: ignore[misc]
assert result == (
1, {SoftDeletable._meta.label: 1}

21
tox.ini
View file

@ -1,23 +1,21 @@
[tox]
envlist =
py{38,39,310,311}-dj{42}
py{310,311,312}-dj{50}
py{310,311,312}-dj{51}
py{310,311}-dj{42}
py{310,311,312}-dj{50,51}
py{310,311,312,313}-dj{51,52}
py{310,311,312,313}-dj{main}
py{314}-dj{52}
py{312,313,314}-dj{main}
flake8
isort
mypy
[gh-actions]
python =
3.7: py37
3.8: py38, flake8, isort, mypy
3.9: py39
3.10: py310
3.10: py310, flake8, isort, mypy
3.11: py311
3.12: py312
3.13: py313
3.14: py314
[testenv]
deps =
@ -43,7 +41,7 @@ commands =
[testenv:flake8]
basepython =
python3.8
python3.10
deps =
flake8
skip_install = True
@ -58,16 +56,15 @@ ignore =
E501
[testenv:isort]
basepython = python3.8
basepython = python3.10
deps = isort
commands =
isort model_utils tests setup.py --check-only --diff
skip_install = True
[testenv:mypy]
basepython = python3.8
basepython = python3.10
deps =
time-machine==2.8.2
-r requirements-mypy.txt
set_env =
SQLITE=1