mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
feat: expand --reload to watch frontend file types (#3028)
This commit is contained in:
parent
8b3010825e
commit
d9cbc06ff1
2 changed files with 139 additions and 19 deletions
|
|
@ -25,6 +25,57 @@ logger = get_logger("cli.run")
|
|||
TransportType = Literal["stdio", "http", "sse", "streamable-http"]
|
||||
LogLevelType = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
|
||||
# File extensions to watch for reload
|
||||
WATCHED_EXTENSIONS: set[str] = {
|
||||
# Python
|
||||
".py",
|
||||
# JavaScript/TypeScript
|
||||
".js",
|
||||
".ts",
|
||||
".jsx",
|
||||
".tsx",
|
||||
# Markup/Content
|
||||
".html",
|
||||
".md",
|
||||
".mdx",
|
||||
".txt",
|
||||
".xml",
|
||||
# Styles
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
# Data/Config
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
# Framework-specific
|
||||
".vue",
|
||||
".svelte",
|
||||
# GraphQL
|
||||
".graphql",
|
||||
".gql",
|
||||
# Images
|
||||
".svg",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".ico",
|
||||
".webp",
|
||||
# Media
|
||||
".mp3",
|
||||
".mp4",
|
||||
".wav",
|
||||
".webm",
|
||||
# Fonts
|
||||
".woff",
|
||||
".woff2",
|
||||
".ttf",
|
||||
".eot",
|
||||
}
|
||||
|
||||
|
||||
def is_url(path: str) -> bool:
|
||||
"""Check if a string is a URL."""
|
||||
|
|
@ -231,9 +282,9 @@ async def run_v1_server_async(
|
|||
await server.run_sse_async()
|
||||
|
||||
|
||||
def _python_file_filter(change: Change, path: str) -> bool:
|
||||
"""Filter for Python files only."""
|
||||
return path.endswith(".py")
|
||||
def _watch_filter(_change: Change, path: str) -> bool:
|
||||
"""Filter for files that should trigger reload."""
|
||||
return any(path.endswith(ext) for ext in WATCHED_EXTENSIONS)
|
||||
|
||||
|
||||
async def _terminate_process(process: asyncio.subprocess.Process) -> None:
|
||||
|
|
@ -300,7 +351,7 @@ async def run_with_reload(
|
|||
|
||||
# Watch for either: file changes OR process death
|
||||
watch_task = asyncio.create_task(
|
||||
anext(aiter(awatch(*watch_paths, watch_filter=_python_file_filter)))
|
||||
anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter)))
|
||||
)
|
||||
wait_task = asyncio.create_task(process.wait())
|
||||
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
||||
|
|
@ -331,7 +382,7 @@ async def run_with_reload(
|
|||
|
||||
# Wait for file change or shutdown (avoid hot loop on crash)
|
||||
watch_task = asyncio.create_task(
|
||||
anext(aiter(awatch(*watch_paths, watch_filter=_python_file_filter)))
|
||||
anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter)))
|
||||
)
|
||||
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
|
|
|
|||
|
|
@ -605,24 +605,93 @@ mcp = fastmcp.FastMCP("TestServer")
|
|||
class TestReloadFunctionality:
|
||||
"""Test reload functionality."""
|
||||
|
||||
def test_python_file_filter_accepts_py_files(self):
|
||||
"""Test that Python file filter accepts .py files."""
|
||||
def test_watch_filter_accepts_watched_extensions(self):
|
||||
"""Test that watch filter accepts common source file extensions."""
|
||||
from watchfiles import Change
|
||||
|
||||
from fastmcp.cli.run import _python_file_filter
|
||||
from fastmcp.cli.run import _watch_filter
|
||||
|
||||
assert _python_file_filter(Change.modified, "/path/to/file.py") is True
|
||||
assert _python_file_filter(Change.added, "server.py") is True
|
||||
assert _python_file_filter(Change.deleted, "/some/dir/module.py") is True
|
||||
# Python
|
||||
assert _watch_filter(Change.modified, "/path/to/file.py") is True
|
||||
assert _watch_filter(Change.added, "server.py") is True
|
||||
# JavaScript/TypeScript
|
||||
assert _watch_filter(Change.modified, "/path/to/file.js") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.ts") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.jsx") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.tsx") is True
|
||||
# Markup/Content
|
||||
assert _watch_filter(Change.modified, "/path/to/file.html") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.md") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.txt") is True
|
||||
# Styles
|
||||
assert _watch_filter(Change.modified, "/path/to/file.css") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.scss") is True
|
||||
# Data/Config
|
||||
assert _watch_filter(Change.modified, "/path/to/file.json") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.yaml") is True
|
||||
# Images
|
||||
assert _watch_filter(Change.modified, "/path/to/file.png") is True
|
||||
assert _watch_filter(Change.modified, "/path/to/file.svg") is True
|
||||
|
||||
def test_python_file_filter_rejects_non_py_files(self):
|
||||
"""Test that Python file filter rejects non-.py files."""
|
||||
def test_watch_filter_rejects_unwatched_extensions(self):
|
||||
"""Test that watch filter rejects files not in the watched set."""
|
||||
from watchfiles import Change
|
||||
|
||||
from fastmcp.cli.run import _python_file_filter
|
||||
from fastmcp.cli.run import _watch_filter
|
||||
|
||||
assert _python_file_filter(Change.modified, "/path/to/file.txt") is False
|
||||
assert _python_file_filter(Change.modified, "/path/to/file.js") is False
|
||||
assert _python_file_filter(Change.modified, "/path/to/file.pyc") is False
|
||||
assert _python_file_filter(Change.modified, "/path/to/.py") is True # Edge case
|
||||
assert _python_file_filter(Change.modified, "Dockerfile") is False
|
||||
assert _watch_filter(Change.modified, "/path/to/file.pyc") is False
|
||||
assert _watch_filter(Change.modified, "/path/to/file.pyo") is False
|
||||
assert _watch_filter(Change.modified, "Dockerfile") is False
|
||||
assert _watch_filter(Change.modified, "/path/to/file.lock") is False
|
||||
assert _watch_filter(Change.modified, "/path/to/.gitignore") is False
|
||||
|
||||
def test_all_watched_extensions_are_accepted(self):
|
||||
"""Test that every extension in WATCHED_EXTENSIONS is accepted."""
|
||||
from watchfiles import Change
|
||||
|
||||
from fastmcp.cli.run import WATCHED_EXTENSIONS, _watch_filter
|
||||
|
||||
for ext in WATCHED_EXTENSIONS:
|
||||
path = f"/path/to/file{ext}"
|
||||
assert _watch_filter(Change.modified, path) is True, (
|
||||
f"Expected {ext} to be watched"
|
||||
)
|
||||
|
||||
def test_watched_extensions_includes_frontend_types(self):
|
||||
"""Verify WATCHED_EXTENSIONS contains the expected frontend file types."""
|
||||
from fastmcp.cli.run import WATCHED_EXTENSIONS
|
||||
|
||||
# Core frontend extensions that must be present
|
||||
expected = {
|
||||
# Python
|
||||
".py",
|
||||
# JavaScript/TypeScript
|
||||
".js",
|
||||
".ts",
|
||||
".jsx",
|
||||
".tsx",
|
||||
# Markup
|
||||
".html",
|
||||
".md",
|
||||
".mdx",
|
||||
".xml",
|
||||
# Styles
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
# Data/Config
|
||||
".json",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
# Images
|
||||
".png",
|
||||
".jpg",
|
||||
".svg",
|
||||
# Media
|
||||
".mp4",
|
||||
".mp3",
|
||||
}
|
||||
for ext in expected:
|
||||
assert ext in WATCHED_EXTENSIONS, f"Expected {ext} in WATCHED_EXTENSIONS"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue