| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| """Contains CLI utilities (styling, helpers).""" |
|
|
| import dataclasses |
| import datetime |
| import importlib.metadata |
| import json |
| import os |
| import re |
| import time |
| from enum import Enum |
| from pathlib import Path |
| from typing import TYPE_CHECKING, Annotated, Any, Callable, Literal, Optional, Sequence, Union, cast |
|
|
| import click |
| import typer |
|
|
| from huggingface_hub import __version__, constants |
| from huggingface_hub.utils import ANSI, get_session, hf_raise_for_status, installation_method, logging, tabulate |
| from huggingface_hub.utils._dotenv import load_dotenv |
|
|
|
|
| logger = logging.get_logger() |
|
|
| |
| _MAX_CELL_LENGTH = 35 |
|
|
| if TYPE_CHECKING: |
| from huggingface_hub.hf_api import HfApi |
|
|
|
|
| def get_hf_api(token: Optional[str] = None) -> "HfApi": |
| |
| from huggingface_hub.hf_api import HfApi |
|
|
| return HfApi(token=token, library_name="huggingface-cli", library_version=__version__) |
|
|
|
|
| |
|
|
| CLI_REFERENCE_URL = "https://huggingface.co/docs/huggingface_hub/en/guides/cli" |
|
|
|
|
| def generate_epilog(examples: list[str], docs_anchor: Optional[str] = None) -> str: |
| """Generate an epilog with examples and a Learn More section. |
| |
| Args: |
| examples: List of example commands (without the `$ ` prefix). |
| docs_anchor: Optional anchor for the docs URL (e.g., "#hf-download"). |
| |
| Returns: |
| Formatted epilog string. |
| """ |
| docs_url = f"{CLI_REFERENCE_URL}{docs_anchor}" if docs_anchor else CLI_REFERENCE_URL |
| examples_str = "\n".join(f" $ {ex}" for ex in examples) |
| return f"""\ |
| Examples |
| {examples_str} |
| |
| Learn more |
| Use `hf <command> --help` for more information about a command. |
| Read the documentation at {docs_url} |
| """ |
|
|
|
|
| TOPIC_T = Union[Literal["main", "help"], str] |
| FallbackHandlerT = Callable[[list[str], set[str]], Optional[int]] |
|
|
|
|
| def _format_epilog_no_indent(epilog: Optional[str], ctx: click.Context, formatter: click.HelpFormatter) -> None: |
| """Write the epilog without indentation.""" |
| if epilog: |
| formatter.write_paragraph() |
| for line in epilog.split("\n"): |
| formatter.write_text(line) |
|
|
|
|
| _ALIAS_SPLIT = re.compile(r"\s*\|\s*") |
|
|
|
|
| class HFCliTyperGroup(typer.core.TyperGroup): |
| """ |
| Typer Group that: |
| - lists commands alphabetically within sections. |
| - separates commands by topic (main, help, etc.). |
| - formats epilog without extra indentation. |
| - supports aliases via pipe-separated names (e.g. ``name="list | ls"``). |
| - rewrites ``--json`` to ``--format json`` for commands that accept ``--format``. |
| - rewrites ``spaces/user/repo`` to ``user/repo --type space`` for commands that accept ``--type``. |
| """ |
|
|
| def resolve_command(self, ctx: click.Context, args: list[str]) -> tuple: |
| cmd_name = args[0] if args and not args[0].startswith("-") else None |
| cmd = self.get_command(ctx, cmd_name) if cmd_name else None |
|
|
| if cmd is not None: |
| self._rewrite_json_shorthand(cmd, args) |
| self._rewrite_repo_type_prefix(cmd, args) |
|
|
| return super().resolve_command(ctx, args) |
|
|
| @staticmethod |
| def _rewrite_json_shorthand(cmd: click.Command, args: list[str]) -> None: |
| """Rewrite hidden ``--json`` shorthand to ``--format json``. |
| |
| Only applies to commands that accept ``--format``. This avoids rewriting |
| ``--json`` for commands that pass args through to external binaries |
| (e.g. ``hf extensions exec``) or that simply don't support ``--format``. |
| """ |
| if "--json" not in args: |
| return |
| has_format_option = any(isinstance(param, click.Option) and "--format" in param.opts for param in cmd.params) |
| if has_format_option: |
| if any(arg == "--format" or arg.startswith("--format=") for arg in args): |
| raise click.UsageError("'--json' and '--format' are mutually exclusive.") |
| idx = args.index("--json") |
| args[idx : idx + 1] = ["--format", "json"] |
|
|
| @staticmethod |
| def _rewrite_repo_type_prefix(cmd: click.Command, args: list[str]) -> None: |
| """Rewrite prefixed repo IDs (e.g. ``spaces/user/repo``) to ``user/repo --type space``. |
| |
| Only applies to commands that have a ``--type`` / ``--repo-type`` option and |
| at least one repo-ID positional argument (any ``click.Argument`` whose name |
| ends with ``_id``, e.g. ``repo_id``, ``from_id``, ``to_id``). When the |
| token that maps to such an argument matches ``{prefix}/org/repo`` (where |
| *prefix* is one of ``spaces``, ``datasets``, or ``models``), the prefix is |
| stripped and an implicit ``--type {type}`` is appended. An error is raised |
| if ``--type`` is also provided explicitly or if multiple prefixed arguments |
| disagree on the repo type. |
| |
| Only repo-ID positional slots are inspected so that other positional |
| arguments (filenames, local paths, patterns …) are never misinterpreted as |
| prefixed repo IDs. |
| """ |
| has_type_option = any(isinstance(param, click.Option) and "--type" in param.opts for param in cmd.params) |
| if not has_type_option: |
| return |
|
|
| |
| repo_id_positions: set[int] = set() |
| arg_idx = 0 |
| for param in cmd.params: |
| if isinstance(param, click.Argument): |
| if param.name in ("repo_id", "from_id", "to_id"): |
| repo_id_positions.add(arg_idx) |
| arg_idx += 1 |
|
|
| if not repo_id_positions: |
| return |
|
|
| |
| value_options: set[str] = set() |
| for param in cmd.params: |
| if isinstance(param, click.Option) and not param.is_flag: |
| for opt in (*param.opts, *param.secondary_opts): |
| value_options.add(opt) |
|
|
| |
| |
| positional_count = 0 |
| repo_id_arg_indices: list[int] = [] |
| i = 1 |
| while i < len(args): |
| arg = args[i] |
| if arg == "--": |
| break |
| if arg.startswith("-"): |
| if "=" in arg or arg not in value_options: |
| i += 1 |
| else: |
| i += 2 |
| else: |
| if positional_count in repo_id_positions: |
| repo_id_arg_indices.append(i) |
| positional_count += 1 |
| i += 1 |
|
|
| if not repo_id_arg_indices: |
| return |
|
|
| |
| inferred_type: Optional[str] = None |
| first_prefix: Optional[str] = None |
| rewrites: list[tuple[int, str]] = [] |
|
|
| for arg_index in repo_id_arg_indices: |
| parts = args[arg_index].split("/", 2) |
| if len(parts) != 3 or parts[0] not in constants.REPO_TYPES_MAPPING: |
| continue |
| prefix = parts[0] |
| mapped_type = constants.REPO_TYPES_MAPPING[prefix] |
| if inferred_type is not None and mapped_type != inferred_type: |
| raise click.UsageError(f"Conflicting repo type prefixes: '{first_prefix}/' and '{prefix}/'.") |
| inferred_type = mapped_type |
| first_prefix = prefix |
| rewrites.append((arg_index, f"{parts[1]}/{parts[2]}")) |
|
|
| if not rewrites: |
| return |
|
|
| |
| if any( |
| arg == "--type" or arg.startswith("--type=") or arg == "--repo-type" or arg.startswith("--repo-type=") |
| for arg in args |
| ): |
| raise click.UsageError( |
| f"Ambiguous repo type: got prefix '{first_prefix}/' in repo ID and explicit --type. Use one or the other." |
| ) |
|
|
| |
| for arg_index, new_value in rewrites: |
| args[arg_index] = new_value |
| args.extend(["--type", inferred_type]) |
|
|
| def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]: |
| |
| cmd = super().get_command(ctx, cmd_name) |
| if cmd is not None: |
| return cmd |
| |
| |
| for registered_name, registered_cmd in self.commands.items(): |
| aliases = _ALIAS_SPLIT.split(registered_name) |
| if cmd_name in aliases: |
| return registered_cmd |
| return None |
|
|
| def _alias_map(self) -> dict[str, list[str]]: |
| """Build a mapping from primary command name to its aliases (if any).""" |
| result: dict[str, list[str]] = {} |
| for registered_name in self.commands: |
| parts = _ALIAS_SPLIT.split(registered_name) |
| primary = parts[0] |
| result[primary] = parts[1:] |
| return result |
|
|
| def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: |
| topics: dict[str, list] = {} |
| alias_map = self._alias_map() |
|
|
| for name in self.list_commands(ctx): |
| cmd = self.get_command(ctx, name) |
| if cmd is None or cmd.hidden: |
| continue |
| help_text = cmd.get_short_help_str(limit=formatter.width) |
| aliases = alias_map.get(name, []) |
| if aliases: |
| help_text = f"{help_text} [alias: {', '.join(aliases)}]" |
| topic = getattr(cmd, "topic", "main") |
| topics.setdefault(topic, []).append((name, help_text)) |
|
|
| with formatter.section("Main commands"): |
| formatter.write_dl(topics["main"]) |
| for topic in sorted(topics.keys()): |
| if topic == "main": |
| continue |
| with formatter.section(f"{topic.capitalize()} commands"): |
| formatter.write_dl(topics[topic]) |
|
|
| def format_epilog(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: |
| |
| |
| all_examples: list[str] = [] |
| for name in self.list_commands(ctx): |
| cmd = self.get_command(ctx, name) |
| if cmd is None or cmd.hidden: |
| continue |
| cmd_examples = getattr(cmd, "examples", []) |
| if cmd_examples: |
| all_examples.append(cmd_examples[0]) |
|
|
| if all_examples: |
| epilog = generate_epilog(all_examples) |
| _format_epilog_no_indent(epilog, ctx, formatter) |
| elif self.epilog: |
| _format_epilog_no_indent(self.epilog, ctx, formatter) |
|
|
| def list_commands(self, ctx: click.Context) -> list[str]: |
| |
| primary_names: list[str] = [] |
| for name in self.commands: |
| primary = _ALIAS_SPLIT.split(name)[0] |
| primary_names.append(primary) |
| return sorted(primary_names) |
|
|
|
|
| def fallback_typer_group_factory( |
| fallback_handler: FallbackHandlerT, |
| extra_commands_provider: Optional[Callable[[], list[tuple[str, str]]]] = None, |
| ) -> type[HFCliTyperGroup]: |
| """Return a Typer group class that runs a fallback handler before command resolution.""" |
|
|
| class FallbackTyperGroup(HFCliTyperGroup): |
| def resolve_command(self, ctx: click.Context, args: list[str]) -> tuple: |
| fallback_exit_code = fallback_handler(args, set(self.commands.keys())) |
| if fallback_exit_code is not None: |
| raise SystemExit(fallback_exit_code) |
| return super().resolve_command(ctx, args) |
|
|
| def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: |
| super().format_commands(ctx, formatter) |
| if extra_commands_provider is not None: |
| entries = extra_commands_provider() |
| if entries: |
| with formatter.section("Extension commands"): |
| formatter.write_dl(entries) |
|
|
| return FallbackTyperGroup |
|
|
|
|
| def HFCliCommand(topic: TOPIC_T, examples: Optional[list[str]] = None) -> type[typer.core.TyperCommand]: |
| def format_epilog(self: click.Command, ctx: click.Context, formatter: click.HelpFormatter) -> None: |
| _format_epilog_no_indent(self.epilog, ctx, formatter) |
|
|
| return type( |
| f"TyperCommand{topic.capitalize()}", |
| (typer.core.TyperCommand,), |
| {"topic": topic, "examples": examples or [], "format_epilog": format_epilog}, |
| ) |
|
|
|
|
| class HFCliApp(typer.Typer): |
| """Custom Typer app for Hugging Face CLI.""" |
|
|
| def command( |
| self, |
| name: Optional[str] = None, |
| *, |
| topic: TOPIC_T = "main", |
| examples: Optional[list[str]] = None, |
| context_settings: Optional[dict[str, Any]] = None, |
| help: Optional[str] = None, |
| epilog: Optional[str] = None, |
| short_help: Optional[str] = None, |
| options_metavar: str = "[OPTIONS]", |
| add_help_option: bool = True, |
| no_args_is_help: bool = False, |
| hidden: bool = False, |
| deprecated: bool = False, |
| rich_help_panel: Optional[str] = None, |
| ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: |
| |
| if epilog is None and examples: |
| epilog = generate_epilog(examples) |
|
|
| def _inner(func: Callable[..., Any]) -> Callable[..., Any]: |
| return super(HFCliApp, self).command( |
| name, |
| cls=HFCliCommand(topic, examples), |
| context_settings=context_settings, |
| help=help, |
| epilog=epilog, |
| short_help=short_help, |
| options_metavar=options_metavar, |
| add_help_option=add_help_option, |
| no_args_is_help=no_args_is_help, |
| hidden=hidden, |
| deprecated=deprecated, |
| rich_help_panel=rich_help_panel, |
| )(func) |
|
|
| return _inner |
|
|
|
|
| def typer_factory( |
| help: str, epilog: Optional[str] = None, cls: Optional[type[typer.core.TyperGroup]] = None |
| ) -> "HFCliApp": |
| """Create a Typer app with consistent settings. |
| |
| Args: |
| help: Help text for the app. |
| epilog: Optional epilog text (use `generate_epilog` to create one). |
| cls: Optional Click group class to use (defaults to `HFCliTyperGroup`). |
| |
| Returns: |
| A configured Typer app. |
| """ |
| if cls is None: |
| cls = HFCliTyperGroup |
| return HFCliApp( |
| help=help, |
| epilog=epilog, |
| add_completion=True, |
| no_args_is_help=True, |
| cls=cls, |
| |
| rich_markup_mode=None, |
| rich_help_panel=None, |
| pretty_exceptions_enable=False, |
| |
| context_settings={ |
| "max_content_width": 120, |
| "help_option_names": ["-h", "--help"], |
| }, |
| ) |
|
|
|
|
| class RepoType(str, Enum): |
| model = "model" |
| dataset = "dataset" |
| space = "space" |
|
|
|
|
| RepoIdArg = Annotated[ |
| str, |
| typer.Argument( |
| help="The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`).", |
| ), |
| ] |
|
|
|
|
| RepoTypeOpt = Annotated[ |
| RepoType, |
| typer.Option( |
| "--type", |
| "--repo-type", |
| help="The type of repository (model, dataset, or space).", |
| ), |
| ] |
|
|
| TokenOpt = Annotated[ |
| Optional[str], |
| typer.Option( |
| help="A User Access Token generated from https://huggingface.co/settings/tokens.", |
| ), |
| ] |
|
|
| PrivateOpt = Annotated[ |
| Optional[bool], |
| typer.Option( |
| help="Whether to create a private repo if repo doesn't exist on the Hub. Ignored if the repo already exists.", |
| ), |
| ] |
|
|
| RevisionOpt = Annotated[ |
| Optional[str], |
| typer.Option( |
| help="Git revision id which can be a branch name, a tag, or a commit hash.", |
| ), |
| ] |
|
|
|
|
| LimitOpt = Annotated[ |
| int, |
| typer.Option(help="Limit the number of results."), |
| ] |
|
|
| AuthorOpt = Annotated[ |
| Optional[str], |
| typer.Option(help="Filter by author or organization."), |
| ] |
|
|
| FilterOpt = Annotated[ |
| Optional[list[str]], |
| typer.Option(help="Filter by tags (e.g. 'text-classification'). Can be used multiple times."), |
| ] |
|
|
| SearchOpt = Annotated[ |
| Optional[str], |
| typer.Option(help="Search query."), |
| ] |
|
|
|
|
| |
|
|
| EnvOpt = Annotated[ |
| Optional[list[str]], |
| typer.Option( |
| "-e", |
| "--env", |
| help="Set environment variables. E.g. --env ENV=value", |
| ), |
| ] |
|
|
| SecretsOpt = Annotated[ |
| Optional[list[str]], |
| typer.Option( |
| "-s", |
| "--secrets", |
| help=( |
| "Set secret environment variables. E.g. --secrets SECRET=value" |
| " or `--secrets HF_TOKEN` to pass your Hugging Face token." |
| ), |
| ), |
| ] |
|
|
| EnvFileOpt = Annotated[ |
| Optional[str], |
| typer.Option( |
| "--env-file", |
| help="Read in a file of environment variables.", |
| ), |
| ] |
|
|
| SecretsFileOpt = Annotated[ |
| Optional[str], |
| typer.Option( |
| help="Read in a file of secret environment variables.", |
| ), |
| ] |
|
|
|
|
| def _get_extended_environ() -> dict[str, str]: |
| """Return a copy of ``os.environ`` with the user's HF token injected (if available).""" |
| from huggingface_hub import get_token |
|
|
| extended_environ = os.environ.copy() |
| if (token := get_token()) is not None: |
| extended_environ["HF_TOKEN"] = token |
| return extended_environ |
|
|
|
|
| def parse_env_map( |
| env: Optional[list[str]] = None, |
| env_file: Optional[str] = None, |
| ) -> dict[str, Optional[str]]: |
| """Parse ``-e``/``--env``/``-s``/``--secrets`` and ``--env-file``/``--secrets-file`` CLI args into a dict. |
| |
| Uses an extended environment that includes the user's HF token so that |
| bare ``--secrets HF_TOKEN`` resolves correctly. |
| """ |
| extended_environ = _get_extended_environ() |
| env_map: dict[str, Optional[str]] = {} |
| if env_file: |
| env_map.update(load_dotenv(Path(env_file).read_text(), environ=extended_environ)) |
| for env_value in env or []: |
| env_map.update(load_dotenv(env_value, environ=extended_environ)) |
| return env_map |
|
|
|
|
| def env_map_to_key_value_list(env_map: dict[str, Optional[str]]) -> Optional[list[dict[str, str]]]: |
| """Convert an env/secrets dict to the ``[{"key": ..., "value": ...}]`` format used by the Hub API.""" |
| if not env_map: |
| return None |
| return [{"key": k, "value": v or ""} for k, v in env_map.items()] |
|
|
|
|
| class OutputFormat(str, Enum): |
| """Output format for CLI list commands.""" |
|
|
| table = "table" |
| json = "json" |
|
|
|
|
| FormatOpt = Annotated[ |
| OutputFormat, |
| typer.Option( |
| help="Output format (table or json).", |
| ), |
| ] |
|
|
| QuietOpt = Annotated[ |
| bool, |
| typer.Option( |
| "-q", |
| "--quiet", |
| help="Print only IDs (one per line).", |
| ), |
| ] |
|
|
|
|
| def _to_header(name: str) -> str: |
| """Convert a camelCase or PascalCase string to SCREAMING_SNAKE_CASE to be used as table header.""" |
| s = re.sub(r"([a-z])([A-Z])", r"\1_\2", name) |
| return s.upper() |
|
|
|
|
| def _format_value(value: Any) -> str: |
| """Convert a value to string for terminal display.""" |
| if not value: |
| return "" |
| if isinstance(value, bool): |
| return "✔" if value else "" |
| if isinstance(value, datetime.datetime): |
| return value.strftime("%Y-%m-%d") |
| if isinstance(value, str) and re.match(r"^\d{4}-\d{2}-\d{2}T", value): |
| return value[:10] |
| if isinstance(value, list): |
| return ", ".join(_format_value(v) for v in value) |
| elif isinstance(value, dict): |
| if "name" in value: |
| return str(value["name"]) |
| |
| return json.dumps(value) |
| return str(value) |
|
|
|
|
| def _format_cell(value: Any, max_len: int = _MAX_CELL_LENGTH) -> str: |
| """Format a value + truncate it for table display.""" |
| cell = _format_value(value) |
| if len(cell) > max_len: |
| cell = cell[: max_len - 3] + "..." |
| return cell |
|
|
|
|
| def print_as_table( |
| items: Sequence[dict[str, Any]], |
| headers: list[str], |
| row_fn: Callable[[dict[str, Any]], list[str]], |
| alignments: Optional[dict[str, str]] = None, |
| ) -> None: |
| """Print items as a formatted table. |
| |
| Args: |
| items: Sequence of dictionaries representing the items to display. |
| headers: List of column headers. |
| row_fn: Function that takes an item dict and returns a list of string values for each column. |
| alignments: Optional mapping of header name to "left" or "right". Defaults to "left". |
| """ |
| if not items: |
| print("No results found.") |
| return |
| rows = cast(list[list[Union[str, int]]], [row_fn(item) for item in items]) |
| screaming_headers = [_to_header(h) for h in headers] |
| |
| screaming_alignments = {_to_header(k): v for k, v in (alignments or {}).items()} |
| print(tabulate(rows, headers=screaming_headers, alignments=screaming_alignments)) |
|
|
|
|
| def print_list_output( |
| items: Sequence[dict[str, Any]], |
| format: OutputFormat, |
| quiet: bool, |
| id_key: str = "id", |
| headers: Optional[list[str]] = None, |
| row_fn: Optional[Callable[[dict[str, Any]], list[str]]] = None, |
| alignments: Optional[dict[str, str]] = None, |
| ) -> None: |
| """Print list command output in the specified format. |
| |
| Args: |
| items: Sequence of dictionaries representing the items to display. |
| format: Output format (table or json). |
| quiet: If True, print only IDs (one per line). |
| id_key: Key to use for extracting IDs in quiet mode. |
| headers: Optional list of column names for headers. If not provided, auto-detected from keys. |
| row_fn: Optional function to extract row values. If not provided, uses _format_cell on each column. |
| alignments: Optional mapping of header name to "left" or "right". Defaults to "left". |
| """ |
| if quiet: |
| for item in items: |
| print(item[id_key]) |
| return |
|
|
| if format == OutputFormat.json: |
| print(json.dumps(list(items), indent=2, default=str)) |
| return |
|
|
| if headers is None: |
| all_columns = list(items[0].keys()) if items else [id_key] |
| headers = [col for col in all_columns if any(_format_cell(item.get(col)) for item in items)] |
|
|
| if row_fn is None: |
|
|
| def row_fn(item: dict[str, Any]) -> list[str]: |
| return [_format_cell(item.get(col)) for col in headers] |
|
|
| print_as_table(items, headers=headers, row_fn=row_fn, alignments=alignments) |
|
|
|
|
| def _serialize_value(v: object) -> object: |
| """Recursively serialize a value to be JSON-compatible.""" |
| if isinstance(v, datetime.datetime): |
| return v.isoformat() |
| elif isinstance(v, dict): |
| return {key: _serialize_value(val) for key, val in v.items() if val is not None} |
| elif isinstance(v, list): |
| return [_serialize_value(item) for item in v] |
| return v |
|
|
|
|
| def api_object_to_dict(info: Any) -> dict[str, Any]: |
| """Convert repo info dataclasses to json-serializable dicts.""" |
| return {k: _serialize_value(v) for k, v in dataclasses.asdict(info).items() if v is not None} |
|
|
|
|
| def make_expand_properties_parser(valid_properties: list[str]): |
| """Create a callback to parse and validate comma-separated expand properties.""" |
|
|
| def _parse_expand_properties(value: Optional[str]) -> Optional[list[str]]: |
| if value is None: |
| return None |
| properties = [p.strip() for p in value.split(",")] |
| for prop in properties: |
| if prop not in valid_properties: |
| raise typer.BadParameter( |
| f"Invalid expand property: '{prop}'. Valid values are: {', '.join(valid_properties)}" |
| ) |
| return properties |
|
|
| return _parse_expand_properties |
|
|
|
|
| |
|
|
|
|
| def check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> None: |
| """ |
| Check whether a newer version of a library 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. |
| |
| Args: |
| library: The library to check for updates. Currently supports "huggingface_hub" and "transformers". |
| """ |
| try: |
| _check_cli_update(library) |
| except Exception: |
| |
| logger.debug("Error while checking for CLI update.", exc_info=True) |
|
|
|
|
| def _check_cli_update(library: Literal["huggingface_hub", "transformers"]) -> None: |
| current_version = importlib.metadata.version(library) |
|
|
| |
| if any(tag in current_version for tag in ["rc", "dev"]): |
| return |
|
|
| |
| 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 |
|
|
| |
| Path(constants.CHECK_FOR_UPDATE_DONE_PATH).parent.mkdir(parents=True, exist_ok=True) |
| Path(constants.CHECK_FOR_UPDATE_DONE_PATH).touch() |
|
|
| |
| response = get_session().get(f"https://pypi.org/pypi/{library}/json", timeout=2) |
| hf_raise_for_status(response) |
| data = response.json() |
| latest_version = data["info"]["version"] |
|
|
| |
| if current_version != latest_version: |
| if library == "huggingface_hub": |
| update_command = _get_huggingface_hub_update_command() |
| else: |
| update_command = _get_transformers_update_command() |
|
|
| click.echo( |
| ANSI.yellow( |
| f"A new version of {library} ({latest_version}) is available! " |
| f"You are using version {current_version}.\n" |
| f"To update, run: {ANSI.bold(update_command)}\n", |
| ) |
| ) |
|
|
|
|
| def _get_huggingface_hub_update_command() -> str: |
| """Return the command to update huggingface_hub.""" |
| method = installation_method() |
| if method == "brew": |
| return "brew upgrade hf" |
| elif method == "hf_installer" and os.name == "nt": |
| return 'powershell -NoProfile -Command "iwr -useb https://hf.co/cli/install.ps1 | iex"' |
| elif method == "hf_installer": |
| return "curl -LsSf https://hf.co/cli/install.sh | bash -" |
| else: |
| return "pip install -U huggingface_hub" |
|
|
|
|
| def _get_transformers_update_command() -> str: |
| """Return the command to update transformers.""" |
| method = installation_method() |
| if method == "hf_installer" and os.name == "nt": |
| return 'powershell -NoProfile -Command "iwr -useb https://hf.co/cli/install.ps1 | iex" -WithTransformers' |
| elif method == "hf_installer": |
| return "curl -LsSf https://hf.co/cli/install.sh | bash -s -- --with-transformers" |
| else: |
| return "pip install -U transformers" |
|
|