Skip to content

Commit 7168a88

Browse files
committed
Feat: resolve (blueprint) variables when parsing python deps
1 parent a696391 commit 7168a88

4 files changed

Lines changed: 95 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: 33 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,33 @@ 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 model.
184+
blueprint_variables: The blueprint variables available to the model.
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 = {
203+
**env,
204+
"context": VariableResolutionContext,
205+
"evaluator": VariableResolutionContext,
206+
}
207+
182208
depends_on = set()
183-
variables = set()
209+
used_variables = set()
184210

185211
for executable in python_env.values():
186212
if not executable.is_definition:
@@ -206,7 +232,7 @@ def get_first_arg(keyword_arg_name: str) -> t.Any:
206232

207233
try:
208234
expression = to_source(first_arg)
209-
return eval(expression, env)
235+
return eval(expression, env, local_env)
210236
except Exception:
211237
if strict_resolution:
212238
raise ConfigError(
@@ -217,25 +243,25 @@ def get_first_arg(keyword_arg_name: str) -> t.Any:
217243
if func.value.id == "context" and func.attr in ("table", "resolve_table"):
218244
depends_on.add(get_first_arg("model_name"))
219245
elif func.value.id in ("context", "evaluator") and func.attr == c.VAR:
220-
variables.add(get_first_arg("var_name").lower())
246+
used_variables.add(get_first_arg("var_name").lower())
221247
elif (
222248
isinstance(node, ast.Attribute)
223249
and isinstance(node.value, ast.Name)
224250
and node.value.id in ("context", "evaluator")
225251
and node.attr == c.GATEWAY
226252
):
227253
# Check whether the gateway attribute is referenced.
228-
variables.add(c.GATEWAY)
254+
used_variables.add(c.GATEWAY)
229255
elif isinstance(node, ast.FunctionDef) and node.name == entrypoint:
230-
variables.update(
256+
used_variables.update(
231257
[
232258
arg.arg
233259
for arg in [*node.args.args, *node.args.kwonlyargs]
234260
if arg.arg != "context"
235261
]
236262
)
237263

238-
return depends_on, variables
264+
return depends_on, used_variables
239265

240266

241267
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
@@ -9725,3 +9725,56 @@ def test_model(context, **kwargs):
97259725
model_executable_str = python_model.render_definition()[1].sql()
97269726
# Make sure the file path is included in the render definition
97279727
assert "# tests/core/test_model.py" in model_executable_str
9728+
9729+
9730+
def test_resolve_interpolated_variables_when_parsing_python_deps():
9731+
@model(
9732+
name="bla.test_interpolate_var_in_dep_py",
9733+
kind="full",
9734+
columns={'"col"': "int"},
9735+
)
9736+
def unimportant_testing_model(context, **kwargs):
9737+
table1 = context.resolve_table(f"{context.var('schema_name')}.table_name")
9738+
table2 = context.resolve_table(f"{context.blueprint_var('schema_name')}.table_name")
9739+
9740+
return context.fetchdf(exp.select("*").from_(table))
9741+
9742+
m = model.get_registry()["bla.test_interpolate_var_in_dep_py"].model(
9743+
module_path=Path("."),
9744+
path=Path("."),
9745+
variables={"schema_name": "foo"},
9746+
blueprint_variables={"schema_name": "baz"},
9747+
)
9748+
9749+
assert m.depends_on == {'"foo"."table_name"', '"baz"."table_name"'}
9750+
assert m.python_env.get(c.SQLMESH_VARS) == Executable.value({"schema_name": "foo"})
9751+
assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value({"schema_name": "baz"})
9752+
9753+
@macro()
9754+
def unimportant_testing_macro(evaluator, *projections):
9755+
evaluator.var(f"{evaluator.var('selector')}_variable")
9756+
evaluator.var(f"{evaluator.blueprint_var('selector')}_variable")
9757+
9758+
return exp.select(*[f'{p} AS "{p}"' for p in projections])
9759+
9760+
m = load_sql_based_model(
9761+
d.parse(
9762+
"""
9763+
MODEL (
9764+
name bla.test_interpolate_var_in_dep_sql
9765+
);
9766+
9767+
@unimportant_testing_macro();
9768+
9769+
SELECT
9770+
1 AS c
9771+
""",
9772+
),
9773+
variables={"selector": "bla", "bla_variable": 1, "baz_variable": 2},
9774+
blueprint_variables={"selector": "baz"},
9775+
)
9776+
9777+
assert m.python_env.get(c.SQLMESH_VARS) == Executable.value(
9778+
{"selector": "bla", "bla_variable": 1, "baz_variable": 2}
9779+
)
9780+
assert m.python_env.get(c.SQLMESH_BLUEPRINT_VARS) == Executable.value({"selector": "baz"})

0 commit comments

Comments
 (0)