Delegate CLI script ownership to modelscope-hub (#1780)

This commit is contained in:
Xingjun.Wang
2026-08-19 17:39:43 +08:00
committed by GitHub
parent f0592cb67e
commit fc1b70780b
4 changed files with 98 additions and 9 deletions

View File

@@ -1,11 +1,17 @@
# Copyright (c) Alibaba, Inc. and its affiliates. # Copyright (c) Alibaba, Inc. and its affiliates.
"""ModelScope CLI — delegates to the modelscope_hub CLI engine. """ModelScope CLI — delegates to the modelscope_hub CLI engine.
The legacy ``modelscope`` / ``ms`` console-script entry points historically The ``modelscope`` / ``ms`` console scripts historically lived here as a
lived here as a hand-rolled argparse tree. The hub CLI in ``modelscope_hub`` hand-rolled argparse tree. ``modelscope_hub`` now owns command registration,
now owns command registration, plugin discovery, and error translation; plugin discovery, error translation *and* the console-script declarations for
this module exists solely to preserve the import path used by the all four aliases (``modelscope``, ``ms``, ``modelscope-hub``, ``ms-hub``), so
``[project.scripts]`` entries in ``pyproject.toml``. 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 import sys

View File

@@ -18,14 +18,21 @@ classifiers = [
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
] ]
[project.urls] [project.urls]
Homepage = "https://github.com/modelscope/modelscope" Homepage = "https://github.com/modelscope/modelscope"
[project.scripts] # No [project.scripts] here on purpose: the `modelscope`, `ms`, `modelscope-hub`
modelscope = "modelscope.cli.cli:run_cmd" # and `ms-hub` console scripts are all owned by the `modelscope-hub`
ms = "modelscope.cli.cli:run_cmd" # 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"] [project.entry-points."modelscope_hub.cli_plugins"]
pipeline = "modelscope.cli.pipeline:PipelineCMD" pipeline = "modelscope.cli.pipeline:PipelineCMD"

View File

@@ -1,5 +1,5 @@
filelock filelock
modelscope-hub>=0.2.0 modelscope-hub>=0.3.0
packaging packaging
requests>=2.25 requests>=2.25
setuptools setuptools

View File

@@ -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()