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
MultiDecoder,
)
from .exceptions import (
+ CookieConflict,
HttpError,
InvalidURL,
ResponseClosed,
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
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):
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`.
"""
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,
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):
"""
--- /dev/null
+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