Fix static issues and platform dependencies

This commit is contained in:
Jeremiah Lowin 2025-05-10 09:05:53 -04:00
commit a4f5b32972

View file

@ -1,6 +1,5 @@
"""Tests for the CLI module."""
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
@ -10,7 +9,6 @@ from typer.testing import CliRunner
from fastmcp.cli import cli
# Set up test runner
runner = CliRunner()
@ -107,7 +105,10 @@ class TestHelperFunctions:
"""Test parsing valid environment variables."""
assert cli._parse_env_var("KEY=VALUE") == ("KEY", "VALUE")
assert cli._parse_env_var("KEY=") == ("KEY", "")
assert cli._parse_env_var("KEY=VALUE=WITH=EQUALS") == ("KEY", "VALUE=WITH=EQUALS")
assert cli._parse_env_var("KEY=VALUE=WITH=EQUALS") == (
"KEY",
"VALUE=WITH=EQUALS",
)
assert cli._parse_env_var(" KEY = VALUE ") == ("KEY", "VALUE")
def test_build_uv_command_basic(self):
@ -117,96 +118,125 @@ class TestHelperFunctions:
def test_build_uv_command_with_editable(self):
"""Test building uv command with editable flag."""
cmd = cli._build_uv_command("file.py", with_editable=Path("/path/to/project"))
project_path = Path("/path/to/project")
cmd = cli._build_uv_command("file.py", with_editable=project_path)
assert cmd == [
"uv", "run", "--with", "fastmcp",
"--with-editable", "/path/to/project",
"fastmcp", "run", "file.py"
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(project_path),
"fastmcp",
"run",
"file.py",
]
def test_build_uv_command_with_packages(self):
"""Test building uv command with additional packages."""
cmd = cli._build_uv_command("file.py", with_packages=["pkg1", "pkg2"])
assert cmd == [
"uv", "run", "--with", "fastmcp",
"--with", "pkg1", "--with", "pkg2",
"fastmcp", "run", "file.py"
"uv",
"run",
"--with",
"fastmcp",
"--with",
"pkg1",
"--with",
"pkg2",
"fastmcp",
"run",
"file.py",
]
def test_build_uv_command_full(self):
"""Test building full uv command with all options."""
project_path = Path("/path/to/project")
cmd = cli._build_uv_command(
"file.py:server",
with_editable=Path("/path/to/project"),
"file.py:server",
with_editable=project_path,
with_packages=["pkg1", "pkg2"],
)
assert cmd == [
"uv", "run", "--with", "fastmcp",
"--with-editable", "/path/to/project",
"--with", "pkg1", "--with", "pkg2",
"fastmcp", "run", "file.py:server"
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(project_path),
"--with",
"pkg1",
"--with",
"pkg2",
"fastmcp",
"run",
"file.py:server",
]
def test_parse_file_path_simple(self):
"""Test parsing simple file path."""
with patch("pathlib.Path.exists") as mock_exists, \
patch("pathlib.Path.is_file") as mock_is_file, \
patch("pathlib.Path.expanduser") as mock_expanduser, \
patch("pathlib.Path.resolve") as mock_resolve:
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("file.py")
mock_resolve.return_value = Path("file.py")
path, obj = cli._parse_file_path("file.py")
assert path == Path("file.py")
assert obj is None
def test_parse_file_path_with_object(self):
"""Test parsing file path with object."""
with patch("pathlib.Path.exists") as mock_exists, \
patch("pathlib.Path.is_file") as mock_is_file, \
patch("pathlib.Path.expanduser") as mock_expanduser, \
patch("pathlib.Path.resolve") as mock_resolve:
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("file.py")
mock_resolve.return_value = Path("file.py")
path, obj = cli._parse_file_path("file.py:server")
assert path == Path("file.py")
assert obj == "server"
def test_parse_file_path_windows(self):
"""Test parsing Windows file path."""
with patch("pathlib.Path.exists") as mock_exists, \
patch("pathlib.Path.is_file") as mock_is_file, \
patch("pathlib.Path.expanduser") as mock_expanduser, \
patch("pathlib.Path.resolve") as mock_resolve:
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = True
mock_expanduser.return_value = Path("C:/path/file.py")
mock_resolve.return_value = Path("C:/path/file.py")
path, obj = cli._parse_file_path("C:/path/file.py:server")
assert path == Path("C:/path/file.py")
assert obj == "server"
def test_parse_file_path_not_file(self, mock_exit, mock_logger):
"""Test parsing path that is not a file."""
with patch("pathlib.Path.exists") as mock_exists, \
patch("pathlib.Path.is_file") as mock_is_file, \
patch("pathlib.Path.expanduser") as mock_expanduser, \
patch("pathlib.Path.resolve") as mock_resolve:
with (
patch("pathlib.Path.exists") as mock_exists,
patch("pathlib.Path.is_file") as mock_is_file,
patch("pathlib.Path.expanduser") as mock_expanduser,
patch("pathlib.Path.resolve") as mock_resolve,
):
mock_exists.return_value = True
mock_is_file.return_value = False
mock_expanduser.return_value = Path("directory")
mock_resolve.return_value = Path("directory")
cli._parse_file_path("directory")
mock_logger.error.assert_called_once()
mock_exit.assert_called_once_with(1)
@ -228,12 +258,13 @@ class TestDevCommand:
def test_dev_command_success(self, temp_python_file, mock_logger):
"""Test successful dev command execution."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import, \
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx, \
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv, \
patch("subprocess.run") as mock_run:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx,
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv,
patch("subprocess.run") as mock_run,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.dependencies = ["extra_dep"]
@ -241,35 +272,36 @@ class TestDevCommand:
mock_get_npx.return_value = "npx"
mock_build_uv.return_value = ["uv", "command"]
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli.app, ["dev", str(temp_python_file)])
assert result.exit_code == 0
mock_run.assert_called_once()
# Check dependencies were passed correctly
mock_build_uv.assert_called_once_with(
str(temp_python_file),
None,
["extra_dep"]
str(temp_python_file), None, ["extra_dep"]
)
def test_dev_command_with_ui_port(self, temp_python_file):
"""Test dev command with UI port."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import, \
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx, \
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv, \
patch("subprocess.run") as mock_run:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx,
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv,
patch("subprocess.run") as mock_run,
):
mock_parse.return_value = (temp_python_file, None)
mock_import.return_value = MagicMock(dependencies=[])
mock_get_npx.return_value = "npx"
mock_build_uv.return_value = ["uv", "command"]
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli.app, ["dev", str(temp_python_file), "--ui-port", "3000"])
result = runner.invoke(
cli.app, ["dev", str(temp_python_file), "--ui-port", "3000"]
)
assert result.exit_code == 0
# Check environment variables were set
env = mock_run.call_args[1]["env"]
assert "CLIENT_PORT" in env
@ -277,21 +309,24 @@ class TestDevCommand:
def test_dev_command_with_server_port(self, temp_python_file):
"""Test dev command with server port."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import, \
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx, \
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv, \
patch("subprocess.run") as mock_run:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx,
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv,
patch("subprocess.run") as mock_run,
):
mock_parse.return_value = (temp_python_file, None)
mock_import.return_value = MagicMock(dependencies=[])
mock_get_npx.return_value = "npx"
mock_build_uv.return_value = ["uv", "command"]
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli.app, ["dev", str(temp_python_file), "--server-port", "8080"])
result = runner.invoke(
cli.app, ["dev", str(temp_python_file), "--server-port", "8080"]
)
assert result.exit_code == 0
# Check environment variables were set
env = mock_run.call_args[1]["env"]
assert "SERVER_PORT" in env
@ -299,24 +334,24 @@ class TestDevCommand:
def test_dev_command_inspector_version(self, temp_python_file):
"""Test dev command with specific inspector version."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import, \
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx, \
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv, \
patch("subprocess.run") as mock_run:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
patch("fastmcp.cli.cli._get_npx_command") as mock_get_npx,
patch("fastmcp.cli.cli._build_uv_command") as mock_build_uv,
patch("subprocess.run") as mock_run,
):
mock_parse.return_value = (temp_python_file, None)
mock_import.return_value = MagicMock(dependencies=[])
mock_get_npx.return_value = "npx"
mock_build_uv.return_value = ["uv", "command"]
mock_run.return_value = MagicMock(returncode=0)
result = runner.invoke(cli.app, [
"dev", str(temp_python_file),
"--inspector-version", "1.0.0"
])
result = runner.invoke(
cli.app, ["dev", str(temp_python_file), "--inspector-version", "1.0.0"]
)
assert result.exit_code == 0
# Check inspector version was used
inspector_cmd = mock_run.call_args[0][0][1]
assert inspector_cmd == "@modelcontextprotocol/inspector@1.0.0"
@ -327,96 +362,117 @@ class TestRunCommand:
def test_run_command_success(self, temp_python_file, mock_logger):
"""Test successful run command execution."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file)])
assert result.exit_code == 0
mock_server.run.assert_called_once_with()
mock_logger.info.assert_called_with(f'Found server "test_server" in {temp_python_file}')
mock_logger.info.assert_called_with(
f'Found server "test_server" in {temp_python_file}'
)
def test_run_command_with_transport(self, temp_python_file):
"""Test run command with transport option."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file), "--transport", "sse"])
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--transport", "sse"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(transport="sse")
def test_run_command_with_host(self, temp_python_file):
"""Test run command with host option."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file), "--host", "0.0.0.0"])
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--host", "0.0.0.0"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(host="0.0.0.0")
def test_run_command_with_port(self, temp_python_file):
"""Test run command with port option."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file), "--port", "8080"])
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--port", "8080"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(port=8080)
def test_run_command_with_log_level(self, temp_python_file):
"""Test run command with log level option."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, ["run", str(temp_python_file), "--log-level", "DEBUG"])
result = runner.invoke(
cli.app, ["run", str(temp_python_file), "--log-level", "DEBUG"]
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(log_level="DEBUG")
def test_run_command_with_multiple_options(self, temp_python_file):
"""Test run command with multiple options."""
with patch("fastmcp.cli.cli._parse_file_path") as mock_parse, \
patch("fastmcp.cli.cli._import_server") as mock_import:
with (
patch("fastmcp.cli.cli._parse_file_path") as mock_parse,
patch("fastmcp.cli.cli._import_server") as mock_import,
):
mock_parse.return_value = (temp_python_file, None)
mock_server = MagicMock()
mock_server.name = "test_server"
mock_import.return_value = mock_server
result = runner.invoke(cli.app, [
"run", str(temp_python_file),
"--transport", "sse",
"--host", "0.0.0.0",
"--port", "8080",
"--log-level", "DEBUG"
])
result = runner.invoke(
cli.app,
[
"run",
str(temp_python_file),
"--transport",
"sse",
"--host",
"0.0.0.0",
"--port",
"8080",
"--log-level",
"DEBUG",
],
)
assert result.exit_code == 0
mock_server.run.assert_called_once_with(
transport="sse",
host="0.0.0.0",
port=8080,
log_level="DEBUG"
transport="sse", host="0.0.0.0", port=8080, log_level="DEBUG"
)