From: Tom Christie Date: Fri, 17 May 2019 09:00:56 +0000 (+0100) Subject: Add Cookies model X-Git-Tag: 0.3.1~18^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1e0409c105c3c2ab34f145002493e8fdd723a702;p=thirdparty%2Fhttpx.git Add Cookies model --- diff --git a/httpcore/__init__.py b/httpcore/__init__.py index c78fcd0b..9afe7e0d 100644 --- a/httpcore/__init__.py +++ b/httpcore/__init__.py @@ -20,7 +20,7 @@ from .exceptions import ( WriteTimeout, ) from .interfaces import BaseReader, BaseWriter, ConcurrencyBackend, Dispatcher, Protocol -from .models import URL, Headers, Origin, QueryParams, Request, Response +from .models import URL, Cookies, Headers, Origin, QueryParams, Request, Response from .status_codes import codes __version__ = "0.3.0" diff --git a/httpcore/models.py b/httpcore/models.py index 1278f3de..1ab2fadf 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -1,10 +1,9 @@ import asyncio import cgi import email.message -import http.client import typing import urllib.request -from http.cookiejar import CookieJar +from http.cookiejar import Cookie, CookieJar from urllib.parse import parse_qsl, urlencode import chardet @@ -47,6 +46,8 @@ HeaderTypes = typing.Union[ typing.List[typing.Tuple[typing.AnyStr, typing.AnyStr]], ] +CookieTypes = typing.Union["Cookies", CookieJar, typing.Dict[str, str]] + AuthTypes = typing.Union[ typing.Tuple[typing.Union[str, bytes], typing.Union[str, bytes]], typing.Callable[["Request"], "Request"], @@ -479,13 +480,14 @@ class Request: data: RequestData = b"", query_params: QueryParamTypes = None, headers: HeaderTypes = None, - cookies: CookieJar = None, + cookies: CookieTypes = 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) + cookies = Cookies(cookies) + cookies.add_cookie_header(self) if isinstance(data, bytes): self.is_streaming = False @@ -543,10 +545,6 @@ 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) @@ -768,19 +766,13 @@ class Response: raise HttpError(message) @property - def cookies(self) -> CookieJar: + def cookies(self) -> "Cookies": 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 - ) + self._cookies = Cookies() + self._cookies.extract_cookies(self) return self._cookies - @property - def cookie_compat(self) -> "CookieCompatResponse": - return CookieCompatResponse(self) - def __repr__(self) -> str: return f"" @@ -861,31 +853,103 @@ class SyncResponse: return self._loop.run_until_complete(self._response.close()) @property - def cookies(self) -> CookieJar: + def cookies(self) -> "Cookies": 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 +class Cookies: + def __init__(self, cookies: CookieTypes = None): + if cookies is None or isinstance(cookies, dict): + self.jar = CookieJar() + if isinstance(cookies, dict): + for key, value in cookies.items(): + self.set(key, value) + elif isinstance(cookies, Cookies): + self.jar = cookies.jar + else: + self.jar = cookies + + def extract_cookies(self, response: Response) -> None: + """ + Loads any cookies based on the response `Set-Cookie` headers. + """ + assert response.request is not None + urlib_response = self._CookieCompatResponse(response) + urllib_request = self._CookieCompatRequest(response.request) + + self.jar.extract_cookies(urlib_response, urllib_request) # type: ignore + + def add_cookie_header(self, request: Request) -> None: + """ + Sets an appropriate 'Cookie:' HTTP header on the `Request`. + """ + urllib_request = self._CookieCompatRequest(request) + self.jar.add_cookie_header(urllib_request) + + def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: + """ + Set a cookie. + """ + kwargs = dict( + version=0, + name=name, + value=value, + port=None, + port_specified=False, + domain=domain, + domain_specified=bool(domain), + domain_initial_dot=domain.startswith("."), + path=path, + path_specified=bool(path), + secure=False, + expires=None, + discard=True, + comment=None, + comment_url=None, + rest={"HttpOnly": None}, + rfc2109=False, ) - self.request = request + cookie = Cookie(**kwargs) # type: ignore + self.jar.set_cookie(cookie) - def add_unredirected_header(self, key: str, value: str) -> None: - super().add_unredirected_header(key, value) - self.request.headers[key] = value + def __setitem__(self, name: str, value: str) -> None: + return self.set(name, value) + def __iter__(self) -> typing.Iterable: + return iter(self.jar) + + class _CookieCompatRequest(urllib.request.Request): + """ + Wraps a `Request` instance up in a compatability interface suitable + for use with `CookieJar` operations. + """ + + 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: + """ + Wraps a `Request` instance up in a compatability interface suitable + for use with `CookieJar` operations. + """ -class CookieCompatResponse(http.client.HTTPResponse): - def __init__(self, response: Response): - self.response = response + 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 + 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 b5619672..63ad08a6 100755 --- a/scripts/test +++ b/scripts/test @@ -8,5 +8,5 @@ fi set -x -PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} ${@} -${PREFIX}coverage report --show-missing --skip-covered --fail-under=100 +PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} --cov-report= ${@} +${PREFIX}coverage report --show-missing --fail-under=100 diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py index 6c6df1a0..c698a789 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookies.py @@ -6,6 +6,7 @@ import pytest from httpcore import ( URL, Client, + Cookies, Dispatcher, Request, Response, @@ -22,44 +23,54 @@ class MockDispatch(Dispatcher): ssl: SSLConfig = None, timeout: TimeoutConfig = None, ) -> Response: - if request.url.path.startswith('/echo_cookies'): + 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'): + 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(): + """ + Send a request including a cookie. + """ + url = "http://example.org/echo_cookies" + cookies = {"example-name": "example-value"} + 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_set_cookie_with_cookiejar(): + """ + Send a request including a cookie, using a `CookieJar` instance. + """ -def test_set_cookie(): url = "http://example.org/echo_cookies" - cookie = create_cookie("example-name", "example-value") cookies = CookieJar() + cookie = Cookie( + version=0, + name="example-name", + value="example-value", + port=None, + port_specified=False, + domain="", + domain_specified=False, + domain_initial_dot=False, + path="/", + path_specified=True, + secure=False, + expires=None, + discard=True, + comment=None, + comment_url=None, + rest={"HttpOnly": None}, + rfc2109=False, + ) cookies.set_cookie(cookie) with Client(dispatch=MockDispatch()) as client: @@ -69,6 +80,22 @@ def test_set_cookie(): assert json.loads(response.text) == {"cookies": "example-name=example-value"} +def test_set_cookie_with_cookies_model(): + """ + Send a request including a cookie, using a `Cookies` instance. + """ + + url = "http://example.org/echo_cookies" + cookies = Cookies() + cookies["example-name"] = "example-value" + + 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"