]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Initial pass at cookie support
authorTom Christie <tom@tomchristie.com>
Thu, 16 May 2019 15:44:33 +0000 (16:44 +0100)
committerTom Christie <tom@tomchristie.com>
Thu, 16 May 2019 15:44:33 +0000 (16:44 +0100)
httpcore/__init__.py
httpcore/client.py
httpcore/models.py
scripts/test
tests/client/test_cookies.py [new file with mode: 0644]
tests/test_timeouts.py

index d30d5365314ef0f890b3eba1680b058f4280650f..c78fcd0bb500d892c3a0646ca45c7acd6d18e696 100644 (file)
@@ -10,7 +10,6 @@ from .exceptions import (
     PoolTimeout,
     ProtocolError,
     ReadTimeout,
-    WriteTimeout,
     RedirectBodyUnavailable,
     RedirectLoop,
     ResponseClosed,
@@ -18,6 +17,7 @@ from .exceptions import (
     StreamConsumed,
     Timeout,
     TooManyRedirects,
+    WriteTimeout,
 )
 from .interfaces import BaseReader, BaseWriter, ConcurrencyBackend, Dispatcher, Protocol
 from .models import URL, Headers, Origin, QueryParams, Request, Response
index 61db5975c7d4149490299db26b87e5f449a4f406..8361fe7325171b047e76e4f23c3158aa7abe4a72 100644 (file)
@@ -1,5 +1,6 @@
 import asyncio
 import typing
+from http.cookiejar import CookieJar
 from types import TracebackType
 
 from .auth import HTTPBasicAuth
@@ -56,6 +57,7 @@ class AsyncClient:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -67,6 +69,7 @@ class AsyncClient:
             url,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -80,6 +83,7 @@ class AsyncClient:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -91,6 +95,7 @@ class AsyncClient:
             url,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -104,6 +109,7 @@ class AsyncClient:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = False,  #  Note: Differs to usual default.
@@ -115,6 +121,7 @@ class AsyncClient:
             url,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -129,6 +136,7 @@ class AsyncClient:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -141,6 +149,7 @@ class AsyncClient:
             data=data,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -155,6 +164,7 @@ class AsyncClient:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -167,6 +177,7 @@ class AsyncClient:
             data=data,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -181,6 +192,7 @@ class AsyncClient:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -193,6 +205,7 @@ class AsyncClient:
             data=data,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -207,6 +220,7 @@ class AsyncClient:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -219,6 +233,7 @@ class AsyncClient:
             data=data,
             query_params=query_params,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -234,6 +249,7 @@ class AsyncClient:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -241,7 +257,12 @@ class AsyncClient:
         timeout: TimeoutConfig = None,
     ) -> Response:
         request = Request(
-            method, url, data=data, query_params=query_params, headers=headers
+            method,
+            url,
+            data=data,
+            query_params=query_params,
+            headers=headers,
+            cookies=cookies,
         )
         self.prepare_request(request)
         response = await self.send(
@@ -453,6 +474,7 @@ class Client:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -460,7 +482,12 @@ class Client:
         timeout: TimeoutConfig = None,
     ) -> SyncResponse:
         request = Request(
-            method, url, data=data, query_params=query_params, headers=headers
+            method,
+            url,
+            data=data,
+            query_params=query_params,
+            headers=headers,
+            cookies=cookies,
         )
         self.prepare_request(request)
         response = self.send(
@@ -479,6 +506,7 @@ class Client:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -489,6 +517,7 @@ class Client:
             "GET",
             url,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -502,6 +531,7 @@ class Client:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -512,6 +542,7 @@ class Client:
             "OPTIONS",
             url,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -525,6 +556,7 @@ class Client:
         *,
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = False,  #  Note: Differs to usual default.
@@ -535,6 +567,7 @@ class Client:
             "HEAD",
             url,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -549,6 +582,7 @@ class Client:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -560,6 +594,7 @@ class Client:
             url,
             data=data,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -574,6 +609,7 @@ class Client:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -585,6 +621,7 @@ class Client:
             url,
             data=data,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -599,6 +636,7 @@ class Client:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -610,6 +648,7 @@ class Client:
             url,
             data=data,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
@@ -624,6 +663,7 @@ class Client:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = None,
         stream: bool = False,
         auth: AuthTypes = None,
         allow_redirects: bool = True,
@@ -635,6 +675,7 @@ class Client:
             url,
             data=data,
             headers=headers,
+            cookies=cookies,
             stream=stream,
             auth=auth,
             allow_redirects=allow_redirects,
index ea26fa4c9c641b480562f5ef8e1b6a4a3b5c95a6..1278f3ded59fc6ab94dcacb8246b05f57d2aa07d 100644 (file)
@@ -1,6 +1,10 @@
 import asyncio
 import cgi
+import email.message
+import http.client
 import typing
+import urllib.request
+from http.cookiejar import CookieJar
 from urllib.parse import parse_qsl, urlencode
 
 import chardet
@@ -475,10 +479,13 @@ class Request:
         data: RequestData = b"",
         query_params: QueryParamTypes = None,
         headers: HeaderTypes = None,
+        cookies: CookieJar = 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)
 
         if isinstance(data, bytes):
             self.is_streaming = False
@@ -536,6 +543,10 @@ 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)
@@ -756,6 +767,20 @@ class Response:
         if message:
             raise HttpError(message)
 
+    @property
+    def cookies(self) -> CookieJar:
+        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
+            )
+        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})>"
 
@@ -835,5 +860,32 @@ class SyncResponse:
     def close(self) -> None:
         return self._loop.run_until_complete(self._response.close())
 
+    @property
+    def cookies(self) -> CookieJar:
+        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
+        )
+        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(http.client.HTTPResponse):
+    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
index 221e1d3d4eb22e54bc241bd0ca4270ad6ecf45ac..b56196728004ca5207fc7e0222480ff9f20917e8 100755 (executable)
@@ -8,5 +8,5 @@ fi
 
 set -x
 
-PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} --cov-report= --cov-fail-under=100 ${@}
-${PREFIX}coverage report -m
+PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov tests --cov ${PACKAGE} ${@}
+${PREFIX}coverage report --show-missing --skip-covered --fail-under=100
diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py
new file mode 100644 (file)
index 0000000..6c6df1a
--- /dev/null
@@ -0,0 +1,83 @@
+import json
+from http.cookiejar import Cookie, CookieJar
+
+import pytest
+
+from httpcore import (
+    URL,
+    Client,
+    Dispatcher,
+    Request,
+    Response,
+    SSLConfig,
+    TimeoutConfig,
+)
+
+
+class MockDispatch(Dispatcher):
+    async def send(
+        self,
+        request: Request,
+        stream: bool = False,
+        ssl: SSLConfig = None,
+        timeout: TimeoutConfig = None,
+    ) -> Response:
+        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'):
+            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():
+    url = "http://example.org/echo_cookies"
+    cookie = create_cookie("example-name", "example-value")
+    cookies = CookieJar()
+    cookies.set_cookie(cookie)
+
+    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"
+
+    with Client(dispatch=MockDispatch()) as client:
+        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"
index d0cb9b215c084f8afa1845ace03863a5389c31a8..f1206fa87cab1d6da78a387be6d5c16a432d2013 100644 (file)
@@ -6,8 +6,8 @@ from httpcore import (
     PoolLimits,
     PoolTimeout,
     ReadTimeout,
-    WriteTimeout,
     TimeoutConfig,
+    WriteTimeout,
 )