|
4 | 4 |
|
5 | 5 | from django.db.models import Manager, Model |
6 | 6 | 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 |
8 | 8 | from mypy.checker import TypeChecker |
9 | 9 | from mypy.nodes import ( |
10 | 10 | ARG_STAR2, |
@@ -458,126 +458,124 @@ def reverse_one_to_one_descriptor(self) -> TypeInfo: |
458 | 458 | def many_to_many_descriptor(self) -> TypeInfo: |
459 | 459 | return self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MANY_TO_MANY_DESCRIPTOR) |
460 | 460 |
|
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 |
469 | 467 |
|
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) |
471 | 470 |
|
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 |
479 | 480 |
|
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 |
489 | 493 |
|
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: |
497 | 514 | 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, [])]) |
502 | 516 | ) |
| 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 |
503 | 528 |
|
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 |
540 | 537 |
|
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 |
542 | 550 |
|
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, [])) |
551 | 567 |
|
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 |
564 | 576 |
|
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 |
581 | 579 |
|
582 | 580 |
|
583 | 581 | class AddExtraFieldMethods(ModelClassInitializer): |
|
0 commit comments