diff --git a/src/modelscope_hub/cli/login.py b/src/modelscope_hub/cli/login.py index d7500da..3983839 100644 --- a/src/modelscope_hub/cli/login.py +++ b/src/modelscope_hub/cli/login.py @@ -1,6 +1,6 @@ -"""``ms login`` and ``ms whoami`` commands. +"""``ms login``, ``ms logout`` and ``ms whoami`` commands. -The two flows share their HubApi construction but live in distinct +The three flows share their HubApi construction but live in distinct :class:`CLICommand` classes so each can be registered, tested and evolved independently. """ @@ -57,6 +57,29 @@ def execute(self) -> None: success(f"Logged in as {identity}.") +class LogoutCommand(CLICommand): + """Clear locally persisted ModelScope credentials.""" + + @staticmethod + def register(subparsers: SubParsers) -> None: + parser = subparsers.add_parser( + "logout", + help="Clear locally persisted ModelScope credentials.", + description="Removes saved cookies, git token and cached user identity.", + ) + # Legacy compat: allow subcommand-level auth flags even though logout + # never contacts the server. This keeps `ms logout --token ...` from + # failing on argument parsing in scripts that pass shared auth flags to + # every command. + add_subcmd_token_endpoint(parser) + parser.set_defaults(_command=LogoutCommand) + + def execute(self) -> None: + api = make_api(self.args) + api.logout() + success("Logged out.") + + class WhoamiCommand(CLICommand): """Show the currently authenticated user.""" diff --git a/src/modelscope_hub/cli/main.py b/src/modelscope_hub/cli/main.py index 28cefcf..d05050c 100644 --- a/src/modelscope_hub/cli/main.py +++ b/src/modelscope_hub/cli/main.py @@ -38,7 +38,7 @@ from .cache import CacheCommand, _CacheClear, _CacheScan from .deploy import DeployCommand, LogsCommand, SettingsCommand, StopCommand from .download import DownloadCommand -from .login import LoginCommand, WhoamiCommand +from .login import LoginCommand, LogoutCommand, WhoamiCommand from .mcp import McpCommand from .repo import CreateCommand, DeleteCommand, InfoCommand, ListCommand, RepoCommand from .secret import SecretCommand @@ -48,6 +48,7 @@ # importing it above and appending it here — that's it. _COMMANDS = [ LoginCommand, + LogoutCommand, WhoamiCommand, CreateCommand, InfoCommand, diff --git a/tests/cli/test_login.py b/tests/cli/test_login.py index 0dc939f..920765d 100644 --- a/tests/cli/test_login.py +++ b/tests/cli/test_login.py @@ -1,8 +1,8 @@ -"""Tests for ``ms login`` and ``ms whoami`` commands. +"""Tests for ``ms login``, ``ms logout`` and ``ms whoami`` commands. Includes: - Parser tests: argument parsing -- Execution tests: mock HubApi for login/whoami logic +- Execution tests: mock HubApi for login/logout/whoami logic - Remote tests: real API (existing) """ @@ -12,7 +12,7 @@ import pytest -from modelscope_hub.cli.login import LoginCommand, WhoamiCommand +from modelscope_hub.cli.login import LoginCommand, LogoutCommand, WhoamiCommand from modelscope_hub.types import UserInfo from .conftest import run_cli @@ -37,6 +37,27 @@ def test_subcmd_endpoint(self, parser): assert args.subcmd_endpoint == "https://custom.cn" +class TestLogoutParser: + """``ms logout`` argument parsing.""" + + def test_no_args(self, parser): + args = parser.parse_args(["logout"]) + assert args._command is LogoutCommand + + def test_subcmd_token_endpoint_is_accepted(self, parser): + args = parser.parse_args( + [ + "logout", + "--token", + "my-tok", + "--endpoint", + "https://x.cn", + ] + ) + assert args.subcmd_token == "my-tok" + assert args.subcmd_endpoint == "https://x.cn" + + class TestWhoamiParser: """``ms whoami`` argument parsing.""" @@ -144,6 +165,25 @@ def test_login_subcmd_endpoint_merged(self, parser, mock_api, capsys): assert args.endpoint == "https://custom.cn" +@pytest.mark.mock_only +class TestLogoutExecute: + """LogoutCommand.execute() logic.""" + + def test_logout_clears_credentials(self, parser, mock_api, capsys): + args = parser.parse_args(["logout"]) + with patch("modelscope_hub.cli.login.make_api", return_value=mock_api): + LogoutCommand(args).execute() + mock_api.logout.assert_called_once() + assert "Logged out" in capsys.readouterr().out + + def test_logout_dispatches_through_run_cmd(self, mock_api): + with patch("modelscope_hub.cli.login.make_api", return_value=mock_api): + code, out, err = run_cli(["logout"]) + assert code == 0 + assert "Logged out" in out + mock_api.logout.assert_called_once() + + @pytest.mark.mock_only class TestWhoamiExecute: """WhoamiCommand.execute() logic."""