mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Add pyright
This commit is contained in:
parent
6a13ab98ad
commit
f1abe01b57
11 changed files with 76 additions and 28 deletions
|
|
@ -18,3 +18,8 @@ repos:
|
|||
- id: ruff-format
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/RobertCraigie/pyright-python
|
||||
rev: v1.1.352
|
||||
hooks:
|
||||
- id: pyright
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ build-backend = "hatchling.build"
|
|||
[project.optional-dependencies]
|
||||
tests = [
|
||||
"pre-commit",
|
||||
"pyright>=1.1.389",
|
||||
"pytest>=8.3.3",
|
||||
"pytest-asyncio>=0.23.5",
|
||||
"pytest-flakefinder",
|
||||
|
|
@ -39,3 +40,15 @@ asyncio_default_fixture_loop_scope = "session"
|
|||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.pyright]
|
||||
include = ["src"]
|
||||
exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
|
||||
pythonVersion = "3.10"
|
||||
pythonPlatform = "Darwin"
|
||||
typeCheckingMode = "basic"
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
useLibraryCodeForTypes = true
|
||||
venvPath = "."
|
||||
venv = ".venv"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -242,6 +243,7 @@ def dev(
|
|||
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
||||
check=True,
|
||||
shell=shell,
|
||||
env=dict(os.environ.items()), # Convert to list of tuples for env update
|
||||
)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
|
@ -423,7 +425,10 @@ def install(
|
|||
# Load from .env file if specified
|
||||
if env_file:
|
||||
try:
|
||||
env_dict.update(dotenv.dotenv_values(env_file))
|
||||
env_values = dotenv.dotenv_values(env_file)
|
||||
env_dict.update(
|
||||
(k, str(v)) for k, v in env_values.items() if v is not None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load .env file: {e}")
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"""Base classes and interfaces for FastMCP resources."""
|
||||
|
||||
import abc
|
||||
from typing import Union
|
||||
from typing import Union, Annotated
|
||||
|
||||
from pydantic import (
|
||||
AnyUrl,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
FileUrl,
|
||||
UrlConstraints,
|
||||
ValidationInfo,
|
||||
field_validator,
|
||||
)
|
||||
|
|
@ -19,8 +19,9 @@ class Resource(BaseModel, abc.ABC):
|
|||
|
||||
model_config = ConfigDict(validate_default=True)
|
||||
|
||||
# uri: Annotated[AnyUrl, BeforeValidator(maybe_cast_str_to_any_url)] = Field(
|
||||
uri: AnyUrl = Field(default=..., description="URI of the resource")
|
||||
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
|
||||
default=..., description="URI of the resource"
|
||||
)
|
||||
name: str | None = Field(description="Name of the resource", default=None)
|
||||
description: str | None = Field(
|
||||
description="Description of the resource", default=None
|
||||
|
|
@ -31,15 +32,6 @@ class Resource(BaseModel, abc.ABC):
|
|||
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
||||
)
|
||||
|
||||
@field_validator("uri", mode="before")
|
||||
def validate_uri(cls, uri: AnyUrl | str) -> AnyUrl:
|
||||
if isinstance(uri, str):
|
||||
# AnyUrl doesn't support triple-slashes, but files do ("file:///absolute/path")
|
||||
if uri.startswith("file://"):
|
||||
return FileUrl(uri)
|
||||
return AnyUrl(uri)
|
||||
return uri
|
||||
|
||||
@field_validator("name", mode="before")
|
||||
@classmethod
|
||||
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ResourceTemplate(BaseModel):
|
|||
result = await result
|
||||
|
||||
return FunctionResource(
|
||||
uri=uri,
|
||||
uri=uri, # type: ignore
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
mime_type=self.mime_type,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from mcp.types import (
|
|||
)
|
||||
from mcp.types import (
|
||||
Prompt as MCPPrompt,
|
||||
PromptArgument as MCPPromptArgument,
|
||||
)
|
||||
from mcp.types import (
|
||||
Resource as MCPResource,
|
||||
|
|
@ -159,7 +160,7 @@ class FastMCP:
|
|||
|
||||
async def call_tool(
|
||||
self, name: str, arguments: dict
|
||||
) -> Sequence[TextContent | ImageContent]:
|
||||
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
||||
"""Call a tool by name with arguments."""
|
||||
context = self.get_context()
|
||||
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
||||
|
|
@ -462,11 +463,11 @@ class FastMCP:
|
|||
name=prompt.name,
|
||||
description=prompt.description,
|
||||
arguments=[
|
||||
{
|
||||
"name": arg.name,
|
||||
"description": arg.description,
|
||||
"required": arg.required,
|
||||
}
|
||||
MCPPromptArgument(
|
||||
name=arg.name,
|
||||
description=arg.description,
|
||||
required=arg.required,
|
||||
)
|
||||
for arg in (prompt.arguments or [])
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class FuncMetadata(BaseModel):
|
|||
|
||||
async def call_fn_with_arg_validation(
|
||||
self,
|
||||
fn: Callable | Awaitable,
|
||||
fn: Callable[..., Any] | Awaitable[Any],
|
||||
fn_is_async: bool,
|
||||
arguments_to_validate: dict[str, Any],
|
||||
arguments_to_pass_directly: dict[str, Any] | None,
|
||||
|
|
@ -64,8 +64,12 @@ class FuncMetadata(BaseModel):
|
|||
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
||||
|
||||
if fn_is_async:
|
||||
if isinstance(fn, Awaitable):
|
||||
return await fn
|
||||
return await fn(**arguments_parsed_dict)
|
||||
return fn(**arguments_parsed_dict)
|
||||
if isinstance(fn, Callable):
|
||||
return fn(**arguments_parsed_dict)
|
||||
raise TypeError("fn must be either Callable or Awaitable")
|
||||
|
||||
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Pre-parse data from JSON.
|
||||
|
|
@ -123,6 +127,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
|
|||
sig = _get_typed_signature(func)
|
||||
params = sig.parameters
|
||||
dynamic_pydantic_model_params: dict[str, Any] = {}
|
||||
globalns = getattr(func, "__globals__", {})
|
||||
for param in params.values():
|
||||
if param.name.startswith("_"):
|
||||
raise InvalidSignature(
|
||||
|
|
@ -153,7 +158,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
|
|||
]
|
||||
|
||||
field_info = FieldInfo.from_annotated_attribute(
|
||||
annotation,
|
||||
_get_typed_annotation(annotation, globalns),
|
||||
param.default
|
||||
if param.default is not inspect.Parameter.empty
|
||||
else PydanticUndefined,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ class Image:
|
|||
if self.path:
|
||||
with open(self.path, "rb") as f:
|
||||
data = base64.b64encode(f.read()).decode()
|
||||
else:
|
||||
elif self.data is not None:
|
||||
data = base64.b64encode(self.data).decode()
|
||||
else:
|
||||
raise ValueError("No image data available")
|
||||
|
||||
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
||||
|
|
|
|||
|
|
@ -320,7 +320,11 @@ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|||
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
||||
)
|
||||
|
||||
assert mock_run.call_args_list[1][1] == {"check": True, "shell": True}
|
||||
# Verify subprocess call kwargs, allowing for environment variables
|
||||
call_kwargs = mock_run.call_args_list[1][1]
|
||||
assert call_kwargs["check"] is True
|
||||
assert call_kwargs["shell"] is True
|
||||
assert isinstance(call_kwargs["env"], dict)
|
||||
else:
|
||||
# same verification for unix, just with different command prefix
|
||||
actual_cmd = mock_run.call_args_list[0][0][0]
|
||||
|
|
@ -342,7 +346,11 @@ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|||
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
||||
)
|
||||
|
||||
assert mock_run.call_args_list[0][1] == {"check": True, "shell": False}
|
||||
# Verify subprocess call kwargs, allowing for environment variables
|
||||
call_kwargs = mock_run.call_args_list[0][1]
|
||||
assert call_kwargs["check"] is True
|
||||
assert call_kwargs["shell"] is False
|
||||
assert isinstance(call_kwargs["env"], dict)
|
||||
|
||||
|
||||
def test_run_with_dependencies(mock_config, server_file):
|
||||
|
|
|
|||
19
uv.lock
generated
19
uv.lock
generated
|
|
@ -228,7 +228,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "fastmcp"
|
||||
version = "0.3.6.dev0+gf03184b.d20241203"
|
||||
version = "0.3.6.dev5+g6a13ab9.d20241203"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
|
@ -245,6 +245,7 @@ dev = [
|
|||
{ name = "ipython" },
|
||||
{ name = "pdbpp" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-flakefinder" },
|
||||
|
|
@ -253,6 +254,7 @@ dev = [
|
|||
]
|
||||
tests = [
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-flakefinder" },
|
||||
|
|
@ -271,6 +273,8 @@ requires-dist = [
|
|||
{ name = "pre-commit", marker = "extra == 'tests'" },
|
||||
{ name = "pydantic", specifier = ">=2.5.3,<3.0.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.6.1" },
|
||||
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.389" },
|
||||
{ name = "pyright", marker = "extra == 'tests'", specifier = ">=1.1.389" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" },
|
||||
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=8.3.3" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.5" },
|
||||
|
|
@ -730,6 +734,19 @@ version = "0.9.0"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be0056080454cdbabe880773c3c5bd66d7b13f0c8b8b8c8da1e0c/pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775", size = 48744 }
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.389"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue