【问题标题】:Enforcing proper usage of python Enum强制正确使用 python Enum
【发布时间】:2020-05-03 21:33:22
【问题描述】:

所以我正在使用一个大型 Django 代码库,该代码库自始至终都使用 python 枚举,例如:

from enum import Enum

class Status(Enum):
    active = 'active'

# ... later
assert some_django_model_instance.status == Status.active.value  # so far so good

...当然,“.value”部分会被遗忘并一直被忽略。现在很难完全放弃枚举,尽管它们的问题多于有用。有没有办法自动检查这样的行:

assert some_django_model_instance.status == Status.active  # someone forgot ".value" here!

例如,使用 mypy 或 pylint,或者可能向基本 Enum 添加一些代码/断言?问题是,Status.active 并没有真正调用任何代码,它只是返回一个类,当然该类永远不会等于 some_django_model_instance.status,它是一个字符串。

【问题讨论】:

  • 不过,assert 行会抛出 AssertionError。这还不足以迫使开发人员重新审视吗?
  • 如果您的 django 是 2.2,您可以在数据库级别 docs.djangoproject.com/en/2.2/ref/models/constraints 添加约束检查。同样在最新的 django 3.0 中有新的选择方式docs.djangoproject.com/en/3.0/ref/models/fields/…。或者您可以扩展保存方法并在保存之前验证您的数据
  • @C.Nivs 当然,这个例子是微不足道的并且会明显地爆炸,但是在具有多个服务的大型代码库中,这个错误变得更加难以手动查找。
  • 如果你想限制一个类的使用/行为,元类可能值得一看
  • 我只能说上面的相等性测试应该是assert some_django_model_instance.status is Status.active,而status 应该是一个枚举成员,而不是一个字符串。我已经看到很多 django-enum 问题来解释如何做到这一点。

标签: python django pylint mypy


【解决方案1】:

您可以使用 --strict-equality 命令行标志/配置标志选项让 mypy 检测这些类型的有问题的比较。启用此标志后,执行some_str == Status.active 将产生如下错误:

error: Non-overlapping equality check (left operand type: "str", right operand type: "Literal[Status.active]")

注意:此标志将检查 all 始终为假的相等比较,而不仅仅是涉及枚举的比较。

但是,对于任何一个操作数定义了自定义 __eq__ 方法的任何相等比较,它将被禁用,因为自定义方法实际上可以做任何事情。

【讨论】:

    【解决方案2】:

    您可以通过子类化 enum.EnumMeta 来强制执行此操作:

    from enum import EnumMeta, Enum as _Enum
    
    class Enum(_Enum, metaclass=EnumMeta):
        def __eq__(self, arg):
            if isinstance(arg, self.__class__):
                return arg is self
            return self.value == arg
    

    现在您不必再致电 enum.value 进行比较:

    class Method(Enum):
        GET = 'GET'
        POST = 'POST'
    
    >>> get = 'GET'
    >>> Method.GET == get
    True
    >>> get == Method.GET
    True
    >>> Method.POST == Method.GET
    False
    

    这解决了其他人不会忘记调用.value 进行比较的问题,但会产生更大的问题,因为现在插入模型时忘记调用.value 的可能性成倍增加.

    要解决此问题,我建议您还继承 models.CharField 以创建您自己的枚举字段:

    class EnumField(models.CharField):
        def __init__(self, enum, **kwargs):
            self.enum = enum
    
        def from_db_value(self, value, expression, connection):
            if value is not None:
                return self.enum(value)
            return None
    
        def to_python(self, value):
            if isinstance(value, self.enum):
                return value.value
            return None
    
        def get_prep_value(self, value):
            if isinstance(value, self.enum):
                value = value.value
            return super().get_prep_value(value)
    
        def deconstruct(self):
            name, path, args, kwargs = super().deconstruct()
            args.append(self.enum)
            return name, path, args, kwargs
    

    现在您也可以在不调用.value 的情况下插入模型:

    class MyModel(models.Model):
        method = EnumField(enum=Method)
    
    >>> MyModel.objects.create(method=Method.GET)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多