Repository: huggingface/huggingface_hub
Pull request #1899 (2023-12-12 15:52:18)

Respect HF_HUB_OFFLINE for every http call
At the moment `HF_HUB_OFFLINE` is only respected when downloading files. This PR makes it used globally for any call to the Hub.

Not respecting it makes it harder to debug things in some cases. For example:
- `snapshot_download` should not make the HTTP call to list the repo files
- `diffusers` should not make a `model_info` call => see https://github.com/huggingface/diffusers/issues/6089 which would have been easier to investigate

With this PR, any unintended call to the Hub will raise a `OfflineModeIsEnabled` error. Either the dependent libraries chose to catch it explicitly and default back to another solution or the user gets an explicit error -which is easier when reporting an issue.

cc @sayakpaul as well

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1899). All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/huggingface/huggingface_hub/pull/1899?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`3d11c57`)](https://app.codecov.io/gh/huggingface/huggingface_hub/commit/3d11c57d9206af009de99980006cd4a1fed15d53?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) 82.06% compared to head [(`1712c8d`)](https://app.codecov.io/gh/huggingface/huggingface_hub/pull/1899?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) 82.28%.


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


```diff
@@            Coverage Diff             @@
##             main    #1899      +/-   ##
==========================================
+ Coverage   82.06%   82.28%   +0.21%     
==========================================
  Files          65       65              
  Lines        8030     8032       +2     
==========================================
+ Hits         6590     6609      +19     
+ Misses       1440     1423      -17     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/huggingface/huggingface_hub/pull/1899?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).

julien-c: sounds like a good idea!
Wauplin: Cool, let's get it merged then :)

Patch:
diff --git a/docs/source/en/package_reference/environment_variables.md b/docs/source/en/package_reference/environment_variables.md
index aaa9b2e2..ad3b9900 100644
--- a/docs/source/en/package_reference/environment_variables.md
+++ b/docs/source/en/package_reference/environment_variables.md
@@ -83,13 +83,11 @@ as `True` if its value is one of `{"1", "ON", "YES", "TRUE"}` (case-insensitive)
 
 ### HF_HUB_OFFLINE
 
-If set, no HTTP calls will me made when trying to fetch files. Only files that are already
-cached will be accessed. This is useful in case your network is slow and you don't care
-about having absolutely the latest version of a file.
+If set, no HTTP calls will me made to the Hugging Face Hub. If you try to download files, only the cached files will be accessed. If no cache file is detected, an error is raised This is useful in case your network is slow and you don't care about having the latest version of a file.
 
-**Note:** even if the latest version of a file is cached, calling `hf_hub_download` still triggers
-a HTTP request to check that a new version is not available. Setting `HF_HUB_OFFLINE=1` will
-skip this call which speeds up your loading time.
+If `HF_HUB_OFFLINE=1` is set as environment variable and you call any method of [`HfApi`], an [`~huggingface_hub.utils.OfflineModeIsEnabled`] exception will be raised.
+
+**Note:** even if the latest version of a file is cached, calling `hf_hub_download` still triggers a HTTP request to check that a new version is not available. Setting `HF_HUB_OFFLINE=1` will skip this call which speeds up your loading time.
 
 ### HF_HUB_DISABLE_IMPLICIT_TOKEN
 
diff --git a/docs/source/en/package_reference/utilities.md b/docs/source/en/package_reference/utilities.md
index 93e73e6c..7fb01565 100644
--- a/docs/source/en/package_reference/utilities.md
+++ b/docs/source/en/package_reference/utilities.md
@@ -176,6 +176,10 @@ user as possible.
 
 [[autodoc]] huggingface_hub.utils.LocalEntryNotFoundError
 
+#### OfflineModeIsEnabled
+
+[[autodoc]] huggingface_hub.utils.OfflineModeIsEnabled
+
 ## Telemetry
 
 `huggingface_hub` includes an helper to send telemetry data. This information helps us debug issues and prioritize new features.
diff --git a/src/huggingface_hub/_snapshot_download.py b/src/huggingface_hub/_snapshot_download.py
index 97cf16be..53f96972 100644
--- a/src/huggingface_hub/_snapshot_download.py
+++ b/src/huggingface_hub/_snapshot_download.py
@@ -10,6 +10,7 @@ from .constants import (
     DEFAULT_REVISION,
     HF_HUB_CACHE,
     HF_HUB_ENABLE_HF_TRANSFER,
+    HF_HUB_OFFLINE,
     REPO_TYPES,
 )
 from .file_download import REGEX_COMMIT_HASH, hf_hub_download, repo_folder_name
@@ -161,7 +162,7 @@ def snapshot_download(
     # appropriate folder in the cache
     # If the specified revision is a commit hash, look inside "snapshots".
     # If the specified revision is a branch or tag, look inside "refs".
-    if local_files_only:
+    if local_files_only or HF_HUB_OFFLINE:
         if REGEX_COMMIT_HASH.match(revision):
             commit_hash = revision
         else:
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 2024cb62..19c82c03 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -50,6 +50,7 @@ from .utils import (
     FileMetadataError,
     GatedRepoError,
     LocalEntryNotFoundError,
+    OfflineModeIsEnabled,
     RepositoryNotFoundError,
     RevisionNotFoundError,
     SoftTemporaryDirectory,
@@ -361,28 +362,11 @@ def http_user_agent(
     )
 
 
-class OfflineModeIsEnabled(ConnectionError):
-    pass
-
-
-def _raise_if_offline_mode_is_enabled(msg: Optional[str] = None):
-    """Raise a OfflineModeIsEnabled error (subclass of ConnectionError) if
-    HF_HUB_OFFLINE is True."""
-    if constants.HF_HUB_OFFLINE:
-        raise OfflineModeIsEnabled(
-            "Offline mode is enabled." if msg is None else "Offline mode is enabled. " + str(msg)
-        )
-
-
 def _request_wrapper(
     method: HTTP_METHOD_T, url: str, *, follow_relative_redirects: bool = False, **params
 ) -> requests.Response:
-    """Wrapper around requests methods to add several features.
-
-    What it does:
-    1. Ensure offline mode is disabled (env variable `HF_HUB_OFFLINE` not set to 1). If enabled, a
-       `OfflineModeIsEnabled` exception is raised.
-    2. Follow relative redirects if `follow_relative_redirects=True` even when `allow_redirection=False`.
+    """Wrapper around requests methods to follow relative redirects if `follow_relative_redirects=True` even when
+    `allow_redirection=False`.
 
     Args:
         method (`str`):
@@ -396,10 +380,7 @@ def _request_wrapper(
         **params (`dict`, *optional*):
             Params to pass to `requests.request`.
     """
-    # 1. Check online mode
-    _raise_if_offline_mode_is_enabled(f"Tried to reach {url}")
-
-    # 2. Force relative redirection
+    # Recursively follow relative redirects
     if follow_relative_redirects:
         response = _request_wrapper(
             method=method,
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index a2e7fcbd..139179a5 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -44,7 +44,7 @@ from ._fixes import SoftTemporaryDirectory, yaml_dump
 from ._git_credential import list_credential_helpers, set_git_credential, unset_git_credential
 from ._headers import build_hf_headers, get_token_to_send, LocalTokenNotFoundError
 from ._hf_folder import HfFolder
-from ._http import configure_http_backend, get_session, http_backoff, reset_sessions
+from ._http import configure_http_backend, get_session, http_backoff, reset_sessions, OfflineModeIsEnabled
 from ._pagination import paginate
 from ._paths import filter_repo_objects, IGNORE_GIT_FOLDER_PATTERNS
 from ._experimental import experimental
diff --git a/src/huggingface_hub/utils/_http.py b/src/huggingface_hub/utils/_http.py
index dd613d1f..69cbfc22 100644
--- a/src/huggingface_hub/utils/_http.py
+++ b/src/huggingface_hub/utils/_http.py
@@ -27,6 +27,7 @@ from requests import Response
 from requests.adapters import HTTPAdapter
 from requests.models import PreparedRequest
 
+from .. import constants
 from . import logging
 from ._typing import HTTP_METHOD_T
 
@@ -40,6 +41,10 @@ X_AMZN_TRACE_ID = "X-Amzn-Trace-Id"
 X_REQUEST_ID = "x-request-id"
 
 
+class OfflineModeIsEnabled(ConnectionError):
+    """Raised when a request is made but `HF_HUB_OFFLINE=1` is set as environment variable."""
+
+
 class UniqueRequestIdAdapter(HTTPAdapter):
     X_AMZN_TRACE_ID = "X-Amzn-Trace-Id"
 
@@ -68,10 +73,21 @@ class UniqueRequestIdAdapter(HTTPAdapter):
             raise
 
 
+class OfflineAdapter(HTTPAdapter):
+    def send(self, request: PreparedRequest, *args, **kwargs) -> Response:
+        raise OfflineModeIsEnabled(
+            f"Cannot reach {request.url}: offline mode is enabled. To disable it, please unset the `HF_HUB_OFFLINE` environment variable."
+        )
+
+
 def _default_backend_factory() -> requests.Session:
     session = requests.Session()
-    session.mount("http://", UniqueRequestIdAdapter())
-    session.mount("https://", UniqueRequestIdAdapter())
+    if constants.HF_HUB_OFFLINE:
+        session.mount("http://", OfflineAdapter())
+        session.mount("https://", OfflineAdapter())
+    else:
+        session.mount("http://", UniqueRequestIdAdapter())
+        session.mount("https://", UniqueRequestIdAdapter())
     return session
 
 


Test patch:
diff --git a/tests/test_utils_http.py b/tests/test_utils_http.py
index 97b5cb70..f82a7a17 100644
--- a/tests/test_utils_http.py
+++ b/tests/test_utils_http.py
@@ -11,7 +11,13 @@ import requests
 from requests import ConnectTimeout, HTTPError
 
 from huggingface_hub.constants import ENDPOINT
-from huggingface_hub.utils._http import configure_http_backend, get_session, http_backoff
+from huggingface_hub.utils._http import (
+    OfflineModeIsEnabled,
+    configure_http_backend,
+    get_session,
+    http_backoff,
+    reset_sessions,
+)
 
 
 URL = "https://www.google.com"
@@ -234,6 +240,19 @@ class TestConfigureSession(unittest.TestCase):
         self.assertNotEqual(repr(main_session), child_session)
 
 
+class OfflineModeSessionTest(unittest.TestCase):
+    def tearDown(self) -> None:
+        reset_sessions()
+        return super().tearDown()
+
+    @patch("huggingface_hub.constants.HF_HUB_OFFLINE", True)
+    def test_offline_mode(self):
+        configure_http_backend()
+        session = get_session()
+        with self.assertRaises(OfflineModeIsEnabled):
+            session.get("https://huggingface.co")
+
+
 class TestUniqueRequestId(unittest.TestCase):
     api_endpoint = ENDPOINT + "/api/tasks"  # any endpoint is fine
 
diff --git a/tests/testing_utils.py b/tests/testing_utils.py
index 5a90d488..2fb6d314 100644
--- a/tests/testing_utils.py
+++ b/tests/testing_utils.py
@@ -15,7 +15,7 @@ from unittest.mock import Mock, patch
 import pytest
 import requests
 
-from huggingface_hub.utils import is_gradio_available, logging
+from huggingface_hub.utils import is_gradio_available, logging, reset_sessions
 from tests.testing_constants import ENDPOINT_PRODUCTION, ENDPOINT_PRODUCTION_URL_SCHEME
 
 
@@ -193,7 +193,9 @@ def offline(mode=OfflineSimulationMode.CONNECTION_FAILS, timeout=1e-16):
                     yield
     elif mode is OfflineSimulationMode.HF_HUB_OFFLINE_SET_TO_1:
         with patch("huggingface_hub.constants.HF_HUB_OFFLINE", True):
+            reset_sessions()
             yield
+        reset_sessions()
     else:
         raise ValueError("Please use a value from the OfflineSimulationMode enum.")