diff --git a/.changes/unreleased/Fixes-20241017-174435.yaml b/.changes/unreleased/Fixes-20241017-174435.yaml new file mode 100644 index 0000000..50a794a --- /dev/null +++ b/.changes/unreleased/Fixes-20241017-174435.yaml @@ -0,0 +1,6 @@ +kind: Fixes +body: catch jinja type errors during render phase +time: 2024-10-17T17:44:35.736867874-03:00 +custom: + Author: devmessias + Issue: "206" diff --git a/dbt_common/clients/jinja.py b/dbt_common/clients/jinja.py index 2cee922..c90a7ef 100644 --- a/dbt_common/clients/jinja.py +++ b/dbt_common/clients/jinja.py @@ -46,6 +46,7 @@ MaterializationArgError, JinjaRenderingError, UndefinedCompilationError, + DbtRuntimeTypeError, ) from dbt_common.exceptions.macros import MacroReturn, UndefinedMacroError, CaughtMacroError @@ -125,6 +126,14 @@ def _compile(self, source: str, filename: str) -> CodeType: return super()._compile(source, filename) # type: ignore +class RenderCallJinjaTypeError(jinja2.TemplateError): + def __init__( + self, + message: str, + ) -> None: + super().__init__(message) + + class MacroFuzzTemplate(jinja2.nativetypes.NativeTemplate): environment_class = MacroFuzzEnvironment # type: ignore @@ -159,6 +168,8 @@ def render(self, *args: Any, **kwargs: Any) -> Any: return self.environment_class.concat( # type: ignore self.root_render_func(ctx) # type: ignore ) + except TypeError as e: + raise RenderCallJinjaTypeError(str(e)) from e except Exception: return self.environment.handle_exception() @@ -531,6 +542,8 @@ def catch_jinja(node: Optional[_NodeProtocol] = None) -> Iterator[None]: raise CompilationError(str(e), node) from e except jinja2.exceptions.UndefinedError as e: raise UndefinedMacroError(str(e), node) from e + except RenderCallJinjaTypeError as e: + raise DbtRuntimeTypeError(str(e), node) from e except CompilationError as exc: exc.add_node(node) raise diff --git a/dbt_common/exceptions/base.py b/dbt_common/exceptions/base.py index fdabd86..8782f68 100644 --- a/dbt_common/exceptions/base.py +++ b/dbt_common/exceptions/base.py @@ -272,3 +272,11 @@ def __str__(self, prefix: str = "! ") -> str: if len(self.cmd) == 0: return f"{self.msg}: No arguments given" return f'{self.msg}: "{self.cmd[0]}"' + + +class DbtRuntimeTypeError(DbtRuntimeError): + MESSAGE = "Type Error" + + @property + def type(self): + return "Type" diff --git a/tests/unit/test_jinja.py b/tests/unit/test_jinja.py index e839296..506d93a 100644 --- a/tests/unit/test_jinja.py +++ b/tests/unit/test_jinja.py @@ -2,7 +2,7 @@ from dbt_common.clients._jinja_blocks import BlockTag from dbt_common.clients.jinja import extract_toplevel_blocks, get_template, render_template -from dbt_common.exceptions import CompilationError +from dbt_common.exceptions import CompilationError, DbtRuntimeTypeError class TestBlockLexer(unittest.TestCase): @@ -447,6 +447,44 @@ def test_if_endfor_newlines(self) -> None: ) +class TestRenderExceptions(unittest.TestCase): + def _dummy_config(self, something: str) -> int: + return int(something == "foo") + + def test_no_type_error(self) -> None: + jinja_string = 'select {{config(something="foo")}} + {{number}} as result' + ctx = {"config": self._dummy_config, "number": 1} + + template = get_template(jinja_string, ctx) + rendered = render_template(template, ctx) + assert "select 1 + 1 as result" == rendered + + def test_type_error(self) -> None: + jinja_string = 'select {{config(something=-"foo")}} as result' + ctx = {"config": self._dummy_config} + + template = get_template(jinja_string, ctx) + with self.assertRaises(DbtRuntimeTypeError): + render_template(template, ctx) + + def test_type_error_iter_none(self) -> None: + jinja_string = """ + {% set mylist = None %} + + {% for item in mylist %} + + select '{{ item }}' as my_item + {{ 'union all' if not loop.last }} + + {% endfor %} + """ + ctx = {"config": self._dummy_config} + + template = get_template(jinja_string, ctx) + with self.assertRaises(DbtRuntimeTypeError): + render_template(template, ctx) + + bar_block = """{% mytype bar %} {# a comment that inside it has