From 617c03e76dc31f52a305ec32b00a17aa25b8573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:16:33 +0200 Subject: [PATCH 01/13] Add test case reproducing Sequence name not defined issue --- .github/workflows/test.yml | 3 +- .../managers/querysets/test_from_queryset.yml | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee8387b85..b5b9ad426 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,7 @@ jobs: strategy: matrix: python-version: ['3.8'] + mypy_ini_file: ['mypy.ini', 'mypy.ini.dev'] steps: - uses: actions/checkout@v3 - name: Setup system dependencies @@ -46,7 +47,7 @@ jobs: pip install -r ./requirements.txt - name: Run tests - run: pytest --mypy-ini-file=mypy.ini + run: pytest --mypy-ini-file="${{ matrix.mypy_ini_file }}" typecheck: runs-on: ubuntu-latest diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index caa8249d4..2a50a25bb 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -384,3 +384,37 @@ MyManager = BaseManager.from_queryset(MyQuerySet) class MyModel(models.Model): objects = MyManager() + + +- case: from_queryset_custom_auth_user_model + main: | + from users.models import User + custom_settings: | + AUTH_USER_MODEL = "users.User" + INSTALLED_APPS = ("django.contrib.auth", "django.contrib.contenttypes", "users") + files: + - path: users/__init__.py + - path: users/models.py + content: | + from django.contrib.auth.models import AbstractBaseUser + from django.db import models + + from .querysets import UserQuerySet + + UserManager = models.Manager.from_queryset(UserQuerySet) + + class User(AbstractBaseUser): + email = models.EmailField(unique=True) + objects = UserManager() + USERNAME_FIELD = "email" + + - path: users/querysets.py + content: | + from django.db import models + from typing import Optional, TYPE_CHECKING + + if TYPE_CHECKING: + from .models import User + + class UserQuerySet(models.QuerySet["User"]): + pass From 6c6fc509388ef6919db5e9e707d0acef5d9c2355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:12:25 +0200 Subject: [PATCH 02/13] Resolve all manager methods as attribute This changes to logic for resolving methods from the base QuerySet class on managers from copying the methods to use the attribute approach that's already used for methods from custom querysets. This resolves the phantom type errors that stem from the copying. --- mypy_django_plugin/transformers/managers.py | 41 ++++++++++++--------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 836d0e93a..c3fe9d351 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -26,11 +26,14 @@ def get_method_type_from_dynamic_manager( - api: TypeChecker, method_name: str, manager_type_info: TypeInfo + api: TypeChecker, method_name: str, manager_type: Instance ) -> Optional[ProperType]: """ Attempt to resolve a method on a manager that was built from '.from_queryset' """ + + manager_type_info = manager_type.type + if ( "django" not in manager_type_info.metadata or "from_queryset_manager" not in manager_type_info.metadata["django"] @@ -56,11 +59,23 @@ def get_funcdef_type(definition: Union[FuncBase, Decorator, None]) -> Optional[P return None assert isinstance(method_type, CallableType) + + variables = method_type.variables + ret_type = method_type.ret_type + + # If the return type is a typevar or unbound type that appears to be a + # queryset we change the return type to be the actual queryset + if isinstance(ret_type, (UnboundType, TypeVarType)) and ret_type.name == "_QS": + ret_type = Instance(queryset_info, manager_type.args) + variables = [] + # Drop any 'self' argument as our manager is already initialized return method_type.copy_modified( arg_types=method_type.arg_types[1:], arg_kinds=method_type.arg_kinds[1:], arg_names=method_type.arg_names[1:], + variables=variables, + ret_type=ret_type, ) @@ -90,7 +105,7 @@ def get_method_type_from_reverse_manager( assert isinstance(model_info.names["_default_manager"].node, Var) manager_instance = model_info.names["_default_manager"].node.type return ( - get_method_type_from_dynamic_manager(api, method_name, manager_instance.type) + get_method_type_from_dynamic_manager(api, method_name, manager_instance) # TODO: Can we assert on None and Instance? if manager_instance is not None and isinstance(manager_instance, Instance) else None @@ -98,9 +113,10 @@ def get_method_type_from_reverse_manager( def resolve_manager_method_from_instance(instance: Instance, method_name: str, ctx: AttributeContext) -> MypyType: + api = helpers.get_typechecker_api(ctx) method_type = get_method_type_from_dynamic_manager( - api, method_name, instance.type + api, method_name, instance ) or get_method_type_from_reverse_manager(api, method_name, instance.type) return method_type if method_type is not None else ctx.default_attr_type @@ -266,25 +282,16 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte # Skip any method that doesn't return _QS original_return_type = get_proper_type(original_return_type) - if isinstance(original_return_type, UnboundType): - if original_return_type.name != "_QS": - continue - elif isinstance(original_return_type, TypeVarType): + if isinstance(original_return_type, (UnboundType, TypeVarType)): if original_return_type.name != "_QS": continue else: continue - # Return the custom queryset parameterized by the manager's type vars - return_type = Instance(derived_queryset_info, self_type.args) - - helpers.copy_method_to_another_class( - class_def_context, - self_type, - new_method_name=name, - method_node=func_node, - return_type=return_type, - original_module_name=class_mro_info.module_name, + helpers.add_new_sym_for_info( + new_manager_info, + name=name, + sym_type=AnyType(TypeOfAny.special_form), ) # Insert the new manager (dynamic) class From 6ef392b91c3fd2657bce74a193c92895a9548936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:55:50 +0200 Subject: [PATCH 03/13] Disable cache in test case Make sure the test will fail regardless of which mypy.ini file is being using. Co-authored-by: Petter Friberg --- tests/typecheck/managers/querysets/test_from_queryset.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index 2a50a25bb..cad25ea82 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -387,6 +387,7 @@ - case: from_queryset_custom_auth_user_model + disable_cache: true main: | from users.models import User custom_settings: | From 6534a90ae0d873125ec91095cfc1fcd661d027d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:02:07 +0200 Subject: [PATCH 04/13] Update comments related to copying methods --- mypy_django_plugin/transformers/managers.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index c3fe9d351..38989ee09 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -248,17 +248,21 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte sym_type=AnyType(TypeOfAny.special_form), ) - # we need to copy all methods in MRO before django.db.models.query.QuerySet - # Gather names of all BaseManager methods + # For methods that are common between BaseManager and QuerySet we need to + # update the return type of the methods on the manager to return the + # correct QuerySet type. This is done by adding the methods as attributes + # with type Any to the manager class, similar to how custom queryset + # methods are handled above. The actual type of these methods are resolved + # in resolve_manager_method. + + # Find all methods that are defined on BaseManager manager_method_names = [] for manager_mro_info in new_manager_info.mro: if manager_mro_info.fullname == fullnames.BASE_MANAGER_CLASS_FULLNAME: - for name, sym in manager_mro_info.names.items(): + for name in manager_mro_info.names: manager_method_names.append(name) - # Copy/alter all methods in common between BaseManager/QuerySet over to the new manager if their return type is - # the QuerySet's self-type. Alter the return type to be the custom queryset, parameterized by the manager's model - # type variable. + # Find the QuerySet type info and add attributes for methods defined on BaseManager for class_mro_info in derived_queryset_info.mro: if class_mro_info.fullname != fullnames.QUERYSET_CLASS_FULLNAME: continue From 544966b71a0c040ae2f11b2e8afb2edb2b46528b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:52:43 +0200 Subject: [PATCH 05/13] Use a predefined list of manager methods to update The list of manager methods that returns a queryset, and thus need to have it's return type changed, is small and well defined. Using a predefined list of methods rather than trying to detect these at runtime makes the code much more readable and probably faster as well. Also add `extra()` to the methods tested in from_queryset_includes_methods_returning_queryset, and sort the methods alphabetically. --- mypy_django_plugin/transformers/managers.py | 90 ++++++++----------- .../managers/querysets/test_from_queryset.yml | 27 +++--- 2 files changed, 52 insertions(+), 65 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 38989ee09..cc2c7d7b3 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -24,6 +24,29 @@ from mypy_django_plugin import errorcodes from mypy_django_plugin.lib import fullnames, helpers +MANAGER_METHODS_RETURNING_QUERYSET = ( + "alias", + "all", + "annotate", + "complex_filter", + "defer", + "difference", + "distinct", + "exclude", + "extra", + "filter", + "intersection", + "none", + "only", + "order_by", + "prefetch_related", + "reverse", + "select_for_update", + "select_related", + "union", + "using", +) + def get_method_type_from_dynamic_manager( api: TypeChecker, method_name: str, manager_type: Instance @@ -63,9 +86,10 @@ def get_funcdef_type(definition: Union[FuncBase, Decorator, None]) -> Optional[P variables = method_type.variables ret_type = method_type.ret_type - # If the return type is a typevar or unbound type that appears to be a - # queryset we change the return type to be the actual queryset - if isinstance(ret_type, (UnboundType, TypeVarType)) and ret_type.name == "_QS": + # 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: ret_type = Instance(queryset_info, manager_type.args) variables = [] @@ -248,55 +272,17 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte sym_type=AnyType(TypeOfAny.special_form), ) - # For methods that are common between BaseManager and QuerySet we need to - # update the return type of the methods on the manager to return the - # correct QuerySet type. This is done by adding the methods as attributes - # with type Any to the manager class, similar to how custom queryset - # methods are handled above. The actual type of these methods are resolved - # in resolve_manager_method. - - # Find all methods that are defined on BaseManager - manager_method_names = [] - for manager_mro_info in new_manager_info.mro: - if manager_mro_info.fullname == fullnames.BASE_MANAGER_CLASS_FULLNAME: - for name in manager_mro_info.names: - manager_method_names.append(name) - - # Find the QuerySet type info and add attributes for methods defined on BaseManager - for class_mro_info in derived_queryset_info.mro: - if class_mro_info.fullname != fullnames.QUERYSET_CLASS_FULLNAME: - continue - for name, sym in class_mro_info.names.items(): - if name not in manager_method_names: - continue - - if isinstance(sym.node, FuncDef): - func_node = sym.node - elif isinstance(sym.node, Decorator): - func_node = sym.node.func - else: - continue - - method_type = func_node.type - if not isinstance(method_type, CallableType): - if not semanal_api.final_iteration: - semanal_api.defer() - return None - original_return_type = method_type.ret_type - - # Skip any method that doesn't return _QS - original_return_type = get_proper_type(original_return_type) - if isinstance(original_return_type, (UnboundType, TypeVarType)): - if original_return_type.name != "_QS": - continue - else: - continue - - helpers.add_new_sym_for_info( - new_manager_info, - name=name, - sym_type=AnyType(TypeOfAny.special_form), - ) + # For methods on BaseManager that return a queryset we need to update the + # return type to be the actual queryset subclass used. This is done by + # adding the methods as attributes with type Any to the manager class, + # similar to how custom queryset methods are handled above. The actual type + # of these methods are resolved in resolve_manager_method. + for name in MANAGER_METHODS_RETURNING_QUERYSET: + helpers.add_new_sym_for_info( + new_manager_info, + name=name, + sym_type=AnyType(TypeOfAny.special_form), + ) # Insert the new manager (dynamic) class assert semanal_api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, new_manager_info, plugin_generated=True)) diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index cad25ea82..6d51aabc1 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -350,24 +350,25 @@ - case: from_queryset_includes_methods_returning_queryset main: | from myapp.models import MyModel - reveal_type(MyModel.objects.none) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.alias) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.all) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.exclude) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.annotate) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.complex_filter) # N: Revealed type is "def (filter_obj: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.union) # N: Revealed type is "def (*other_qs: Any, *, all: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.intersection) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.defer) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.difference) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.select_for_update) # N: Revealed type is "def (nowait: builtins.bool =, skip_locked: builtins.bool =, of: typing.Sequence[builtins.str] =, no_key: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.select_related) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.prefetch_related) # N: Revealed type is "def (*lookups: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.annotate) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.alias) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.order_by) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.distinct) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.reverse) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]" - reveal_type(MyModel.objects.defer) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.exclude) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.extra) # N: Revealed type is "def (select: Union[builtins.dict[builtins.str, Any], None] =, where: Union[builtins.list[builtins.str], None] =, params: Union[builtins.list[Any], None] =, tables: Union[builtins.list[builtins.str], None] =, order_by: Union[typing.Sequence[builtins.str], None] =, select_params: Union[typing.Sequence[Any], None] =) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.filter) # N: Revealed type is "def (*args: Any, **kwargs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.intersection) # N: Revealed type is "def (*other_qs: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.none) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.only) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.order_by) # N: Revealed type is "def (*field_names: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.prefetch_related) # N: Revealed type is "def (*lookups: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.reverse) # N: Revealed type is "def () -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.select_for_update) # N: Revealed type is "def (nowait: builtins.bool =, skip_locked: builtins.bool =, of: typing.Sequence[builtins.str] =, no_key: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.select_related) # N: Revealed type is "def (*fields: Any) -> myapp.models.MyQuerySet[myapp.models.MyModel]" + reveal_type(MyModel.objects.union) # N: Revealed type is "def (*other_qs: Any, *, all: builtins.bool =) -> myapp.models.MyQuerySet[myapp.models.MyModel]" reveal_type(MyModel.objects.using) # N: Revealed type is "def (alias: Union[builtins.str, None]) -> myapp.models.MyQuerySet[myapp.models.MyModel]" installed_apps: - myapp From 748ae31ca91121300ce63f1417fdf377e7503560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:04:20 +0200 Subject: [PATCH 06/13] Revert changes in .github/workflows/tests.yml With cache_disable: true on the test case this is no longer needed to reproduce the bug. --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5b9ad426..ee8387b85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,6 @@ jobs: strategy: matrix: python-version: ['3.8'] - mypy_ini_file: ['mypy.ini', 'mypy.ini.dev'] steps: - uses: actions/checkout@v3 - name: Setup system dependencies @@ -47,7 +46,7 @@ jobs: pip install -r ./requirements.txt - name: Run tests - run: pytest --mypy-ini-file="${{ matrix.mypy_ini_file }}" + run: pytest --mypy-ini-file=mypy.ini typecheck: runs-on: ubuntu-latest From 066794b8009c41c62553a92d7d554f918621107e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:07:24 +0200 Subject: [PATCH 07/13] Remove unsued imports and change type of constant - Remove unused imports left behind - Change MANAGER_METHODS_RETURNING_QUERYSET to Final[FrozenSet[str]] --- mypy_django_plugin/transformers/managers.py | 48 +++++++++++---------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index cc2c7d7b3..adedfba2e 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Final, FrozenSet, Optional, Union from mypy.checker import TypeChecker, fill_typevars from mypy.nodes import ( @@ -19,32 +19,34 @@ from mypy.plugin import AttributeContext, ClassDefContext, DynamicClassDefContext, MethodContext from mypy.types import AnyType, CallableType, Instance, ProperType from mypy.types import Type as MypyType -from mypy.types import TypeOfAny, TypeVarType, UnboundType, get_proper_type +from mypy.types import TypeOfAny from mypy_django_plugin import errorcodes from mypy_django_plugin.lib import fullnames, helpers -MANAGER_METHODS_RETURNING_QUERYSET = ( - "alias", - "all", - "annotate", - "complex_filter", - "defer", - "difference", - "distinct", - "exclude", - "extra", - "filter", - "intersection", - "none", - "only", - "order_by", - "prefetch_related", - "reverse", - "select_for_update", - "select_related", - "union", - "using", +MANAGER_METHODS_RETURNING_QUERYSET: Final[FrozenSet[str]] = frozenset( + ( + "alias", + "all", + "annotate", + "complex_filter", + "defer", + "difference", + "distinct", + "exclude", + "extra", + "filter", + "intersection", + "none", + "only", + "order_by", + "prefetch_related", + "reverse", + "select_for_update", + "select_related", + "union", + "using", + ) ) From a36853f34a9962ebf7496399cece3f0a4d3ea8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:13:18 +0200 Subject: [PATCH 08/13] Import Final from typing_extensions Was added in 3.8, we still support 3.7 --- mypy_django_plugin/transformers/managers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index adedfba2e..b018b0d2d 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -1,4 +1,5 @@ -from typing import Final, FrozenSet, Optional, Union +from typing import FrozenSet, Optional, Union +from typing_extensions import Final from mypy.checker import TypeChecker, fill_typevars from mypy.nodes import ( From f4d0ad52b69ecb620e263b277289490c86164887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:15:10 +0200 Subject: [PATCH 09/13] Sort imports properly --- mypy_django_plugin/transformers/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index b018b0d2d..aa4f0ddb3 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -1,5 +1,4 @@ from typing import FrozenSet, Optional, Union -from typing_extensions import Final from mypy.checker import TypeChecker, fill_typevars from mypy.nodes import ( @@ -21,6 +20,7 @@ from mypy.types import AnyType, CallableType, Instance, ProperType from mypy.types import Type as MypyType from mypy.types import TypeOfAny +from typing_extensions import Final from mypy_django_plugin import errorcodes from mypy_django_plugin.lib import fullnames, helpers From 65a8b36d81446cefcd1052ec120eae4a8ff427d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:35:18 +0200 Subject: [PATCH 10/13] Remove explicit typing of final frozenset Co-authored-by: Nikita Sobolev --- mypy_django_plugin/transformers/managers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index aa4f0ddb3..88dddb1b7 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -1,4 +1,4 @@ -from typing import FrozenSet, Optional, Union +from typing import Optional, Union from mypy.checker import TypeChecker, fill_typevars from mypy.nodes import ( @@ -25,7 +25,7 @@ from mypy_django_plugin import errorcodes from mypy_django_plugin.lib import fullnames, helpers -MANAGER_METHODS_RETURNING_QUERYSET: Final[FrozenSet[str]] = frozenset( +MANAGER_METHODS_RETURNING_QUERYSET: Final = frozenset( ( "alias", "all", From cad32576d3d8d28cfa601694a14b1f65b8b77c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:43:03 +0200 Subject: [PATCH 11/13] Add comment for test case --- tests/typecheck/managers/querysets/test_from_queryset.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index 6d51aabc1..e440065d5 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -387,7 +387,13 @@ objects = MyManager() +# This tests a regression where mypy would generate phantom warnings about +# undefined types due to unresolved types when copying methods from QuerySet to +# a manager dynamically created using Manager.from_queryset(). +# +# For details see: https://github.com/typeddjango/django-stubs/issues/1022 - case: from_queryset_custom_auth_user_model + # Cache needs to be disable to consistenly reproduce the bug disable_cache: true main: | from users.models import User From 0572e365c85192b5d24c44721e65cf7fdb095a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:44:21 +0200 Subject: [PATCH 12/13] Fix typo --- tests/typecheck/managers/querysets/test_from_queryset.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/typecheck/managers/querysets/test_from_queryset.yml b/tests/typecheck/managers/querysets/test_from_queryset.yml index e440065d5..142f1f25a 100644 --- a/tests/typecheck/managers/querysets/test_from_queryset.yml +++ b/tests/typecheck/managers/querysets/test_from_queryset.yml @@ -393,7 +393,7 @@ # # For details see: https://github.com/typeddjango/django-stubs/issues/1022 - case: from_queryset_custom_auth_user_model - # Cache needs to be disable to consistenly reproduce the bug + # Cache needs to be disabled to consistenly reproduce the bug disable_cache: true main: | from users.models import User From 2599c6bf2b0f6c6c9ee9fd841ec742f8610d8293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20Lj=C3=B8dal?= <544451+ljodal@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:30:55 +0200 Subject: [PATCH 13/13] Rename variable --- mypy_django_plugin/transformers/managers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 88dddb1b7..28ac77418 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -52,13 +52,13 @@ def get_method_type_from_dynamic_manager( - api: TypeChecker, method_name: str, manager_type: Instance + api: TypeChecker, method_name: str, manager_instance: Instance ) -> Optional[ProperType]: """ Attempt to resolve a method on a manager that was built from '.from_queryset' """ - manager_type_info = manager_type.type + manager_type_info = manager_instance.type if ( "django" not in manager_type_info.metadata @@ -93,7 +93,7 @@ def get_funcdef_type(definition: Union[FuncBase, Decorator, None]) -> Optional[P # 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: - ret_type = Instance(queryset_info, manager_type.args) + ret_type = Instance(queryset_info, manager_instance.args) variables = [] # Drop any 'self' argument as our manager is already initialized