From 028a6ab5ddc67576046ad446461f35d981ed8e3c Mon Sep 17 00:00:00 2001 From: Tarek Ziade Date: Thu, 2 Apr 2026 10:02:31 +0200 Subject: [PATCH 1/3] lazy load for the CLI --- setup.py | 2 +- src/transformers/cli/transformers.py | 24 +++------------------- tests/cli/conftest.py | 4 ++-- tests/cli/test_transformers.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 tests/cli/test_transformers.py diff --git a/setup.py b/setup.py index 22b4ffc7fbe7..76640612c436 100644 --- a/setup.py +++ b/setup.py @@ -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:main"]}, python_requires=python_requires, install_requires=list(install_requires), classifiers=[ diff --git a/src/transformers/cli/transformers.py b/src/transformers/cli/transformers.py index cefee1ca97c8..af18115d6351 100644 --- a/src/transformers/cli/transformers.py +++ b/src/transformers/cli/transformers.py @@ -11,30 +11,12 @@ # 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. -"""Transformers CLI.""" +"""Compatibility wrapper for the public `transformers` CLI entrypoint.""" -from huggingface_hub import check_cli_update, typer_factory +from transformers_cli 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__": diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index cec797a3a485..93c991a87b92 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -16,7 +16,7 @@ import pytest from typer.testing import CliRunner -import transformers.cli.transformers +import transformers_cli @pytest.fixture @@ -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(transformers_cli.app, list(args), catch_exceptions=False) finally: sys.stdout.close = old_out_close sys.stderr.close = old_err_close diff --git a/tests/cli/test_transformers.py b/tests/cli/test_transformers.py new file mode 100644 index 000000000000..031e8a27f814 --- /dev/null +++ b/tests/cli/test_transformers.py @@ -0,0 +1,30 @@ +# 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. +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.importlib.import_module", side_effect=AssertionError("subcommands should stay lazy")): + output = cli("--help") + + assert output.exit_code == 0 From 78ac75363bae59cfc15ea55340f460aa13d06304 Mon Sep 17 00:00:00 2001 From: Tarek Ziade Date: Thu, 2 Apr 2026 10:30:04 +0200 Subject: [PATCH 2/3] forgot to add a file --- setup.py | 2 +- src/transformers/cli/transformers.py | 4 +- src/transformers_cli/__init__.py | 19 +++ src/transformers_cli/run.py | 176 +++++++++++++++++++++++++++ tests/cli/conftest.py | 4 +- tests/cli/test_transformers.py | 4 +- 6 files changed, 203 insertions(+), 6 deletions(-) create mode 100644 src/transformers_cli/__init__.py create mode 100644 src/transformers_cli/run.py diff --git a/setup.py b/setup.py index 76640612c436..b452a5568a7e 100644 --- a/setup.py +++ b/setup.py @@ -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:main"]}, + entry_points={"console_scripts": ["transformers=transformers_cli.run:main"]}, python_requires=python_requires, install_requires=list(install_requires), classifiers=[ diff --git a/src/transformers/cli/transformers.py b/src/transformers/cli/transformers.py index af18115d6351..ee59906b1199 100644 --- a/src/transformers/cli/transformers.py +++ b/src/transformers/cli/transformers.py @@ -11,9 +11,9 @@ # 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. -"""Compatibility wrapper for the public `transformers` CLI entrypoint.""" +"""Transformers CLI.""" -from transformers_cli import app, main +from transformers_cli.run import app, main __all__ = ["app", "main"] diff --git a/src/transformers_cli/__init__.py b/src/transformers_cli/__init__.py new file mode 100644 index 000000000000..8102b934ec4e --- /dev/null +++ b/src/transformers_cli/__init__.py @@ -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"] diff --git a/src/transformers_cli/run.py b/src/transformers_cli/run.py new file mode 100644 index 000000000000..c5bbf8f009d5 --- /dev/null +++ b/src/transformers_cli/run.py @@ -0,0 +1,176 @@ +# 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. +"""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"] diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 93c991a87b92..4474da0a4160 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -16,7 +16,7 @@ import pytest from typer.testing import CliRunner -import transformers_cli +from transformers_cli.run import app @pytest.fixture @@ -33,7 +33,7 @@ def _noop(*a, **k): sys.stdout.close = _noop sys.stderr.close = _noop try: - return runner.invoke(transformers_cli.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 diff --git a/tests/cli/test_transformers.py b/tests/cli/test_transformers.py index 031e8a27f814..93af75ad1b54 100644 --- a/tests/cli/test_transformers.py +++ b/tests/cli/test_transformers.py @@ -24,7 +24,9 @@ def test_top_level_help(cli): def test_top_level_help_does_not_load_subcommands(cli): - with patch("transformers_cli.importlib.import_module", side_effect=AssertionError("subcommands should stay lazy")): + with patch( + "transformers_cli.run.importlib.import_module", side_effect=AssertionError("subcommands should stay lazy") + ): output = cli("--help") assert output.exit_code == 0 From 0e27279611594dba0c362e527981e473d1b4461f Mon Sep 17 00:00:00 2001 From: Tarek Ziade Date: Thu, 2 Apr 2026 10:34:36 +0200 Subject: [PATCH 3/3] yeah time flies --- src/transformers_cli/run.py | 2 +- tests/cli/test_transformers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transformers_cli/run.py b/src/transformers_cli/run.py index c5bbf8f009d5..cdc40456de70 100644 --- a/src/transformers_cli/run.py +++ b/src/transformers_cli/run.py @@ -1,4 +1,4 @@ -# Copyright 2025 The HuggingFace Team. All rights reserved. +# 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. diff --git a/tests/cli/test_transformers.py b/tests/cli/test_transformers.py index 93af75ad1b54..547dca1ad57f 100644 --- a/tests/cli/test_transformers.py +++ b/tests/cli/test_transformers.py @@ -1,4 +1,4 @@ -# Copyright 2025 The HuggingFace Team. All rights reserved. +# 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.