ITADN
typeddjango/django-stubs
typeddjango/django-stubs · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈
django-stubs

test Checked with mypy StackOverflow

本软件包包含 类型存根 和一个自定义 mypy 插件,旨在为 Django 框架提供更精确的静态类型和类型推断。Django 使用了一些 Python “魔法”,这使得某些代码模式难以获得精确的类型。这就是我们需要这个项目的原因。最终目标是能够获取大多数常见模式的精确类型。

支持的类型检查器

  • mypy:通过我们的自定义 mypy 插件提供完整且全面的支持,包含多项高级功能
  • pyright:基础支持,已在 CI 中检查
  • pyrefly:基础支持,已在 CI 中检查
  • ty:基础支持,已在 CI 中检查

安装

pip install 'django-stubs[compatible-mypy]'

要让 mypy 识别该插件,你需要添加

[mypy]
plugins =
    mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = "myproject.settings"

在你的 mypy.inisetup.cfg 文件 中。

pyproject.toml 配置也受支持:

[tool.mypy]
plugins = ["mypy_django_plugin.main"]

[tool.django-stubs]
django_settings_module = "myproject.settings"

这里发生了两件事:

  1. 我们需要显式列出我们的插件,以便被 mypy 加载
  2. 你可以像上面那样指定 django_settings_module,或者让 django_stubs 使用你环境中的 DJANGO_SETTINGS_MODULE 变量。

这个完全可用的 typed boilerplate 可以作为你的示例。

版本兼容性

我们依赖于不同版本的 djangomypy

django-stubsMypy 版本Django 版本Django 部分支持Python 版本
6.1.0 (unreleased)1.13 - 2.36.16.0, 5.23.10 - 3.14
6.0.91.13 - 2.36.05.2, 5.1, 5.03.10 - 3.14
6.0.81.13 - 2.36.05.2, 5.1, 5.03.10 - 3.14
6.0.71.13 - 2.36.05.2, 5.1, 5.03.10 - 3.14
6.0.61.13 - 2.16.05.2, 5.1, 5.03.10 - 3.14
6.0.51.13 - 2.16.05.2, 5.1, 5.03.10 - 3.14
6.0.41.13 - 2.06.05.2, 5.1, 5.03.10 - 3.14
6.0.31.13 - 1.206.05.2, 5.1, 5.03.10 - 3.14
6.0.21.13 - 1.206.05.2, 5.1, 5.03.10 - 3.14
6.0.11.13 - 1.196.05.2, 5.1, 5.03.10 - 3.14
6.0.01.13 - 1.196.05.2, 5.1, 5.03.10 - 3.14
5.2.91.13 - 1.195.25.1, 5.03.10 - 3.13
5.2.81.13 - 1.195.25.1, 5.03.10 - 3.13
5.2.71.13 - 1.185.25.1, 5.03.10 - 3.13
5.2.61.13 - 1.185.25.1, 5.03.10 - 3.13
5.2.51.13 - 1.185.25.1, 5.03.10 - 3.13
5.2.41.13 - 1.185.25.1, 5.03.10 - 3.13
5.2.31.13 - 1.185.25.1, 5.03.10 - 3.13
5.2.21.13 - 1.175.25.1, 5.03.10 - 3.13
5.2.11.13 - 1.165.25.1, 5.03.10 - 3.13
5.2.01.13+5.25.1, 5.03.10 - 3.13

“部分”支持的含义,以及我们为何不固定到确切的 Django/mypy 版本,已在 https://github.com/typeddjango/django-stubs/discussions/2101#discussioncomment-9276632 中说明。

功能

Model Meta 属性的类型检查

[!NOTE] 如果你正在使用 mypy 插件并且已安装 django_stub_ext,你的模型 Meta 类 将在无需进一步更改的情况下自动进行类型检查。

通过继承 TypedModelMeta 类,你可以确保为 属性使用了正确的类型:

from django.db import models
from django_stubs_ext.db.models import TypedModelMeta

class MyModel(models.Model):
    example = models.CharField(max_length=100)

    class Meta(TypedModelMeta):
        ordering = ["example"]
        constraints = [
            models.UniqueConstraint(fields=["example"], name="unique_example"),
        ]

其他带类型的基类

  • django_stubs_ext.db.router.TypedDatabaseRouter 可用于实现自定义数据库路由器时作为基类。

设置

django-stubs 有一些设置,你可以在以下位置查看:

  • pyproject.toml,位于表格 [tool.django-stubs]
  • mypy.ini 位于表格 [mypy.plugins.django-stubs]

支持的设置如下:

  • django_settings_module,一个字符串,默认为 os.getenv(DJANGO_SETTINGS_MODULE)

    指定你的设置模块的导入路径,与 Django 的 DJANGO_SETTINGS_MODULE 环境变量 相同。

  • strict_settings,一个布尔值,默认 true

    如果使用动态设置,请设置为 false,如下文所述

  • strict_model_abstract_attrs,一个布尔值,默认 true

    如果你希望保留 .objects.DoesNotExist.NotUpdated.MultipleObjectsReturned 属性在 models.Model 类型上,请设置为 false参见此处了解 为什么默认这样做是危险的。

常见问题

这是官方的 Django 项目吗?

不是。目前我们独立于 Django。 有一个提案 将我们的项目合并到 Django 本身。 你可以通过点赞该 PR 来表示支持。

在生产环境中使用这个项目安全吗?

是的,安全!这个项目完全不会影响你的运行时。 它只影响 mypy 类型检查过程。

但是,如果没有 mypy,使用这个项目就没有任何意义。

当我安装此插件并运行 mypy 时,mypy 崩溃了

当前实现使用 Django 的运行时来提取有关模型的信息,因此如果你安装的应用程序或 models.py 损坏,它可能会崩溃。

换句话说,如果你的 manage.py runserver 崩溃,mypy 也会崩溃。 你也可以运行 mypy 并加上 --tb 选项以获取有关错误的额外信息。

我无法在类型注解中使用 QuerySet 或 Manager

当你尝试使用 QuerySet[MyModel]Manager[MyModel] 或其他基于 Django 的 Generic 类型时,你可能会遇到 TypeError: 'type' object is not subscriptable

这是因为这些 Django 类在运行时不支持 __class_getitem__ 魔术方法。

  1. 你可以使用我们的 django_stubs_ext 辅助工具,它会修补我们在 django 中作为 Generic 使用的所有类型。

    安装它:

    pip install django-stubs-ext  # as a production dependency

然后在您的顶层设置中放置:

import django_stubs_ext

django_stubs_ext.monkeypatch()

你可以使用 django_stubs_ext.monkeypatch(extra_classes=[YourDesiredType]) 添加额外的类型进行补丁

如果你在 django.contrib.auth.forms 中使用泛型符号,你将不得不手动在你的第一个 AppConfig.ready 中进行 monkeypatching。 目前这是必需的,因为 django.contrib.auth.forms 在 django 初始化之前无法被导入。

```python
import django_stubs_ext
from django.apps import AppConfig

class ClientsConfig(AppConfig):
    name = "clients"

    def ready(self) -> None:
        from django.contrib.auth.forms import SetPasswordMixin, SetUnusablePasswordMixin

        # For Django version prior to 5.1, use `extra_classes=[SetPasswordForm, AdminPasswordChangeForm]` instead.
        django_stubs_ext.monkeypatch(extra_classes=[SetPasswordMixin, SetUnusablePasswordMixin])
```

2. 你也可以使用字符串代替:'QuerySet[MyModel]''Manager[MyModel]',这样它在 mypy 中会作为类型生效,而在运行时则作为普通的 str

如何创建一个保证拥有已认证用户的 HttpRequest?

Django 内置的 HttpRequest 具有属性 user,它解析为类型 User | AnonymousUser,其中 User 是由 AUTH_USER_MODEL 设置指定的用户模型。

如果你想要一个 HttpRequest,并且可以对其进行类型注解,同时你知道用户是已认证的,你可以像这样对普通的 HttpRequest 类进行子类化:

