import asyncio
import http.server
import json
import socketserver
import threading
import pytest
from eggfetch.compat.httpx import Client, AsyncClient, Timeout
class _TimeoutHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/get":
body = json.dumps({"method": "GET"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
elif self.path == "/echo-headers":
headers = {k: v for k, v in self.headers.items()}
body = json.dumps({"headers": headers}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
self.rfile.read(content_length) if content_length else b""
body = json.dumps({"method": "POST"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
def log_message(self, format, *args):
pass
class _ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
@pytest.fixture(scope="module")
def server():
srv = _ThreadedHTTPServer(("127.0.0.1", 0), _TimeoutHandler)
port = srv.server_address[1]
t = threading.Thread(target=srv.serve_forever, daemon=True)
t.start()
yield f"http://127.0.0.1:{port}"
srv.shutdown()
class TestClientTimeoutDefaults:
def test_default_timeout_is_timeout_object(self):
client = Client()
assert isinstance(client.timeout, Timeout)
client.close()
def test_default_timeout_value(self):
client = Client()
assert client.timeout.total == 5.0
assert client.timeout.connect == 5.0
assert client.timeout.read == 5.0
assert client.timeout.write == 5.0
assert client.timeout.pool == 5.0
client.close()
def test_scalar_timeout_sets_all_phases(self):
client = Client(timeout=10.0)
assert client.timeout.total == 10.0
assert client.timeout.connect == 10.0
assert client.timeout.read == 10.0
assert client.timeout.write == 10.0
assert client.timeout.pool == 10.0
client.close()
def test_timeout_object_preserved(self):
t = Timeout(timeout=5.0, connect=1.0, read=2.0, write=3.0, pool=4.0)
client = Client(timeout=t)
assert client.timeout.total == 5.0
assert client.timeout.connect == 1.0
assert client.timeout.read == 2.0
assert client.timeout.write == 3.0
assert client.timeout.pool == 4.0
client.close()
class TestPerRequestTimeoutNone:
def test_timeout_none_on_send(self, server):
with Client(timeout=5.0) as client:
req = client.build_request("GET", f"{server}/get")
resp = client.send(req, timeout=None)
assert resp.status_code == 200
def test_timeout_none_on_request_method(self, server):
with Client(timeout=5.0) as client:
resp = client.request("GET", f"{server}/get", timeout=None)
assert resp.status_code == 200
def test_timeout_none_on_get(self, server):
with Client(timeout=5.0) as client:
resp = client.get(f"{server}/get", timeout=None)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_timeout_none_on_send(self, server):
async with AsyncClient(timeout=5.0) as client:
req = client.build_request("GET", f"{server}/get")
resp = await client.send(req, timeout=None)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_timeout_none_on_request(self, server):
async with AsyncClient(timeout=5.0) as client:
resp = await client.request("GET", f"{server}/get", timeout=None)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_timeout_none_on_get(self, server):
async with AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{server}/get", timeout=None)
assert resp.status_code == 200
class TestPerRequestTimeoutOverride:
def test_scalar_timeout_override(self, server):
with Client(timeout=5.0) as client:
resp = client.get(f"{server}/get", timeout=10.0)
assert resp.status_code == 200
def test_timeout_object_override(self, server):
with Client(timeout=5.0) as client:
resp = client.get(f"{server}/get", timeout=Timeout(10.0))
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_scalar_timeout_override(self, server):
async with AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{server}/get", timeout=10.0)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_timeout_object_override(self, server):
async with AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{server}/get", timeout=Timeout(10.0))
assert resp.status_code == 200
class TestPhaseSpecificTimeout:
def test_connect_phase_timeout(self, server):
t = Timeout(timeout=5.0, connect=2.0)
with Client(timeout=t) as client:
resp = client.get(f"{server}/get")
assert resp.status_code == 200
def test_read_phase_timeout(self, server):
t = Timeout(timeout=5.0, read=3.0)
with Client(timeout=t) as client:
resp = client.get(f"{server}/get")
assert resp.status_code == 200
def test_write_phase_timeout(self, server):
t = Timeout(timeout=5.0, write=3.0)
with Client(timeout=t) as client:
resp = client.post(f"{server}/get", content=b"test")
assert resp.status_code == 200
def test_pool_phase_timeout(self, server):
t = Timeout(timeout=5.0, pool=2.0)
with Client(timeout=t) as client:
resp = client.get(f"{server}/get")
assert resp.status_code == 200
def test_all_phases_independent(self, server):
t = Timeout(timeout=10.0, connect=1.0, read=2.0, write=3.0, pool=4.0)
with Client(timeout=t) as client:
resp = client.get(f"{server}/get")
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_async_phase_specific(self, server):
t = Timeout(timeout=5.0, connect=2.0, read=3.0)
async with AsyncClient(timeout=t) as client:
resp = await client.get(f"{server}/get")
assert resp.status_code == 200
class TestTimeoutValidation:
def test_negative_timeout_raises(self):
with pytest.raises(ValueError, match="positive"):
Timeout(timeout=-1.0)
def test_negative_phase_raises(self):
with pytest.raises(ValueError, match="positive"):
Timeout(timeout=5.0, connect=-1.0)
def test_nan_timeout_raises(self):
with pytest.raises(ValueError, match="NaN|finite"):
Timeout(timeout=float("nan"))
def test_non_numeric_timeout_raises(self):
with pytest.raises(TypeError, match="None or a number"):
Timeout(timeout="invalid")
def test_zero_timeout_is_valid(self):
t = Timeout(timeout=0.0)
assert t.total == 0.0
def test_none_phase_not_valid_in_constructor(self):
t = Timeout(timeout=5.0)
assert t.connect == 5.0
assert t.read == 5.0
assert t.write == 5.0
assert t.pool == 5.0
class TestTimeoutObject:
def test_as_dict(self):
t = Timeout(timeout=5.0, connect=1.0, read=2.0, write=3.0, pool=4.0)
d = t.as_dict
assert d == {"connect": 1.0, "read": 2.0, "write": 3.0, "pool": 4.0}
def test_equality(self):
a = Timeout(timeout=5.0, connect=1.0)
b = Timeout(timeout=5.0, connect=1.0)
assert a == b
def test_inequality(self):
a = Timeout(timeout=5.0)
b = Timeout(timeout=10.0)
assert a != b
def test_repr_uses_total_when_unified(self):
t = Timeout(timeout=5.0)
assert repr(t) == "Timeout(timeout=5.0)"
def test_repr_explicit_phases(self):
t = Timeout(timeout=5.0, connect=1.0)
r = repr(t)
assert "connect=1.0" in r
def test_copy(self):
import copy
t = Timeout(timeout=5.0, connect=1.0)
t2 = copy.copy(t)
assert t == t2
assert t is not t2