Repository: huggingface/huggingface_hub
Pull request #1303 (2023-01-20 17:20:41)

Revisit "search the hub" page (+HfApi reference page)
Related to https://github.com/huggingface/huggingface_hub/issues/1250, https://github.com/huggingface/huggingface_hub/issues/1280 and https://github.com/huggingface/huggingface_hub/pull/1300.

`ModelSearchArguments` and `DatasetSearchArguments` are only half-maintained and caused quite some trouble recently. Additionally they are now quite innefficient to instantiate as they require to load all models/datasets from the Hub before filtering them. The PR aims to show that those helpers can be useful but are optional.

Also in the same PR I revisited a bit the HfApi reference page to make it less verbose and more structured on the right tab.

- [Search the hub guide](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1303/en/searching-the-hub#search-the-hub)
- [HfApi reference page](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1303/en/package_reference/hf_api#hfapi-client)

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

Patch:
diff --git a/docs/source/how-to-cache.mdx b/docs/source/how-to-cache.mdx
index b72b5384..f3b98615 100644
--- a/docs/source/how-to-cache.mdx
+++ b/docs/source/how-to-cache.mdx
@@ -87,6 +87,46 @@ That `README.md` file is actually a symlink linking to the blob that has the has
 By creating the skeleton this way we open the mechanism to file sharing: if the same file was fetched in
 revision `bbbbbb`, it would have the same hash and the file would not need to be re-downloaded.
 
+### .no_exist (advanced)
+
+In addition to the `blobs`, `refs` and `snapshots` folders, you might also find a `.no_exist` folder
+in your cache. This folder keeps track of files that you've tried to download once but don't exist
+on the Hub. Its structure is the same as the `snapshots` folder with 1 subfolder per known revision:
+
+```
+<CACHE_DIR>/<REPO_NAME>/.no_exist/aaaaaa/config_that_does_not_exist.json
+```
+
+Unlike the `snapshots` folder, files are simple empty files (no symlinks). In this example,
+the file `"config_that_does_not_exist.json"` does not exist on the Hub for the revision `"aaaaaa"`.
+As it only stores empty files, this folder is neglectable is term of disk usage.
+
+So now you might wonder, why is this information even relevant?
+In some cases, a framework tries to load optional files for a model. Saving the non-existence
+of optional files makes it faster to load a model as it saves 1 HTTP call per possible optional file.
+This is for example the case in `transformers` where each tokenizer can support additional files.
+The first time you load the tokenizer on your machine, it will cache which optional files exists (and
+which doesn't) to make the loading time faster for the next initializations.
+
+To test if a file is cached locally (without making any HTTP request), you can use the [`try_to_load_from_cache`]
+helper. It will either return the filepath (if exists and cached), the object `_CACHED_NO_EXIST` (if non-existence
+is cached) or `None` (if we don't know).
+
+```python
+from huggingface_hub import try_to_load_from_cache, _CACHED_NO_EXIST
+
+filepath = try_to_load_from_cache()
+if isinstance(filepath, str):
+    # file exists and is cached
+    ...
+elif filepath is _CACHED_NO_EXIST:
+    # non-existence of file is cached
+    ...
+else:
+    # file is not cached
+    ...
+```
+
 ### In practice
 
 In practice, your cache should look like the following tree:
diff --git a/docs/source/package_reference/cache.mdx b/docs/source/package_reference/cache.mdx
index 2fb86bf0..709564f1 100644
--- a/docs/source/package_reference/cache.mdx
+++ b/docs/source/package_reference/cache.mdx
@@ -6,7 +6,11 @@ for a detailed presentation of caching at HF.
 
 ## Helpers
 
-## cached_assets_path
+### try_to_load_from_cache
+
+[[autodoc]] huggingface_hub.try_to_load_from_cache
+
+### cached_assets_path
 
 [[autodoc]] huggingface_hub.cached_assets_path
 
diff --git a/docs/source/package_reference/hf_api.mdx b/docs/source/package_reference/hf_api.mdx
index c5a6e09f..05faf47c 100644
--- a/docs/source/package_reference/hf_api.mdx
+++ b/docs/source/package_reference/hf_api.mdx
@@ -1,84 +1,71 @@
-# Hugging Face Hub API
+# HfApi Client
 
-Below is the documentation for the `HfApi` class, which serves as a Python wrapper for the Hugging Face
-Hub's API.
+Below is the documentation for the `HfApi` class, which serves as a Python wrapper for the Hugging Face Hub's API.
 
-All methods from the `HfApi` are also accessible from the package's root directly, both approaches are detailed
-below.
+All methods from the `HfApi` are also accessible from the package's root directly. Both approaches are detailed below.
 
-The following approach uses the method from the root of the package:
+Using the root method is more straightforward but the [`HfApi`] class gives you more flexibility.
+In particular, you can pass a token that will be reused in all HTTP calls. This is different
+than `huggingface-cli login` or [`login`] as the token is not persisted on the machine.
+It is also possible to provide a different endpoint or configure a custom user-agent.
 
 ```python
-from huggingface_hub import list_models
+from huggingface_hub import HfApi, list_models
 
+# Use root method
 models = list_models()
-```
-
-The following approach uses the `HfApi` class:
-
-```python
-from huggingface_hub import HfApi
-
-hf_api = HfApi()
-models = hf_api.list_models()
-```
-
-Using the [`HfApi`] class directly enables you to configure the client. In particular, a
-token can be passed to be authenticated in all API calls. This is different than
-`huggingface-cli login` or [`login`] as the token is not persisted on the machine. One
-can also specify a different endpoint than the Hugging Face's Hub (for example to interact
-with a Private Hub).
-
-```py
-from huggingface_hub import HfApi
 
+# Or configure a HfApi client
 hf_api = HfApi(
     endpoint="https://huggingface.co", # Can be a Private Hub endpoint.
     token="hf_xxx", # Token is not persisted on the machine.
 )
+models = hf_api.list_models()
 ```
 
-### HfApi
+## HfApi
 
 [[autodoc]] HfApi
 
-### RepoUrl
+## API Dataclasses
 
-[[autodoc]] huggingface_hub.hf_api.RepoUrl
-
-### ModelInfo
+### CommitInfo
 
-[[autodoc]] huggingface_hub.hf_api.ModelInfo
+[[autodoc]] huggingface_hub.hf_api.CommitInfo
 
 ### DatasetInfo
 
 [[autodoc]] huggingface_hub.hf_api.DatasetInfo
 
-### SpaceInfo
-
-[[autodoc]] huggingface_hub.hf_api.SpaceInfo
-
-### RepoFile
+### GitRefInfo
 
-[[autodoc]] huggingface_hub.hf_api.RepoFile
+[[autodoc]] huggingface_hub.hf_api.GitRefInfo
 
 ### GitRefs
 
 [[autodoc]] huggingface_hub.hf_api.GitRefs
 
-### GitRefInfo
+### ModelInfo
 
-[[autodoc]] huggingface_hub.hf_api.GitRefInfo
+[[autodoc]] huggingface_hub.hf_api.ModelInfo
 
-### CommitInfo
+### RepoFile
 
-[[autodoc]] huggingface_hub.hf_api.CommitInfo
+[[autodoc]] huggingface_hub.hf_api.RepoFile
+
+### RepoUrl
+
+[[autodoc]] huggingface_hub.hf_api.RepoUrl
+
+### SpaceInfo
+
+[[autodoc]] huggingface_hub.hf_api.SpaceInfo
 
 ### UserLikes
 
 [[autodoc]] huggingface_hub.hf_api.UserLikes
 
-## `create_commit` API
+## CommitOperation
 
 Below are the supported values for [`CommitOperation`]:
 
@@ -86,7 +73,7 @@ Below are the supported values for [`CommitOperation`]:
 
 [[autodoc]] CommitOperationDelete
 
-## Hugging Face local storage
+## Token helper
 
 `huggingface_hub` stores the authentication information locally so that it may be re-used in subsequent
 methods.
@@ -95,7 +82,7 @@ It does this using the [`HfFolder`] utility, which saves data at the root of the
 
 [[autodoc]] HfFolder
 
-## Filtering helpers
+## Search helpers
 
 Some helpers to filter repositories on the Hub are available in the `huggingface_hub` package.
 
diff --git a/docs/source/searching-the-hub.mdx b/docs/source/searching-the-hub.mdx
index a78b0352..aa925944 100644
--- a/docs/source/searching-the-hub.mdx
+++ b/docs/source/searching-the-hub.mdx
@@ -1,40 +1,70 @@
-# Searching the Hub Efficiently with Python
+# Searching the Hub
 
-In this tutorial, we will explore how to interact and explore the Hugging Face Hub with the `huggingface_hub` library to help find available models and datasets quickly.
+In this tutorial, you will learn how to search models, datasets and spaces on the Hub using `huggingface_hub`.
 
-## The Basics
+## How to list repositories ?
 
-`huggingface_hub` is a Python library that allows anyone to freely extract useful information from the Hub, as well as downloading and publishing models. You can install it with:
+`huggingface_hub` library includes an HTTP client [`HfApi`] to interact with the Hub.
+Among other things, it can list models, datasets and spaces stored on the Hub:
 
-
-```bash
-pip install huggingface_hub
-```
-
-It comes packaged with an interface that can interact with the Hub in the [`HfApi`] class:
-
-
-```python
+```py
 >>> from huggingface_hub import HfApi
 >>> api = HfApi()
+>>> models = api.list_models()
 ```
 
-This class lets you perform a variety of operations that interact with the raw Hub API. We'll be focusing on two specific functions:
-- [`list_models`]
-- [`list_datasets`]
+The output of [`list_models`] is an iterator over the models stored on the Hub.
+
+Similarly, you can use [`list_datasets`] to list datasets and [`list_spaces`] to list Spaces.
 
-If you look at what can be passed into each function, you will find the parameter list looks something like:
+## How to filter repositories ?
+
+Listing repositories is great but now you might want to filter your search.
+The list helpers have several attributes like:
 - `filter`
 - `author`
 - `search`
 - ...
 
-Two of these parameters are intuitive (`author` and `search`), but what about that `filter`? 🤔 Let's dive into a few helpers quickly and revisit that question.
+Two of these parameters are intuitive (`author` and `search`), but what about that `filter`?
+`filter` takes as input a [`ModelFilter`] object (or [`DatasetFilter`]). You can instantiate
+it by specifying which models you want to filter. 
+
+Let's see an example to get all models on the Hub that does image classification, have been
+trained on the imagenet dataset and that runs with PyTorch. That can be done with a single
+[`ModelFilter`]. Attributes are combined as "logical AND".
+
+```py
+models = hf_api.list_models(
+    filter=ModelFilter(
+		task="image-classification",
+		library="pytorch",
+		trained_dataset="imagenet"
+	)
+)
+```
+
+While filtering, you can also sort the models and take only the top results. For example,
+the following example fetches the top 5 most downloaded datasets on the Hub:
 
-## Search Parameters
+```py
+>>> list_datasets(sort="downloads", direction=-1, limit=5)
+[DatasetInfo: {
+        id: glue
+		downloads: 897789
+		(...)
+```
 
-The `huggingface_hub` provides a user-friendly interface to know what exactly can be passed into this `filter` parameter through the [`ModelSearchArguments`] and [`DatasetSearchArguments`] classes:
 
+## How to explore filter options ?
+
+Now you know how to filter your list of models/datasets/spaces. The problem you might
+have is that you don't know exactly what you are looking for. No worries! We also provide
+some helpers that allows you to discover what arguments can be passed in your query.
+
+[`ModelSearchArguments`] and [`DatasetSearchArguments`] are nested namespace objects that
+have **every single option** available on the Hub and that will return what should be passed
+to `filter`. The best of all is: it has tab completion 🎊 .
 
 ```python
 >>> from huggingface_hub import ModelSearchArguments, DatasetSearchArguments
@@ -43,24 +73,17 @@ The `huggingface_hub` provides a user-friendly interface to know what exactly ca
 >>> dataset_args = DatasetSearchArguments()
 ```
 
-These are nested namespace objects that have **every single option** available on the Hub and that will return what should be passed to `filter`. The best of all is: it has tab completion 🎊 .
-
 <Tip warning={true}>
 
-[`ModelSearchArguments`] and [`DatasetSearchArguments`] are legacy helpers meant for exploratory
-purposes only. Their initialization require listing all models and datasets on the Hub which
-makes them increasingly slower as the number of repos on the Hub increases. For some production-ready code,
-consider passing raw strings when making a filtered search on the Hub.
+Before continuing, please we aware that [`ModelSearchArguments`] and [`DatasetSearchArguments`]
+are legacy helpers meant for exploratory purposes only. Their initialization require listing
+all models and datasets on the Hub which makes them increasingly slower as the number of repos
+on the Hub increases. For some production-ready code, consider passing raw strings when making
+a filtered search on the Hub.
 
 </Tip>
 
-## Searching for a Model
-
-Let's pose a problem that would be complicated to solve without access to this information:
-> I want to search the Hub for all PyTorch models trained on the `glue` dataset that can do Text Classification.
-
-If you check what is available in `model_args` by checking it's output, you will find:
-
+Now, let's check what is available in `model_args` by checking it's output, you will find:
 
 ```python
 >>> model_args
@@ -74,11 +97,11 @@ Available Attributes or Keys:
  * pipeline_tag
 ```
 
-It has a variety of attributes or keys available to you. This is because it is both an object and a dictionary, so you can either do `model_args["author"]` or `model_args.author`. For this tutorial, let's follow the latter format.
+It has a variety of attributes or keys available to you. This is because it is both an object
+and a dictionary, so you can either do `model_args["author"]` or `model_args.author`.
 
 The first criteria is getting all PyTorch models. This would be found under the `library` attribute, so let's see if it is there:
 
-
 ```python
 >>> model_args.library
 Available Attributes or Keys:
@@ -113,7 +136,6 @@ Available Attributes or Keys:
 
 It is! The `PyTorch` name is there, so you'll need to use `model_args.library.PyTorch`:
 
-
 ```python
 >>> model_args.library.PyTorch
 'pytorch'
@@ -125,7 +147,8 @@ Below is an animation repeating the process for finding both the `Text Classific
 
 ![Animation exploring `model_args.dataset`](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/search_glue.gif)
 
-Now that all the pieces are there, the last step is to combine them all for something the API can use through the [`ModelFilter`] and [`DatasetFilter`] classes. The classes transform the outputs of the previous step into something the API can use conveniently:
+Now that all the pieces are there, the last step is to combine them all for something the
+API can use through the [`ModelFilter`] and [`DatasetFilter`] classes (i.e. strings).
 
 
 ```python
@@ -153,14 +176,16 @@ ModelInfo: {
 }
 ```
 
-As you can see, it found the models that fit all the criteria. You can even take it further by passing in an array for each of the parameters from before. For example, let's take a look for the same configuration, but also include `TensorFlow` in the filter:
+As you can see, it found the models that fit all the criteria. You can even take it further
+by passing in an array for each of the parameters from before. For example, let's take a look
+for the same configuration, but also include `TensorFlow` in the filter:
 
 
 ```python
 >>> filt = ModelFilter(
 ...     task=model_args.pipeline_tag.TextClassification, 
 ...     library=[model_args.library.PyTorch, model_args.library.TensorFlow]
->>> )
+... )
 >>> api.list_models(filter=filt)[0]
 ModelInfo: {
 	modelId: distilbert-base-uncased-finetuned-sst-2-english
@@ -178,156 +203,17 @@ ModelInfo: {
 }
 ```
 
-## Searching for a Dataset
-
-Similarly to finding a model, you can find a dataset easily by following the same steps.
-
-The new scenario will be:
-> I want to search the Hub for all datasets that can be used for `text_classification` and are in English.
-
-First, you should look at what is available in the [`DatasetSearchArguments`], similar to the [`ModelSearchArguments`]:
-
-
-```python
->>> dataset_args = DatasetSearchArguments()
->>> dataset_args
-Available Attributes or Keys:
- * author
- * benchmark
- * dataset_name
- * language_creators
- * language
- * license
- * multilinguality
- * size_categories
- * task_categories
- * task_ids
-```
-
-`text_classification` is a *task*, so first you should check `task_categories`:
-
-
-```python
->>> dataset_args.task_categories
-Available Attributes or Keys:
- * CodeGeneration
- * Evaluationoflanguagemodels
- * InclusiveLanguage
- * InformationRetrieval
- * SemanticSearch
- * Summarization
- * Text2Textgeneration (Key only)
- * TextNeutralization
- * TokenClassification
- * Translation
- * audio_classification
- * automatic_speech_recognition
- * caption_retrieval
- * code_generation
- * computer_vision
- * conditional_text_generation
- * conversational
- * cross_language_transcription
- * crowdsourced
- * dialogue_system
- * entity_extraction
- * feature_extraction
- * fill_mask
- * generative_modelling
- * gpt_3 (Key only)
- * grammaticalerrorcorrection
- * image
- * image_captioning
- * image_classification
- * image_retrieval
- * image_segmentation
- * image_to_text
- * information_retrieval
- * language_modeling
- * machine_translation
- * multiple_choice
- * named_entity_disambiguation
- * named_entity_recognition
- * natural_language_inference
- * news_classification
- * object_detection
- * other
- * other_test
- * other_text_search
- * paraphrase
- * paraphrasedetection
- * query_paraphrasing
- * question_answering
- * question_generation
- * question_pairing
- * sentiment_analysis
- * sequence2sequence (Key only)
- * sequence_modeling
- * speech_processing
- * structure_prediction
- * summarization
- * table_to_text
- * tabular_to_text
- * text2text_generation (Key only)
- * text_classification
- * text_generation
- * text_generation_other_code_modeling
- * text_generation_other_common_sense_inference
- * text_generation_other_discourse_analysis
- * text_regression
- * text_retrieval
- * text_scoring
- * text_to_structured
- * text_to_tabular
- * textual_entailment
- * time_series_forecasting
- * token_classification
- * transkation
- * translation
- * tts
- * unpaired_image_to_image_translation
- * zero_shot_information_retrieval
- * zero_shot_retrieval
-```
-
-There you will find `text_classification`, so you should use `dataset_args.task_categories.text_classification`.
-
-Next we need to find the proper language. There is a `language` property we can check. These are two-letter language codes, so you should check if it has `en`:
-
+This query is strictly equivalent to:
 
-```python
->>> "en" in dataset_args.language
-True
-```
-
-Now that the pieces are found, you can write a filter:
-
-
-```python
->>> filt = DatasetFilter(
-...    language=dataset_args.language.en,
-...    task_categories=dataset_args.task_categories.text_classification
+```py
+>>> filt = ModelFilter(
+...     task="text-classification", 
+...     library=["pytorch", "tensorflow"],
 ... )
 ```
 
-And search the API!
-
-
-```python
->>> api.list_datasets(filter=filt)[0]
-DatasetInfo: {
-    id: Abirate/english_quotes
-    lastModified: None
-    tags: ['annotations_creators:expert-generated', 'language_creators:expert-generated', 'language_creators:crowdsourced', 'language:en', 'multilinguality:monolingual', 'source_datasets:original', 'task_categories:text-classification', 'task_ids:multi-label-classification']
-    private: False
-    author: Abirate
-    description: None
-    citation: None
-    cardData: None
-    siblings: None
-    gated: False
-}
-```
-
+Here, the [`ModelSearchArguments`] has been a helper to explore the options available on the Hub.
+However, it is not a requirement to make a search. Another way to do that is to visit the
+[models](https://huggingface.co/models) and [datasets](https://huggingface.co/datasets) pages
+in your browser, search for some parameters and look at the values in the URL.
 
-With these two functionalities combined, you can search for all available parameters and tags within the Hub to search for with ease for both Datasets and Models!
diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py
index a41519a0..e103f6ed 100644
--- a/src/huggingface_hub/__init__.py
+++ b/src/huggingface_hub/__init__.py
@@ -94,6 +94,7 @@ _SUBMOD_ATTRS = {
     ],
     "file_download": [
         "HfFileMetadata",
+        "_CACHED_NO_EXIST",
         "cached_download",
         "get_hf_file_metadata",
         "hf_hub_download",
@@ -336,6 +337,7 @@ if TYPE_CHECKING:  # pragma: no cover
     from .fastai_utils import _save_pretrained_fastai  # noqa: F401
     from .fastai_utils import from_pretrained_fastai  # noqa: F401
     from .fastai_utils import push_to_hub_fastai  # noqa: F401
+    from .file_download import _CACHED_NO_EXIST  # noqa: F401
     from .file_download import HfFileMetadata  # noqa: F401
     from .file_download import cached_download  # noqa: F401
     from .file_download import get_hf_file_metadata  # noqa: F401
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index aae1549c..1d5c1f06 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -1177,18 +1177,24 @@ def hf_hub_download(
                 "We have no connection or you passed local_files_only, so"
                 " force_download is not an accepted option."
             )
+
+        # Try to get "commit_hash" from "revision"
+        commit_hash = None
         if REGEX_COMMIT_HASH.match(revision):
             commit_hash = revision
         else:
             ref_path = os.path.join(storage_folder, "refs", revision)
-            with open(ref_path) as f:
-                commit_hash = f.read()
-
-        pointer_path = os.path.join(
-            storage_folder, "snapshots", commit_hash, relative_filename
-        )
-        if os.path.exists(pointer_path):
-            return pointer_path
+            if os.path.isfile(ref_path):
+                with open(ref_path) as f:
+                    commit_hash = f.read()
+
+        # Return pointer file if exists
+        if commit_hash is not None:
+            pointer_path = os.path.join(
+                storage_folder, "snapshots", commit_hash, relative_filename
+            )
+            if os.path.exists(pointer_path):
+                return pointer_path
 
         # If we couldn't find an appropriate file on disk,
         # raise an error.
@@ -1327,6 +1333,23 @@ def try_to_load_from_cache(
             - The exact path to the cached file if it's found in the cache
             - A special value `_CACHED_NO_EXIST` if the file does not exist at the given commit hash and this fact was
               cached.
+
+    Example:
+
+    ```python
+    from huggingface_hub import try_to_load_from_cache, _CACHED_NO_EXIST
+
+    filepath = try_to_load_from_cache()
+    if isinstance(filepath, str):
+        # file exists and is cached
+        ...
+    elif filepath is _CACHED_NO_EXIST:
+        # non-existence of file is cached
+        ...
+    else:
+        # file is not cached
+        ...
+    ```
     """
     if revision is None:
         revision = "main"
@@ -1345,25 +1368,32 @@ def try_to_load_from_cache(
     if not os.path.isdir(repo_cache):
         # No cache for this model
         return None
-    for subfolder in ["refs", "snapshots"]:
-        if not os.path.isdir(os.path.join(repo_cache, subfolder)):
-            return None
 
-    # Resolve refs (for instance to convert main to the associated commit sha)
-    cached_refs = os.listdir(os.path.join(repo_cache, "refs"))
-    if revision in cached_refs:
-        with open(os.path.join(repo_cache, "refs", revision)) as f:
-            revision = f.read()
+    refs_dir = os.path.join(repo_cache, "refs")
+    snapshots_dir = os.path.join(repo_cache, "snapshots")
+    no_exist_dir = os.path.join(repo_cache, ".no_exist")
 
-    if os.path.isfile(os.path.join(repo_cache, ".no_exist", revision, filename)):
+    # Resolve refs (for instance to convert main to the associated commit sha)
+    if os.path.isdir(refs_dir):
+        cached_refs = os.listdir(refs_dir)
+        if revision in cached_refs:
+            with open(os.path.join(refs_dir, revision)) as f:
+                revision = f.read()
+
+    # Check if file is cached as "no_exist"
+    if os.path.isfile(os.path.join(no_exist_dir, revision, filename)):
         return _CACHED_NO_EXIST
 
-    cached_shas = os.listdir(os.path.join(repo_cache, "snapshots"))
+    # Check if revision folder exists
+    if not os.path.exists(snapshots_dir):
+        return None
+    cached_shas = os.listdir(snapshots_dir)
     if revision not in cached_shas:
         # No cache for this revision and we won't try to return a random revision
         return None
 
-    cached_file = os.path.join(repo_cache, "snapshots", revision, filename)
+    # Check if file exists in cache
+    cached_file = os.path.join(snapshots_dir, revision, filename)
     return cached_file if os.path.isfile(cached_file) else None
 
 


Test patch:
diff --git a/tests/test_file_download.py b/tests/test_file_download.py
index a82cb372..1e23d276 100644
--- a/tests/test_file_download.py
+++ b/tests/test_file_download.py
@@ -318,6 +318,24 @@ class CachedDownloadTests(unittest.TestCase):
         self.assertEqual(filepath.name, CONFIG_NAME)
         self.assertEqual(Path(filepath).parent.parent.name, "snapshots")
 
+    def test_hf_hub_download_offline_no_refs(self):
+        """Regression test for #1305.
+
+        If "refs/" dir did not exists on "local_files_only" (or connection broken), a
+        non-explicit `FileNotFoundError` was raised (for the "/refs/revision" file) instead
+        of the documented `LocalEntryNotFoundError` (for the actual searched file).
+
+        See https://github.com/huggingface/huggingface_hub/issues/1305.
+        """
+        with SoftTemporaryDirectory() as cache_dir:
+            with self.assertRaises(LocalEntryNotFoundError):
+                hf_hub_download(
+                    DUMMY_MODEL_ID,
+                    filename=CONFIG_NAME,
+                    local_files_only=True,
+                    cache_dir=cache_dir,
+                )
+
     def test_hf_hub_url_with_empty_subfolder(self):
         """
         Check subfolder arg is processed correctly when empty string is passed to
@@ -348,7 +366,7 @@ class CachedDownloadTests(unittest.TestCase):
         metadata = filename_to_url(filepath, legacy_cache_layout=True)
         self.assertEqual(metadata[1], f'"{DUMMY_MODEL_ID_PINNED_SHA1}"')
 
-    def test_try_to_load_from_cache(self):
+    def test_try_to_load_from_cache_exist(self):
         # Make sure the file is cached
         filepath = hf_hub_download(DUMMY_MODEL_ID, filename=CONFIG_NAME)
 
@@ -389,6 +407,53 @@ class CachedDownloadTests(unittest.TestCase):
         # If file non-existence is not cached, returns None
         self.assertIsNone(try_to_load_from_cache(DUMMY_MODEL_ID, filename="dummy2"))
 
+    def test_try_to_load_from_cache_specific_commit_id_exist(self):
+        """Regression test for #1306.
+
+        See https://github.com/huggingface/huggingface_hub/pull/1306."""
+        with SoftTemporaryDirectory() as cache_dir:
+            # Cache file from specific commit id (no "refs/"" folder)
+            commit_id = HfApi().model_info(DUMMY_MODEL_ID).sha
+            filepath = hf_hub_download(
+                DUMMY_MODEL_ID,
+                filename=CONFIG_NAME,
+                revision=commit_id,
+                cache_dir=cache_dir,
+            )
+
+            # Must be able to retrieve it "offline"
+            attempt = try_to_load_from_cache(
+                DUMMY_MODEL_ID,
+                filename=CONFIG_NAME,
+                revision=commit_id,
+                cache_dir=cache_dir,
+            )
+            self.assertEqual(filepath, attempt)
+
+    def test_try_to_load_from_cache_specific_commit_id_no_exist(self):
+        """Regression test for #1306.
+
+        See https://github.com/huggingface/huggingface_hub/pull/1306."""
+        with SoftTemporaryDirectory() as cache_dir:
+            # Cache file from specific commit id (no "refs/"" folder)
+            commit_id = HfApi().model_info(DUMMY_MODEL_ID).sha
+            with self.assertRaises(EntryNotFoundError):
+                hf_hub_download(
+                    DUMMY_MODEL_ID,
+                    filename="missing_file",
+                    revision=commit_id,
+                    cache_dir=cache_dir,
+                )
+
+            # Must be able to retrieve it "offline"
+            attempt = try_to_load_from_cache(
+                DUMMY_MODEL_ID,
+                filename="missing_file",
+                revision=commit_id,
+                cache_dir=cache_dir,
+            )
+            self.assertEqual(attempt, _CACHED_NO_EXIST)
+
     def test_get_hf_file_metadata_basic(self) -> None:
         """Test getting metadata from a file on the Hub."""
         url = hf_hub_url(