Skip to content

Commit 0fb3bfd

Browse files
authored
Break what apparently is a cycle involving custom User model and QuerySet.as_manager() (#2377)
1 parent eb6535f commit 0fb3bfd

File tree

2 files changed

+56
-7
lines changed

2 files changed

+56
-7
lines changed

mypy_django_plugin/transformers/managers.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -511,19 +511,24 @@ def _defer() -> None:
511511
base_as_manager = queryset_info.get("as_manager")
512512
if base_as_manager is None:
513513
return
514-
base_as_manager_type = get_proper_type(base_as_manager.type)
515-
if not isinstance(base_as_manager_type, CallableType):
516-
return
517-
base_as_manager_ret_type = get_proper_type(base_as_manager_type.ret_type)
518-
if not isinstance(base_as_manager_ret_type, Instance):
519-
return
520514

521-
base_ret_type = base_as_manager_ret_type.type
522515
manager_sym = semanal_api.lookup_fully_qualified_or_none(fullnames.MANAGER_CLASS_FULLNAME)
523516
if manager_sym is None or not isinstance(manager_sym.node, TypeInfo):
524517
return _defer()
525518

526519
manager_base = manager_sym.node
520+
base_ret_type = manager_base
521+
522+
if base_as_manager.fullname != f"{fullnames.QUERYSET_CLASS_FULLNAME}.as_manager":
523+
base_as_manager_type = get_proper_type(base_as_manager.type)
524+
if not isinstance(base_as_manager_type, CallableType):
525+
return
526+
base_as_manager_ret_type = get_proper_type(base_as_manager_type.ret_type)
527+
if not isinstance(base_as_manager_ret_type, Instance):
528+
return
529+
530+
base_ret_type = base_as_manager_ret_type.type
531+
527532
manager_class_name = f"{manager_base.name}From{queryset_info.name}"
528533
current_module = semanal_api.modules[semanal_api.cur_mod_id]
529534
existing_sym = current_module.names.get(manager_class_name)

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,47 @@
431431
class MyQuerySet(models.QuerySet):
432432
pass
433433
objects = MyQuerySet.as_manager()
434+
435+
- case: queryset_as_manager_foreignkey_cycle
436+
main: |
437+
from payments.models import Payout
438+
reveal_type(Payout.objects) # N: Revealed type is "payments.models.ManagerFromPayoutQuerySet[payments.models.Payout]"
439+
440+
custom_settings: |
441+
INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "payments", "accounts")
442+
AUTH_USER_MODEL = "accounts.Account"
443+
files:
444+
- path: payments/__init__.py
445+
- path: payments/models.py
446+
content: |
447+
from __future__ import annotations
448+
449+
from typing_extensions import Self
450+
451+
from django.contrib.auth import get_user_model
452+
from django.db import models
453+
454+
UserModel = get_user_model()
455+
456+
class Transaction(models.Model):
457+
user = models.ForeignKey(
458+
UserModel, on_delete=models.CASCADE, related_name="transactions"
459+
)
460+
461+
class PayoutQuerySet(models.QuerySet["Payout"]):
462+
def unapplied(self) -> Self:
463+
return self
464+
465+
class Payout(models.Model):
466+
triggered_by = models.ForeignKey(
467+
Transaction, on_delete=models.CASCADE, related_name="payouts"
468+
)
469+
470+
objects = PayoutQuerySet.as_manager()
471+
472+
- path: accounts/__init__.py
473+
- path: accounts/models.py
474+
content: |
475+
from django.contrib.auth.models import AbstractUser
476+
477+
class Account(AbstractUser): pass

0 commit comments

Comments
 (0)