Skip to content

QuerySet's .annotate() returns an incorrect type when chained with .values() #602

Open
@kracekumar

Description

@kracekumar

Bug report

What's wrong

Iterating over the annotate result and accessing the aggregated item throws Model is not indexable.

class Author(models.Model):
    author_name = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "authors"
        verbose_name = "Author"
        verbose_name_plural = "Authors"

    def __str__(self):
        return self.author_name

class Book(models.Model):
    BOOK_STATUS=(
        ('PUBLISHED', 'Published'),
        ('ON_HOLD', 'On Hold'),
    )
    book_name = models.CharField(max_length=255)
    author = models.ForeignKey('Author',on_delete=models.CASCADE,
                               related_name='author')
    status = models.CharField(max_length=255, choices = BOOK_STATUS,
                              default=BOOK_STATUS[0][0])
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table="books"
        verbose_name="Book"
        verbose_name_plural="Books"

    def __str__(self):
        return self.book_name

class AuthorCount(TypedDict):
    authors: int

def get_book_counts_for_author() -> QuerySet[Book]:
    qs = Book.objects.values('author')
    qs1 = qs.annotate(
        authors=Count('author'))
    return qs1


def consume() -> list[int]:
    counts = []
    for item in get_book_counts_for_author():
        counts.append(item['authors']) # error happens here
    return counts

Mypy error

...
polls/models.py:79: error: Value of type "Book" is not indexable

What should be the proper way to annotate the code here other than type: ignore?

Failed Approaches

  • get_book_counts_for_author() -> QuerySet[AuthorCount]:

Output:

polls/models.py:69: error: Type argument "TypedDict('polls.models.AuthorCount', {'author': builtins.int})" of "QuerySet" must be a subtype of "django.db.models.base.Model"

  • get_book_counts_for_author() -> Union[QuerySet[Book], AuthorCount]:

Output:

polls/models.py:80: error: Value of type "Union[Book, str]" is not indexable
polls/models.py:80: error: Invalid index type "str" for "Union[Book, str]"; expected type "Union[int, slice]"
polls/models.py:83: error: Incompatible return value type (got "List[Union[Any, str]]", expected "List[int]")
  • Tried overriding the __getitem__, __iter__ like
class BookAnnotate(QuerySet[_T]):
    def __iter__(self) -> Iterator[AuthorCount]: ...

But output was polls/models.py:65: error: Return type "Iterator[AuthorCount]" of "__iter__" incompatible with return type "Iterator[_T]" in supertype "QuerySet". The same problem happens with __getittem__. Also QuerySet[AuthorType] fails since it's not a instance of Model.

Working approach

def get_book_counts_for_author() -> Union[QuerySet[Book], AuthorCount]:
    qs = Book.objects.values('author')
    qs1 = qs.annotate(
        authors=Count('author'))
    return qs1


def consume() -> list[Optional[int]]:
    counts = []
    for item in get_book_counts_for_author():
        if isinstance(item, dict):
            counts.append(item['authors'])
    return counts

Is there a better way to annotate the code for the annotate return value. The bad part of the code is to keep checking the instance type in the consuming function and adding optional throughout the code.

How is that should be

I don't know

System information

  • OS: OSX
  • python version: 3.9.4
  • django version: 3.2
  • mypy version: 0.812
  • django-stubs version: 1.8.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions