Repository: huggingface/huggingface_hub
Pull request #992 (2022-08-12 14:56:16)

Enable coverage in CI
Related to https://github.com/huggingface/huggingface_hub/issues/977.

Draft mode since it's not close to be reviewed :D

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_992). All of your documentation changes will be reflected on that endpoint.
Wauplin: **Update:** code coverage is now working as expected on the `./src/huggingface_face` folder !

Coverage is measured at ~80% (see [codecov report](https://app.codecov.io/gh/huggingface/huggingface_hub/compare/main...977-enable-code-coverage-in-ci/tree)). At the moment, most branch/compare sections of codecov are broken. I suspect this is because it tries to compare against main branch that has cov reports only on the `tests/` folder. I tried to "erase repo coverage content" from the [settings page](https://app.codecov.io/gh/huggingface/huggingface_hub/settings) since previous reports were unrelevant but I think I don't have the rights to do it.

I also made a refactoring of [python-tests.yml](https://github.com/huggingface/huggingface_hub/blob/4ce3a315300f8b743b2e058a4f8c514355b13acd/.github/workflows/python-tests.yml) workflow to leverage even more the github matrix and avoid duplication of steps. I think it would be easier to review it directly from the file instead of looking at the diff. In theory I kept exactly the same steps for each build (+add the cov reports). 

@LysandreJik @osanseviero could you please have a look at it ? Thanks in advance 🙂 🙏 


Wauplin: The workaround I used to get proper coverage is to run tests from the `./src` folder and using `python -m pytest` instead of `pytest`. My understanding is that it "forces" python to import `huggingface_hub` from the current directory in priority (since `./src/huggingface_hub` has same name) instead of the installed one.

Then I updated the `"./tests/"` target to `"../tests/"` and created a `"../coverage.xml"` report.

Patch:
diff --git a/README.md b/README.md
index cc838b52..5fd62b9c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
 # `huggingface_hub`
 
+<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
+<a href="https://codecov.io/gh/huggingface/huggingface_hub"><img alt="Code coverage" src="https://codecov.io/gh/huggingface/huggingface_hub/branch/main/graph/badge.svg?token=RXP95LE2XL"></a>
+<a href="https://github.com/huggingface/huggingface_hub/releases"><img alt="GitHub release" src="https://img.shields.io/github/release/huggingface/huggingface_hub.svg"></a>
+<a href="https://huggingface.co/docs/huggingface_hub/index"><img alt="Documentation" src="https://img.shields.io/website/http/huggingface.co/docs/huggingface_hub/index.svg?down_color=red&down_message=offline&up_message=online&label=doc"></a>
 
 ## Welcome to the huggingface_hub library
 
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 69cb7601..00000000
--- a/codecov.yml
+++ /dev/null
@@ -1,1 +0,0 @@
-comment: false
diff --git a/setup.py b/setup.py
index 1cd4a871..0245e511 100644
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,7 @@ extras["tensorflow"] = ["tensorflow", "pydot", "graphviz"]
 
 extras["testing"] = [
     "pytest",
+    "pytest-cov",
     "datasets",
     "soundfile",
 ]
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 30ac43bb..929c638e 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -13,7 +13,7 @@ from functools import partial
 from hashlib import sha256
 from pathlib import Path
 from typing import BinaryIO, Dict, Optional, Tuple, Union
-from urllib.parse import quote
+from urllib.parse import quote, urlparse
 
 import packaging.version
 
@@ -34,7 +34,11 @@ from .constants import (
 )
 from .hf_api import HfFolder
 from .utils import logging, tqdm
-from .utils._errors import LocalEntryNotFoundError, _raise_for_status
+from .utils._errors import (
+    EntryNotFoundError,
+    LocalEntryNotFoundError,
+    _raise_for_status,
+)
 
 
 logger = logging.get_logger(__name__)
@@ -365,7 +369,7 @@ def _raise_if_offline_mode_is_enabled(msg: Optional[str] = None):
         )
 
 
-def _request_with_retry(
+def _request_wrapper(
     method: str,
     url: str,
     *,
@@ -373,34 +377,82 @@ def _request_with_retry(
     base_wait_time: float = 0.5,
     max_wait_time: float = 2,
     timeout: float = 10.0,
+    follow_relative_redirects: bool = False,
     **params,
 ) -> requests.Response:
-    """Wrapper around requests to retry in case it fails with a `ConnectTimeout`, with
-    exponential backoff.
-
-        Note that if the environment variable HF_HUB_OFFLINE is set to 1, then a
-        `OfflineModeIsEnabled` error is raised.
-
-        Args:
-            method (`str`):
-                HTTP method, such as 'GET' or 'HEAD'.
-            url (`str`):
-                The URL of the resource to fetch.
-            max_retries (`int`, *optional*, defaults to `0`):
-                Maximum number of retries, defaults to 0 (no retries).
-            base_wait_time (`float`, *optional*, defaults to `0.5`):
-                Duration (in seconds) to wait before retrying the first time.
-                Wait time between retries then grows exponentially, capped by
-                `max_wait_time`.
-            max_wait_time (`float`, *optional*, defaults to `2`):
-                Maximum amount of time between two retries, in seconds.
-            timeout (`float`, *optional*, defaults to `10`):
-                How many seconds to wait for the server to send data before
-                giving up which is passed to `requests.request`.
-            **params (`dict`, *optional*):
-                Params to pass to `requests.request`.
+    """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 redirections if `follow_relative_redirects=True` even when
+       `allow_redirection` kwarg is set to False.
+    3. Retry in case request fails with a `ConnectTimeout`, with exponential backoff.
+
+    Args:
+        method (`str`):
+            HTTP method, such as 'GET' or 'HEAD'.
+        url (`str`):
+            The URL of the resource to fetch.
+        max_retries (`int`, *optional*, defaults to `0`):
+            Maximum number of retries, defaults to 0 (no retries).
+        base_wait_time (`float`, *optional*, defaults to `0.5`):
+            Duration (in seconds) to wait before retrying the first time.
+            Wait time between retries then grows exponentially, capped by
+            `max_wait_time`.
+        max_wait_time (`float`, *optional*, defaults to `2`):
+            Maximum amount of time between two retries, in seconds.
+        timeout (`float`, *optional*, defaults to `10`):
+            How many seconds to wait for the server to send data before
+            giving up which is passed to `requests.request`.
+        follow_relative_redirects (`bool`, *optional*, defaults to `False`)
+            If True, relative redirection (redirection to the same site) will be
+            resolved even when `allow_redirection` kwarg is set to False. Useful when we
+            want to follow a redirection to a renamed repository without following
+            redirection to a CDN.
+        **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
+    if follow_relative_redirects:
+        response = _request_wrapper(
+            method=method,
+            url=url,
+            max_retries=max_retries,
+            base_wait_time=base_wait_time,
+            max_wait_time=max_wait_time,
+            timeout=timeout,
+            follow_relative_redirects=False,
+            **params,
+        )
+
+        # If redirection, we redirect only relative paths.
+        # This is useful in case of a renamed repository.
+        if 300 <= response.status_code <= 399:
+            parsed_target = urlparse(response.headers["Location"])
+            if parsed_target.netloc == "":
+                # This means it is a relative 'location' headers, as allowed by RFC 7231.
+                # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
+                # We want to follow this relative redirect !
+                #
+                # Highly inspired by `resolve_redirects` from requests library.
+                # See https://github.com/psf/requests/blob/main/requests/sessions.py#L159
+                return _request_wrapper(
+                    method=method,
+                    url=urlparse(url)._replace(path=parsed_target.path).geturl(),
+                    max_retries=max_retries,
+                    base_wait_time=base_wait_time,
+                    max_wait_time=max_wait_time,
+                    timeout=timeout,
+                    follow_relative_redirects=True,  # resolve recursively
+                    **params,
+                )
+        return response
+
+    # 3. Exponential backoff
     tries, success = 0, False
     while not success:
         tries += 1
@@ -424,6 +476,14 @@ def _request_with_retry(
     return response
 
 
+def _request_with_retry(*args, **kwargs) -> requests.Response:
+    """Deprecated method. Please use `_request_wrapper` instead.
+
+    Alias to keep backward compatibility (used in Transformers).
+    """
+    return _request_wrapper(*args, **kwargs)
+
+
 def http_get(
     url: str,
     temp_file: BinaryIO,
@@ -440,7 +500,7 @@ def http_get(
     headers = copy.deepcopy(headers)
     if resume_size > 0:
         headers["Range"] = "bytes=%d-" % (resume_size,)
-    r = _request_with_retry(
+    r = _request_wrapper(
         method="GET",
         url=url,
         stream=True,
@@ -593,11 +653,12 @@ def cached_download(
     etag = None
     if not local_files_only:
         try:
-            r = _request_with_retry(
+            r = _request_wrapper(
                 method="HEAD",
                 url=url,
                 headers=headers,
                 allow_redirects=False,
+                follow_relative_redirects=True,
                 proxies=proxies,
                 timeout=etag_timeout,
             )
@@ -611,10 +672,10 @@ def cached_download(
                     "Distant resource does not have an ETag, we won't be able to"
                     " reliably ensure reproducibility."
                 )
-            # In case of a redirect,
-            # save an extra redirect on the request.get call,
+            # In case of a redirect, save an extra redirect on the request.get call,
             # and ensure we download the exact atomic version even if it changed
             # between the HEAD and the GET (unlikely, but hey).
+            # Useful for lfs blobs that are stored on a CDN.
             if 300 <= r.status_code <= 399:
                 url_to_download = r.headers["Location"]
         except (requests.exceptions.SSLError, requests.exceptions.ProxyError):
@@ -787,6 +848,20 @@ def _create_relative_symlink(src: str, dst: str) -> None:
             raise
 
 
+def _cache_commit_hash_for_specific_revision(
+    storage_folder: str, revision: str, commit_hash: str
+) -> None:
+    """Cache reference between a revision (tag, branch or truncated commit hash) and the corresponding commit hash.
+
+    Does nothing if `revision` is already a proper `commit_hash` or reference is already cached.
+    """
+    if revision != commit_hash:
+        ref_path = os.path.join(storage_folder, "refs", revision)
+        os.makedirs(os.path.dirname(ref_path), exist_ok=True)
+        with open(ref_path, "w") as f:
+            f.write(commit_hash)
+
+
 def repo_folder_name(*, repo_id: str, repo_type: str) -> str:
     """Return a serialized version of a hf.co repo name and type, safe for disk storage
     as a single non-nested folder.
@@ -1011,15 +1086,32 @@ def hf_hub_download(
     commit_hash = None
     if not local_files_only:
         try:
-            r = _request_with_retry(
+            r = _request_wrapper(
                 method="HEAD",
                 url=url,
                 headers=headers,
                 allow_redirects=False,
+                follow_relative_redirects=True,
                 proxies=proxies,
                 timeout=etag_timeout,
             )
-            _raise_for_status(r)
+            try:
+                _raise_for_status(r)
+            except EntryNotFoundError:
+                commit_hash = r.headers.get(HUGGINGFACE_HEADER_X_REPO_COMMIT)
+                if commit_hash is not None and not legacy_cache_layout:
+                    no_exist_file_path = (
+                        Path(storage_folder)
+                        / ".no_exist"
+                        / commit_hash
+                        / relative_filename
+                    )
+                    no_exist_file_path.parent.mkdir(parents=True, exist_ok=True)
+                    no_exist_file_path.touch()
+                    _cache_commit_hash_for_specific_revision(
+                        storage_folder, revision, commit_hash
+                    )
+                raise
             commit_hash = r.headers[HUGGINGFACE_HEADER_X_REPO_COMMIT]
             if commit_hash is None:
                 raise OSError(
@@ -1038,10 +1130,10 @@ def hf_hub_download(
                     " reliably ensure reproducibility."
                 )
             etag = _normalize_etag(etag)
-            # In case of a redirect,
-            # save an extra redirect on the request.get call,
+            # In case of a redirect, save an extra redirect on the request.get call,
             # and ensure we download the exact atomic version even if it changed
             # between the HEAD and the GET (unlikely, but hey).
+            # Useful for lfs blobs that are stored on a CDN.
             if 300 <= r.status_code <= 399:
                 url_to_download = r.headers["Location"]
                 if (
@@ -1115,11 +1207,7 @@ def hf_hub_download(
     # if passed revision is not identical to commit_hash
     # then revision has to be a branch name or tag name.
     # In that case store a ref.
-    if revision != commit_hash:
-        ref_path = os.path.join(storage_folder, "refs", revision)
-        os.makedirs(os.path.dirname(ref_path), exist_ok=True)
-        with open(ref_path, "w") as f:
-            f.write(commit_hash)
+    _cache_commit_hash_for_specific_revision(storage_folder, revision, commit_hash)
 
     if os.path.exists(pointer_path) and not force_download:
         return pointer_path


Test patch:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index e95320c2..779b9f19 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -5,14 +5,8 @@ on:
     branches:
       - main
       - ci_*
-    paths-ignore:
-      - "js/**"
-      - "api-inference-community/**"
   pull_request:
     types: [assigned, opened, synchronize, reopened]
-    paths-ignore:
-      - "js/**"
-      - "api-inference-community/**"
 
 env:
   HUGGINGFACE_CO_STAGING: yes
@@ -21,117 +15,90 @@ jobs:
   build:
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
         python-version: ["3.7", "3.10"]
-        test_repository: ["Repository only", "Everything else"]
+        test_name: ["Repository only", "Everything else", "fastai", "torch", "tensorflow"]
+        include:
+          - python-version: "3.10" # LFS not ran on 3.7
+            test_name: "lfs"
 
     steps:
       - uses: actions/checkout@v2
-
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python-version }}
 
-      - name: Install dependencies
+      # Install dependencies
+      - name: Configure and install dependencies
         run: |
-          sudo apt install -y libsndfile1-dev
           pip install --upgrade pip
           pip install .[testing]
 
-      - run: |
-          if [[ "${{ matrix.test_repository }}" == "Repository only" ]]
-          then
-            pytest --log-cli-level=INFO -sv ./tests/test_repository.py -k RepositoryTest
+          case "${{ matrix.test_name }}" in
 
-          else
-            pytest --log-cli-level=INFO -sv ./tests/ -k 'not RepositoryTest'
-          fi
+            "Repository only" | "Everything else")
+              sudo apt install -y libsndfile1-dev
+              ;;
 
-  build_pytorch:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ["3.7", "3.10"]
-
-    steps:
-    - uses: actions/checkout@v2
+            lfs)
+              git config --global user.email "ci@dummy.com"
+              git config --global user.name "ci"
+              ;;
 
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
+            fastai | torch)
+              pip install .[${{ matrix.test_name }}]
+              ;;
 
-    - name: Install dependencies
-      run: |
-        pip install --upgrade pip
-        pip install .[testing,torch]
+            tensorflow)
+              sudo apt install -y graphviz
+              pip install .[tensorflow]
+              ;;
 
-    - run: pytest -sv ./tests/test_hubmixin*
+          esac
 
-  build_tensorflow:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ["3.7", "3.10"]
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-
-    - name: Install dependencies
-      run: |
-        sudo apt install -y graphviz
-        pip install --upgrade pip
-        pip install .[testing,tensorflow]
-
-    - run: pytest -sv ./tests/test_tf*
-
-    - run: pytest -sv ./tests/test_keras*
+      # Run tests
+      - name: Run tests
+        working-directory: ./src # For code coverage to work
+        run: |
+          PYTEST="python -m pytest -sv --log-cli-level=INFO --cov=./huggingface_hub --cov-report=xml:../coverage.xml"
 
-  build_fastai:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ["3.7", "3.9"]
+          case "${{ matrix.test_name }}" in
 
-    steps:
-    - uses: actions/checkout@v2
+            "Repository only")
+              eval "$PYTEST ../tests -k 'RepositoryTest'"
+              ;;
 
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
+            "Everything else")
+              eval "$PYTEST ../tests -k 'not RepositoryTest'"
+              ;;
 
-    - name: Install dependencies
-      run: |
-        pip install --upgrade pip
-        pip install .[testing,fastai]
+            lfs)
+              eval "RUN_GIT_LFS_TESTS=1 $PYTEST ../tests -k 'HfLargefilesTest'"
+            ;;
 
-    - run: pytest -sv ./tests/test_fastai*
 
-  tests_lfs:
-    runs-on: ubuntu-latest
+            fastai)
+              eval "$PYTEST ../tests/test_fastai*"
+            ;;
 
-    steps:
-    - uses: actions/checkout@v2
+            tensorflow)
+              # Cannot be on same line since '_tf*' checks if tensorflow is NOT imported by default
+              eval "$PYTEST ../tests/test_tf*"
+              eval "$PYTEST ../tests/test_keras*"
+            ;;
 
-    - name: Set up Python
-      uses: actions/setup-python@v2
-      with:
-        python-version: "3.10"
+            torch)
+              eval "$PYTEST ../tests/test_hubmixin*"
+            ;;
 
-    - run: |
-        git config --global user.email "ci@dummy.com"
-        git config --global user.name "ci"
+          esac
 
-    - name: Install dependencies
-      run: |
-        pip install --upgrade pip
-        pip install .[testing]
 
-    - run: RUN_GIT_LFS_TESTS=1 pytest -sv ./tests/ -k "HfLargefilesTest"
+      # Upload code coverage
+      - name: Upload coverage reports to Codecov with GitHub Action
+        uses: codecov/codecov-action@v3
+        with:
+          files: ./coverage.xml
+          verbose: true
\ No newline at end of file
diff --git a/tests/test_cache_layout.py b/tests/test_cache_layout.py
index df152645..74dfb8f1 100644
--- a/tests/test_cache_layout.py
+++ b/tests/test_cache_layout.py
@@ -13,6 +13,7 @@ from huggingface_hub import (
     upload_file,
 )
 from huggingface_hub.utils import logging
+from huggingface_hub.utils._errors import EntryNotFoundError
 
 from .testing_constants import ENDPOINT_STAGING, TOKEN, USER
 from .testing_utils import repo_name, with_production_testing
@@ -32,92 +33,109 @@ def get_file_contents(path):
 @with_production_testing
 class CacheFileLayoutHfHubDownload(unittest.TestCase):
     def test_file_downloaded_in_cache(self):
-        with tempfile.TemporaryDirectory() as cache:
-            hf_hub_download(MODEL_IDENTIFIER, "file_0.txt", cache_dir=cache)
-
-            expected_directory_name = f'models--{MODEL_IDENTIFIER.replace("/", "--")}'
-            expected_path = os.path.join(cache, expected_directory_name)
-
-            refs = os.listdir(os.path.join(expected_path, "refs"))
-            snapshots = os.listdir(os.path.join(expected_path, "snapshots"))
-
-            expected_reference = "main"
-
-            # Only reference should be `main`.
-            self.assertListEqual(refs, [expected_reference])
-
-            with open(os.path.join(expected_path, "refs", expected_reference)) as f:
-                snapshot_name = f.readline().strip()
-
-            # The `main` reference should point to the only snapshot we have downloaded
-            self.assertListEqual(snapshots, [snapshot_name])
+        for revision, expected_reference in (
+            (None, "main"),
+            ("file-2", "file-2"),
+        ):
+            with self.subTest(revision), tempfile.TemporaryDirectory() as cache:
+                with tempfile.TemporaryDirectory() as cache:
+                    hf_hub_download(
+                        MODEL_IDENTIFIER,
+                        "file_0.txt",
+                        cache_dir=cache,
+                        revision=revision,
+                    )
 
-            snapshot_path = os.path.join(expected_path, "snapshots", snapshot_name)
-            snapshot_content = os.listdir(snapshot_path)
+                    expected_directory_name = (
+                        f'models--{MODEL_IDENTIFIER.replace("/", "--")}'
+                    )
+                    expected_path = os.path.join(cache, expected_directory_name)
 
-            # Only a single file in the snapshot
-            self.assertEqual(len(snapshot_content), 1)
+                    refs = os.listdir(os.path.join(expected_path, "refs"))
+                    snapshots = os.listdir(os.path.join(expected_path, "snapshots"))
 
-            snapshot_content_path = os.path.join(snapshot_path, snapshot_content[0])
+                    # Only reference should be the expected one.
+                    self.assertListEqual(refs, [expected_reference])
 
-            # The snapshot content should link to a blob
-            self.assertTrue(os.path.islink(snapshot_content_path))
+                    with open(
+                        os.path.join(expected_path, "refs", expected_reference)
+                    ) as f:
+                        snapshot_name = f.readline().strip()
 
-            resolved_blob_relative = os.readlink(snapshot_content_path)
-            resolved_blob_absolute = os.path.normpath(
-                os.path.join(snapshot_path, resolved_blob_relative)
-            )
+                    # The `main` reference should point to the only snapshot we have downloaded
+                    self.assertListEqual(snapshots, [snapshot_name])
 
-            with open(resolved_blob_absolute) as f:
-                blob_contents = f.read().strip()
+                    snapshot_path = os.path.join(
+                        expected_path, "snapshots", snapshot_name
+                    )
+                    snapshot_content = os.listdir(snapshot_path)
 
-            # The contents of the file should be 'File 0'.
-            self.assertEqual(blob_contents, "File 0")
+                    # Only a single file in the snapshot
+                    self.assertEqual(len(snapshot_content), 1)
 
-    def test_file_downloaded_in_cache_with_revision(self):
-        with tempfile.TemporaryDirectory() as cache:
-            hf_hub_download(
-                MODEL_IDENTIFIER, "file_0.txt", cache_dir=cache, revision="file-2"
-            )
+                    snapshot_content_path = os.path.join(
+                        snapshot_path, snapshot_content[0]
+                    )
 
-            expected_directory_name = f'models--{MODEL_IDENTIFIER.replace("/", "--")}'
-            expected_path = os.path.join(cache, expected_directory_name)
+                    # The snapshot content should link to a blob
+                    self.assertTrue(os.path.islink(snapshot_content_path))
 
-            refs = os.listdir(os.path.join(expected_path, "refs"))
-            snapshots = os.listdir(os.path.join(expected_path, "snapshots"))
+                    resolved_blob_relative = os.readlink(snapshot_content_path)
+                    resolved_blob_absolute = os.path.normpath(
+                        os.path.join(snapshot_path, resolved_blob_relative)
+                    )
 
-            expected_reference = "file-2"
+                    with open(resolved_blob_absolute) as f:
+                        blob_contents = f.readline().strip()
+
+                    # The contents of the file should be 'File 0'.
+                    self.assertEqual(blob_contents, "File 0")
+
+    def test_no_exist_file_is_cached(self):
+        revisions = [None, "file-2"]
+        expected_references = ["main", "file-2"]
+        for revision, expected_reference in zip(revisions, expected_references):
+            with self.subTest(revision), tempfile.TemporaryDirectory() as cache:
+                filename = "this_does_not_exist.txt"
+                with self.assertRaises(EntryNotFoundError):
+                    # The file does not exist, so we get an exception.
+                    hf_hub_download(
+                        MODEL_IDENTIFIER, filename, cache_dir=cache, revision=revision
+                    )
 
-            # Only reference should be `file-2`.
-            self.assertListEqual(refs, [expected_reference])
+                expected_directory_name = (
+                    f'models--{MODEL_IDENTIFIER.replace("/", "--")}'
+                )
+                expected_path = os.path.join(cache, expected_directory_name)
 
-            with open(os.path.join(expected_path, "refs", expected_reference)) as f:
-                snapshot_name = f.read().strip()
+                refs = os.listdir(os.path.join(expected_path, "refs"))
+                no_exist_snapshots = os.listdir(
+                    os.path.join(expected_path, ".no_exist")
+                )
 
-            # The `main` reference should point to the only snapshot we have downloaded
-            self.assertListEqual(snapshots, [snapshot_name])
+                # Only reference should be `main`.
+                self.assertListEqual(refs, [expected_reference])
 
-            snapshot_path = os.path.join(expected_path, "snapshots", snapshot_name)
-            snapshot_content = os.listdir(snapshot_path)
+                with open(os.path.join(expected_path, "refs", expected_reference)) as f:
+                    snapshot_name = f.readline().strip()
 
-            # Only a single file in the snapshot
-            self.assertEqual(len(snapshot_content), 1)
+                # The `main` reference should point to the only snapshot we have downloaded
+                self.assertListEqual(no_exist_snapshots, [snapshot_name])
 
-            snapshot_content_path = os.path.join(snapshot_path, snapshot_content[0])
+                no_exist_path = os.path.join(expected_path, ".no_exist", snapshot_name)
+                no_exist_content = os.listdir(no_exist_path)
 
-            # The snapshot content should link to a blob
-            self.assertTrue(os.path.islink(snapshot_content_path))
+                # Only a single file in the no_exist snapshot
+                self.assertEqual(len(no_exist_content), 1)
 
-            resolved_blob_relative = os.readlink(snapshot_content_path)
-            resolved_blob_absolute = os.path.normpath(
-                os.path.join(snapshot_path, resolved_blob_relative)
-            )
+                # The no_exist content should be our file
+                self.assertEqual(no_exist_content[0], filename)
 
-            with open(resolved_blob_absolute) as f:
-                blob_contents = f.readline().strip()
+                with open(os.path.join(no_exist_path, filename)) as f:
+                    content = f.read().strip()
 
-            # The contents of the file should be 'File 0'.
-            self.assertEqual(blob_contents, "File 0")
+                # The contents of the file should be empty.
+                self.assertEqual(content, "")
 
     def test_file_download_happens_once(self):
         # Tests that a file is only downloaded once if it's not updated.
diff --git a/tests/test_file_download.py b/tests/test_file_download.py
index dde36720..2abd0f7b 100644
--- a/tests/test_file_download.py
+++ b/tests/test_file_download.py
@@ -42,6 +42,7 @@ from .testing_utils import (
     DUMMY_MODEL_ID_PINNED_SHA256,
     DUMMY_MODEL_ID_REVISION_INVALID,
     DUMMY_MODEL_ID_REVISION_ONE_SPECIFIC_COMMIT,
+    DUMMY_RENAMED_MODEL_ID,
     SAMPLE_DATASET_IDENTIFIER,
     OfflineSimulationMode,
     offline,
@@ -217,6 +218,35 @@ class CachedDownloadTests(unittest.TestCase):
             (url, '"95aa6a52d5d6a735563366753ca50492a658031da74f301ac5238b03966972c9"'),
         )
 
+    def test_download_from_a_renamed_repo_with_hf_hub_download(self):
+        """Checks `hf_hub_download` works also on a renamed repo.
+
+        Regression test for #981.
+        https://github.com/huggingface/huggingface_hub/issues/981
+        """
+        with TemporaryDirectory() as tmpdir:
+            filepath = hf_hub_download(
+                DUMMY_RENAMED_MODEL_ID, "config.json", cache_dir=tmpdir
+            )
+            self.assertTrue(os.path.exists(filepath))
+
+    def test_download_from_a_renamed_repo_with_cached_download(self):
+        """Checks `cached_download` works also on a renamed repo.
+
+        Regression test for #981.
+        https://github.com/huggingface/huggingface_hub/issues/981
+        """
+        with pytest.warns(FutureWarning):
+            with TemporaryDirectory() as tmpdir:
+                filepath = cached_download(
+                    hf_hub_url(
+                        DUMMY_RENAMED_MODEL_ID,
+                        filename="config.json",
+                    ),
+                    cache_dir=tmpdir,
+                )
+                self.assertTrue(os.path.exists(filepath))
+
     def test_hf_hub_download_legacy(self):
         filepath = hf_hub_download(
             DUMMY_MODEL_ID,
diff --git a/tests/testing_utils.py b/tests/testing_utils.py
index c2b134d9..58188c89 100644
--- a/tests/testing_utils.py
+++ b/tests/testing_utils.py
@@ -34,6 +34,10 @@ DUMMY_MODEL_ID_PINNED_SHA256 = (
 )
 # Sha-256 of pytorch_model.bin on the top of `main`, for checking purposes
 
+# "hf-internal-testing/dummy-will-be-renamed" has been renamed to "hf-internal-testing/dummy-renamed"
+DUMMY_RENAMED_MODEL_ID = (  # Regression test #941
+    "hf-internal-testing/dummy-will-be-renamed"
+)
 
 SAMPLE_DATASET_IDENTIFIER = "lhoestq/custom_squad"
 # Example dataset ids