From 2d34e0efe771f086ef83342ce8239e08dcaae362 Mon Sep 17 00:00:00 2001 From: tzing Date: Tue, 4 Nov 2025 09:16:54 +0800 Subject: [PATCH 1/7] add: accept dict type --- tugboat/engine/types.py | 5 +++-- tugboat/types.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tugboat/engine/types.py b/tugboat/engine/types.py index 40fc510..f40e3a9 100644 --- a/tugboat/engine/types.py +++ b/tugboat/engine/types.py @@ -106,11 +106,12 @@ class DiagnosisModel(BaseModel): ] fix: Annotated[ - str | None, + str | dict[str, Any], + None, Field( default=None, description=( - "Analyzer's suggested fix presented as plain text. Treat this as guidance, not a guaranteed solution." + "Analyzer's suggested fix. Treat this as guidance, not a guaranteed solution." ), ), ] diff --git a/tugboat/types.py b/tugboat/types.py index c42e794..c40c09e 100644 --- a/tugboat/types.py +++ b/tugboat/types.py @@ -75,7 +75,7 @@ class Diagnosis(TypedDict): Use :py:class:`Field` to indicate a specific field rather than a value. """ - fix: NotRequired[str | None] + fix: NotRequired[str | dict[str, Any] | None] """Possible alternative value to fix the issue.""" ctx: NotRequired[dict[str, Bundle]] From aedd342bf05bc6a908848f571cd7467bd64f4c6f Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 10:54:45 +0800 Subject: [PATCH 2/7] chore: support dict --- .../formatters/test_console_formatter.py | 66 +++++++++++++++++-- tugboat/console/formatters/console.py | 38 ++++++++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/tests/console/formatters/test_console_formatter.py b/tests/console/formatters/test_console_formatter.py index 5b3dfb7..28a5ef7 100644 --- a/tests/console/formatters/test_console_formatter.py +++ b/tests/console/formatters/test_console_formatter.py @@ -173,16 +173,74 @@ def test_3(self, fixture_dir: Path): """ ), f"Output:\n{report}" + def test_4(self, fixture_dir: Path): + manifest_path = fixture_dir / "sample-workflow.yaml" + + formatter = ConsoleFormatter() + formatter.update( + content=manifest_path.read_text(), + diagnoses=[ + DiagnosisModel.model_validate( + { + "type": "failure", + "line": 5, + "column": 1, + "code": "T04", + "loc": ("spec",), + "msg": "Test failure message", + "fix": { + "entries": [ + {"name": "entry1", "value": 123}, + ] + }, + } + ) + ], + ) + + with io.StringIO() as buffer: + formatter.dump(buffer) + report = buffer.getvalue() + + assert report == IsOutputEqual( + """\ + T04 Test failure message + @:5:1 + + 3 | metadata: + 4 | generateName: hello-world- + 5 | spec: + | └ T04 at $.spec + 6 | entrypoint: hello-world + 7 | templates: + + Test failure message + + Do you mean: + entries: + - name: entry1 + value: 123 + + """ + ), f"Output:\n{report}" + class IsOutputEqual(DirtyEquals[str]): def __init__(self, text: str): - text = textwrap.dedent(text).rstrip(" ") - super().__init__(text) - self.text = text + super().__init__(textwrap.dedent(text)) def equals(self, other): - return self.text == other + expected_content: str + (expected_content,) = self._repr_args + for expected_line, actual_line in zip( + expected_content.splitlines(), + other.splitlines(), + strict=True, + ): + if expected_line.rstrip() != actual_line.rstrip(): + return False + return True class TestCalcHighlightRange: diff --git a/tugboat/console/formatters/console.py b/tugboat/console/formatters/console.py index 269b8dc..3aec583 100644 --- a/tugboat/console/formatters/console.py +++ b/tugboat/console/formatters/console.py @@ -7,6 +7,8 @@ from dataclasses import dataclass import click +import ruamel.yaml +from ruamel.yaml.scalarstring import LiteralScalarString from tugboat.console.formatters.base import OutputFormatter from tugboat.settings import settings @@ -220,12 +222,31 @@ def suggestion(self) -> str: buf.write(" ") buf.write(Style.DoYouMean.fmt("Do you mean: ")) - if "\n" in self.diagnosis.fix: + # case: structured suggestion + if isinstance(self.diagnosis.fix, dict): + buf.write("\n") + + suggested_structure = transform_multiline_strings(self.diagnosis.fix) + with io.StringIO() as yaml_buf: + yaml_dumper = ruamel.yaml.YAML() + yaml_dumper.preserve_quotes = True + yaml_dumper.default_flow_style = False + yaml_dumper.dump(suggested_structure, yaml_buf) + + yaml_buf.seek(0) + for line in yaml_buf: + buf.write(" ") + buf.write(Style.Suggestion.fmt(line)) + + # case: multi-line string + elif "\n" in self.diagnosis.fix: buf.write("|-\n") for line in self.diagnosis.fix.splitlines(): buf.write(" ") buf.write(Style.Suggestion.fmt(line)) buf.write("\n") + + # case: single-line string or other types else: buf.write(Style.Suggestion.fmt(self.diagnosis.fix)) buf.write("\n") @@ -314,3 +335,18 @@ def calc_highlight_range(line: str, offset: int, substr: Any) -> tuple[int, int] col_end = col_start + len(value) return col_start, col_end + + +def transform_multiline_strings(obj): + """ + Ensure that multi-line strings in the object are represented as literal + scalars. + """ + if isinstance(obj, dict): + return {k: transform_multiline_strings(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [transform_multiline_strings(item) for item in obj] + elif isinstance(obj, str) and "\n" in obj: + return LiteralScalarString(obj) + else: + return obj From 62c6321d3f6e9af64f7af15d25135a6c8b856042 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 11:19:19 +0800 Subject: [PATCH 3/7] enh: use object for artifact_prohibited_value_field --- tugboat/engine/pydantic.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tugboat/engine/pydantic.py b/tugboat/engine/pydantic.py index 5b1f4c1..877ee81 100644 --- a/tugboat/engine/pydantic.py +++ b/tugboat/engine/pydantic.py @@ -330,7 +330,11 @@ def translate_pydantic_error(error: ErrorDetails) -> Diagnosis: # noqa: C901 } with contextlib.suppress(Exception): - diagnosis["fix"] = json.dumps({"raw": {"data": error["input"]}}) + diagnosis["fix"] = { + "raw": { + "data": error["input"], + }, + } return diagnosis From 305c014ed09c622361fab23385634bee9fb6e137 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 11:29:48 +0800 Subject: [PATCH 4/7] enh: field description --- tugboat/engine/types.py | 11 +++++++---- tugboat/types.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tugboat/engine/types.py b/tugboat/engine/types.py index f40e3a9..8d31951 100644 --- a/tugboat/engine/types.py +++ b/tugboat/engine/types.py @@ -50,9 +50,9 @@ class DiagnosisModel(BaseModel): default="failure", description=( "Diagnostic severity reported by the analyzer.\n" - "* `error`: analysis stopped because processing could not continue.\n" - "* `failure`: definite rule violation that prevents success.\n" - "* `warning`: potential issue that should be reviewed.\n" + "- `error`: analysis stopped because processing could not continue.\n" + "- `failure`: definite rule violation that prevents success.\n" + "- `warning`: potential issue that should be reviewed.\n" ), ), ] @@ -111,7 +111,10 @@ class DiagnosisModel(BaseModel): Field( default=None, description=( - "Analyzer's suggested fix. Treat this as guidance, not a guaranteed solution." + "Suggested fix for the detected issue. This is guidance only, not a guaranteed solution.\n" + "- String: Exact replacement text for the problematic field value.\n" + "- Dictionary: Structured fix with multiple field modifications.\n" + "- None: No automatic fix available." ), ), ] diff --git a/tugboat/types.py b/tugboat/types.py index c40c09e..1aaa6e6 100644 --- a/tugboat/types.py +++ b/tugboat/types.py @@ -36,9 +36,9 @@ class Diagnosis(TypedDict): The diagnosis type. When not provided, it defaults to "failure". - * ``error`` indicates a critical issue that prevents the analyzer from running. - * ``failure`` indicates an issue that the analyzer has detected. - * ``warning`` indicates a potential issue that the analyzer has detected. + - ``error`` indicates a critical issue that prevents the analyzer from running. + - ``failure`` indicates an issue that the analyzer has detected. + - ``warning`` indicates a potential issue that the analyzer has detected. This is not a critical issue, but it may require attention. """ @@ -76,7 +76,12 @@ class Diagnosis(TypedDict): """ fix: NotRequired[str | dict[str, Any] | None] - """Possible alternative value to fix the issue.""" + """ + Suggested fix for the detected issue. + + - Provided as a string: Represents the exact replacement text for the problematic field value. + - Provided as a dictionary: Represents a structured fix containing multiple field modifications. + """ ctx: NotRequired[dict[str, Bundle]] """ From 0d120eb77afca5d83c14de3f77a466861de4f123 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 11:34:16 +0800 Subject: [PATCH 5/7] doc: changelog --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 49105b5..542a8b7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,8 @@ Unreleased :octicon:`code-review` API Changes ++++++++++++++++++++++++++++++++++ +* The :py:attr:`Diagnosis.fix ` field now accepts :py:class:`dict` types in addition to :py:class:`str`. + Use :py:class:`dict` when suggesting fixes that involve multiple fields or complex object replacements, while :py:class:`str` remain suitable for simple value corrections. * Refactor :py:mod:`tugboat.constraints` to provide more flexible validation functions: * Diagnostic messages have been improved for clarity. From 12552aee8807778efc8da2917567aa224a680cdf Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 11:39:05 +0800 Subject: [PATCH 6/7] fix: test case for artifact_prohibited_value_field --- tests/engine/test_pydantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/engine/test_pydantic.py b/tests/engine/test_pydantic.py index 46c28d7..667b1e5 100644 --- a/tests/engine/test_pydantic.py +++ b/tests/engine/test_pydantic.py @@ -314,7 +314,7 @@ def test_artifact_prohibited_value_field(self): "Use 'raw' artifact type instead." ), "input": "value", - "fix": '{"raw": {"data": "foobar"}}', + "fix": {"raw": {"data": "foobar"}}, } def test_parameter_value_type_error_1(self): From 4a51d64f1e821303f3c35a7a71edbbb6b5aee224 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 11:41:01 +0800 Subject: [PATCH 7/7] fix: type annotation --- tugboat/engine/types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tugboat/engine/types.py b/tugboat/engine/types.py index 8d31951..ea10370 100644 --- a/tugboat/engine/types.py +++ b/tugboat/engine/types.py @@ -106,8 +106,7 @@ class DiagnosisModel(BaseModel): ] fix: Annotated[ - str | dict[str, Any], - None, + str | dict[str, Any] | None, Field( default=None, description=(