Typechecking Django und DRF

Wie Sie bereits wissen, liebe ich die optionale statische Eingabe. Die Sache ist, dass es manchmal nicht optional, aber unmöglich ist. Weil wir viele große untypisierte Projekte in Pythons Ökosystem haben.


Django und Django-Rest-Framework waren zwei davon. Waren. Denn jetzt können sie getippt werden! Lassen Sie mich Typed Django Organisation und Stubs für django und drf .


Dies wird ein kurzes Tutorial und eine Anleitung für die ersten Schritte sein.


Ein großes Lob


Ich möchte @mkurnikov für die Leitung des Projekts und allen Mitwirkenden, die dies ermöglicht haben, ein großes Dankeschön sagen. Ihr seid alle großartig!


TLDR


In diesem Artikel zeige ich, wie Typen mit django und drf . Das Ergebnis können Sie hier einsehen .


Sie können auch wemake-django-template , um Ihre neuen Projekte mit allem zu starten, was bereits konfiguriert ist. Es sieht genauso aus wie im Beispielprojekt.


Erste Schritte


In diesem kleinen Tutorial zeige ich Ihnen einige Funktionen von django-stubs und djangorestframework-stubs in Aktion. Ich hoffe, das wird Sie davon überzeugen, dass es eine gute Sache ist, jemanden zu haben, der die Dinge nach Ihnen überprüft.


Sie können sich immer auf die Originaldokumentation beziehen. Dort werden auch alle Schritte behandelt.


Zu Beginn benötigen wir ein neues Projekt und eine saubere virtuelle Umgebung , damit wir unsere Abhängigkeiten installieren können:


 pip install django django-stubs mypy 

Dann müssen wir mypy richtig konfigurieren. Es kann in zwei Schritte unterteilt werden. Zuerst konfigurieren wir den mypy selbst:


 # 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 

Dann konfigurieren wir das django-stubs Plugin:


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

Was machen wir hier?


  1. Wir fügen ein benutzerdefiniertes mypy Plugin hinzu, mit dessen Hilfe die Typprüfung in einigen komplizierten Django-spezifischen Situationen (wie Modellen, Abfragesätzen, Einstellungen usw.) erraten kann.
  2. Wir fügen auch eine benutzerdefinierte Konfiguration für django-stubs , um auf die Einstellungen zu verweisen, die wir für Django verwenden. Es muss importiert werden.

Das Endergebnis finden Sie hier .


Wir haben jetzt alles installiert und konfiguriert. Lassen Sie uns die Dinge überprüfen!


Typechecking-Ansichten


Beginnen wir mit der Eingabe von Ansichten, da dies mit diesem Plugin am einfachsten ist.


Hier ist unsere einfache funktionsbasierte Ansicht:


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

Lassen Sie uns laufen und sehen, welche Typen es kennt. Beachten Sie, dass wir möglicherweise PYTHONPATH ändern PYTHONPATH , damit mypy unser Projekt importieren kann:


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

Versuchen wir etwas zu zerbrechen:


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

Nein, es gibt einen Tippfehler und mypy wird ihn fangen:


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

Es funktioniert! Ok, aber das ist ziemlich einfach. Lassen Sie uns unser Beispiel etwas komplizieren und ein benutzerdefiniertes Modell erstellen, um zu zeigen, wie wir Modelle und Abfragesätze eingeben können.


Typprüfmodelle und Abfragesatz


Djangos ORM ist ein Killer-Feature. Es ist sehr flexibel und dynamisch. Es bedeutet auch, dass es schwer zu tippen ist. Sehen wir uns einige Funktionen an, die bereits von django-stubs abgedeckt werden.


Unsere Modelldefinition:


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

Und jedes Feld dieses Modells wird von django-stubs abgedeckt. Mal sehen, welche Typen aufgedeckt werden:


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

Alles sieht gut aus! django-stubs bietet ein benutzerdefiniertes mypy Plugin zum Konvertieren von mypy in korrekte mypy . Deshalb werden alle Typen korrekt angezeigt.


Das zweite große Feature des django-stubs Plugins ist, dass wir 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, ) 

Und so kann es überprüft werden:


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

Wir können sogar Abfragesätze mit .values() und .values_list() -Aufrufen kommentieren. Dieses Plugin ist schlau!


Ich habe mehrere Jahre lang mit Annotationsmethoden zu kämpfen, die QuerySet . Diese Funktion löst ein großes Problem für mich: kein Iterable[BlogPost] oder List[User] . Ich kann jetzt echte Typen verwenden.


Typechecking-APIs


Das Eingeben von Ansichten, Modellen, Formularen, Befehlen, URLs und Administratoren ist jedoch nicht alles, was wir haben. TypedDjango hat auch Typisierungen für djangorestframework . Lassen Sie es uns installieren und konfigurieren:


 pip install djangorestframework djangorestframework-stubs 

Und wir können anfangen, Serialisierer zu erstellen:


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

Ansichten:


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

Und Router:


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

Es sieht nicht einmal so aus, als hätte sich etwas geändert, aber alles im Inneren ist getippt: Einstellungen, Serializer, Viewsets und Router. Sie können schrittweise Eingaben dort hinzufügen, wo Sie sie am meisten benötigen.


Versuchen wir, queryset = BlogPost.objects.all() zu ändern.
zu queryset = [1, 2, 3] in unseren Ansichten:


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

Nein, es wird nicht funktionieren! Korrigieren Sie Ihren Code!


Fazit


Das Eingeben der Framework-Schnittstellen ist eine großartige Sache. In Kombination mit Tools wie returns und mappers können typsichere und deklarative Geschäftslogik in typisierte Framework-Schnittstellen geschrieben werden. Und um die Anzahl der Fehler in der Ebene zwischen diesen beiden zu verringern.


Mit der optionalen schrittweisen statischen Typisierung können Sie auch schnell beginnen und Typen hinzufügen, wenn Ihre API stabilisiert ist oder von Anfang an typengesteuert entwickelt wird.


django-stubs und djangorestframework-stubs sind jedoch neue Projekte. Es gibt immer noch viele Fehler, geplante Funktionen und fehlende Typspezifikationen. Wir freuen uns über jeden Beitrag der Community, um die Entwickler-Tools in Python wirklich großartig zu machen.


Ursprünglich in meinem Blog veröffentlicht .

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


All Articles