Skip to content

Commit 6d07585

Browse files
authored
Support processing of other relations and fields when one is broken (#1877)
1 parent 0b8b124 commit 6d07585

File tree

2 files changed

+127
-110
lines changed

2 files changed

+127
-110
lines changed

mypy_django_plugin/transformers/models.py

Lines changed: 108 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from django.db.models import Manager, Model
66
from django.db.models.fields import DateField, DateTimeField, Field
7-
from django.db.models.fields.reverse_related import ManyToManyRel, OneToOneRel
7+
from django.db.models.fields.reverse_related import ForeignObjectRel, ManyToManyRel, OneToOneRel
88
from mypy.checker import TypeChecker
99
from mypy.nodes import (
1010
ARG_STAR2,
@@ -458,126 +458,124 @@ def reverse_one_to_one_descriptor(self) -> TypeInfo:
458458
def many_to_many_descriptor(self) -> TypeInfo:
459459
return self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MANY_TO_MANY_DESCRIPTOR)
460460

461-
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
462-
# add related managers
463-
for relation in self.django_context.get_model_relations(model_cls):
464-
attname = relation.get_accessor_name()
465-
if attname is None or attname in self.model_classdef.info.names:
466-
# No reverse accessor or already declared. Note that this would also leave any
467-
# explicitly declared(i.e. non-inferred) reverse accessors alone
468-
continue
461+
def process_relation(self, relation: ForeignObjectRel) -> None:
462+
attname = relation.get_accessor_name()
463+
if attname is None or attname in self.model_classdef.info.names:
464+
# No reverse accessor or already declared. Note that this would also leave any
465+
# explicitly declared(i.e. non-inferred) reverse accessors alone
466+
return
469467

470-
related_model_cls = self.django_context.get_field_related_model_cls(relation)
468+
related_model_cls = self.django_context.get_field_related_model_cls(relation)
469+
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
471470

472-
try:
473-
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
474-
except helpers.IncompleteDefnException as exc:
475-
if not self.api.final_iteration:
476-
raise exc
477-
else:
478-
continue
471+
if isinstance(relation, OneToOneRel):
472+
self.add_new_node_to_model_class(
473+
attname,
474+
Instance(
475+
self.reverse_one_to_one_descriptor,
476+
[Instance(self.model_classdef.info, []), Instance(related_model_info, [])],
477+
),
478+
)
479+
return
479480

480-
if isinstance(relation, OneToOneRel):
481-
self.add_new_node_to_model_class(
482-
attname,
483-
Instance(
484-
self.reverse_one_to_one_descriptor,
485-
[Instance(self.model_classdef.info, []), Instance(related_model_info, [])],
486-
),
487-
)
488-
continue
481+
elif isinstance(relation, ManyToManyRel):
482+
# TODO: 'relation' should be based on `TypeInfo` instead of Django runtime.
483+
to_fullname = helpers.get_class_fullname(relation.remote_field.model)
484+
to_model_info = self.lookup_typeinfo_or_incomplete_defn_error(to_fullname)
485+
assert relation.through is not None
486+
through_fullname = helpers.get_class_fullname(relation.through)
487+
through_model_info = self.lookup_typeinfo_or_incomplete_defn_error(through_fullname)
488+
self.add_new_node_to_model_class(
489+
attname,
490+
Instance(self.many_to_many_descriptor, [Instance(to_model_info, []), Instance(through_model_info, [])]),
491+
)
492+
return
489493

490-
elif isinstance(relation, ManyToManyRel):
491-
# TODO: 'relation' should be based on `TypeInfo` instead of Django runtime.
492-
to_fullname = helpers.get_class_fullname(relation.remote_field.model)
493-
to_model_info = self.lookup_typeinfo_or_incomplete_defn_error(to_fullname)
494-
assert relation.through is not None
495-
through_fullname = helpers.get_class_fullname(relation.through)
496-
through_model_info = self.lookup_typeinfo_or_incomplete_defn_error(through_fullname)
494+
related_manager_info = None
495+
try:
496+
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS)
497+
default_manager = related_model_info.names.get("_default_manager")
498+
if not default_manager:
499+
raise helpers.IncompleteDefnException()
500+
except helpers.IncompleteDefnException as exc:
501+
if not self.api.final_iteration:
502+
raise exc
503+
504+
# If a django model has a Manager class that cannot be
505+
# resolved statically (if it is generated in a way where we
506+
# cannot import it, like `objects = my_manager_factory()`),
507+
# we fallback to the default related manager, so you at
508+
# least get a base level of working type checking.
509+
#
510+
# See https://github.com/typeddjango/django-stubs/pull/993
511+
# for more information on when this error can occur.
512+
fallback_manager = self.get_or_create_manager_with_any_fallback(related_manager=True)
513+
if fallback_manager is not None:
497514
self.add_new_node_to_model_class(
498-
attname,
499-
Instance(
500-
self.many_to_many_descriptor, [Instance(to_model_info, []), Instance(through_model_info, [])]
501-
),
515+
attname, Instance(fallback_manager, [Instance(related_model_info, [])])
502516
)
517+
related_model_fullname = related_model_cls.__module__ + "." + related_model_cls.__name__
518+
self.ctx.api.fail(
519+
(
520+
"Couldn't resolve related manager for relation "
521+
f"{relation.name!r} (from {related_model_fullname}."
522+
f"{relation.field})."
523+
),
524+
self.ctx.cls,
525+
code=MANAGER_MISSING,
526+
)
527+
return
503528

