From fc1b70780b7d56ef27854558310222663ad2727b Mon Sep 17 00:00:00 2001 From: "Xingjun.Wang" Date: Wed, 19 Aug 2026 17:39:43 +0800 Subject: [PATCH] Delegate CLI script ownership to modelscope-hub (#1780) --- modelscope/cli/cli.py | 16 +++++-- pyproject.toml | 13 +++-- requirements/hub.txt | 2 +- tests/cli/test_cli_entry_points.py | 76 ++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 tests/cli/test_cli_entry_points.py diff --git a/modelscope/cli/cli.py b/modelscope/cli/cli.py index 030986dd..41eb5d24 100644 --- a/modelscope/cli/cli.py +++ b/modelscope/cli/cli.py @@ -1,11 +1,17 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """ModelScope CLI — delegates to the modelscope_hub CLI engine. -The legacy ``modelscope`` / ``ms`` console-script entry points historically -lived here as a hand-rolled argparse tree. The hub CLI in ``modelscope_hub`` -now owns command registration, plugin discovery, and error translation; -this module exists solely to preserve the import path used by the -``[project.scripts]`` entries in ``pyproject.toml``. +The ``modelscope`` / ``ms`` console scripts historically lived here as a +hand-rolled argparse tree. ``modelscope_hub`` now owns command registration, +plugin discovery, error translation *and* the console-script declarations for +all four aliases (``modelscope``, ``ms``, ``modelscope-hub``, ``ms-hub``), so +that a single distribution writes those files and neither package can strand +the other's CLI on upgrade. + +This module stays as the ``python -m modelscope.cli.cli`` entry point and as a +stable import path for callers that reference it directly. The commands this +package adds on top of the hub CLI are contributed through the +``modelscope_hub.cli_plugins`` entry-point group declared in ``pyproject.toml``. """ import sys diff --git a/pyproject.toml b/pyproject.toml index 0b620f96..80cdab19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,14 +18,21 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] [project.urls] Homepage = "https://github.com/modelscope/modelscope" -[project.scripts] -modelscope = "modelscope.cli.cli:run_cmd" -ms = "modelscope.cli.cli:run_cmd" +# No [project.scripts] here on purpose: the `modelscope`, `ms`, `modelscope-hub` +# and `ms-hub` console scripts are all owned by the `modelscope-hub` +# distribution that this package depends on. When two distributions declare the +# same script name, upgrading or removing either one can overwrite or delete the +# other's file and leave the user with no CLI at all; OS packagers such as +# FreeBSD pkg reject two ports owning one path outright. This package +# contributes its own commands through the plugin group below instead. +# +# `python -m modelscope.cli.cli` remains available as a direct entry point. [project.entry-points."modelscope_hub.cli_plugins"] pipeline = "modelscope.cli.pipeline:PipelineCMD" diff --git a/requirements/hub.txt b/requirements/hub.txt index f7dd5252..a564f367 100644 --- a/requirements/hub.txt +++ b/requirements/hub.txt @@ -1,5 +1,5 @@ filelock -modelscope-hub>=0.2.0 +modelscope-hub>=0.3.0 packaging requests>=2.25 setuptools diff --git a/tests/cli/test_cli_entry_points.py b/tests/cli/test_cli_entry_points.py new file mode 100644 index 00000000..eee639f0 --- /dev/null +++ b/tests/cli/test_cli_entry_points.py @@ -0,0 +1,76 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Guards for CLI console-script ownership. + +Every ModelScope console script (``modelscope``, ``ms``, ``modelscope-hub``, +``ms-hub``) is installed by the ``modelscope-hub`` distribution. If this package +declared any of them again, the two distributions would fight over the same +file: upgrading or uninstalling either one could delete the other's script and +leave the user with no CLI, and OS packagers such as FreeBSD pkg refuse to let +two ports own one path. These tests keep that arrangement from regressing +silently. +""" + +import subprocess +import sys +import unittest +from pathlib import Path + +PLUGIN_GROUP = 'modelscope_hub.cli_plugins' +HUB_DIST = 'modelscope-hub' +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _pyproject(): + """Parse the project metadata, or skip where tomllib is unavailable.""" + if sys.version_info < (3, 11): + # tomllib is stdlib from 3.11; the declaration it reads is not + # interpreter-specific, so checking on newer runtimes is enough. + raise unittest.SkipTest('stdlib tomllib requires Python 3.11+') + import tomllib + return tomllib.loads( + (REPO_ROOT / 'pyproject.toml').read_text(encoding='utf-8')) + + +class TestConsoleScriptOwnership(unittest.TestCase): + """This package must not declare console scripts of its own.""" + + def test_declares_no_console_scripts(self): + scripts = _pyproject()['project'].get('scripts', {}) + self.assertEqual( + scripts, {}, + 'Console scripts belong to the modelscope-hub distribution. ' + 'Declaring them here lets either package delete the other side.') + + def test_contributes_commands_as_plugins(self): + """Giving up the scripts only works if the plugin group survives.""" + entry_points = _pyproject()['project']['entry-points'] + self.assertIn(PLUGIN_GROUP, entry_points) + self.assertTrue(entry_points[PLUGIN_GROUP]) + + def test_depends_on_the_script_owner(self): + """The CLI now comes from modelscope-hub, so it cannot be optional.""" + requirements = (REPO_ROOT / 'requirements' + / 'hub.txt').read_text(encoding='utf-8') + self.assertIn(HUB_DIST, requirements) + + +class TestCliShim(unittest.TestCase): + """``python -m modelscope.cli.cli`` must survive the handover.""" + + def test_delegates_to_the_hub_engine(self): + from modelscope_hub.cli.main import run_cmd as hub_run_cmd + + from modelscope.cli import cli + self.assertIs(cli._run_cmd, hub_run_cmd) + + def test_module_entry_point_still_runs(self): + result = subprocess.run( + [sys.executable, '-m', 'modelscope.cli.cli', '--version'], + capture_output=True, + text=True) + self.assertEqual(result.returncode, 0) + self.assertIn('modelscope', result.stdout) + + +if __name__ == '__main__': + unittest.main()