我想写一个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图中的复杂箭头形状

Pandas:计算中间时间条目的总时间增量

python—telegraph—bot send_voice发送空文件

如何使用matplotlib查看并列直方图

有没有办法让Re.Sub报告它所做的每一次替换?

如何在SQLAlchemy + Alembic中定义一个"Index()",在基表中的列上

read_csv分隔符正在创建无关的空列

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

pytest、xdist和共享生成的文件依赖项

VSCode Pylance假阳性(?)对ImportError的react

是否将Pandas 数据帧标题/标题以纯文本格式转换为字符串输出?

try 在单个WITH_COLUMNS_SEQ操作中链接表达式时,使用Polars数据帧时出现ComputeError

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

`Convert_time_zone`函数用于根据为极点中的每一行指定的时区检索值

将.exe文件从.py转换后出现问题.";ModuleNotFoundError:没有名为';Selify;的模块

不同 chromium 版本的selenium未检测到的 chromium 驱动器?

从语法生成后出现Antlr4 Python运行时错误