战斗python类型注解

Fighting python type annotations

我有一个非常简单的 class 继承自 requests.Session。代码目前看起来像:

import requests
import urllib.parse

from typing import Any, Optional, Union, cast

default_gutendex_baseurl = "https://gutendex.com/"


class Gutendex(requests.Session):
    def __init__(self, baseurl: Optional[str] = None):
        super().__init__()
        self.baseurl = baseurl or default_gutendex_baseurl

    def search(self, keywords: str) -> Any:
        res = self.get("/books", params={"search": keywords})
        res.raise_for_status()
        return res.json()

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if self.baseurl and not url.startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url)

        return super().request(method, url, *args, **kwargs)

我很难让 mypyrequest 方法感到满意。

第一个挑战是获取要验证的参数;环境 url: Union[str, bytes] 必须匹配中的类型注释 types-requests。我刚刚举起手来获得 *args**kwargs 正确,因为唯一的解决方案似乎是 重现各个参数注释,但我很高兴 保持原样。

随着函数签名的处理,mypy 现在正在抱怨 关于对 startswith 的调用:

example.py:23: error: Argument 1 to "startswith" of "bytes" has incompatible type "str"; expected "Union[bytes, Tuple[bytes, ...]]"

我可以用明确的方式解决这个问题 cast:

        if not cast(str, url).startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url)

...但这似乎只是引入了复杂性。

然后对urllib.parse.urljoin的调用不满意:

example.py:24: error: Value of type variable "AnyStr" of "urljoin" cannot be "Sequence[object]"
example.py:24: error: Incompatible types in assignment (expression has type "Sequence[object]", variable has type "Union[str, bytes]")

我不太确定这些错误是怎么回事。

我已经通过将显式转换移动到 方法:

      def request(
          self, method: str, url: Union[str, bytes], *args, **kwargs
      ) -> requests.Response:
          _url = url.decode() if isinstance(url, bytes) else url

          if not _url.startswith("http"):
              _url = urllib.parse.urljoin(self.baseurl, _url)

          return super().request(method, _url, *args, **kwargs)

但这感觉像是一个棘手的解决方法。

所以:


根据评论,这个:

        if self.baseurl and not url.startswith(
            "http" if isinstance(url, str) else b"http"
        ):

失败:

example.py:25: error: Argument 1 to "startswith" of "str" has incompatible type "Union[str, bytes]"; expected "Union[str, Tuple[str, ...]]"
example.py:25: error: Argument 1 to "startswith" of "bytes" has incompatible type "Union[str, bytes]"; expected "Union[bytes, Tuple[bytes, ...]]"

这解决了整个问题:

import requests
import urllib.parse

from typing import Union, cast

default_gutendex_baseurl = "https://gutendex.com/"


class Gutendex(requests.Session):
    def __init__(self, baseurl: str = None):
        super().__init__()
        self.baseurl = baseurl or default_gutendex_baseurl

    def search(self, keywords: str) -> dict[str, str]:
        res = self.get("/books", params={"search": keywords})
        res.raise_for_status()
        return res.json()

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if isinstance(url, str):
            if not url.startswith("http"):
                url = urllib.parse.urljoin(self.baseurl, url)

            return super().request(method, url, *args, **kwargs)
        else:
            raise TypeError('Gutendex does not support bytes type url arguments')

你不能不处理bytes,如果你说你接受它。如果 bytes 通过,只需引发异常或做一些更好的事情。如果你喜欢危险的生活,甚至只是 pass

此代码在 mypy 中验证得很好。

有点令人失望的是,这样的东西无法验证:

        if not url.startswith("http"):
            url = urllib.parse.urljoin(self.baseurl, url if isinstance(url, str) else url.decode())
        return super().request(method, url, *args, **kwargs)

即使 url.startswith 无法在 str 时获得 bytes,反之亦然,它仍然无法验证。 mypy 无法通过运行时逻辑进行验证,因此您只能执行以下操作:

    def request(
        self, method: str, url: Union[str, bytes], *args, **kwargs
    ) -> requests.Response:
        if isinstance(url, str):
            if not url.startswith("http"):
                url = urllib.parse.urljoin(self.baseurl, url)

            return super().request(method, url, *args, **kwargs)
        else:
            if not url.startswith(b"http"):
                url = urllib.parse.urljoin(self.baseurl, url.decode())

            return super().request(method, url, *args, **kwargs)

两者都支持,但以丑陋的方式重复了逻辑。