From 228425e3098bbae55d84eaa8e9b93b9da17bb851 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:22:11 -0400 Subject: [PATCH] Avoid propagating logs (#1042) * Avoid propagating logs * Set up caplog for non-root loggers * Update tests --- src/fastmcp/utilities/logging.py | 3 +++ src/fastmcp/utilities/tests.py | 13 +++++++++ tests/deprecated/test_settings.py | 9 ++++--- tests/prompts/test_prompt_manager.py | 10 +++++-- tests/resources/test_resource_manager.py | 9 +++++-- tests/server/test_mount.py | 34 +++++++++++++----------- tests/tools/test_tool.py | 4 +-- tests/tools/test_tool_manager.py | 7 +++-- 8 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/fastmcp/utilities/logging.py b/src/fastmcp/utilities/logging.py index d30eba0dc..810be92d5 100644 --- a/src/fastmcp/utilities/logging.py +++ b/src/fastmcp/utilities/logging.py @@ -50,3 +50,6 @@ def configure_logging( logger.removeHandler(hdlr) logger.addHandler(handler) + + # Don't propagate to the root logger + logger.propagate = False diff --git a/src/fastmcp/utilities/tests.py b/src/fastmcp/utilities/tests.py index 113163718..c1e8e1907 100644 --- a/src/fastmcp/utilities/tests.py +++ b/src/fastmcp/utilities/tests.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import logging import multiprocessing import socket import time @@ -129,3 +130,15 @@ def run_server_in_process( proc.join(timeout=2) if proc.is_alive(): raise RuntimeError("Server process failed to terminate even after kill") + + +@contextmanager +def caplog_for_fastmcp(caplog): + """Context manager to capture logs from FastMCP loggers even when propagation is disabled.""" + caplog.clear() + logger = logging.getLogger("FastMCP") + logger.addHandler(caplog.handler) + try: + yield + finally: + logger.removeHandler(caplog.handler) diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index e2bc66708..d1c57f17b 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -6,6 +6,7 @@ import pytest from fastmcp import FastMCP from fastmcp.settings import Settings +from fastmcp.utilities.tests import caplog_for_fastmcp # reset deprecation warnings for this module pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") @@ -313,7 +314,8 @@ class TestDeprecatedEnvironmentVariables: try: os.environ[env_var_name] = "192.168.1.1" - settings = Settings() + with caplog_for_fastmcp(caplog): + settings = Settings() # Check that a warning was logged assert any( @@ -341,8 +343,9 @@ class TestDeprecatedSettingsProperty: """Test that accessing fastmcp.settings.settings logs a deprecation warning.""" from fastmcp import settings - # Access the deprecated property - deprecated_settings = settings.settings + with caplog_for_fastmcp(caplog): + # Access the deprecated property + deprecated_settings = settings.settings # Check that a warning was logged assert any( diff --git a/tests/prompts/test_prompt_manager.py b/tests/prompts/test_prompt_manager.py index 5b359e56f..a002c1b25 100644 --- a/tests/prompts/test_prompt_manager.py +++ b/tests/prompts/test_prompt_manager.py @@ -7,6 +7,7 @@ from fastmcp.exceptions import NotFoundError, PromptError from fastmcp.prompts import Prompt from fastmcp.prompts.prompt import FunctionPrompt, PromptMessage, TextContent from fastmcp.prompts.prompt_manager import PromptManager +from fastmcp.utilities.tests import caplog_for_fastmcp class TestPromptManager: @@ -31,7 +32,10 @@ class TestPromptManager: manager = PromptManager(duplicate_behavior="warn") prompt = Prompt.from_function(fn) first = manager.add_prompt(prompt) - second = manager.add_prompt(prompt) + + with caplog_for_fastmcp(caplog): + second = manager.add_prompt(prompt) + assert first == second assert "Prompt already exists" in caplog.text @@ -58,7 +62,9 @@ class TestPromptManager: prompt = Prompt.from_function(test_fn, name="test_prompt") manager.add_prompt(prompt) - manager.add_prompt(prompt) + + with caplog_for_fastmcp(caplog): + manager.add_prompt(prompt) assert "Prompt already exists: test_prompt" in caplog.text # Should have the prompt diff --git a/tests/resources/test_resource_manager.py b/tests/resources/test_resource_manager.py index c458c49b5..ca8d66d69 100644 --- a/tests/resources/test_resource_manager.py +++ b/tests/resources/test_resource_manager.py @@ -11,6 +11,7 @@ from fastmcp.resources import ( ResourceTemplate, ) from fastmcp.resources.resource import FunctionResource +from fastmcp.utilities.tests import caplog_for_fastmcp @pytest.fixture @@ -78,7 +79,9 @@ class TestResourceManager: ) manager.add_resource(resource) - manager.add_resource(resource) + + with caplog_for_fastmcp(caplog): + manager.add_resource(resource) assert "Resource already exists" in caplog.text # Should have the resource @@ -181,7 +184,9 @@ class TestResourceManager: ) manager.add_template(template) - manager.add_template(template) + + with caplog_for_fastmcp(caplog): + manager.add_template(template) assert "Template already exists" in caplog.text # Should have the template diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 942809c4e..f6f7e198e 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -8,6 +8,7 @@ from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.client.transports import FastMCPTransport, SSETransport from fastmcp.server.proxy import FastMCPProxy +from fastmcp.utilities.tests import caplog_for_fastmcp class TestBasicMount: @@ -290,25 +291,26 @@ class TestMultipleServerMount: main_app.mount(unreachable_proxy, "unreachable") # All object types should work from working server despite unreachable proxy - async with Client(main_app) as client: - # Test tools - tools = await client.list_tools() - tool_names = [tool.name for tool in tools] - assert "working_working_tool" in tool_names + with caplog_for_fastmcp(caplog): + async with Client(main_app) as client: + # Test tools + tools = await client.list_tools() + tool_names = [tool.name for tool in tools] + assert "working_working_tool" in tool_names - # Test calling a tool - result = await client.call_tool("working_working_tool", {}) - assert result.data == "Working tool" + # Test calling a tool + result = await client.call_tool("working_working_tool", {}) + assert result.data == "Working tool" - # Test resources - resources = await client.list_resources() - resource_uris = [str(resource.uri) for resource in resources] - assert "working://working/data" in resource_uris + # Test resources + resources = await client.list_resources() + resource_uris = [str(resource.uri) for resource in resources] + assert "working://working/data" in resource_uris - # Test prompts - prompts = await client.list_prompts() - prompt_names = [prompt.name for prompt in prompts] - assert "working_working_prompt" in prompt_names + # Test prompts + prompts = await client.list_prompts() + prompt_names = [prompt.name for prompt in prompts] + assert "working_working_prompt" in prompt_names # Verify that warnings were logged for the unreachable server warning_messages = [ diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index 119d52f38..afe86d2ff 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -15,6 +15,7 @@ from typing_extensions import TypedDict from fastmcp.tools.tool import Tool, _convert_to_content from fastmcp.utilities.json_schema import compress_schema +from fastmcp.utilities.tests import caplog_for_fastmcp from fastmcp.utilities.types import Audio, File, Image @@ -1007,12 +1008,11 @@ class TestConvertResultToContent: def test_custom_serializer_error_fallback(self, caplog): """Test that if a custom serializer fails, it falls back to the default.""" - import logging def custom_serializer_that_fails(data): raise ValueError("Serialization failed") - with caplog.at_level(logging.WARNING): + with caplog_for_fastmcp(caplog): result = _convert_to_content( {"a": 1}, serializer=custom_serializer_that_fails ) diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 5ebf50c95..1e3611548 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -12,6 +12,7 @@ from fastmcp import Context, FastMCP from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.tools import FunctionTool, ToolManager from fastmcp.tools.tool import Tool +from fastmcp.utilities.tests import caplog_for_fastmcp from fastmcp.utilities.types import Image @@ -178,8 +179,10 @@ class TestAddTools: tool1 = Tool.from_function(test_fn, name="test_tool") manager.add_tool(tool1) - tool2 = Tool.from_function(test_fn, name="test_tool") - manager.add_tool(tool2) + + with caplog_for_fastmcp(caplog): + tool2 = Tool.from_function(test_fn, name="test_tool") + manager.add_tool(tool2) assert "Tool already exists: test_tool" in caplog.text # Should have the tool