[英]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.我很难让mypy
对request
方法感到满意。
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.