Repository: huggingface/huggingface_hub
Pull request #1442 (2023-04-18 13:52:04)

Accept `backend_factory` to configure Sessions
Instead of using parameters (that are dependent on `requests` and that we would need to document/maintain), the user has to provide its own session factory. This will be less error-prone and allow any kind of configuration. For example, mounting a custom adapter will become possible, as done in https://github.com/narugo1992/hfmirror/blob/a5daefa54576de81da1b880ef4c3df9f2cf53000/hfmirror/utils/session.py#L28

codecov[bot]: ## [Codecov](https://codecov.io/gh/huggingface/huggingface_hub/pull/1442?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) Report
Patch coverage: **`100.00`**% and project coverage change: **`-0.08`** :warning:
> Comparison is base [(`30c5040`)](https://codecov.io/gh/huggingface/huggingface_hub/commit/30c50400313ecd012c82e521c14c25f7bfc83b76?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) 82.49% compared to head [(`417c773`)](https://codecov.io/gh/huggingface/huggingface_hub/pull/1442?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) 82.42%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1442      +/-   ##
==========================================
- Coverage   82.49%   82.42%   -0.08%     
==========================================
  Files          53       53              
  Lines        5628     5598      -30     
==========================================
- Hits         4643     4614      -29     
+ Misses        985      984       -1     
```


| [Impacted Files](https://codecov.io/gh/huggingface/huggingface_hub/pull/1442?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) | Coverage Δ | |
|---|---|---|
| [src/huggingface\_hub/utils/\_http.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1442?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9faHR0cC5weQ==) | `100.00% <100.00%> (+1.20%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/huggingface/huggingface_hub/pull/1442?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).


Patch:
diff --git a/docs/source/package_reference/utilities.mdx b/docs/source/package_reference/utilities.mdx
index 65d768be..35c533c7 100644
--- a/docs/source/package_reference/utilities.mdx
+++ b/docs/source/package_reference/utilities.mdx
@@ -94,8 +94,8 @@ True
 In some environments, you might want to configure how HTTP calls are made, for example if you are using a proxy.
 `huggingface_hub` let you configure this globally using [`configure_http_backend`]. All requests made to the Hub will
 then use your settings. Under the hood, `huggingface_hub` uses `requests.Session` so you might want to refer to the
-[`requests` documentation](https://requests.readthedocs.io/en/latest/user/advanced) to learn more about the parameters
-available.
+[`requests` documentation](https://requests.readthedocs.io/en/latest/user/advanced) to learn more about the available
+parameters.
 
 Since `requests.Session` is not guaranteed to be thread-safe, `huggingface_hub` creates one session instance per thread.
 Using sessions allows us to keep the connection open between HTTP calls and ultimately save time. If you are
diff --git a/src/huggingface_hub/utils/_http.py b/src/huggingface_hub/utils/_http.py
index 1870f402..cec904f8 100644
--- a/src/huggingface_hub/utils/_http.py
+++ b/src/huggingface_hub/utils/_http.py
@@ -16,10 +16,9 @@
 import io
 import threading
 import time
-from dataclasses import dataclass
 from functools import lru_cache
 from http import HTTPStatus
-from typing import Any, Dict, Optional, Tuple, Type, Union
+from typing import Callable, Tuple, Type, Union
 
 import requests
 from requests import Response
@@ -31,106 +30,73 @@ from ._typing import HTTP_METHOD_T
 
 logger = logging.get_logger(__name__)
 
+BACKEND_FACTORY_T = Callable[[], requests.Session]
+_GLOBAL_BACKEND_FACTORY: BACKEND_FACTORY_T = requests.Session
 
-@dataclass
-class SessionParameters:
-    """Data structure containing configuration for `requests.Session`.
 
-    Visit the [`requests` documentation](https://requests.readthedocs.io/en/latest/user/advanced) for a description of
-    each parameter.
+def configure_http_backend(backend_factory: BACKEND_FACTORY_T = requests.Session) -> None:
     """
-
-    headers: Optional[Dict[str, Any]] = None
-    auth: Optional[Any] = None
-    proxies: Optional[Dict[str, Any]] = None
-    hooks: Optional[Dict[str, list]] = None
-    verify: Optional[bool] = None
-    cert: Optional[Union[str, Tuple[str, str]]] = None
-    max_redirects: Optional[int] = None
-    trust_env: Optional[bool] = None
-
-
-_GLOBAL_SESSION_PARAMETERS = SessionParameters()
-
-
-def configure_http_backend(
-    headers: Optional[Dict[str, Any]] = None,
-    auth: Optional[Any] = None,
-    proxies: Optional[Dict[str, Any]] = None,
-    hooks: Optional[Dict[str, list]] = None,
-    verify: Optional[bool] = None,
-    cert: Optional[Union[str, Tuple[str, str]]] = None,
-    max_redirects: Optional[int] = None,
-    trust_env: Optional[bool] = None,
-) -> None:
-    """
-    Configure the HTTP backend. All HTTP calls from `huggingface_hub` will use these settings. This can be useful if
-    you are running your scripts in a specific environment requiring custom configuration (e.g. custom proxy or
-    certifications).
-
-    To get a list of all available parameters, you can refer to the official [`requests` documentation](https://requests.readthedocs.io/en/latest/user/advanced).
-    Any parameter left to `None` will default to the default value from `requests`.
+    Configure the HTTP backend by providing a `backend_factory`. Any HTTP calls made by `huggingface_hub` will use a
+    Session object instantiated by this factory. This can be useful if you are running your scripts in a specific
+    environment requiring custom configuration (e.g. custom proxy or certifications).
 
     Use [`get_session`] to get a configured Session. Since `requests.Session` is not guaranteed to be thread-safe,
-    `huggingface_hub` creates 1 Session instance per thread. They all share the same initial configuration set in
-    [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between calls. Max
-    size is 128 to avoid memory leaks if thousands of threads are spawned.
+    `huggingface_hub` creates 1 Session instance per thread. They are all instantiated using the same `backend_factory`
+    set in [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between
+    calls. Max size is 128 to avoid memory leaks if thousands of threads are spawned.
 
     See [this issue](https://github.com/psf/requests/issues/2766) to know more about thread-safety in `requests`.
 
     Example:
     ```py
+    import requests
     from huggingface_hub import configure_http_backend, get_session
 
-    # Configure proxy
-    configure_http_backend(
-        proxies={"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"},
-    )
+    # Create a factory function that returns a Session with configured proxies
+    def backend_factory() -> requests.Session:
+        session = requests.Session()
+        session.proxies = {"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"}
+        return session
+
+    # Set it as the default session factory
+    configure_http_backend(backend_factory=backend_factory)
 
     # In practice, this is mostly done internally in `huggingface_hub`
-    session = get_session().get("https://huggingface.co/api/models/gpt2")
+    session = get_session()
     ```
     """
-    global _GLOBAL_SESSION_PARAMETERS
-    _GLOBAL_SESSION_PARAMETERS = SessionParameters(
-        headers=headers,
-        auth=auth,
-        proxies=proxies,
-        hooks=hooks,
-        verify=verify,
-        cert=cert,
-        max_redirects=max_redirects,
-        trust_env=trust_env,
-    )
+    global _GLOBAL_BACKEND_FACTORY
+    _GLOBAL_BACKEND_FACTORY = backend_factory
     _get_session_from_cache.cache_clear()
 
 
-def get_http_backend_configuration() -> SessionParameters:
-    return _GLOBAL_SESSION_PARAMETERS
-
-
 def get_session() -> requests.Session:
     """
-    Get a `requests.Session` object, using configuration from the user.
+    Get a `requests.Session` object, using the session factory from the user.
 
     Use [`get_session`] to get a configured Session. Since `requests.Session` is not guaranteed to be thread-safe,
-    `huggingface_hub` creates 1 Session instance per thread. They all share the same initial configuration set in
-    [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between calls. Max
-    size is 128 to avoid memory leaks if thousands of threads are spawned.
+    `huggingface_hub` creates 1 Session instance per thread. They are all instantiated using the same `backend_factory`
+    set in [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between
+    calls. Max size is 128 to avoid memory leaks if thousands of threads are spawned.
 
     See [this issue](https://github.com/psf/requests/issues/2766) to know more about thread-safety in `requests`.
 
     Example:
     ```py
+    import requests
     from huggingface_hub import configure_http_backend, get_session
 
-    # Configure proxy
-    configure_http_backend(
-        proxies={"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"},
-    )
+    # Create a factory function that returns a Session with configured proxies
+    def backend_factory() -> requests.Session:
+        session = requests.Session()
+        session.proxies = {"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"}
+        return session
+
+    # Set it as the default session factory
+    configure_http_backend(backend_factory=backend_factory)
 
     # In practice, this is mostly done internally in `huggingface_hub`
-    session = get_session().get("https://huggingface.co/api/models/gpt2")
+    session = get_session()
     ```
     """
     return _get_session_from_cache(thread_ident=threading.get_ident())
@@ -139,30 +105,10 @@ def get_session() -> requests.Session:
 @lru_cache(maxsize=128)  # default value for Python>=3.8. Let's keep the same for Python3.7
 def _get_session_from_cache(thread_ident: int) -> requests.Session:
     """
-    Create a new session per thread using global parameters. Using LRU cache (maxsize 128) to avoid memory leaks when
+    Create a new session per thread using global factory. Using LRU cache (maxsize 128) to avoid memory leaks when
     using thousands of threads. Cache is cleared when `configure_http_backend` is called.
     """
-    session = requests.Session()
-    parameters = _GLOBAL_SESSION_PARAMETERS
-
-    if parameters.headers is not None:
-        session.headers.update(parameters.headers)
-    if parameters.auth is not None:
-        session.auth = parameters.auth
-    if parameters.proxies is not None:
-        session.proxies.update(parameters.proxies)
-    if parameters.hooks is not None:
-        session.hooks.update(parameters.hooks)
-    if parameters.verify is not None:
-        session.verify = parameters.verify
-    if parameters.cert is not None:
-        session.cert = parameters.cert
-    if parameters.max_redirects is not None:
-        session.max_redirects = parameters.max_redirects
-    if parameters.trust_env is not None:
-        session.trust_env = parameters.trust_env
-
-    return session
+    return _GLOBAL_BACKEND_FACTORY()
 
 
 def http_backoff(


Test patch:
diff --git a/tests/test_utils_http.py b/tests/test_utils_http.py
index f6f0acc3..76af9fbd 100644
--- a/tests/test_utils_http.py
+++ b/tests/test_utils_http.py
@@ -4,13 +4,10 @@ import unittest
 from typing import Generator
 from unittest.mock import Mock, call, patch
 
+import requests
 from requests import ConnectTimeout, HTTPError
 
-from huggingface_hub.utils._http import (
-    configure_http_backend,
-    get_session,
-    http_backoff,
-)
+from huggingface_hub.utils._http import configure_http_backend, get_session, http_backoff
 
 
 URL = "https://www.google.com"
@@ -148,6 +145,12 @@ class TestConfigureSession(unittest.TestCase):
         # Clear all sessions after tests
         configure_http_backend()
 
+    @staticmethod
+    def _factory() -> requests.Session:
+        session = requests.Session()
+        session.headers.update({"x-test-header": 4})
+        return session
+
     def test_default_configuration(self) -> None:
         session = get_session()
         self.assertEqual(session.headers["connection"], "keep-alive")  # keep connection alive by default
@@ -160,32 +163,13 @@ class TestConfigureSession(unittest.TestCase):
         self.assertEqual(session.hooks, {"response": []})
 
     def test_set_configuration(self) -> None:
-        configure_http_backend(
-            headers={"x-test-header": 4},
-            auth="some auth",
-            proxies={"http": "127.0.0.1:300"},
-            hooks={"something": ["some hook"]},  # invalid, but it's just an example
-            verify=False,
-            cert="path/to/cart.pem",
-            max_redirects=72,
-            trust_env=False,
-        )
-        session = get_session()
+        configure_http_backend(backend_factory=self._factory)
 
-        self.assertEqual(session.auth, "some auth")
-        self.assertEqual(session.proxies, {"http": "127.0.0.1:300"})  # default value is an empty dict
-        self.assertEqual(session.verify, False)
-        self.assertEqual(session.cert, "path/to/cart.pem")
-        self.assertEqual(session.max_redirects, 72)
-        self.assertEqual(session.trust_env, False)
-
-        # Header and hooks are not overwritten but updated
+        # Check headers have been set correctly
+        session = get_session()
         self.assertNotEqual(session.headers, {"x-test-header": 4})
         self.assertEqual(session.headers["x-test-header"], 4)
 
-        self.assertNotEqual(session.hooks, {"something": ["some hook"]})
-        self.assertEqual(session.hooks["something"], ["some hook"])
-
     def test_get_session_twice(self):
         session_1 = get_session()
         session_2 = get_session()
@@ -194,12 +178,12 @@ class TestConfigureSession(unittest.TestCase):
     def test_get_session_twice_but_reconfigure_in_between(self):
         """Reconfiguring the session clears the cache."""
         session_1 = get_session()
-        configure_http_backend(auth="some auth")
+        configure_http_backend(backend_factory=self._factory)
 
         session_2 = get_session()
         self.assertIsNot(session_1, session_2)
-        self.assertIsNone(session_1.auth)
-        self.assertEqual(session_2.auth, "some auth")
+        self.assertIsNone(session_1.headers.get("x-test-header"))
+        self.assertEqual(session_2.headers["x-test-header"], 4)
 
     def test_get_session_multiple_threads(self):
         N = 3