Django 与 mypy:如何解决由于扩展“AbstractUser”的自定义“User”模型类重新定义字段而导致的不兼容类型错误?

sun*_*unw 5 python django mypy django-stubs

我有一个现有的 Django 项目,它使用User扩展的自定义模型类AbstractUser。由于各种重要原因,我们需要重新定义该email字段,如下:

class User(AbstractUser):
    ...
    email = models.EmailField(db_index=True, blank=True, null=True, unique=True)
    ...
Run Code Online (Sandbox Code Playgroud)

最近添加了通过 mypy 进行键入检查。但是,当我执行 mypy 检查时,出现以下错误:

错误:赋值中的类型不兼容(表达式的类型为“EmailField[str | int | Combinable | None, str | None]”,基类“AbstractUser”将类型定义为“EmailField[str | int | Combinable, str]”)[任务]

我怎样才能让 mypy 允许这种类型重新分配?我不想仅仅使用,# type: ignore因为我希望使用它的类型保护。

对于上下文,如果我确实使用# type: ignore,那么我会从我的代码库中得到数十个以下 mypy 错误的实例:

错误:无法确定“电子邮件”的类型 [has-type]

以下是我的设置的详细信息:

python version: 3.10.5
django version: 3.2.19
mypy version: 1.6.1
django-stubs[compatible-mypy] version: 4.2.6
django-stubs-ext version: 4.2.5
typing-extensions version: 4.8.0
Run Code Online (Sandbox Code Playgroud)

Von*_*onC 1

您可以尝试使用与基类兼容的类型注释AbstractUser,但也允许自定义模型中的字段None值。emailUser

from typing import Optional
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    email: Optional[models.EmailField] = models.EmailField(db_index=True, blank=True, null=True, unique=True)
Run Code Online (Sandbox Code Playgroud)

email如果您显式地将字段注释为Optional[models.EmailField],则表明可以emailmodels.EmailField或类型None,这应该满足 Django 和 mypy 的要求。

通过使用Optional类型注释,您可以保持自定义模型的完整性User,同时也遵守 mypy 的类型检查标准。不再出现“不兼容类型”错误。


Incompatible types in assignment (expression has type "EmailField[Any, Any] | None", base class "AbstractUser" defined the type as "EmailField[str | int | Combinable, str]") [assignment]

email这意味着自定义模型中字段的类型定义User与类期望的类型不完全兼容AbstractUser。鉴于 mypy 对类型一致性非常严格,尤其是在覆盖子类中的字段时,您需要确保您的字段定义与预期类型匹配,AbstractUser同时还允许None.

创建一个类型变量,其中包含 允许的类型AbstractUser以及您的附加要求None
并在字段定义中使用此类型变量来满足基类和您的自定义要求。

from typing import Union, TypeVar
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.fields import Combinable

# Define a TypeVar that includes the types from AbstractUser and None
EmailFieldType = TypeVar("EmailFieldType", models.EmailField, None)

class User(AbstractUser):
    email: Union[EmailFieldType, models.EmailField[str | int | Combinable, str]] = models.EmailField(db_index=True, blank=True, null=True, unique=True)
Run Code Online (Sandbox Code Playgroud)

EmailFieldType现在是一个类型变量,允许email字段为 amodels.EmailFieldNone,与 中指定的类型对齐AbstractUser。在字段定义中使用Union应该满足 的特定类型要求AbstractUser以及您的自定义模型允许的需要None


error: Type variable "myproject.models.User.EmailFieldType" is unbound`
Run Code Online (Sandbox Code Playgroud)

TypeVar您可以直接使用Union来指定字段可以是或email所需的类型,而不是使用 a 。这将简化类型定义,并且对于 mypy 来说应该更容易解释。AbstractUserNone

error: Type variable "myproject.models.User.EmailFieldType" is unbound`
Run Code Online (Sandbox Code Playgroud)

emailmodels.EmailField字段被声明为和的并集None。这应该满足类型要求,AbstractUser同时也允许None作为值。


不幸的是同样的问题再次发生

然后,您将需要更复杂的类型处理或符合 mypy 期望的解决方法,而不损害 Django 模型的完整性。

尝试创建一个自定义子类models.EmailField,以与 Django 和 mypy 兼容的方式显式处理可为 null 的场景。

from typing import Union
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.fields import Combinable

class User(AbstractUser):
    email: Union[models.EmailField, None] = models.EmailField(db_index=True, blank=True, null=True, unique=True)
Run Code Online (Sandbox Code Playgroud)

作为更严厉的措施,您可以尝试完全重新定义该领域并谨慎处理迁移。这种方法需要email从模型中删除该字段,然后使用新规范重新添加它。这更具侵入性,需要仔细的迁移处理。