您的第一次try (构造子类mypy.types.Type
的实例)是正确的- mypy简单地将其称为mypy.types.RequiredType
,NotRequired
通过构造函数指定为实例状态,如下所示: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 不加载任何运行时子包.