from django.http import HttpRequest
from my_user_app.models import MyUser


class AuthenticatedHttpRequest(HttpRequest):
    user: MyUser

然后,当你确定用户已认证时,使用 AuthenticatedHttpRequest 代替标准的 HttpRequest。例如,在使用 @login_required 装饰器的视图中。

为什么我的自定义管理器会出现不兼容的返回类型错误?

如果你在没有使用泛型的情况下声明自定义管理器并覆盖内置 方法,你可能会看到关于不兼容错误信息的错误消息, 类似于以下内容:

from django.db import models

class MyManager(model.Manager):
    def create(self, **kwargs) -> "MyModel":
        pass

将导致此错误信息:

error: Return type "MyModel" of "create" incompatible with return type "_T" in supertype "BaseManager"

这是因为 Manager 类是泛型的,但未指定泛型时,内置的 manager 方法预期返回基类 manager 的泛型类型,即任意模型。要解决此问题,您应使用您的模型作为类型变量来声明您的 manager:

class MyManager(models.Manager["MyModel"]):
    ...

如何标注我调用了 QuerySet.annotate 的情况?

Django-stubs 提供了一个特殊类型 django_stubs_ext.WithAnnotations[Model, <Annotations>],它表示 Model 已被标注,这意味着模型实例需要额外的属性。

你应该提供这些属性的 TypedDict,例如 WithAnnotations[MyModel, MyTypedDict],以指定 存在哪些标注属性。

目前,mypy 插件可以识别传递给 QuerySet.annotate 的特定名称, 并将它们包含在类型中,但不会记录这些属性的类型。

关于特定标注字段的知识尚未用于为 QuerySetvaluesvalues_listfilter 方法创建更具体的类型,但是关于模型已被标注的知识 确实 被用于为 values/values_list 创建更广泛的类型结果类型,并允许对任何字段进行 filter

from typing import TypedDict
from django_stubs_ext import WithAnnotations
from django.db import models
from django.db.models.expressions import Value


class MyModel(models.Model):
    username = models.CharField(max_length=100)


class MyTypedDict(TypedDict):
    foo: str


def func(m: WithAnnotations[MyModel, MyTypedDict]) -> str:
    print(m.bar)  # Error, since field "bar" is not in MyModel or MyTypedDict.
    return m.foo  # OK, since we said field "foo" was allowed


func(MyModel.objects.annotate(foo=Value("")).get(id=1))  # OK
func(MyModel.objects.annotate(bar=Value("")).get(id=1))  # Error

你也可以在自定义的 QuerySet 方法中使用 WithAnnotations,方法是使 queryset 针对某个模型 TypeVar 泛型化。这样,返回类型在链式调用和 manager 访问中都能保持准确:

from typing import TypeVar, TypedDict
from django.db import models
from django.db.models import Manager
from django_stubs_ext import WithAnnotations


class SalesDict(TypedDict):
    total_sales: int


_Model = TypeVar("_Model", bound=models.Model, covariant=True)


class ProductQuerySet(models.QuerySet[_Model]):
    def with_sales(self) -> "ProductQuerySet[WithAnnotations[_Model, SalesDict]]":
        return self.annotate(total_sales=models.Sum("order__amount"))


ProductManager = Manager.from_queryset(ProductQuerySet)


class Product(models.Model):
    name = models.CharField(max_length=100)
    objects = ProductManager()


product = Product.objects.with_sales().get(id=1)
product.total_sales  # OK, int
product.name  # OK, str

为什么会出现提及 _StrPromise 的不兼容参数类型错误?

Django 的惰性翻译函数(例如 gettext_lazy)返回的是 Promise 而不是 str。这两种类型不能互换使用。因此,这些函数的返回类型被更改以反映这一点。

如果你在代码中遇到此错误,你可以将 Promise 转换为 str(这将导致翻译被求值),或者在类型提示中使用来自 django-stubs-extStrPromiseStrOrPromise 类型。选择哪种解决方案取决于具体案例。有关更多信息,请参阅 Django 文档中的处理惰性翻译对象

