Skip to content

Commit a7ddfda

Browse files
authored
ci: add strict-no-cover to detect unnecessary coverage pragmas (#1897)
1 parent 3cfdea0 commit a7ddfda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+355
-358
lines changed

.github/workflows/shared.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ jobs:
6262
uv run --frozen --no-sync coverage combine
6363
uv run --frozen --no-sync coverage report
6464
65+
- name: Check for unnecessary no cover pragmas
66+
if: runner.os != 'Windows'
67+
run: uv run --frozen --no-sync strict-no-cover
68+
6569
readme-snippets:
6670
runs-on: ubuntu-latest
6771
steps:

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ dev = [
6666
"pytest-pretty>=1.2.0",
6767
"inline-snapshot>=0.23.0",
6868
"dirty-equals>=0.9.0",
69-
"coverage[toml]>=7.13.1",
69+
"coverage[toml]>=7.10.7,<=7.13",
7070
"pillow>=12.0",
71+
"strict-no-cover",
7172
]
7273
docs = [
7374
"mkdocs>=1.6.1",
@@ -163,6 +164,7 @@ members = ["examples/clients/*", "examples/servers/*", "examples/snippets"]
163164

164165
[tool.uv.sources]
165166
mcp = { workspace = true }
167+
strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" }
166168

167169
[tool.pytest.ini_options]
168170
log_cli = true
@@ -198,7 +200,6 @@ branch = true
198200
patch = ["subprocess"]
199201
concurrency = ["multiprocessing", "thread"]
200202
source = ["src", "tests"]
201-
relative_files = true
202203
omit = [
203204
"src/mcp/client/__main__.py",
204205
"src/mcp/server/__main__.py",
@@ -215,6 +216,7 @@ ignore_errors = true
215216
precision = 2
216217
exclude_lines = [
217218
"pragma: no cover",
219+
"pragma: lax no cover",
218220
"if TYPE_CHECKING:",
219221
"@overload",
220222
"raise NotImplementedError",

src/mcp/cli/claude.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def update_claude_config(
7272
)
7373

7474
config_file = config_dir / "claude_desktop_config.json"
75-
if not config_file.exists(): # pragma: no cover
75+
if not config_file.exists(): # pragma: lax no cover
7676
try:
7777
config_file.write_text("{}")
7878
except Exception:

src/mcp/cli/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _build_uv_command(
7777

7878
if with_packages:
7979
for pkg in with_packages:
80-
if pkg: # pragma: no cover
80+
if pkg: # pragma: no branch
8181
cmd.extend(["--with", pkg])
8282

8383
# Add mcp run command

src/mcp/client/auth/oauth2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def prepare_token_auth(
192192
headers = {} # pragma: no cover
193193

194194
if not self.client_info:
195-
return data, headers # pragma: no cover
195+
return data, headers
196196

197197
auth_method = self.client_info.token_endpoint_auth_method
198198

@@ -418,7 +418,7 @@ async def _refresh_token(self) -> httpx.Request:
418418
raise OAuthTokenError("No client info available") # pragma: no cover
419419

420420
if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint:
421-
token_url = str(self.context.oauth_metadata.token_endpoint) # pragma: no cover
421+
token_url = str(self.context.oauth_metadata.token_endpoint)
422422
else:
423423
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
424424
token_url = urljoin(auth_base_url, "/token")
@@ -534,7 +534,7 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
534534
)
535535

536536
# Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers)
537-
for url in asm_discovery_urls: # pragma: no cover
537+
for url in asm_discovery_urls: # pragma: no branch
538538
oauth_metadata_request = create_oauth_metadata_request(url)
539539
oauth_metadata_response = yield oauth_metadata_request
540540

src/mcp/client/session.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ async def set_logging_level(
250250
meta: RequestParamsMeta | None = None,
251251
) -> types.EmptyResult:
252252
"""Send a logging/setLevel request."""
253-
return await self.send_request( # pragma: no cover
253+
return await self.send_request(
254254
types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),
255255
types.EmptyResult,
256256
)
@@ -285,14 +285,14 @@ async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None
285285

286286
async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
287287
"""Send a resources/subscribe request."""
288-
return await self.send_request( # pragma: no cover
288+
return await self.send_request(
289289
types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),
290290
types.EmptyResult,
291291
)
292292

293293
async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:
294294
"""Send a resources/unsubscribe request."""
295-
return await self.send_request( # pragma: no cover
295+
return await self.send_request(
296296
types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),
297297
types.EmptyResult,
298298
)
@@ -344,7 +344,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
344344
try:
345345
validate(result.structured_content, output_schema)
346346
except ValidationError as e:
347-
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}") # pragma: no cover
347+
raise RuntimeError(f"Invalid structured content returned by tool {name}: {e}")
348348
except SchemaError as e: # pragma: no cover
349349
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
350350

src/mcp/client/session_group.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,22 +223,22 @@ async def disconnect_from_server(self, session: mcp.ClientSession) -> None:
223223
)
224224
)
225225

226-
if session_known_for_components: # pragma: no cover
226+
if session_known_for_components: # pragma: no branch
227227
component_names = self._sessions.pop(session) # Pop from _sessions tracking
228228

229229
# Remove prompts associated with the session.
230230
for name in component_names.prompts:
231-
if name in self._prompts:
231+
if name in self._prompts: # pragma: no branch
232232
del self._prompts[name]
233233
# Remove resources associated with the session.
234234
for name in component_names.resources:
235-
if name in self._resources:
235+
if name in self._resources: # pragma: no branch
236236
del self._resources[name]
237237
# Remove tools associated with the session.
238238
for name in component_names.tools:
239-
if name in self._tools:
239+
if name in self._tools: # pragma: no branch
240240
del self._tools[name]
241-
if name in self._tool_to_session:
241+
if name in self._tool_to_session: # pragma: no branch
242242
del self._tool_to_session[name]
243243

244244
# Clean up the session's resources via its dedicated exit stack

src/mcp/client/sse.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ async def sse_reader(task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED):
119119
await read_stream_writer.send(session_message)
120120
case _: # pragma: no cover
121121
logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover
122-
except SSEError as sse_exc: # pragma: no cover
123-
logger.exception("Encountered SSE exception") # pragma: no cover
124-
raise sse_exc # pragma: no cover
125-
except Exception as exc: # pragma: no cover
126-
logger.exception("Error in sse_reader") # pragma: no cover
127-
await read_stream_writer.send(exc) # pragma: no cover
122+
except SSEError as sse_exc: # pragma: lax no cover
123+
logger.exception("Encountered SSE exception")
124+
raise sse_exc
125+
except Exception as exc: # pragma: lax no cover
126+
logger.exception("Error in sse_reader")
127+
await read_stream_writer.send(exc)
128128
finally:
129129
await read_stream_writer.aclose()
130130

@@ -143,8 +143,8 @@ async def post_writer(endpoint_url: str):
143143
)
144144
response.raise_for_status()
145145
logger.debug(f"Client message sent successfully: {response.status_code}")
146-
except Exception: # pragma: no cover
147-
logger.exception("Error in post_writer") # pragma: no cover
146+
except Exception: # pragma: lax no cover
147+
logger.exception("Error in post_writer")
148148
finally:
149149
await write_stream.aclose()
150150

src/mcp/client/stdio.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def get_default_environment() -> dict[str, str]:
5656

5757
for key in DEFAULT_INHERITED_ENV_VARS:
5858
value = os.environ.get(key)
59-
if value is None:
60-
continue # pragma: no cover
59+
if value is None: # pragma: lax no cover
60+
continue
6161

6262
if value.startswith("()"): # pragma: no cover
6363
# Skip functions, which are a security risk
@@ -158,7 +158,7 @@ async def stdout_reader():
158158

159159
session_message = SessionMessage(message)
160160
await read_stream_writer.send(session_message)
161-
except anyio.ClosedResourceError: # pragma: no cover
161+
except anyio.ClosedResourceError: # pragma: lax no cover
162162
await anyio.lowlevel.checkpoint()
163163

164164
async def stdin_writer():
@@ -225,8 +225,8 @@ def _get_executable_command(command: str) -> str:
225225
"""
226226
if sys.platform == "win32": # pragma: no cover
227227
return get_windows_executable_command(command)
228-
else:
229-
return command # pragma: no cover
228+
else: # pragma: lax no cover
229+
return command
230230

231231

232232
async def _create_platform_compatible_process(
@@ -243,14 +243,14 @@ async def _create_platform_compatible_process(
243243
"""
244244
if sys.platform == "win32": # pragma: no cover
245245
process = await create_windows_process(command, args, env, errlog, cwd)
246-
else:
246+
else: # pragma: lax no cover
247247
process = await anyio.open_process(
248248
[command, *args],
249249
env=env,
250250
stderr=errlog,
251251
cwd=cwd,
252252
start_new_session=True,
253-
) # pragma: no cover
253+
)
254254

