]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Add Cookies model
authorTom Christie <tom@tomchristie.com>
Fri, 17 May 2019 09:00:56 +0000 (10:00 +0100)
committerTom Christie <tom@tomchristie.com>
Fri, 17 May 2019 09:00:56 +0000 (10:00 +0100)
httpcore/__init__.py
httpcore/models.py
scripts/test
tests/client/test_cookies.py

index c78fcd0bb500d892c3a0646ca45c7acd6d18e696..9afe7e0d87b0807a218d172dd8b35bd74d611743 100644 (file)
@@ -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"
index 1278f3ded59fc6ab94dcacb8246b05f57d2aa07d..1ab2fadf77c3eb64e02dab25232254d43e83c276 100644 (file)
@@ -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"<Response({self.status_code}, {self.reason_phrase!r})>"
 
@@ -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"<Response({self.status_code}, {self.reason_phrase!r})>"
 
 
-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
index b56196728004ca5207fc7e0222480ff9f20917e8..63ad08a6a2e75aa6d0b0973cecbab0ab95163128 100755 (executable)
@@ -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
index 6c6df1a0bbb14ba1059ca8ec3405c2222e03e0c5..c698a78967190b54c15a00b1f148579a090853b4 100644 (file)
@@ -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"