Repository: huggingface/huggingface_hub
Pull request #985 (2022-08-10 15:59:03)

Use a finer exception when local_files_only=True and a file is missing in cache
Fix https://github.com/huggingface/huggingface_hub/issues/979. See discussion is issue about `LocalEntryNotFound` inheritance.

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_985). All of your documentation changes will be reflected on that endpoint.

Patch:
diff --git a/docs/source/package_reference/utilities.mdx b/docs/source/package_reference/utilities.mdx
index 96415fac..6ca1afad 100644
--- a/docs/source/package_reference/utilities.mdx
+++ b/docs/source/package_reference/utilities.mdx
@@ -58,3 +58,5 @@ See below for all custom errors thrown by different methods across the package.
 [[autodoc]] huggingface_hub.utils.RevisionNotFoundError
 
 [[autodoc]] huggingface_hub.utils.EntryNotFoundError
+
+[[autodoc]] huggingface_hub.utils.LocalEntryNotFoundError
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 4f18090b..88e0fc80 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -35,7 +35,7 @@ from .constants import (
 )
 from .hf_api import HfFolder
 from .utils import logging
-from .utils._errors import _raise_for_status
+from .utils._errors import LocalEntryNotFoundError, _raise_for_status
 
 
 logger = logging.get_logger(__name__)
