Skip to content

Commit

Permalink
Fix Self typed custom queryset methods incompatible with base queryse…
Browse files Browse the repository at this point in the history
…t type (#1840)
  • Loading branch information
moranabadie committed Nov 29, 2023
1 parent 0a61d81 commit e18a070
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 16 deletions.
22 changes: 12 additions & 10 deletions mypy_django_plugin/transformers/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,21 @@ def _process_dynamic_method(
variables = method_type.variables
ret_type = method_type.ret_type

if not is_fallback_queryset:
queryset_instance = Instance(queryset_info, manager_instance.args)
else:
# The fallback queryset inherits _QuerySet, which has two generics
# instead of the one exposed on QuerySet. That means that we need
# to add the model twice. In real code it's not possible to inherit
# from _QuerySet, as it doesn't exist at runtime, so this fix is
# only needed for plugin-generated querysets.
queryset_instance = Instance(queryset_info, [manager_instance.args[0], manager_instance.args[0]])

# For methods on the manager that return a queryset we need to override the
# return type to be the actual queryset class, not the base QuerySet that's
# used by the typing stubs.
if method_name in MANAGER_METHODS_RETURNING_QUERYSET:
if not is_fallback_queryset:
ret_type = Instance(queryset_info, manager_instance.args)
else:
# The fallback queryset inherits _QuerySet, which has two generics
# instead of the one exposed on QuerySet. That means that we need
# to add the model twice. In real code it's not possible to inherit
# from _QuerySet, as it doesn't exist at runtime, so this fix is
# only needed for pluign-generated querysets.
ret_type = Instance(queryset_info, [manager_instance.args[0], manager_instance.args[0]])
ret_type = queryset_instance
variables = []
args_types = method_type.arg_types[1:]
if _has_compatible_type_vars(base_that_has_method):
Expand All @@ -138,7 +140,7 @@ def _process_dynamic_method(
]
if base_that_has_method.self_type:
# Manages -> Self returns
ret_type = _replace_type_var(ret_type, base_that_has_method.self_type.fullname, manager_instance)
ret_type = _replace_type_var(ret_type, base_that_has_method.self_type.fullname, queryset_instance)

# Drop any 'self' argument as our manager is already initialized
return method_type.copy_modified(
Expand Down
21 changes: 17 additions & 4 deletions tests/typecheck/managers/querysets/test_as_manager.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
- case: self_return_management
main: |
from myapp.models import MyModel
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]"
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]]"
from myapp.models import MyModel, MyModel2
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.MyQuerySet[myapp.models.MyModel]"
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.MyQuerySet[myapp.models.MyModel]]"
reveal_type(MyModel.objects.example_simple().just_int()) # N: Revealed type is "builtins.int"
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "builtins.dict[builtins.str, myapp.models.ManagerFromMyQuerySet[myapp.models.MyModel]]"
reveal_type(MyModel.objects.example_dict()) # N: Revealed type is "builtins.dict[builtins.str, myapp.models.MyQuerySet[myapp.models.MyModel]]"
reveal_type(MyModel2.objects.method()) # N: Revealed type is "myapp.models.MyQuerySet2"
installed_apps:
- myapp
Expand All @@ -19,13 +20,20 @@
class BaseQuerySet(models.QuerySet):
def example_dict(self) -> Dict[str, Self]: ...
class MyQuerySet2(models.QuerySet["MyModel2"]):
def method(self) -> "MyQuerySet2":
return self
class MyQuerySet(BaseQuerySet):
def example_simple(self) -> Self: ...
def example_list(self) -> List[Self]: ...
def just_int(self) -> int: ...
class MyModel(models.Model):
objects = MyQuerySet.as_manager()
class MyModel2(models.Model):
objects = MyQuerySet2.as_manager()
- case: declares_manager_type_like_django
main: |
from myapp.models import MyModel
Expand Down Expand Up @@ -192,6 +200,8 @@
reveal_type(MyOtherModel.objects.dummy_override()) # N: Revealed type is "myapp.models.MyOtherModel"
reveal_type(MyOtherModel.objects.example_mixin(MyOtherModel())) # N: Revealed type is "myapp.models.MyOtherModel"
reveal_type(MyOtherModel.objects.example_other_mixin()) # N: Revealed type is "myapp.models.MyOtherModel"
reveal_type(MyOtherModel.objects.test_self()) # N: Revealed type is "myapp.models._MyModelQuerySet2[myapp.models.MyOtherModel]"
reveal_type(MyOtherModel.objects.test_sub_self()) # N: Revealed type is "myapp.models._MyModelQuerySet2[myapp.models.MyOtherModel]"
installed_apps:
- myapp
files:
Expand All @@ -200,6 +210,7 @@
content: |
from typing import TypeVar, Generic
from django.db import models
from typing_extensions import Self
T = TypeVar("T", bound=models.Model)
T_2 = TypeVar("T_2", bound=models.Model)
Expand All @@ -215,12 +226,14 @@
def override(self) -> T: ...
def override2(self) -> T: ...
def dummy_override(self) -> int: ...
def test_sub_self(self) -> Self: ...
class _MyModelQuerySet2(SomeMixin, _MyModelQuerySet[T_2]):
def example_2(self) -> T_2: ...
def override(self) -> T_2: ...
def override2(self) -> T_2: ...
def dummy_override(self) -> T_2: ... # type: ignore[override]
def test_self(self) -> Self: ...
class MyModelQuerySet(_MyModelQuerySet2["MyModel"]):
def override(self) -> "MyModel": ...
Expand Down
4 changes: 2 additions & 2 deletions tests/typecheck/managers/querysets/test_from_queryset.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
- case: from_queryset_self_return_management
main: |
from myapp.models import MyModel
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.BaseManagerFromModelQuerySet[myapp.models.MyModel]"
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.BaseManagerFromModelQuerySet[myapp.models.MyModel]]"
reveal_type(MyModel.objects.example_simple()) # N: Revealed type is "myapp.models.ModelQuerySet[myapp.models.MyModel]"
reveal_type(MyModel.objects.example_list()) # N: Revealed type is "builtins.list[myapp.models.ModelQuerySet[myapp.models.MyModel]]"
installed_apps:
- myapp
files:
Expand Down

0 comments on commit e18a070

Please sign in to comment.