255255
return process
256256

@@ -267,7 +267,7 @@ async def _terminate_process_tree(process: Process | FallbackProcess, timeout_se
267267
"""
268268
if sys.platform == "win32": # pragma: no cover
269269
await terminate_windows_process_tree(process, timeout_seconds)
270-
else: # pragma: no cover
270+
else: # pragma: lax no cover
271271
# FallbackProcess should only be used for Windows compatibility
272272
assert isinstance(process, Process)
273273
await terminate_posix_process_tree(process, timeout_seconds)

src/mcp/client/streamable_http.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer:
181181

182182
headers = self._prepare_headers()
183183
if last_event_id:
184-
headers[LAST_EVENT_ID] = last_event_id # pragma: no cover
184+
headers[LAST_EVENT_ID] = last_event_id
185185

186186
async with aconnect_sse(client, "GET", self.url, headers=headers) as event_source:
187187
event_source.response.raise_for_status()
@@ -190,17 +190,17 @@ async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer:
190190
async for sse in event_source.aiter_sse():
191191
# Track last event ID for reconnection
192192
if sse.id:
193-
last_event_id = sse.id # pragma: no cover
193+
last_event_id = sse.id
194194
# Track retry interval from server
195195
if sse.retry is not None:
196-
retry_interval_ms = sse.retry # pragma: no cover
196+
retry_interval_ms = sse.retry
197197

198198
await self._handle_sse_event(sse, read_stream_writer)
199199

200200
# Stream ended normally (server closed) - reset attempt counter
201201
attempt = 0
202202

203-
except Exception as exc: # pragma: no cover
203+
except Exception as exc: # pragma: lax no cover
204204
logger.debug(f"GET stream error: {exc}")
205205
attempt += 1
206206

@@ -333,8 +333,8 @@ async def _handle_sse_response(
333333
if is_complete:
334334
await response.aclose()
335335
return # Normal completion, no reconnect needed
336-
except Exception as e: # pragma: no cover
337-
logger.debug(f"SSE stream ended: {e}")
336+
except Exception as e:
337+
logger.debug(f"SSE stream ended: {e}") # pragma: no cover
338338

339339
# Stream ended without response - reconnect if we received an event with ID
340340
if last_event_id is not None: # pragma: no branch
@@ -472,20 +472,20 @@ async def handle_request_async():
472472
await read_stream_writer.aclose()
473473
await write_stream.aclose()
474474

475-
async def terminate_session(self, client: httpx.AsyncClient) -> None: # pragma: no cover
475+
async def terminate_session(self, client: httpx.AsyncClient) -> None:
476476
"""Terminate the session by sending a DELETE request."""
477-
if not self.session_id:
477+
if not self.session_id: # pragma: lax no cover
478478
return
479479

480480
try:
481481
headers = self._prepare_headers()
482482
response = await client.delete(self.url, headers=headers)
483483

484-
if response.status_code == 405:
484+
if response.status_code == 405: # pragma: lax no cover
485485
logger.debug("Server does not allow session termination")
486-
elif response.status_code not in (200, 204):
486+
elif response.status_code not in (200, 204): # pragma: lax no cover
487487
logger.warning(f"Session termination failed: {response.status_code}")
488-
except Exception as exc:
488+
except Exception as exc: # pragma: no cover
489489
logger.warning(f"Session termination failed: {exc}")
490490

491491
def get_session_id(self) -> str | None:

0 commit comments

Comments
 (0)