简体   繁体   English

格斗python类型注解

[英]Fighting python type annotations

I have a very simple class that inherits from requests.Session .我有一个非常简单的 class 继承自requests.Session The code currently looks like:代码目前看起来像:

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)

I'm having a hard time making mypy happy with the request method.我很难让mypyrequest方法感到满意。

The first challenge was getting the parameters to validate;第一个挑战是获取要验证的参数; setting url: Union[str, bytes] was necessary to match the type annotation in types-requests .设置url: Union[str, bytes]是匹配types-requests中的类型注释所必需的。 I've just thrown up my hands on getting *args and **kwargs correct, because the only solution appears to be reproducing the individual parameter annotations, but I'm happy to leave that as it.我刚刚举手使*args**kwargs正确,因为唯一的解决方案似乎是重现单个参数注释,但我很高兴将其保留。

With the function signature dealt with, mypy is now complaining about the call to startswith :处理完 function 签名后, mypy现在抱怨对startswith的调用:

example.py:23: error: Argument 1 to "startswith" of "bytes" has incompatible type "str"; example.py:23:错误:“bytes”的“startswith”的参数 1 具有不兼容的类型“str”; expected "Union[bytes, Tuple[bytes, ...]]"预期“联合[字节,元组[字节,...]]”

I can resolve that with an explicit cast :我可以通过明确的cast来解决这个问题:

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

...but that seems like it's just introducing complexity. ...但这似乎只是引入了复杂性。

And then it's unhappy with the call to urllib.parse.urljoin :然后对urllib.parse.urljoin的调用不满意:

example.py:24: error: Value of type variable "AnyStr" of "urljoin" cannot be "Sequence[object]" example.py:24:错误:“urljoin”的类型变量“AnyStr”的值不能是“Sequence[object]”
example.py:24: error: Incompatible types in assignment (expression has type "Sequence[object]", variable has type "Union[str, bytes]") example.py:24:错误:赋值类型不兼容(表达式的类型为“Sequence[object]”,变量的类型为“Union[str, bytes]”)

I'm not really sure what to make of these errors.我不确定如何处理这些错误。

I've fixed things for now by moving the explicit cast to the top of the method:我现在通过将显式强制转换移动到方法的顶部来解决问题:

      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)

But that feels like a hacky workaround.但这感觉像是一个 hacky 解决方法。

So:所以:

  • I think I have the function signature as correct as I care to get it, but are the type annotations on url correct or are they incorrect and resulting in problems?我想我的 function 签名和我想得到的一样正确,但是url上的类型注释是正确的还是不正确并导致问题?

  • What is going on with the errors around urljoin ? urljoin周围的错误是怎么回事?


From the comments, this:从评论中可以看出:

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

Fails with:失败:

example.py:25: error: Argument 1 to "startswith" of "str" has incompatible type "Union[str, bytes]"; example.py:25:错误:“str”的“startswith”的参数 1 具有不兼容的类型“Union[str, bytes]”; expected "Union[str, Tuple[str, ...]]"预期“联合[str,元组[str,...]]”
example.py:25: error: Argument 1 to "startswith" of "bytes" has incompatible type "Union[str, bytes]"; example.py:25:错误:“bytes”的“startswith”的参数 1 具有不兼容的类型“Union [str, bytes]”; expected "Union[bytes, Tuple[bytes, ...]]"预期“联合[字节,元组[字节,...]]”

This resolves the entire issue:这解决了整个问题:

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')

You can't just not deal with bytes if you say you accept it.如果你说你接受它,你不能只是不处理bytes Just raise an exception or do something nicer if bytes get passed.如果bytes被传递,只需引发异常或做一些更好的事情。 Or even just pass if you like living dangerously.或者如果你喜欢危险地生活,甚至只是pass

This code validates just fine in mypy .此代码在mypy中验证得很好。

What's a bit disappointing is that something like this doesn't validate:有点令人失望的是,这样的事情无法验证:

        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)

Even though there is no way url.startswith gets a bytes when it's a str or vice versa, it still won't validate.即使url.startswith无法在str时获取bytes ,反之亦然,它仍然无法验证。 mypy can't validate through the runtime logic, so instead you're stuck doing something like: 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)

Which supports both, but repeats the logic in an ugly fashion.它支持两者,但以丑陋的方式重复逻辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM