From f248845133928ffab1ad63f0efb6813bb976eaeb Mon Sep 17 00:00:00 2001 From: Bill Easton Date: Mon, 13 Apr 2026 19:07:32 -0500 Subject: [PATCH] Enable PERF and T20 ruff rules (#3845) Co-authored-by: Claude Opus 4.6 (1M context) --- pyproject.toml | 9 ++++++++ src/fastmcp/cli/generate.py | 8 +++---- src/fastmcp/cli/install/goose.py | 3 +-- .../client/sampling/handlers/anthropic.py | 22 +++++++++---------- .../client/sampling/handlers/google_genai.py | 6 ++--- .../client/sampling/handlers/openai.py | 9 ++++---- .../server/providers/prefab_synthesis.py | 8 ++++--- .../server/transforms/prompts_as_tools.py | 2 +- .../server/transforms/resources_as_tools.py | 4 ++-- src/fastmcp/utilities/inspect.py | 12 +++++----- 10 files changed, 45 insertions(+), 38 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1572feef0..d3748bbb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,7 @@ error-on-warning = true fixable = ["ALL"] ignore = [ "COM812", + "PERF203", # try-except in loop — all existing hits are intentional (retry loops, error skipping) "PLR0913", # Too many arguments, MCP Servers have a lot of arguments, OKAY?! "SIM102", # Dont require combining if statements ] @@ -194,12 +195,14 @@ extend-select = [ "INP", # flake8-no-pep420: Require __init__.py in namespace packages "ISC", # flake8-implicit-str-concat: Prevent accidental string concatenation "LOG", # flake8-logging: Catches logging module misuse + "PERF", # perflint: Performance anti-patterns (unnecessary copies, allocations) "PIE", # flake8-pie: More idiomatic Python code "PLE", # pylint-error: Catches actual errors (invalid operations, syntax issues) "RSE", # flake8-raise: Unnecessary parentheses on raise "RUF", # Ruff-specific: Modern best practices unique to Ruff "SIM", # flake8-simplify: Simplifies verbose code patterns "SLOT", # flake8-slots: Enforce __slots__ where applicable + "T20", # flake8-print: Catch accidental print() in library code "TID", # flake8-tidy-imports: Banned imports and relative import enforcement "UP", # pyupgrade: Modernize syntax for newer Python versions ] @@ -211,6 +214,10 @@ known-first-party = ["fastmcp"] "__init__.py" = ["F401", "I001", "RUF013"] # allow imports not at the top of the file "src/fastmcp/__init__.py" = ["E402"] +# CLI and example code legitimately uses print() for user-facing output +"src/fastmcp/cli/**.py" = ["T20"] +"src/fastmcp/client/oauth_callback.py" = ["T20"] +"src/fastmcp/contrib/**/example.py" = ["T20"] "!src/**.py" = [ # Only enforce extended ruff rules for code in src/ "B", # flake8-bugbear "C4", # flake8-comprehensions @@ -221,12 +228,14 @@ known-first-party = ["fastmcp"] "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging + "PERF", # perflint "PIE", # flake8-pie "PLE", # pylint-error "RSE", # flake8-raise "RUF", # Ruff-specific "SIM", # flake8-simplify "SLOT", # flake8-slots + "T20", # flake8-print "TID", # flake8-tidy-imports ] diff --git a/src/fastmcp/cli/generate.py b/src/fastmcp/cli/generate.py index b5e652909..c46cac50e 100644 --- a/src/fastmcp/cli/generate.py +++ b/src/fastmcp/cli/generate.py @@ -261,7 +261,7 @@ def _tool_function_source(tool: mcp.types.Tool) -> str: # Build call arguments, using parsed versions for JSON params call_arg_parts = [] - for prop_name, _ in properties.items(): + for prop_name in properties: safe_name = _to_python_identifier(prop_name) if any(pn == prop_name for pn, _ in json_params): call_arg_parts.append(f"{prop_name!r}: {safe_name}_parsed") @@ -313,8 +313,7 @@ def generate_cli_script( lines.append("from rich.console import Console") lines.append("") lines.append("from fastmcp import Client") - for imp in sorted(extra_imports): - lines.append(imp) + lines.extend(sorted(extra_imports)) lines.append("") # --- Transport config --- @@ -506,8 +505,7 @@ def generate_cli_script( "# ---------------------------------------------------------------------------" ) - for tool in tools: - lines.append(_tool_function_source(tool)) + lines.extend(_tool_function_source(tool) for tool in tools) # --- Entry point --- lines.append("") diff --git a/src/fastmcp/cli/install/goose.py b/src/fastmcp/cli/install/goose.py index 16161dcf1..8d5712833 100644 --- a/src/fastmcp/cli/install/goose.py +++ b/src/fastmcp/cli/install/goose.py @@ -47,8 +47,7 @@ def generate_goose_deeplink( extension_id = _slugify(name) params: list[str] = [f"cmd={quote(command, safe='')}"] - for arg in args: - params.append(f"arg={quote(arg, safe='')}") + params.extend(f"arg={quote(arg, safe='')}" for arg in args) params.append(f"id={quote(extension_id, safe='')}") params.append(f"name={quote(name, safe='')}") params.append(f"description={quote(description, safe='')}") diff --git a/src/fastmcp/client/sampling/handlers/anthropic.py b/src/fastmcp/client/sampling/handlers/anthropic.py index 0939121d1..945da5e2f 100644 --- a/src/fastmcp/client/sampling/handlers/anthropic.py +++ b/src/fastmcp/client/sampling/handlers/anthropic.py @@ -216,12 +216,11 @@ class AnthropicSamplingHandler: # Extract text content from the result result_content: str | list[TextBlockParam] = "" if item.content: - text_blocks: list[TextBlockParam] = [] - for sub_item in item.content: - if isinstance(sub_item, TextContent): - text_blocks.append( - TextBlockParam(type="text", text=sub_item.text) - ) + text_blocks: list[TextBlockParam] = [ + TextBlockParam(type="text", text=sub_item.text) + for sub_item in item.content + if isinstance(sub_item, TextContent) + ] if len(text_blocks) == 1: result_content = text_blocks[0]["text"] elif text_blocks: @@ -270,12 +269,11 @@ class AnthropicSamplingHandler: if isinstance(content, ToolResultContent): result_content_str: str | list[TextBlockParam] = "" if content.content: - text_parts: list[TextBlockParam] = [] - for item in content.content: - if isinstance(item, TextContent): - text_parts.append( - TextBlockParam(type="text", text=item.text) - ) + text_parts: list[TextBlockParam] = [ + TextBlockParam(type="text", text=item.text) + for item in content.content + if isinstance(item, TextContent) + ] if len(text_parts) == 1: result_content_str = text_parts[0]["text"] elif text_parts: diff --git a/src/fastmcp/client/sampling/handlers/google_genai.py b/src/fastmcp/client/sampling/handlers/google_genai.py index 614138722..7404fb1eb 100644 --- a/src/fastmcp/client/sampling/handlers/google_genai.py +++ b/src/fastmcp/client/sampling/handlers/google_genai.py @@ -280,9 +280,9 @@ def _convert_messages_to_google_genai_content( # Handle list content (tool calls + results) if isinstance(content, list): - parts: list[Part] = [] - for item in content: - parts.append(_sampling_content_to_google_genai_part(item)) + parts: list[Part] = [ + _sampling_content_to_google_genai_part(item) for item in content + ] if message.role == "user": google_messages.append(UserContent(parts=parts)) diff --git a/src/fastmcp/client/sampling/handlers/openai.py b/src/fastmcp/client/sampling/handlers/openai.py index bbd6a0ae5..2c8db6935 100644 --- a/src/fastmcp/client/sampling/handlers/openai.py +++ b/src/fastmcp/client/sampling/handlers/openai.py @@ -231,10 +231,11 @@ class OpenAISamplingHandler: # Collect tool results (added after assistant message) content_text = "" if item.content: - result_texts = [] - for sub_item in item.content: - if isinstance(sub_item, TextContent): - result_texts.append(sub_item.text) + result_texts = [ + sub_item.text + for sub_item in item.content + if isinstance(sub_item, TextContent) + ] content_text = "\n".join(result_texts) tool_messages.append( ChatCompletionToolMessageParam( diff --git a/src/fastmcp/server/providers/prefab_synthesis.py b/src/fastmcp/server/providers/prefab_synthesis.py index ddbf83248..2ee639d8c 100644 --- a/src/fastmcp/server/providers/prefab_synthesis.py +++ b/src/fastmcp/server/providers/prefab_synthesis.py @@ -173,9 +173,11 @@ def _walk_prefab_tools(server: FastMCP) -> list[Tool]: if isinstance(inner, FastMCPApp): sources.append(inner._local) for src in sources: - for component in src._components.values(): - if isinstance(component, Tool) and _is_prefab_tool(component): - results.append(component) + results.extend( + component + for component in src._components.values() + if isinstance(component, Tool) and _is_prefab_tool(component) + ) # Recurse into aggregate children from fastmcp.server.providers.aggregate import AggregateProvider diff --git a/src/fastmcp/server/transforms/prompts_as_tools.py b/src/fastmcp/server/transforms/prompts_as_tools.py index 078b250d0..2caa13c0f 100644 --- a/src/fastmcp/server/transforms/prompts_as_tools.py +++ b/src/fastmcp/server/transforms/prompts_as_tools.py @@ -104,7 +104,7 @@ class PromptsAsTools(Transform): result: list[dict[str, Any]] = [] for p in prompts: - result.append( + result.append( # noqa: PERF401 { "name": p.name, "description": p.description, diff --git a/src/fastmcp/server/transforms/resources_as_tools.py b/src/fastmcp/server/transforms/resources_as_tools.py index 780e513b7..2b0350205 100644 --- a/src/fastmcp/server/transforms/resources_as_tools.py +++ b/src/fastmcp/server/transforms/resources_as_tools.py @@ -110,7 +110,7 @@ class ResourcesAsTools(Transform): result: list[dict[str, Any]] = [] for r in resources: - result.append( + result.append( # noqa: PERF401 { "uri": str(r.uri), "name": r.name, @@ -120,7 +120,7 @@ class ResourcesAsTools(Transform): ) for t in templates: - result.append( + result.append( # noqa: PERF401 { "uri_template": t.uri_template, "name": t.name, diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py index 369351ba5..2e3348e22 100644 --- a/src/fastmcp/utilities/inspect.py +++ b/src/fastmcp/utilities/inspect.py @@ -136,7 +136,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed prompt information prompt_infos = [] for prompt in prompts_list: - prompt_infos.append( + prompt_infos.append( # noqa: PERF401 PromptInfo( key=prompt.key, name=prompt.name or prompt.key, @@ -156,7 +156,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed resource information resource_infos = [] for resource in resources_list: - resource_infos.append( + resource_infos.append( # noqa: PERF401 ResourceInfo( key=resource.key, uri=str(resource.uri), @@ -178,7 +178,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed template information template_infos = [] for template in templates_list: - template_infos.append( + template_infos.append( # noqa: PERF401 TemplateInfo( key=template.key, uri_template=template.uri_template, @@ -258,7 +258,7 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo: # Extract detailed tool information from MCP Tool objects tool_infos = [] for mcp_tool in mcp_tools: - tool_infos.append( + tool_infos.append( # noqa: PERF401 ToolInfo( key=mcp_tool.name, name=mcp_tool.name, @@ -301,7 +301,7 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo: # Extract detailed resource information from MCP Resource objects resource_infos = [] for mcp_resource in mcp_resources: - resource_infos.append( + resource_infos.append( # noqa: PERF401 ResourceInfo( key=str(mcp_resource.uri), uri=str(mcp_resource.uri), @@ -321,7 +321,7 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo: # Extract detailed template information from MCP ResourceTemplate objects template_infos = [] for mcp_template in mcp_templates: - template_infos.append( + template_infos.append( # noqa: PERF401 TemplateInfo( key=str(mcp_template.uriTemplate), uri_template=str(mcp_template.uriTemplate),