Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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.
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")

# 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 = "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",
)
)
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
Loading