Repository: typeddjango/django-stubs

Issue:
Plugin crash on abstract model filtering with custom Manager
python 3.10
django 4.1
mypy 0.982
django-stubs HEAD (a78c592)
django-stubs-ext HEAD (a78c592)

Crash in plugin when using an abstract model with a custom manager:

```
./myproj/app/models.py:15: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
https://mypy.readthedocs.io/en/stable/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.982
Traceback (most recent call last):
  File "mypy/checkexpr.py", line 4619, in accept
  File "mypy/nodes.py", line 1822, in accept
  File "mypy/checkexpr.py", line 385, in visit_call_expr
  File "mypy/checkexpr.py", line 505, in visit_call_expr_inner
  File "mypy/checkexpr.py", line 1155, in check_call_expr_with_callee_type
  File "mypy/checkexpr.py", line 1238, in check_call
  File "mypy/checkexpr.py", line 1438, in check_callable_call
  File "mypy/checkexpr.py", line 995, in apply_function_plugin
  File "django-stubs/mypy_django_plugin/transformers/orm_lookups.py", line 40, in typecheck_queryset_filter
    lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
  File "django-stubs/mypy_django_plugin/django/context.py", line 388, in resolve_lookup_expected_type
    lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
  File "/code/myproj/.venv/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1217, in solve_lookup_type
    _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
  File "/code/myproj/.venv/lib/python3.10/site-packages/django/db/models/sql/query.py", line 1656, in names_to_path
    name = opts.pk.name
AttributeError: 'NoneType' object has no attribute 'name'
./myproj/app/models.py:15: : note: use --pdb to drop into pdb
```

Minimal reproduction example (with or without django_stubs_ext.monkeypatch):

NB: This needs to be in a models file in an installed app.
```
from django.db import models


class BaseManager(models.Manager):
    pass


class BaseModel(models.Model):
    objects = BaseManager()

    class Meta:
        abstract = True

    def lock(self) -> None:
        type(self).objects.filter(pk=self.pk).select_for_update().values_list("pk")


class MyModel(BaseModel):
    pass

```


Discussion:
We just ran into the same issue, same scenario. Not sure what's changed on our end that it suddenly started happening, but for now we need to disable static type scanning in our builds.

Poking around the plugin code, the `Query.names_to_path` seems to assume all models have a primary key field, which abstract models don't:

```
        for pos, name in enumerate(names):
            cur_names_with_path = (name, [])
            if name == 'pk':  # ...and model is NOT abstract
                name = opts.pk.name
```
python: 3.10.3
django: 3.2.16
mypy: 0.991
django-stubs: 1.13

FWIW, I came up with this hack as a workaround:

```
from django.db.models.base import ModelBase

class AbstractModelBase(ModelBase):

    class DummyField:
        def __init__(self, name):
            self.name = name

    def __new__(mcs, name, bases, attrs, **kwargs):
        model_class = super().__new__(mcs, name, bases, attrs, **kwargs)
        options = model_class._meta
        if options.abstract and options.pk is None:
            options.pk = AbstractModelBase.DummyField("dummy")
        return model_class
```
Basically sets the abstract model's primary key field to be a dummy object with a `name` property. Has
no effect on application behavior, but tricks the plugin into working correctly.

One can then use this as the python metaclass for any abstract model classes that refer to `self.pk` in code, e.g.
```
class BaseModel(models.Model, metaclass=AbstractModelBase):
```


Resolving patch:
diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py
--- a/mypy_django_plugin/django/context.py
+++ b/mypy_django_plugin/django/context.py
@@ -384,6 +384,10 @@ def resolve_lookup_into_field(
 
     def resolve_lookup_expected_type(self, ctx: MethodContext, model_cls: Type[Model], lookup: str) -> MypyType:
         query = Query(model_cls)
+        if lookup == "pk" or lookup.startswith("pk__") and query.get_meta().pk is None:
+            # Primary key lookup when no primary key field is found, model is presumably
+            # abstract and we can't say anything about 'pk'.
+            return AnyType(TypeOfAny.implementation_artifact)
         try:
             lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
             if is_expression:


Test patch:
diff --git a/tests/typecheck/models/test_abstract.yml b/tests/typecheck/models/test_abstract.yml
new file mode 100644
--- /dev/null
+++ b/tests/typecheck/models/test_abstract.yml
@@ -0,0 +1,9 @@
+- case: test_filter_on_abstract_user_pk
+  main: |
+    from django.contrib.auth.models import AbstractUser
+    AbstractUser.objects.get(pk=1)
+    AbstractUser.objects.get(pk__in=[1])
+    reveal_type(AbstractUser().pk)  # N: Revealed type is "Any"
+    AbstractUser.objects.get(pkey=1)  # ER: Cannot resolve keyword 'pkey' into field..*
+  installed_apps:
+    - django.contrib.auth