假设我有一个简单的验证函数:
def is_valid_build_target(target: Any, throw=False) -> bool:
target = str(target)
allowed_targets = ["dev", "prod"]
is_allowed = target.lower() in ALLOWED_TARGETS
if not is_allowed and throw:
raise ValueError(
f"Invalid target '{target}'. Must be one of: {ALLOWED_TARGETS}"
)
assert target is not None
return is_allowed
但是如果我想使用这个函数,Mypy 不会将该断言传递回堆栈(大概是因为
target
是一个原语,所以它不是通过引用传递的):
Target = Literal["dev", "prod"]
target: Target | None = cast(Target | None, os.getenv("APP_TARGET", None))
if not is_valid_build_target(target):
raise ValueError(f"Invalid target, I could have used throw=True, but I wanted a custom error message")
# Mypy still thinks `target` could be None
如果我内联执行函数逻辑,或者之后使用
assert
,那么它就可以工作,但现在我没有将运行时验证逻辑保留在独立函数中:
if not is_valid_build_target(target):
raise ValueError(f"Invalid target...")
assert target is not None
# happy Mypy
验证函数内部有什么方法可以验证这一点并让 Mypy 满意吗?
我会让你的验证函数执行以下两件事之一:
我还会首先检查
os.getenv
返回 None
,因为根本不存在的变量与无效字符串值是一个不同的问题(尽管具有相同的可能解决方案)。
from typing import Literal, cast
Target = Literal["dev", "prod"]
ALLOWED_TARGETS = typing.get_args(Target)
def valid_build_target(target: str) -> Target:
lower_target = target.lower()
if lower_target in ALLOWED_TARGETS:
# It's a shame type narrowing doesn't work here.
return cast(Target, lower_target)
else:
raise ValueError(
f"Invalid target '{target}'. Must be one of: {ALLOWED_TARGETS}"
)
possible_target = os.getenv("APP_TARGET")
if possible_target is None:
raise ValueError("APP_TARGET not defined")
target: Target = valid_build_target(possible_target)