Repository: TobikoData/sqlmesh
Pull request #3025 (2024-08-19 22:23:54)

Fix: serialization regression with sqlmesh.core.dialect.normalize_model_name
`sqlmesh.core.dialect.normalize_model_name` uses lru_cache. 

https://github.com/z3z1ma/sqlmesh/blob/0b3aeac041b280cb79bee51613648a9fce4c5646/sqlmesh/core/dialect.py#L960-L981

Serialization fails on this object when calling inspect.getfile
This is a common import. This bug would've been introduced by #3008 as I don't think there was an lru cache there before

I added a failing test by simply referencing the function in question in the main_func in test_metaprogramming

<img width="869" alt="image" src="https://github.com/user-attachments/assets/7e431fe0-7bf6-4dd1-b007-a2edcb6a9c2b">

---

I fixed it by using `inspect.unwrap` to safely unwrap to func specifically in the getfile call. This returns the correct file path.
https://docs.python.org/3/library/inspect.html#inspect.unwrap

I have confirmed tests pass and this works on my production project too (whereas before it was bugged/un-runnable). 

---

In the future, due to the fact that getfile can throw at all, we should probably add a try except unless we think our builtin guard is good enough (though this obviously slipped through).


Patch:
diff --git a/setup.py b/setup.py
index fb8a2731..a3ff28c5 100644
--- a/setup.py
+++ b/setup.py
@@ -47,7 +47,7 @@ setup(
         "requests",
         "rich[jupyter]",
         "ruamel.yaml",
-        "sqlglot[rs]~=25.14.0",
+        "sqlglot[rs]~=25.15.0",
     ],
     extras_require={
         "bigquery": [
diff --git a/sqlmesh/utils/metaprogramming.py b/sqlmesh/utils/metaprogramming.py
index 786e847d..e1ad299c 100644
--- a/sqlmesh/utils/metaprogramming.py
+++ b/sqlmesh/utils/metaprogramming.py
@@ -351,7 +351,9 @@ def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable
 
             # We can't call getfile on built-in callables
             # https://docs.python.org/3/library/inspect.html#inspect.getfile
-            file_path = Path(inspect.getfile(v)) if not inspect.isbuiltin(v) else None
+            file_path = (
+                Path(inspect.getfile(inspect.unwrap(v))) if not inspect.isbuiltin(v) else None
+            )
 
             if _is_relative_to(file_path, path):
                 serialized[k] = Executable(


Test patch:
diff --git a/tests/utils/test_metaprogramming.py b/tests/utils/test_metaprogramming.py
index f236a752..38fdbeaf 100644
--- a/tests/utils/test_metaprogramming.py
+++ b/tests/utils/test_metaprogramming.py
@@ -9,6 +9,7 @@ from pytest_mock.plugin import MockerFixture
 from sqlglot.expressions import to_table
 
 import tests.utils.test_date as test_date
+from sqlmesh.core.dialect import normalize_model_name
 from sqlmesh.core import constants as c
 from sqlmesh.utils.errors import SQLMeshError
 from sqlmesh.utils.metaprogramming import (
@@ -42,7 +43,7 @@ def test_print_exception(mocker: MockerFixture):
 
     expected_message = f"""Traceback (most recent call last):
 
-  File "{__file__}", line 39, in test_print_exception
+  File "{__file__}", line 40, in test_print_exception
     eval("test_fun()", env)
 
   File "<string>", line 1, in <module>
@@ -109,6 +110,7 @@ def main_func(y: int) -> int:
     MyClass()
     DataClass(x=y)
     noop_metadata()
+    normalize_model_name("test")
 
     def closure(z: int) -> int:
         return z + Z
@@ -123,6 +125,7 @@ def test_func_globals() -> None:
         "DataClass": DataClass,
         "MyClass": MyClass,
         "noop_metadata": noop_metadata,
+        "normalize_model_name": normalize_model_name,
         "other_func": other_func,
         "sqlglot": sqlglot,
     }
@@ -155,6 +158,7 @@ def test_normalize_source() -> None:
     MyClass()
     DataClass(x=y)
     noop_metadata()
+    normalize_model_name('test')
 
     def closure(z: int):
         return z + Z
@@ -195,6 +199,7 @@ def test_serialize_env() -> None:
     MyClass()
     DataClass(x=y)
     noop_metadata()
+    normalize_model_name('test')
 
     def closure(z: int):
         return z + Z
@@ -252,6 +257,10 @@ class DataClass:
     return None""",
             is_metadata=True,
         ),
+        "normalize_model_name": Executable(
+            payload="from sqlmesh.core.dialect import normalize_model_name",
+            kind=ExecutableKind.IMPORT,
+        ),
         "other_func": Executable(
             name="other_func",
             path="test_metaprogramming.py",