Skip to content

Commit 023106f

Browse files
authored
Emit error and set fallback type for managers that can't be resolved (#999)
* Emit error and set fallback type for managers that can't be resolved * fixup! Emit error and set fallback type for managers that can't be resolved
1 parent 719cd3a commit 023106f

File tree

4 files changed

+119
-48
lines changed

4 files changed

+119
-48
lines changed

mypy_django_plugin/errorcodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from mypy.errorcodes import ErrorCode
22

33
MANAGER_UNTYPED = ErrorCode("django-manager", "Untyped manager disallowed", "Django")
4-
MANAGER_MISSING = ErrorCode("django-manager-missing", "Couldn't resolve related manager for model", "Django")
4+
MANAGER_MISSING = ErrorCode("django-manager-missing", "Couldn't resolve manager for model", "Django")

mypy_django_plugin/transformers/models.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.db.models.fields.related import ForeignKey
66
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
77
from mypy.checker import TypeChecker
8-
from mypy.nodes import ARG_STAR2, Argument, Context, FuncDef, TypeInfo, Var
8+
from mypy.nodes import ARG_STAR2, Argument, AssignmentStmt, Context, FuncDef, NameExpr, TypeInfo, Var
99
from mypy.plugin import AnalyzeTypeContext, AttributeContext, CheckerPluginInterface, ClassDefContext
1010
from mypy.plugins import common
1111
from mypy.semanal import SemanticAnalyzer
@@ -234,7 +234,7 @@ def create_new_model_parametrized_manager(self, name: str, base_manager_info: Ty
234234
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
235235
manager_info: Optional[TypeInfo]
236236

237-
encountered_incomplete_manager_def = False
237+
incomplete_manager_defs = set()
238238
for manager_name, manager in model_cls._meta.managers_map.items():
239239
manager_class_name = manager.__class__.__name__
240240
manager_fullname = helpers.get_class_fullname(manager.__class__)
@@ -243,13 +243,11 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
243243
except helpers.IncompleteDefnException as exc:
244244
# Check if manager is a generated (dynamic class) manager
245245
base_manager_fullname = helpers.get_class_fullname(manager.__class__.__bases__[0])
246-
manager_info = self.get_generated_manager_info(manager_fullname, base_manager_fullname)
247-
if manager_info is None:
246+
if manager_fullname not in self.get_generated_manager_mappings(base_manager_fullname):
248247
# Manager doesn't appear to be generated. Track that we encountered an
249248
# incomplete definition and skip
250-
encountered_incomplete_manager_def = True
251-
continue
252-
_, manager_class_name = manager_info.fullname.rsplit(".", maxsplit=1)
249+
incomplete_manager_defs.add(manager_name)
250+
continue
253251

254252
if manager_name not in self.model_classdef.info.names:
255253
manager_type = Instance(manager_info, [Instance(self.model_classdef.info, [])])
@@ -275,9 +273,38 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
275273

276274
self.add_new_node_to_model_class(manager_name, custom_manager_type)
277275

278-
if encountered_incomplete_manager_def and not self.api.final_iteration:
279-
# Unless we're on the final round, see if another round could figuring out all manager types
276+
if incomplete_manager_defs and not self.api.final_iteration:
277+
# Unless we're on the final round, see if another round could figure out all manager types
280278
raise helpers.IncompleteDefnException()
279+
elif self.api.final_iteration:
280+
for manager_name in incomplete_manager_defs:
281+
# We act graceful and set the type as the bare minimum we know of
282+
# (Django's default) before finishing. And emit an error, to allow for
283+
# ignoring a more specialised manager not being resolved while still
284+
# setting _some_ type
285+
django_manager_info = self.lookup_typeinfo(fullnames.MANAGER_CLASS_FULLNAME)
286+
assert (
287+
django_manager_info is not None
288+
), f"Type info for Django's {fullnames.MANAGER_CLASS_FULLNAME} missing"
289+
self.add_new_node_to_model_class(
290+
manager_name, Instance(django_manager_info, [Instance(self.model_classdef.info, [])])
291+
)
292+
# Find expression for e.g. `objects = SomeManager()`
293+
manager_expr = [
294+
expr
295+
for expr in self.ctx.cls.defs.body
296+
if (
297+
isinstance(expr, AssignmentStmt)
298+
and isinstance(expr.lvalues[0], NameExpr)
299+
and expr.lvalues[0].name == manager_name
300+
)
301+
]
302+
manager_fullname = f"{self.model_classdef.fullname}.{manager_name}"
303+
self.api.fail(
304+
f'Could not resolve manager type for "{manager_fullname}"',
305+
manager_expr[0] if manager_expr else self.ctx.cls,
306+
code=MANAGER_MISSING,
307+
)
281308

282309

283310
class AddDefaultManagerAttribute(ModelClassInitializer):

tests/typecheck/fields/test_related.yml

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -639,43 +639,6 @@
639639
class Article(LibraryEntity):
640640
pass
641641
642-
643-
- case: test_related_managers_when_manager_is_dynamically_generated_and_cannot_be_imported
644-
main: |
645-
from myapp import models
646-
installed_apps:
647-
- myapp
648-
files:
649-
- path: myapp/__init__.py
650-
- path: myapp/models.py
651-
content: |
652-
from django.db import models
653-
654-
class User(models.Model):
655-
name = models.TextField()
656-
657-
def DynamicManager() -> models.Manager:
658-
class InnerManager(models.Manager):
659-
def some_method(self, arg: str) -> None:
660-
return None
661-
662-
return InnerManager()
663-
664-
class Booking(models.Model):
665-
renter = models.ForeignKey(User, on_delete=models.PROTECT)
666-
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='bookingowner_set')
667-
668-
objects = DynamicManager()
669-
670-
def process_booking(user: User):
671-
reveal_type(user.bookingowner_set)
672-
reveal_type(user.booking_set)
673-
out: |
674-
myapp/models:3: error: Couldn't resolve related manager for relation 'booking' (from myapp.models.Booking.myapp.Booking.renter).
675-
myapp/models:3: error: Couldn't resolve related manager for relation 'bookingowner_set' (from myapp.models.Booking.myapp.Booking.owner).
676-
myapp/models:20: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]"
677-
myapp/models:21: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]"
678-
679642
- case: foreign_key_relationship_for_models_with_custom_manager
680643
main: |
681644
from myapp.models import Transaction
@@ -686,10 +649,12 @@
686649
- path: myapp/models.py
687650
content: |
688651
from django.db import models
652+
from django.db.models.manager import BaseManager
689653
class TransactionQuerySet(models.QuerySet):
690654
pass
655+
TransactionManager = BaseManager.from_queryset(TransactionQuerySet)
691656
class Transaction(models.Model):
692-
objects = TransactionQuerySet.as_manager()
657+
objects = TransactionManager()
693658
def test(self) -> None:
694659
self.transactionlog_set
695660
class TransactionLog(models.Model):

