Skip to content

Commit bd79325

Browse files
Your Nameclaude
andcommitted
fix(missing_await): suppress dual sync/async client FPs; fix ruff lint
Dual sync/async client pattern (openai-python, httpx): when a file defines SyncFoo.close() and AsyncFoo.close() in the same module, pact cannot resolve which class 'self' refers to and incorrectly flags sync callers. Apply the same collision-skip logic already used for module-level functions to methods. Also fix two ruff lint errors that were breaking CI: - test_checker.py:1835 unused variable 'p' (-> _write_src without assignment) - test_reduce.py:22 unused import _live_roots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1f60663 commit bd79325

3 files changed

Lines changed: 51 additions & 3 deletions

File tree

failure_mode.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,15 @@ def _scan_file_missing_await(path: str) -> list[FailureEvidence]:
859859
for node in _ast.walk(tree)
860860
if isinstance(node, _ast.FunctionDef)
861861
}
862+
# Sync method names: same-named sync def with self/cls — dual sync/async
863+
# client pattern (e.g. SyncClient.close + AsyncClient.close in one file).
864+
sync_method_names: set[str] = {
865+
node.name
866+
for node in _ast.walk(tree)
867+
if isinstance(node, _ast.FunctionDef)
868+
and node.args.args
869+
and node.args.args[0].arg in ("self", "cls")
870+
}
862871
module_async: set[str] = set()
863872
method_async: set[str] = set()
864873
for node in _ast.walk(tree):
@@ -870,7 +879,11 @@ def _scan_file_missing_await(path: str) -> list[FailureEvidence]:
870879
# Heuristic: if the first argument is self/cls, it's a method
871880
args = node.args.args
872881
if args and args[0].arg in ("self", "cls"):
873-
method_async.add(node.name)
882+
# Skip if the same method name also exists as a sync def —
883+
# dual sync/async client pattern; pact can't resolve which class
884+
# self refers to and would cross-contaminate (e.g. openai-python).
885+
if node.name not in sync_method_names:
886+
method_async.add(node.name)
874887
else:
875888
# Skip names that also have a sync def — closure-level name
876889
# collision; pact cannot resolve which definition is called.

test_checker.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1832,7 +1832,7 @@ def make():
18321832

18331833
def test_overload_stub_mutable_default_not_flagged(tmp_path):
18341834
"""@overload type stubs never execute at runtime — mutable defaults in them are FPs."""
1835-
p = _write_src(
1835+
_write_src(
18361836
tmp_path,
18371837
"client.py",
18381838
"""
@@ -2015,3 +2015,39 @@ def deploy(env: str, force: bool = False) -> None:
20152015
violations = check_codebase(tmp_path)
20162016
ra = [v for v in violations if v.context == "required_arg_missing" and "typer_tool.py" in v.file]
20172017
assert len(ra) == 0, f"Expected 0 required_arg_missing for @app.command(), got {len(ra)}: {[(v.line, v.call) for v in ra]}"
2018+
2019+
2020+
def test_dual_sync_async_client_missing_await_not_flagged(tmp_path):
2021+
"""Dual sync/async client in one file: self.close() in async method of AsyncClient
2022+
must not be flagged because the same name exists as sync def in SyncClient."""
2023+
_write_src(
2024+
tmp_path,
2025+
"clients.py",
2026+
"""
2027+
import httpx
2028+
2029+
class SyncClient:
2030+
def close(self) -> None:
2031+
self._http.close()
2032+
2033+
def request(self, method: str, url: str) -> httpx.Response:
2034+
return self._http.request(method, url)
2035+
2036+
def _prepare_request(self, method: str) -> dict:
2037+
return {"method": method}
2038+
2039+
class AsyncClient:
2040+
async def close(self) -> None:
2041+
await self._http.aclose()
2042+
2043+
async def request(self, method: str, url: str) -> httpx.Response:
2044+
self._prepare_request(method)
2045+
return await self._http.request(method, url)
2046+
2047+
async def _prepare_request(self, method: str) -> dict:
2048+
return {"method": method}
2049+
""",
2050+
)
2051+
violations = check_codebase(tmp_path)
2052+
ma = [v for v in violations if v.context == "missing_await" and "clients.py" in v.file]
2053+
assert len(ma) == 0, f"Expected 0 missing_await for dual sync/async client, got {len(ma)}: {[(v.line, v.call) for v in ma]}"

test_reduce.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
find_passthroughs,
2020
find_sccs,
2121
_build_digraph,
22-
_live_roots,
2322
)
2423
from .extractor import FunctionManifest, CallSite
2524
from .encoder import Violation

0 commit comments

Comments
 (0)