Repository: huggingface/huggingface_hub
Pull request #987 (2022-08-11 10:00:08)

Add helpers to disable progress bars globally + tests
Solve https://github.com/huggingface/huggingface_hub/issues/978 (see API discussion in issue).

@sgugger Since both `HF_HUB_DISABLE_PROGRESS_BARS` env variable and `disable_progress_bars()`/`enable_progress_bars()` helpers are defined, there might be a conflict at some point if not correctly handled. For example a library, could call `disable_progress_bars()` even though the user has set `HF_HUB_DISABLE_PROGRESS_BARS=0`. Current solution is that `disable_progress_bars()` has higher priority but I'm not 100% sure it's clever.

The other solution would be the opposite: if `HF_HUB_DISABLE_PROGRESS_BARS` is set (either 0 or 1), then `disable_progress_bars`/`enable_progress_bars` are ignored in the code with a warning (`"cannot programmatical disable progress bars as verbosity has been set by environment variable"`). 

What is your opinion about it ? I feel that both are correct, it's more a usage preference.

**EDIT:** after discussion below, we went for solution 2 (environment variable has priority).

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_987). All of your documentation changes will be reflected on that endpoint.
Wauplin: > I think the env variable should get priority over programmatic functions. My reasoning being that if the user goes out of their way to set it on the env level, it's really clear they want that thing all the time. But it's just my 2 cents and going the other way around also works.

Makes sense. That's what I also started to think when writing the PR text actually but didn't want to change the code before having a second opinion :D 
Will make the change.
Wauplin: @sgugger I made the changes to give priority to env variable in commit https://github.com/huggingface/huggingface_hub/pull/987/commits/b47378a7f6b9f10906bb5d817c78255e8ca37598. 
Could you quickly review it so that I can merge ? Thanks in advance 🙏 

Patch:
diff --git a/src/huggingface_hub/constants.py b/src/huggingface_hub/constants.py
index 42c7bded..fce9f638 100644
--- a/src/huggingface_hub/constants.py
+++ b/src/huggingface_hub/constants.py
@@ -1,5 +1,6 @@
 import os
 import re
+from typing import Optional
 
 
 # Possible values for env variables
@@ -7,6 +8,19 @@ import re
 ENV_VARS_TRUE_VALUES = {"1", "ON", "YES", "TRUE"}
 ENV_VARS_TRUE_AND_AUTO_VALUES = ENV_VARS_TRUE_VALUES.union({"AUTO"})
 
+
+def _is_true(value: Optional[str]) -> bool:
+    if value is None:
+        return False
+    return value.upper() in ENV_VARS_TRUE_VALUES
+
+
+def _is_true_or_auto(value: Optional[str]) -> bool:
+    if value is None:
+        return False
+    return value.upper() in ENV_VARS_TRUE_AND_AUTO_VALUES
+
+
 # Constants for file downloads
 
 PYTORCH_WEIGHTS_NAME = "pytorch_model.bin"
@@ -23,10 +37,7 @@ REGEX_COMMIT_OID = re.compile(r"[A-Fa-f0-9]{5,40}")
 
 HUGGINGFACE_CO_URL_HOME = "https://huggingface.co/"
 
