Repository: encode/httpx

Issue:
Renaming PoolLimits -> Limits.
Another 1.0 final run-in bit of polishing here...

I think we should probably renaming `pool_limits=...` to `limits=...` and `httpx.PoolLimits(...)` to `httpx.Limits(...)`.
If we *are* going to do this, then we should do so with a gentle deprecation, and we should target 0.14.

There's two motivations here...

* Prefer a more concise style.
* At some point we may have limits that are *not* strictly to do with the connection pool. (For example, it'd be helpful particularly with the AsyncClient to be able to enforce a client-wide max concurrent outgoing requests. Which we might just want to unilaterally apply across *all* mounted transports/proxies)

I'm open to push back on this if anyone thinks this is a bit over-zealous, and I *am* mindful that it introduces a bit of thrash. On the flip side, if there's a time to do this, then pre-1.0 is the time.


Discussion:
Some other thoughts around why this be a good renaming too. There's a few other limits we might want to put in place, that are not necessarily "PoolLimits". I've included some possible future examples here...

* `max_response_size = 100_000_000`  # Raise an error on any responses >100MB.
* `keepalive_expiry = 5.0`  # Control the HTTP keep alive duration.
* `max_concurrent_requests = 100`  # Client-wide semaphore over all requests.
* `max_request_duration = 60.0`  # Raise an error on responses that exceed 1 minute total download time.

Resolving patch:
diff --git a/httpx/__init__.py b/httpx/__init__.py
--- a/httpx/__init__.py
+++ b/httpx/__init__.py
@@ -2,7 +2,7 @@
 from ._api import delete, get, head, options, patch, post, put, request, stream
 from ._auth import Auth, BasicAuth, DigestAuth
 from ._client import AsyncClient, Client
-from ._config import PoolLimits, Proxy, Timeout, create_ssl_context
+from ._config import Limits, PoolLimits, Proxy, Timeout, create_ssl_context
 from ._exceptions import (
     CloseError,
     ConnectError,
@@ -59,6 +59,7 @@
     "BasicAuth",
     "Client",
     "DigestAuth",
+    "Limits",
     "PoolLimits",
     "Proxy",
     "Timeout",
diff --git a/httpx/_client.py b/httpx/_client.py
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -7,11 +7,11 @@
 
 from ._auth import Auth, BasicAuth, FunctionAuth
 from ._config import (
+    DEFAULT_LIMITS,
     DEFAULT_MAX_REDIRECTS,
-    DEFAULT_POOL_LIMITS,
     DEFAULT_TIMEOUT_CONFIG,
     UNSET,
-    PoolLimits,
+    Limits,
     Proxy,
     Timeout,
     UnsetType,
@@ -50,6 +50,7 @@
     get_logger,
     same_origin,
     should_not_be_proxied,
+    warn_deprecated,
 )
 
 logger = get_logger(__name__)
@@ -422,8 +423,7 @@ class Client(BaseClient):
     URLs.
     * **timeout** - *(optional)* The timeout configuration to use when sending
     requests.
-    * **pool_limits** - *(optional)* The connection pool configuration to use
-    when determining the maximum number of concurrently open HTTP connections.
+    * **limits** - *(optional)* The limits configuration to use.
     * **max_redirects** - *(optional)* The maximum number of redirect responses
     that should be followed.
     * **base_url** - *(optional)* A URL to use as the base when building
@@ -448,7 +448,8 @@ def __init__(
         http2: bool = False,
         proxies: ProxiesTypes = None,
         timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
+        pool_limits: Limits = None,
         max_redirects: int = DEFAULT_MAX_REDIRECTS,
         base_url: URLTypes = None,
         transport: httpcore.SyncHTTPTransport = None,
@@ -466,13 +467,20 @@ def __init__(
             trust_env=trust_env,
         )
 
+        if pool_limits is not None:
+            warn_deprecated(
+                "Client(..., pool_limits=...) is deprecated and will raise "
+                "errors in the future. Use Client(..., limits=...) instead."
+            )
+            limits = pool_limits
+
         proxy_map = self._get_proxy_map(proxies, trust_env)
 
         self._transport = self._init_transport(
             verify=verify,
             cert=cert,
             http2=http2,
-            pool_limits=pool_limits,
+            limits=limits,
             transport=transport,
             app=app,
             trust_env=trust_env,
@@ -487,7 +495,7 @@ def __init__(
                 verify=verify,
                 cert=cert,
                 http2=http2,
-                pool_limits=pool_limits,
+                limits=limits,
                 trust_env=trust_env,
             )
             for key, proxy in proxy_map.items()
@@ -499,7 +507,7 @@ def _init_transport(
         verify: VerifyTypes = True,
         cert: CertTypes = None,
         http2: bool = False,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
         transport: httpcore.SyncHTTPTransport = None,
         app: typing.Callable = None,
         trust_env: bool = True,
@@ -514,8 +522,8 @@ def _init_transport(
 
         return httpcore.SyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=pool_limits.max_keepalive,
-            max_connections=pool_limits.max_connections,
+            max_keepalive=limits.max_keepalive,
+            max_connections=limits.max_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
@@ -526,7 +534,7 @@ def _init_proxy_transport(
         verify: VerifyTypes = True,
         cert: CertTypes = None,
         http2: bool = False,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
         trust_env: bool = True,
     ) -> httpcore.SyncHTTPTransport:
         ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
@@ -536,8 +544,8 @@ def _init_proxy_transport(
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=pool_limits.max_keepalive,
-            max_connections=pool_limits.max_connections,
+            max_keepalive=limits.max_keepalive,
+            max_connections=limits.max_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
@@ -946,8 +954,7 @@ class AsyncClient(BaseClient):
     URLs.
     * **timeout** - *(optional)* The timeout configuration to use when sending
     requests.
-    * **pool_limits** - *(optional)* The connection pool configuration to use
-    when determining the maximum number of concurrently open HTTP connections.
+    * **limits** - *(optional)* The limits configuration to use.
     * **max_redirects** - *(optional)* The maximum number of redirect responses
     that should be followed.
     * **base_url** - *(optional)* A URL to use as the base when building
@@ -972,7 +979,8 @@ def __init__(
         http2: bool = False,
         proxies: ProxiesTypes = None,
         timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
+        pool_limits: Limits = None,
         max_redirects: int = DEFAULT_MAX_REDIRECTS,
         base_url: URLTypes = None,
         transport: httpcore.AsyncHTTPTransport = None,
@@ -990,13 +998,20 @@ def __init__(
             trust_env=trust_env,
         )
 
+        if pool_limits is not None:
+            warn_deprecated(
+                "AsyncClient(..., pool_limits=...) is deprecated and will raise "
+                "errors in the future. Use AsyncClient(..., limits=...) instead."
+            )
+            limits = pool_limits
+
         proxy_map = self._get_proxy_map(proxies, trust_env)
 
         self._transport = self._init_transport(
             verify=verify,
             cert=cert,
             http2=http2,
-            pool_limits=pool_limits,
+            limits=limits,
             transport=transport,
             app=app,
             trust_env=trust_env,
@@ -1011,7 +1026,7 @@ def __init__(
                 verify=verify,
                 cert=cert,
                 http2=http2,
-                pool_limits=pool_limits,
+                limits=limits,
                 trust_env=trust_env,
             )
             for key, proxy in proxy_map.items()
@@ -1023,7 +1038,7 @@ def _init_transport(
         verify: VerifyTypes = True,
         cert: CertTypes = None,
         http2: bool = False,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
         transport: httpcore.AsyncHTTPTransport = None,
         app: typing.Callable = None,
         trust_env: bool = True,
@@ -1038,8 +1053,8 @@ def _init_transport(
 
         return httpcore.AsyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=pool_limits.max_keepalive,
-            max_connections=pool_limits.max_connections,
+            max_keepalive=limits.max_keepalive,
+            max_connections=limits.max_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
@@ -1050,7 +1065,7 @@ def _init_proxy_transport(
         verify: VerifyTypes = True,
         cert: CertTypes = None,
         http2: bool = False,
-        pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
+        limits: Limits = DEFAULT_LIMITS,
         trust_env: bool = True,
     ) -> httpcore.AsyncHTTPTransport:
         ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
@@ -1060,8 +1075,8 @@ def _init_proxy_transport(
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=pool_limits.max_keepalive,
-            max_connections=pool_limits.max_connections,
+            max_keepalive=limits.max_keepalive,
+            max_connections=limits.max_connections,
             keepalive_expiry=KEEPALIVE_EXPIRY,
             http2=http2,
         )
diff --git a/httpx/_config.py b/httpx/_config.py
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -317,9 +317,9 @@ def __repr__(self) -> str:
         )
 
 
-class PoolLimits:
+class Limits:
     """
-    Limits on the number of connections in a connection pool.
+    Configuration for limits to various client behaviors.
 
     **Parameters:**
 
@@ -350,6 +350,17 @@ def __repr__(self) -> str:
         )
 
 
+class PoolLimits(Limits):
+    def __init__(
+        self, *, max_keepalive: int = None, max_connections: int = None,
+    ) -> None:
+        warn_deprecated(
+            "httpx.PoolLimits(...) is deprecated and will raise errors in the future. "
+            "Use httpx.Limits(...) instead."
+        )
+        super().__init__(max_keepalive=max_keepalive, max_connections=max_connections)
+
+
 class Proxy:
     def __init__(
         self, url: URLTypes, *, headers: HeaderTypes = None, mode: str = "DEFAULT",
@@ -389,5 +400,5 @@ def __repr__(self) -> str:
 
 
 DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
-DEFAULT_POOL_LIMITS = PoolLimits(max_keepalive=10, max_connections=100)
+DEFAULT_LIMITS = Limits(max_keepalive=10, max_connections=100)
 DEFAULT_MAX_REDIRECTS = 20


Test patch:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -194,3 +194,13 @@ def test_merge_url_hsts(url: str, scheme: str, is_ssl: bool):
     request = client.build_request("GET", url)
     assert request.url.scheme == scheme
     assert request.url.is_ssl == is_ssl
+
+
+def test_pool_limits_deprecated():
+    limits = httpx.Limits()
+
+    with pytest.warns(DeprecationWarning):
+        httpx.Client(pool_limits=limits)
+
+    with pytest.warns(DeprecationWarning):
+        httpx.AsyncClient(pool_limits=limits)
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -101,13 +101,18 @@ def test_create_ssl_context_with_get_request(server, cert_pem_file):
 
 
 def test_limits_repr():
-    limits = httpx.PoolLimits(max_connections=100)
-    assert repr(limits) == "PoolLimits(max_keepalive=None, max_connections=100)"
+    limits = httpx.Limits(max_connections=100)
+    assert repr(limits) == "Limits(max_keepalive=None, max_connections=100)"
 
 
 def test_limits_eq():
-    limits = httpx.PoolLimits(max_connections=100)
-    assert limits == httpx.PoolLimits(max_connections=100)
+    limits = httpx.Limits(max_connections=100)
+    assert limits == httpx.Limits(max_connections=100)
+
+
+def test_pool_limits_deprecated():
+    with pytest.warns(DeprecationWarning):
+        httpx.PoolLimits()
 
 
 def test_timeout_eq():
diff --git a/tests/test_timeouts.py b/tests/test_timeouts.py
--- a/tests/test_timeouts.py
+++ b/tests/test_timeouts.py
@@ -34,10 +34,10 @@ async def test_connect_timeout(server):
 
 @pytest.mark.usefixtures("async_environment")
 async def test_pool_timeout(server):
-    pool_limits = httpx.PoolLimits(max_connections=1)
+    limits = httpx.Limits(max_connections=1)
     timeout = httpx.Timeout(None, pool=1e-4)
 
-    async with httpx.AsyncClient(pool_limits=pool_limits, timeout=timeout) as client:
+    async with httpx.AsyncClient(limits=limits, timeout=timeout) as client:
         async with client.stream("GET", server.url):
             with pytest.raises(httpx.PoolTimeout):
                 await client.get("http://localhost:8000/")