Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def run(self):
package_data={"": ["**/*.cu", "**/*.cpp", "**/*.cuh", "**/*.h", "**/*.pyx", "py.typed"]},
zip_safe=False,
extras_require=extras,
entry_points={"console_scripts": ["transformers=transformers.cli.transformers:main"]},
entry_points={"console_scripts": ["transformers=transformers_cli.run:main"]},
python_requires=python_requires,
install_requires=list(install_requires),
classifiers=[
Expand Down
22 changes: 2 additions & 20 deletions src/transformers/cli/transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,10 @@
# limitations under the License.
"""Transformers CLI."""

from huggingface_hub import check_cli_update, typer_factory
from transformers_cli.run import app, main

from transformers.cli.add_new_model_like import add_new_model_like
from transformers.cli.chat import Chat
from transformers.cli.download import download
from transformers.cli.serve import Serve
from transformers.cli.system import env, version


app = typer_factory(help="Transformers CLI")

app.command()(add_new_model_like)
app.command(name="chat")(Chat)
app.command()(download)
app.command()(env)
app.command(name="serve")(Serve)
app.command()(version)


def main():
check_cli_update("transformers")
app()
__all__ = ["app", "main"]


if __name__ == "__main__":
Expand Down
19 changes: 19 additions & 0 deletions src/transformers_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Public exports for the lightweight `transformers` CLI entrypoint."""

from .run import app, main


__all__ = ["app", "main"]
176 changes: 176 additions & 0 deletions src/transformers_cli/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Copyright 2026 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Lightweight entrypoint for the public `transformers` CLI."""

import importlib
import os
import sys
from collections.abc import Sequence
from dataclasses import dataclass

import click
import typer
from huggingface_hub import check_cli_update, typer_factory
from huggingface_hub.cli._cli_utils import HFCliTyperGroup
from typer.main import get_command as get_typer_command


_CONTEXT_SETTINGS = {
"help_option_names": ["-h", "--help"],
"max_content_width": 120,
}

_SKIP_UPDATE_ARGUMENTS = {
"-h",
"--help",
"--install-completion",
"--show-completion",
}


@dataclass(frozen=True)
class LazyCommandSpec:
name: str
module_name: str
attr_name: str
short_help: str
topic: str = "main"
aliases: tuple[str, ...] = ()


_COMMAND_SPECS = (
LazyCommandSpec(
name="add-new-model-like",
module_name="transformers.cli.add_new_model_like",
attr_name="add_new_model_like",
short_help="Add a new model to the library, based on an existing one.",
),
LazyCommandSpec(
name="chat",
module_name="transformers.cli.chat",
attr_name="Chat",
short_help="Chat with a model from the command line.",
),
LazyCommandSpec(
name="download",
module_name="transformers.cli.download",
attr_name="download",
short_help="Download a model and its tokenizer from the Hub.",
),
LazyCommandSpec(
name="env",
module_name="transformers.cli.system",
attr_name="env",
short_help="Print information about the environment.",
),
LazyCommandSpec(
name="serve",
module_name="transformers.cli.serve",
attr_name="Serve",
short_help="Run a FastAPI server to serve models on-demand with an OpenAI compatible API.",
),
LazyCommandSpec(
name="version",
module_name="transformers.cli.system",
attr_name="version",
short_help="Print CLI version.",
),
)

_COMMANDS_BY_NAME = {spec.name: spec for spec in _COMMAND_SPECS}
_COMMANDS_BY_ALIAS = {alias: spec for spec in _COMMAND_SPECS for alias in spec.aliases}


def _build_click_command(spec: LazyCommandSpec) -> click.Command:
command = getattr(importlib.import_module(spec.module_name), spec.attr_name)
command_app = typer.Typer(
add_completion=False,
no_args_is_help=False,
rich_markup_mode=None,
pretty_exceptions_enable=False,
context_settings=_CONTEXT_SETTINGS,
)
command_app.command(name=spec.name, short_help=spec.short_help)(command)
return get_typer_command(command_app)


class TransformersCliGroup(HFCliTyperGroup):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._loaded_commands: dict[str, click.Command] = {}

def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
spec = _COMMANDS_BY_NAME.get(cmd_name) or _COMMANDS_BY_ALIAS.get(cmd_name)
if spec is None:
return None
if spec.name not in self._loaded_commands:
self._loaded_commands[spec.name] = _build_click_command(spec)
return self._loaded_commands[spec.name]

def _alias_map(self) -> dict[str, list[str]]:
return {spec.name: list(spec.aliases) for spec in _COMMAND_SPECS}

def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
topics: dict[str, list[tuple[str, str]]] = {}
alias_map = self._alias_map()

for spec in _COMMAND_SPECS:
help_text = spec.short_help
aliases = alias_map.get(spec.name, [])
if aliases:
help_text = f"{help_text} [alias: {', '.join(aliases)}]"
topics.setdefault(spec.topic, []).append((spec.name, help_text))

with formatter.section("Main commands"):
formatter.write_dl(topics.get("main", []))

for topic in sorted(topics):
if topic == "main":
continue
with formatter.section(f"{topic.capitalize()} commands"):
formatter.write_dl(topics[topic])

def list_commands(self, ctx: click.Context) -> list[str]:
return [spec.name for spec in _COMMAND_SPECS]

def format_epilog(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
if self.epilog:
formatter.write_paragraph()
formatter.write_text(self.epilog)


app = typer_factory(help="Transformers CLI", cls=TransformersCliGroup)


@app.callback()
def callback() -> None:
return None


def _should_check_for_updates(args: Sequence[str]) -> bool:
if not args:
return False
if os.environ.get("_TRANSFORMERS_COMPLETE") is not None:
return False
return not any(arg in _SKIP_UPDATE_ARGUMENTS for arg in args)


def main(args: Sequence[str] | None = None):
cli_args = list(sys.argv[1:] if args is None else args)
if _should_check_for_updates(cli_args):
check_cli_update("transformers")
return app(args=cli_args, prog_name="transformers")


__all__ = ["app", "main"]
4 changes: 2 additions & 2 deletions tests/cli/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pytest
from typer.testing import CliRunner

import transformers.cli.transformers
from transformers_cli.run import app


@pytest.fixture
Expand All @@ -33,7 +33,7 @@ def _noop(*a, **k):
sys.stdout.close = _noop
sys.stderr.close = _noop
try:
return runner.invoke(transformers.cli.transformers.app, list(args), catch_exceptions=False)
return runner.invoke(app, list(args), catch_exceptions=False)
finally:
sys.stdout.close = old_out_close
sys.stderr.close = old_err_close
Expand Down
32 changes: 32 additions & 0 deletions tests/cli/test_transformers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest.mock import patch


def test_top_level_help(cli):
output = cli("--help")
assert output.exit_code == 0
assert "Transformers CLI" in output.output
assert "Main commands" in output.output
assert "chat" in output.output
assert "serve" in output.output


def test_top_level_help_does_not_load_subcommands(cli):
with patch(
"transformers_cli.run.importlib.import_module", side_effect=AssertionError("subcommands should stay lazy")
):
output = cli("--help")

assert output.exit_code == 0
Loading