diff --git a/tests/formats/dataclass/test_generator.py b/tests/formats/dataclass/test_generator.py index bceabdfd7..ffecd747d 100644 --- a/tests/formats/dataclass/test_generator.py +++ b/tests/formats/dataclass/test_generator.py @@ -212,10 +212,39 @@ def test_package_name(self): self.assertEqual("", self.generator.package_name("")) - def test_format_with_invalid_code(self): - src_code = """a = "1""" + def test_ruff_code_with_invalid_code(self): + src_code = ( + "class AlternativeText:\n" + " class Meta:\n" + ' namespace = "xsdata"\n' + "\n" + " foo: Optional[Union[]] = field(\n" + " init=False,\n" + ' metadata={"type": "Ignore"}\n' + " )\n" + " bar: str\n" + " thug: str" + ) file_path = Path(__file__) self.generator.config.output.max_line_length = 55 - with self.assertRaises(CodegenError): + with self.assertRaises(CodegenError) as cm: self.generator.ruff_code(src_code, file_path) + + expected = ( + "\n" + "\n" + " class AlternativeText:\n" + " class Meta:\n" + ' namespace = "xsdata"\n' + " \n" + "---> foo: Optional[Union[]] = field(\n" + " init=False,\n" + ' metadata={"type": "Ignore"}\n' + " )" + ) + self.assertEqual(expected, cm.exception.meta.get("source")) + + def test_code_excerpt_with_no_line_number(self): + actual = self.generator.code_excerpt("foobar", "") + self.assertEqual("NA", actual) diff --git a/xsdata/formats/dataclass/generator.py b/xsdata/formats/dataclass/generator.py index eaf23f8df..f33d22109 100644 --- a/xsdata/formats/dataclass/generator.py +++ b/xsdata/formats/dataclass/generator.py @@ -1,3 +1,4 @@ +import re import subprocess from pathlib import Path from textwrap import indent @@ -255,5 +256,25 @@ def ruff_code(self, src_code: str, file_path: Path) -> str: return src_code_encoded.decode() except subprocess.CalledProcessError as e: - error = indent(e.stderr.decode(), " ") - raise CodegenError("Ruff failed", details=error) + details = e.stderr.decode().replace("error: ", "").strip() + source = self.code_excerpt(details, src_code_encoded.decode()) + raise CodegenError("Ruff failed", details=details, source=source) + + @classmethod + def code_excerpt(cls, details: str, src_code: str) -> str: + """Extract source code excerpt from the error details message.""" + match = re.search(r"(\d+):(\d+)", details) + if match: + line_number = int(match.group(1)) - 1 + lines = src_code.split("\n") + start = max(0, line_number - 4) + end = min(len(lines), line_number + 4) + + excerpt = ["\n"] + for index in range(start, end): + prepend = "--->" if index == line_number else " " + excerpt.append(f"{prepend}{lines[index]}") + + return "\n".join(excerpt) + + return "NA"