Как использовать подсказки типа Python с Django QuerySet?

Можно ли указать тип записей в Django QuerySet с подсказками типа Python? Что-то вроде QuerySet[SomeModel]?

Например, у нас есть модель:

class SomeModel(models.Model):
    smth = models.IntegerField()

И мы хотим передать QuerySet этой модели в качестве параметра в func:

def somefunc(rows: QuerySet):
    pass

Но как указать тип записей в QuerySet, например, с помощью List[SomeModel]:

def somefunc(rows: List[SomeModel]):
    pass

но с QuerySet?

Ответ 1

В одном решении может использоваться класс ввода Union.

from typing import Union, List
from django.db.models import QuerySet
from my_app.models import MyModel

def somefunc(row: Union[QuerySet, List[MyModel]]):
    pass

Теперь, когда вы нарезаете аргумент row, он будет знать, что возвращаемый тип представляет собой либо другой список MyModel, либо экземпляр MyModel, а также намекает, что методы класса QuerySet доступны в row аргумент тоже.

Ответ 2

Я сделал этот вспомогательный класс, чтобы получить подсказку общего типа:

from django.db.models import QuerySet
from typing import Iterator, Union, TypeVar, Generic

T = TypeVar("T")

class ModelType(Generic[T]):
    def __iter__(self) -> Iterator[Union[T, QuerySet]]:
        pass

Тогда используйте это так:

def somefunc(row: ModelType[SomeModel]):
    pass

Это уменьшает шум каждый раз, когда я использую этот тип, и делает его пригодным для использования между моделями (например, ModelType[DifferentModel]).

Ответ 3

Я тоже ищу решение. В списке разработчиков Django есть thread, в котором они обсуждают, как реализовать такую ​​функцию.

В настоящее время они разрабатывают расширение Django для mypy, но похоже, что нам может быть не повезло для нашего конкретного запроса. В своей дорожной карте под заголовком "Наверное, никогда":

Querysets могут иметь некоторую частичную поддержку, но сложные аргументы (например, те, которые используются для фильтрации и получения запросов), или объекты Q и F находятся за пределами выразительные возможности mypy, как сейчас.

Утверждая, что нам просто нужно будет использовать простой ole QuerySet, пока условия не улучшатся.

Ответ 4

Это улучшенный класс помощников Ор Дуана.

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

_Z = TypeVar("_Z")  

class QueryType(Generic[_Z], QuerySet):
    def __iter__(self) -> Iterator[_Z]: ...

Этот класс используется специально для объекта QuerySet, например, когда вы используете filter в запросе.
Пример:

from some_file import QueryType

sample_query: QueryType[SampleClass] = SampleClass.objects.filter(name=name)

Теперь интерпретатор распознает sample_query как объект QuerySet, и вы будете получать предложения, такие как count(), и, просматривая объекты, вы будете получать предложения для SampleClass

Примечание
Этот формат подсказок по типу доступен начиная с python3.6.


Вы также можете использовать django_hint, который имеет классы хинтинга специально для Django.

Ответ 5

Это немного неуклюже.

from typing import Generator
from models import MyModel

def make_a_queryset()->Generator[MyModel]:
    return (i for in in  MyModel.objects.all())

Я думаю, что это сохраняет ленивую оценку, но я не уверен.

Ответ 6

Существует специальный пакет под названием django-stubs (имя следует за PEP561) для ввода кода django.

Вот как это работает:

# server/apps/main/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render

def index(request: HttpRequest) -> HttpResponse:
    reveal_type(request.is_ajax)
    reveal_type(request.user)
    return render(request, 'main/index.html')

Выход:

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/views.py:14: note: Revealed type is 'def () -> builtins.bool'
server/apps/main/views.py:15: note: Revealed type is 'django.contrib.auth.models.User'

И с моделями и с QuerySet:

# server/apps/main/logic/repo.py
from django.db.models.query import QuerySet

from server.apps.main.models import BlogPost

def published_posts() -> 'QuerySet[BlogPost]':  # works fine!
    return BlogPost.objects.filter(
        is_published=True,
    )

Выход:

reveal_type(published_posts().first())
# => Union[server.apps.main.models.BlogPost*, None]

Ответ 7

На самом деле вы можете делать то, что вы хотите, если вы импортируете модуль аннотаций:

from __future__ import annotations
from django.db import models
from django.db.models.query import QuerySet

class MyModel(models.Model):
    pass

def my_function() -> QuerySet[MyModel]:
    return MyModel.objects.all()

Ни MyPy, ни интерпретатор Python не будут жаловаться на это или выдвигать исключения (протестировано на Python 3.7). MyPy, вероятно, не сможет проверить тип, но если все, что вам нужно, это документировать ваш тип возврата, этого должно быть достаточно.

Ответ 8

ИМХО, правильный способ сделать это - определить тип, который наследует QuerySet, и указать общий тип возвращаемого значения для итератора.

    from django.db.models import QuerySet
    from typing import Iterator, TypeVar, Generic, Optional

    T = TypeVar("T")


    class QuerySetType(Generic[T], QuerySet):  # QuerySet + Iterator

        def __iter__(self) -> Iterator[T]:
            pass

        def first(self) -> Optional[T]:
            pass

        # ... add more refinements


Тогда вы можете использовать это так:

users: QuerySetType[User] = User.objects.all()
for user in users:
   print(user.email)  # typing OK!
user = users.first()  # typing OK!

Ответ 9

Если вы хотите передать параметр, просто выполните это:

def somefunc(smth):
    pass

если вы хотите вернуть функцию типа списка list().