Skip to content

Commit 9f2d852

Browse files
Use bounded covariant TypeVars for explicit-arg QuerySet subclasses; restore master reparametrize_generic_class semantics for Manager/Field
1 parent e09c0f9 commit 9f2d852

6 files changed

Lines changed: 106 additions & 59 deletions

File tree

mypy_django_plugin/transformers/managers.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -606,10 +606,25 @@ def _defer() -> None:
606606
)
607607

608608

609-
def reparametrize_generic_class(ctx: ClassDefContext, base_class_fullname: str) -> None:
609+
def _is_omitted_generic(arg: MypyType) -> bool:
610+
"""True if ``arg`` is the ``Any`` mypy substitutes for an unparametrized generic."""
611+
proper_arg = get_proper_type(arg)
612+
return isinstance(proper_arg, AnyType) and proper_arg.type_of_any is TypeOfAny.from_omitted_generics
613+
614+
615+
def reparametrize_generic_class(
616+
ctx: ClassDefContext,
617+
base_class_fullname: str,
618+
*,
619+
bind_explicit_args: bool = False,
620+
) -> None:
610621
"""
611622
Add implicit generics to classes that are defined without generic.
612623
624+
When ``bind_explicit_args`` is True, also reparametrize partially or
625+
fully-explicit parametrizations by binding each explicit arg as both
626+
the bound and the PEP 696 default of a synthesized covariant TypeVar.
627+
613628
Note that this does not happen if mypy is run with disallow_any_generics = True,
614629
as not specifying the generic type is then considered an error.
615630
"""
@@ -633,10 +648,31 @@ def reparametrize_generic_class(ctx: ClassDefContext, base_class_fullname: str)
633648
if parent_class is None or not parent_class.args:
634649
return
635650

636-
type_vars = [
637-
type_var.copy_modified(id=type_var.id, default=parent_type_var)
638-
for type_var, parent_type_var in zip(parent_class.type.defn.type_vars, parent_class.args, strict=True)
639-
]
651+
# Bind explicit args only when the direct parent is the canonical base class
652+
is_direct_parent = parent_class.type.fullname == base_class_fullname
653+
if not (bind_explicit_args and is_direct_parent):
654+
if not _is_omitted_generic(parent_class.args[0]):
655+
return
656+
type_vars = list(parent_class.type.defn.type_vars)
657+
else:
658+
type_vars = []
659+
for type_var, parent_type_var in zip(parent_class.type.defn.type_vars, parent_class.args, strict=True):
660+
if _is_omitted_generic(parent_type_var):
661+
# Arg was omitted -- reuse the parent's TypeVar so its existing
662+
# PEP 696 default (e.g. ``_Row = TypeVar(default=_Model)``) is preserved.
663+
type_vars.append(type_var)
664+
else:
665+
# Arg was supplied explicitly -- synthesize a TypeVar bounded by the
666+
# user's arg so the subclass stays effectively concrete in user-facing
667+
# contexts (defaults + bound + covariance) while still being generic
668+
# enough for the plugin to flow annotation row types through.
669+
type_vars.append(
670+
type_var.copy_modified(
671+
id=type_var.id,
672+
upper_bound=parent_type_var,
673+
default=parent_type_var,
674+
)
675+
)
640676

641677
# If we end up with placeholders we need to defer so the placeholders are
642678
# resolved in a future iteration
@@ -654,6 +690,7 @@ def reparametrize_generic_class(ctx: ClassDefContext, base_class_fullname: str)
654690
def reparametrize_any_queryset_hook(ctx: ClassDefContext) -> None:
655691
"""
656692
Add implicit generics to QuerySet subclasses that are defined without generic.
693+
This is required for `.annotate` / `.values` call to update the typevars accordingly.
657694
658695
Eg.
659696
@@ -665,10 +702,20 @@ class MyQuerySet(models.QuerySet): ...
665702
_Row = TypeVar('_Row', covariant=True, default=_Model)
666703
class MyQuerySet(models.QuerySet[_Model, _Row]): ...
667704
705+
And
706+
707+
class BookQuerySet(models.QuerySet["Book"]): ...
708+
709+
is interpreted as:
710+
711+
_Model = TypeVar('_Model', bound=Book, covariant=True, default=Book)
712+
_Row = TypeVar('_Row', bound=Book, covariant=True, default=Book)
713+
class BookQuerySet(models.QuerySet[_Model, _Row]): ...
714+
668715
Note that this does not happen if mypy is run with disallow_any_generics = True,
669716
as not specifying the generic type is then considered an error.
670717
"""
671-
reparametrize_generic_class(ctx, fullnames.QUERYSET_CLASS_FULLNAME)
718+
reparametrize_generic_class(ctx, fullnames.QUERYSET_CLASS_FULLNAME, bind_explicit_args=True)
672719

673720

674721
def reparametrize_any_manager_hook(ctx: ClassDefContext) -> None:

