Skip to content

Commit 7ee6fb7

Browse files
authored
Feat: resolve (blueprint) variables when parsing python deps (#4399)
1 parent ff0783a commit 7ee6fb7

4 files changed

Lines changed: 91 additions & 12 deletions

File tree

docs/concepts/models/python_models.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,18 +241,16 @@ def execute(
241241
context.resolve_table("docs_example.another_dependency")
242242
```
243243

244-
User-defined [global variables](global-variables) can also be used in `resolve_table` calls, as long as the `depends_on` keyword argument is present and contains the required dependencies. This is shown in the following example:
244+
User-defined [global variables](global-variables) or [blueprint variables](#python-model-blueprinting) can also be used in `resolve_table` calls, as shown in the following example (similarly for `blueprint_var()`):
245245

246246
```python linenums="1"
247247
@model(
248248
"@schema_name.test_model2",
249249
kind="FULL",
250250
columns={"id": "INT"},
251-
depends_on=["@schema_name.test_model1"],
252251
)
253252
def execute(context, **kwargs):
254-
schema_name = context.var("schema_name")
255-
table = context.resolve_table(f"{schema_name}.test_model1")
253+
table = context.resolve_table(f"{context.var('schema_name')}.test_model1")
256254
select_query = exp.select("*").from_(table)
257255
return context.fetchdf(select_query)
258256
```

sqlmesh/core/model/common.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def _add_variables_to_python_env(
145145
python_env,
146146
None,
147147
strict_resolution=strict_resolution,
148+
variables=variables,
149+
blueprint_variables=blueprint_variables,
148150
)
149151
used_variables = (used_variables or set()) | python_used_variables
150152

@@ -163,7 +165,11 @@ def _add_variables_to_python_env(
163165

164166

165167
def parse_dependencies(
166-
python_env: t.Dict[str, Executable], entrypoint: t.Optional[str], strict_resolution: bool = True
168+
python_env: t.Dict[str, Executable],
169+
entrypoint: t.Optional[str],
170+
strict_resolution: bool = True,
171+
variables: t.Optional[t.Dict[str, t.Any]] = None,
172+
blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None,
167173
) -> t.Tuple[t.Set[str], t.Set[str]]:
168174
"""
169175
Parses the source of a model function and finds upstream table dependencies
@@ -174,13 +180,29 @@ def parse_dependencies(
174180
entrypoint: The name of the function.
175181
strict_resolution: If true, the arguments of `table` and `resolve_table` calls must
176182
be resolvable at parse time, otherwise an exception will be raised.
183+
variables: The variables available to the python environment.
184+
blueprint_variables: The blueprint variables available to the python environment.
177185
178186
Returns:
179187
A tuple containing the set of upstream table dependencies and the set of referenced variables.
180188
"""
189+
190+
class VariableResolutionContext:
191+
"""This enables calls like `resolve_table` to reference `var()` and `blueprint_var()`."""
192+
193+
@staticmethod
194+
def var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
195+
return (variables or {}).get(var_name.lower(), default)
196+
197+
@staticmethod
198+
def blueprint_var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
199+
return (blueprint_variables or {}).get(var_name.lower(), default)
200+
181201
env = prepare_env(python_env)
202+
local_env = dict.fromkeys(("context", "evaluator"), VariableResolutionContext)
203+
182204
depends_on = set()
183-
variables = set()
205+
used_variables = set()
184206

185207
for executable in python_env.values():
186208
if not executable.is_definition:
@@ -206,7 +228,7 @@ def get_first_arg(keyword_arg_name: str) -> t.Any:
206228

207229
try:
208230
expression = to_source(first_arg)
209-
return eval(expression, env)
231+
return eval(expression, env, local_env)
210232
except Exception:
211233
if strict_resolution:
212234
raise ConfigError(
@@ -217,25 +239,25 @@ def get_first_arg(keyword_arg_name: str) -> t.Any:
217239
if func.value.id == "context" and func.attr in ("table", "resolve_table"):
218240
depends_on.add(get_first_arg("model_name"))
219241
elif func.value.id in ("context", "evaluator") and func.attr == c.VAR:
220-
variables.add(get_first_arg("var_name").lower())
242+
used_variables.add(get_first_arg("var_name").lower())
221243
elif (
222244
isinstance(node, ast.Attribute)
223245
and isinstance(node.value, ast.Name)
224246
and node.value.id in ("context", "evaluator")
225247
and node.attr == c.GATEWAY
226248
):
227249
# Check whether the gateway attribute is referenced.
228-
variables.add(c.GATEWAY)
250+
used_variables.add(c.GATEWAY)
229251
elif isinstance(node, ast.FunctionDef) and node.name == entrypoint:
230-
variables.update(
252+
used_variables.update(
231253
[
232254
arg.arg
233255
for arg in [*node.args.args, *node.args.kwonlyargs]
234256
if arg.arg != "context"
235257
]
236258
)
237259

238-
return depends_on, variables
260+
return depends_on, used_variables
239261

240262

241263
def single_value_or_tuple(values: t.Sequence) -> exp.Identifier | exp.Tuple:

sqlmesh/core/model/definition.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2286,7 +2286,13 @@ def create_python_model(
22862286
dependencies_unspecified = depends_on is None
22872287

22882288
parsed_depends_on, referenced_variables = (
2289-
parse_dependencies(python_env, entrypoint, strict_resolution=dependencies_unspecified)
2289+
parse_dependencies(
2290+
python_env,
2291+
entrypoint,
2292+
strict_resolution=dependencies_unspecified,
2293+
variables=variables,
2294+
blueprint_variables=blueprint_variables,
2295+
)
22902296
if python_env is not None
22912297
else (set(), set())
22922298
)

tests/core/test_model.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9861,3 +9861,56 @@ def test_model(context, **kwargs):
98619861
model_executable_str = python_model.render_definition()[1].sql()
98629862
# Make sure the file path is included in the render definition
98639863
assert "# tests/core/test_model.py" in model_executable_str
9864+
9865+
9866+
def test_resolve_interpolated_variables_when_parsing_python_deps():
9867+
@model(
9868+
name="bla.test_interpolate_var_in_dep_py",
9869+
kind="full",
9870+
columns={'"col"': "int"},
9871+
)
9872+
def unimportant_testing_model(context, **kwargs):
9873+
table1 = context.resolve_table(f"{context.var('schema_name')}.table_name")
9874+
table2 = context.resolve_table(f"{context.blueprint_var('schema_name')}.table_name")
9875+
9876+
return context.fetchdf(exp.select("*").from_(table))
9877+
9878+
m = model.get_registry()["bla.test_interpolate_var_in_dep_py"].model(
9879+
module_path=Path("."),
9880+
path=Path("."),
9881+
variables={"schema_name": "foo"},
9882+
blueprint_variables={"schema_name": "baz"},
9883+
)
9884+
9885+
assert m.depends_on == {'"foo"."table_name"', '"baz"."table_name"'}
9886+
assert m.python_env.get(c.SQLMESH_VARS) == Executable.value({"schema_name": "foo"})
9887+
assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value({"schema_name": "baz"})
9888+
9889+
@macro()
9890+
def unimportant_testing_macro(evaluator, *projections):
9891+
evaluator.var(f"{evaluator.var('selector')}_variable")
9892+
evaluator.var(f"{evaluator.blueprint_var('selector')}_variable")
9893+
9894+
return exp.select(*[f'{p} AS "{p}"' for p in projections])
9895+
9896+
m = load_sql_based_model(
9897+
d.parse(
9898+
"""
9899+
MODEL (
9900+
name bla.test_interpolate_var_in_dep_sql
9901+
);
9902+
9903+
@unimportant_testing_macro();
9904+
9905+
SELECT
9906+
1 AS c
9907+
""",
9908+
),
9909+
variables={"selector": "bla", "bla_variable": 1, "baz_variable": 2},
9910+
blueprint_variables={"selector": "baz"},
9911+
)
9912+
9913+
assert m.python_env.get(c.SQLMESH_VARS) == Executable.value(
9914+
{"selector": "bla", "bla_variable": 1, "baz_variable": 2}
9915+
)
9916+
assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value({"selector": "baz"})

0 commit comments

Comments
 (0)