Якщо ви не знали, як додати типізацію до вашого проєкту на 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
Що відбувається у цьому фрагменті?
- Ми додаємо кастомний плагін
mypy
, аби перевірка типів змогла проходити навіть в моделях, queryset, налаштуваннях Django; - Ми також додаємо кастомну конфігурацію для
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
— нові інструменти. Там є ще достатньо багів, запланованих фіч, відсутніх специфікацій до типів тощо.
Ще немає коментарів