]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Add Cookies model
authorTom Christie <tom@tomchristie.com>
Fri, 17 May 2019 10:45:02 +0000 (11:45 +0100)
committerTom Christie <tom@tomchristie.com>
Fri, 17 May 2019 10:45:02 +0000 (11:45 +0100)
httpcore/__init__.py
httpcore/exceptions.py
httpcore/models.py
tests/client/test_cookie_handling.py [moved from tests/client/test_cookies.py with 94% similarity]
tests/models/test_cookies.py [new file with mode: 0644]

index 9afe7e0d87b0807a218d172dd8b35bd74d611743..fddb2cff955aa4afa3d18059205fcc7425e3f87f 100644 (file)
@@ -5,6 +5,7 @@ from .dispatch.connection import HTTPConnection
 from .dispatch.connection_pool import ConnectionPool
 from .exceptions import (
     ConnectTimeout,
+    CookieConflict,
     DecodingError,
     InvalidURL,
     PoolTimeout,
index ad04013216557ad01d25ad833de9c8da5a945371..ed3117309f5a65e16dfeb552af50ef538e6b3c83 100644 (file)
@@ -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.
+    """
index 1ab2fadf77c3eb64e02dab25232254d43e83c276..7fc4e65f827185a62ac7a67fc863c0ffe2ecfa86 100644 (file)
@@ -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"<Response({self.status_code}, {self.reason_phrase!r})>"
 
 
-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):
         """
similarity index 94%
rename from tests/client/test_cookies.py
rename to tests/client/test_cookie_handling.py
index c698a78967190b54c15a00b1f148579a090853b4..64fd9fc5f6b6caf1fbdc98e7bee1ea98079781a6 100644 (file)
@@ -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 (file)
index 0000000..705245d
--- /dev/null
@@ -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