对Django和DRF进行类型检查

如您所知,喜欢可选的静态类型。 问题是,有时它不是可选的,但却是不可能的。 因为我们在Python的生态系统中有许多大型的无类型项目。


Django和Django-Rest-Framework是其中两个。 被。 因为现在可以输入了! 让我介绍djangodrf Typed Django组织和存根。


这将是一个简洁的教程和入门指南。


荣誉


我要对@mkurnikov领导这个项目以及所有实现这一目标的贡献者表示由衷的感谢。 你们真棒!


TLDR


在本文中,我将展示类型如何与djangodrf 。 您可以在这里查看结果


您也可以使用wemake-django-template在所有已配置的项目中启动新项目。 它看起来与示例项目完全一样。


开始使用


在这个小教程中,我将向您展示django-stubsdjangorestframework-stubs一些功能。 我希望这能使您相信,有人在您之后仔细检查事情是一件好事。


您始终可以参考原始文档。 所有步骤也都包括在内。


首先,我们需要一个新项目一个干净的虚拟环境 ,因此我们可以安装依赖项:


 pip install django django-stubs mypy 

然后,我们将需要正确配置mypy 。 它可以分为两个步骤。 首先,我们配置mypy本身:


 # 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 

然后我们配置django-stubs插件:


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

我们在这里做什么?


  1. 我们添加了一个自定义的mypy 插件,以帮助类型检查器在某些特定于Django的复杂情况下(例如模型,queryset,设置等)猜测类型。
  2. 我们还为django-stubs添加了自定义配置,以将其指向用于Django的设置。 它将需要导入它。

最终结果可以在这里找到。


现在,我们已安装并配置了所有内容。 让我们输入检查内容!


类型检查视图


让我们从键入视图开始,因为使用此插件最简单。


这是我们基于函数的简单视图:


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

有效! 好的,但这很简单。 让我们将示例复杂一点,并创建一个自定义模型,以展示如何键入模型和查询集。


类型检查模型和查询集


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

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]': # works fine! return BlogPost.objects.filter( is_published=True, ) 

以及如何检查它:


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

我们甚至可以使用.values().values_list()调用注释.values() .values_list() 。 这个插件很聪明!


多年来,我一直在尝试使用注释方法来返回QuerySet 。 此功能为我解决了一个大问题:不再有Iterable[BlogPost]List[User] 。 我现在可以使用实型。


类型检查API


但是,键入视图,模型,表单,命令,URL和admin并不是我们的全部。 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'] 

观看次数:


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


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

不,这行不通! 修正您的代码!


结论


键入框架接口是一件很棒的事情。 当与诸如returnsmappers之类的工具结合使用时,它将允许编写封装在类型化框架接口中的类型安全和声明性业务逻辑。 并减少这两者之间的层中的错误数量。


可选的渐进式静态类型还使您能够快速启动并仅在API稳定后才添加类型,或者从一开始就进行类型驱动的开发。


但是, django-stubsdjangorestframework-stubs是新项目。 仍然存在许多错误,计划中的功能,缺少类型说明。 我们欢迎社区做出的一切贡献,以使Python开发人员工具真正地变得很棒。


最初发布在我的博客上

Source: https://habr.com/ru/post/zh-CN465007/


All Articles