如何在类型别名或协议中使用mypy的`NewType`?

3
我想定义一个像这样的NewType:
from typing import NewType
from os import PathLike

AnyPath = PathLike[str] | str
RepoPath = NewType("RepoPath", AnyPath)          # ERR: Argument 2 to NewType(...) must be subclassable (got "Union[PathLike[str], str]")
# RepoPath = NewType("RepoPath", PathLike[str])  # ERR: NewType cannot be used with protocol classes

基本上,我希望以后可以将原始路径“str”或“pathlib.Path”传递给函数,并且它们可以强制执行这是特定于“Repo”的路径,而不是随机路径。这很有用,因为我的代码中有很多路径和url等,我不想让它们混淆(我也不想使用(应用程序)匈牙利符号)。是否有一种好的方法让类型检查器为我完成这个任务?

为什么在这里使用Union不是一个好选择? - C.Nivs
@C.Nivs 我不明白:我在这里使用 AnyPath = PathLike[str] | str = Union[PathLike[str], str]。但如果我创建一个函数 def foo(repo: AnyPath): ...,那么我不能保证 repo 是指向 Repo 的路径,它可能只是一个编程错误,一些在此上下文中没有意义的路径(或字符串)。NewType 在调用函数时使其明确,您正在传递一个标记为“RepoPath”的 str/PathLike,因此不太可能是编程错误。与 safe_str=escape(unsafe_str) 相同-两者都是字符串,但具有不同的含义。 - Greedo
抱歉,我想我有点误读了代码。类型签名不能保证路径有效,所以我假设您正在对路径本身进行一些运行时验证。否则,您仍然可以将任何内容传递给函数。这个可能是有用的阅读材料。基本上,NewType接受一个可调用对象作为其第二个参数,而这不是您代码中发生的情况。 - C.Nivs
@C.Nivs 是的,我已经阅读了那篇文章,并且对差异有了很好的理解,也知道为什么我写的代码不起作用。我想问的是,在类型系统的限制下如何实现所需的效果 - 我只是使用代码来说明我的意思。请看我下面发布的答案,其中提供了一个想法 - 但它并不完美,允许 pathlib.Path 但不允许 os.PathLike - Greedo
当引入NewType时,曾经讨论过允许基于联合类型和类似的类型创建NewType。然而,这个想法被放弃了,现在要求NewType必须基于“类似类”的对象。 - Carl
1个回答

0

好的,这里有一个没有协议的解决方案 - 即不接受任何定义 os.PathLike __fspath__ 的东西,而是只允许具体的 pathlib.Path str

基本上创建两个NewType,然后接受它们的联合,而不是单个NewType,该联合是子类型的联合。

from typing import NewType, overload, TypeAlias  # py3.10 +
from pathlib import Path
#from os import PathLike  # can't get this to work with NewType

AnyPath: TypeAlias = Path | str

RepoPathP = NewType("RepoPathP", Path)
RepoPathS = NewType("RepoPathS", str)

AnyRepoPath: TypeAlias = RepoPathP | RepoPathS


@overload
def RepoPath(path: str) -> RepoPathS: ...
@overload
def RepoPath(path: Path) -> RepoPathP: ...

def RepoPath(path: AnyPath) -> AnyRepoPath:
    if isinstance(path, str):
        return RepoPathS(path)
    else:
        return RepoPathP(path)


def foo(repo: AnyRepoPath) -> None:
    print(repo)

    
foo("bad")                        # Argument 1 to "foo" has incompatible type "str"; expected "Union[RepoPathP, RepoPathS]"
foo(Path("still bad"))            # Argument 1 to "foo" has incompatible type "Path"; expected "Union[RepoPathP, RepoPathS]"
foo(RepoPath("good"))             # Pass
foo(RepoPath(Path("also good")))  # Pass

正如所提到的,这并不完美,因为:

class MyCustomPath():
    def __fspath__(self) -> str:
        return r"C:/my/custom/dir"

path: os.PathLike = MyCustomPath() #fine, as expected
repo = RepoPath(path) #fails, since RepoPath accepts only str|Path, not str|PathLike

理想情况下,我希望它能够成功


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接