如果这是在 Django 代码中报告的,请报告一个问题或提交一个拉取请求来修复类型提示。

如何使用自定义库来处理 Django 设置?

使用类似 django-split-settingsdjango-configurations 的东西会使 mypy 难以推断你的设置。

在使用类似以下内容时,也可能出现这种情况:

try:
    from .local_settings import *
except Exception:
    pass

所以,mypy 不会喜欢这段代码:

from django.conf import settings

settings.CUSTOM_VALUE  # E: 'Settings' object has no attribute 'CUSTOM_VALUE'

为了处理这种边缘情况,我们有一个特殊设置 strict_settings(默认为 True), 你可以将其切换为 False,以便在运行时设置模块具有给定值时始终返回 Any 且不引发任何错误, 例如 pyproject.toml

[tool.django-stubs]
strict_settings = false

mypy.ini

[mypy.plugins.django-stubs]
strict_settings = false

然后:

from typing import reveal_type

# Works:
reveal_type(settings.EXISTS_AT_RUNTIME)  # N: Any

# Errors:
reveal_type(settings.MISSING)  # E: 'Settings' object has no attribute 'MISSING'

如何将 type[Model] 注解与 .objects 属性一起使用?

假设你有一个类似这样的函数, 它接受一个模型类型并访问其 .object 属性:

from django.db import models

def assert_zero_count(model_type: type[models.Model]) -> None:
    assert model_type.objects.count() == 0

此代码会引发 mypy 错误:

error: "type[Model]" has no attribute "objects"  [attr-defined]

这是一个常见问题:某些 type[models.Model] 类型可能没有 .objects 可用。 一个显著的例子是:抽象模型。 参见此处的推理

因此,对于一般情况,你应该编写:

def assert_zero_count(model_type: type[models.Model]) -> None:
    assert model_type._default_manager.count() == 0

可通过 strict_model_abstract_attrs = false 配置以跳过从 model.Model 中移除 .objects.DoesNotExist.NotUpdated.MultipleObjectsReturned 属性, 如果你正在使用我们的 mypy 插件。

请自行承担风险使用此设置,因为它可能会隐藏有效的错误。

如何为自定义 models.Field 添加类型?

[!NOTE] 这需要类型泛型支持,请参阅 此部分 以启用它。

Django models.Field(及其子类)是具有两个参数的泛型类型:

  • _ST:可用于设置值时的类型
  • _GT:获取值时将返回的类型

当你创建一个子类时,根据你希望自定义字段对使用者而言的类型严格程度,你有两个选择。

  1. 泛型子类:
from typing import TypeVar, reveal_type
from django.db import models

_ST = TypeVar("_ST", contravariant=True)
_GT = TypeVar("_GT", covariant=True)

class MyIntegerField(models.IntegerField[_ST, _GT]):
    ...

class User(models.Model):
    my_field = MyIntegerField()


reveal_type(User().my_field) # N: Revealed type is "int"
User().my_field = "12"  # OK (because Django IntegerField allows str and will try to coerce it)
  1. 非泛型子类(更严格):
from typing import reveal_type
from django.db import models

# This is a non-generic subclass being very explicit
# that it expects only int when setting values.
class MyStrictIntegerField(models.IntegerField[int, int]):
    ...

class User(models.Model):
    my_field = MyStrictIntegerField()


reveal_type(User().my_field) # N: Revealed type is "int"
User().my_field = "12" # E: Incompatible types in assignment (expression has type "str", variable has type "int")

参见 mypy 文档中关于泛型类子类的章节。

相关项目

贡献

本项目是开源且由社区驱动的。因此,我们鼓励各种规模的贡献。您可以通过以下任何方式进行贡献:

  1. 贡献代码(例如改进存根、添加插件功能、编写测试等)- 为此,请遵循贡献指南
  2. 协助进行代码审查和议题讨论。
  3. 识别并报告错误和问题。
  4. StackOverflow 上提问和回答问题。

您也随时可以通过 gitter 联系我们,讨论您的贡献!