From dab26d0e0eea3d60da08ab8d536e12d96165ec27 Mon Sep 17 00:00:00 2001 From: Aleksander Vognild Burkow Date: Mon, 13 Jun 2022 17:49:59 +0200 Subject: [PATCH] Add fallback related manager in final iteration of AddRelatedManagers If a django model has a Manager class that cannot be resolved statically (if it is generated in a way where we cannot import it, like `objects = my_manager_factory()`), we fallback to the default related manager, so you at least get a base level of working type checking. --- mypy_django_plugin/errorcodes.py | 1 + mypy_django_plugin/transformers/models.py | 22 ++++++++++++++ tests/typecheck/fields/test_related.yml | 36 +++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/mypy_django_plugin/errorcodes.py b/mypy_django_plugin/errorcodes.py index 475144a34..df77dfe45 100644 --- a/mypy_django_plugin/errorcodes.py +++ b/mypy_django_plugin/errorcodes.py @@ -1,3 +1,4 @@ from mypy.errorcodes import ErrorCode MANAGER_UNTYPED = ErrorCode("django-manager", "Untyped manager disallowed", "Django") +MANAGER_MISSING = ErrorCode("django-manager-missing", "Couldn't resolve related manager for model", "Django") diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index a6e405265..e0b3cef80 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -14,6 +14,7 @@ from mypy.types import TypedDictType, TypeOfAny from mypy_django_plugin.django.context import DjangoContext +from mypy_django_plugin.errorcodes import MANAGER_MISSING from mypy_django_plugin.lib import fullnames, helpers from mypy_django_plugin.lib.fullnames import ANNOTATIONS_FULLNAME, ANY_ATTR_ALLOWED_CLASS_FULLNAME, MODEL_CLASS_FULLNAME from mypy_django_plugin.lib.helpers import add_new_class_for_module @@ -341,6 +342,7 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: continue if isinstance(relation, (ManyToOneRel, ManyToManyRel)): + related_manager_info = None try: related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error( fullnames.RELATED_MANAGER_CLASS @@ -352,6 +354,26 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: if not self.api.final_iteration: raise exc else: + if related_manager_info: + """ + If a django model has a Manager class that cannot be + resolved statically (if it is generated in a way + where we cannot import it, like `objects = my_manager_factory()`), + we fallback to the default related manager, so you + at least get a base level of working type checking. + + See https://github.com/typeddjango/django-stubs/pull/993 + for more information on when this error can occur. + """ + self.add_new_node_to_model_class( + attname, Instance(related_manager_info, [Instance(related_model_info, [])]) + ) + related_model_fullname = related_model_cls.__module__ + "." + related_model_cls.__name__ + self.ctx.api.fail( + f"Couldn't resolve related manager for relation {relation.name!r} (from {related_model_fullname}.{relation.field}).", + self.ctx.cls, + code=MANAGER_MISSING, + ) continue # Check if the related model has a related manager subclassed from the default manager diff --git a/tests/typecheck/fields/test_related.yml b/tests/typecheck/fields/test_related.yml index 5f5cac65f..675ade94f 100644 --- a/tests/typecheck/fields/test_related.yml +++ b/tests/typecheck/fields/test_related.yml @@ -640,6 +640,42 @@ pass +- case: test_related_managers_when_manager_is_dynamically_generated_and_cannot_be_imported + main: | + from myapp import models + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + + class User(models.Model): + name = models.TextField() + + def DynamicManager() -> models.Manager: + class InnerManager(models.Manager): + def some_method(self, arg: str) -> None: + return None + + return InnerManager() + + class Booking(models.Model): + renter = models.ForeignKey(User, on_delete=models.PROTECT) + owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='bookingowner_set') + + objects = DynamicManager() + + def process_booking(user: User): + reveal_type(user.bookingowner_set) + reveal_type(user.booking_set) + out: | + myapp/models:3: error: Couldn't resolve related manager for relation 'booking' (from myapp.models.Booking.myapp.Booking.renter). + myapp/models:3: error: Couldn't resolve related manager for relation 'bookingowner_set' (from myapp.models.Booking.myapp.Booking.owner). + myapp/models:20: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]" + myapp/models:21: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]" + - case: foreign_key_relationship_for_models_with_custom_manager main: | from myapp.models import Transaction