13

I have a Team model in my Django project. I create its custom model manager with QuerySet.as_manager().

class TeamQuerySet(models.QuerySet):
    def active(self) -> "models.QuerySet[Team]":
        return self.filter(is_active=True)

class Team(models.Model):
    is_active = models.BooleanField()

    objects = TeamQuerySet.as_manager()

When I try to execute Team.objects.active(), mypy gives the following error:

error: "Manager[Any]" has no attribute "active"
In [5]: Team.objects
Out[5]: <django.db.models.manager.ManagerFromTeamQuerySet at 0x10eee1f70>

If I was explicitly defining a TeamManager class, there would be not a problem. How can I hint the type of Django model field objects to a dynamically generated class?

viam0Zah
  • 25,949
  • 8
  • 77
  • 100

3 Answers3

9

Based on the Manager[Any] I assume you are already using django-stubs.

Unfortunately it looks like there's an open issue to make QuerySet.as_manager generic over the model it's attached to that has not been resolved yet.

Even if the PR addressing the issue got merged I'm afraid it wouldn't address your immediate issue because the as_manager needs to be generic over the generic QuerySet subclass used to create the manager in order for both .active to be available and attributes relating to Team be available.

In this regard this other PR, which is unfortunately quite stale, seems to properly address your issue.

Simon Charette
  • 5,009
  • 1
  • 25
  • 33
  • 1
    Thanks for taking the time to explore this issue. – viam0Zah Aug 13 '20 at 10:09
  • To add to the above you can work around this limitation by adding a `cast(TeamQuerySet[Team], ...)` around your `TeamQuerySet.as_manager()` call. Since the `Manager` generated by `as_manager` behaves 99% like the queryset it was created from it should cover your use case. – Simon Charette Oct 08 '20 at 13:24
  • Finally, your annotation on `TeamQuerySet.active` is wrong as it's actually returning `TeamQuerySet[T]` where `T` is the generic. I think you can omit it and mypy will figure it out by itself. – Simon Charette Oct 08 '20 at 13:26
  • thanks for looking into it. I tried your suggestion, however I got the error `"TeamQuerySet" expects no type arguments, but 1 given`. – viam0Zah Oct 09 '20 at 07:54
  • Not sure what's causing it. Maybe you need to make sure to extend `models.QuerySet['Team']` instead? And you'd only need a `cast(TeamQuerySet, TeamQuerySet.as_manager())`? – Simon Charette Oct 09 '20 at 11:46
  • can someone put a good example on how to do the casting until its correctly fixed? – Ali baba Jul 22 '21 at 19:43
0

I've worked around this with a little switch-a-roo for MyPy's sake:

_Q = TypeVar("_Q", bound="WorkflowQuerySet")


class WorkflowQuerySet(models.QuerySet["WorkflowModel"]):
    """
    Queryset for workflow objects.
    """

    def count_objects(self) -> int:
        raise NotImplementedError

    def latest_objects(self: _Q) -> _Q:
        raise NotImplementedError


if TYPE_CHECKING:
    # Create a type MyPy understands
    class WorkflowManager(models.Manager["WorkflowModel"]):
        def count_objects(self) -> int:
            ...

        def latest_objects(self) -> _Q:
            ...


else:
    WorkflowManager = WorkflowQuerySet.as_manager


class WorkflowModel(models.Model):
    """
    A model that has workflow.
    """

    objects = WorkflowManager()
Danielle Madeley
  • 2,616
  • 1
  • 19
  • 26
0

Here is my answer using generics and typevar

from typing import Generic, TypeVar
from django.db import models

class BookQueryset(models.QuerySet['Book']):
    ...

class Book(models.Model):
   objects: BookQueryset = BookQueryset.as_manager()


book = Book.objects.all()[0]

If you inspect book is type Book

viam0Zah
  • 25,949
  • 8
  • 77
  • 100
  • This gives the following error to me with mypy: `error: Incompatible types in assignment (expression has type "BookQueryset", base class "Model" defined the type as "BaseManager[Any]")` – viam0Zah Jul 19 '22 at 21:26