From 4e863a4e1f0ab31bba7dbcfefb3d5ea09be77dad Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 6 Apr 2026 17:27:17 +0200 Subject: [PATCH 1/5] Fix custom `tqdm_class` silently broken in non-TTY environments --- src/huggingface_hub/_snapshot_download.py | 12 ++++--- src/huggingface_hub/utils/tqdm.py | 33 ++++++++++++++++++-- tests/test_utils_tqdm.py | 38 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/huggingface_hub/_snapshot_download.py b/src/huggingface_hub/_snapshot_download.py index ed72652904..d45768f801 100644 --- a/src/huggingface_hub/_snapshot_download.py +++ b/src/huggingface_hub/_snapshot_download.py @@ -18,8 +18,9 @@ ) from .file_download import REGEX_COMMIT_HASH, DryRunFileInfo, hf_hub_download, repo_folder_name from .hf_api import DatasetInfo, HfApi, ModelInfo, RepoFile, SpaceInfo -from .utils import OfflineModeIsEnabled, filter_repo_objects, is_tqdm_disabled, logging, validate_hf_hub_args -from .utils import tqdm as hf_tqdm +from .utils import OfflineModeIsEnabled, filter_repo_objects, logging, validate_hf_hub_args +from .utils.tqdm import _create_progress_bar +from .utils.tqdm import tqdm as hf_tqdm logger = logging.get_logger(__name__) @@ -385,14 +386,15 @@ def snapshot_download( # Create a progress bar for the bytes downloaded # This progress bar is shared across threads/files and gets updated each time we fetch # metadata for a file. - bytes_progress = tqdm_class( + bytes_progress = _create_progress_bar( + cls=tqdm_class, + log_level=logger.getEffectiveLevel(), + name="huggingface_hub.snapshot_download", desc="Downloading (incomplete total...)", - disable=is_tqdm_disabled(log_level=logger.getEffectiveLevel()), total=0, initial=0, unit="B", unit_scale=True, - name="huggingface_hub.snapshot_download", ) class _AggregatedTqdm: diff --git a/src/huggingface_hub/utils/tqdm.py b/src/huggingface_hub/utils/tqdm.py index 50e8a499bc..28822d4940 100644 --- a/src/huggingface_hub/utils/tqdm.py +++ b/src/huggingface_hub/utils/tqdm.py @@ -279,6 +279,32 @@ def _inner_read(size: int | None = -1) -> bytes: pbar.close() +def _create_progress_bar(*, cls: type[old_tqdm], log_level: int, name: str | None = None, **kwargs) -> old_tqdm: + """Create a progress bar, handling custom vs HF subclass differences. + + For our `tqdm` subclass (or subclasses of it): respects all disable signals + (`HF_HUB_DISABLE_PROGRESS_BARS`, `disable_progress_bars()`, log level) and uses + `disable=None` for TTY auto-detection (see https://github.com/huggingface/huggingface_hub/pull/2000), + unless `TQDM_POSITION=-1` forces bars on (https://github.com/huggingface/huggingface_hub/pull/2698). + + For other classes: does not inject `disable` or `name`. the custom class is fully + responsible for its own behavior. Vanilla tqdm defaults to `disable=False` (bar shows). + Omits `name` which vanilla tqdm rejects with `TqdmKeyError`. See https://github.com/huggingface/huggingface_hub/issues/4050. + """ + # issubclass() crashes on non-class callables (e.g. functools.partial), guard with isinstance. + if not (isinstance(cls, type) and issubclass(cls, tqdm)): + return cls(**kwargs) # type: ignore[return-value] + + # HF subclass: apply all disable signals + TTY auto-detection. + if are_progress_bars_disabled(name) or log_level == logging.NOTSET: + disable: bool | None = True + elif os.getenv("TQDM_POSITION") == "-1": + disable = False + else: + disable = None + return cls(disable=disable, name=name, **kwargs) # type: ignore[return-value] + + def _get_progress_bar_context( *, desc: str, @@ -297,12 +323,13 @@ def _get_progress_bar_context( # Makes it easier to use the same code path for both cases but in the later # case, the progress bar is not closed when exiting the context manager. - return (tqdm_class or tqdm)( # type: ignore + return _create_progress_bar( # type: ignore[return-value] + cls=tqdm_class or tqdm, + log_level=log_level, + name=name, unit=unit, unit_scale=unit_scale, total=total, initial=initial, desc=desc, - disable=is_tqdm_disabled(log_level=log_level), - name=name, ) diff --git a/tests/test_utils_tqdm.py b/tests/test_utils_tqdm.py index add76152c6..79d3c22bc7 100644 --- a/tests/test_utils_tqdm.py +++ b/tests/test_utils_tqdm.py @@ -1,3 +1,6 @@ +import io +import logging +import sys import time import unittest from pathlib import Path @@ -5,6 +8,7 @@ import pytest from pytest import CaptureFixture +from tqdm.auto import tqdm as vanilla_tqdm from huggingface_hub.utils import ( SoftTemporaryDirectory, @@ -14,6 +18,7 @@ tqdm, tqdm_stream_file, ) +from huggingface_hub.utils.tqdm import _get_progress_bar_context class CapsysBaseTest(unittest.TestCase): @@ -235,3 +240,36 @@ def test_progress_bar_respects_group(self) -> None: captured = self.capsys.readouterr() assert captured.out == "" assert "10/10" in captured.err + + +class TestCreateProgressBarCustomClass: + """Regression tests for https://github.com/huggingface/huggingface_hub/issues/4050.""" + + def test_custom_tqdm_class_not_disabled_in_non_tty(self): + """Custom tqdm_class should not be silently disabled in non-TTY.""" + fake_stderr = io.StringIO() + with patch.object(sys, "stderr", fake_stderr): + bar = _get_progress_bar_context( + desc="test", + log_level=logging.INFO, + total=100, + tqdm_class=vanilla_tqdm, + name="huggingface_hub.test", + ) + with bar as pbar: + assert not pbar.disable + pbar.update(50) + pbar.update(50) + assert pbar.n == 100 + + def test_custom_tqdm_class_no_name_kwarg(self): + """Custom tqdm_class should not receive HF-specific 'name' kwarg.""" + bar = _get_progress_bar_context( + desc="test", + log_level=logging.INFO, + total=10, + tqdm_class=vanilla_tqdm, + name="huggingface_hub.test", + ) + with bar as pbar: + pbar.update(10) From 5934897c8edb8454e0a56a687022a3a2022a5936 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 6 Apr 2026 17:33:29 +0200 Subject: [PATCH 2/5] one more test --- tests/test_utils_tqdm.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_utils_tqdm.py b/tests/test_utils_tqdm.py index 79d3c22bc7..1694342ac8 100644 --- a/tests/test_utils_tqdm.py +++ b/tests/test_utils_tqdm.py @@ -262,6 +262,19 @@ def test_custom_tqdm_class_not_disabled_in_non_tty(self): pbar.update(50) assert pbar.n == 100 + @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", True) + def test_custom_tqdm_class_ignores_hf_disable_signal(self): + """Custom tqdm_class is not affected by HF_HUB_DISABLE_PROGRESS_BARS.""" + bar = _get_progress_bar_context( + desc="test", + log_level=logging.INFO, + total=10, + tqdm_class=vanilla_tqdm, + name="huggingface_hub.test", + ) + with bar as pbar: + assert not pbar.disable + def test_custom_tqdm_class_no_name_kwarg(self): """Custom tqdm_class should not receive HF-specific 'name' kwarg.""" bar = _get_progress_bar_context( From ca0dc92d249f8424c7e8762889fa34bf8bbf0cb0 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 6 Apr 2026 17:41:36 +0200 Subject: [PATCH 3/5] nit --- src/huggingface_hub/utils/tqdm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/utils/tqdm.py b/src/huggingface_hub/utils/tqdm.py index 28822d4940..3020287607 100644 --- a/src/huggingface_hub/utils/tqdm.py +++ b/src/huggingface_hub/utils/tqdm.py @@ -280,7 +280,7 @@ def _inner_read(size: int | None = -1) -> bytes: def _create_progress_bar(*, cls: type[old_tqdm], log_level: int, name: str | None = None, **kwargs) -> old_tqdm: - """Create a progress bar, handling custom vs HF subclass differences. + """Create a progress bar. For our `tqdm` subclass (or subclasses of it): respects all disable signals (`HF_HUB_DISABLE_PROGRESS_BARS`, `disable_progress_bars()`, log level) and uses From 5d059bf5f4395af68fdd1d3a25cdd428e65fc5e8 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 6 Apr 2026 17:44:42 +0200 Subject: [PATCH 4/5] fix --- src/huggingface_hub/utils/tqdm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/utils/tqdm.py b/src/huggingface_hub/utils/tqdm.py index 3020287607..491298f695 100644 --- a/src/huggingface_hub/utils/tqdm.py +++ b/src/huggingface_hub/utils/tqdm.py @@ -323,7 +323,7 @@ def _get_progress_bar_context( # Makes it easier to use the same code path for both cases but in the later # case, the progress bar is not closed when exiting the context manager. - return _create_progress_bar( # type: ignore[return-value] + return _create_progress_bar( # type: ignore cls=tqdm_class or tqdm, log_level=log_level, name=name, From d5d3eba80b862b84f16f3118bec3a25ba49852dd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Apr 2026 15:22:52 +0000 Subject: [PATCH 5/5] Use is_tqdm_disabled in progress bar helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: célina --- src/huggingface_hub/utils/tqdm.py | 10 +++------- tests/test_utils_tqdm.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/huggingface_hub/utils/tqdm.py b/src/huggingface_hub/utils/tqdm.py index 491298f695..19a78f34e1 100644 --- a/src/huggingface_hub/utils/tqdm.py +++ b/src/huggingface_hub/utils/tqdm.py @@ -295,13 +295,9 @@ def _create_progress_bar(*, cls: type[old_tqdm], log_level: int, name: str | Non if not (isinstance(cls, type) and issubclass(cls, tqdm)): return cls(**kwargs) # type: ignore[return-value] - # HF subclass: apply all disable signals + TTY auto-detection. - if are_progress_bars_disabled(name) or log_level == logging.NOTSET: - disable: bool | None = True - elif os.getenv("TQDM_POSITION") == "-1": - disable = False - else: - disable = None + # HF subclass: keep the historical log-level / TTY behavior. Group-based + # disabling is already handled in `tqdm.__init__`. + disable = is_tqdm_disabled(log_level) return cls(disable=disable, name=name, **kwargs) # type: ignore[return-value] diff --git a/tests/test_utils_tqdm.py b/tests/test_utils_tqdm.py index 1694342ac8..fcc05f54c8 100644 --- a/tests/test_utils_tqdm.py +++ b/tests/test_utils_tqdm.py @@ -241,6 +241,17 @@ def test_progress_bar_respects_group(self) -> None: assert captured.out == "" assert "10/10" in captured.err + @patch("huggingface_hub.utils._tqdm.HF_HUB_DISABLE_PROGRESS_BARS", None) + def test_progress_bar_context_respects_group(self) -> None: + disable_progress_bars("foo.bar") + with _get_progress_bar_context( + desc="test", + log_level=logging.INFO, + total=10, + name="foo.bar.something", + ) as pbar: + assert pbar.disable + class TestCreateProgressBarCustomClass: """Regression tests for https://github.com/huggingface/huggingface_hub/issues/4050."""