504-
else:
505-
related_manager_info = None
506-
try:
507-
related_manager_info = self.lookup_typeinfo_or_incomplete_defn_error(
508-
fullnames.RELATED_MANAGER_CLASS
509-
)
510-
default_manager = related_model_info.names.get("_default_manager")
511-
if not default_manager:
512-
raise helpers.IncompleteDefnException()
513-
except helpers.IncompleteDefnException as exc:
514-
if not self.api.final_iteration:
515-
raise exc
516-
517-
# If a django model has a Manager class that cannot be
518-
# resolved statically (if it is generated in a way where we
519-
# cannot import it, like `objects = my_manager_factory()`),
520-
# we fallback to the default related manager, so you at
521-
# least get a base level of working type checking.
522-
#
523-
# See https://github.com/typeddjango/django-stubs/pull/993
524-
# for more information on when this error can occur.
525-
fallback_manager = self.get_or_create_manager_with_any_fallback(related_manager=True)
526-
if fallback_manager is not None:
527-
self.add_new_node_to_model_class(
528-
attname, Instance(fallback_manager, [Instance(related_model_info, [])])
529-
)
530-
related_model_fullname = related_model_cls.__module__ + "." + related_model_cls.__name__
531-
self.ctx.api.fail(
532-
(
533-
"Couldn't resolve related manager for relation "
534-
f"{relation.name!r} (from {related_model_fullname}."
535-
f"{relation.field})."
536-
),
537-
self.ctx.cls,
538-
code=MANAGER_MISSING,
539-
)
529+
# Check if the related model has a related manager subclassed from the default manager
530+
# TODO: Support other reverse managers than `_default_manager`
531+
default_reverse_manager_info = helpers.get_reverse_manager_info(
532+
self.api, model_info=related_model_info, derived_from="_default_manager"
533+
)
534+
if default_reverse_manager_info:
535+
self.add_new_node_to_model_class(attname, Instance(default_reverse_manager_info, []))
536+
return
540537

541-
continue
538+
# The reverse manager we're looking for doesn't exist. So we
539+
# create it. The (default) reverse manager type is built from a
540+
# RelatedManager and the default manager on the related model
541+
parametrized_related_manager_type = Instance(related_manager_info, [Instance(related_model_info, [])])
542+
default_manager_type = default_manager.type
543+
assert default_manager_type is not None
544+
assert isinstance(default_manager_type, Instance)
545+
# When the default manager isn't custom there's no need to create a new type
546+
# as `RelatedManager` has `models.Manager` as base
547+
if default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME:
548+
self.add_new_node_to_model_class(attname, parametrized_related_manager_type)
549+
return
542550

