Skip to content

Commit 0bb7dbb

Browse files
authored
Fix crash using .values_list("pk") on abstract model (#1355)
1 parent 2dcb47c commit 0bb7dbb

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

mypy_django_plugin/django/context.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import sys
33
from collections import defaultdict
44
from contextlib import contextmanager
5-
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union
5+
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, Optional, Sequence, Set, Tuple, Type, Union
66

77
from django.core.exceptions import FieldError
88
from django.db import models
99
from django.db.models.base import Model
10+
from django.db.models.expressions import Expression
1011
from django.db.models.fields import AutoField, CharField, Field
1112
from django.db.models.fields.related import ForeignKey, RelatedField
1213
from django.db.models.fields.reverse_related import ForeignObjectRel
@@ -19,6 +20,7 @@
1920
from mypy.types import AnyType, Instance
2021
from mypy.types import Type as MypyType
2122
from mypy.types import TypeOfAny, UnionType
23+
from typing_extensions import Literal
2224

2325
from mypy_django_plugin.lib import fullnames, helpers
2426
from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME
@@ -375,30 +377,40 @@ def _resolve_field_from_parts(
375377
assert isinstance(field, (Field, ForeignObjectRel))
376378
return field
377379

378-
def resolve_lookup_into_field(
380+
def solve_lookup_type(
379381
self, model_cls: Type[Model], lookup: str
380-
) -> Union["Field[Any, Any]", ForeignObjectRel]:
382+
) -> Optional[Tuple[Sequence[str], Sequence[str], Union[Expression, Literal[False]]]]:
381383
query = Query(model_cls)
382-
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
384+
if (lookup == "pk" or lookup.startswith("pk__")) and query.get_meta().pk is None:
385+
# Primary key lookup when no primary key field is found, model is presumably
386+
# abstract and we can't say anything about 'pk'.
387+
return None
388+
return query.solve_lookup_type(lookup)
383389

390+
def resolve_lookup_into_field(
391+
self, model_cls: Type[Model], lookup: str
392+
) -> Union["Field[Any, Any]", ForeignObjectRel, None]:
393+
solved_lookup = self.solve_lookup_type(model_cls, lookup)
394+
if solved_lookup is None:
395+
return None
396+
lookup_parts, field_parts, is_expression = solved_lookup
384397
if lookup_parts:
385398
raise LookupsAreUnsupported()
386399
return self._resolve_field_from_parts(field_parts, model_cls)
387400

388401
def resolve_lookup_expected_type(self, ctx: MethodContext, model_cls: Type[Model], lookup: str) -> MypyType:
389-
query = Query(model_cls)
390-
if lookup == "pk" or lookup.startswith("pk__") and query.get_meta().pk is None:
391-
# Primary key lookup when no primary key field is found, model is presumably
392-
# abstract and we can't say anything about 'pk'.
393-
return AnyType(TypeOfAny.implementation_artifact)
394402
try:
395-
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
396-
if is_expression:
397-
return AnyType(TypeOfAny.explicit)
403+
solved_lookup = self.solve_lookup_type(model_cls, lookup)
398404
except FieldError as exc:
399405
ctx.api.fail(exc.args[0], ctx.context)
400406
return AnyType(TypeOfAny.from_error)
401407

408+
if solved_lookup is None:
409+
return AnyType(TypeOfAny.implementation_artifact)
410+
lookup_parts, field_parts, is_expression = solved_lookup
411+
if is_expression:
412+
return AnyType(TypeOfAny.explicit)
413+
402414
field = self._resolve_field_from_parts(field_parts, model_cls)
403415

404416
lookup_cls = None

mypy_django_plugin/transformers/querysets.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def get_field_type_from_lookup(
5858
except LookupsAreUnsupported:
5959
return AnyType(TypeOfAny.explicit)
6060

61-
if (isinstance(lookup_field, RelatedField) and lookup_field.column == lookup) or isinstance(
61+
if lookup_field is None:
62+
return AnyType(TypeOfAny.implementation_artifact)
63+
elif (isinstance(lookup_field, RelatedField) and lookup_field.column == lookup) or isinstance(
6264
lookup_field, ForeignObjectRel
6365
):
6466
related_model_cls = django_context.get_field_related_model_cls(lookup_field)

tests/typecheck/models/test_abstract.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,29 @@
77
AbstractUser.objects.get(pkey=1) # ER: Cannot resolve keyword 'pkey' into field..*
88
installed_apps:
99
- django.contrib.auth
10+
11+
- case: test_fetch_pk_with_custom_manager_on_abstract_model
12+
main: |
13+
from myapp.models import MyModel
14+
installed_apps:
15+
- myapp
16+
files:
17+
- path: myapp/__init__.py
18+
- path: myapp/models.py
19+
content: |
20+
from django.db import models
21+
22+
class BaseManager(models.Manager):
23+
pass
24+
25+
class BaseModel(models.Model):
26+
objects = BaseManager()
27+
28+
class Meta:
29+
abstract = True
30+
31+
def lock(self) -> None:
32+
reveal_type(type(self).objects.values_list("pk")) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.BaseModel, Tuple[Any]]"
33+
34+
class MyModel(BaseModel):
35+
field = models.IntegerField()

0 commit comments

Comments
 (0)