Skip to content

Commit 6f0733f

Browse files
committed
[hf CLI] check for updates and notify user
1 parent e9fa836 commit 6f0733f

File tree

9 files changed

+169
-7
lines changed

9 files changed

+169
-7
lines changed

.github/workflows/check-installers.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ jobs:
3636
3737
export PATH="$BIN_DIR:$PATH"
3838
39+
- name: Check installation
40+
shell: bash
41+
run: |
3942
HF_VERSION_PATH="$HF_TEST_ROOT/hf-version.txt"
4043
hf version | tee "$HF_VERSION_PATH"
4144
if ! grep -Eq 'huggingface_hub version: [0-9]+(\.[0-9]+){1,2}' "$HF_VERSION_PATH"; then
@@ -48,13 +51,22 @@ jobs:
4851
4952
rm -rf "$HF_TEST_ROOT"
5053
54+
- name: Check installation method
55+
shell: bash
56+
run: |
57+
hf env
58+
if ! hf env | grep -Fq 'Installation method: hf_installer'; then
59+
echo "❌ Error: not installed with hf_installer."
60+
exit 1
61+
fi
62+
5163
windows-installer:
5264
runs-on: windows-latest
5365
steps:
5466
- name: Checkout repository
5567
uses: actions/checkout@v4
5668

57-
- name: Run installer
69+
- name: Run installer
5870
shell: pwsh
5971
run: |
6072
$hfTestRoot = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())
@@ -86,3 +98,12 @@ jobs:
8698
Remove-Item Env:NO_COLOR
8799
88100
Remove-Item -Path $hfTestRoot -Recurse -Force
101+
102+
- name: Check installation method
103+
shell: pwsh
104+
run: |
105+
hf env
106+
if (-not (hf env | Select-String -SimpleMatch 'Installation method: hf_installer')) {
107+
Write-Error "❌ Error: not installed with hf_installer."
108+
exit 1
109+
}

src/huggingface_hub/cli/_cli_utils.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@
1313
# limitations under the License.
1414
"""Contains CLI utilities (styling, helpers)."""
1515

16+
import importlib.metadata
1617
import os
18+
import time
1719
from enum import Enum
1820
from typing import Annotated, Optional, Union
1921

2022
import click
2123
import typer
2224

23-
from huggingface_hub import __version__
25+
from huggingface_hub import __version__, constants
2426
from huggingface_hub.hf_api import HfApi
27+
from huggingface_hub.utils import get_session, hf_raise_for_status, installation_method, logging
28+
29+
30+
logger = logging.get_logger()
2531

2632

2733
class ANSI:
@@ -144,3 +150,79 @@ class RepoType(str, Enum):
144150

