diff --git a/fastmcp_slim/fastmcp/client/__init__.py b/fastmcp_slim/fastmcp/client/__init__.py index 9fc2f350b..84500b822 100644 --- a/fastmcp_slim/fastmcp/client/__init__.py +++ b/fastmcp_slim/fastmcp/client/__init__.py @@ -1,27 +1,55 @@ -from fastmcp import _install_hints +from typing import TYPE_CHECKING -try: +from fastmcp import _install_hints +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, +) + +if TYPE_CHECKING: from .auth import ( - BearerAuth, - ClientCredentialsOAuthProvider, - OAuth, - PrivateKeyJWTOAuthProvider, + BearerAuth as BearerAuth, ) - from .client import Client + from .auth import ( + ClientCredentialsOAuthProvider as ClientCredentialsOAuthProvider, + ) + from .auth import ( + OAuth as OAuth, + ) + from .auth import ( + PrivateKeyJWTOAuthProvider as PrivateKeyJWTOAuthProvider, + ) + from .client import Client as Client from .transports import ( - ClientTransport, - FastMCPTransport, - NodeStdioTransport, - NpxStdioTransport, - PythonStdioTransport, - SSETransport, - StdioTransport, - StreamableHttpTransport, - UvStdioTransport, - UvxStdioTransport, + ClientTransport as ClientTransport, + ) + from .transports import ( + FastMCPTransport as FastMCPTransport, + ) + from .transports import ( + NodeStdioTransport as NodeStdioTransport, + ) + from .transports import ( + NpxStdioTransport as NpxStdioTransport, + ) + from .transports import ( + PythonStdioTransport as PythonStdioTransport, + ) + from .transports import ( + SSETransport as SSETransport, + ) + from .transports import ( + StdioTransport as StdioTransport, + ) + from .transports import ( + StreamableHttpTransport as StreamableHttpTransport, + ) + from .transports import ( + UvStdioTransport as UvStdioTransport, + ) + from .transports import ( + UvxStdioTransport as UvxStdioTransport, ) -except ImportError as exc: - raise ImportError(_install_hints.CLIENT_SUPPORT) from exc __all__ = [ "BearerAuth", @@ -40,3 +68,35 @@ __all__ = [ "UvStdioTransport", "UvxStdioTransport", ] + +_LAZY_IMPORTS = { + "BearerAuth": (".auth", "BearerAuth"), + "Client": (".client", "Client"), + "ClientCredentialsOAuthProvider": ( + ".auth", + "ClientCredentialsOAuthProvider", + ), + "ClientTransport": (".transports", "ClientTransport"), + "FastMCPTransport": (".transports", "FastMCPTransport"), + "NodeStdioTransport": (".transports", "NodeStdioTransport"), + "NpxStdioTransport": (".transports", "NpxStdioTransport"), + "OAuth": (".auth", "OAuth"), + "PrivateKeyJWTOAuthProvider": (".auth", "PrivateKeyJWTOAuthProvider"), + "PythonStdioTransport": (".transports", "PythonStdioTransport"), + "SSETransport": (".transports", "SSETransport"), + "StdioTransport": (".transports", "StdioTransport"), + "StreamableHttpTransport": (".transports", "StreamableHttpTransport"), + "UvStdioTransport": (".transports", "UvStdioTransport"), + "UvxStdioTransport": (".transports", "UvxStdioTransport"), +} + + +def __getattr__(name: str) -> object: + try: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + except ImportError as exc: + raise ImportError(_install_hints.CLIENT_SUPPORT) from exc + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/client/auth/__init__.py b/fastmcp_slim/fastmcp/client/auth/__init__.py index e706c7f18..decda464b 100644 --- a/fastmcp_slim/fastmcp/client/auth/__init__.py +++ b/fastmcp_slim/fastmcp/client/auth/__init__.py @@ -1,11 +1,23 @@ -from .bearer import BearerAuth -from .client_credentials import ( - ClientCredentialsOAuthProvider, - PrivateKeyJWTOAuthProvider, - SignedJWTParameters, - static_assertion_provider, +from typing import TYPE_CHECKING + +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, ) -from .oauth import OAuth + +if TYPE_CHECKING: + from .bearer import BearerAuth as BearerAuth + from .client_credentials import ( + ClientCredentialsOAuthProvider as ClientCredentialsOAuthProvider, + ) + from .client_credentials import ( + PrivateKeyJWTOAuthProvider as PrivateKeyJWTOAuthProvider, + ) + from .client_credentials import SignedJWTParameters as SignedJWTParameters + from .client_credentials import ( + static_assertion_provider as static_assertion_provider, + ) + from .oauth import OAuth as OAuth __all__ = [ "BearerAuth", @@ -15,3 +27,29 @@ __all__ = [ "SignedJWTParameters", "static_assertion_provider", ] + +_LAZY_IMPORTS = { + "BearerAuth": (".bearer", "BearerAuth"), + "ClientCredentialsOAuthProvider": ( + ".client_credentials", + "ClientCredentialsOAuthProvider", + ), + "OAuth": (".oauth", "OAuth"), + "PrivateKeyJWTOAuthProvider": ( + ".client_credentials", + "PrivateKeyJWTOAuthProvider", + ), + "SignedJWTParameters": (".client_credentials", "SignedJWTParameters"), + "static_assertion_provider": ( + ".client_credentials", + "static_assertion_provider", + ), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/client/transports/__init__.py b/fastmcp_slim/fastmcp/client/transports/__init__.py index 287697b5c..89881d338 100644 --- a/fastmcp_slim/fastmcp/client/transports/__init__.py +++ b/fastmcp_slim/fastmcp/client/transports/__init__.py @@ -1,25 +1,37 @@ -from mcp.server.mcpserver import MCPServer as SDKServer +from typing import TYPE_CHECKING -from fastmcp.client.transports.base import ( - ClientTransport, - ClientTransportT, - SessionKwargs, -) -from fastmcp.client.transports.config import MCPConfigTransport -from fastmcp.client.transports.http import StreamableHttpTransport -from fastmcp.client.transports.inference import infer_transport -from fastmcp.client.transports.sse import SSETransport -from fastmcp.client.transports.memory import FastMCPTransport -from fastmcp.client.transports.stdio import ( - FastMCPStdioTransport, - NodeStdioTransport, - NpxStdioTransport, - PythonStdioTransport, - StdioTransport, - UvStdioTransport, - UvxStdioTransport, +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, ) +if TYPE_CHECKING: + from mcp.server.mcpserver import MCPServer as SDKServer + + from fastmcp.client.transports.base import ClientTransport as ClientTransport + from fastmcp.client.transports.base import ClientTransportT as ClientTransportT + from fastmcp.client.transports.base import SessionKwargs as SessionKwargs + from fastmcp.client.transports.config import ( + MCPConfigTransport as MCPConfigTransport, + ) + from fastmcp.client.transports.http import ( + StreamableHttpTransport as StreamableHttpTransport, + ) + from fastmcp.client.transports.inference import infer_transport as infer_transport + from fastmcp.client.transports.memory import FastMCPTransport as FastMCPTransport + from fastmcp.client.transports.sse import SSETransport as SSETransport + from fastmcp.client.transports.stdio import ( + FastMCPStdioTransport as FastMCPStdioTransport, + ) + from fastmcp.client.transports.stdio import NodeStdioTransport as NodeStdioTransport + from fastmcp.client.transports.stdio import NpxStdioTransport as NpxStdioTransport + from fastmcp.client.transports.stdio import ( + PythonStdioTransport as PythonStdioTransport, + ) + from fastmcp.client.transports.stdio import StdioTransport as StdioTransport + from fastmcp.client.transports.stdio import UvStdioTransport as UvStdioTransport + from fastmcp.client.transports.stdio import UvxStdioTransport as UvxStdioTransport + __all__ = [ "ClientTransport", "FastMCPStdioTransport", @@ -34,3 +46,48 @@ __all__ = [ "UvxStdioTransport", "infer_transport", ] + +_LAZY_IMPORTS = { + "ClientTransport": ("fastmcp.client.transports.base", "ClientTransport"), + "ClientTransportT": ("fastmcp.client.transports.base", "ClientTransportT"), + "FastMCPStdioTransport": ( + "fastmcp.client.transports.stdio", + "FastMCPStdioTransport", + ), + "FastMCPTransport": ("fastmcp.client.transports.memory", "FastMCPTransport"), + "MCPConfigTransport": ( + "fastmcp.client.transports.config", + "MCPConfigTransport", + ), + "NodeStdioTransport": ( + "fastmcp.client.transports.stdio", + "NodeStdioTransport", + ), + "NpxStdioTransport": ( + "fastmcp.client.transports.stdio", + "NpxStdioTransport", + ), + "PythonStdioTransport": ( + "fastmcp.client.transports.stdio", + "PythonStdioTransport", + ), + "SDKServer": ("mcp.server.mcpserver", "MCPServer"), + "SSETransport": ("fastmcp.client.transports.sse", "SSETransport"), + "SessionKwargs": ("fastmcp.client.transports.base", "SessionKwargs"), + "StdioTransport": ("fastmcp.client.transports.stdio", "StdioTransport"), + "StreamableHttpTransport": ( + "fastmcp.client.transports.http", + "StreamableHttpTransport", + ), + "UvStdioTransport": ("fastmcp.client.transports.stdio", "UvStdioTransport"), + "UvxStdioTransport": ("fastmcp.client.transports.stdio", "UvxStdioTransport"), + "infer_transport": ("fastmcp.client.transports.inference", "infer_transport"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/prompts/__init__.py b/fastmcp_slim/fastmcp/prompts/__init__.py index b94b5952d..091f68dd5 100644 --- a/fastmcp_slim/fastmcp/prompts/__init__.py +++ b/fastmcp_slim/fastmcp/prompts/__init__.py @@ -1,5 +1,18 @@ -from .function_prompt import FunctionPrompt, prompt -from .base import Message, Prompt, PromptArgument, PromptMessage, PromptResult +from typing import TYPE_CHECKING + +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, +) + +if TYPE_CHECKING: + from .base import Message as Message + from .base import Prompt as Prompt + from .base import PromptArgument as PromptArgument + from .base import PromptMessage as PromptMessage + from .base import PromptResult as PromptResult + from .function_prompt import FunctionPrompt as FunctionPrompt + from .function_prompt import prompt as prompt __all__ = [ "FunctionPrompt", @@ -10,3 +23,21 @@ __all__ = [ "PromptResult", "prompt", ] + +_LAZY_IMPORTS = { + "FunctionPrompt": (".function_prompt", "FunctionPrompt"), + "Message": (".base", "Message"), + "Prompt": (".base", "Prompt"), + "PromptArgument": (".base", "PromptArgument"), + "PromptMessage": (".base", "PromptMessage"), + "PromptResult": (".base", "PromptResult"), + "prompt": (".function_prompt", "prompt"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/resources/__init__.py b/fastmcp_slim/fastmcp/resources/__init__.py index b0e5b4524..f31e45693 100644 --- a/fastmcp_slim/fastmcp/resources/__init__.py +++ b/fastmcp_slim/fastmcp/resources/__init__.py @@ -1,15 +1,24 @@ -from .function_resource import FunctionResource, resource -from .base import Resource, ResourceContent, ResourceResult -from .security import ResourceSecurity -from .template import ResourceTemplate -from .types import ( - BinaryResource, - DirectoryResource, - FileResource, - HttpResource, - TextResource, +from typing import TYPE_CHECKING + +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, ) +if TYPE_CHECKING: + from .base import Resource as Resource + from .base import ResourceContent as ResourceContent + from .base import ResourceResult as ResourceResult + from .function_resource import FunctionResource as FunctionResource + from .function_resource import resource as resource + from .security import ResourceSecurity as ResourceSecurity + from .template import ResourceTemplate as ResourceTemplate + from .types import BinaryResource as BinaryResource + from .types import DirectoryResource as DirectoryResource + from .types import FileResource as FileResource + from .types import HttpResource as HttpResource + from .types import TextResource as TextResource + __all__ = [ "BinaryResource", "DirectoryResource", @@ -24,3 +33,26 @@ __all__ = [ "TextResource", "resource", ] + +_LAZY_IMPORTS = { + "BinaryResource": (".types", "BinaryResource"), + "DirectoryResource": (".types", "DirectoryResource"), + "FileResource": (".types", "FileResource"), + "FunctionResource": (".function_resource", "FunctionResource"), + "HttpResource": (".types", "HttpResource"), + "Resource": (".base", "Resource"), + "ResourceContent": (".base", "ResourceContent"), + "ResourceResult": (".base", "ResourceResult"), + "ResourceSecurity": (".security", "ResourceSecurity"), + "ResourceTemplate": (".template", "ResourceTemplate"), + "TextResource": (".types", "TextResource"), + "resource": (".function_resource", "resource"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/server/middleware/__init__.py b/fastmcp_slim/fastmcp/server/middleware/__init__.py index 8df6962bd..292cb7816 100644 --- a/fastmcp_slim/fastmcp/server/middleware/__init__.py +++ b/fastmcp_slim/fastmcp/server/middleware/__init__.py @@ -1,10 +1,16 @@ -from .authorization import AuthMiddleware -from .middleware import ( - CallNext, - Middleware, - MiddlewareContext, +from typing import TYPE_CHECKING + +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, ) -from .ping import PingMiddleware + +if TYPE_CHECKING: + from .authorization import AuthMiddleware as AuthMiddleware + from .middleware import CallNext as CallNext + from .middleware import Middleware as Middleware + from .middleware import MiddlewareContext as MiddlewareContext + from .ping import PingMiddleware as PingMiddleware __all__ = [ "AuthMiddleware", @@ -13,3 +19,19 @@ __all__ = [ "MiddlewareContext", "PingMiddleware", ] + +_LAZY_IMPORTS = { + "AuthMiddleware": (".authorization", "AuthMiddleware"), + "CallNext": (".middleware", "CallNext"), + "Middleware": (".middleware", "Middleware"), + "MiddlewareContext": (".middleware", "MiddlewareContext"), + "PingMiddleware": (".ping", "PingMiddleware"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/server/providers/__init__.py b/fastmcp_slim/fastmcp/server/providers/__init__.py index f138404f2..aecdab1fa 100644 --- a/fastmcp_slim/fastmcp/server/providers/__init__.py +++ b/fastmcp_slim/fastmcp/server/providers/__init__.py @@ -27,20 +27,32 @@ Example: from typing import TYPE_CHECKING -from fastmcp.server.providers.aggregate import AggregateProvider -from fastmcp.server.providers.base import Provider -from fastmcp.server.providers.fastmcp_provider import FastMCPProvider -from fastmcp.server.providers.filesystem import FileSystemProvider -from fastmcp.server.providers.local_provider import LocalProvider -from fastmcp.server.providers.skills import ( - ClaudeSkillsProvider, - SkillProvider, - SkillsDirectoryProvider, +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, ) if TYPE_CHECKING: + from fastmcp.server.providers.aggregate import ( + AggregateProvider as AggregateProvider, + ) + from fastmcp.server.providers.base import Provider as Provider + from fastmcp.server.providers.fastmcp_provider import ( + FastMCPProvider as FastMCPProvider, + ) + from fastmcp.server.providers.filesystem import ( + FileSystemProvider as FileSystemProvider, + ) + from fastmcp.server.providers.local_provider import LocalProvider as LocalProvider from fastmcp.server.providers.openapi import OpenAPIProvider as OpenAPIProvider from fastmcp.server.providers.proxy import ProxyProvider as ProxyProvider + from fastmcp.server.providers.skills import ( + ClaudeSkillsProvider as ClaudeSkillsProvider, + ) + from fastmcp.server.providers.skills import SkillProvider as SkillProvider + from fastmcp.server.providers.skills import ( + SkillsDirectoryProvider as SkillsDirectoryProvider, + ) __all__ = [ "AggregateProvider", @@ -55,15 +67,35 @@ __all__ = [ "SkillsDirectoryProvider", ] +_LAZY_IMPORTS = { + "AggregateProvider": ("fastmcp.server.providers.aggregate", "AggregateProvider"), + "ClaudeSkillsProvider": ( + "fastmcp.server.providers.skills", + "ClaudeSkillsProvider", + ), + "FastMCPProvider": ( + "fastmcp.server.providers.fastmcp_provider", + "FastMCPProvider", + ), + "FileSystemProvider": ( + "fastmcp.server.providers.filesystem", + "FileSystemProvider", + ), + "LocalProvider": ("fastmcp.server.providers.local_provider", "LocalProvider"), + "OpenAPIProvider": ("fastmcp.server.providers.openapi", "OpenAPIProvider"), + "Provider": ("fastmcp.server.providers.base", "Provider"), + "ProxyProvider": ("fastmcp.server.providers.proxy", "ProxyProvider"), + "SkillProvider": ("fastmcp.server.providers.skills", "SkillProvider"), + "SkillsDirectoryProvider": ( + "fastmcp.server.providers.skills", + "SkillsDirectoryProvider", + ), +} + def __getattr__(name: str) -> object: - """Lazy import for providers to avoid circular imports.""" - if name == "ProxyProvider": - from fastmcp.server.providers.proxy import ProxyProvider + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) - return ProxyProvider - if name == "OpenAPIProvider": - from fastmcp.server.providers.openapi import OpenAPIProvider - return OpenAPIProvider - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/server/transforms/__init__.py b/fastmcp_slim/fastmcp/server/transforms/__init__.py index 411a2e0f8..82a6dddb8 100644 --- a/fastmcp_slim/fastmcp/server/transforms/__init__.py +++ b/fastmcp_slim/fastmcp/server/transforms/__init__.py @@ -23,12 +23,31 @@ from __future__ import annotations from collections.abc import Awaitable, Sequence from typing import TYPE_CHECKING, Protocol +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, +) from fastmcp.utilities.versions import VersionSpec if TYPE_CHECKING: from fastmcp.prompts.base import Prompt from fastmcp.resources.base import Resource from fastmcp.resources.template import ResourceTemplate + from fastmcp.server.transforms.namespace import Namespace as Namespace + from fastmcp.server.transforms.prompts_as_tools import ( + PromptsAsTools as PromptsAsTools, + ) + from fastmcp.server.transforms.resources_as_tools import ( + ResourcesAsTools as ResourcesAsTools, + ) + from fastmcp.server.transforms.tool_transform import ( + ToolTransform as ToolTransform, + ) + from fastmcp.server.transforms.version_filter import ( + VersionFilter as VersionFilter, + ) + from fastmcp.server.transforms.visibility import Visibility as Visibility + from fastmcp.server.transforms.visibility import is_enabled as is_enabled from fastmcp.tools.base import Tool @@ -219,14 +238,6 @@ class Transform: return await call_next(name, version=version) -# Re-export built-in transforms (must be after Transform class to avoid circular imports) -from fastmcp.server.transforms.visibility import Visibility, is_enabled # noqa: E402 -from fastmcp.server.transforms.namespace import Namespace # noqa: E402 -from fastmcp.server.transforms.prompts_as_tools import PromptsAsTools # noqa: E402 -from fastmcp.server.transforms.resources_as_tools import ResourcesAsTools # noqa: E402 -from fastmcp.server.transforms.tool_transform import ToolTransform # noqa: E402 -from fastmcp.server.transforms.version_filter import VersionFilter # noqa: E402 - __all__ = [ "Namespace", "PromptsAsTools", @@ -238,3 +249,33 @@ __all__ = [ "Visibility", "is_enabled", ] + +_LAZY_IMPORTS = { + "Namespace": ("fastmcp.server.transforms.namespace", "Namespace"), + "PromptsAsTools": ( + "fastmcp.server.transforms.prompts_as_tools", + "PromptsAsTools", + ), + "ResourcesAsTools": ( + "fastmcp.server.transforms.resources_as_tools", + "ResourcesAsTools", + ), + "ToolTransform": ( + "fastmcp.server.transforms.tool_transform", + "ToolTransform", + ), + "VersionFilter": ( + "fastmcp.server.transforms.version_filter", + "VersionFilter", + ), + "Visibility": ("fastmcp.server.transforms.visibility", "Visibility"), + "is_enabled": ("fastmcp.server.transforms.visibility", "is_enabled"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/tools/__init__.py b/fastmcp_slim/fastmcp/tools/__init__.py index d3f7303fc..956446d0f 100644 --- a/fastmcp_slim/fastmcp/tools/__init__.py +++ b/fastmcp_slim/fastmcp/tools/__init__.py @@ -1,6 +1,18 @@ -from .function_tool import FunctionTool, tool -from .base import InputRequiredToolResult, Tool, ToolResult -from .tool_transform import forward, forward_raw +from typing import TYPE_CHECKING + +from fastmcp.utilities.lazy_imports import ( + list_module_attributes, + resolve_lazy_import, +) + +if TYPE_CHECKING: + from .base import InputRequiredToolResult as InputRequiredToolResult + from .base import Tool as Tool + from .base import ToolResult as ToolResult + from .function_tool import FunctionTool as FunctionTool + from .function_tool import tool as tool + from .tool_transform import forward as forward + from .tool_transform import forward_raw as forward_raw __all__ = [ "FunctionTool", @@ -11,3 +23,21 @@ __all__ = [ "forward_raw", "tool", ] + +_LAZY_IMPORTS = { + "FunctionTool": (".function_tool", "FunctionTool"), + "InputRequiredToolResult": (".base", "InputRequiredToolResult"), + "Tool": (".base", "Tool"), + "ToolResult": (".base", "ToolResult"), + "forward": (".tool_transform", "forward"), + "forward_raw": (".tool_transform", "forward_raw"), + "tool": (".function_tool", "tool"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_import(name, __name__, globals(), _LAZY_IMPORTS) + + +def __dir__() -> list[str]: + return list_module_attributes(globals(), _LAZY_IMPORTS) diff --git a/fastmcp_slim/fastmcp/utilities/lazy_imports.py b/fastmcp_slim/fastmcp/utilities/lazy_imports.py new file mode 100644 index 000000000..3e7c23734 --- /dev/null +++ b/fastmcp_slim/fastmcp/utilities/lazy_imports.py @@ -0,0 +1,31 @@ +"""Helpers for exposing public names without eagerly importing their modules.""" + +from collections.abc import Mapping +from importlib import import_module +from typing import Any + +LazyImports = Mapping[str, tuple[str, str]] + + +def resolve_lazy_import( + name: str, + package: str, + namespace: dict[str, Any], + lazy_imports: LazyImports, +) -> object: + """Resolve and cache a lazily exported module attribute.""" + try: + module_name, attr_name = lazy_imports[name] + except KeyError: + raise AttributeError(f"module {package!r} has no attribute {name!r}") from None + + value = getattr(import_module(module_name, package), attr_name) + namespace[name] = value + return value + + +def list_module_attributes( + namespace: dict[str, Any], lazy_imports: LazyImports +) -> list[str]: + """Include unresolved lazy exports in module introspection.""" + return sorted(namespace.keys() | lazy_imports.keys()) diff --git a/tests/test_lazy_package_imports.py b/tests/test_lazy_package_imports.py new file mode 100644 index 000000000..645f72ac1 --- /dev/null +++ b/tests/test_lazy_package_imports.py @@ -0,0 +1,142 @@ +"""Fresh-interpreter guards for lazy public package exports.""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap + +import pytest + + +@pytest.mark.parametrize( + ("statement", "excluded_modules"), + [ + ( + "from fastmcp.client import BearerAuth", + ( + "fastmcp.client.auth.client_credentials", + "fastmcp.client.auth.oauth", + "fastmcp.client.client", + "fastmcp.client.transports", + ), + ), + ( + "from fastmcp.client.transports import ClientTransport", + ( + "fastmcp.client.transports.config", + "fastmcp.client.transports.http", + "fastmcp.client.transports.inference", + "fastmcp.client.transports.memory", + "fastmcp.client.transports.sse", + "fastmcp.client.transports.stdio", + ), + ), + ( + "from fastmcp.tools import Tool", + ("fastmcp.tools.function_tool", "fastmcp.tools.tool_transform"), + ), + ( + "from fastmcp.resources import Resource", + ( + "fastmcp.resources.function_resource", + "fastmcp.resources.security", + "fastmcp.resources.template", + "fastmcp.resources.types", + ), + ), + ( + "from fastmcp.prompts import Prompt", + ("fastmcp.prompts.function_prompt",), + ), + ( + "from fastmcp.server.providers import Provider", + ( + "fastmcp.server.providers.aggregate", + "fastmcp.server.providers.fastmcp_provider", + "fastmcp.server.providers.filesystem", + "fastmcp.server.providers.local_provider", + "fastmcp.server.providers.skills", + ), + ), + ( + "from fastmcp.server.middleware import Middleware", + ( + "fastmcp.server.middleware.authorization", + "fastmcp.server.middleware.ping", + ), + ), + ( + "from fastmcp.server.transforms import Transform", + ( + "fastmcp.server.transforms.namespace", + "fastmcp.server.transforms.prompts_as_tools", + "fastmcp.server.transforms.resources_as_tools", + "fastmcp.server.transforms.tool_transform", + "fastmcp.server.transforms.version_filter", + "fastmcp.server.transforms.visibility", + ), + ), + ], +) +@pytest.mark.subprocess_heavy +def test_narrow_import_does_not_load_sibling_implementations( + statement: str, excluded_modules: tuple[str, ...] +) -> None: + script = textwrap.dedent( + f""" + import sys + + {statement} + + excluded = {excluded_modules!r} + loaded = [ + name + for name in sys.modules + if any(name == root or name.startswith(f"{{root}}.") for root in excluded) + ] + assert not loaded, loaded + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + + +@pytest.mark.subprocess_heavy +def test_all_public_package_exports_resolve() -> None: + script = textwrap.dedent( + """ + import importlib + + packages = ( + "fastmcp.client", + "fastmcp.client.auth", + "fastmcp.client.transports", + "fastmcp.prompts", + "fastmcp.resources", + "fastmcp.server.middleware", + "fastmcp.server.providers", + "fastmcp.server.transforms", + "fastmcp.tools", + ) + for package_name in packages: + package = importlib.import_module(package_name) + assert set(package.__all__) <= set(dir(package)) + for export in package.__all__: + assert getattr(package, export) is not None, (package_name, export) + """ + ) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr