From 07653f84f30c46c438f78b8e735b648854cacf09 Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Mon, 25 Mar 2024 16:29:55 +0100 Subject: [PATCH 1/6] Add dependencies for static type checking --- requirements-mypy.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements-mypy.txt diff --git a/requirements-mypy.txt b/requirements-mypy.txt new file mode 100644 index 00000000..c0422a75 --- /dev/null +++ b/requirements-mypy.txt @@ -0,0 +1,2 @@ +mypy==1.9.0 +django-stubs==4.2.7 From f3335a7fe18e03ef7b63698caf2c6debfdc4eff2 Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Thu, 16 Mar 2023 18:24:33 +0100 Subject: [PATCH 2/6] Add configuration for mypy This does not require annotations yet, but it will check all code outside of functions. --- mypy.ini | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..918bc713 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,16 @@ +[mypy] +implicit_reexport=False +pretty=True +show_error_codes=True +strict_equality=True +warn_redundant_casts=True +warn_unreachable=True +warn_unused_ignores=True + +mypy_path = $MYPY_CONFIG_FILE_DIR + +plugins = + mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = "tests.settings" From 62cecfeb250db68d6da1324b9cd0c4f6ce3550a7 Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Tue, 21 Mar 2023 14:26:11 +0100 Subject: [PATCH 3/6] Suppress error about `__version__` being `None` when not installed Suppress rather than annotate, because people type checking against `django-model-utils` will always have it installed and therefore should not have to deal with `__version__` being `None`. --- model_utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model_utils/__init__.py b/model_utils/__init__.py index cf1b22c5..14b50cc0 100644 --- a/model_utils/__init__.py +++ b/model_utils/__init__.py @@ -7,6 +7,6 @@ __version__ = importlib.metadata.version('django-model-utils') except importlib.metadata.PackageNotFoundError: # pragma: no cover # package is not installed - __version__ = None + __version__ = None # type: ignore[assignment] __all__ = ("Choices", "FieldTracker", "ModelTracker") From 441e3adee55b32ad398a57e31f81099e5febdb21 Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Mon, 25 Mar 2024 18:06:07 +0100 Subject: [PATCH 4/6] Make default port a string Django can handle both strings and integers, but typeshed expects the default value to match the mapping's value type. --- tests/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/settings.py b/tests/settings.py index c4de9219..4593b4a5 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -19,7 +19,7 @@ "USER": os.environ.get("POSTGRES_USER", 'postgres'), "PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), "HOST": os.environ.get("POSTGRES_HOST", "localhost"), - "PORT": os.environ.get("POSTGRES_PORT", 5432) + "PORT": os.environ.get("POSTGRES_PORT", "5432") }, } SECRET_KEY = 'dummy' From 2f58a1160d574500bacb1221c44289955dead7cd Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Mon, 25 Mar 2024 17:30:48 +0100 Subject: [PATCH 5/6] Add minimal type annotations to make mypy pass Avoid using `Self` as a type argument: for some reason this fails when mypy has an empty cache, but passes when the cache has been filled. Maybe it's a weird interaction between the mypy core and the django-stubs plugin? --- tests/models.py | 22 +++++++++++++--------- tests/test_fields/test_field_tracker.py | 15 +++++++++------ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/models.py b/tests/models.py index 9164c102..44f75907 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import ClassVar + from django.db import models from django.db.models import Manager from django.db.models.query_utils import DeferredAttribute @@ -32,7 +36,7 @@ class InheritanceManagerTestParent(models.Model): related_self = models.OneToOneField( "self", related_name="imtests_self", null=True, on_delete=models.CASCADE) - objects = InheritanceManager() + objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager() def __str__(self): return "{}({})".format( @@ -44,7 +48,7 @@ def __str__(self): class InheritanceManagerTestChild1(InheritanceManagerTestParent): non_related_field_using_descriptor_2 = models.FileField(upload_to="test") normal_field_2 = models.TextField() - objects = InheritanceManager() + objects: ClassVar[InheritanceManager[InheritanceManagerTestParent]] = InheritanceManager() class InheritanceManagerTestGrandChild1(InheritanceManagerTestChild1): @@ -172,8 +176,8 @@ class Post(models.Model): order = models.IntegerField() objects = models.Manager() - public = QueryManager(published=True) - public_confirmed = QueryManager( + public: ClassVar[QueryManager[Post]] = QueryManager(published=True) + public_confirmed: ClassVar[QueryManager[Post]] = QueryManager( models.Q(published=True) & models.Q(confirmed=True)) public_reversed = QueryManager(published=True).order_by("-order") @@ -194,7 +198,7 @@ class Meta: class AbstractTracked(models.Model): - number = 1 + number: models.IntegerField class Meta: abstract = True @@ -340,13 +344,13 @@ class SoftDeletable(SoftDeletableModel): """ name = models.CharField(max_length=20) - all_objects = models.Manager() + all_objects: ClassVar[Manager[SoftDeletable]] = models.Manager() class CustomSoftDelete(SoftDeletableModel): is_read = models.BooleanField(default=False) - objects = CustomSoftDeleteManager() + objects: ClassVar[CustomSoftDeleteManager[SoftDeletableModel]] = CustomSoftDeleteManager() class StringyDescriptor: @@ -390,7 +394,7 @@ class ModelWithCustomDescriptor(models.Model): class BoxJoinModel(models.Model): name = models.CharField(max_length=32) - objects = JoinManager() + objects: ClassVar[JoinManager[BoxJoinModel]] = JoinManager() class JoinItemForeignKey(models.Model): @@ -400,7 +404,7 @@ class JoinItemForeignKey(models.Model): null=True, on_delete=models.CASCADE ) - objects = JoinManager() + objects: ClassVar[JoinManager[JoinItemForeignKey]] = JoinManager() class CustomUUIDModel(UUIDModel): diff --git a/tests/test_fields/test_field_tracker.py b/tests/test_fields/test_field_tracker.py index b7da2f94..42bd66b1 100644 --- a/tests/test_fields/test_field_tracker.py +++ b/tests/test_fields/test_field_tracker.py @@ -1,7 +1,10 @@ +from __future__ import annotations + from unittest import skip from django.core.cache import cache from django.core.exceptions import FieldError +from django.db import models from django.db.models.fields.files import FieldFile from django.test import TestCase @@ -73,7 +76,7 @@ def test_pre_save_previous(self): class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): - tracked_class = Tracked + tracked_class: type[models.Model] = Tracked def setUp(self): self.instance = self.tracked_class() @@ -280,7 +283,7 @@ def test_with_deferred_fields_access_multiple(self): class FieldTrackedModelCustomTests(FieldTrackerTestCase, FieldTrackerCommonTests): - tracked_class = TrackedNotDefault + tracked_class: type[models.Model] = TrackedNotDefault def setUp(self): self.instance = self.tracked_class() @@ -411,7 +414,7 @@ def test_current(self): class FieldTrackedModelMultiTests(FieldTrackerTestCase, FieldTrackerCommonTests): - tracked_class = TrackedMultiple + tracked_class: type[models.Model] = TrackedMultiple def setUp(self): self.instance = self.tracked_class() @@ -502,8 +505,8 @@ def test_current(self): class FieldTrackerForeignKeyTests(FieldTrackerTestCase): - fk_class = Tracked - tracked_class = TrackedFK + fk_class: type[models.Model] = Tracked + tracked_class: type[models.Model] = TrackedFK def setUp(self): self.old_fk = self.fk_class.objects.create(number=8) @@ -729,7 +732,7 @@ def test_current(self): class ModelTrackerTests(FieldTrackerTests): - tracked_class = ModelTracked + tracked_class: type[models.Model] = ModelTracked def test_cache_compatible(self): cache.set('key', self.instance) From c75e54a1b89e34d714dca2225cb88d0ef548b65d Mon Sep 17 00:00:00 2001 From: Maarten ter Huurne Date: Mon, 25 Mar 2024 16:30:13 +0100 Subject: [PATCH 6/6] Add mypy environment to tox configuration --- tox.ini | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fcf087ac..f77e732c 100644 --- a/tox.ini +++ b/tox.ini @@ -8,11 +8,12 @@ envlist = py{310,311,312}-dj{main} flake8 isort + mypy [gh-actions] python = 3.7: py37 - 3.8: py38, flake8, isort + 3.8: py38, flake8, isort, mypy 3.9: py39 3.10: py310 3.11: py311 @@ -63,3 +64,13 @@ deps = isort commands = isort model_utils tests setup.py --check-only --diff skip_install = True + +[testenv:mypy] +basepython = python3.8 +deps = + time-machine==2.8.2 + -r requirements-mypy.txt +set_env = + SQLITE=1 +commands = + mypy model_utils tests