@@ -553,6 +553,8 @@ def cached_download(
           If the revision to download from cannot be found.
         - [`~huggingface_hub.utils.EntryNotFoundError`]
           If the file to download cannot be found.
+        - [`~huggingface_hub.utils.LocalEntryNotFoundError`]
+          If network is disabled or unavailable and file is not found in cache.
 
     </Tip>
     """
@@ -659,13 +661,13 @@ def cached_download(
                 # the models might've been found if local_files_only=False
                 # Notify the user about that
                 if local_files_only:
-                    raise ValueError(
+                    raise LocalEntryNotFoundError(
                         "Cannot find the requested files in the cached path and"
                         " outgoing traffic has been disabled. To enable model look-ups"
                         " and downloads online, set 'local_files_only' to False."
                     )
                 else:
-                    raise ValueError(
+                    raise LocalEntryNotFoundError(
                         "Connection error, and we cannot find the requested files in"
                         " the cached path. Please try again or make sure your Internet"
                         " connection is on."
@@ -911,6 +913,8 @@ def hf_hub_download(
           If the revision to download from cannot be found.
         - [`~huggingface_hub.utils.EntryNotFoundError`]
           If the file to download cannot be found.
+        - [`~huggingface_hub.utils.LocalEntryNotFoundError`]
+          If network is disabled or unavailable and file is not found in cache.
 
     </Tip>
     """
@@ -1089,13 +1093,13 @@ def hf_hub_download(
         # the models might've been found if local_files_only=False
         # Notify the user about that
         if local_files_only:
-            raise ValueError(
+            raise LocalEntryNotFoundError(
                 "Cannot find the requested files in the disk cache and"
                 " outgoing traffic has been disabled. To enable hf.co look-ups"
                 " and downloads online, set 'local_files_only' to False."
             )
         else:
-            raise ValueError(
+            raise LocalEntryNotFoundError(
                 "Connection error, and we cannot find the requested files in"
                 " the disk cache. Please try again or make sure your Internet"
                 " connection is on."
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index a09fbcf8..8ec0881e 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -15,5 +15,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License
 
-from ._errors import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError
+from ._errors import (
+    EntryNotFoundError,
+    LocalEntryNotFoundError,
+    RepositoryNotFoundError,
+    RevisionNotFoundError,
+)
 from ._subprocess import run_subprocess
diff --git a/src/huggingface_hub/utils/_errors.py b/src/huggingface_hub/utils/_errors.py
index 60aedccd..e2f01b72 100644
--- a/src/huggingface_hub/utils/_errors.py
+++ b/src/huggingface_hub/utils/_errors.py
@@ -57,6 +57,28 @@ class EntryNotFoundError(HTTPError):
         super().__init__(message, response=response)
 
 
+class LocalEntryNotFoundError(EntryNotFoundError, FileNotFoundError, ValueError):
+    """
+    Raised when trying to access a file that is not on the disk when network is
+    disabled or unavailable (connection issue). The entry may exist on the Hub.
+
+    Note: `ValueError` type is to ensure backward compatibility.
+    Note: `LocalEntryNotFoundError` derives from `HTTPError` because of `EntryNotFoundError`
+          even when it is not a network issue.
+
+    Example:
+
+    ```py
+    >>> from huggingface_hub import hf_hub_download
+    >>> hf_hub_download('bert-base-cased', '<non-cached-file>',  local_files_only=True)
+    huggingface_hub.utils._errors.LocalEntryNotFoundError: Cannot find the requested files in the disk cache and outgoing traffic has been disabled. To enable hf.co look-ups and downloads online, set 'local_files_only' to False.
+    ```
+    """
+
+    def __init__(self, message):
+        super().__init__(message, response=None)
+
+
 class BadRequestError(ValueError, HTTPError):
     """
     Raised by `_raise_convert_bad_request` when the server returns HTTP 400 error


Test patch:
diff --git a/tests/test_file_download.py b/tests/test_file_download.py
index 7a9dcf0a..dde36720 100644
--- a/tests/test_file_download.py
+++ b/tests/test_file_download.py
@@ -11,8 +11,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
+import os
 import unittest
+from tempfile import TemporaryDirectory
+
+import pytest
 
 from huggingface_hub.constants import (
     CONFIG_NAME,
@@ -28,6 +31,7 @@ from huggingface_hub.file_download import (
 )
 from huggingface_hub.utils import (
     EntryNotFoundError,
+    LocalEntryNotFoundError,
     RepositoryNotFoundError,
     RevisionNotFoundError,
 )
@@ -88,14 +92,50 @@ class CachedDownloadTests(unittest.TestCase):
                     cached_download(valid_url, legacy_cache_layout=True)
                 )
 
-    def test_file_not_found(self):
-        # Valid revision (None) but missing file.
+    def test_file_not_found_on_repo(self):
+        # Valid revision (None) but missing file on repo.
         url = hf_hub_url(DUMMY_MODEL_ID, filename="missing.bin")
         with self.assertRaisesRegex(
             EntryNotFoundError, "404 Client Error: Entry Not Found"
         ):
             _ = cached_download(url, legacy_cache_layout=True)
 
+    def test_file_not_found_locally_and_network_disabled(self):
+        # Valid file but missing locally and network is disabled.
+        with TemporaryDirectory() as tmpdir:
+            # Download a first time to get the refs ok
+            filepath = hf_hub_download(
+                DUMMY_MODEL_ID,
+                filename=CONFIG_NAME,
+                cache_dir=tmpdir,
+                local_files_only=False,
+            )
+
+            # Remove local file
+            os.remove(filepath)
+
+            # Get without network must fail
+            with pytest.raises(LocalEntryNotFoundError):
+                hf_hub_download(
+                    DUMMY_MODEL_ID,
+                    filename=CONFIG_NAME,
+                    cache_dir=tmpdir,
+                    local_files_only=True,
+                )
+
+    def test_file_not_found_locally_and_network_disabled_legacy(self):
+        # Valid file but missing locally and network is disabled.
+        url = hf_hub_url(DUMMY_MODEL_ID, filename=CONFIG_NAME)
+        with TemporaryDirectory() as tmpdir:
+            # Get without network must fail
+            with pytest.raises(LocalEntryNotFoundError):
+                cached_download(
+                    url,
+                    legacy_cache_layout=True,
+                    local_files_only=True,
+                    cache_dir=tmpdir,
+                )
+
     def test_revision_not_found(self):
         # Valid file but missing revision
         url = hf_hub_url(