From 69b463f52015352465aff680425d224c7dbe7837 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:53:44 +0300 Subject: [PATCH 1/4] Fix(dbt_cli): Add global error handling group for dbt subcommands --- sqlmesh_dbt/cli.py | 4 +- sqlmesh_dbt/error.py | 7 ++++ tests/dbt/cli/test_global_flags.py | 61 ++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/sqlmesh_dbt/cli.py b/sqlmesh_dbt/cli.py index 2daa3f9d54..c215663f0a 100644 --- a/sqlmesh_dbt/cli.py +++ b/sqlmesh_dbt/cli.py @@ -2,7 +2,7 @@ import sys import click from sqlmesh_dbt.operations import DbtOperations, create -from sqlmesh_dbt.error import cli_global_error_handler +from sqlmesh_dbt.error import cli_global_error_handler, ErrorHandlingGroup from pathlib import Path from sqlmesh_dbt.options import YamlParamType import functools @@ -43,7 +43,7 @@ def _cleanup() -> None: exclude_option = click.option("--exclude", multiple=True, help="Specify the nodes to exclude.") -@click.group(invoke_without_command=True) +@click.group(cls=ErrorHandlingGroup, invoke_without_command=True) @click.option("--profile", help="Which existing profile to load. Overrides output.profile") @click.option("-t", "--target", help="Which target to load for the given profile") @click.option( diff --git a/sqlmesh_dbt/error.py b/sqlmesh_dbt/error.py index 005ca87c50..ee6a80da72 100644 --- a/sqlmesh_dbt/error.py +++ b/sqlmesh_dbt/error.py @@ -27,3 +27,10 @@ def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any: raise return wrapper + + +class ErrorHandlingGroup(click.Group): + def add_command(self, cmd: click.Command, name: str | None = None) -> None: + if cmd.callback: + cmd.callback = cli_global_error_handler(cmd.callback) + super().add_command(cmd, name=name) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py index 802d359346..42dcef5bfa 100644 --- a/tests/dbt/cli/test_global_flags.py +++ b/tests/dbt/cli/test_global_flags.py @@ -2,6 +2,9 @@ from pathlib import Path import pytest from click.testing import Result +from unittest.mock import patch +from sqlmesh.utils.errors import SQLMeshError +from sqlglot.errors import SqlglotError pytestmark = pytest.mark.slow @@ -28,3 +31,61 @@ def test_profile_and_target(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... result = invoke_cli(["--profile", "jaffle_shop", "--target", "dev"]) assert result.exit_code == 0 assert "No command specified" in result.output + + +def test_run_error_handler(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): + with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: + mock_run.side_effect = SQLMeshError("Test error message") + + result = invoke_cli(["run"]) + + # tesg that SQLMeshError are handled gracefully in run command + assert result.exit_code == 1 + assert "Traceback" not in result.output + + with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: + mock_run.side_effect = SqlglotError("Invalid SQL syntax") + + result = invoke_cli(["run"]) + + assert result.exit_code == 1 + assert "Error: Invalid SQL syntax" in result.output + assert "Traceback" not in result.output + + with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: + mock_run.side_effect = ValueError("Invalid configuration value") + + result = invoke_cli(["run"]) + + assert result.exit_code == 1 + assert "Error: Invalid configuration value" in result.output + assert "Traceback" not in result.output + + with patch("sqlmesh_dbt.operations.DbtOperations.list_") as mock_list: + mock_list.side_effect = SQLMeshError("List command error") + + result = invoke_cli(["list"]) + + assert result.exit_code == 1 + assert "Error: List command error" in result.output + assert "Traceback" not in result.output + + with patch("sqlmesh_dbt.cli.create") as mock_create: + mock_create.side_effect = SQLMeshError("Failed to load project") + + # use without subcommand + result = invoke_cli(["--profile", "jaffle_shop"]) + + assert result.exit_code == 1 + assert "Error: Failed to load project" in result.output + assert "Traceback" not in result.output + + with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: + mock_run.side_effect = SQLMeshError("Error with selector") + + # with select option + result = invoke_cli(["run", "--select", "model1"]) + + assert result.exit_code == 1 + assert "Error: Error with selector" in result.output + assert "Traceback" not in result.output From 9d89e95354ba0efbe9537db55ea4b2cafdc153d3 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:12:00 +0300 Subject: [PATCH 2/4] fix python compatibility --- sqlmesh_dbt/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmesh_dbt/error.py b/sqlmesh_dbt/error.py index ee6a80da72..49a2f8195b 100644 --- a/sqlmesh_dbt/error.py +++ b/sqlmesh_dbt/error.py @@ -30,7 +30,7 @@ def wrapper(*args: t.List[t.Any], **kwargs: t.Any) -> t.Any: class ErrorHandlingGroup(click.Group): - def add_command(self, cmd: click.Command, name: str | None = None) -> None: + def add_command(self, cmd: click.Command, name: t.Optional[str] = None) -> None: if cmd.callback: cmd.callback = cli_global_error_handler(cmd.callback) super().add_command(cmd, name=name) From 1585fd156a95c3159955eb2fb2f1bfab9a739cdb Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:00:28 +0300 Subject: [PATCH 3/4] use mockerfixture instead --- tests/dbt/cli/test_global_flags.py | 88 ++++++++++++++++-------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py index 42dcef5bfa..38a155ffc3 100644 --- a/tests/dbt/cli/test_global_flags.py +++ b/tests/dbt/cli/test_global_flags.py @@ -1,8 +1,8 @@ import typing as t from pathlib import Path import pytest +from pytest_mock import MockerFixture from click.testing import Result -from unittest.mock import patch from sqlmesh.utils.errors import SQLMeshError from sqlglot.errors import SqlglotError @@ -33,59 +33,63 @@ def test_profile_and_target(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[... assert "No command specified" in result.output -def test_run_error_handler(jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result]): - with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: - mock_run.side_effect = SQLMeshError("Test error message") +def test_run_error_handler( + jaffle_shop_duckdb: Path, invoke_cli: t.Callable[..., Result], mocker: MockerFixture +) -> None: + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = SQLMeshError("Test error message") - result = invoke_cli(["run"]) - - # tesg that SQLMeshError are handled gracefully in run command - assert result.exit_code == 1 - assert "Traceback" not in result.output - - with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: - mock_run.side_effect = SqlglotError("Invalid SQL syntax") + result = invoke_cli(["run"]) + assert result.exit_code == 1 + assert "Error: Test error message" in result.output + assert "Traceback" not in result.output - result = invoke_cli(["run"]) + # test SqlglotError in run command + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = SqlglotError("Invalid SQL syntax") - assert result.exit_code == 1 - assert "Error: Invalid SQL syntax" in result.output - assert "Traceback" not in result.output + result = invoke_cli(["run"]) - with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: - mock_run.side_effect = ValueError("Invalid configuration value") + assert result.exit_code == 1 + assert "Error: Invalid SQL syntax" in result.output + assert "Traceback" not in result.output - result = invoke_cli(["run"]) + # test ValueError in run command + mock_run = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run.side_effect = ValueError("Invalid configuration value") - assert result.exit_code == 1 - assert "Error: Invalid configuration value" in result.output - assert "Traceback" not in result.output + result = invoke_cli(["run"]) - with patch("sqlmesh_dbt.operations.DbtOperations.list_") as mock_list: - mock_list.side_effect = SQLMeshError("List command error") + assert result.exit_code == 1 + assert "Error: Invalid configuration value" in result.output + assert "Traceback" not in result.output - result = invoke_cli(["list"]) + # test SQLMeshError in list command + mock_list = mocker.patch("sqlmesh_dbt.operations.DbtOperations.list_") + mock_list.side_effect = SQLMeshError("List command error") - assert result.exit_code == 1 - assert "Error: List command error" in result.output - assert "Traceback" not in result.output + result = invoke_cli(["list"]) - with patch("sqlmesh_dbt.cli.create") as mock_create: - mock_create.side_effect = SQLMeshError("Failed to load project") + assert result.exit_code == 1 + assert "Error: List command error" in result.output + assert "Traceback" not in result.output - # use without subcommand - result = invoke_cli(["--profile", "jaffle_shop"]) + # test SQLMeshError in main command wuthout subcommand + mock_create = mocker.patch("sqlmesh_dbt.cli.create") + mock_create.side_effect = SQLMeshError("Failed to load project") + result = invoke_cli(["--profile", "jaffle_shop"]) - assert result.exit_code == 1 - assert "Error: Failed to load project" in result.output - assert "Traceback" not in result.output + assert result.exit_code == 1 + assert "Error: Failed to load project" in result.output + assert "Traceback" not in result.output + mocker.stopall() - with patch("sqlmesh_dbt.operations.DbtOperations.run") as mock_run: - mock_run.side_effect = SQLMeshError("Error with selector") + # test error with select option + mock_run_select = mocker.patch("sqlmesh_dbt.operations.DbtOperations.run") + mock_run_select.side_effect = SQLMeshError("Error with selector") - # with select option - result = invoke_cli(["run", "--select", "model1"]) + result = invoke_cli(["run", "--select", "model1"]) - assert result.exit_code == 1 - assert "Error: Error with selector" in result.output - assert "Traceback" not in result.output + assert result.exit_code == 1 + assert "Error: Error with selector" in result.output + assert "Traceback" not in result.output From dad35a86c4bd20556481c7c380934282ae5e0faf Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:12:57 +0300 Subject: [PATCH 4/4] fix typo --- tests/dbt/cli/test_global_flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dbt/cli/test_global_flags.py b/tests/dbt/cli/test_global_flags.py index 38a155ffc3..66dee7236c 100644 --- a/tests/dbt/cli/test_global_flags.py +++ b/tests/dbt/cli/test_global_flags.py @@ -74,7 +74,7 @@ def test_run_error_handler( assert "Error: List command error" in result.output assert "Traceback" not in result.output - # test SQLMeshError in main command wuthout subcommand + # test SQLMeshError in main command without subcommand mock_create = mocker.patch("sqlmesh_dbt.cli.create") mock_create.side_effect = SQLMeshError("Failed to load project") result = invoke_cli(["--profile", "jaffle_shop"])