Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions httpx/concurrency/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,10 @@ def semaphore(self) -> typing.Optional[asyncio.BoundedSemaphore]:
self._semaphore = asyncio.BoundedSemaphore(value=max_connections)
return self._semaphore

async def acquire(self) -> None:
async def acquire(self, timeout: float = None) -> None:
if self.semaphore is None:
return

timeout = self.pool_limits.pool_timeout
try:
await asyncio.wait_for(self.semaphore.acquire(), timeout)
except asyncio.TimeoutError:
Expand Down
2 changes: 1 addition & 1 deletion httpx/concurrency/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class BasePoolSemaphore:
Abstracts away any asyncio-specific interfaces.
"""

async def acquire(self) -> None:
async def acquire(self, timeout: float = None) -> None:
raise NotImplementedError() # pragma: no cover

def release(self) -> None:
Expand Down
4 changes: 2 additions & 2 deletions httpx/concurrency/trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ def semaphore(self) -> typing.Optional[trio.Semaphore]:
)
return self._semaphore

async def acquire(self) -> None:
async def acquire(self, timeout: float = None) -> None:
if self.semaphore is None:
return

timeout = _or_inf(self.pool_limits.pool_timeout)
timeout = _or_inf(timeout)

with trio.move_on_after(timeout):
await self.semaphore.acquire()
Expand Down
41 changes: 27 additions & 14 deletions httpx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

CertTypes = typing.Union[str, typing.Tuple[str, str], typing.Tuple[str, str, str]]
VerifyTypes = typing.Union[str, bool, ssl.SSLContext]
TimeoutTypes = typing.Union[float, typing.Tuple[float, float, float], "TimeoutConfig"]
TimeoutTypes = typing.Union[
float, typing.Tuple[float, float, float, float], "TimeoutConfig"
]
HTTPVersionTypes = typing.Union[
str, typing.List[str], typing.Tuple[str], "HTTPVersionConfig"
]
Expand Down Expand Up @@ -227,44 +229,62 @@ def __init__(
connect_timeout: float = None,
read_timeout: float = None,
write_timeout: float = None,
pool_timeout: float = None,
):
if timeout is None:
self.connect_timeout = connect_timeout
self.read_timeout = read_timeout
self.write_timeout = write_timeout
self.pool_timeout = pool_timeout
else:
# Specified as a single timeout value
assert connect_timeout is None
assert read_timeout is None
assert write_timeout is None
assert pool_timeout is None
if isinstance(timeout, TimeoutConfig):
self.connect_timeout = timeout.connect_timeout
self.read_timeout = timeout.read_timeout
self.write_timeout = timeout.write_timeout
self.pool_timeout = timeout.pool_timeout
elif isinstance(timeout, tuple):
self.connect_timeout = timeout[0]
self.read_timeout = timeout[1]
self.write_timeout = timeout[2]
self.write_timeout = None if len(timeout) < 3 else timeout[2]
self.pool_timeout = None if len(timeout) < 4 else timeout[3]
else:
self.connect_timeout = timeout
self.read_timeout = timeout
self.write_timeout = timeout
self.pool_timeout = timeout

def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.connect_timeout == other.connect_timeout
and self.read_timeout == other.read_timeout
and self.write_timeout == other.write_timeout
and self.pool_timeout == other.pool_timeout
)

def __repr__(self) -> str:
class_name = self.__class__.__name__
if len({self.connect_timeout, self.read_timeout, self.write_timeout}) == 1:
if (
len(
{
self.connect_timeout,
self.read_timeout,
self.write_timeout,
self.pool_timeout,
}
)
== 1
):
return f"{class_name}(timeout={self.connect_timeout})"
return (
f"{class_name}(connect_timeout={self.connect_timeout}, "
f"read_timeout={self.read_timeout}, write_timeout={self.write_timeout})"
f"read_timeout={self.read_timeout}, write_timeout={self.write_timeout}, "
f"pool_timeout={self.pool_timeout})"
)


Expand Down Expand Up @@ -320,34 +340,27 @@ class PoolLimits:
"""

def __init__(
self,
*,
soft_limit: int = None,
hard_limit: int = None,
pool_timeout: float = None,
self, *, soft_limit: int = None, hard_limit: int = None,
):
self.soft_limit = soft_limit
self.hard_limit = hard_limit
self.pool_timeout = pool_timeout

def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.soft_limit == other.soft_limit
and self.hard_limit == other.hard_limit
and self.pool_timeout == other.pool_timeout
)

def __repr__(self) -> str:
class_name = self.__class__.__name__
return (
f"{class_name}(soft_limit={self.soft_limit}, "
f"hard_limit={self.hard_limit}, pool_timeout={self.pool_timeout})"
f"{class_name}(soft_limit={self.soft_limit}, hard_limit={self.hard_limit})"
)


