From c88a38b8b50213cefc61499714ff635b46112fb3 Mon Sep 17 00:00:00 2001 From: chariri Date: Tue, 9 Jun 2026 17:32:34 +0900 Subject: [PATCH] chore(api): Suppress unknown contract checks by default (#36969) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/dev/lint_response_contracts.py | 19 ++++++++--- .../commands/test_lint_response_contracts.py | 32 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/api/dev/lint_response_contracts.py b/api/dev/lint_response_contracts.py index 6cdb3e289c..4ba79e0fed 100644 --- a/api/dev/lint_response_contracts.py +++ b/api/dev/lint_response_contracts.py @@ -7,9 +7,9 @@ or ``Model.model_validate(...).model_dump()`` return. Raw dictionaries, raw lists, ``None`` responses, streaming helpers, missing response schemas, and returns with non-literal status codes are classified as -unknown so reviewers can triage them without blocking unrelated work. The one -intentional non-schema mismatch is a known body/schema on a no-body status such -as 204, 205, or 304. +unknown. Unknown details are hidden by default to keep routine output focused; +pass ``--include-unknown`` when triaging them. The one intentional non-schema +mismatch is a known body/schema on a no-body status such as 204, 205, or 304. """ from __future__ import annotations @@ -589,7 +589,7 @@ def as_jsonable(check: ContractCheck) -> dict[str, Any]: return data -def print_text_report(checks: Sequence[ContractCheck], *, include_valid: bool) -> None: +def print_text_report(checks: Sequence[ContractCheck], *, include_unknown: bool, include_valid: bool) -> None: counts = Counter(check.classification for check in checks) sys.stdout.write( "Response contract lint: " @@ -601,6 +601,8 @@ def print_text_report(checks: Sequence[ContractCheck], *, include_valid: bool) - for classification in ("mismatch", "refactorable", "unknown", "valid"): filtered = [check for check in checks if check.classification == classification] + if classification == "unknown" and not include_unknown: + continue if classification == "valid" and not include_valid: continue if not filtered: @@ -619,6 +621,7 @@ def parse_args() -> argparse.Namespace: nargs="*", help="Files or directories to lint. Defaults to Flask controller directories.", ) + parser.add_argument("--include-unknown", action="store_true", help="Print unknown route methods in output.") parser.add_argument("--include-valid", action="store_true", help="Print valid route methods in text output.") parser.add_argument("--json", action="store_true", help="Emit machine-readable JSON.") parser.add_argument( @@ -650,10 +653,16 @@ def main() -> int: if args.json: grouped = defaultdict(list) for check in checks: + if check.classification == "unknown" and not args.include_unknown: + continue grouped[check.classification].append(as_jsonable(check)) sys.stdout.write(f"{json.dumps(grouped, indent=2, sort_keys=True)}\n") else: - print_text_report(checks, include_valid=bool(args.include_valid)) + print_text_report( + checks, + include_unknown=bool(args.include_unknown), + include_valid=bool(args.include_valid), + ) has_mismatch = any(check.classification == "mismatch" for check in checks) has_unknown = any(check.classification == "unknown" for check in checks) diff --git a/api/tests/unit_tests/commands/test_lint_response_contracts.py b/api/tests/unit_tests/commands/test_lint_response_contracts.py index 8f3860f231..351fdf0d92 100644 --- a/api/tests/unit_tests/commands/test_lint_response_contracts.py +++ b/api/tests/unit_tests/commands/test_lint_response_contracts.py @@ -2,6 +2,8 @@ import importlib.util import sys from pathlib import Path +import pytest + def _load_lint_response_contracts_module(): api_dir = Path(__file__).parents[3] @@ -115,7 +117,7 @@ class StreamApi(Resource): assert {actual.model for actual in checks[0].actual} == {"StreamResponse"} -def test_main_is_report_only_by_default_for_mismatches(tmp_path: Path, monkeypatch): +def test_main_is_report_only_by_default_for_mismatches(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): module = _load_lint_response_contracts_module() controller_path = tmp_path / "controllers" / "sample.py" controller_path.parent.mkdir() @@ -137,6 +139,34 @@ class BadDeleteApi(Resource): assert module.main() == 1 +def test_main_hides_unknown_details_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys): + module = _load_lint_response_contracts_module() + controller_path = tmp_path / "controllers" / "sample.py" + controller_path.parent.mkdir() + controller_path.write_text( + """ +@ns.route("/items") +class ItemApi(Resource): + @ns.response(200, "OK", ns.models[ItemResponse.__name__]) + def get(self): + return dump_response(ItemResponse, item), status_code +""", + encoding="utf-8", + ) + + monkeypatch.setattr(sys, "argv", ["lint_response_contracts.py", str(controller_path)]) + assert module.main() == 0 + default_output = capsys.readouterr().out + assert "1 unknown" in default_output + assert "UNKNOWN:" not in default_output + + monkeypatch.setattr(sys, "argv", ["lint_response_contracts.py", "--include-unknown", str(controller_path)]) + assert module.main() == 0 + include_unknown_output = capsys.readouterr().out + assert "UNKNOWN:" in include_unknown_output + assert "non-literal or unsupported status" in include_unknown_output + + def test_class_level_route_and_response_docs_apply_to_methods(tmp_path: Path): checks = _checks_for_source( tmp_path,