-ENV_VARS_TRUE_VALUES = {"1", "ON", "YES", "TRUE"}
-_staging_mode = (
-    os.environ.get("HUGGINGFACE_CO_STAGING", "NO").upper() in ENV_VARS_TRUE_VALUES
-)
+_staging_mode = _is_true(os.environ.get("HUGGINGFACE_CO_STAGING"))
 
 ENDPOINT = os.getenv("HF_ENDPOINT") or (
     "https://hub-ci.huggingface.co" if _staging_mode else "https://huggingface.co"
@@ -68,8 +79,16 @@ default_cache_path = os.path.join(hf_cache_home, "hub")
 
 HUGGINGFACE_HUB_CACHE = os.getenv("HUGGINGFACE_HUB_CACHE", default_cache_path)
 
-HF_HUB_OFFLINE = os.environ.get("HF_HUB_OFFLINE", "AUTO").upper()
-if HF_HUB_OFFLINE in ENV_VARS_TRUE_VALUES:
-    HF_HUB_OFFLINE = True
-else:
-    HF_HUB_OFFLINE = False
+HF_HUB_OFFLINE = _is_true(os.environ.get("HF_HUB_OFFLINE"))
+
+
+# Here, `True` will disable progress bars globally without possibility of enabling it
+# programatically. `False` will enable them without possibility of disabling them.
+# If environement variable is not set (None), then the user is free to enable/disable
+# them programmatically.
+# TL;DR: env variable has priority over code
+HF_HUB_DISABLE_PROGRESS_BARS: Optional[bool] = os.environ.get(
+    "HF_HUB_DISABLE_PROGRESS_BARS"
+)
+if HF_HUB_DISABLE_PROGRESS_BARS is not None:
+    HF_HUB_DISABLE_PROGRESS_BARS = _is_true(HF_HUB_DISABLE_PROGRESS_BARS)
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 88e0fc80..30ac43bb 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -16,7 +16,6 @@ from typing import BinaryIO, Dict, Optional, Tuple, Union
 from urllib.parse import quote
 
 import packaging.version
-from tqdm.auto import tqdm
 
 import requests
 from filelock import FileLock
@@ -34,7 +33,7 @@ from .constants import (
     REPO_TYPES_URL_PREFIXES,
 )
 from .hf_api import HfFolder
-from .utils import logging
+from .utils import logging, tqdm
 from .utils._errors import LocalEntryNotFoundError, _raise_for_status
 
 
diff --git a/src/huggingface_hub/repository.py b/src/huggingface_hub/repository.py
index 1b605ee8..1dec7d4c 100644
--- a/src/huggingface_hub/repository.py
+++ b/src/huggingface_hub/repository.py
@@ -10,15 +10,13 @@ from pathlib import Path
 from typing import Callable, Dict, Iterator, List, Optional, Tuple, Union
 from urllib.parse import urlparse
 
-from tqdm.auto import tqdm
-
 from huggingface_hub.constants import REPO_TYPES_URL_PREFIXES, REPOCARD_NAME
 from huggingface_hub.repocard import metadata_load, metadata_save
 from requests.exceptions import HTTPError
 
 from .hf_api import HfApi, HfFolder, repo_type_and_id_from_hf_id
 from .lfs import LFS_MULTIPART_UPLOAD_COMMAND
-from .utils import logging, run_subprocess
+from .utils import logging, run_subprocess, tqdm
 
 
 logger = logging.get_logger(__name__)
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index 8ec0881e..0f0da23b 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -15,6 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License
 
+from . import tqdm as _tqdm  # _tqdm is the module
 from ._errors import (
     EntryNotFoundError,
     LocalEntryNotFoundError,
@@ -22,3 +23,9 @@ from ._errors import (
     RevisionNotFoundError,
 )
 from ._subprocess import run_subprocess
+from .tqdm import (
+    are_progress_bars_disabled,
+    disable_progress_bars,
+    enable_progress_bars,
+    tqdm,
+)
diff --git a/src/huggingface_hub/utils/tqdm.py b/src/huggingface_hub/utils/tqdm.py
new file mode 100644
index 00000000..608ebb2f
--- /dev/null
+++ b/src/huggingface_hub/utils/tqdm.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# coding=utf-8
+# Copyright 2021 The HuggingFace Inc. team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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
+"""Utility helpers to handle progress bars in `huggingface_hub`.
+
+Usage:
+    1. Use `huggingface_hub.utils.tqdm` as you would use `tqdm.tqdm` or `tqdm.auto.tqdm`.
+    2. To disable progress bars, either use `disable_progress_bars()` helper or set the
+       environement variable `HF_HUB_DISABLE_PROGRESS_BARS` to 1.
+    3. To re-enable progress bars, use `enable_progress_bars()`.
+    4. To check weither progress bars are disabled, use `are_progress_bars_disabled()`.
+
+NOTE: Environment variable `HF_HUB_DISABLE_PROGRESS_BARS` has the priority.
+
+Example:
+    ```py
+    from huggingface_hub.utils import (
+        are_progress_bars_disabled,
+        disable_progress_bars,
+        enable_progress_bars,
+        tqdm,
+    )
+
+    # Disable progress bars globally
+    disable_progress_bars()
+
+    # Use as normal `tqdm`
+    for _ in tqdm(range(5)):
+       do_something()
+
+    # Still not showing progress bars, as `disable=False` is overwritten to `True`.
+    for _ in tqdm(range(5), disable=False):
+       do_something()
+
+    are_progress_bars_disabled() # True
+
+    # Re-enable progress bars globally
+    enable_progress_bars()
+
+    # Progress bar will be shown !
+    for _ in tqdm(range(5)):
+       do_something()
+    ```
+"""
+import warnings
+
+from tqdm.auto import tqdm as old_tqdm
+
+from ..constants import HF_HUB_DISABLE_PROGRESS_BARS
+
+
+# `HF_HUB_DISABLE_PROGRESS_BARS` is `Optional[bool]` while `_hf_hub_progress_bars_disabled`
+# is a `bool`. If `HF_HUB_DISABLE_PROGRESS_BARS` is set to True or False, it has priority.
+# If `HF_HUB_DISABLE_PROGRESS_BARS` is None, it means the user have not set the
+# environment variable and is free to enable/disable progress bars programmatically.
+# TL;DR: env variable has priority over code.
+#
+# By default, progress bars are enabled.
+_hf_hub_progress_bars_disabled: bool = HF_HUB_DISABLE_PROGRESS_BARS or False
+
+
+def disable_progress_bars() -> None:
+    """
+    Disable globally progress bars used in `huggingface_hub` except if
+    `HF_HUB_DISABLE_PROGRESS_BARS` environement variable has been set.
+    """
+    if HF_HUB_DISABLE_PROGRESS_BARS is False:
+        warnings.warn(
+            "Cannot disable progress bars: environment variable"
+            " `HF_HUB_DISABLE_PROGRESS_BARS=0` is set and has priority."
+        )
+        return
+    global _hf_hub_progress_bars_disabled
+    _hf_hub_progress_bars_disabled = True
+
+
+def enable_progress_bars() -> None:
+    """
+    Enable globally progress bars used in `huggingface_hub` except if
+    `HF_HUB_DISABLE_PROGRESS_BARS` environement variable has been set.
+    """
+    if HF_HUB_DISABLE_PROGRESS_BARS is True:
+        warnings.warn(
+            "Cannot enable progress bars: environment variable"
+            " `HF_HUB_DISABLE_PROGRESS_BARS=1` is set and has priority."
+        )
+        return
+    global _hf_hub_progress_bars_disabled
+    _hf_hub_progress_bars_disabled = False
+
+
+def are_progress_bars_disabled() -> bool:
+    """Return weither progress bars are globally disabled or not."""
+    global _hf_hub_progress_bars_disabled
+    return _hf_hub_progress_bars_disabled
+
+
+class tqdm(old_tqdm):
+    """
+    Class to override `disable` argument in case progress bars are globally disabled.
+
+    Taken from https://github.com/tqdm/tqdm/issues/619#issuecomment-619639324.
+    """
+
+    def __init__(self, *args, **kwargs):
+        if are_progress_bars_disabled():
+            kwargs["disable"] = True
+        super().__init__(*args, **kwargs)


Test patch:
diff --git a/tests/test_utils_tqdm.py b/tests/test_utils_tqdm.py
new file mode 100644
index 00000000..3d1020e1
--- /dev/null
+++ b/tests/test_utils_tqdm.py
@@ -0,0 +1,116 @@
+import unittest
+from unittest.mock import patch
+
+import pytest
+from pytest import CaptureFixture
+
+from huggingface_hub.utils import (
+    are_progress_bars_disabled,
+    disable_progress_bars,
+    enable_progress_bars,
+    tqdm,
+)
+
+
+class TestTqdmUtils(unittest.TestCase):
+    @pytest.fixture(autouse=True)
+    def capsys(self, capsys: CaptureFixture) -> None:
+        """Workaround to make capsys work in unittest framework.
+
+        Capsys is a convenient pytest fixture to capture stdout.
+        See https://waylonwalker.com/pytest-capsys/.
+
+        Taken from https://github.com/pytest-dev/pytest/issues/2504#issuecomment-309475790.
+        """
+        self.capsys = capsys
+
+    def setUp(self) -> None:
+        """Get verbosity to set it back after the tests."""
+        self._previous_are_progress_bars_disabled = are_progress_bars_disabled()
+        return super().setUp()
+
+    def tearDown(self) -> None:
+        """Set back progress bars verbosity as before testing."""
+        if self._previous_are_progress_bars_disabled:
+            disable_progress_bars()
+        else:
+            enable_progress_bars()
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None)
+    def test_tqdm_helpers(self) -> None:
+        """Test helpers to enable/disable progress bars."""
+        disable_progress_bars()
+        self.assertTrue(are_progress_bars_disabled())
+
+        enable_progress_bars()
+        self.assertFalse(are_progress_bars_disabled())
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", True)
+    def test_cannot_enable_tqdm_when_env_variable_is_set(self) -> None:
+        """
+        Test helpers cannot enable/disable progress bars when
+        `HF_HUB_DISABLE_PROGRESS_BARS` is set.
+        """
+        disable_progress_bars()
+        self.assertTrue(are_progress_bars_disabled())
+
+        with self.assertWarns(UserWarning):
+            enable_progress_bars()
+        self.assertTrue(are_progress_bars_disabled())  # Still disabled !
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", False)
+    def test_cannot_disable_tqdm_when_env_variable_is_set(self) -> None:
+        """
+        Test helpers cannot enable/disable progress bars when
+        `HF_HUB_DISABLE_PROGRESS_BARS` is set.
+        """
+        enable_progress_bars()
+        self.assertFalse(are_progress_bars_disabled())
+
+        with self.assertWarns(UserWarning):
+            disable_progress_bars()
+        self.assertFalse(are_progress_bars_disabled())  # Still enabled !
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None)
+    def test_tqdm_disabled(self) -> None:
+        """Test TQDM not outputing anything when globally disabled."""
+        disable_progress_bars()
+        for _ in tqdm(range(10)):
+            pass
+
+        captured = self.capsys.readouterr()
+        self.assertEqual(captured.out, "")
+        self.assertEqual(captured.err, "")
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None)
+    def test_tqdm_disabled_cannot_be_forced(self) -> None:
+        """Test TQDM cannot be forced when globally disabled."""
+        disable_progress_bars()
+        for _ in tqdm(range(10), disable=False):
+            pass
+
+        captured = self.capsys.readouterr()
+        self.assertEqual(captured.out, "")
+        self.assertEqual(captured.err, "")
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None)
+    def test_tqdm_can_be_disabled_when_globally_enabled(self) -> None:
+        """Test TQDM can still be locally disabled even when globally enabled."""
+        enable_progress_bars()
+        for _ in tqdm(range(10), disable=True):
+            pass
+
+        captured = self.capsys.readouterr()
+        self.assertEqual(captured.out, "")
+        self.assertEqual(captured.err, "")
+
+    @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None)
+    def test_tqdm_enabled(self) -> None:
+        """Test TQDM work normally when globally enabled."""
+        enable_progress_bars()
+        for _ in tqdm(range(10)):
+            pass
+
+        captured = self.capsys.readouterr()
+        self.assertEqual(captured.out, "")
+        self.assertIn("10/10", captured.err)  # tqdm log