Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-installers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- 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())
Expand Down
2 changes: 1 addition & 1 deletion src/huggingface_hub/_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
126 changes: 72 additions & 54 deletions src/huggingface_hub/cli/_cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +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 TYPE_CHECKING, Annotated, Optional, Union
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Optional

import click
import typer

from huggingface_hub import __version__
from huggingface_hub import __version__, constants
from huggingface_hub.utils import ANSI, get_session, hf_raise_for_status, installation_method, logging


logger = logging.get_logger()


if TYPE_CHECKING:
Expand All @@ -34,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


Expand Down Expand Up @@ -150,3 +105,66 @@ class RepoType(str, Enum):
help="Git revision id which can be a branch name, a tag, or a commit hash.",
),
]


### 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.
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. 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()
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")

# Skip if current version is a pre-release or dev version
if any(tag in current_version for tag in ["rc", "dev"]):
return

# 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":
update_command = "brew upgrade huggingface-cli"
elif method == "hf_installer" and os.name == "nt":
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
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",
)
)
4 changes: 2 additions & 2 deletions src/huggingface_hub/cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
4 changes: 2 additions & 2 deletions src/huggingface_hub/cli/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---
Expand Down
4 changes: 2 additions & 2 deletions src/huggingface_hub/cli/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
3 changes: 2 additions & 1 deletion src/huggingface_hub/cli/hf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,6 +52,7 @@

def main():
logging.set_verbosity_info()
check_cli_update()
app()


Expand Down
3 changes: 1 addition & 2 deletions src/huggingface_hub/cli/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/huggingface_hub/cli/upload_large_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
4 changes: 4 additions & 0 deletions src/huggingface_hub/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ 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"))

# 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.
HF_DEBUG = _is_true(os.environ.get("HF_DEBUG"))
Expand Down
2 changes: 2 additions & 0 deletions src/huggingface_hub/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
get_tensorboard_version,
get_tf_version,
get_torch_version,
installation_method,
is_aiohttp_available,
is_colab_enterprise,
is_fastai_available,
Expand All @@ -109,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 (
Expand Down
2 changes: 1 addition & 1 deletion src/huggingface_hub/utils/_cache_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
4 changes: 2 additions & 2 deletions src/huggingface_hub/utils/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,9 +716,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)

Expand Down
49 changes: 48 additions & 1 deletion src/huggingface_hub/utils/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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()
Expand Down
Loading