tests/typecheck/fields/test_related.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -906,15 +906,15 @@
906906
from myapp.models.store import Store
907907
from myapp.models.user import User
908908
reveal_type(Store().purchases) # N: Revealed type is "myapp.models.purchase.Purchase_RelatedManager"
909-
reveal_type(Store().purchases.queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
910-
reveal_type(Store().purchases.filter()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
909+
reveal_type(Store().purchases.queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
910+
reveal_type(Store().purchases.filter()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
911911
reveal_type(Store().purchases.get()) # N: Revealed type is "myapp.models.purchase.Purchase"
912-
reveal_type(Store().purchases.filter().queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
912+
reveal_type(Store().purchases.filter().queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
913913
reveal_type(User().purchases) # N: Revealed type is "myapp.models.purchase.Purchase_RelatedManager"
914-
reveal_type(User().purchases.queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
915-
reveal_type(User().purchases.filter()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
914+
reveal_type(User().purchases.queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
915+
reveal_type(User().purchases.filter()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
916916
reveal_type(User().purchases.get()) # N: Revealed type is "myapp.models.purchase.Purchase"
917-
reveal_type(User().purchases.filter().queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet"
917+
reveal_type(User().purchases.filter().queryset_method()) # N: Revealed type is "myapp.models.querysets.PurchaseQuerySet[myapp.models.purchase.Purchase, myapp.models.purchase.Purchase]"
918918
installed_apps:
919919
- myapp
920920
files:

tests/typecheck/managers/querysets/test_as_manager.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "list[myapp.models.MyQuerySet[myapp.models.MyModel]]"
77
reveal_type(MyModel.objects.example_simple().just_int()) # N: Revealed type is "int"
88
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "dict[str, myapp.models.MyQuerySet[myapp.models.MyModel]]"
9-
reveal_type(MyModelWithoutSelf.objects.method()) # N: Revealed type is "myapp.models.QuerySetWithoutSelf"
9+
reveal_type(MyModelWithoutSelf.objects.method()) # N: Revealed type is "myapp.models.QuerySetWithoutSelf[myapp.models.MyModelWithoutSelf, myapp.models.MyModelWithoutSelf]"
1010
1111
installed_apps:
1212
- myapp
@@ -129,8 +129,8 @@
129129
main: |
130130
from typing_extensions import reveal_type
131131
from myapp.models import MyModel
132-
reveal_type(MyModel.objects.custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet"
133-
reveal_type(MyModel.objects.all().custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet"
132+
reveal_type(MyModel.objects.custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
133+
reveal_type(MyModel.objects.all().custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
134134
reveal_type(MyModel.objects.returns_int_sequence()) # N: Revealed type is "typing.Sequence[int]"
135135
installed_apps:
136136
- myapp
@@ -156,8 +156,8 @@
156156
from typing_extensions import reveal_type
157157
from myapp.models import MyModel1, MyModel2
158158
kls: type[MyModel1 | MyModel2] = MyModel1
159-
reveal_type(kls.objects.custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet1 | myapp.models.ModelQuerySet2"
160-
reveal_type(kls.objects.all().custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet1 | myapp.models.ModelQuerySet2"
159+
reveal_type(kls.objects.custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet1[myapp.models.MyModel1, myapp.models.MyModel1] | myapp.models.ModelQuerySet2[myapp.models.MyModel2, myapp.models.MyModel2]"
160+
reveal_type(kls.objects.all().custom_queryset_method()) # N: Revealed type is "myapp.models.ModelQuerySet1[myapp.models.MyModel1, myapp.models.MyModel1] | myapp.models.ModelQuerySet2[myapp.models.MyModel2, myapp.models.MyModel2]"
161161
reveal_type(kls.objects.returns_thing()) # N: Revealed type is "int | str"
162162
reveal_type(kls.objects.get()) # N: Revealed type is "myapp.models.MyModel1 | myapp.models.MyModel2"
163163
installed_apps:
@@ -195,7 +195,7 @@
195195
from myapp.models import MyModel, MyModelManager
196196
reveal_type(MyModelManager) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[Any]"
197197
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
198-
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
198+
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
199199
installed_apps:
200200
- myapp
201201
files:
@@ -217,7 +217,7 @@
217217
from myapp.models import MyModel, ManagerFromModelQuerySet
218218
reveal_type(ManagerFromModelQuerySet) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet1[Any]"
219219
reveal_type(MyModel.objects) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet1[myapp.models.MyModel]"
220-
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
220+
reveal_type(MyModel.objects.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
221221
installed_apps:
222222
- myapp
223223
files:
@@ -371,8 +371,8 @@
371371
from myapp.models import MyModel
372372
reveal_type(MyModel.objects_1) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
373373
reveal_type(MyModel.objects_2) # N: Revealed type is "myapp.models.ManagerFromModelQuerySet[myapp.models.MyModel]"
374-
reveal_type(MyModel.objects_1.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
375-
reveal_type(MyModel.objects_2.all()) # N: Revealed type is "myapp.models.ModelQuerySet"
374+
reveal_type(MyModel.objects_1.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
375+
reveal_type(MyModel.objects_2.all()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel, myapp.models.MyModel]"
376376
installed_apps:
377377
- myapp
378378
files:

0 commit comments

Comments
 (0)