diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 108e6c6676..72e4c253d4 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -62,6 +62,10 @@ jobs: uv run --frozen --no-sync coverage combine uv run --frozen --no-sync coverage report + - name: Check for unnecessary no cover pragmas + if: runner.os != 'Windows' + run: uv run --frozen --no-sync strict-no-cover + readme-snippets: runs-on: ubuntu-latest steps: diff --git a/pyproject.toml b/pyproject.toml index 1b83cf1229..f3f80e4e91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,8 +66,9 @@ dev = [ "pytest-pretty>=1.2.0", "inline-snapshot>=0.23.0", "dirty-equals>=0.9.0", - "coverage[toml]>=7.13.1", + "coverage[toml]>=7.10.7,<=7.13", "pillow>=12.0", + "strict-no-cover", ] docs = [ "mkdocs>=1.6.1", @@ -163,6 +164,7 @@ members = ["examples/clients/*", "examples/servers/*", "examples/snippets"] [tool.uv.sources] mcp = { workspace = true } +strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" } [tool.pytest.ini_options] log_cli = true @@ -198,7 +200,6 @@ branch = true patch = ["subprocess"] concurrency = ["multiprocessing", "thread"] source = ["src", "tests"] -relative_files = true omit = [ "src/mcp/client/__main__.py", "src/mcp/server/__main__.py", @@ -215,6 +216,7 @@ ignore_errors = true precision = 2 exclude_lines = [ "pragma: no cover", + "pragma: lax no cover", "if TYPE_CHECKING:", "@overload", "raise NotImplementedError", diff --git a/src/mcp/cli/claude.py b/src/mcp/cli/claude.py index f2dc6888a1..7c2f650edd 100644 --- a/src/mcp/cli/claude.py +++ b/src/mcp/cli/claude.py @@ -72,7 +72,7 @@ def update_claude_config( ) config_file = config_dir / "claude_desktop_config.json" - if not config_file.exists(): # pragma: no cover + if not config_file.exists(): # pragma: lax no cover try: config_file.write_text("{}") except Exception: diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index c4cae0dce3..5e1e641ac4 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -77,7 +77,7 @@ def _build_uv_command( if with_packages: for pkg in with_packages: - if pkg: # pragma: no cover + if pkg: # pragma: no branch cmd.extend(["--with", pkg]) # Add mcp run command diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index 8e699b57a0..98df4d25d0 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -192,7 +192,7 @@ def prepare_token_auth( headers = {} # pragma: no cover if not self.client_info: - return data, headers # pragma: no cover + return data, headers auth_method = self.client_info.token_endpoint_auth_method @@ -418,7 +418,7 @@ async def _refresh_token(self) -> httpx.Request: raise OAuthTokenError("No client info available") # pragma: no cover if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint: - token_url = str(self.context.oauth_metadata.token_endpoint) # pragma: no cover + token_url = str(self.context.oauth_metadata.token_endpoint) else: auth_base_url = self.context.get_authorization_base_url(self.context.server_url) token_url = urljoin(auth_base_url, "/token") @@ -534,7 +534,7 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx. ) # Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers) - for url in asm_discovery_urls: # pragma: no cover + for url in asm_discovery_urls: # pragma: no branch oauth_metadata_request = create_oauth_metadata_request(url) oauth_metadata_response = yield oauth_metadata_request diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index d5d4c86077..7098132b03 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -250,7 +250,7 @@ async def set_logging_level( meta: RequestParamsMeta | None = None, ) -> types.EmptyResult: """Send a logging/setLevel request.""" - return await self.send_request( # pragma: no cover + return await self.send_request( types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)), types.EmptyResult, ) @@ -285,14 +285,14 @@ async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult: """Send a resources/subscribe request.""" - return await self.send_request( # pragma: no cover + return await self.send_request( types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)), types.EmptyResult, ) async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult: """Send a resources/unsubscribe request.""" - return await self.send_request( # pragma: no cover + return await self.send_request( types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)), types.EmptyResult, ) @@ -344,7 +344,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) - try: validate(result.structured_content, output_schema) except ValidationError as e: - raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover + raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") except SchemaError as e: # pragma: no cover raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index 3ae8fa1cbc..32395829f2 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -223,22 +223,22 @@ async def disconnect_from_server(self, session: mcp.ClientSession) -> None: ) ) - if session_known_for_components: # pragma: no cover + if session_known_for_components: # pragma: no branch component_names = self._sessions.pop(session) # Pop from _sessions tracking # Remove prompts associated with the session. for name in component_names.prompts: - if name in self._prompts: + if name in self._prompts: # pragma: no branch del self._prompts[name] # Remove resources associated with the session. for name in component_names.resources: - if name in self._resources: + if name in self._resources: # pragma: no branch del self._resources[name] # Remove tools associated with the session. for name in component_names.tools: - if name in self._tools: + if name in self._tools: # pragma: no branch del self._tools[name] - if name in self._tool_to_session: + if name in self._tool_to_session: # pragma: no branch del self._tool_to_session[name] # Clean up the session's resources via its dedicated exit stack diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 47e5b845a9..208427c436 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -119,12 +119,12 @@ async def sse_reader(task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED): await read_stream_writer.send(session_message) case _: # pragma: no cover logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover - except SSEError as sse_exc: # pragma: no cover - logger.exception("Encountered SSE exception") # pragma: no cover - raise sse_exc # pragma: no cover - except Exception as exc: # pragma: no cover - logger.exception("Error in sse_reader") # pragma: no cover - await read_stream_writer.send(exc) # pragma: no cover + except SSEError as sse_exc: # pragma: lax no cover + logger.exception("Encountered SSE exception") + raise sse_exc + except Exception as exc: # pragma: lax no cover + logger.exception("Error in sse_reader") + await read_stream_writer.send(exc) finally: await read_stream_writer.aclose() @@ -143,8 +143,8 @@ async def post_writer(endpoint_url: str): ) response.raise_for_status() logger.debug(f"Client message sent successfully: {response.status_code}") - except Exception: # pragma: no cover - logger.exception("Error in post_writer") # pragma: no cover + except Exception: # pragma: lax no cover + logger.exception("Error in post_writer") finally: await write_stream.aclose() diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index 19fdec5a38..2e52b4066b 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -56,8 +56,8 @@ def get_default_environment() -> dict[str, str]: for key in DEFAULT_INHERITED_ENV_VARS: value = os.environ.get(key) - if value is None: - continue # pragma: no cover + if value is None: # pragma: lax no cover + continue if value.startswith("()"): # pragma: no cover # Skip functions, which are a security risk @@ -158,7 +158,7 @@ async def stdout_reader(): session_message = SessionMessage(message) await read_stream_writer.send(session_message) - except anyio.ClosedResourceError: # pragma: no cover + except anyio.ClosedResourceError: # pragma: lax no cover await anyio.lowlevel.checkpoint() async def stdin_writer(): @@ -225,8 +225,8 @@ def _get_executable_command(command: str) -> str: """ if sys.platform == "win32": # pragma: no cover return get_windows_executable_command(command) - else: - return command # pragma: no cover + else: # pragma: lax no cover + return command async def _create_platform_compatible_process( @@ -243,14 +243,14 @@ async def _create_platform_compatible_process( """ if sys.platform == "win32": # pragma: no cover process = await create_windows_process(command, args, env, errlog, cwd) - else: + else: # pragma: lax no cover process = await anyio.open_process( [command, *args], env=env, stderr=errlog, cwd=cwd, start_new_session=True, - ) # pragma: no cover + ) return process @@ -267,7 +267,7 @@ async def _terminate_process_tree(process: Process | FallbackProcess, timeout_se """ if sys.platform == "win32": # pragma: no cover await terminate_windows_process_tree(process, timeout_seconds) - else: # pragma: no cover + else: # pragma: lax no cover # FallbackProcess should only be used for Windows compatibility assert isinstance(process, Process) await terminate_posix_process_tree(process, timeout_seconds) diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 555dd1290c..f6d164574e 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -181,7 +181,7 @@ async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer: headers = self._prepare_headers() if last_event_id: - headers[LAST_EVENT_ID] = last_event_id # pragma: no cover + headers[LAST_EVENT_ID] = last_event_id async with aconnect_sse(client, "GET", self.url, headers=headers) as event_source: event_source.response.raise_for_status() @@ -190,17 +190,17 @@ async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer: async for sse in event_source.aiter_sse(): # Track last event ID for reconnection if sse.id: - last_event_id = sse.id # pragma: no cover + last_event_id = sse.id # Track retry interval from server if sse.retry is not None: - retry_interval_ms = sse.retry # pragma: no cover + retry_interval_ms = sse.retry await self._handle_sse_event(sse, read_stream_writer) # Stream ended normally (server closed) - reset attempt counter attempt = 0 - except Exception as exc: # pragma: no cover + except Exception as exc: # pragma: lax no cover logger.debug(f"GET stream error: {exc}") attempt += 1 @@ -333,8 +333,8 @@ async def _handle_sse_response( if is_complete: await response.aclose() return # Normal completion, no reconnect needed - except Exception as e: # pragma: no cover - logger.debug(f"SSE stream ended: {e}") + except Exception as e: + logger.debug(f"SSE stream ended: {e}") # pragma: no cover # Stream ended without response - reconnect if we received an event with ID if last_event_id is not None: # pragma: no branch @@ -472,20 +472,20 @@ async def handle_request_async(): await read_stream_writer.aclose() await write_stream.aclose() - async def terminate_session(self, client: httpx.AsyncClient) -> None: # pragma: no cover + async def terminate_session(self, client: httpx.AsyncClient) -> None: """Terminate the session by sending a DELETE request.""" - if not self.session_id: + if not self.session_id: # pragma: lax no cover return try: headers = self._prepare_headers() response = await client.delete(self.url, headers=headers) - if response.status_code == 405: + if response.status_code == 405: # pragma: lax no cover logger.debug("Server does not allow session termination") - elif response.status_code not in (200, 204): + elif response.status_code not in (200, 204): # pragma: lax no cover logger.warning(f"Session termination failed: {response.status_code}") - except Exception as exc: + except Exception as exc: # pragma: no cover logger.warning(f"Session termination failed: {exc}") def get_session_id(self) -> str | None: diff --git a/src/mcp/server/auth/handlers/token.py b/src/mcp/server/auth/handlers/token.py index 14f6f68720..534a478a91 100644 --- a/src/mcp/server/auth/handlers/token.py +++ b/src/mcp/server/auth/handlers/token.py @@ -178,7 +178,7 @@ async def handle(self, request: Request): except TokenError as e: return self.response(TokenErrorResponse(error=e.error, error_description=e.error_description)) - case RefreshTokenRequest(): # pragma: no cover + case RefreshTokenRequest(): # pragma: no branch refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token) if refresh_token is None or refresh_token.client_id != token_request.client_id: # if token belongs to different client, pretend it doesn't exist diff --git a/src/mcp/server/auth/middleware/client_auth.py b/src/mcp/server/auth/middleware/client_auth.py index 4e4d9be2f6..8a6a1b5188 100644 --- a/src/mcp/server/auth/middleware/client_auth.py +++ b/src/mcp/server/auth/middleware/client_auth.py @@ -13,7 +13,7 @@ class AuthenticationError(Exception): def __init__(self, message: str): - self.message = message # pragma: no cover + self.message = message class ClientAuthenticator: @@ -96,15 +96,15 @@ async def authenticate_request(self, request: Request) -> OAuthClientInformation # If client from the store expects a secret, validate that the request provides # that secret - if client.client_secret: # pragma: no branch + if client.client_secret: if not request_client_secret: - raise AuthenticationError("Client secret is required") # pragma: no cover + raise AuthenticationError("Client secret is required") # hmac.compare_digest requires that both arguments are either bytes or a `str` containing # only ASCII characters. Since we do not control `request_client_secret`, we encode both # arguments to bytes. if not hmac.compare_digest(client.client_secret.encode(), request_client_secret.encode()): - raise AuthenticationError("Invalid client_secret") # pragma: no cover + raise AuthenticationError("Invalid client_secret") if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()): raise AuthenticationError("Client secret has expired") # pragma: no cover diff --git a/src/mcp/server/experimental/task_context.py b/src/mcp/server/experimental/task_context.py index 871cefd9f5..32394d0ad0 100644 --- a/src/mcp/server/experimental/task_context.py +++ b/src/mcp/server/experimental/task_context.py @@ -247,8 +247,7 @@ async def elicit( response_data = await resolver.wait() await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) return ElicitResult.model_validate(response_data) - except anyio.get_cancelled_exc_class(): # pragma: no cover - # Coverage can't track async exception handlers reliably. + except anyio.get_cancelled_exc_class(): # This path is tested in test_elicit_restores_status_on_cancellation # which verifies status is restored to "working" after cancellation. await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) @@ -408,8 +407,7 @@ async def create_message( response_data = await resolver.wait() await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) return CreateMessageResult.model_validate(response_data) - except anyio.get_cancelled_exc_class(): # pragma: no cover - # Coverage can't track async exception handlers reliably. + except anyio.get_cancelled_exc_class(): # This path is tested in test_create_message_restores_status_on_cancellation # which verifies status is restored to "working" after cancellation. await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index 791442f87e..683886fd8b 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -124,7 +124,7 @@ class FileResource(Resource): @pydantic.field_validator("path") @classmethod - def validate_absolute_path(cls, path: Path) -> Path: # pragma: no cover + def validate_absolute_path(cls, path: Path) -> Path: """Ensure path is absolute.""" if not path.is_absolute(): raise ValueError("Path must be absolute") diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index d0a550280e..71cd81eb27 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -873,10 +873,10 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send): # pragma: no app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url), ) ) - else: # pragma: no cover + else: # Auth is disabled, no need for RequireAuthMiddleware # Since handle_sse is an ASGI app, we need to create a compatible endpoint - async def sse_endpoint(request: Request) -> Response: + async def sse_endpoint(request: Request) -> Response: # pragma: no cover # Convert the Starlette request to ASGI parameters return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage] diff --git a/src/mcp/server/fastmcp/tools/base.py b/src/mcp/server/fastmcp/tools/base.py index b784d0f53d..e6a64f4d96 100644 --- a/src/mcp/server/fastmcp/tools/base.py +++ b/src/mcp/server/fastmcp/tools/base.py @@ -118,7 +118,7 @@ async def run( def _is_async_callable(obj: Any) -> bool: - while isinstance(obj, functools.partial): # pragma: no cover + while isinstance(obj, functools.partial): # pragma: lax no cover obj = obj.func return inspect.iscoroutinefunction(obj) or ( diff --git a/src/mcp/server/fastmcp/utilities/context_injection.py b/src/mcp/server/fastmcp/utilities/context_injection.py index f1aeda39ec..80923e873f 100644 --- a/src/mcp/server/fastmcp/utilities/context_injection.py +++ b/src/mcp/server/fastmcp/utilities/context_injection.py @@ -25,8 +25,7 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None: # Get type hints to properly resolve string annotations try: hints = typing.get_type_hints(fn) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - except Exception: # pragma: no cover + except Exception: # pragma: lax no cover # If we can't resolve type hints, we can't find the context parameter return None diff --git a/src/mcp/server/fastmcp/utilities/logging.py b/src/mcp/server/fastmcp/utilities/logging.py index 2da0cab32c..43c4d9ba77 100644 --- a/src/mcp/server/fastmcp/utilities/logging.py +++ b/src/mcp/server/fastmcp/utilities/logging.py @@ -25,7 +25,7 @@ def configure_logging( level: the log level to use """ handlers: list[logging.Handler] = [] - try: # pragma: no cover + try: from rich.console import Console from rich.logging import RichHandler diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 6bea4126ff..9137d4eafe 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -233,7 +233,7 @@ def get_capabilities( tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed) # Set logging capabilities if handler exists - if types.SetLevelRequest in self.request_handlers: # pragma: no cover + if types.SetLevelRequest in self.request_handlers: logging_capability = types.LoggingCapability() # Set completions capabilities if handler exists @@ -379,7 +379,7 @@ def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any mime_type=mime_type or "text/plain", **meta_kwargs, ) - case bytes() as data: # pragma: no cover + case bytes() as data: # pragma: no branch return types.BlobResourceContents( uri=req.params.uri, blob=base64.b64encode(data).decode(), @@ -388,7 +388,7 @@ def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any ) match result: - case str() | bytes() as data: # pragma: no cover + case str() | bytes() as data: # pragma: lax no cover warnings.warn( "Returning str or bytes from read_resource is deprecated. " "Use Iterable[ReadResourceContents] instead.", @@ -416,7 +416,7 @@ def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any return decorator - def set_logging_level(self): # pragma: no cover + def set_logging_level(self): def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]): logger.debug("Registering handler for SetLevelRequest") @@ -429,7 +429,7 @@ async def handler(req: types.SetLevelRequest): return decorator - def subscribe_resource(self): # pragma: no cover + def subscribe_resource(self): def decorator(func: Callable[[str], Awaitable[None]]): logger.debug("Registering handler for SubscribeRequest") @@ -442,7 +442,7 @@ async def handler(req: types.SubscribeRequest): return decorator - def unsubscribe_resource(self): # pragma: no cover + def unsubscribe_resource(self): def decorator(func: Callable[[str], Awaitable[None]]): logger.debug("Registering handler for UnsubscribeRequest") @@ -468,7 +468,7 @@ async def handler(req: types.ListToolsRequest): result = await wrapper(req) # Handle both old style (list[Tool]) and new style (ListToolsResult) - if isinstance(result, types.ListToolsResult): # pragma: no cover + if isinstance(result, types.ListToolsResult): # Refresh the tool cache with returned tools for tool in result.tools: validate_and_warn_tool_name(tool.name) @@ -571,7 +571,7 @@ async def handler(req: types.CallToolRequest): # tool returned structured content only maybe_structured_content = cast(StructuredContent, results) unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))] - elif hasattr(results, "__iter__"): # pragma: no cover + elif hasattr(results, "__iter__"): # tool returned unstructured content only unstructured_content = cast(UnstructuredContent, results) maybe_structured_content = None @@ -714,7 +714,7 @@ async def _handle_message( await self._handle_request( message, responder.request, session, lifespan_context, raise_exceptions ) - case Exception(): # pragma: no cover + case Exception(): logger.error(f"Received exception from stream: {message}") await session.send_log_message( level="error", @@ -726,7 +726,7 @@ async def _handle_message( case _: await self._handle_notification(message) - for warning in w: # pragma: no cover + for warning in w: # pragma: lax no cover logger.info("Warning: %s: %s", warning.category.__name__, warning.message) async def _handle_request( @@ -781,16 +781,16 @@ async def _handle_request( ) ) response = await handler(req) - except McpError as err: # pragma: no cover + except McpError as err: response = err.error - except anyio.get_cancelled_exc_class(): # pragma: no cover + except anyio.get_cancelled_exc_class(): logger.info( "Request %s cancelled - duplicate response suppressed", message.request_id, ) return - except Exception as err: # pragma: no cover - if raise_exceptions: + except Exception as err: + if raise_exceptions: # pragma: no cover raise err response = types.ErrorData(code=0, message=str(err), data=None) finally: diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 5a70ee02e0..50a441d693 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -114,7 +114,7 @@ def _receive_notification_adapter(self) -> TypeAdapter[types.ClientNotification] @property def client_params(self) -> types.InitializeRequestParams | None: - return self._client_params # pragma: no cover + return self._client_params @property def experimental(self) -> ExperimentalServerSessionFeatures: @@ -126,20 +126,20 @@ def experimental(self) -> ExperimentalServerSessionFeatures: self._experimental_features = ExperimentalServerSessionFeatures(self) return self._experimental_features - def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover + def check_client_capability(self, capability: types.ClientCapabilities) -> bool: """Check if the client supports a specific capability.""" - if self._client_params is None: + if self._client_params is None: # pragma: lax no cover return False client_caps = self._client_params.capabilities - if capability.roots is not None: + if capability.roots is not None: # pragma: lax no cover if client_caps.roots is None: return False if capability.roots.list_changed and not client_caps.roots.list_changed: return False - if capability.sampling is not None: + if capability.sampling is not None: # pragma: lax no cover if client_caps.sampling is None: return False if capability.sampling.context is not None and client_caps.sampling.context is None: @@ -147,17 +147,17 @@ def check_client_capability(self, capability: types.ClientCapabilities) -> bool: if capability.sampling.tools is not None and client_caps.sampling.tools is None: return False - if capability.elicitation is not None and client_caps.elicitation is None: + if capability.elicitation is not None and client_caps.elicitation is None: # pragma: lax no cover return False - if capability.experimental is not None: + if capability.experimental is not None: # pragma: lax no cover if client_caps.experimental is None: return False for exp_key, exp_value in capability.experimental.items(): if exp_key not in client_caps.experimental or client_caps.experimental[exp_key] != exp_value: return False - if capability.tasks is not None: + if capability.tasks is not None: # pragma: lax no cover if client_caps.tasks is None: return False if not check_tasks_capability(capability.tasks, client_caps.tasks): @@ -544,7 +544,7 @@ def _build_elicit_form_request( # Add related-task metadata if associated with a parent task if related_task_id is not None: # Defensive: model_dump() never includes _meta, but guard against future changes - if "_meta" not in params_data: # pragma: no cover + if "_meta" not in params_data: # pragma: no branch params_data["_meta"] = {} params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( task_id=related_task_id @@ -589,7 +589,7 @@ def _build_elicit_url_request( # Add related-task metadata if associated with a parent task if related_task_id is not None: # Defensive: model_dump() never includes _meta, but guard against future changes - if "_meta" not in params_data: # pragma: no cover + if "_meta" not in params_data: # pragma: no branch params_data["_meta"] = {} params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( task_id=related_task_id @@ -659,7 +659,7 @@ def _build_create_message_request( # Add related-task metadata if associated with a parent task if related_task_id is not None: # Defensive: model_dump() never includes _meta, but guard against future changes - if "_meta" not in params_data: # pragma: no cover + if "_meta" not in params_data: # pragma: no branch params_data["_meta"] = {} params_data["_meta"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata( task_id=related_task_id diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index b37a85746d..e9156f7ba8 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -222,7 +222,7 @@ def close_standalone_sse_stream(self) -> None: # pragma: no cover """ self.close_sse_stream(GET_STREAM_KEY) - def _create_session_message( # pragma: no cover + def _create_session_message( self, message: JSONRPCMessage, request: Request, @@ -238,10 +238,10 @@ def _create_session_message( # pragma: no cover # Only provide close callbacks when client supports resumability if self._event_store and protocol_version >= "2025-11-25": - async def close_stream_callback() -> None: + async def close_stream_callback() -> None: # pragma: no cover self.close_sse_stream(request_id) - async def close_standalone_stream_callback() -> None: + async def close_standalone_stream_callback() -> None: # pragma: no cover self.close_standalone_sse_stream() metadata = ServerMessageMetadata( @@ -308,7 +308,7 @@ def _create_error_response( headers=response_headers, ) - def _create_json_response( # pragma: no cover + def _create_json_response( self, response_message: JSONRPCMessage | None, status_code: HTTPStatus = HTTPStatus.OK, @@ -316,10 +316,10 @@ def _create_json_response( # pragma: no cover ) -> Response: """Create a JSON response from a JSONRPCMessage""" response_headers = {"Content-Type": CONTENT_TYPE_JSON} - if headers: + if headers: # pragma: lax no cover response_headers.update(headers) - if self.mcp_session_id: + if self.mcp_session_id: # pragma: lax no cover response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id return Response( @@ -345,14 +345,14 @@ def _create_event_data(self, event_message: EventMessage) -> dict[str, str]: # return event_data - async def _clean_up_memory_streams(self, request_id: RequestId) -> None: # pragma: no cover + async def _clean_up_memory_streams(self, request_id: RequestId) -> None: """Clean up memory streams for a given request ID.""" - if request_id in self._request_streams: + if request_id in self._request_streams: # pragma: no branch try: # Close the request stream await self._request_streams[request_id][0].aclose() await self._request_streams[request_id][1].aclose() - except Exception: + except Exception: # pragma: no cover # During cleanup, we catch all exceptions since streams might be in various states logger.debug("Error closing memory streams - may already be closed") finally: @@ -366,7 +366,7 @@ async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> No # Validate request headers for DNS rebinding protection is_post = request.method == "POST" error_response = await self._security.validate_request(request, is_post=is_post) - if error_response: # pragma: no cover + if error_response: await error_response(scope, receive, send) return @@ -405,12 +405,12 @@ def _check_content_type(self, request: Request) -> bool: return any(part == CONTENT_TYPE_JSON for part in content_type_parts) - async def _validate_accept_header(self, request: Request, scope: Scope, send: Send) -> bool: # pragma: no cover + async def _validate_accept_header(self, request: Request, scope: Scope, send: Send) -> bool: """Validate Accept header based on response mode. Returns True if valid.""" has_json, has_sse = self._check_accept_headers(request) if self.is_json_response_enabled: # For JSON-only responses, only require application/json - if not has_json: + if not has_json: # pragma: lax no cover response = self._create_error_response( "Not Acceptable: Client must accept application/json", HTTPStatus.NOT_ACCEPTABLE, @@ -456,7 +456,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re await response(scope, receive, send) return - try: # pragma: no cover + try: message = jsonrpc_message_adapter.validate_python(raw_message, by_name=False) except ValidationError as e: # pragma: no cover response = self._create_error_response( @@ -513,12 +513,12 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re ) # Extract the request ID outside the try block for proper scope - request_id = str(message.id) # pragma: no cover + request_id = str(message.id) # Register this stream for the request ID - self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0) # pragma: no cover - request_stream_reader = self._request_streams[request_id][1] # pragma: no cover + self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0) + request_stream_reader = self._request_streams[request_id][1] - if self.is_json_response_enabled: # pragma: no cover + if self.is_json_response_enabled: # Process the message metadata = ServerMessageMetadata(request_context=request) session_message = SessionMessage(message, metadata=metadata) @@ -529,13 +529,13 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re response_message = None # Use similar approach to SSE writer for consistency - async for event_message in request_stream_reader: + async for event_message in request_stream_reader: # pragma: no branch # If it's a response, this is what we're waiting for if isinstance(event_message.message, JSONRPCResponse | JSONRPCError): response_message = event_message.message break # For notifications and request, keep waiting - else: + else: # pragma: no cover logger.debug(f"received: {event_message.message.method}") # At this point we should have a response @@ -543,7 +543,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re # Create JSON response response = self._create_json_response(response_message) await response(scope, receive, send) - else: + else: # pragma: no cover # This shouldn't happen in normal operation logger.error("No response message received before stream closed") response = self._create_error_response( @@ -551,7 +551,7 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re HTTPStatus.INTERNAL_SERVER_ERROR, ) await response(scope, receive, send) - except Exception: + except Exception: # pragma: no cover logger.exception("Error processing JSON response") response = self._create_error_response( "Error processing request", @@ -762,7 +762,7 @@ async def terminate(self) -> None: request_stream_keys = list(self._request_streams.keys()) # Close all request streams asynchronously - for key in request_stream_keys: # pragma: no cover + for key in request_stream_keys: # pragma: lax no cover await self._clean_up_memory_streams(key) # Clear the request streams dictionary immediately @@ -967,9 +967,9 @@ async def connect( # Start a task group for message routing async with anyio.create_task_group() as tg: # Create a message router that distributes messages to request streams - async def message_router(): # pragma: no cover + async def message_router(): try: - async for session_message in write_stream_reader: + async for session_message in write_stream_reader: # pragma: no branch # Determine which request stream(s) should receive this message message = session_message.message target_request_id = None @@ -980,7 +980,7 @@ async def message_router(): # pragma: no cover # send it there target_request_id = response_id # Extract related_request_id from meta if it exists - elif ( + elif ( # pragma: no cover session_message.metadata is not None and isinstance( session_message.metadata, @@ -996,7 +996,7 @@ async def message_router(): # pragma: no cover # regardless of whether a client is connected # messages will be replayed on the re-connect event_id = None - if self._event_store: + if self._event_store: # pragma: lax no cover event_id = await self._event_store.store_event(request_stream_id, message) logger.debug(f"Stored {event_id} from {request_stream_id}") @@ -1004,13 +1004,13 @@ async def message_router(): # pragma: no cover try: # Send both the message and the event ID await self._request_streams[request_stream_id][0].send(EventMessage(message, event_id)) - except ( + except ( # pragma: no cover anyio.BrokenResourceError, anyio.ClosedResourceError, ): # Stream might be closed, remove from registry self._request_streams.pop(request_stream_id, None) - else: + else: # pragma: no cover logger.debug( f"""Request stream {request_stream_id} not found for message. Still processing message as the client @@ -1021,7 +1021,7 @@ async def message_router(): # pragma: no cover logger.debug("Read stream closed by client") else: logger.exception("Unexpected closure of read stream in message router") - except Exception: + except Exception: # pragma: lax no cover logger.exception("Error in message router") # Start the message router @@ -1031,7 +1031,7 @@ async def message_router(): # pragma: no cover # Yield the streams for the caller to use yield read_stream, write_stream finally: - for stream_id in list(self._request_streams.keys()): # pragma: no cover + for stream_id in list(self._request_streams.keys()): # pragma: lax no cover await self._clean_up_memory_streams(stream_id) self._request_streams.clear() diff --git a/src/mcp/server/transport_security.py b/src/mcp/server/transport_security.py index ee1e4505a7..c8c0499013 100644 --- a/src/mcp/server/transport_security.py +++ b/src/mcp/server/transport_security.py @@ -86,9 +86,9 @@ def _validate_origin(self, origin: str | None) -> bool: # pragma: no cover logger.warning(f"Invalid Origin header: {origin}") return False - def _validate_content_type(self, content_type: str | None) -> bool: # pragma: no cover + def _validate_content_type(self, content_type: str | None) -> bool: """Validate the Content-Type header for POST requests.""" - if not content_type: + if not content_type: # pragma: lax no cover logger.warning("Missing Content-Type header in POST request") return False @@ -107,7 +107,7 @@ async def validate_request(self, request: Request, is_post: bool = False) -> Res # Always validate Content-Type for POST requests if is_post: # pragma: no branch content_type = request.headers.get("content-type") - if not self._validate_content_type(content_type): # pragma: no cover + if not self._validate_content_type(content_type): return Response("Invalid Content-Type header", status_code=400) # Skip remaining validation if DNS rebinding protection is disabled diff --git a/src/mcp/shared/session.py b/src/mcp/shared/session.py index 341e1fac03..b7d68c15e8 100644 --- a/src/mcp/shared/session.py +++ b/src/mcp/shared/session.py @@ -150,7 +150,7 @@ def in_flight(self) -> bool: # pragma: no cover return not self._completed and not self.cancelled @property - def cancelled(self) -> bool: # pragma: no cover + def cancelled(self) -> bool: return self._cancel_scope.cancel_called @@ -250,11 +250,11 @@ async def send_request( # Set up progress token if progress callback is provided request_data = request.model_dump(by_alias=True, mode="json", exclude_none=True) - if progress_callback is not None: # pragma: no cover + if progress_callback is not None: # Use request_id as progress token - if "params" not in request_data: + if "params" not in request_data: # pragma: lax no cover request_data["params"] = {} - if "_meta" not in request_data["params"]: # pragma: no branch + if "_meta" not in request_data["params"]: # pragma: lax no cover request_data["params"]["_meta"] = {} request_data["params"]["_meta"]["progressToken"] = request_id # Store the callback for this request @@ -304,7 +304,7 @@ async def send_notification( jsonrpc="2.0", **notification.model_dump(by_alias=True, mode="json", exclude_none=True), ) - session_message = SessionMessage( # pragma: no cover + session_message = SessionMessage( message=jsonrpc_notification, metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None, ) @@ -384,7 +384,7 @@ async def _receive_loop(self) -> None: await self._in_flight[cancelled_id].cancel() else: # Handle progress notifications callback - if isinstance(notification, ProgressNotification): # pragma: no cover + if isinstance(notification, ProgressNotification): progress_token = notification.params.progress_token # If there is a progress callback for this token, # call it with the progress information @@ -400,9 +400,9 @@ async def _receive_loop(self) -> None: logging.exception("Progress callback raised an exception") await self._received_notification(notification) await self._handle_incoming(notification) - except Exception: # pragma: no cover + except Exception: # For other validation errors, log and continue - logging.warning( + logging.warning( # pragma: no cover f"Failed to validate notification:. Message was: {message.message}", exc_info=True, ) @@ -413,11 +413,11 @@ async def _receive_loop(self) -> None: # This is expected when the client disconnects abruptly. # Without this handler, the exception would propagate up and # crash the server's task group. - logging.debug("Read stream closed by client") # pragma: no cover - except Exception as e: # pragma: no cover + logging.debug("Read stream closed by client") + except Exception as e: # Other exceptions are not expected and should be logged. We purposefully # catch all exceptions here to avoid crashing the server. - logging.exception(f"Unhandled exception in receive loop: {e}") + logging.exception(f"Unhandled exception in receive loop: {e}") # pragma: no cover finally: # after the read stream is closed, we need to send errors # to any pending requests @@ -482,9 +482,9 @@ async def _handle_response(self, message: SessionMessage) -> None: # Fall back to normal response streams stream = self._response_streams.pop(response_id, None) - if stream: # pragma: no cover + if stream: await stream.send(message.message) - else: # pragma: no cover + else: await self._handle_incoming(RuntimeError(f"Received response with an unknown request ID: {message}")) async def _received_request(self, responder: RequestResponder[ReceiveRequestT, SendResultT]) -> None: diff --git a/tests/client/auth/extensions/test_client_credentials.py b/tests/client/auth/extensions/test_client_credentials.py index a4faada4a8..0003b16797 100644 --- a/tests/client/auth/extensions/test_client_credentials.py +++ b/tests/client/auth/extensions/test_client_credentials.py @@ -23,7 +23,7 @@ def __init__(self): self._tokens: OAuthToken | None = None self._client_info: OAuthClientInformationFull | None = None - async def get_tokens(self) -> OAuthToken | None: # pragma: no cover + async def get_tokens(self) -> OAuthToken | None: return self._tokens async def set_tokens(self, tokens: OAuthToken) -> None: # pragma: no cover diff --git a/tests/client/conftest.py b/tests/client/conftest.py index 7314a37351..268e968aa4 100644 --- a/tests/client/conftest.py +++ b/tests/client/conftest.py @@ -40,7 +40,7 @@ def clear(self) -> None: self.client.sent_messages.clear() self.server.sent_messages.clear() - def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]: # pragma: no cover + def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]: """Get client-sent requests, optionally filtered by method.""" return [ req.message diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 2f531cc653..7ad24f2df9 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -1696,7 +1696,7 @@ async def callback_handler() -> tuple[str, str | None]: final_response = httpx.Response(200, request=final_request) try: await auth_flow.asend(final_response) - except StopAsyncIteration: # pragma: no cover + except StopAsyncIteration: pass @pytest.mark.anyio diff --git a/tests/client/test_notification_response.py b/tests/client/test_notification_response.py index 06d893ac68..83d9770974 100644 --- a/tests/client/test_notification_response.py +++ b/tests/client/test_notification_response.py @@ -94,7 +94,7 @@ def non_sdk_server(non_sdk_server_port: int) -> Generator[None, None, None]: proc.start() # Wait for server to be ready - try: # pragma: no cover + try: wait_for_server(non_sdk_server_port, timeout=10.0) except TimeoutError: # pragma: no cover proc.kill() diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 4059a92682..9f1e085e9b 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -157,7 +157,7 @@ async def test_stdio_client_universal_cleanup(): @pytest.mark.anyio @pytest.mark.skipif(sys.platform == "win32", reason="Windows signal handling is different") -async def test_stdio_client_sigint_only_process(): # pragma: no cover +async def test_stdio_client_sigint_only_process(): # pragma: lax no cover """Test cleanup with a process that ignores SIGTERM but responds to SIGINT.""" # Create a Python script that ignores SIGTERM but handles SIGINT script_content = textwrap.dedent( @@ -481,7 +481,7 @@ def handle_term(sig, frame): await anyio.sleep(0.5) # Verify child is writing - if os.path.exists(marker_file): # pragma: no cover + if os.path.exists(marker_file): # pragma: no branch size1 = os.path.getsize(marker_file) await anyio.sleep(0.3) size2 = os.path.getsize(marker_file) diff --git a/tests/client/transports/test_memory.py b/tests/client/transports/test_memory.py index b97ebcea2f..942ff6c8e0 100644 --- a/tests/client/transports/test_memory.py +++ b/tests/client/transports/test_memory.py @@ -34,10 +34,8 @@ def fastmcp_server() -> FastMCP: """Create a FastMCP server for testing.""" server = FastMCP("test") - # pragma: no cover on handlers below - they exist only to register capabilities. - # Transport tests verify stream creation and basic protocol, not handler invocation. @server.tool() - def greet(name: str) -> str: # pragma: no cover + def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" diff --git a/tests/experimental/tasks/server/test_server.py b/tests/experimental/tasks/server/test_server.py index cb8b737f28..5711e55c92 100644 --- a/tests/experimental/tasks/server/test_server.py +++ b/tests/experimental/tasks/server/test_server.py @@ -310,8 +310,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - async for message in server_session.incoming_messages: # pragma: no cover + async for message in server_session.incoming_messages: # pragma: no branch await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) @@ -388,9 +387,9 @@ async def run_server(): ), ) as server_session: async with anyio.create_task_group() as tg: - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - async def handle_messages(): # pragma: no cover - async for message in server_session.incoming_messages: + + async def handle_messages(): + async for message in server_session.incoming_messages: # pragma: no branch await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) @@ -573,7 +572,7 @@ async def test_build_elicit_form_request() -> None: assert ( request_with_task.params["_meta"]["io.modelcontextprotocol/related-task"]["taskId"] == "test-task-123" ) - finally: # pragma: no cover + finally: await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -619,7 +618,7 @@ async def test_build_elicit_url_request() -> None: assert ( request_with_task.params["_meta"]["io.modelcontextprotocol/related-task"]["taskId"] == "test-task-789" ) - finally: # pragma: no cover + finally: await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -670,7 +669,7 @@ async def test_build_create_message_request() -> None: request_with_task.params["_meta"]["io.modelcontextprotocol/related-task"]["taskId"] == "sampling-task-456" ) - finally: # pragma: no cover + finally: await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -707,7 +706,7 @@ async def test_send_message() -> None: received = await server_to_client_receive.receive() assert isinstance(received.message, JSONRPCNotification) assert received.message.method == "test/notification" - finally: # pragma: no cover + finally: # pragma: lax no cover await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -761,7 +760,7 @@ def route_error(self, request_id: str | int, error: ErrorData) -> bool: assert len(routed_responses) == 1 assert routed_responses[0]["id"] == "test-req-1" assert routed_responses[0]["response"]["status"] == "ok" - finally: # pragma: no cover + finally: # pragma: lax no cover await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -816,7 +815,7 @@ def route_error(self, request_id: str | int, error: ErrorData) -> bool: assert len(routed_errors) == 1 assert routed_errors[0]["id"] == "test-req-2" assert routed_errors[0]["error"].message == "Test error" - finally: # pragma: no cover + finally: # pragma: lax no cover await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -874,7 +873,7 @@ def route_error(self, request_id: str | int, error: ErrorData) -> bool: # Verify both routers were called (first returned False, second returned True) assert router_calls == ["non_matching_response", "matching_response"] - finally: # pragma: no cover + finally: # pragma: lax no cover await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() @@ -933,7 +932,7 @@ def route_error(self, request_id: str | int, error: ErrorData) -> bool: # Verify both routers were called (first returned False, second returned True) assert router_calls == ["non_matching_error", "matching_error"] - finally: # pragma: no cover + finally: # pragma: lax no cover await server_to_client_send.aclose() await server_to_client_receive.aclose() await client_to_server_send.aclose() diff --git a/tests/issues/test_1027_win_unreachable_cleanup.py b/tests/issues/test_1027_win_unreachable_cleanup.py index 0c569edb2a..78ff54070f 100644 --- a/tests/issues/test_1027_win_unreachable_cleanup.py +++ b/tests/issues/test_1027_win_unreachable_cleanup.py @@ -102,7 +102,7 @@ def echo(text: str) -> str: # Give server a moment to complete cleanup with anyio.move_on_after(5.0): - while not Path(cleanup_marker).exists(): # pragma: no cover + while not Path(cleanup_marker).exists(): # pragma: lax no cover await anyio.sleep(0.1) # Verify cleanup marker was created - this works now that stdio_client @@ -113,9 +113,9 @@ def echo(text: str) -> str: finally: # Clean up files for path in [server_script, startup_marker, cleanup_marker]: - try: # pragma: no cover + try: # pragma: lax no cover Path(path).unlink() - except FileNotFoundError: # pragma: no cover + except FileNotFoundError: # pragma: lax no cover pass @@ -204,7 +204,7 @@ def echo(text: str) -> str: await anyio.sleep(0.1) # Check if process is still running - if hasattr(process, "returncode") and process.returncode is not None: # pragma: no cover + if hasattr(process, "returncode") and process.returncode is not None: # pragma: lax no cover pytest.fail(f"Server process exited with code {process.returncode}") assert Path(startup_marker).exists(), "Server startup marker not created" @@ -217,14 +217,14 @@ def echo(text: str) -> str: try: with anyio.fail_after(5.0): # Increased from 2.0 to 5.0 await process.wait() - except TimeoutError: # pragma: no cover + except TimeoutError: # pragma: lax no cover # If it doesn't exit after stdin close, terminate it process.terminate() await process.wait() # Check if cleanup ran with anyio.move_on_after(5.0): - while not Path(cleanup_marker).exists(): # pragma: no cover + while not Path(cleanup_marker).exists(): # pragma: lax no cover await anyio.sleep(0.1) # Verify the cleanup ran - stdin closure enables graceful shutdown @@ -234,7 +234,7 @@ def echo(text: str) -> str: finally: # Clean up files for path in [server_script, startup_marker, cleanup_marker]: - try: # pragma: no cover + try: # pragma: lax no cover Path(path).unlink() - except FileNotFoundError: # pragma: no cover + except FileNotFoundError: # pragma: lax no cover pass diff --git a/tests/issues/test_129_resource_templates.py b/tests/issues/test_129_resource_templates.py index 26b58343c3..2411210675 100644 --- a/tests/issues/test_129_resource_templates.py +++ b/tests/issues/test_129_resource_templates.py @@ -33,10 +33,10 @@ def get_user_profile(user_id: str) -> str: # pragma: no cover assert len(templates) == 2 # Verify template details - greeting_template = next(t for t in templates if t.name == "get_greeting") # pragma: no cover + greeting_template = next(t for t in templates if t.name == "get_greeting") assert greeting_template.uri_template == "greeting://{name}" assert greeting_template.description == "Get a personalized greeting" - profile_template = next(t for t in templates if t.name == "get_user_profile") # pragma: no cover + profile_template = next(t for t in templates if t.name == "get_user_profile") assert profile_template.uri_template == "users://{user_id}/profile" assert profile_template.description == "Dynamic user data" diff --git a/tests/issues/test_1363_race_condition_streamable_http.py b/tests/issues/test_1363_race_condition_streamable_http.py index caa6db46ec..db2a82d073 100644 --- a/tests/issues/test_1363_race_condition_streamable_http.py +++ b/tests/issues/test_1363_race_condition_streamable_http.py @@ -99,7 +99,7 @@ def check_logs_for_race_condition_errors(caplog: pytest.LogCaptureFixture, test_ # Check for specific race condition errors in logs errors_found: list[str] = [] - for record in caplog.records: # pragma: no cover + for record in caplog.records: # pragma: lax no cover message = record.getMessage() if "ClosedResourceError" in message: errors_found.append("ClosedResourceError") diff --git a/tests/issues/test_88_random_error.py b/tests/issues/test_88_random_error.py index a29231d77d..4ea2f1a454 100644 --- a/tests/issues/test_88_random_error.py +++ b/tests/issues/test_88_random_error.py @@ -104,7 +104,7 @@ async def client( # proving server is still responsive result = await session.call_tool("fast", read_timeout_seconds=None) assert result.content == [TextContent(type="text", text="fast 3")] - scope.cancel() # pragma: no cover + scope.cancel() # pragma: lax no cover # Run server and client in separate task groups to avoid cancellation server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage](1) diff --git a/tests/issues/test_malformed_input.py b/tests/issues/test_malformed_input.py index cb60ca42a6..da586f3098 100644 --- a/tests/issues/test_malformed_input.py +++ b/tests/issues/test_malformed_input.py @@ -82,7 +82,7 @@ async def test_malformed_initialize_request_does_not_crash_server(): except anyio.WouldBlock: # pragma: no cover pytest.fail("No response received - server likely crashed") - finally: # pragma: no cover + finally: # pragma: lax no cover # Close all streams to ensure proper cleanup await read_send_stream.aclose() await write_send_stream.aclose() @@ -143,7 +143,7 @@ async def test_multiple_concurrent_malformed_requests(): assert isinstance(response, JSONRPCError) assert response.id == f"malformed_{i}" assert response.error.code == INVALID_PARAMS - finally: # pragma: no cover + finally: # pragma: lax no cover # Close all streams to ensure proper cleanup await read_send_stream.aclose() await write_send_stream.aclose() diff --git a/tests/server/fastmcp/auth/test_auth_integration.py b/tests/server/fastmcp/auth/test_auth_integration.py index 5000c7b386..a78a86cf0b 100644 --- a/tests/server/fastmcp/auth/test_auth_integration.py +++ b/tests/server/fastmcp/auth/test_auth_integration.py @@ -172,7 +172,7 @@ async def load_access_token(self, token: str) -> AccessToken | None: async def revoke_token(self, token: AccessToken | RefreshToken) -> None: match token: - case RefreshToken(): # pragma: no cover + case RefreshToken(): # pragma: lax no cover # Remove the refresh token del self.refresh_tokens[token.token] diff --git a/tests/server/fastmcp/resources/test_file_resources.py b/tests/server/fastmcp/resources/test_file_resources.py index 0eb24f0632..39272b051b 100644 --- a/tests/server/fastmcp/resources/test_file_resources.py +++ b/tests/server/fastmcp/resources/test_file_resources.py @@ -18,9 +18,9 @@ def temp_file(): f.write(content) path = Path(f.name).resolve() yield path - try: # pragma: no cover + try: # pragma: lax no cover path.unlink() - except FileNotFoundError: # pragma: no cover + except FileNotFoundError: # pragma: lax no cover pass # File was already deleted by the test @@ -101,7 +101,7 @@ async def test_missing_file_error(self, temp_file: Path): @pytest.mark.skipif(os.name == "nt", reason="File permissions behave differently on Windows") @pytest.mark.anyio - async def test_permission_error(self, temp_file: Path): # pragma: no cover + async def test_permission_error(self, temp_file: Path): # pragma: lax no cover """Test reading a file without permissions.""" temp_file.chmod(0o000) # Remove all permissions try: diff --git a/tests/server/fastmcp/resources/test_resource_manager.py b/tests/server/fastmcp/resources/test_resource_manager.py index 5fd4bc8529..3d6dd9be91 100644 --- a/tests/server/fastmcp/resources/test_resource_manager.py +++ b/tests/server/fastmcp/resources/test_resource_manager.py @@ -18,9 +18,9 @@ def temp_file(): f.write(content) path = Path(f.name).resolve() yield path - try: # pragma: no cover + try: # pragma: lax no cover path.unlink() - except FileNotFoundError: # pragma: no cover + except FileNotFoundError: # pragma: lax no cover pass # File was already deleted by the test diff --git a/tests/server/fastmcp/resources/test_resource_template.py b/tests/server/fastmcp/resources/test_resource_template.py index f3d3ba5e45..022e86db82 100644 --- a/tests/server/fastmcp/resources/test_resource_template.py +++ b/tests/server/fastmcp/resources/test_resource_template.py @@ -15,7 +15,7 @@ class TestResourceTemplate: def test_template_creation(self): """Test creating a template from a function.""" - def my_func(key: str, value: int) -> dict[str, Any]: # pragma: no cover + def my_func(key: str, value: int) -> dict[str, Any]: return {"key": key, "value": value} template = ResourceTemplate.from_function( @@ -239,7 +239,7 @@ def get_dynamic(id: str) -> str: # pragma: no cover async def test_template_created_resources_inherit_annotations(self): """Test that resources created from templates inherit annotations.""" - def get_item(item_id: str) -> str: # pragma: no cover + def get_item(item_id: str) -> str: return f"Item {item_id}" annotations = Annotations(priority=0.6) diff --git a/tests/server/fastmcp/test_elicitation.py b/tests/server/fastmcp/test_elicitation.py index efed572e4b..587dcebb06 100644 --- a/tests/server/fastmcp/test_elicitation.py +++ b/tests/server/fastmcp/test_elicitation.py @@ -65,12 +65,10 @@ async def test_stdio_elicitation(): create_ask_user_tool(mcp) # Create a custom handler for elicitation requests - async def elicitation_callback( - context: RequestContext[ClientSession, None], params: ElicitRequestParams - ): # pragma: no cover + async def elicitation_callback(context: RequestContext[ClientSession, None], params: ElicitRequestParams): if params.message == "Tool wants to ask: What is your name?": return ElicitResult(action="accept", content={"answer": "Test User"}) - else: + else: # pragma: no cover raise ValueError(f"Unexpected elicitation message: {params.message}") await call_tool_and_assert( @@ -99,10 +97,10 @@ async def test_elicitation_schema_validation(): def create_validation_tool(name: str, schema_class: type[BaseModel]): @mcp.tool(name=name, description=f"Tool testing {name}") - async def tool(ctx: Context[ServerSession, None]) -> str: # pragma: no cover + async def tool(ctx: Context[ServerSession, None]) -> str: try: await ctx.elicit(message="This should fail validation", schema=schema_class) - return "Should not reach here" + return "Should not reach here" # pragma: no cover except TypeError as e: return f"Validation failed as expected: {str(e)}" @@ -190,10 +188,10 @@ class InvalidOptionalSchema(BaseModel): optional_list: list[int] | None = Field(default=None, description="Invalid optional list") @mcp.tool(description="Tool with invalid optional field") - async def invalid_optional_tool(ctx: Context[ServerSession, None]) -> str: # pragma: no cover + async def invalid_optional_tool(ctx: Context[ServerSession, None]) -> str: try: await ctx.elicit(message="This should fail", schema=InvalidOptionalSchema) - return "Should not reach here" + return "Should not reach here" # pragma: no cover except TypeError as e: return f"Validation failed: {str(e)}" diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index 8d3ac6ec50..ba1d30d26d 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -516,7 +516,7 @@ async def test_str_annotation_runtime_validation(): containing valid JSON to ensure they are passed as strings, not parsed objects. """ - def handle_json_payload(payload: str, strict_mode: bool = False) -> str: # pragma: no cover + def handle_json_payload(payload: str, strict_mode: bool = False) -> str: """Function that processes a JSON payload as a string.""" # This function expects to receive the raw JSON string # It might parse it later after validation or logging @@ -833,7 +833,7 @@ def func_returning_unannotated() -> UnannotatedClass: # pragma: no cover def test_tool_call_result_is_unstructured_and_not_converted(): - def func_returning_call_tool_result() -> CallToolResult: # pragma: no cover + def func_returning_call_tool_result() -> CallToolResult: return CallToolResult(content=[]) meta = func_metadata(func_returning_call_tool_result) @@ -846,7 +846,7 @@ def test_tool_call_result_annotated_is_structured_and_converted(): class PersonClass(BaseModel): name: str - def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: # pragma: no cover + def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: return CallToolResult(content=[], structured_content={"name": "Brandon"}) meta = func_metadata(func_returning_annotated_tool_call_result) @@ -866,7 +866,7 @@ def test_tool_call_result_annotated_is_structured_and_invalid(): class PersonClass(BaseModel): name: str - def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: # pragma: no cover + def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]: return CallToolResult(content=[], structured_content={"person": "Brandon"}) meta = func_metadata(func_returning_annotated_tool_call_result) @@ -1092,7 +1092,7 @@ def func_with_reserved_names( # pragma: no cover async def test_basemodel_reserved_names_validation(): """Test that validation and calling works with reserved parameter names""" - def func_with_reserved_names( # pragma: no cover + def func_with_reserved_names( model_dump: str, model_validate: int, dict: list[str], diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 6d1cee58ef..5b6030be96 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -887,7 +887,7 @@ async def test_resource_multiple_mismatched_params(self): def get_data_mismatched(org: str, repo_2: str) -> str: # pragma: no cover return f"Data for {org}" - """Test that a resource with no parameters works as a regular resource""" # pragma: no cover + """Test that a resource with no parameters works as a regular resource""" mcp = FastMCP() @mcp.resource("resource://static") diff --git a/tests/server/fastmcp/test_tool_manager.py b/tests/server/fastmcp/test_tool_manager.py index b09ae7de15..5ac1bf282f 100644 --- a/tests/server/fastmcp/test_tool_manager.py +++ b/tests/server/fastmcp/test_tool_manager.py @@ -182,7 +182,7 @@ def f(x: int) -> int: # pragma: no cover class TestCallTools: @pytest.mark.anyio async def test_call_tool(self): - def sum(a: int, b: int) -> int: # pragma: no cover + def sum(a: int, b: int) -> int: """Add two numbers.""" return a + b @@ -193,7 +193,7 @@ def sum(a: int, b: int) -> int: # pragma: no cover @pytest.mark.anyio async def test_call_async_tool(self): - async def double(n: int) -> int: # pragma: no cover + async def double(n: int) -> int: """Double a number.""" return n * 2 @@ -260,7 +260,7 @@ async def test_call_unknown_tool(self): @pytest.mark.anyio async def test_call_tool_with_list_int_input(self): - def sum_vals(vals: list[int]) -> int: # pragma: no cover + def sum_vals(vals: list[int]) -> int: return sum(vals) manager = ToolManager() @@ -273,7 +273,7 @@ def sum_vals(vals: list[int]) -> int: # pragma: no cover @pytest.mark.anyio async def test_call_tool_with_list_str_or_str_input(self): - def concat_strs(vals: list[str] | str) -> str: # pragma: no cover + def concat_strs(vals: list[str] | str) -> str: return vals if isinstance(vals, str) else "".join(vals) manager = ToolManager() @@ -297,7 +297,7 @@ class Shrimp(BaseModel): shrimp: list[Shrimp] x: None - def name_shrimp(tank: MyShrimpTank, ctx: Context[ServerSessionT, None]) -> list[str]: # pragma: no cover + def name_shrimp(tank: MyShrimpTank, ctx: Context[ServerSessionT, None]) -> list[str]: return [x.name for x in tank.shrimp] manager = ToolManager() @@ -375,7 +375,7 @@ def tool_with_context(x: int, ctx: Context[ServerSessionT, None]) -> str: async def test_context_injection_async(self): """Test that context is properly injected in async tools.""" - async def async_tool(x: int, ctx: Context[ServerSessionT, None]) -> str: # pragma: no cover + async def async_tool(x: int, ctx: Context[ServerSessionT, None]) -> str: assert isinstance(ctx, Context) return str(x) @@ -467,7 +467,7 @@ class UserOutput(BaseModel): name: str age: int - def get_user(user_id: int) -> UserOutput: # pragma: no cover + def get_user(user_id: int) -> UserOutput: """Get user by ID.""" return UserOutput(name="John", age=30) @@ -481,7 +481,7 @@ def get_user(user_id: int) -> UserOutput: # pragma: no cover async def test_tool_with_primitive_output(self): """Test tool with primitive return type.""" - def double_number(n: int) -> int: # pragma: no cover + def double_number(n: int) -> int: """Double a number.""" return 10 @@ -502,7 +502,7 @@ class UserDict(TypedDict): expected_output = {"name": "Alice", "age": 25} - def get_user_dict(user_id: int) -> UserDict: # pragma: no cover + def get_user_dict(user_id: int) -> UserDict: """Get user as dict.""" return UserDict(name="Alice", age=25) @@ -522,7 +522,7 @@ class Person: expected_output = {"name": "Bob", "age": 40} - def get_person() -> Person: # pragma: no cover + def get_person() -> Person: """Get a person.""" return Person("Bob", 40) @@ -539,7 +539,7 @@ async def test_tool_with_list_output(self): expected_list = [1, 2, 3, 4, 5] expected_output = {"result": expected_list} - def get_numbers() -> list[int]: # pragma: no cover + def get_numbers() -> list[int]: """Get a list of numbers.""" return expected_list @@ -590,7 +590,7 @@ def get_user() -> UserOutput: # pragma: no cover async def test_tool_with_dict_str_any_output(self): """Test tool with dict[str, Any] return type.""" - def get_config() -> dict[str, Any]: # pragma: no cover + def get_config() -> dict[str, Any]: """Get configuration""" return {"debug": True, "port": 8080, "features": ["auth", "logging"]} @@ -615,7 +615,7 @@ def get_config() -> dict[str, Any]: # pragma: no cover async def test_tool_with_dict_str_typed_output(self): """Test tool with dict[str, T] return type for specific T.""" - def get_scores() -> dict[str, int]: # pragma: no cover + def get_scores() -> dict[str, int]: """Get player scores""" return {"alice": 100, "bob": 85, "charlie": 92} @@ -879,7 +879,7 @@ def divide(a: int, b: int) -> float: # pragma: no cover async def test_call_removed_tool_raises_error(self): """Test that calling a removed tool raises ToolError.""" - def greet(name: str) -> str: # pragma: no cover + def greet(name: str) -> str: """Greet someone.""" return f"Hello, {name}!" diff --git a/tests/server/test_completion_with_context.py b/tests/server/test_completion_with_context.py index 19c591340d..5a8d67f09e 100644 --- a/tests/server/test_completion_with_context.py +++ b/tests/server/test_completion_with_context.py @@ -102,7 +102,7 @@ async def handle_completion( db = context.arguments.get("database") if db == "users_db": return Completion(values=["users", "sessions", "permissions"], total=3, has_more=False) - elif db == "products_db": # pragma: no cover + elif db == "products_db": return Completion(values=["products", "categories", "inventory"], total=3, has_more=False) return Completion(values=[], total=0, has_more=False) # pragma: no cover @@ -153,7 +153,7 @@ async def handle_completion( raise ValueError("Please select a database first to see available tables") # Normal completion if context is provided db = context.arguments.get("database") - if db == "test_db": # pragma: no cover + if db == "test_db": return Completion(values=["users", "orders", "products"], total=3, has_more=False) return Completion(values=[], total=0, has_more=False) # pragma: no cover diff --git a/tests/server/test_lowlevel_input_validation.py b/tests/server/test_lowlevel_input_validation.py index eb644938ff..3f977bcc1b 100644 --- a/tests/server/test_lowlevel_input_validation.py +++ b/tests/server/test_lowlevel_input_validation.py @@ -70,8 +70,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - async for message in server_session.incoming_messages: # pragma: no cover + async for message in server_session.incoming_messages: # pragma: no branch await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_lowlevel_output_validation.py b/tests/server/test_lowlevel_output_validation.py index 3b1b7236b5..92d9c047ca 100644 --- a/tests/server/test_lowlevel_output_validation.py +++ b/tests/server/test_lowlevel_output_validation.py @@ -71,8 +71,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - async for message in server_session.incoming_messages: # pragma: no cover + async for message in server_session.incoming_messages: # pragma: no branch await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_lowlevel_tool_annotations.py b/tests/server/test_lowlevel_tool_annotations.py index 614ca2dce5..68543136eb 100644 --- a/tests/server/test_lowlevel_tool_annotations.py +++ b/tests/server/test_lowlevel_tool_annotations.py @@ -20,7 +20,7 @@ async def test_lowlevel_server_tool_annotations(): # Create a tool with annotations @server.list_tools() - async def list_tools(): # pragma: no cover + async def list_tools(): return [ Tool( name="echo", @@ -67,8 +67,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - async for message in server_session.incoming_messages: # pragma: no cover + async for message in server_session.incoming_messages: # pragma: no branch await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 5de9882223..d4dbdca304 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -391,8 +391,7 @@ async def test_create_message_tool_result_validation(): # Case 8: empty messages list - skips validation entirely # Covers the `if messages:` branch (line 280->302) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.move_on_after(0.01): # pragma: no cover + with anyio.move_on_after(0.01): # pragma: no branch await session.create_message(messages=[], max_tokens=100) diff --git a/tests/shared/test_progress_notifications.py b/tests/shared/test_progress_notifications.py index 13edcec01d..81aa1ccbc5 100644 --- a/tests/shared/test_progress_notifications.py +++ b/tests/shared/test_progress_notifications.py @@ -335,9 +335,7 @@ def mock_log_exception(msg: str, *args: Any, **kwargs: Any) -> None: logged_errors.append(msg % args if args else msg) # Create a progress callback that raises an exception - async def failing_progress_callback( - progress: float, total: float | None, message: str | None - ) -> None: # pragma: no cover + async def failing_progress_callback(progress: float, total: float | None, message: str | None) -> None: raise ValueError("Progress callback failed!") # Create a server with a tool that sends progress notifications diff --git a/tests/shared/test_session.py b/tests/shared/test_session.py index 89fe18ebbc..fa903f8ff4 100644 --- a/tests/shared/test_session.py +++ b/tests/shared/test_session.py @@ -97,8 +97,7 @@ async def make_request(client: Client): ) # Give cancellation time to process - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.fail_after(1): # pragma: no cover + with anyio.fail_after(1): # pragma: no branch await ev_cancelled.wait() @@ -149,8 +148,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.fail_after(2): # pragma: no cover + with anyio.fail_after(2): # pragma: no branch await ev_response_received.wait() assert len(result_holder) == 1 @@ -205,8 +203,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.fail_after(2): # pragma: no cover + with anyio.fail_after(2): # pragma: no branch await ev_error_received.wait() assert len(error_holder) == 1 @@ -260,8 +257,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.fail_after(2): # pragma: no cover + with anyio.fail_after(2): # pragma: no branch await ev_timeout.wait() @@ -305,8 +301,7 @@ async def mock_server(): tg.start_soon(make_request, client_session) tg.start_soon(mock_server) - # TODO(Marcelo): Drop the pragma once https://github.com/coveragepy/coveragepy/issues/1987 is fixed. - with anyio.fail_after(1): # pragma: no cover + with anyio.fail_after(1): await ev_closed.wait() - with anyio.fail_after(1): # pragma: no cover + with anyio.fail_after(1): # pragma: no branch await ev_response.wait() diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index ed86f9860e..b1332772a3 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -72,14 +72,14 @@ # Helper functions -def extract_protocol_version_from_sse(response: requests.Response) -> str: # pragma: no cover +def extract_protocol_version_from_sse(response: requests.Response) -> str: """Extract the negotiated protocol version from an SSE initialization response.""" assert response.headers.get("Content-Type") == "text/event-stream" for line in response.text.splitlines(): if line.startswith("data: "): init_data = json.loads(line[6:]) return init_data["result"]["protocolVersion"] - raise ValueError("Could not extract protocol version from SSE response") + raise ValueError("Could not extract protocol version from SSE response") # pragma: no cover # Simple in-memory event store for testing @@ -90,9 +90,7 @@ def __init__(self): self._events: list[tuple[StreamId, EventId, types.JSONRPCMessage | None]] = [] self._event_id_counter = 0 - async def store_event( # pragma: no cover - self, stream_id: StreamId, message: types.JSONRPCMessage | None - ) -> EventId: + async def store_event(self, stream_id: StreamId, message: types.JSONRPCMessage | None) -> EventId: """Store an event and return its ID.""" self._event_id_counter += 1 event_id = str(self._event_id_counter) @@ -871,7 +869,7 @@ def test_get_sse_stream(basic_server: None, basic_server_url: str): init_data = None assert init_response.headers.get("Content-Type") == "text/event-stream" for line in init_response.text.splitlines(): # pragma: no branch - if line.startswith("data: "): # pragma: no cover + if line.startswith("data: "): init_data = json.loads(line[6:]) break assert init_data is not None @@ -931,7 +929,7 @@ def test_get_validation(basic_server: None, basic_server_url: str): init_data = None assert init_response.headers.get("Content-Type") == "text/event-stream" for line in init_response.text.splitlines(): # pragma: no branch - if line.startswith("data: "): # pragma: no cover + if line.startswith("data: "): init_data = json.loads(line[6:]) break assert init_data is not None @@ -1155,8 +1153,8 @@ async def test_streamable_http_client_session_termination(basic_server: None, ba tools = await session.list_tools() assert len(tools.tools) == 10 - headers: dict[str, str] = {} # pragma: no cover - if captured_session_id: # pragma: no cover + headers: dict[str, str] = {} # pragma: lax no cover + if captured_session_id: # pragma: lax no cover headers[MCP_SESSION_ID_HEADER] = captured_session_id async with create_mcp_http_client(headers=headers) as httpx_client: @@ -1219,8 +1217,8 @@ async def mock_delete(self: httpx.AsyncClient, *args: Any, **kwargs: Any) -> htt tools = await session.list_tools() assert len(tools.tools) == 10 - headers: dict[str, str] = {} # pragma: no cover - if captured_session_id: # pragma: no cover + headers: dict[str, str] = {} # pragma: lax no cover + if captured_session_id: # pragma: lax no cover headers[MCP_SESSION_ID_HEADER] = captured_session_id async with create_mcp_http_client(headers=headers) as httpx_client: @@ -1281,7 +1279,7 @@ async def on_resumption_token_update(token: str) -> None: captured_protocol_version = result.protocol_version # Start the tool that will wait on lock in a task - async with anyio.create_task_group() as tg: + async with anyio.create_task_group() as tg: # pragma: no branch async def run_tool(): metadata = ClientMessageMetadata( @@ -1304,20 +1302,21 @@ async def run_tool(): # Kill the client session while tool is waiting on lock tg.cancel_scope.cancel() - # Verify we received exactly one notification - assert len(captured_notifications) == 1 # pragma: no cover - assert isinstance(captured_notifications[0], types.LoggingMessageNotification) # pragma: no cover - assert captured_notifications[0].params.data == "First notification before lock" # pragma: no cover - - # Clear notifications for the second phase - captured_notifications = [] # pragma: no cover - - # Now resume the session with the same mcp-session-id and protocol version - headers: dict[str, Any] = {} # pragma: no cover - if captured_session_id: # pragma: no cover - headers[MCP_SESSION_ID_HEADER] = captured_session_id - if captured_protocol_version: # pragma: no cover - headers[MCP_PROTOCOL_VERSION_HEADER] = captured_protocol_version + # Verify we received exactly one notification (inside ClientSession + # so coverage tracks these on Python 3.11, see PR #1897 for details) + assert len(captured_notifications) == 1 # pragma: lax no cover + assert isinstance(captured_notifications[0], types.LoggingMessageNotification) # pragma: lax no cover + assert captured_notifications[0].params.data == "First notification before lock" # pragma: lax no cover + + # Clear notifications and set up headers for phase 2 (between connections, + # not tracked by coverage on Python 3.11 due to cancel scope + sys.settrace bug) + captured_notifications = [] # pragma: lax no cover + assert captured_session_id is not None # pragma: lax no cover + assert captured_protocol_version is not None # pragma: lax no cover + headers: dict[str, Any] = { # pragma: lax no cover + MCP_SESSION_ID_HEADER: captured_session_id, + MCP_PROTOCOL_VERSION_HEADER: captured_protocol_version, + } async with create_mcp_http_client(headers=headers) as httpx_client: async with streamable_http_client(f"{server_url}/mcp", http_client=httpx_client) as ( @@ -1325,7 +1324,9 @@ async def run_tool(): write_stream, _, ): - async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session: + async with ClientSession( + read_stream, write_stream, message_handler=message_handler + ) as session: # pragma: no branch result = await session.send_request( types.CallToolRequest(params=types.CallToolRequestParams(name="release_lock", arguments={})), types.CallToolResult, @@ -1347,9 +1348,8 @@ async def run_tool(): # We should have received the remaining notifications assert len(captured_notifications) == 1 - - assert isinstance(captured_notifications[0], types.LoggingMessageNotification) # pragma: no cover - assert captured_notifications[0].params.data == "Second notification after lock" # pragma: no cover + assert isinstance(captured_notifications[0], types.LoggingMessageNotification) + assert captured_notifications[0].params.data == "Second notification after lock" @pytest.mark.anyio @@ -1582,8 +1582,8 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No contexts.append(context_data) # Verify each request had its own context - assert len(contexts) == 3 # pragma: no cover - for i, ctx in enumerate(contexts): # pragma: no cover + assert len(contexts) == 3 + for i, ctx in enumerate(contexts): assert ctx["request_id"] == f"request-{i}" assert ctx["headers"].get("x-request-id") == f"request-{i}" assert ctx["headers"].get("x-custom-value") == f"value-{i}" @@ -2153,7 +2153,7 @@ async def on_resumption_token(token: str) -> None: assert "Completed 3 checkpoints" in result.content[0].text # 4 priming + 3 notifications + 1 response = 8 tokens - assert len(resumption_tokens) == 8, ( # pragma: no cover + assert len(resumption_tokens) == 8, ( # pragma: lax no cover f"Expected 8 resumption tokens (4 priming + 3 notifs + 1 response), " f"got {len(resumption_tokens)}: {resumption_tokens}" ) diff --git a/tests/shared/test_ws.py b/tests/shared/test_ws.py index 8fb7aeec3b..501fe049b7 100644 --- a/tests/shared/test_ws.py +++ b/tests/shared/test_ws.py @@ -105,7 +105,7 @@ def run_server(server_port: int) -> None: # pragma: no cover time.sleep(0.5) -@pytest.fixture() # pragma: no cover +@pytest.fixture() def server(server_port: int) -> Generator[None, None, None]: proc = multiprocessing.Process(target=run_server, kwargs={"server_port": server_port}, daemon=True) print("starting process") diff --git a/tests/test_examples.py b/tests/test_examples.py index 327729d4a4..6f390e33f6 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -92,7 +92,7 @@ async def test_desktop(monkeypatch: pytest.MonkeyPatch): assert file_1 in content.text assert file_2 in content.text # might be a bug, but the test is passing - else: # pragma: no cover + else: # pragma: lax no cover assert "/fake/path/file1.txt" in content.text assert "/fake/path/file2.txt" in content.text diff --git a/uv.lock b/uv.lock index 0f736e1259..0a5b7a9d45 100644 --- a/uv.lock +++ b/uv.lock @@ -306,101 +306,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, ] [package.optional-dependencies] @@ -762,6 +762,7 @@ dev = [ { name = "pytest-pretty" }, { name = "pytest-xdist" }, { name = "ruff" }, + { name = "strict-no-cover" }, { name = "trio" }, ] docs = [ @@ -797,7 +798,7 @@ provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ - { name = "coverage", extras = ["toml"], specifier = ">=7.13.1" }, + { name = "coverage", extras = ["toml"], specifier = ">=7.10.7,<=7.13" }, { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "inline-snapshot", specifier = ">=0.23.0" }, { name = "mcp", extras = ["cli", "ws"], editable = "." }, @@ -809,6 +810,7 @@ dev = [ { name = "pytest-pretty", specifier = ">=1.2.0" }, { name = "pytest-xdist", specifier = ">=3.6.1" }, { name = "ruff", specifier = ">=0.8.5" }, + { name = "strict-no-cover", git = "https://github.com/pydantic/strict-no-cover" }, { name = "trio", specifier = ">=0.26.2" }, ] docs = [ @@ -2385,6 +2387,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, ] +[[package]] +name = "strict-no-cover" +version = "0.1.1" +source = { git = "https://github.com/pydantic/strict-no-cover#7fc59da2c4dff919db2095a0f0e47101b657131d" } +dependencies = [ + { name = "pydantic" }, +] + [[package]] name = "tomli" version = "2.2.1"