diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 065a166..ce73854 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77218be..620a617 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/CHANGES.rst b/CHANGES.rst index e91541b..e303104 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) ------------------ diff --git a/model_utils/managers.py b/model_utils/managers.py index 1cd21cf..e2f7e04 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -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). diff --git a/requirements-mypy.txt b/requirements-mypy.txt index ed46f7a..6fecd15 100644 --- a/requirements-mypy.txt +++ b/requirements-mypy.txt @@ -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 diff --git a/setup.py b/setup.py index 5571148..a898571 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tests/models.py b/tests/models.py index 8c6b046..bb3c02f 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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): diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index b823eb6..1317c23 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -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) diff --git a/tests/test_managers/test_inheritance_manager.py b/tests/test_managers/test_inheritance_manager.py index 68e8a74..b82edad 100644 --- a/tests/test_managers/test_inheritance_manager.py +++ b/tests/test_managers/test_inheritance_manager.py @@ -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( diff --git a/tests/test_models/test_softdeletable_model.py b/tests/test_models/test_softdeletable_model.py index 8f4e656..c2d6fe7 100644 --- a/tests/test_models/test_softdeletable_model.py +++ b/tests/test_models/test_softdeletable_model.py @@ -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} diff --git a/tox.ini b/tox.ini index 2dd9492..d343b29 100644 --- a/tox.ini +++ b/tox.ini @@ -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