Vérification typographique de Django et DRF

Comme vous le savez déjà, j'aime la frappe statique optionnelle. Le fait est que parfois ce n'est pas facultatif, mais impossible. Parce que nous avons beaucoup de grands projets non typés dans l'écosystème de Python.


Django et Django-Rest-Framework étaient deux d'entre eux. Étaient. Parce que maintenant ils peuvent être tapés! Permettez-moi de vous présenter l'organisation django Django et les django pour django et drf .


Cela va être un tutoriel concis et un guide de démarrage.


Bravo


Je tiens à dire un grand merci à @mkurnikov pour avoir dirigé le projet et à tous les contributeurs qui ont rendu cela possible. Vous êtes tous géniaux!


TLDR


Dans cet article, je montre comment les types fonctionnent avec django et drf . Vous pouvez voir le résultat ici .


Et vous pouvez également utiliser wemake-django-template pour démarrer vos nouveaux projets avec tout ce qui est déjà configuré. Il ressemblera exactement à l'exemple de projet.


Pour commencer


Dans ce petit tutoriel, je vais vous montrer plusieurs fonctionnalités de django-stubs djangorestframework-stubs et djangorestframework-stubs en action. J'espère que cela vous convaincra qu'avoir quelqu'un pour vérifier les choses après vous est une bonne chose.


Vous pouvez toujours vous référer à la documentation d'origine. Toutes les étapes y sont également abordées.


Pour commencer, nous aurons besoin d' un nouveau projet et d' un environnement virtuel propre , afin que nous puissions installer nos dépendances:


 pip install django django-stubs mypy 

Ensuite, nous devrons configurer correctement mypy . Il peut être divisé en deux étapes. Tout d'abord, nous configurons le mypy lui-même:


 # 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 

Ensuite, nous configurons le plugin django-stubs :


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

Que faisons-nous ici?


  1. Nous ajoutons un plugin mypy personnalisé pour aider le vérificateur de types à deviner les types dans certaines situations complexes spécifiques à Django (comme les modèles, le jeu de requêtes, les paramètres, etc.)
  2. Nous ajoutons également une configuration personnalisée pour django-stubs pour la pointer vers les paramètres que nous utilisons pour Django. Il devra l'importer.

Le résultat final peut être trouvé ici .


Nous avons maintenant tout installé et configuré. Tapons des choses de contrôle!


Vérification des vues


Commençons par taper les vues car c'est la chose la plus simple à faire avec ce plugin.


Voici notre simple vue basée sur les fonctions:


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

Courons et voyons de quels types il est au courant. Notez que nous devrons peut-être modifier PYTHONPATH , afin que mypy puisse importer notre projet:


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

Essayons de casser quelque chose:


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

Non, il y a une faute de frappe et mypy l'attrapera:


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

Ça marche! D'accord, mais c'est assez simple. Compliquons un peu notre exemple et créons un modèle personnalisé pour montrer comment taper des modèles et des ensembles de requêtes.


Vérification des modèles et jeu de requêtes


L'ORM de Django est une fonctionnalité qui tue. Il est très flexible et dynamique. Cela signifie également qu'il est difficile à taper. Voyons quelques fonctionnalités déjà couvertes par django-stubs .


Notre définition de modèle:


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

Et chaque domaine de ce modèle est couvert par des django-stubs . Voyons quels types sont révélés:


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

Tout a l'air bien! django-stubs fournit un plugin mypy personnalisé pour convertir les champs du modèle en types d'instance corrects. C'est pourquoi tous les types sont correctement révélés.


La deuxième grande fonctionnalité du plugin django-stubs est que nous pouvons taper 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, ) 

Et voici comment cela peut être vérifié:


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

Nous pouvons même annoter des ensembles de requêtes avec des .values() et .values_list() . Ce plugin est intelligent!


J'ai du mal avec les méthodes d'annotation renvoyant des QuerySet depuis plusieurs années. Cette fonctionnalité résout un gros problème pour moi: plus d' Iterable[BlogPost] ou de List[User] . Je peux maintenant utiliser de vrais types.


Vérification des API


Mais, taper des vues, des modèles, des formulaires, des commandes, des URL et des administrateurs n'est pas tout ce que nous avons. TypedDjango a également des typages pour djangorestframework . Installons-le et configurons-le:


 pip install djangorestframework djangorestframework-stubs 

Et nous pouvons commencer à créer des sérialiseurs:


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

Vues:


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

Et les routeurs:


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

Il ne semble même pas que quelque chose ait changé, mais tout à l'intérieur est tapé: paramètres, sérialiseurs, ensembles de vues et routeurs. Il vous permettra d'ajouter progressivement des saisies là où vous en avez le plus besoin.


Essayons de changer queryset = BlogPost.objects.all()
to queryset = [1, 2, 3] dans nos vues:


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

Non, ça ne marchera pas! Réparez votre code!


Conclusion


Taper les interfaces du framework est une chose géniale à avoir. Lorsqu'il est combiné avec des outils tels que les returns et les mappers il permettra d'écrire une logique métier déclarative et sécurisée enveloppée dans des interfaces de framework typées. Et pour diminuer le nombre d'erreurs dans la couche entre ces deux.


La saisie statique progressive facultative vous permet également de démarrer rapidement et d'ajouter des types uniquement lorsque votre API est stabilisée ou de suivre un développement axé sur les types dès le début.


Cependant, django-stubs et djangorestframework-stubs sont de nouveaux projets. Il y a encore beaucoup de bugs, de fonctionnalités planifiées, de spécifications de type manquantes. Nous accueillons chaque contribution de la communauté pour rendre les outils de développement en Python vraiment géniaux.


Publié à l'origine sur mon blog .

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


All Articles