From 6e78b2c1246e2932a07d86e036e419f61a84c6e0 Mon Sep 17 00:00:00 2001 From: Kashvi Jain Date: Wed, 24 Jun 2026 13:31:49 +0530 Subject: [PATCH 1/3] Fixed #121 -- Added tests for CLI syntax error formatting. --- docs/en/dev/changelog.md | 5 ++ src/quater/cli/errors.py | 27 +++++-- tests/unit/cli/test_errors.py | 138 ++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 tests/unit/cli/test_errors.py diff --git a/docs/en/dev/changelog.md b/docs/en/dev/changelog.md index 2558682..09dd427 100644 --- a/docs/en/dev/changelog.md +++ b/docs/en/dev/changelog.md @@ -16,6 +16,11 @@ Read [Stability](/en/dev/stability) before upgrading Quater versions. Unreleased changes on `main`. Renamed to the version number when the release is cut. +### Changed + +- CLI: improved syntax error formatting to correctly display multiline source + text and caret positioning for indentation and end-of-line edge cases. + ### Fixed - Fixed `TestClient` cookie replay so cookies stored from `Set-Cookie` respect diff --git a/src/quater/cli/errors.py b/src/quater/cli/errors.py index 1f9be3a..e2b94ee 100644 --- a/src/quater/cli/errors.py +++ b/src/quater/cli/errors.py @@ -43,12 +43,23 @@ def _syntax_error_source_lines(exc: SyntaxError) -> list[str]: if not stripped: return [] - lines = [f" {stripped}"] - if exc.offset is None: - return lines - - leading_spaces = len(line) - len(stripped) - caret_position = max(exc.offset - leading_spaces - 1, 0) - caret_position = min(caret_position, max(len(stripped) - 1, 0)) - lines.append(f" {' ' * caret_position}^") + source_lines = stripped.splitlines() + + caret_line_index: int | None = 0 + if exc.lineno is not None: + if 1 <= exc.lineno <= len(source_lines): + caret_line_index = exc.lineno - 1 + elif len(source_lines) > 1: + caret_line_index = None + + lines: list[str] = [] + for index, source_line in enumerate(source_lines): + lines.append(f" {source_line}") + if exc.offset is None or caret_line_index is None or index != caret_line_index: + continue + + leading_spaces = len(line) - len(stripped) if index == 0 else 0 + caret_position = max(exc.offset - leading_spaces - 1, 0) + caret_position = min(caret_position, max(len(source_line) - 1, 0)) + lines.append(f" {' ' * caret_position}^") return lines diff --git a/tests/unit/cli/test_errors.py b/tests/unit/cli/test_errors.py new file mode 100644 index 0000000..e552d8f --- /dev/null +++ b/tests/unit/cli/test_errors.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from quater.cli.errors import format_syntax_error + + +def test_format_syntax_error_without_line_or_column() -> None: + exc = SyntaxError("invalid syntax") + result = format_syntax_error("Error", exc) + + assert "invalid syntax" in result + assert "line" not in result.lower() + assert "column" not in result.lower() + + +def test_format_syntax_error_without_source_text() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 1, 1, None), + ) + + result = format_syntax_error("Error", exc) + + assert "invalid syntax" in result + + +def test_format_syntax_error_blank_source_text() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 1, 1, ""), + ) + + result = format_syntax_error("Error", exc) + + assert "invalid syntax" in result + + +def test_format_syntax_error_column_before_indentation() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 1, 1, " value"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 1, column 1)", + " value", + " ^", + ] + ) + + +def test_format_syntax_error_column_after_line_end() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 1, 100, "value"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 1, column 100)", + " value", + " ^", + ] + ) + + +def test_format_syntax_error_single_source_line_can_have_later_file_line() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 99, 5, "bad +"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 99, column 5)", + " bad +", + " ^", + ] + ) + + +def test_format_syntax_error_multiline_text() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 1, 1, "line1\nline2"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 1, column 1)", + " line1", + " ^", + " line2", + ] + ) + + +def test_format_syntax_error_multiline_text_with_error_on_later_line() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 2, 4, "line1\n bad"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 2, column 4)", + " line1", + " bad", + " ^", + ] + ) + + +def test_format_syntax_error_multiline_text_with_unmatched_line_skips_caret() -> None: + exc = SyntaxError( + "invalid syntax", + ("test.py", 99, 2, "line1\nline2"), + ) + + result = format_syntax_error("Error", exc) + + assert result == "\n".join( + [ + "Error: invalid syntax (line 99, column 2)", + " line1", + " line2", + ] + ) From 654ea74f318d9c0ecb1201737e5e44237f31f6be Mon Sep 17 00:00:00 2001 From: Kashvi Jain Date: Fri, 26 Jun 2026 21:51:48 +0530 Subject: [PATCH 2/3] Add manual reference validation for documentation --- scripts/generate_reference.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/generate_reference.py b/scripts/generate_reference.py index 304b676..4684488 100644 --- a/scripts/generate_reference.py +++ b/scripts/generate_reference.py @@ -764,7 +764,7 @@ def render_reference( public_api: tuple[str, ...], pages_by_symbol: Mapping[str, ReferencePage], ) -> dict[Path, str]: - manual_outputs = read_manual_reference(public_api, pages_by_symbol) + manual_outputs = read_manual_reference(package, public_api, pages_by_symbol) if manual_outputs is not None: return manual_outputs @@ -782,6 +782,7 @@ def render_reference( def read_manual_reference( + package: Any, public_api: tuple[str, ...], pages_by_symbol: Mapping[str, ReferencePage], ) -> dict[Path, str] | None: @@ -803,6 +804,8 @@ def read_manual_reference( raise SystemExit( f"Manual reference page {page.path} does not document {name!r}" ) + + return outputs From d6fa03e4fbb97234c5cf4c8c550baf22882f2d6c Mon Sep 17 00:00:00 2001 From: Kashvi Jain Date: Sat, 27 Jun 2026 11:35:03 +0530 Subject: [PATCH 3/3] Apply ruff formating --- scripts/generate_reference.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/generate_reference.py b/scripts/generate_reference.py index 4684488..a334fc5 100644 --- a/scripts/generate_reference.py +++ b/scripts/generate_reference.py @@ -804,8 +804,7 @@ def read_manual_reference( raise SystemExit( f"Manual reference page {page.path} does not document {name!r}" ) - - + return outputs