DEFAULT_SSL_CONFIG = SSLConfig(cert=None, verify=True)
DEFAULT_TIMEOUT_CONFIG = TimeoutConfig(timeout=5.0)
DEFAULT_POOL_LIMITS = PoolLimits(soft_limit=10, hard_limit=100, pool_timeout=5.0)
DEFAULT_POOL_LIMITS = PoolLimits(soft_limit=10, hard_limit=100)
DEFAULT_CA_BUNDLE_PATH = Path(certifi.where())
DEFAULT_MAX_REDIRECTS = 20
18 changes: 14 additions & 4 deletions httpx/dispatch/connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CertTypes,
HTTPVersionTypes,
PoolLimits,
TimeoutConfig,
TimeoutTypes,
VerifyTypes,
)
Expand Down Expand Up @@ -93,7 +94,7 @@ def __init__(
):
self.verify = verify
self.cert = cert
self.timeout = timeout
self.timeout = TimeoutConfig(timeout)
self.pool_limits = pool_limits
self.http_versions = http_versions
self.is_closed = False
Expand All @@ -117,7 +118,9 @@ async def send(
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> Response:
connection = await self.acquire_connection(origin=request.url.origin)
connection = await self.acquire_connection(
origin=request.url.origin, timeout=timeout
)
try:
response = await connection.send(
request, verify=verify, cert=cert, timeout=timeout
Expand All @@ -129,12 +132,19 @@ async def send(

return response

async def acquire_connection(self, origin: Origin) -> HTTPConnection:
async def acquire_connection(
self, origin: Origin, timeout: TimeoutTypes = None
) -> HTTPConnection:
logger.trace(f"acquire_connection origin={origin!r}")
connection = self.pop_connection(origin)

if connection is None:
await self.max_connections.acquire()
if timeout is None:
pool_timeout = self.timeout.pool_timeout
else:
pool_timeout = TimeoutConfig(timeout).pool_timeout

await self.max_connections.acquire(timeout=pool_timeout)
connection = HTTPConnection(
origin,
verify=self.verify,
Expand Down
6 changes: 4 additions & 2 deletions httpx/dispatch/proxy_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ def build_auth_header(self, username: str, password: str) -> str:
token = b64encode(b":".join(userpass)).decode().strip()
return f"Basic {token}"

async def acquire_connection(self, origin: Origin) -> HTTPConnection:
async def acquire_connection(
self, origin: Origin, timeout: TimeoutTypes = None
) -> HTTPConnection:
if self.should_forward_origin(origin):
logger.trace(
f"forward_connection proxy_url={self.proxy_url!r} origin={origin!r}"
)
return await super().acquire_connection(self.proxy_url.origin)
return await super().acquire_connection(self.proxy_url.origin, timeout)
else:
logger.trace(
f"tunnel_connection proxy_url={self.proxy_url!r} origin={origin!r}"
Expand Down
13 changes: 6 additions & 7 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ def test_empty_http_version():

def test_limits_repr():
limits = httpx.PoolLimits(hard_limit=100)
assert (
repr(limits) == "PoolLimits(soft_limit=None, hard_limit=100, pool_timeout=None)"
)
assert repr(limits) == "PoolLimits(soft_limit=None, hard_limit=100)"


def test_ssl_eq():
Expand All @@ -185,6 +183,7 @@ def test_timeout_from_nothing():
assert timeout.connect_timeout is None
assert timeout.read_timeout is None
assert timeout.write_timeout is None
assert timeout.pool_timeout is None


def test_timeout_from_none():
Expand All @@ -198,7 +197,7 @@ def test_timeout_from_one_none_value():


def test_timeout_from_tuple():
timeout = httpx.TimeoutConfig(timeout=(5.0, 5.0, 5.0))
timeout = httpx.TimeoutConfig(timeout=(5.0, 5.0, 5.0, 5.0))
assert timeout == httpx.TimeoutConfig(timeout=5.0)


Expand All @@ -212,9 +211,9 @@ def test_timeout_repr():
assert repr(timeout) == "TimeoutConfig(timeout=5.0)"

timeout = httpx.TimeoutConfig(read_timeout=5.0)
assert (
repr(timeout)
== "TimeoutConfig(connect_timeout=None, read_timeout=5.0, write_timeout=None)"
assert repr(timeout) == (
"TimeoutConfig(connect_timeout=None, read_timeout=5.0, "
"write_timeout=None, pool_timeout=None)"
)


Expand Down
7 changes: 5 additions & 2 deletions tests/test_timeouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ async def test_connect_timeout(server, backend):


async def test_pool_timeout(server, backend):
pool_limits = PoolLimits(hard_limit=1, pool_timeout=1e-4)
pool_limits = PoolLimits(hard_limit=1)
timeout = TimeoutConfig(pool_timeout=1e-4)

async with Client(pool_limits=pool_limits, backend=backend) as client:
async with Client(
pool_limits=pool_limits, timeout=timeout, backend=backend
) as client:
response = await client.get(server.url, stream=True)

with pytest.raises(PoolTimeout):
Expand Down