Skip to content

Commit b0bc176

Browse files
authored
fix: replace ast.Str and drop astor (#5850)
Signed-off-by: lafirm <136463254+lafirm@users.noreply.github.com>
1 parent 13787d7 commit b0bc176

6 files changed

Lines changed: 138 additions & 15 deletions

File tree

docs/prerequisites.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This page describes the system prerequisites needed to run SQLMesh and provides
44

55
## SQLMesh prerequisites
66

7-
You'll need Python 3.8 or higher to use SQLMesh. You can check your python version by running the following command:
7+
You'll need Python 3.9 or higher to use SQLMesh. You can check your python version by running the following command:
88
```bash
99
python3 --version
1010
```

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ authors = [{ name = "SQLMesh Contributors" }]
77
license = { file = "LICENSE" }
88
requires-python = ">= 3.9"
99
dependencies = [
10-
"astor",
1110
"click",
1211
"croniter",
1312
"duckdb>=0.10.0,!=0.10.3",
@@ -202,7 +201,6 @@ disable_error_code = "annotation-unchecked"
202201
[[tool.mypy.overrides]]
203202
module = [
204203
"api.*",
205-
"astor.*",
206204
"IPython.*",
207205
"hyperscript.*",
208206
"py.*",

sqlmesh/core/model/common.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import typing as t
55
from pathlib import Path
66

7-
from astor import to_source
87
from difflib import get_close_matches
98
from sqlglot import exp
109
from sqlglot.helper import ensure_list
@@ -387,7 +386,7 @@ def get_first_arg(keyword_arg_name: str) -> t.Any:
387386
)
388387

389388
try:
390-
expression = to_source(first_arg)
389+
expression = ast.unparse(t.cast(ast.expr, first_arg))
391390
return eval(expression, env, local_env)
392391
except Exception:
393392
if strict_resolution:
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Re-normalize python_env payloads using ast.unparse after dropping astor.
2+
3+
SQLMesh previously used the third-party `astor` library to serialise Python
4+
function source code (`normalize_source`). That library has been replaced with
5+
the stdlib `ast.unparse`, which produces subtly different text for the same
6+
AST (e.g. `lambda : x` → `lambda: x`, condensed multi-line signatures, etc.).
7+
8+
Because `python_env` payloads are included in each snapshot's `data_hash`,
9+
any model that contains Python code (Python models, SQL models with Python
10+
macros/signals) would otherwise appear as *Directly Modified* after the upgrade,
11+
potentially triggering a full backfill.
12+
13+
This migration re-normalises every stored `Executable` payload of
14+
`kind == "definition"` via `ast.unparse(ast.parse(payload))`. The
15+
subsequent `_migrate_rows` pass then recomputes fingerprints from the updated
16+
payloads so that they match what the current code produces when loading models
17+
from disk. The migrated snapshots are flagged `migrated = True`, so no
18+
unexpected backfills are scheduled.
19+
"""
20+
21+
import ast
22+
import json
23+
24+
from sqlglot import exp
25+
26+
from sqlmesh.utils.migration import index_text_type, blob_text_type
27+
28+
29+
def migrate_schemas(engine_adapter, schema, **kwargs): # type: ignore
30+
pass
31+
32+
33+
def migrate_rows(engine_adapter, schema, **kwargs): # type: ignore
34+
import pandas as pd
35+
36+
snapshots_table = "_snapshots"
37+
if schema:
38+
snapshots_table = f"{schema}.{snapshots_table}"
39+
40+
index_type = index_text_type(engine_adapter.dialect)
41+
blob_type = blob_text_type(engine_adapter.dialect)
42+
43+
new_snapshots = []
44+
migration_needed = False
45+
46+
for (
47+
name,
48+
identifier,
49+
version,
50+
snapshot,
51+
kind_name,
52+
updated_ts,
53+
unpaused_ts,
54+
ttl_ms,
55+
unrestorable,
56+
forward_only,
57+
dev_version,
58+
fingerprint,
59+
) in engine_adapter.fetchall(
60+
exp.select(
61+
"name",
62+
"identifier",
63+
"version",
64+
"snapshot",
65+
"kind_name",
66+
"updated_ts",
67+
"unpaused_ts",
68+
"ttl_ms",
69+
"unrestorable",
70+
"forward_only",
71+
"dev_version",
72+
"fingerprint",
73+
).from_(snapshots_table),
74+
quote_identifiers=True,
75+
):
76+
parsed_snapshot = json.loads(snapshot)
77+
python_env = parsed_snapshot["node"].get("python_env") or {}
78+
for executable in python_env.values():
79+
if executable.get("kind") != "definition":
80+
continue
81+
new_payload = ast.unparse(ast.parse(executable["payload"])).strip()
82+
if new_payload != executable["payload"]:
83+
executable["payload"] = new_payload
84+
migration_needed = True
85+
86+
new_snapshots.append(
87+
{
88+
"name": name,
89+
"identifier": identifier,
90+
"version": version,
91+
"snapshot": json.dumps(parsed_snapshot),
92+
"kind_name": kind_name,
93+
"updated_ts": updated_ts,
94+
"unpaused_ts": unpaused_ts,
95+
"ttl_ms": ttl_ms,
96+
"unrestorable": unrestorable,
97+
"forward_only": forward_only,
98+
"dev_version": dev_version,
99+
"fingerprint": fingerprint,
100+
}
101+
)
102+
103+
if migration_needed and new_snapshots:
104+
engine_adapter.delete_from(snapshots_table, "TRUE")
105+
106+
engine_adapter.insert_append(
107+
snapshots_table,
108+
pd.DataFrame(new_snapshots),
109+
target_columns_to_types={
110+
"name": exp.DataType.build(index_type),
111+
"identifier": exp.DataType.build(index_type),
112+
"version": exp.DataType.build(index_type),
113+
"snapshot": exp.DataType.build(blob_type),
114+
"kind_name": exp.DataType.build(index_type),
115+
"updated_ts": exp.DataType.build("bigint"),
116+
"unpaused_ts": exp.DataType.build("bigint"),
117+
"ttl_ms": exp.DataType.build("bigint"),
118+
"unrestorable": exp.DataType.build("boolean"),
119+
"forward_only": exp.DataType.build("boolean"),
120+
"dev_version": exp.DataType.build(index_type),
121+
"fingerprint": exp.DataType.build(blob_type),
122+
},
123+
)

sqlmesh/utils/metaprogramming.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
from numbers import Number
1818
from pathlib import Path
1919

20-
from astor import to_source
21-
2220
from sqlmesh.core import constants as c
2321
from sqlmesh.utils import format_exception, unique
2422
from sqlmesh.utils.errors import SQLMeshError
@@ -267,14 +265,19 @@ def normalize_source(obj: t.Any) -> str:
267265

268266
# remove docstrings
269267
body = node.body
270-
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Str):
268+
if (
269+
body
270+
and isinstance(body[0], ast.Expr)
271+
and isinstance(body[0].value, ast.Constant)
272+
and isinstance(body[0].value.value, str)
273+
):
271274
node.body = body[1:]
272275

273276
# remove function return type annotation
274277
if isinstance(node, ast.FunctionDef):
275278
node.returns = None
276279

277-
return to_source(root_node).strip()
280+
return ast.unparse(root_node).strip()
278281

279282

280283
def build_env(

tests/utils/test_metaprogramming.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
import typing as t
23
from contextlib import contextmanager
34
from dataclasses import dataclass
@@ -50,7 +51,7 @@ def test_print_exception(mocker: MockerFixture):
5051
except Exception as ex:
5152
print_exception(ex, test_env, out_mock)
5253

53-
expected_message = r""" File ".*?.tests.utils.test_metaprogramming\.py", line 49, in test_print_exception
54+
expected_message = r""" File ".*?.tests.utils.test_metaprogramming\.py", line 50, in test_print_exception
5455
eval\("test_fun\(\)", env\).*
5556
5657
File '/test/path.py' \(or imported file\), line 2, in test_fun
@@ -220,8 +221,7 @@ def closure() -> int:
220221
def test_normalize_source() -> None:
221222
assert (
222223
normalize_source(main_func)
223-
== """def main_func(y: int, foo=exp.true(), *, bar=expressions.Literal.number(1) + 2
224-
):
224+
== """def main_func(y: int, foo=exp.true(), *, bar=expressions.Literal.number(1) + 2):
225225
sqlglot.parse_one('1')
226226
MyClass(47)
227227
DataClass(x=y)
@@ -271,8 +271,7 @@ def test_serialize_env() -> None:
271271
name="main_func",
272272
alias="MAIN",
273273
path="test_metaprogramming.py",
274-
payload="""def main_func(y: int, foo=exp.true(), *, bar=expressions.Literal.number(1) + 2
275-
):
274+
payload="""def main_func(y: int, foo=exp.true(), *, bar=expressions.Literal.number(1) + 2):
276275
sqlglot.parse_one('1')
277276
MyClass(47)
278277
DataClass(x=y)
@@ -370,7 +369,8 @@ def sample_context_manager():
370369
"my_lambda": Executable(
371370
name="my_lambda",
372371
path="test_metaprogramming.py",
373-
payload="my_lambda = lambda : print('z')",
372+
# Match normalize_source output across Python versions
373+
payload=ast.unparse(ast.parse("my_lambda = lambda: print('z')")).strip(),
374374
),
375375
"normalize_model_name": Executable(
376376
payload="from sqlmesh.core.dialect import normalize_model_name",

0 commit comments

Comments
 (0)