Перевірка типів у Django та DRF

6 хв. читання

Якщо ви не знали, як додати типізацію до вашого проєкту на Django або Django-Rest-Framework, то цей посібник допоможе вам.

Розглянемо, як працювати з типами в django та drf. Готовий код доступний за посиланням.

Ви також можете використати wemake-django-template, аби розпочати вже з готовою конфігурацію. Ваш проєкт на цьому етапі буде таким самим, як у прикладі.

Почнемо

З посібника ви дізнаєтесь, як використовувати фічі django-stubs та djangorestframework-stubs на практиці.

Ви можете додатково перевіряти кожен крок в офіційній документації — вона охоплює всі моменти, згадані в статті.

Для початку нам знадобиться новий проєкт та чисте віртуальне середовище, щоб ми могли встановити наші залежності:

pip install django django-stubs mypy

Далі необхідно буде сконфігурувати mypy коректно. Цей процес можна розділити на два етапи: спершу ми налаштовуємо безпосередньо mypy:

# setup.cfg
[mypy]
# Конфігурація mypy: https://mypy.readthedocs.io/en/latest/config_file.html
python_version = 3.7

check_untyped_defs = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_decorators = True
ignore_errors = False
ignore_missing_imports = True
implicit_reexport = False
strict_optional = True
strict_equality = True
no_implicit_optional = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unreachable = True
warn_no_return = True

Далі ми сконфігуруємо плагін django-stubs:

# setup.cfg
[mypy]
# Приєднуємо до секції `mypy`:
plugins =
  mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = server.settings

Що відбувається у цьому фрагменті?

  1. Ми додаємо кастомний плагін mypy, аби перевірка типів змогла проходити навіть в моделях, queryset, налаштуваннях Django;
  2. Ми також додаємо кастомну конфігурацію для django-stubs, аби сповістити Django-налаштування.

Кінцевий результат можна знайти за посиланням.

Тепер ми все встановили та налаштували. Перейдемо до перевірки типів!

Перевірка типів у view

Почнемо типізацію саме з views, адже цей процес буде найпростішим.

Приклад простого функціонального view:

# 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, аби mypy зміг імпортувати наш проєкт.

» 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'

Спробуймо щось зламати:

# server/apps/main/views.py
def index(request: HttpRequest) -> HttpResponse:
    return render(request.META, 'main/index.html')

Не вдалось. mypy все відловив:

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/views.py:18: error: Argument 1 to "render" has incompatible type "Dict[str, Any]"; expected "HttpRequest"

Як бачимо, все працює! Однак наш приклад дуже спрощений. Ускладнимо його трохи, аби дізнатися, як працювати з типами в моделях та queryset.

Перевірка типів у моделях та queryset

Django ORM — кілер-фіча, дуже гнучка та динамічна. Але це також означає, що її важко типізувати. Оглянемо деякі фічі, які вже реалізовує django-stubs.

Приклад оголошення моделі:

# server/apps/main/models.py
from django.contrib.auth import get_user_model
from django.db import models

User = get_user_model()

class BlogPost(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    text = models.TextField()

    is_published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self) -> str:
        reveal_type(self.id)  # відображаємо всі поля моделі
        reveal_type(self.author)
        reveal_type(self.text)
        reveal_type(self.is_published)
        reveal_type(self.created_at)
        return '<BlogPost {0}>'.format(self.id)

Так кожне поле моделі покрито django-stubs. Як щодо типів?

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/models.py:21: note: Revealed type is 'builtins.int*'
server/apps/main/models.py:22: note: Revealed type is 'django.contrib.auth.models.User*'
server/apps/main/models.py:23: note: Revealed type is 'builtins.str*'
server/apps/main/models.py:24: note: Revealed type is 'builtins.bool*'
server/apps/main/models.py:25: note: Revealed type is 'datetime.datetime*'

Все чудово! django-stubs передбачає кастомний плагін mypy для конвертації полів моделі у коректні типи екземпляра. Ось чому всі типи визначились коректно.

Ще одна цікава функція плагіна django-stubs полягає в тому, що ми можемо типізувати 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]': # працює!
    return BlogPost.objects.filter(
        is_published=True,
    )

А так його можна перевірити:

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

Ми навіть можемо анотувати наші querysets викликами .values() та .values_list(), і плагін все зрозуміє.

Така фіча допомогла авторові розв'язати надокучливу, через яку анотаційні методи повертали QuerySets. І більше жодних Iterable[BlogPost] чи List[User]. Тільки реальні типи.

Перевірка типів в API

Однак типізація view, моделей, форм, команд, url — ще не все. TypedDjango також підтримує типи для djangorestframework. Встановимо та сконфігуруємо їх:

pip install djangorestframework djangorestframework-stubs

А тепер ми можемо почати створення серіалізаторів:

# server/apps/main/serializers.py
from rest_framework import serializers

from server.apps.main.models import BlogPost, User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'email']

class BlogPostSerializer(serializers.HyperlinkedModelSerializer):
    author = UserSerializer()

    class Meta:
        model = BlogPost
        fields = ['author', 'text', 'is_published', 'created_at']

Views:

# server/apps/main/views.py
from rest_framework import viewsets

from server.apps.main.serializers import BlogPostSerializer
from server.apps.main.models import BlogPost

class BlogPostViewset(viewsets.ModelViewSet):
    serializer_class = BlogPostSerializer
    queryset = BlogPost.objects.all()

Та роути:

# server/apps/main/urls.py
from django.urls import path, include
from rest_framework import routers

from server.apps.main.views import BlogPostViewset, index

router = routers.DefaultRouter()
router.register(r'posts', BlogPostViewset)

urlpatterns = [
    path('', include(router.urls)),
    # ...
]

Ви могли навіть не помітити, чи щось змінилось у коді, але тепер все типізовано: налаштування, серіалізатори, вьюсети та роути. Тож ми можемо поступово додавати типи там, де нам це потрібно.

Спробуймо замінити queryset = BlogPost.objects.all() на queryset = [1, 2, 3] в нашому view:

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/views.py:25: error: Incompatible types in assignment (expression has type "List[int]", base class "GenericAPIView" defined the type as "Optional[QuerySet[Any]]")

Тепер нічого не працюватиме!

Висновок

Типізація інтерфейсів фреймворку — надзвичайно корисна річ. Якщо поєднувати її з інструментами на зразок returns та mappers, можна досягнути написання суворо типізованої та декларативної бізнес-логіки, огорнутої у типізовані інтерфейси фреймворку. Так зменшується ймовірність помилок на межі різних архітектурних рівнів.

Вибіркова поступова статична типізація також дасть змогу вам швидко почати додавати типи, лише якщо ваше API є сталим чи використовує сувору типізацію з самого початку.

Варто пам'ятати, що django-stubs та djangorestframework-stubs — нові інструменти. Там є ще достатньо багів, запланованих фіч, відсутніх специфікацій до типів тощо.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація