Skip to content

Commit 8b7c621

Browse files
[hf CLI] check for updates and notify user (#3418)
* [hf CLI] check for updates and notify user * no alpha or beta * dirty check * check once every 24h * move ANSI / tabulate utils to their own module to avoid circular import issues * do not touch installers CI * Update src/huggingface_hub/cli/_cli_utils.py Co-authored-by: célina <hanouticelina@gmail.com> * docstring * update powershell command --------- Co-authored-by: célina <hanouticelina@gmail.com>
1 parent 75adf0d commit 8b7c621

File tree

20 files changed

+223
-76
lines changed

20 files changed

+223
-76
lines changed

.github/workflows/check-installers.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
- name: Checkout repository
5555
uses: actions/checkout@v4
5656

57-
- name: Run installer
57+
- name: Run installer
5858
shell: pwsh
5959
run: |
6060
$hfTestRoot = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())

docs/source/en/guides/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ On macOS and Linux:
2828
On Windows:
2929

3030
```powershell
31-
>>> powershell -c "irm https://hf.co/cli/install.ps1 | iex"
31+
>>> powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex"
3232
```
3333

3434
Once installed, you can check that the CLI is correctly setup:

docs/source/en/installation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ curl -LsSf https://hf.co/cli/install.sh | sh
116116
On Windows:
117117

118118
```powershell
119-
powershell -c "irm https://hf.co/cli/install.ps1 | iex"
119+
powershell -ExecutionPolicy ByPass -c "irm https://hf.co/cli/install.ps1 | iex"
120120
```
121121

122122
## Install with conda

src/huggingface_hub/_login.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from typing import Optional
2121

2222
from . import constants
23-
from .cli._cli_utils import ANSI
2423
from .utils import (
24+
ANSI,
2525
capture_output,
2626
get_token,
2727
is_google_colab,

src/huggingface_hub/cli/_cli_utils.py

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +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
18-
from typing import TYPE_CHECKING, Annotated, Optional, Union
20+
from pathlib import Path
21+
from typing import TYPE_CHECKING, Annotated, Optional
1922

2023
import click
2124
import typer
2225

23-
from huggingface_hub import __version__
26+
from huggingface_hub import __version__, constants
27+
from huggingface_hub.utils import ANSI, get_session, hf_raise_for_status, installation_method, logging
28+
29+
30+
logger = logging.get_logger()
2431

2532

2633
if TYPE_CHECKING:
@@ -34,58 +41,6 @@ def get_hf_api(token: Optional[str] = None) -> "HfApi":
3441
return HfApi(token=token, library_name="hf", library_version=__version__)
3542

3643

37-
class ANSI:
38-
"""
39-
Helper for en.wikipedia.org/wiki/ANSI_escape_code
40-
"""
41-
42-
_bold = "\u001b[1m"
43-
_gray = "\u001b[90m"
44-
_red = "\u001b[31m"
45-
_reset = "\u001b[0m"
46-
_yellow = "\u001b[33m"
47-
48-
@classmethod
49-
def bold(cls, s: str) -> str:
50-
return cls._format(s, cls._bold)
51-
52-
@classmethod
53-
def gray(cls, s: str) -> str:
54-
return cls._format(s, cls._gray)
55-
56-
@classmethod
57-
def red(cls, s: str) -> str:
58-
return cls._format(s, cls._bold + cls._red)
59-
60-
@classmethod
61-
def yellow(cls, s: str) -> str:
62-
return cls._format(s, cls._yellow)
63-
64-
@classmethod
65-
def _format(cls, s: str, code: str) -> str:
66-
if os.environ.get("NO_COLOR"):
67-
# See https://no-color.org/
68-
return s
69-
return f"{code}{s}{cls._reset}"
70-
71-
72-
def tabulate(rows: list[list[Union[str, int]]], headers: list[str]) -> str:
73-
"""
74-
Inspired by:
75-
76-
- stackoverflow.com/a/8356620/593036
77-
- stackoverflow.com/questions/9535954/printing-lists-as-tabular-data
78-
"""
79-
col_widths = [max(len(str(x)) for x in col) for col in zip(*rows, headers)]
80-
row_format = ("{{:{}}} " * len(headers)).format(*col_widths)
81-
lines = []
82-
lines.append(row_format.format(*headers))
83-
lines.append(row_format.format(*["-" * w for w in col_widths]))
84-
for row in rows:
85-
lines.append(row_format.format(*row))
86-
return "\n".join(lines)
87-
88-
8944
#### TYPER UTILS
9045

9146

@@ -150,3 +105,66 @@ class RepoType(str, Enum):
150105
help="Git revision id which can be a branch name, a tag, or a commit hash.",
151106
),
152107
]
108+
109+
110+
### PyPI VERSION CHECKER
111+
112+
113+
def check_cli_update() -> None:
114+
"""
115+
Check whether a newer version of `huggingface_hub` is available on PyPI.
116+
117+
If a newer version is found, notify the user and suggest updating.
118+
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.
119+
120+
This function is called at the entry point of the CLI. It only performs the check once every 24 hours, and any error
121+
during the check is caught and logged, to avoid breaking the CLI.
122+
"""
123+
try:
124+
_check_cli_update()
125+
except Exception:
126+
# We don't want the CLI to fail on version checks, no matter the reason.
127+
logger.debug("Error while checking for CLI update.", exc_info=True)
128+
129+
130+
def _check_cli_update() -> None:
131+
current_version = importlib.metadata.version("huggingface_hub")
132+
133+
# Skip if current version is a pre-release or dev version
134+
if any(tag in current_version for tag in ["rc", "dev"]):
135+
return
136+
137+
# Skip if already checked in the last 24 hours
138+
if os.path.exists(constants.CHECK_FOR_UPDATE_DONE_PATH):
139+
mtime = os.path.getmtime(constants.CHECK_FOR_UPDATE_DONE_PATH)
140+
if (time.time() - mtime) < 24 * 3600:
141+
return
142+
143+
# Touch the file to mark that we did the check now
144+
Path(constants.CHECK_FOR_UPDATE_DONE_PATH).touch()
145+
146+
# Check latest version from PyPI
147+
response = get_session().get("https://pypi.org/pypi/huggingface_hub/json", timeout=2)
148+
hf_raise_for_status(response)
149+
data = response.json()
150+
latest_version = data["info"]["version"]
151+
152+
# If latest version is different from current, notify user
153+
if current_version != latest_version:
154+
method = installation_method()
155+
if method == "brew":
156+
update_command = "brew upgrade huggingface-cli"
157+
elif method == "hf_installer" and os.name == "nt":
158+
update_command = 'powershell -NoProfile -Command "iwr -useb https://hf.co/cli/install.ps1 | iex"'
159+
elif method == "hf_installer":
160+
update_command = "curl -LsSf https://hf.co/cli/install.sh | sh -"
161+
else: # unknown => likely pip
162+
update_command = "pip install -U huggingface_hub"
163+
164+
click.echo(
165+
ANSI.yellow(
166+
f"A new version of huggingface_hub ({latest_version}) is available! "
167+
f"You are using version {current_version}.\n"
168+
f"To update, run: {ANSI.bold(update_command)}\n",
169+
)
170+
)

src/huggingface_hub/cli/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
from huggingface_hub.hf_api import whoami
4040

4141
from .._login import auth_list, auth_switch, login, logout
42-
from ..utils import get_stored_tokens, get_token, logging
43-
from ._cli_utils import ANSI, TokenOpt, typer_factory
42+
from ..utils import ANSI, get_stored_tokens, get_token, logging
43+
from ._cli_utils import TokenOpt, typer_factory
4444

4545

4646
logger = logging.get_logger(__name__)

src/huggingface_hub/cli/cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323

2424
import typer
2525

26-
from ..utils import CachedRepoInfo, CachedRevisionInfo, CacheNotFound, HFCacheInfo, scan_cache_dir
27-
from ._cli_utils import ANSI, tabulate, typer_factory
26+
from ..utils import ANSI, CachedRepoInfo, CachedRevisionInfo, CacheNotFound, HFCacheInfo, scan_cache_dir, tabulate
27+
from ._cli_utils import typer_factory
2828

2929

3030
# --- DELETE helpers (from delete_cache.py) ---

src/huggingface_hub/cli/download.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444
from huggingface_hub import logging
4545
from huggingface_hub._snapshot_download import snapshot_download
4646
from huggingface_hub.file_download import DryRunFileInfo, hf_hub_download
47-
from huggingface_hub.utils import _format_size, disable_progress_bars, enable_progress_bars
47+
from huggingface_hub.utils import _format_size, disable_progress_bars, enable_progress_bars, tabulate
4848

49-
from ._cli_utils import RepoIdArg, RepoTypeOpt, RevisionOpt, TokenOpt, tabulate
49+
from ._cli_utils import RepoIdArg, RepoTypeOpt, RevisionOpt, TokenOpt
5050

5151

5252
logger = logging.get_logger(__name__)

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/cli/repo.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@
2727
import typer
2828

2929
from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError, RevisionNotFoundError
30-
from huggingface_hub.utils import logging
30+
from huggingface_hub.utils import ANSI, logging
3131

3232
from ._cli_utils import (
33-
ANSI,
3433
PrivateOpt,
3534
RepoIdArg,
3635
RepoType,

0 commit comments

Comments
 (0)