543-
# Check if the related model has a related manager subclassed from the default manager
544-
# TODO: Support other reverse managers than `_default_manager`
545-
default_reverse_manager_info = helpers.get_reverse_manager_info(
546-
self.api, model_info=related_model_info, derived_from="_default_manager"
547-
)
548-
if default_reverse_manager_info:
549-
self.add_new_node_to_model_class(attname, Instance(default_reverse_manager_info, []))
550-
continue
551+
# The reverse manager is based on the related model's manager, so it makes most sense to add the new
552+
# related manager in that module
553+
new_related_manager_info = helpers.add_new_class_for_module(
554+
module=self.api.modules[related_model_info.module_name],
555+
name=f"{related_model_cls.__name__}_RelatedManager",
556+
bases=[parametrized_related_manager_type, default_manager_type],
557+
)
558+
new_related_manager_info.metadata["django"] = {"related_manager_to_model": related_model_info.fullname}
559+
# Stash the new reverse manager type fullname on the related model, so we don't duplicate
560+
# or have to create it again for other reverse relations
561+
helpers.set_reverse_manager_info(
562+
related_model_info,
563+
derived_from="_default_manager",
564+
fullname=new_related_manager_info.fullname,
565+
)
566+
self.add_new_node_to_model_class(attname, Instance(new_related_manager_info, []))
551567

552-
# The reverse manager we're looking for doesn't exist. So we
553-
# create it. The (default) reverse manager type is built from a
554-
# RelatedManager and the default manager on the related model
555-
parametrized_related_manager_type = Instance(related_manager_info, [Instance(related_model_info, [])])
556-
default_manager_type = default_manager.type
557-
assert default_manager_type is not None
558-
assert isinstance(default_manager_type, Instance)
559-
# When the default manager isn't custom there's no need to create a new type
560-
# as `RelatedManager` has `models.Manager` as base
561-
if default_manager_type.type.fullname == fullnames.MANAGER_CLASS_FULLNAME:
562-
self.add_new_node_to_model_class(attname, parametrized_related_manager_type)
563-
continue
568+
def run_with_model_cls(self, model_cls: Type[Model]) -> None:
569+
# add related managers etc.
570+
processing_incomplete = False
571+
for relation in self.django_context.get_model_relations(model_cls):
572+
try:
573+
self.process_relation(relation)
574+
except helpers.IncompleteDefnException:
575+
processing_incomplete = True
564576

565-
# The reverse manager is based on the related model's manager, so it makes most sense to add the new
566-
# related manager in that module
567-
new_related_manager_info = helpers.add_new_class_for_module(
568-
module=self.api.modules[related_model_info.module_name],
569-
name=f"{related_model_cls.__name__}_RelatedManager",
570-
bases=[parametrized_related_manager_type, default_manager_type],
571-
)
572-
new_related_manager_info.metadata["django"] = {"related_manager_to_model": related_model_info.fullname}
573-
# Stash the new reverse manager type fullname on the related model, so we don't duplicate
574-
# or have to create it again for other reverse relations
575-
helpers.set_reverse_manager_info(
576-
related_model_info,
577-
derived_from="_default_manager",
578-
fullname=new_related_manager_info.fullname,
579-
)
580-
self.add_new_node_to_model_class(attname, Instance(new_related_manager_info, []))
577+
if processing_incomplete and not self.api.final_iteration:
578+
raise helpers.IncompleteDefnException
581579

582580

583581
class AddExtraFieldMethods(ModelClassInitializer):

tests/typecheck/models/test_related_fields.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,22 @@
123123
on_delete=models.CASCADE,
124124
related_name="model_2s",
125125
)
126+
127+
- case: test_processes_other_relations_when_one_field_is_broken
128+
main: |
129+
from myapp.models import MyModel
130+
reveal_type(MyModel().others) # N: Revealed type is "django.db.models.fields.related_descriptors.RelatedManager[myapp.models.Other]"
131+
installed_apps:
132+
- myapp
133+
files:
134+
- path: myapp/__init__.py
135+
- path: myapp/models.py
136+
content: |
137+
from django.db import models
138+
139+
class MyModel(models.Model):
140+
# Plugin doesn't handle lazy reference without app label
141+
broken = models.ManyToManyField("MyModel", related_name="mymodels") # type: ignore[var-annotated]
142+
143+
class Other(models.Model):
144+
field = models.ForeignKey(MyModel, related_name="others", on_delete=models.CASCADE)

0 commit comments

Comments
 (0)