Comprobación de tipos Django y DRF

Como ya sabes, me encanta la escritura estática opcional. El caso es que a veces no es opcional, sino imposible. Porque tenemos muchos proyectos grandes sin tipo en el ecosistema de Python.


Django y Django-Rest-Framework fueron dos de ellos. Estaban ¡Porque ahora se pueden escribir! Permítanme presentarles la organización Typed Django y los drf para django y drf .


Este será un tutorial conciso y una guía de inicio.


Kudos


Quiero agradecer a @mkurnikov por liderar el proyecto y a todos los contribuyentes que lo hicieron posible. ¡Todos ustedes son geniales!


TLDR


En este artículo, drf cómo funcionan los tipos con django y drf . Puedes ver el resultado aquí .


Y también puede usar wemake-django-template para comenzar sus nuevos proyectos con todo lo que ya está configurado. Se verá exactamente como el proyecto de ejemplo.


Empezando


En este pequeño tutorial, le mostraré varias características de django-stubs y djangorestframework-stubs en acción. Espero que esto te convenza de que tener a alguien que revise las cosas después de ti es algo bueno.


Siempre puede consultar la documentación original. Todos los pasos también están cubiertos allí.


Para comenzar necesitaremos un nuevo proyecto y un entorno virtual limpio , para que podamos instalar nuestras dependencias:


 pip install django django-stubs mypy 

Entonces tendremos que configurar mypy correctamente. Se puede dividir en dos pasos. Primero, configuramos el mypy sí:


 # setup.cfg [mypy] # The mypy configurations: 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 

Luego configuramos el complemento django-stubs :


 # setup.cfg [mypy] # Appending to `mypy` section: plugins = mypy_django_plugin.main [mypy.plugins.django-stubs] django_settings_module = server.settings 

Que hacemos aqui


  1. mypy complemento mypy personalizado para ayudar al verificador de tipos a adivinar los tipos en algunas situaciones complicadas específicas de Django (como modelos, conjunto de consultas, configuraciones, etc.)
  2. También agregamos una configuración personalizada para django-stubs para señalarla a la configuración, la usamos para Django. Tendrá que importarlo.

El resultado final se puede encontrar aquí .


Ahora tenemos todo instalado y configurado. ¡Escribamos cosas de cheques!


Vistas de comprobación de tipo


Comencemos escribiendo las vistas, ya que es lo más fácil de hacer con este complemento.


Aquí está nuestra vista simple basada en funciones:


 # 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') 

Corramos y veamos de qué tipos es consciente. Tenga en cuenta que es posible que necesitemos modificar PYTHONPATH , por lo que mypy podría importar nuestro proyecto:


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

Intentemos romper algo:


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

No, hay un error tipográfico y mypy lo detectará:


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

Funciona! Ok, pero eso es bastante sencillo. Vamos a complicar un poco nuestro ejemplo y crear un modelo personalizado para mostrar cómo podemos escribir modelos y conjuntos de consultas.


Modelos de comprobación de tipo y conjunto de consultas


El ORM de Django es una característica asesina. Es muy flexible y dinámico. También significa que es difícil de escribir. Veamos algunas características que ya están cubiertas por django-stubs .


Nuestra definición del modelo:


 # 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) # example reveal of all fields in a model reveal_type(self.author) reveal_type(self.text) reveal_type(self.is_published) reveal_type(self.created_at) return '<BlogPost {0}>'.format(self.id) 

Y cada campo de este modelo está cubierto por django-stubs . Veamos qué tipos se revelan:


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

¡Todo se ve bien! django-stubs proporciona un complemento mypy personalizado para convertir campos de modelo en tipos de instancia correctos. Es por eso que todos los tipos se revelan correctamente.


La segunda gran característica del complemento django-stubs es que podemos escribir 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, ) 

Y así es como se puede verificar:


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

Incluso podemos anotar conjuntos de consultas con .values() y .values_list() . ¡Este complemento es inteligente!


He luchado con los métodos de anotación que devuelven QuerySet s durante varios años. Esta característica me resuelve un gran problema: no más Iterable[BlogPost] o List[User] . Ahora puedo usar tipos reales.


API de comprobación de tipo


Pero, escribir vistas, modelos, formularios, comandos, URL y admin no es todo lo que tenemos. TypedDjango también tiene tipings para djangorestframework . Vamos a instalarlo y configurarlo:


 pip install djangorestframework djangorestframework-stubs 

Y podemos comenzar a crear serializadores:


 # 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'] 

Vistas:


 # 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() 

Y enrutadores:


 # 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)), # ... ] 

Ni siquiera parece que algo haya cambiado, pero todo lo que está dentro está escrito: configuraciones, serializadores, vistas y enrutadores. Le permitirá agregar incrementalmente las tipificaciones donde más las necesita.


Intentemos cambiar queryset = BlogPost.objects.all()
a queryset = [1, 2, 3] en nuestras vistas:


 » 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]]") 

¡No, no funcionará! ¡Arregla tu código!


Conclusión


Escribir las interfaces de framework es algo increíble. Cuando se combina con herramientas como returns y mappers , permitirá escribir una lógica comercial declarativa y segura de tipos envuelta en interfaces de marco escritas. Y para disminuir el número de errores en la capa entre estos dos.


La escritura estática gradual opcional también le permite comenzar rápidamente y agregar tipos solo cuando su API está estabilizada o ir con un desarrollo basado en tipos desde el principio.


Sin embargo, django-stubs y djangorestframework-stubs son proyectos nuevos. Todavía hay muchos errores, características planificadas, especificaciones de tipo faltantes. Damos la bienvenida a cada contribución de la comunidad para hacer que las herramientas de desarrollador en Python sean realmente increíbles.


Publicado originalmente en mi blog .

Source: https://habr.com/ru/post/465007/


All Articles