From 6f0733f7bf781eba30f094f5f7018926fac3620f Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Mon, 6 Oct 2025 17:14:00 +0200 Subject: [PATCH 1/9] [hf CLI] check for updates and notify user --- .github/workflows/check-installers.yml | 23 ++++++- src/huggingface_hub/cli/_cli_utils.py | 84 +++++++++++++++++++++++++- src/huggingface_hub/cli/hf.py | 3 +- src/huggingface_hub/constants.py | 3 + src/huggingface_hub/utils/__init__.py | 1 + src/huggingface_hub/utils/_http.py | 4 +- src/huggingface_hub/utils/_runtime.py | 49 ++++++++++++++- utils/installers/install.ps1 | 4 ++ utils/installers/install.sh | 5 +- 9 files changed, 169 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-installers.yml b/.github/workflows/check-installers.yml index 92dc13f396..0d582641ed 100644 --- a/.github/workflows/check-installers.yml +++ b/.github/workflows/check-installers.yml @@ -36,6 +36,9 @@ jobs: export PATH="$BIN_DIR:$PATH" + - name: Check installation + shell: bash + run: | HF_VERSION_PATH="$HF_TEST_ROOT/hf-version.txt" hf version | tee "$HF_VERSION_PATH" if ! grep -Eq 'huggingface_hub version: [0-9]+(\.[0-9]+){1,2}' "$HF_VERSION_PATH"; then @@ -48,13 +51,22 @@ jobs: rm -rf "$HF_TEST_ROOT" + - name: Check installation method + shell: bash + run: | + hf env + if ! hf env | grep -Fq 'Installation method: hf_installer'; then + echo "❌ Error: not installed with hf_installer." + exit 1 + fi + windows-installer: runs-on: windows-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Run installer + - name: Run installer shell: pwsh run: | $hfTestRoot = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString()) @@ -86,3 +98,12 @@ jobs: Remove-Item Env:NO_COLOR Remove-Item -Path $hfTestRoot -Recurse -Force + + - name: Check installation method + shell: pwsh + run: | + hf env + if (-not (hf env | Select-String -SimpleMatch 'Installation method: hf_installer')) { + Write-Error "❌ Error: not installed with hf_installer." + exit 1 + } diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 2cd08d3416..56a238ba16 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -13,15 +13,21 @@ # limitations under the License. """Contains CLI utilities (styling, helpers).""" +import importlib.metadata import os +import time from enum import Enum from typing import Annotated, Optional, Union import click import typer -from huggingface_hub import __version__ +from huggingface_hub import __version__, constants from huggingface_hub.hf_api import HfApi +from huggingface_hub.utils import get_session, hf_raise_for_status, installation_method, logging + + +logger = logging.get_logger() class ANSI: @@ -144,3 +150,79 @@ class RepoType(str, Enum): def get_hf_api(token: Optional[str] = None) -> HfApi: return HfApi(token=token, library_name="hf", library_version=__version__) + + +### PyPI VERSION CHECKER + + +def check_cli_update() -> None: + """ + Check whether a newer version of `huggingface_hub` is available on PyPI. + + If a newer version is found, notify the user and suggest updating. + The latest PyPI version is cached locally in `$HF_HOME/pypi_latest_version` for 24 hours to prevent repeated notifications. + If current version is a pre-release (e.g. `1.0.0.rc1`), or a dev version (e.g. `1.0.0.dev1`), no check is performed. + + This function is called at the entry point of the CLI. + """ + try: + _check_cli_update() + except Exception: + # We don't want the CLI to fail on version checks, no matter the reason. + logger.debug("Error while checking for CLI update.", exc_info=True) + + +def _check_cli_update() -> None: + current_version = importlib.metadata.version("huggingface_hub") + + if any(tag in current_version for tag in ["a", "b", "rc", "dev"]): + # Don't check for pre-releases or dev versions + return + + cached_version = _get_cached_pypi_version() + if cached_version is None: + latest_version = _get_pypi_version() + _cache_pypi_version(latest_version) + else: + latest_version = cached_version + + if current_version != latest_version: + method = installation_method() + if method == "brew": + update_command = "brew upgrade huggingface-cli" + elif method == "hf_installer" and os.name == "nt": + update_command = "curl -LsSf https://hf.co/cli/install.ps1 | pwsh -" + elif method == "hf_installer": + update_command = "curl -LsSf https://hf.co/cli/install.sh | sh -" + else: # unknown => likely pip + update_command = "pip install -U huggingface_hub" + + click.echo( + ANSI.yellow( + f"A new version of huggingface_hub ({latest_version}) is available! " + f"You are using version {current_version}.\n" + f"To update, run: {ANSI.bold(update_command)}\n", + ) + ) + + +def _get_pypi_version() -> str: + response = get_session().get("https://pypi.org/pypi/huggingface_hub/json", timeout=2) + hf_raise_for_status(response) + data = response.json() + return data["info"]["version"] + + +def _get_cached_pypi_version() -> Optional[str]: + if os.path.exists(constants.PYPI_LATEST_VERSION_PATH): + mtime = os.path.getmtime(constants.PYPI_LATEST_VERSION_PATH) + # If the file is older than 24h, we don't use it + if (time.time() - mtime) < 24 * 3600: + with open(constants.PYPI_LATEST_VERSION_PATH, "r", encoding="utf-8") as f: + return f.read().strip() + return None + + +def _cache_pypi_version(version: str) -> None: + with open(constants.PYPI_LATEST_VERSION_PATH, "w", encoding="utf-8") as f: + f.write(version) diff --git a/src/huggingface_hub/cli/hf.py b/src/huggingface_hub/cli/hf.py index bb8b3b80d0..8306eff084 100644 --- a/src/huggingface_hub/cli/hf.py +++ b/src/huggingface_hub/cli/hf.py @@ -13,7 +13,7 @@ # limitations under the License. -from huggingface_hub.cli._cli_utils import typer_factory +from huggingface_hub.cli._cli_utils import check_cli_update, typer_factory from huggingface_hub.cli.auth import auth_cli from huggingface_hub.cli.cache import cache_cli from huggingface_hub.cli.download import download @@ -52,6 +52,7 @@ def main(): logging.set_verbosity_info() + check_cli_update() app() diff --git a/src/huggingface_hub/constants.py b/src/huggingface_hub/constants.py index 2ca29cf294..a96c150e1a 100644 --- a/src/huggingface_hub/constants.py +++ b/src/huggingface_hub/constants.py @@ -163,6 +163,9 @@ def _as_int(value: Optional[str]) -> Optional[int]: HF_HUB_OFFLINE = _is_true(os.environ.get("HF_HUB_OFFLINE") or os.environ.get("TRANSFORMERS_OFFLINE")) +# Used to check if CLI is up-to-date +PYPI_LATEST_VERSION_PATH = os.path.join(HF_HOME, "pypi_latest_version") + # If set, log level will be set to DEBUG and all requests made to the Hub will be logged # as curl commands for reproducibility. HF_DEBUG = _is_true(os.environ.get("HF_DEBUG")) diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py index 1b2eccdafc..43dc0cb1c6 100644 --- a/src/huggingface_hub/utils/__init__.py +++ b/src/huggingface_hub/utils/__init__.py @@ -86,6 +86,7 @@ get_tensorboard_version, get_tf_version, get_torch_version, + installation_method, is_aiohttp_available, is_colab_enterprise, is_fastai_available, diff --git a/src/huggingface_hub/utils/_http.py b/src/huggingface_hub/utils/_http.py index ff59f4b092..a203757c10 100644 --- a/src/huggingface_hub/utils/_http.py +++ b/src/huggingface_hub/utils/_http.py @@ -744,9 +744,9 @@ def _curlify(request: httpx.Request) -> str: flat_parts = [] for k, v in parts: if k: - flat_parts.append(quote(k)) + flat_parts.append(quote(str(k))) if v: - flat_parts.append(quote(v)) + flat_parts.append(quote(str(v))) return " ".join(flat_parts) diff --git a/src/huggingface_hub/utils/_runtime.py b/src/huggingface_hub/utils/_runtime.py index dd390df87c..44a96f9967 100644 --- a/src/huggingface_hub/utils/_runtime.py +++ b/src/huggingface_hub/utils/_runtime.py @@ -19,7 +19,8 @@ import platform import sys import warnings -from typing import Any +from pathlib import Path +from typing import Any, Literal from .. import __version__, constants @@ -322,6 +323,49 @@ def is_colab_enterprise() -> bool: return os.environ.get("VERTEX_PRODUCT") == "COLAB_ENTERPRISE" +# Check how huggingface_hub has been installed + + +def installation_method() -> Literal["brew", "hf_installer", "unknown"]: + """Return the installation method of the current environment. + + - "hf_installer" if installed via the official installer script + - "brew" if installed via Homebrew + - "unknown" otherwise + """ + if _is_brew_installation(): + return "brew" + elif _is_hf_installer_installation(): + return "hf_installer" + else: + return "unknown" + + +def _is_brew_installation() -> bool: + """Check if running from a Homebrew installation. + + Note: AI-generated by Claude. + """ + exe_path = Path(sys.executable).resolve() + exe_str = str(exe_path) + + # Check common Homebrew paths + # /opt/homebrew (Apple Silicon), /usr/local (Intel) + return "/Cellar/" in exe_str or "/opt/homebrew/" in exe_str or exe_str.startswith("/usr/local/Cellar/") + + +def _is_hf_installer_installation() -> bool: + """Return `True` if the current environment was set up via the official hf installer script. + + i.e. using one of + curl -LsSf https://hf.co/cli/install.sh | sh + curl -LsSf https://hf.co/cli/install.ps1 | pwsh - + """ + venv = sys.prefix # points to venv root if active + marker = Path(venv) / ".hf_installer_marker" + return marker.exists() + + def dump_environment_info() -> dict[str, Any]: """Dump information about the machine to help debugging issues. @@ -366,6 +410,9 @@ def dump_environment_info() -> dict[str, Any]: except Exception: pass + # How huggingface_hub has been installed? + info["Installation method"] = installation_method() + # Installed dependencies info["Torch"] = get_torch_version() info["httpx"] = get_httpx_version() diff --git a/utils/installers/install.ps1 b/utils/installers/install.ps1 index 8fd82e39d9..1852439665 100644 --- a/utils/installers/install.ps1 +++ b/utils/installers/install.ps1 @@ -223,6 +223,10 @@ function New-VirtualEnvironment { } if (-not $?) { throw "Failed to create virtual environment" } + # Mark this installation as installer-managed + $markerFile = Join-Path $VENV_DIR ".hf_installer_marker" + New-Item -Path $markerFile -ItemType File -Force | Out-Null + # Use the venv's python -m pip for deterministic upgrades $script:VenvPython = Join-Path $SCRIPTS_DIR "python.exe" Write-Log "Upgrading pip..." diff --git a/utils/installers/install.sh b/utils/installers/install.sh index d8030ea585..011fcfb30c 100755 --- a/utils/installers/install.sh +++ b/utils/installers/install.sh @@ -180,7 +180,7 @@ detect_os() { # Install Python if not available python_version_supported() { - "$1" - <<'PY' >/dev/null 2>&1 + "$1" <<'PY' >/dev/null 2>&1 import sys sys.exit(0 if sys.version_info >= (3, 9) else 1) PY @@ -268,6 +268,9 @@ create_venv() { run_command "Failed to create virtual environment at $VENV_DIR" "$PYTHON_CMD" -m venv "$VENV_DIR" + # Mark this installation as installer-managed + touch "$VENV_DIR/.hf_installer_marker" + # Use the venv python for pip management log_info "Upgrading pip..." run_command "Failed to upgrade pip" "$VENV_DIR/bin/python" -m pip install --upgrade pip From b6dae5ebde0cbbfd887770fd0b5021cc15bcfe07 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Mon, 6 Oct 2025 17:16:32 +0200 Subject: [PATCH 2/9] no alpha or beta --- src/huggingface_hub/cli/_cli_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 56a238ba16..d2d1227013 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -175,7 +175,7 @@ def check_cli_update() -> None: def _check_cli_update() -> None: current_version = importlib.metadata.version("huggingface_hub") - if any(tag in current_version for tag in ["a", "b", "rc", "dev"]): + if any(tag in current_version for tag in ["rc", "dev"]): # Don't check for pre-releases or dev versions return From 1956b27ca44afa77159450392bf70ae70fef63a7 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Mon, 6 Oct 2025 17:21:59 +0200 Subject: [PATCH 3/9] dirty check --- .github/workflows/check-installers.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check-installers.yml b/.github/workflows/check-installers.yml index 0d582641ed..fa70ec22aa 100644 --- a/.github/workflows/check-installers.yml +++ b/.github/workflows/check-installers.yml @@ -36,9 +36,6 @@ jobs: export PATH="$BIN_DIR:$PATH" - - name: Check installation - shell: bash - run: | HF_VERSION_PATH="$HF_TEST_ROOT/hf-version.txt" hf version | tee "$HF_VERSION_PATH" if ! grep -Eq 'huggingface_hub version: [0-9]+(\.[0-9]+){1,2}' "$HF_VERSION_PATH"; then @@ -49,17 +46,14 @@ jobs: NO_COLOR=1 hf --help - rm -rf "$HF_TEST_ROOT" - - - name: Check installation method - shell: bash - run: | hf env if ! hf env | grep -Fq 'Installation method: hf_installer'; then echo "❌ Error: not installed with hf_installer." exit 1 fi + rm -rf "$HF_TEST_ROOT" + windows-installer: runs-on: windows-latest steps: @@ -97,13 +91,10 @@ jobs: } Remove-Item Env:NO_COLOR - Remove-Item -Path $hfTestRoot -Recurse -Force - - - name: Check installation method - shell: pwsh - run: | hf env if (-not (hf env | Select-String -SimpleMatch 'Installation method: hf_installer')) { Write-Error "❌ Error: not installed with hf_installer." exit 1 } + + Remove-Item -Path $hfTestRoot -Recurse -Force From 15d8e88daf10b208dc846cc145beb8c201d27f99 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Tue, 7 Oct 2025 16:29:26 +0200 Subject: [PATCH 4/9] check once every 24h --- src/huggingface_hub/cli/_cli_utils.py | 46 ++++++++++----------------- src/huggingface_hub/constants.py | 5 +-- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index d71e984fa2..8d22ea9514 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -17,6 +17,7 @@ import os import time from enum import Enum +from pathlib import Path from typing import TYPE_CHECKING, Annotated, Optional, Union import click @@ -182,17 +183,26 @@ def check_cli_update() -> None: def _check_cli_update() -> None: current_version = importlib.metadata.version("huggingface_hub") + # Skip if current version is a pre-release or dev version if any(tag in current_version for tag in ["rc", "dev"]): - # Don't check for pre-releases or dev versions return - cached_version = _get_cached_pypi_version() - if cached_version is None: - latest_version = _get_pypi_version() - _cache_pypi_version(latest_version) - else: - latest_version = cached_version + # Skip if already checked in the last 24 hours + if os.path.exists(constants.CHECK_FOR_UPDATE_DONE_PATH): + mtime = os.path.getmtime(constants.CHECK_FOR_UPDATE_DONE_PATH) + if (time.time() - mtime) < 24 * 3600: + return + + # Touch the file to mark that we did the check now + Path(constants.CHECK_FOR_UPDATE_DONE_PATH).touch() + + # Check latest version from PyPI + response = get_session().get("https://pypi.org/pypi/huggingface_hub/json", timeout=2) + hf_raise_for_status(response) + data = response.json() + latest_version = data["info"]["version"] + # If latest version is different from current, notify user if current_version != latest_version: method = installation_method() if method == "brew": @@ -211,25 +221,3 @@ def _check_cli_update() -> None: f"To update, run: {ANSI.bold(update_command)}\n", ) ) - - -def _get_pypi_version() -> str: - response = get_session().get("https://pypi.org/pypi/huggingface_hub/json", timeout=2) - hf_raise_for_status(response) - data = response.json() - return data["info"]["version"] - - -def _get_cached_pypi_version() -> Optional[str]: - if os.path.exists(constants.PYPI_LATEST_VERSION_PATH): - mtime = os.path.getmtime(constants.PYPI_LATEST_VERSION_PATH) - # If the file is older than 24h, we don't use it - if (time.time() - mtime) < 24 * 3600: - with open(constants.PYPI_LATEST_VERSION_PATH, "r", encoding="utf-8") as f: - return f.read().strip() - return None - - -def _cache_pypi_version(version: str) -> None: - with open(constants.PYPI_LATEST_VERSION_PATH, "w", encoding="utf-8") as f: - f.write(version) diff --git a/src/huggingface_hub/constants.py b/src/huggingface_hub/constants.py index a96c150e1a..90ba0a5889 100644 --- a/src/huggingface_hub/constants.py +++ b/src/huggingface_hub/constants.py @@ -163,8 +163,9 @@ def _as_int(value: Optional[str]) -> Optional[int]: HF_HUB_OFFLINE = _is_true(os.environ.get("HF_HUB_OFFLINE") or os.environ.get("TRANSFORMERS_OFFLINE")) -# Used to check if CLI is up-to-date -PYPI_LATEST_VERSION_PATH = os.path.join(HF_HOME, "pypi_latest_version") +# File created to mark that the version check has been done. +# Check is performed once per 24 hours at most. +CHECK_FOR_UPDATE_DONE_PATH = os.path.join(HF_HOME, ".check_for_update_done") # If set, log level will be set to DEBUG and all requests made to the Hub will be logged # as curl commands for reproducibility. From 93d5cdd2b3f147cd645edfb73b25d1f265c780ec Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Tue, 7 Oct 2025 16:37:22 +0200 Subject: [PATCH 5/9] move ANSI / tabulate utils to their own module to avoid circular import issues --- src/huggingface_hub/_login.py | 2 +- src/huggingface_hub/cli/_cli_utils.py | 57 +-------------- src/huggingface_hub/cli/auth.py | 4 +- src/huggingface_hub/cli/cache.py | 4 +- src/huggingface_hub/cli/download.py | 4 +- src/huggingface_hub/cli/repo.py | 3 +- .../cli/upload_large_folder.py | 4 +- src/huggingface_hub/utils/__init__.py | 1 + src/huggingface_hub/utils/_cache_manager.py | 2 +- src/huggingface_hub/utils/_terminal.py | 69 +++++++++++++++++++ ...st_utils_cli.py => test_utils_terminal.py} | 2 +- 11 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 src/huggingface_hub/utils/_terminal.py rename tests/{test_utils_cli.py => test_utils_terminal.py} (97%) diff --git a/src/huggingface_hub/_login.py b/src/huggingface_hub/_login.py index 4a358293c2..7700ab5b23 100644 --- a/src/huggingface_hub/_login.py +++ b/src/huggingface_hub/_login.py @@ -20,8 +20,8 @@ from typing import Optional from . import constants -from .cli._cli_utils import ANSI from .utils import ( + ANSI, capture_output, get_token, is_google_colab, diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 8d22ea9514..4389216e68 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -18,14 +18,13 @@ import time from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Annotated, Optional, Union +from typing import TYPE_CHECKING, Annotated, Optional import click import typer from huggingface_hub import __version__, constants -from huggingface_hub.hf_api import HfApi -from huggingface_hub.utils import get_session, hf_raise_for_status, installation_method, logging +from huggingface_hub.utils import ANSI, get_session, hf_raise_for_status, installation_method, logging logger = logging.get_logger() @@ -42,58 +41,6 @@ def get_hf_api(token: Optional[str] = None) -> "HfApi": return HfApi(token=token, library_name="hf", library_version=__version__) -class ANSI: - """ - Helper for en.wikipedia.org/wiki/ANSI_escape_code - """ - - _bold = "\u001b[1m" - _gray = "\u001b[90m" - _red = "\u001b[31m" - _reset = "\u001b[0m" - _yellow = "\u001b[33m" - - @classmethod - def bold(cls, s: str) -> str: - return cls._format(s, cls._bold) - - @classmethod - def gray(cls, s: str) -> str: - return cls._format(s, cls._gray) - - @classmethod - def red(cls, s: str) -> str: - return cls._format(s, cls._bold + cls._red) - - @classmethod - def yellow(cls, s: str) -> str: - return cls._format(s, cls._yellow) - - @classmethod - def _format(cls, s: str, code: str) -> str: - if os.environ.get("NO_COLOR"): - # See https://no-color.org/ - return s - return f"{code}{s}{cls._reset}" - - -def tabulate(rows: list[list[Union[str, int]]], headers: list[str]) -> str: - """ - Inspired by: - - - stackoverflow.com/a/8356620/593036 - - stackoverflow.com/questions/9535954/printing-lists-as-tabular-data - """ - col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)] - row_format = ("{{:{}}} " * len(headers)).format(*col_widths) - lines = [] - lines.append(row_format.format(*headers)) - lines.append(row_format.format(*["-" * w for w in col_widths])) - for row in rows: - lines.append(row_format.format(*row)) - return "\n".join(lines) - - #### TYPER UTILS diff --git a/src/huggingface_hub/cli/auth.py b/src/huggingface_hub/cli/auth.py index ce075b31f6..cb522c918c 100644 --- a/src/huggingface_hub/cli/auth.py +++ b/src/huggingface_hub/cli/auth.py @@ -39,8 +39,8 @@ from huggingface_hub.hf_api import whoami from .._login import auth_list, auth_switch, login, logout -from ..utils import get_stored_tokens, get_token, logging -from ._cli_utils import ANSI, TokenOpt, typer_factory +from ..utils import ANSI, get_stored_tokens, get_token, logging +from ._cli_utils import TokenOpt, typer_factory logger = logging.get_logger(__name__) diff --git a/src/huggingface_hub/cli/cache.py b/src/huggingface_hub/cli/cache.py index c57f9f207f..35f7540821 100644 --- a/src/huggingface_hub/cli/cache.py +++ b/src/huggingface_hub/cli/cache.py @@ -23,8 +23,8 @@ import typer -from ..utils import CachedRepoInfo, CachedRevisionInfo, CacheNotFound, HFCacheInfo, scan_cache_dir -from ._cli_utils import ANSI, tabulate, typer_factory +from ..utils import ANSI, CachedRepoInfo, CachedRevisionInfo, CacheNotFound, HFCacheInfo, scan_cache_dir, tabulate +from ._cli_utils import typer_factory # --- DELETE helpers (from delete_cache.py) --- diff --git a/src/huggingface_hub/cli/download.py b/src/huggingface_hub/cli/download.py index 84251cfe39..655e44d0f7 100644 --- a/src/huggingface_hub/cli/download.py +++ b/src/huggingface_hub/cli/download.py @@ -44,9 +44,9 @@ from huggingface_hub import logging from huggingface_hub._snapshot_download import snapshot_download from huggingface_hub.file_download import DryRunFileInfo, hf_hub_download -from huggingface_hub.utils import _format_size, disable_progress_bars, enable_progress_bars +from huggingface_hub.utils import _format_size, disable_progress_bars, enable_progress_bars, tabulate -from ._cli_utils import RepoIdArg, RepoTypeOpt, RevisionOpt, TokenOpt, tabulate +from ._cli_utils import RepoIdArg, RepoTypeOpt, RevisionOpt, TokenOpt logger = logging.get_logger(__name__) diff --git a/src/huggingface_hub/cli/repo.py b/src/huggingface_hub/cli/repo.py index 793c1247c8..bb67ba9172 100644 --- a/src/huggingface_hub/cli/repo.py +++ b/src/huggingface_hub/cli/repo.py @@ -27,10 +27,9 @@ import typer from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError, RevisionNotFoundError -from huggingface_hub.utils import logging +from huggingface_hub.utils import ANSI, logging from ._cli_utils import ( - ANSI, PrivateOpt, RepoIdArg, RepoType, diff --git a/src/huggingface_hub/cli/upload_large_folder.py b/src/huggingface_hub/cli/upload_large_folder.py index fd5d6bb08e..af4fc55836 100644 --- a/src/huggingface_hub/cli/upload_large_folder.py +++ b/src/huggingface_hub/cli/upload_large_folder.py @@ -20,9 +20,9 @@ import typer from huggingface_hub import logging -from huggingface_hub.utils import disable_progress_bars +from huggingface_hub.utils import ANSI, disable_progress_bars -from ._cli_utils import ANSI, PrivateOpt, RepoIdArg, RepoType, RepoTypeOpt, RevisionOpt, TokenOpt, get_hf_api +from ._cli_utils import PrivateOpt, RepoIdArg, RepoType, RepoTypeOpt, RevisionOpt, TokenOpt, get_hf_api logger = logging.get_logger(__name__) diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py index b5fe7ad930..bccc01174d 100644 --- a/src/huggingface_hub/utils/__init__.py +++ b/src/huggingface_hub/utils/__init__.py @@ -110,6 +110,7 @@ from ._safetensors import SafetensorsFileMetadata, SafetensorsRepoMetadata, TensorInfo from ._subprocess import capture_output, run_interactive_subprocess, run_subprocess from ._telemetry import send_telemetry +from ._terminal import ANSI, tabulate from ._typing import is_jsonable, is_simple_optional_type, unwrap_simple_optional_type from ._validators import validate_hf_hub_args, validate_repo_id from ._xet import ( diff --git a/src/huggingface_hub/utils/_cache_manager.py b/src/huggingface_hub/utils/_cache_manager.py index 2d927cf278..3ec03f35e8 100644 --- a/src/huggingface_hub/utils/_cache_manager.py +++ b/src/huggingface_hub/utils/_cache_manager.py @@ -24,9 +24,9 @@ from huggingface_hub.errors import CacheNotFound, CorruptedCacheException -from ..cli._cli_utils import tabulate from ..constants import HF_HUB_CACHE from . import logging +from ._terminal import tabulate logger = logging.get_logger(__name__) diff --git a/src/huggingface_hub/utils/_terminal.py b/src/huggingface_hub/utils/_terminal.py new file mode 100644 index 0000000000..6463b4b3cc --- /dev/null +++ b/src/huggingface_hub/utils/_terminal.py @@ -0,0 +1,69 @@ +# Copyright 2025 The HuggingFace 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. +"""Contains utilities to print stuff to the terminal (styling, helpers).""" + +import os +from typing import Union + + +class ANSI: + """ + Helper for en.wikipedia.org/wiki/ANSI_escape_code + """ + + _bold = "\u001b[1m" + _gray = "\u001b[90m" + _red = "\u001b[31m" + _reset = "\u001b[0m" + _yellow = "\u001b[33m" + + @classmethod + def bold(cls, s: str) -> str: + return cls._format(s, cls._bold) + + @classmethod + def gray(cls, s: str) -> str: + return cls._format(s, cls._gray) + + @classmethod + def red(cls, s: str) -> str: + return cls._format(s, cls._bold + cls._red) + + @classmethod + def yellow(cls, s: str) -> str: + return cls._format(s, cls._yellow) + + @classmethod + def _format(cls, s: str, code: str) -> str: + if os.environ.get("NO_COLOR"): + # See https://no-color.org/ + return s + return f"{code}{s}{cls._reset}" + + +def tabulate(rows: list[list[Union[str, int]]], headers: list[str]) -> str: + """ + Inspired by: + + - stackoverflow.com/a/8356620/593036 + - stackoverflow.com/questions/9535954/printing-lists-as-tabular-data + """ + col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)] + row_format = ("{{:{}}} " * len(headers)).format(*col_widths) + lines = [] + lines.append(row_format.format(*headers)) + lines.append(row_format.format(*["-" * w for w in col_widths])) + for row in rows: + lines.append(row_format.format(*row)) + return "\n".join(lines) diff --git a/tests/test_utils_cli.py b/tests/test_utils_terminal.py similarity index 97% rename from tests/test_utils_cli.py rename to tests/test_utils_terminal.py index 5e6cdedeab..5b5f39bf5e 100644 --- a/tests/test_utils_cli.py +++ b/tests/test_utils_terminal.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from huggingface_hub.cli._cli_utils import ANSI, tabulate +from huggingface_hub.utils._terminal import ANSI, tabulate class TestCLIUtils(unittest.TestCase): From ae5bcbb99aff85ce305ee41df1849c579cc5604e Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Tue, 7 Oct 2025 16:39:29 +0200 Subject: [PATCH 6/9] do not touch installers CI --- .github/workflows/check-installers.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/check-installers.yml b/.github/workflows/check-installers.yml index fa70ec22aa..9a3757e1bc 100644 --- a/.github/workflows/check-installers.yml +++ b/.github/workflows/check-installers.yml @@ -46,12 +46,6 @@ jobs: NO_COLOR=1 hf --help - hf env - if ! hf env | grep -Fq 'Installation method: hf_installer'; then - echo "❌ Error: not installed with hf_installer." - exit 1 - fi - rm -rf "$HF_TEST_ROOT" windows-installer: @@ -91,10 +85,4 @@ jobs: } Remove-Item Env:NO_COLOR - hf env - if (-not (hf env | Select-String -SimpleMatch 'Installation method: hf_installer')) { - Write-Error "❌ Error: not installed with hf_installer." - exit 1 - } - Remove-Item -Path $hfTestRoot -Recurse -Force From 962e8c8167e8ba429764c316855d6762103294fa Mon Sep 17 00:00:00 2001 From: Lucain Date: Wed, 8 Oct 2025 09:14:47 +0200 Subject: [PATCH 7/9] Update src/huggingface_hub/cli/_cli_utils.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: célina --- src/huggingface_hub/cli/_cli_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 4389216e68..732c1c9d17 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -155,7 +155,7 @@ def _check_cli_update() -> None: if method == "brew": update_command = "brew upgrade huggingface-cli" elif method == "hf_installer" and os.name == "nt": - update_command = "curl -LsSf https://hf.co/cli/install.ps1 | pwsh -" + update_command = 'powershell -NoProfile -Command "iwr -useb https://hf.co/cli/install.ps1 | iex"' elif method == "hf_installer": update_command = "curl -LsSf https://hf.co/cli/install.sh | sh -" else: # unknown => likely pip From a546a89162eb5cc45acf5304947210b49bcfe824 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Wed, 8 Oct 2025 09:16:14 +0200 Subject: [PATCH 8/9] docstring --- src/huggingface_hub/cli/_cli_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/huggingface_hub/cli/_cli_utils.py b/src/huggingface_hub/cli/_cli_utils.py index 732c1c9d17..911aefbe4a 100644 --- a/src/huggingface_hub/cli/_cli_utils.py +++ b/src/huggingface_hub/cli/_cli_utils.py @@ -115,10 +115,10 @@ def check_cli_update() -> None: Check whether a newer version of `huggingface_hub` is available on PyPI. If a newer version is found, notify the user and suggest updating. - The latest PyPI version is cached locally in `$HF_HOME/pypi_latest_version` for 24 hours to prevent repeated notifications. If current version is a pre-release (e.g. `1.0.0.rc1`), or a dev version (e.g. `1.0.0.dev1`), no check is performed. - This function is called at the entry point of the CLI. + This function is called at the entry point of the CLI. It only performs the check once every 24 hours, and any error + during the check is caught and logged, to avoid breaking the CLI. """ try: _check_cli_update() From b975ece664df7544b1a1476a4439a0c19fe96374 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Wed, 8 Oct 2025 11:11:22 +0200 Subject: [PATCH 9/9] update powershell command --- docs/source/en/guides/cli.md | 2 +- docs/source/en/installation.md | 2 +- src/huggingface_hub/utils/_runtime.py | 2 +- utils/installers/install.ps1 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/en/guides/cli.md b/docs/source/en/guides/cli.md index 9cd5e73e77..c377718c19 100644 --- a/docs/source/en/guides/cli.md +++ b/docs/source/en/guides/cli.md @@ -28,7 +28,7 @@ On macOS and Linux: On Windows: ```powershell ->>> powershell -c "irm https://hf.co/cli/install.ps1 | iex" +>>> powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex" ``` Once installed, you can check that the CLI is correctly setup: diff --git a/docs/source/en/installation.md b/docs/source/en/installation.md index 73382afc84..e2c19bb69c 100644 --- a/docs/source/en/installation.md +++ b/docs/source/en/installation.md @@ -116,7 +116,7 @@ curl -LsSf https://hf.co/cli/install.sh | sh On Windows: ```powershell -powershell -c "irm https://hf.co/cli/install.ps1 | iex" +powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex" ``` ## Install with conda diff --git a/src/huggingface_hub/utils/_runtime.py b/src/huggingface_hub/utils/_runtime.py index 44a96f9967..445be52baf 100644 --- a/src/huggingface_hub/utils/_runtime.py +++ b/src/huggingface_hub/utils/_runtime.py @@ -359,7 +359,7 @@ def _is_hf_installer_installation() -> bool: i.e. using one of curl -LsSf https://hf.co/cli/install.sh | sh - curl -LsSf https://hf.co/cli/install.ps1 | pwsh - + powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex" """ venv = sys.prefix # points to venv root if active marker = Path(venv) / ".hf_installer_marker" diff --git a/utils/installers/install.ps1 b/utils/installers/install.ps1 index 1852439665..cb7f507cb8 100644 --- a/utils/installers/install.ps1 +++ b/utils/installers/install.ps1 @@ -1,5 +1,5 @@ # Hugging Face CLI Installer for Windows -# Usage: powershell -c "irm https://hf.co/cli/install.ps1 | iex" +# Usage: powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex" # Or: curl -LsSf https://hf.co/cli/install.ps1 | pwsh - <#