tests/typecheck/managers/test_managers.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,82 @@
442442
class MyModel(models.Model):
443443
site = models.ForeignKey(Site, on_delete=models.CASCADE)
444444
on_site = CurrentSiteManager()
445+
446+
- case: test_emits_error_for_unresolved_managers
447+
main: |
448+
from myapp import models
449+
installed_apps:
450+
- myapp
451+
files:
452+
- path: myapp/__init__.py
453+
- path: myapp/models.py
454+
content: |
455+
from django.db import models
456+
457+
def LocalManager() -> models.Manager:
458+
"""
459+
Returns a manager instance of an inlined manager type that can't
460+
be resolved.
461+
"""
462+
class InnerManager(models.Manager):
463+
...
464+
465+
return InnerManager()
466+
467+
class User(models.Model):
468+
name = models.TextField()
469+
470+
class Booking(models.Model):
471+
renter = models.ForeignKey(User, on_delete=models.PROTECT)
472+
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='bookingowner_set')
473+
474+
objects = LocalManager()
475+
476+
class TwoUnresolvable(models.Model):
477+
objects = LocalManager()
478+
second_objects = LocalManager()
479+
480+
class AbstractUnresolvable(models.Model):
481+
objects = LocalManager()
482+
483+
class Meta:
484+
abstract = True
485+
486+
class InvisibleUnresolvable(AbstractUnresolvable):
487+
text = models.TextField()
488+
489+
def process_booking(user: User):
490+
reveal_type(User.objects)
491+
reveal_type(User._default_manager)
492+
493+
reveal_type(Booking.objects)
494+
reveal_type(Booking._default_manager)
495+
496+
reveal_type(TwoUnresolvable.objects)
497+
reveal_type(TwoUnresolvable.second_objects)
498+
reveal_type(TwoUnresolvable._default_manager)
499+
500+
reveal_type(InvisibleUnresolvable.objects)
501+
reveal_type(InvisibleUnresolvable._default_manager)
502+
503+
reveal_type(user.bookingowner_set)
504+
reveal_type(user.booking_set)
505+
out: |
506+
myapp/models:13: error: Couldn't resolve related manager for relation 'booking' (from myapp.models.Booking.myapp.Booking.renter).
507+
myapp/models:13: error: Couldn't resolve related manager for relation 'bookingowner_set' (from myapp.models.Booking.myapp.Booking.owner).
508+
myapp/models:20: error: Could not resolve manager type for "myapp.models.Booking.objects"
509+
myapp/models:23: error: Could not resolve manager type for "myapp.models.TwoUnresolvable.objects"
510+
myapp/models:24: error: Could not resolve manager type for "myapp.models.TwoUnresolvable.second_objects"
511+
myapp/models:27: error: Could not resolve manager type for "myapp.models.AbstractUnresolvable.objects"
512+
myapp/models:32: error: Could not resolve manager type for "myapp.models.InvisibleUnresolvable.objects"
513+
myapp/models:36: note: Revealed type is "django.db.models.manager.Manager[myapp.models.User]"
514+
myapp/models:37: note: Revealed type is "django.db.models.manager.Manager[myapp.models.User]"
515+
myapp/models:39: note: Revealed type is "django.db.models.manager.Manager[myapp.models.Booking]"
516+
myapp/models:40: note: Revealed type is "django.db.models.manager.BaseManager[myapp.models.Booking]"
517+
myapp/models:42: note: Revealed type is "django.db.models.manager.Manager[myapp.models.TwoUnresolvable]"
518+
myapp/models:43: note: Revealed type is "django.db.models.manager.Manager[myapp.models.TwoUnresolvable]"
519+
myapp/models:44: note: Revealed type is "django.db.models.manager.BaseManager[myapp.models.TwoUnresolvable]"
520+
myapp/models:46: note: Revealed type is "django.db.models.manager.Manager[myapp.models.InvisibleUnresolvable]"
521+
myapp/models:47: note: Revealed type is "django.db.models.manager.BaseManager[myapp.models.InvisibleUnresolvable]"
522+
myapp/models:49: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]"
523+
myapp/models:50: note: Revealed type is "django.db.models.manager.RelatedManager[myapp.models.Booking]"

0 commit comments

Comments
 (0)