django-stubs icon indicating copy to clipboard operation
django-stubs copied to clipboard

Value of `_Row` not filled in for fully-typed queryset methods

Open bwo opened this issue 2 years ago • 0 comments

What's wrong

In order to work around #1845 I tried this:

from django.db import models
from typing import TypeVar, Any, TYPE_CHECKING, cast, reveal_type


_T = TypeVar("_T", bound=models.Model)

if TYPE_CHECKING:
    from django.db.models.query import _Row, _QuerySet
else:
    from django.db.models.query import QuerySet as _QuerySet

    _Row = TypeVar("_Row")


class SpecialQuerySet(_QuerySet[_T, _Row]):
    if TYPE_CHECKING:

        def values(
            self, *fields: str | models.expressions.Combinable, **expressions: Any
        ) -> "SpecialQuerySet[_T, dict[str, Any]]":
            return cast(
                SpecialQuerySet[_T, dict[str, Any]],
                super().values(*fields, **expressions),
            )

        def values_list(
            self,
            *fields: str | models.expressions.Combinable,
            flat: bool = False,
            named: bool = False,
        ) -> "SpecialQuerySet[_T, Any]":
            return cast(
                SpecialQuerySet[_T, Any],
                super().values_list(*fields, flat=flat, named=named),
            )

    def special_get(self, x: int, **kwargs: Any) -> _Row:
        if x % 2 == 0:
            return self.get(**kwargs)
        else:
            return self.filter(pk=3).get(**kwargs)

# this is necessary for obscure reasons to get line 4 of test() to work
class FooQuerySet(SpecialQuerySet[_T, _T]):
    pass
SManager = models.Manager.from_queryset(FooQuerySet)

class Foo(models.Model):

    text = models.TextField()
    objects = SManager()


def test() -> None:
    reveal_type(Foo.objects.special_get(4, text="hi").text)
    reveal_type(Foo.objects.values("text").special_get(4))
    reveal_type(Foo.objects.get)
    reveal_type(Foo.objects.filter(text="text").special_get(4))

This almost works! That is, it works for Foo.objects.values(), and with the FooQuerySet for .filter().special_get(). The rest of it does not work:

 mypy .
foo/models.py:55: error: "_Row" has no attribute "text"  [attr-defined]
foo/models.py:55: note: Revealed type is "Any"
foo/models.py:56: note: Revealed type is "TypedDict({'text': builtins.str})"
foo/models.py:57: note: Revealed type is "def (*args: Any, **kwargs: Any) -> digest.models.Foo"
foo/models.py:58: note: Revealed type is "digest.models.Foo"

If you want this to work completely, the only choice, as far as I can tell, is manually implementing a manager, not using from_queryset(), and re-typing every single manager method—not just .values() and others that change _Row, but filter(), all(), etc—literally all of them!

System information

  • OS: macos 13.5.2
  • python version: 3.11
  • django version: 4.2
  • mypy version: 1.6.1 (also happens with 1.7 and 1.8-dev versions)
  • django-stubs version: 4.2.6
  • django-stubs-ext version: 4.2.5

bwo avatar Nov 22 '23 17:11 bwo