我想写一个mypy插件,以便为NotRequired[Optional[T]]引入一个类型别名.(正如我在this question中发现的那样,不可能用普通的python语言编写这种类型别名,因为在TypedDict定义之外不允许使用NotRequired.)

我的 idea 是定义一个泛型Possibly类型,如下所示:

# possibly.__init__.py

from typing import Generic, TypeVar

T = TypeVar("T")

class Possibly(Generic[T]):
    pass

然后我想让我的插件用NotRequired[Optional[X]]替换任何出现的Possibly[X].我try 了以下方法:

# possibly.plugin

from mypy.plugin import Plugin


class PossiblyPlugin(Plugin):
    def get_type_analyze_hook(self, fullname: str):
        if fullname != "possibly.Possibly":
            return
        return self._replace_possibly

    def _replace_possibly(self, ctx):
        arguments = ctx.type.args
        breakpoint()


def plugin(version):
    return PossiblyPlugin

在断点,我知道我必须基于arguments构造一个mypy.types.Type的子类的实例.但我没有找到建造NotRequired的方法.mypy.types没有对应的类型.我想这可能是因为typing.NotRequired不是一个班级,而是一个typing._SpecialForm.(我猜这是因为NotRequired不影响值类型,但它出现在TypedDict.__optional_keys__的定义上.)

所以,我想到了一个不同的策略:我可以判断TypedDict,看看哪些字段标记为Possibly,并设置TypedDict实例的.__optional_keys__使字段不需要,并将Possibly类型替换为mypy.types.UnionType(*arguments, None).但是我没有找到在mypy.plugin.Plugin上使用哪种方法来将TypedDict带入上下文.

所以,我被困住了.这是我第一次深入研究mypy的内部 struct .你能给我一些方向,如何实现我想做的事吗?

推荐答案

您的第一次try (构造子类mypy.types.Type的实例)是正确的- mypy简单地将其称为mypy.types.RequiredTypeNotRequired通过构造函数指定为实例状态,如下所示:mypy.types.RequiredType(<type>, required=False).

以下是实现_replace_possibly的初步try :

def _replace_possibly(ctx: mypy.plugin.AnalyzeTypeContext) -> mypy.types.Type:
    """
    Transform `possibly.Possibly[<type>]` into `typing.NotRequired[<type> | None]`. Most
    of the implementation is copied from
    `mypy.typeanal.TypeAnalyser.try_analyze_special_unbound_type`.

    All `set_line` calls in the implementation are for reporting purposes, so that if
    any errors occur, mypy will report them in the correct line and column in the file.
    """

    if len(ctx.type.args) != 1:
        ctx.api.fail(
            "possibly.Possibly[] must have exactly one type argument",
            ctx.type,
            code=mypy.errorcodes.VALID_TYPE,
        )
        return mypy.types.AnyType(mypy.types.TypeOfAny.from_error)

    # Disallow usage of `Possibly` outside of `TypedDict`. Note: This check uses
    # non-exposed API, but must be done, because (as of mypy==1.8.0) the plugin will
    # otherwise crash.
    type_analyser: mypy.typeanal.TypeAnalyser = ctx.api  # type: ignore[assignment]
    if not type_analyser.allow_required:
        ctx.api.fail(
            "possibly.Possibly[] can only be used in a TypedDict definition",
            ctx.type,
            code=mypy.errorcodes.VALID_TYPE,
        )
        return mypy.types.AnyType(mypy.types.TypeOfAny.from_error)

    # Make mypy analyse `<type>` and get the analysed type
    analysed_type = ctx.api.analyze_type(ctx.type.args[0])
    # Make a new instance of a `None` type context to represent `None` in the union
    # `<type> | None`
    unionee_nonetype = mypy.types.NoneType()
    unionee_nonetype.set_line(analysed_type)
    # Make a new instance of a union type context to represent `<type> | None`.
    union_type = mypy.types.UnionType((analysed_type, unionee_nonetype))
    union_type.set_line(ctx.type)
    # Make the `NotRequired[<type> | None]` type context
    not_required_type = mypy.types.RequiredType(union_type, required=False)
    not_required_type.set_line(ctx.type)
    return not_required_type

行动中的Your compliance tests人:

import typing_extensions as t

import possibly

class Struct(t.TypedDict):
    possibly_string: possibly.Possibly[str]
>>> non_compliant: Struct = {"possibly_string": int}  # mypy: Incompatible types (expression has type "type[int]", TypedDict item "possibly_string" has type "str | None") [typeddict-item]
>>> compliant_absent: Struct = {}  # OK
>>> compliant_none: Struct = {"possibly_string": None}  # OK
>>> compliant_present: Struct = {"possibly_string": "a string, indeed"}  # OK

备注:

  • Mypy的插件系统很强大,但并不是完全没有文档.为了编写插件,判断mypy的内部 struct 最简单的方法是使用现有的类型构造incorrectly,查看错误消息中使用了什么字符串或字符串模式,然后try 使用字符串/模式找到mypy的实现.例如,以下是typing.NotRequired的错误用法:

    from typing_extensions import TypedDict, NotRequired
    
    class A(TypedDict):
        a: NotRequired[int, str]  # mypy: NotRequired[] must have exactly one type argument
    

    您可以找到这条消息here,它表明,尽管typing.NotRequired不是一个类,但mypy将其建模为与任何其他泛型一样的类型,这可能是因为分析AST很容易.

  • 您的插件代码的组织当前是这样的:

    possibly/
      __init__.py
      plugin.py
    

    当mypy加载您的插件时,possibly.__init__中的任何运行时代码都将与插件一起加载,因为mypy将在try 加载入口点possibly.plugin.plugin时导入possibly.所有运行时代码,包括可能从第三方程序包中拉出的任何代码,都将在每次运行mypy时加载.我不认为这是可取的,除非您能保证您的包是轻量级的并且没有依赖项.

    事实上,当我写这篇文章的时候,我意识到numpy的mypy插件(numpy.typing.mypy_plugin)加载了numpy(一个很大的库!)因为这个组织.

    有一些方法可以绕过这个问题,而不必将插件目录与包分开--您必须在__init__中实现一些东西,如果它被mypy调用,则try 不加载任何运行时子包.

Python相关问答推荐

更改matplotlib彩色条的字体并勾选标签?

@Property方法上的inspect.getmembers出现意外行为,引发异常

我从带有langchain的mongoDB中的vector serch获得一个空数组

处理(潜在)不断增长的任务队列的并行/并行方法

如何避免Chained when/then分配中的Mypy不兼容类型警告?

如果值不存在,列表理解返回列表

如何在polars(pythonapi)中解构嵌套 struct ?

如何获取TFIDF Transformer中的值?

基于字符串匹配条件合并两个帧

ODE集成中如何终止solve_ivp的无限运行

移动条情节旁边的半小提琴情节在海运

在ubuntu上安装dlib时出错

Pandas Loc Select 到NaN和值列表

如何指定列数据类型

python中csv. Dictreader. fieldname的类型是什么?'

如何删除重复的文字翻拍?

如何编辑此代码,使其从多个EXCEL文件的特定工作表中提取数据以显示在单独的文件中

为什么Visual Studio Code说我的代码在使用Pandas concat函数后无法访问?

Python:在cmd中添加参数时的语法

如何在Polars中处理用户自定义函数的多行结果?