diff --git a/CHANGES/9873.bugfix.rst b/CHANGES/9873.bugfix.rst new file mode 100644 index 00000000000..90f708fa879 --- /dev/null +++ b/CHANGES/9873.bugfix.rst @@ -0,0 +1 @@ +Added a backward compatibility layer to `~aiohttp.RequestInfo` to allow creating these objects without a `real_url` -- by :user:`bdraco`. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 8e7ebbcbebe..8f7ed23f286 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -43,6 +43,7 @@ from .formdata import FormData from .hdrs import CONTENT_TYPE from .helpers import ( + _SENTINEL, BaseTimerContext, BasicAuth, HeadersMixin, @@ -104,13 +105,31 @@ class ContentDisposition: filename: Optional[str] -class RequestInfo(NamedTuple): +class _RequestInfo(NamedTuple): url: URL method: str headers: "CIMultiDictProxy[str]" real_url: URL +class RequestInfo(_RequestInfo): + + def __new__( + cls, + url: URL, + method: str, + headers: "CIMultiDictProxy[str]", + real_url: URL = _SENTINEL, # type: ignore[assignment] + ) -> "RequestInfo": + """Create a new RequestInfo instance. + + For backwards compatibility, the real_url parameter is optional. + """ + return tuple.__new__( + cls, (url, method, headers, url if real_url is _SENTINEL else real_url) + ) + + class Fingerprint: HASHFUNC_BY_DIGESTLEN = { 16: md5, @@ -326,7 +345,9 @@ def port(self) -> Optional[int]: def request_info(self) -> RequestInfo: headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers) # These are created on every request, so we use a NamedTuple - # for performance reasons. + # for performance reasons. We don't use the RequestInfo.__new__ + # method because it has a different signature which is provided + # for backwards compatibility only. return tuple.__new__( RequestInfo, (self.url, self.method, headers, self.original_url) ) diff --git a/tests/test_client_request.py b/tests/test_client_request.py index d20374ee163..e2ab308cec7 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -1544,3 +1544,46 @@ async def test_connection_key_without_proxy() -> None: ) assert req.connection_key.proxy_headers_hash is None await req.close() + + +def test_request_info_back_compat() -> None: + """Test RequestInfo can be created without real_url.""" + url = URL("http://example.com") + other_url = URL("http://example.org") + assert ( + aiohttp.RequestInfo( + url=url, method="GET", headers=CIMultiDictProxy(CIMultiDict()) + ).real_url + is url + ) + assert ( + aiohttp.RequestInfo(url, "GET", CIMultiDictProxy(CIMultiDict())).real_url is url + ) + assert ( + aiohttp.RequestInfo( + url, "GET", CIMultiDictProxy(CIMultiDict()), real_url=url + ).real_url + is url + ) + assert ( + aiohttp.RequestInfo( + url, "GET", CIMultiDictProxy(CIMultiDict()), real_url=other_url + ).real_url + is other_url + ) + + +def test_request_info_tuple_new() -> None: + """Test RequestInfo must be created with real_url using tuple.__new__.""" + url = URL("http://example.com") + with pytest.raises(IndexError): + tuple.__new__( + aiohttp.RequestInfo, (url, "GET", CIMultiDictProxy(CIMultiDict())) + ).real_url + + assert ( + tuple.__new__( + aiohttp.RequestInfo, (url, "GET", CIMultiDictProxy(CIMultiDict()), url) + ).real_url + is url + )