为模型中的 Django 字段指定类型(对于 Pylint)

Specify typing for Django field in model (for Pylint)

我已经创建了基于 CharField 的自定义 Django 模型字段子类,但它使用 to_python 来确保模型对象 returned 具有更复杂的对象(有些是列表,有些是带有特定格式等)——我正在使用 MySQL,因此某些 PostGreSql 字段类型不可用。

一切都很好,但 Pylint 认为这些字段中的所有值都是字符串,因此我在使用这些模型的代码上收到很多 "unsupported-membership-test" 和 "unsubscriptable-object" 警告。我可以单独禁用它们,但我更愿意让 Pylint 知道这些模型 return 某些对象类型。类型提示没有帮助,例如:

class MealPrefs(models.Model):
    user = ...foreign key...
    prefs: dict = custom_fields.DictOfListsExtendsCharField(
            default={'breakfast': ['cereal', 'toast'], 'lunch': []},
            )

我知道某些内置的 Django 字段 return Pylint 的正确类型(CharField、IntegerField)和某些其他扩展已经找到了指定它们类型的方法,因此 Pylint 很高兴(MultiSelectField)但深入研究他们的代码,我无法弄清楚 "magic" 指定类型 returned 的位置。

(注:本题与Django表单域的INPUT:type无关)

谢谢!

出于好奇我看了这个,我认为大多数 "magic" 实际上是为了 pytest-django

在 Django 源代码中,例如对于 CharField,没有什么可以真正给类型提示器一个概念,即这是一个字符串。由于 class 仅继承自 Field,它也是其他非字符串字段的父级,因此知识需要在其他地方编码。

另一方面,通过挖掘 pylint-django 的源代码,我发现了最有可能发生这种情况的地方:

pylint_django.transforms.fields 中,有几个字段以类似的方式进行了硬编码:

_STR_FIELDS = ('CharField', 'SlugField', 'URLField', 'TextField', 'EmailField',
               'CommaSeparatedIntegerField', 'FilePathField', 'GenericIPAddressField',
               'IPAddressField', 'RegexField', 'SlugField')

在下面,一个名称可疑的函数 apply_type_shim,根据字段类型('str'、'int'、'dict', 'list', 等等)

此附加信息传递给 inference_tipaccording to the astroid docs,用于添加推理信息(强调我的):

astroid can be used as more than an AST library, it also offers some basic support of inference, it can infer what names might mean in a given context, it can be used to solve attributes in a highly complex class hierarchy, etc. We call this mechanism generally inference throughout the project.

astroid is the underlying library used by Pylint to represent Python code, so I'm pretty sure that's how the information gets passed to Pylint. If you follow what happens when you import the plugin, you'll find this interesting bit in pylint_django/.plugin,它实际上导入了 transforms,有效地将推理提示添加到 AST 节点。

我想如果你想用你自己的 classes 达到同样的效果,你可以:

  1. 直接派生自另一个 Django 模型 class,该模型已经具有您要查找的关联类型。
  2. 创建并注册一个等效的 pylint 插件,它也将使用 Astroid 向 class 添加信息,以便 Pylint 知道如何处理它。

我最初以为您使用的插件 pylint-django, but maybe you explicitly use prospector 如果找到 Django 会自动安装 pylint-django。

检查器 pylint 其插件不会使用来自 Python 类型注释 (PEP 484) 的信息来检查代码。它可以在不理解注释的情况下解析带有注释的代码,例如如果名称仅用于注释,则不要警告 "unused-import"。如果 class A() 没有方法 __contains__,消息 unsupported-membership-test 会在表达式 something in object_A 的一行中报告。同样,消息 unsubscriptable-object 与方法 __getitem__.

相关

您可以通过这种方式为您的自定义字段修补 pylint-django
添加功能:

def my_apply_type_shim(cls, _context=None):  # noqa
    if cls.name == 'MyListField':
        base_nodes = scoped_nodes.builtin_lookup('list')
    elif cls.name == 'MyDictField':
        base_nodes = scoped_nodes.builtin_lookup('dict')
    else:
        return apply_type_shim(cls, _context)
    base_nodes = [n for n in base_nodes[1] if not isinstance(n, nodes.ImportFrom)]
    return iter([cls] + base_nodes)

进入pylint_django/transforms/fields.py

并在同一文件的这一行将 apply_type_shim 替换为 my_apply_type_shim

def add_transforms(manager):
    manager.register_transform(nodes.ClassDef, inference_tip(my_apply_type_shim), is_model_or_form_field)

如果它们在 ModelFormView.


备注:

我还考虑了一个插件存根解决方案,它做同样的事情,但是 "prospector" 的替代方案对于 SO 来说似乎太复杂了,所以我更喜欢在安装后简单地修补源代码。

类 Model 或 FormView 是唯一由元classes 创建的 classes,在 Django 中使用。通过插件代码模拟 metaclass 并控制分析简单属性是个好主意。如果我记得,MyPy,在此处的一些评论中引用,也有一个用于 Django 的插件 mypy-django,但仅用于 FormView,因为为 django.db 编写注释比使用属性更复杂。 - 我试着花了一周的时间。