From: Tom Christie Date: Thu, 16 May 2019 15:44:33 +0000 (+0100) Subject: Initial pass at cookie support X-Git-Tag: 0.3.1~18^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4a3325f87b01d768cd33b4ed79f81c1f8869f33b;p=thirdparty%2Fhttpx.git Initial pass at cookie support --- diff --git a/httpcore/__init__.py b/httpcore/__init__.py index d30d5365..c78fcd0b 100644 --- a/httpcore/__init__.py +++ b/httpcore/__init__.py @@ -10,7 +10,6 @@ from .exceptions import ( PoolTimeout, ProtocolError, ReadTimeout, - WriteTimeout, RedirectBodyUnavailable, RedirectLoop, ResponseClosed, @@ -18,6 +17,7 @@ from .exceptions import ( StreamConsumed, Timeout, TooManyRedirects, + WriteTimeout, ) from .interfaces import BaseReader, BaseWriter, ConcurrencyBackend, Dispatcher, Protocol from .models import URL, Headers, Origin, QueryParams, Request, Response diff --git a/httpcore/client.py b/httpcore/client.py index 61db5975..8361fe73 100644 --- a/httpcore/client.py +++ b/httpcore/client.py @@ -1,5 +1,6 @@ import asyncio import typing +from http.cookiejar import CookieJar from types import TracebackType from .auth import HTTPBasicAuth @@ -56,6 +57,7 @@ class AsyncClient: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -67,6 +69,7 @@ class AsyncClient: url, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -80,6 +83,7 @@ class AsyncClient: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -91,6 +95,7 @@ class AsyncClient: url, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -104,6 +109,7 @@ class AsyncClient: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = False, #  Note: Differs to usual default. @@ -115,6 +121,7 @@ class AsyncClient: url, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -129,6 +136,7 @@ class AsyncClient: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -141,6 +149,7 @@ class AsyncClient: data=data, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -155,6 +164,7 @@ class AsyncClient: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -167,6 +177,7 @@ class AsyncClient: data=data, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -181,6 +192,7 @@ class AsyncClient: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -193,6 +205,7 @@ class AsyncClient: data=data, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -207,6 +220,7 @@ class AsyncClient: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -219,6 +233,7 @@ class AsyncClient: data=data, query_params=query_params, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -234,6 +249,7 @@ class AsyncClient: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -241,7 +257,12 @@ class AsyncClient: timeout: TimeoutConfig = None, ) -> Response: request = Request( - method, url, data=data, query_params=query_params, headers=headers + method, + url, + data=data, + query_params=query_params, + headers=headers, + cookies=cookies, ) self.prepare_request(request) response = await self.send( @@ -453,6 +474,7 @@ class Client: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -460,7 +482,12 @@ class Client: timeout: TimeoutConfig = None, ) -> SyncResponse: request = Request( - method, url, data=data, query_params=query_params, headers=headers + method, + url, + data=data, + query_params=query_params, + headers=headers, + cookies=cookies, ) self.prepare_request(request) response = self.send( @@ -479,6 +506,7 @@ class Client: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -489,6 +517,7 @@ class Client: "GET", url, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -502,6 +531,7 @@ class Client: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -512,6 +542,7 @@ class Client: "OPTIONS", url, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -525,6 +556,7 @@ class Client: *, query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = False, #  Note: Differs to usual default. @@ -535,6 +567,7 @@ class Client: "HEAD", url, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -549,6 +582,7 @@ class Client: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -560,6 +594,7 @@ class Client: url, data=data, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -574,6 +609,7 @@ class Client: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -585,6 +621,7 @@ class Client: url, data=data, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -599,6 +636,7 @@ class Client: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -610,6 +648,7 @@ class Client: url, data=data, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, @@ -624,6 +663,7 @@ class Client: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, stream: bool = False, auth: AuthTypes = None, allow_redirects: bool = True, @@ -635,6 +675,7 @@ class Client: url, data=data, headers=headers, + cookies=cookies, stream=stream, auth=auth, allow_redirects=allow_redirects, diff --git a/httpcore/models.py b/httpcore/models.py index ea26fa4c..1278f3de 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -1,6 +1,10 @@ import asyncio import cgi +import email.message +import http.client import typing +import urllib.request +from http.cookiejar import CookieJar from urllib.parse import parse_qsl, urlencode import chardet @@ -475,10 +479,13 @@ class Request: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, + cookies: CookieJar = None, ): self.method = method.upper() self.url = URL(url, query_params=query_params) self.headers = Headers(headers) + if cookies: + cookies.add_cookie_header(self.cookie_compat) if isinstance(data, bytes): self.is_streaming = False @@ -536,6 +543,10 @@ class Request: for item in reversed(auto_headers): self.headers.raw.insert(0, item) + @property + def cookie_compat(self) -> "CookieCompatRequest": + return CookieCompatRequest(self) + def __repr__(self) -> str: class_name = self.__class__.__name__ url = str(self.url) @@ -756,6 +767,20 @@ class Response: if message: raise HttpError(message) + @property + def cookies(self) -> CookieJar: + if not hasattr(self, "_cookies"): + assert self.request is not None + self._cookies = CookieJar() + self._cookies.extract_cookies( + self.cookie_compat, self.request.cookie_compat + ) + return self._cookies + + @property + def cookie_compat(self) -> "CookieCompatResponse": + return CookieCompatResponse(self) + def __repr__(self) -> str: return f"" @@ -835,5 +860,32 @@ class SyncResponse: def close(self) -> None: return self._loop.run_until_complete(self._response.close()) + @property + def cookies(self) -> CookieJar: + return self._response.cookies + def __repr__(self) -> str: return f"" + + +class CookieCompatRequest(urllib.request.Request): + def __init__(self, request: Request) -> None: + super().__init__( + url=str(request.url), headers=dict(request.headers), method=request.method + ) + self.request = request + + def add_unredirected_header(self, key: str, value: str) -> None: + super().add_unredirected_header(key, value) + self.request.headers[key] = value + + +class CookieCompatResponse(http.client.HTTPResponse): + def __init__(self, response: Response): + self.response = response + + def info(self) -> email.message.Message: + info = email.message.Message() + for key, value in self.response.headers.items(): + info[key] = value + return info diff --git a/scripts/test b/scripts/test index 221e1d3d..b5619672 100755 --- a/scripts/test +++ b/scripts/test @@ -8,5 +8,5 @@ fi set -x -PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} --cov-report= --cov-fail-under=100 ${@} -${PREFIX}coverage report -m +PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} ${@} +${PREFIX}coverage report --show-missing --skip-covered --fail-under=100 diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py new file mode 100644 index 00000000..6c6df1a0 --- /dev/null +++ b/tests/client/test_cookies.py @@ -0,0 +1,83 @@ +import json +from http.cookiejar import Cookie, CookieJar + +import pytest + +from httpcore import ( + URL, + Client, + Dispatcher, + Request, + Response, + SSLConfig, + TimeoutConfig, +) + + +class MockDispatch(Dispatcher): + async def send( + self, + request: Request, + stream: bool = False, + ssl: SSLConfig = None, + timeout: TimeoutConfig = None, + ) -> Response: + if request.url.path.startswith('/echo_cookies'): + body = json.dumps({"cookies": request.headers.get("Cookie")}).encode() + return Response(200, content=body, request=request) + elif request.url.path.startswith('/set_cookie'): + headers = {"set-cookie": "example-name=example-value"} + return Response(200, headers=headers, request=request) + + +def create_cookie(name, value, **kwargs): + result = { + "version": 0, + "name": name, + "value": value, + "port": None, + "domain": "", + "path": "/", + "secure": False, + "expires": None, + "discard": True, + "comment": None, + "comment_url": None, + "rest": {"HttpOnly": None}, + "rfc2109": False, + } + + result.update(kwargs) + result["port_specified"] = bool(result["port"]) + result["domain_specified"] = bool(result["domain"]) + result["domain_initial_dot"] = result["domain"].startswith(".") + result["path_specified"] = bool(result["path"]) + + return Cookie(**result) + + +def test_set_cookie(): + url = "http://example.org/echo_cookies" + cookie = create_cookie("example-name", "example-value") + cookies = CookieJar() + cookies.set_cookie(cookie) + + with Client(dispatch=MockDispatch()) as client: + response = client.get(url, cookies=cookies) + + assert response.status_code == 200 + assert json.loads(response.text) == {"cookies": "example-name=example-value"} + + +def test_get_cookie(): + url = "http://example.org/set_cookie" + + with Client(dispatch=MockDispatch()) as client: + response = client.get(url) + + assert response.status_code == 200 + cookies = list(response.cookies) + assert len(cookies) == 1 + cookie = cookies[0] + assert cookie.name == "example-name" + assert cookie.value == "example-value" diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py index d0cb9b21..f1206fa8 100644 --- a/tests/test_timeouts.py +++ b/tests/test_timeouts.py @@ -6,8 +6,8 @@ from httpcore import ( PoolLimits, PoolTimeout, ReadTimeout, - WriteTimeout, TimeoutConfig, + WriteTimeout, )