145151
def get_hf_api(token: Optional[str] = None) -> HfApi:
146152
return HfApi(token=token, library_name="hf", library_version=__version__)
153+
154+
155+
### PyPI VERSION CHECKER
156+
157+
158+
def check_cli_update() -> None:
159+
"""
160+
Check whether a newer version of `huggingface_hub` is available on PyPI.
161+
162+
If a newer version is found, notify the user and suggest updating.
163+
The latest PyPI version is cached locally in `$HF_HOME/pypi_latest_version` for 24 hours to prevent repeated notifications.
164+
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.
165+
166+
This function is called at the entry point of the CLI.
167+
"""
168+
try:
169+
_check_cli_update()
170+
except Exception:
171+
# We don't want the CLI to fail on version checks, no matter the reason.
172+
logger.debug("Error while checking for CLI update.", exc_info=True)
173+
174+
175+
def _check_cli_update() -> None:
176+
current_version = importlib.metadata.version("huggingface_hub")
177+
178+
if any(tag in current_version for tag in ["a", "b", "rc", "dev"]):
179+
# Don't check for pre-releases or dev versions
180+
return
181+
182+
cached_version = _get_cached_pypi_version()
183+
if cached_version is None:
184+
latest_version = _get_pypi_version()
185+
_cache_pypi_version(latest_version)
186+
else:
187+
latest_version = cached_version
188+
189+
if current_version != latest_version:
190+
method = installation_method()
191+
if method == "brew":
192+
update_command = "brew upgrade huggingface-cli"
193+
elif method == "hf_installer" and os.name == "nt":
194+
update_command = "curl -LsSf https://hf.co/cli/install.ps1 | pwsh -"
195+
elif method == "hf_installer":
196+
update_command = "curl -LsSf https://hf.co/cli/install.sh | sh -"
197+
else: # unknown => likely pip
198+
update_command = "pip install -U huggingface_hub"
199+
200+
click.echo(
201+
ANSI.yellow(
202+
f"A new version of huggingface_hub ({latest_version}) is available! "
203+
f"You are using version {current_version}.\n"
204+
f"To update, run: {ANSI.bold(update_command)}\n",
205+
)
206+
)
207+
208+
209+
def _get_pypi_version() -> str:
210+
response = get_session().get("https://pypi.org/pypi/huggingface_hub/json", timeout=2)
211+
hf_raise_for_status(response)
212+
data = response.json()
213+
return data["info"]["version"]
214+
215+
216+
def _get_cached_pypi_version() -> Optional[str]:
217+
if os.path.exists(constants.PYPI_LATEST_VERSION_PATH):
218+
mtime = os.path.getmtime(constants.PYPI_LATEST_VERSION_PATH)
219+
# If the file is older than 24h, we don't use it
220+
if (time.time() - mtime) < 24 * 3600:
221+
with open(constants.PYPI_LATEST_VERSION_PATH, "r", encoding="utf-8") as f:
222+
return f.read().strip()
223+
return None
224+
225+
226+
def _cache_pypi_version(version: str) -> None:
227+
with open(constants.PYPI_LATEST_VERSION_PATH, "w", encoding="utf-8") as f:
228+
f.write(version)

src/huggingface_hub/cli/hf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515

16-
from huggingface_hub.cli._cli_utils import typer_factory
16+
from huggingface_hub.cli._cli_utils import check_cli_update, typer_factory
1717
from huggingface_hub.cli.auth import auth_cli
1818
from huggingface_hub.cli.cache import cache_cli
1919
from huggingface_hub.cli.download import download
@@ -52,6 +52,7 @@
5252

5353
def main():
5454
logging.set_verbosity_info()
55+
check_cli_update()
5556
app()
5657

5758

src/huggingface_hub/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ def _as_int(value: Optional[str]) -> Optional[int]:
163163

164164
HF_HUB_OFFLINE = _is_true(os.environ.get("HF_HUB_OFFLINE") or os.environ.get("TRANSFORMERS_OFFLINE"))
165165

166+
# Used to check if CLI is up-to-date
167+
PYPI_LATEST_VERSION_PATH = os.path.join(HF_HOME, "pypi_latest_version")
168+
166169
# If set, log level will be set to DEBUG and all requests made to the Hub will be logged
167170
# as curl commands for reproducibility.
168171
HF_DEBUG = _is_true(os.environ.get("HF_DEBUG"))

src/huggingface_hub/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
get_tensorboard_version,
8787
get_tf_version,
8888
get_torch_version,
89+
installation_method,
8990
is_aiohttp_available,
9091
is_colab_enterprise,
9192
is_fastai_available,

src/huggingface_hub/utils/_http.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,9 @@ def _curlify(request: httpx.Request) -> str:
744744
flat_parts = []
745745
for k, v in parts:
746746
if k:
747-
flat_parts.append(quote(k))
747+
flat_parts.append(quote(str(k)))
748748
if v:
749-
flat_parts.append(quote(v))
749+
flat_parts.append(quote(str(v)))
750750

751751
return " ".join(flat_parts)
752752

src/huggingface_hub/utils/_runtime.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
import platform
2020
import sys
2121
import warnings
22-
from typing import Any
22+
from pathlib import Path
23+
from typing import Any, Literal
2324

2425
from .. import __version__, constants
2526

@@ -322,6 +323,49 @@ def is_colab_enterprise() -> bool:
322323
return os.environ.get("VERTEX_PRODUCT") == "COLAB_ENTERPRISE"
323324

324325

326+
# Check how huggingface_hub has been installed
327+
328+
329+
def installation_method() -> Literal["brew", "hf_installer", "unknown"]:
330+
"""Return the installation method of the current environment.
331+
332+
- "hf_installer" if installed via the official installer script
333+
- "brew" if installed via Homebrew
334+
- "unknown" otherwise
335+
"""
336+
if _is_brew_installation():
337+
return "brew"
338+
elif _is_hf_installer_installation():
339+
return "hf_installer"
340+
else:
341+
return "unknown"
342+
343+
344+
def _is_brew_installation() -> bool:
345+
"""Check if running from a Homebrew installation.
346+
347+
Note: AI-generated by Claude.
348+
"""
349+
exe_path = Path(sys.executable).resolve()
350+
exe_str = str(exe_path)
351+
352+
# Check common Homebrew paths
353+
# /opt/homebrew (Apple Silicon), /usr/local (Intel)
354+
return "/Cellar/" in exe_str or "/opt/homebrew/" in exe_str or exe_str.startswith("/usr/local/Cellar/")
355+
356+
357+
def _is_hf_installer_installation() -> bool:
358+
"""Return `True` if the current environment was set up via the official hf installer script.
359+
360+
i.e. using one of
361+
curl -LsSf https://hf.co/cli/install.sh | sh
362+
curl -LsSf https://hf.co/cli/install.ps1 | pwsh -
363+
"""
364+
venv = sys.prefix # points to venv root if active
365+
marker = Path(venv) / ".hf_installer_marker"
366+
return marker.exists()
367+
368+
325369
def dump_environment_info() -> dict[str, Any]:
326370
"""Dump information about the machine to help debugging issues.
327371
@@ -366,6 +410,9 @@ def dump_environment_info() -> dict[str, Any]:
366410
except Exception:
367411
pass
368412

413+
# How huggingface_hub has been installed?
414+
info["Installation method"] = installation_method()
415+
369416
# Installed dependencies
370417
info["Torch"] = get_torch_version()
371418
info["httpx"] = get_httpx_version()

utils/installers/install.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ function New-VirtualEnvironment {
223223
}
224224
if (-not $?) { throw "Failed to create virtual environment" }
225225

226+
# Mark this installation as installer-managed
227+
$markerFile = Join-Path $VENV_DIR ".hf_installer_marker"
228+
New-Item -Path $markerFile -ItemType File -Force | Out-Null
229+
226230
# Use the venv's python -m pip for deterministic upgrades
227231
$script:VenvPython = Join-Path $SCRIPTS_DIR "python.exe"
228232
Write-Log "Upgrading pip..."

utils/installers/install.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ detect_os() {
180180

181181
# Install Python if not available
182182
python_version_supported() {
183-
"$1" - <<'PY' >/dev/null 2>&1
183+
"$1" <<'PY' >/dev/null 2>&1
184184
import sys
185185
sys.exit(0 if sys.version_info >= (3, 9) else 1)
186186
PY
@@ -268,6 +268,9 @@ create_venv() {
268268

269269
run_command "Failed to create virtual environment at $VENV_DIR" "$PYTHON_CMD" -m venv "$VENV_DIR"
270270

271+
# Mark this installation as installer-managed
272+
touch "$VENV_DIR/.hf_installer_marker"
273+
271274
# Use the venv python for pip management
272275
log_info "Upgrading pip..."
273276
run_command "Failed to upgrade pip" "$VENV_DIR/bin/python" -m pip install --upgrade pip

0 commit comments

Comments
 (0)