From: Tom Christie Date: Fri, 17 May 2019 10:45:02 +0000 (+0100) Subject: Add Cookies model X-Git-Tag: 0.3.1~18^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=24b69236693dc75f5df948c81cf0a9ae1132a0ae;p=thirdparty%2Fhttpx.git Add Cookies model --- diff --git a/httpcore/__init__.py b/httpcore/__init__.py index 9afe7e0d..fddb2cff 100644 --- a/httpcore/__init__.py +++ b/httpcore/__init__.py @@ -5,6 +5,7 @@ from .dispatch.connection import HTTPConnection from .dispatch.connection_pool import ConnectionPool from .exceptions import ( ConnectTimeout, + CookieConflict, DecodingError, InvalidURL, PoolTimeout, diff --git a/httpcore/exceptions.py b/httpcore/exceptions.py index ad040132..ed311730 100644 --- a/httpcore/exceptions.py +++ b/httpcore/exceptions.py @@ -120,3 +120,9 @@ class InvalidURL(Exception): """ URL was missing a hostname, or was not one of HTTP/HTTPS. """ + + +class CookieConflict(Exception): + """ + Attempted to lookup a cookie by name, but multiple cookies existed. + """ diff --git a/httpcore/models.py b/httpcore/models.py index 1ab2fadf..7fc4e65f 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -3,6 +3,7 @@ import cgi import email.message import typing import urllib.request +from collections.abc import MutableMapping from http.cookiejar import Cookie, CookieJar from urllib.parse import parse_qsl, urlencode @@ -17,6 +18,7 @@ from .decoders import ( MultiDecoder, ) from .exceptions import ( + CookieConflict, HttpError, InvalidURL, ResponseClosed, @@ -487,7 +489,7 @@ class Request: self.headers = Headers(headers) if cookies: cookies = Cookies(cookies) - cookies.add_cookie_header(self) + cookies.set_cookie_header(self) if isinstance(data, bytes): self.is_streaming = False @@ -860,8 +862,12 @@ class SyncResponse: return f"" -class Cookies: - def __init__(self, cookies: CookieTypes = None): +class Cookies(MutableMapping): + """ + HTTP Cookies, as a mutable mapping. + """ + + def __init__(self, cookies: CookieTypes = None) -> None: if cookies is None or isinstance(cookies, dict): self.jar = CookieJar() if isinstance(cookies, dict): @@ -882,7 +888,7 @@ class Cookies: self.jar.extract_cookies(urlib_response, urllib_request) # type: ignore - def add_cookie_header(self, request: Request) -> None: + def set_cookie_header(self, request: Request) -> None: """ Sets an appropriate 'Cookie:' HTTP header on the `Request`. """ @@ -891,7 +897,7 @@ class Cookies: def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: """ - Set a cookie. + Set a cookie value by name. May optionally include domain and path. """ kwargs = dict( version=0, @@ -915,11 +921,85 @@ class Cookies: cookie = Cookie(**kwargs) # type: ignore self.jar.set_cookie(cookie) + def get( # type: ignore + self, name: str, default: str = None, domain: str = None, path: str = None + ) -> typing.Optional[str]: + """ + Get a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to retrieve. + """ + value = None + for cookie in self.jar: + if cookie.name == name: + if domain is None or cookie.domain == domain: # type: ignore + if path is None or cookie.path == path: + if value is not None: + message = f"Multiple cookies exist with name={name}" + raise CookieConflict(message) + value = cookie.value + + if value is None: + return default + return value + + def delete(self, name: str, domain: str = None, path: str = None) -> None: + """ + Delete a cookie by name. May optionally include domain and path + in order to specify exactly which cookie to delete. + """ + if domain is not None and path is not None: + return self.jar.clear(domain, path, name) + + remove = [] + for cookie in self.jar: + if cookie.name == name: + if domain is None or cookie.domain == domain: # type: ignore + if path is None or cookie.path == path: + remove.append(cookie) + + for cookie in remove: + self.jar.clear(cookie.domain, cookie.path, cookie.name) # type: ignore + + def clear(self, domain: str = None, path: str = None) -> None: + """ + Delete all cookies. Optionally include a domain and path in + order to only delete a subset of all the cookies. + """ + args = [] + if domain is not None: + args.append(domain) + if path is not None: + assert domain is not None + args.append(path) + self.jar.clear(*args) + + def update(self, cookies: CookieTypes) -> None: # type: ignore + cookies = Cookies(cookies) + for cookie in cookies.jar: + self.jar.set_cookie(cookie) + def __setitem__(self, name: str, value: str) -> None: return self.set(name, value) - def __iter__(self) -> typing.Iterable: - return iter(self.jar) + def __getitem__(self, name: str) -> str: + value = self.get(name) + if value is None: + raise KeyError(name) + return value + + def __delitem__(self, name: str) -> None: + return self.delete(name) + + def __len__(self) -> int: + return len(self.jar) + + def __iter__(self) -> typing.Iterator[str]: + return (cookie.name for cookie in self.jar) + + def __bool__(self) -> bool: + for cookie in self.jar: + return True + return False class _CookieCompatRequest(urllib.request.Request): """ diff --git a/tests/client/test_cookies.py b/tests/client/test_cookie_handling.py similarity index 94% rename from tests/client/test_cookies.py rename to tests/client/test_cookie_handling.py index c698a789..64fd9fc5 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookie_handling.py @@ -103,8 +103,4 @@ def test_get_cookie(): 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" + assert response.cookies["example-name"] == "example-value" diff --git a/tests/models/test_cookies.py b/tests/models/test_cookies.py new file mode 100644 index 00000000..705245d6 --- /dev/null +++ b/tests/models/test_cookies.py @@ -0,0 +1,50 @@ +import pytest + +from httpcore import CookieConflict, Cookies + + +def test_cookies(): + cookies = Cookies({"name": "value"}) + assert cookies["name"] == "value" + assert "name" in cookies + assert len(cookies) == 1 + assert dict(cookies) == {"name": "value"} + assert bool(cookies) is True + + del cookies["name"] + assert "name" not in cookies + assert len(cookies) == 0 + assert dict(cookies) == {} + assert bool(cookies) is False + + +def test_cookies_update(): + cookies = Cookies() + more_cookies = Cookies() + more_cookies.set("name", "value", domain="example.com") + + cookies.update(more_cookies) + assert dict(cookies) == {"name": "value"} + assert cookies.get("name", domain="example.com") == "value" + + +def test_cookies_with_domain(): + cookies = Cookies() + cookies.set("name", "value", domain="example.com") + cookies.set("name", "value", domain="example.org") + + with pytest.raises(CookieConflict): + cookies["name"] + + cookies.clear(domain="example.com") + assert len(cookies) == 1 + + +def test_cookies_with_domain_and_path(): + cookies = Cookies() + cookies.set("name", "value", domain="example.com", path="/subpath/1") + cookies.set("name", "value", domain="example.com", path="/subpath/2") + cookies.clear(domain="example.com", path="/subpath/1") + assert len(cookies) == 1 + cookies.delete("name", domain="example.com", path="/subpath/2") + assert len(cookies) == 0