Repository: encode/httpx

Issue:
Timeout tests



Discussion:

Resolving patch:
diff --git a/httpcore/__init__.py b/httpcore/__init__.py
--- a/httpcore/__init__.py
+++ b/httpcore/__init__.py
@@ -1,6 +1,13 @@
 from .config import PoolLimits, SSLConfig, TimeoutConfig
 from .datastructures import URL, Request, Response
-from .exceptions import ResponseClosed, StreamConsumed
+from .exceptions import (
+    ConnectTimeout,
+    PoolTimeout,
+    ReadTimeout,
+    ResponseClosed,
+    StreamConsumed,
+    Timeout,
+)
 from .pool import ConnectionPool
 
 __version__ = "0.0.3"
diff --git a/httpcore/compat.py b/httpcore/compat.py
--- a/httpcore/compat.py
+++ b/httpcore/compat.py
@@ -1,4 +1,4 @@
 try:
     import brotli
-except ImportError:
-    brotli = None  # pragma: nocover
+except ImportError:  # pragma: nocover
+    brotli = None
diff --git a/httpcore/pool.py b/httpcore/pool.py
--- a/httpcore/pool.py
+++ b/httpcore/pool.py
@@ -16,18 +16,23 @@
 )
 from .connections import Connection
 from .datastructures import URL, Request, Response
+from .exceptions import PoolTimeout
 
 ConnectionKey = typing.Tuple[str, str, int]  # (scheme, host, port)
 
 
 class ConnectionSemaphore:
-    def __init__(self, max_connections: int = None):
+    def __init__(self, max_connections: int = None, timeout: float = None):
+        self.timeout = timeout
         if max_connections is not None:
             self.semaphore = asyncio.BoundedSemaphore(value=max_connections)
 
     async def acquire(self) -> None:
         if hasattr(self, "semaphore"):
-            await self.semaphore.acquire()
+            try:
+                await asyncio.wait_for(self.semaphore.acquire(), self.timeout)
+            except asyncio.TimeoutError:
+                raise PoolTimeout()
 
     def release(self) -> None:
         if hasattr(self, "semaphore"):
@@ -52,7 +57,7 @@ def __init__(
             {}
         )  # type: typing.Dict[ConnectionKey, typing.List[Connection]]
         self._connection_semaphore = ConnectionSemaphore(
-            max_connections=self.limits.hard_limit
+            max_connections=self.limits.hard_limit, timeout=self.timeout.pool_timeout
         )
 
     async def request(


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,6 +7,25 @@
 
 async def app(scope, receive, send):
     assert scope["type"] == "http"
+    if scope["path"] == "/slow_response":
+        await slow_response(scope, receive, send)
+    else:
+        await hello_world(scope, receive, send)
+
+
+async def hello_world(scope, receive, send):
+    await send(
+        {
+            "type": "http.response.start",
+            "status": 200,
+            "headers": [[b"content-type", b"text/plain"]],
+        }
+    )
+    await send({"type": "http.response.body", "body": b"Hello, world!"})
+
+
+async def slow_response(scope, receive, send):
+    await asyncio.sleep(0.01)
     await send(
         {
             "type": "http.response.start",
@@ -28,5 +47,4 @@ async def server():
         yield server
     finally:
         server.should_exit = True
-        server.force_exit = True
         await task
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
new file mode 100644
--- /dev/null
+++ b/tests/test_timeouts.py
@@ -0,0 +1,26 @@
+import pytest
+
+import httpcore
+
+
+@pytest.mark.asyncio
+async def test_read_timeout(server):
+    timeout = httpcore.TimeoutConfig(read_timeout=0.0001)
+
+    async with httpcore.ConnectionPool(timeout=timeout) as http:
+        with pytest.raises(httpcore.ReadTimeout):
+            await http.request("GET", "http://127.0.0.1:8000/slow_response")
+
+
+@pytest.mark.asyncio
+async def test_pool_timeout(server):
+    timeout = httpcore.TimeoutConfig(pool_timeout=0.0001)
+    limits = httpcore.PoolLimits(hard_limit=1)
+
+    async with httpcore.ConnectionPool(timeout=timeout, limits=limits) as http:
+        response = await http.request("GET", "http://127.0.0.1:8000/", stream=True)
+
+        with pytest.raises(httpcore.PoolTimeout):
+            await http.request("GET", "http://localhost:8000/")
+
+        await response.read()