From 9d635bc026862a02db80f2fa31a6eb1041bf0294 Mon Sep 17 00:00:00 2001 From: dgenio Date: Sun, 1 Mar 2026 07:49:27 +0000 Subject: [PATCH] Address maintainer review: reload support, type annotation, test imports --- src/fastmcp/cli/cli.py | 21 ++++++++++++++-- src/fastmcp/cli/run.py | 3 ++- tests/cli/test_run.py | 56 ++++++++++++++---------------------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index bdacd4851..f6472c470 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -481,8 +481,6 @@ async def run( ignored_options.append("--port") if path: ignored_options.append("--path") - if reload: - ignored_options.append("--reload") if ignored_options: logger.warning( f"Options {', '.join(ignored_options)} are ignored in module mode " @@ -506,6 +504,25 @@ async def run( if env.build_command(test_cmd) != test_cmd: env_builder = env.build_command + if reload: + # Build a fastmcp run command for the reload watcher to restart + reload_cmd = ["fastmcp", "run", server_spec, "--module", "--no-reload"] + if log_level: + reload_cmd.extend(["--log-level", log_level]) + if no_banner: + reload_cmd.append("--no-banner") + if env_builder is not None: + reload_cmd.append("--skip-env") + if server_args: + reload_cmd.append("--") + reload_cmd.extend(server_args) + if env_builder is not None: + reload_cmd = env_builder(reload_cmd) + await run_module.run_with_reload( + reload_cmd, reload_dirs=reload_dir, is_stdio=True + ) + return + run_module.run_module_command( server_spec, env_command_builder=env_builder, diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index 8257aa2a2..36b029c38 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -8,6 +8,7 @@ import re import signal import subprocess import sys +from collections.abc import Callable from pathlib import Path from typing import Any, Literal @@ -259,7 +260,7 @@ async def run_command( def run_module_command( module_name: str, *, - env_command_builder: Any | None = None, + env_command_builder: Callable[[list[str]], list[str]] | None = None, extra_args: list[str] | None = None, ) -> None: """Run a Python module directly using ``python -m ``. diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index d55445b13..462bd7734 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -1,13 +1,18 @@ import inspect import json +import subprocess +import sys from pathlib import Path +from unittest.mock import AsyncMock, MagicMock, patch import pytest from pydantic import ValidationError +from fastmcp.cli.cli import inspector, run from fastmcp.cli.run import ( create_mcp_config_server, is_url, + run_module_command, ) from fastmcp.client.client import Client from fastmcp.client.transports import FastMCPTransport @@ -702,10 +707,6 @@ class TestRunModuleCommand: def test_runs_python_m_module(self): """Test that run_module_command invokes python -m .""" - from unittest.mock import MagicMock, patch - - from fastmcp.cli.run import run_module_command - mock_result = MagicMock() mock_result.returncode = 0 @@ -725,10 +726,6 @@ class TestRunModuleCommand: def test_forwards_extra_args(self): """Test that extra arguments are forwarded after the module name.""" - from unittest.mock import MagicMock, patch - - from fastmcp.cli.run import run_module_command - mock_result = MagicMock() mock_result.returncode = 0 @@ -746,10 +743,6 @@ class TestRunModuleCommand: def test_uses_env_command_builder(self): """Test that env_command_builder wraps the command.""" - from unittest.mock import MagicMock, patch - - from fastmcp.cli.run import run_module_command - mock_result = MagicMock() mock_result.returncode = 0 @@ -774,11 +767,6 @@ class TestRunModuleCommand: def test_exits_with_subprocess_error_code(self): """Test that non-zero exit codes from the module are propagated.""" - import subprocess - from unittest.mock import patch - - from fastmcp.cli.run import run_module_command - with ( patch( "fastmcp.cli.run.subprocess.run", @@ -792,11 +780,6 @@ class TestRunModuleCommand: def test_no_env_builder_runs_plain_python(self): """Test that without env_command_builder, plain python is used.""" - import sys - from unittest.mock import MagicMock, patch - - from fastmcp.cli.run import run_module_command - mock_result = MagicMock() mock_result.returncode = 0 @@ -819,8 +802,6 @@ class TestRunModuleMode: async def test_run_module_mode_requires_server_spec(self): """Test that module mode exits with error when server_spec is None.""" - from fastmcp.cli.cli import run - with pytest.raises(SystemExit) as exc_info: await run(None, module=True) @@ -828,10 +809,6 @@ class TestRunModuleMode: async def test_run_module_mode_warns_ignored_options(self, caplog): """Test that ignored options produce a warning in module mode.""" - from unittest.mock import MagicMock, patch - - from fastmcp.cli.cli import run - mock_result = MagicMock() mock_result.returncode = 0 @@ -846,17 +823,12 @@ class TestRunModuleMode: transport="sse", host="0.0.0.0", port=8080, - reload=True, ) assert any("ignored in module mode" in r.message for r in caplog.records) async def test_run_module_mode_delegates_to_run_module_command(self): """Test that module mode calls run_module_command with correct args.""" - from unittest.mock import MagicMock, patch - - from fastmcp.cli.cli import run - mock_result = MagicMock() mock_result.returncode = 0 @@ -872,16 +844,26 @@ class TestRunModuleMode: assert "-m" in cmd assert "my_module" in cmd + async def test_run_module_mode_with_reload(self): + """Test that --reload in module mode delegates to run_with_reload.""" + with patch( + "fastmcp.cli.run.run_with_reload", new_callable=AsyncMock + ) as mock_reload: + await run("my_module", module=True, reload=True, skip_env=True) + + mock_reload.assert_called_once() + cmd = mock_reload.call_args[0][0] + assert "fastmcp" in cmd + assert "--module" in cmd + assert "--no-reload" in cmd + assert "my_module" in cmd + class TestInspectorModuleMode: """Test the inspector command's module-mode handling.""" async def test_inspector_module_mode_skips_load_server(self): """Test that inspector with module=True skips load_server() and forwards --module.""" - from unittest.mock import AsyncMock, MagicMock, patch - - from fastmcp.cli.cli import inspector - mock_config = MagicMock() mock_config.deployment.port = 8080 mock_config.environment.build_command = lambda cmd: cmd