sqlmesh.utils.jinja
1from __future__ import annotations 2 3import base64 4import importlib 5import json 6import re 7import typing as t 8import zlib 9from collections import defaultdict 10from enum import Enum 11from sys import exc_info 12from traceback import walk_tb 13 14from jinja2 import Environment, Template, nodes, UndefinedError 15from jinja2.runtime import Macro 16from sqlglot import Dialect, Parser, TokenType 17from sqlglot.expressions import Expression 18 19from sqlmesh.core import constants as c 20from sqlmesh.core import dialect as d 21from sqlmesh.utils import AttributeDict 22from sqlmesh.utils.pydantic import PRIVATE_FIELDS, PydanticModel, field_serializer, field_validator 23from sqlmesh.utils.metaprogramming import SqlValue 24 25 26if t.TYPE_CHECKING: 27 CallNames = t.Tuple[t.Tuple[str, ...], t.Union[nodes.Call, nodes.Getattr]] 28 29SQLMESH_JINJA_PACKAGE = "sqlmesh.utils.jinja" 30 31 32def b64decode(value: t.Union[str, bytes]) -> str: 33 """Decode a base64-encoded value and return it as UTF-8 text. 34 35 Intended for base64-encoded string/JSON secrets (for example a service-account 36 key stored in an environment variable), not arbitrary binary payloads. 37 """ 38 decoded = value.encode("utf-8") if isinstance(value, str) else value 39 return base64.b64decode(decoded).decode("utf-8") 40 41 42def b64encode(value: t.Union[str, bytes]) -> str: 43 """Base64-encode a value and return the encoding as UTF-8 text. 44 45 The input is treated as UTF-8 text, mirroring ``b64decode``; it is intended for 46 string/JSON secrets rather than arbitrary binary payloads. 47 """ 48 encoded = value.encode("utf-8") if isinstance(value, str) else value 49 return base64.b64encode(encoded).decode("utf-8") 50 51 52def create_builtin_filters() -> t.Dict[str, t.Callable]: 53 return { 54 "b64decode": b64decode, 55 "b64encode": b64encode, 56 } 57 58 59def environment(**kwargs: t.Any) -> Environment: 60 extensions = kwargs.pop("extensions", []) 61 extensions.append("jinja2.ext.do") 62 extensions.append("jinja2.ext.loopcontrols") 63 env = Environment(extensions=extensions, **kwargs) 64 env.filters.update(create_builtin_filters()) 65 return env 66 67 68ENVIRONMENT = environment() 69 70 71class MacroReference(PydanticModel, frozen=True): 72 package: t.Optional[str] = None 73 name: str 74 75 @property 76 def reference(self) -> str: 77 if self.package is None: 78 return self.name 79 return ".".join((self.package, self.name)) 80 81 def __str__(self) -> str: 82 return self.reference 83 84 85class MacroInfo(PydanticModel): 86 """Class to hold macro and its calls""" 87 88 definition: str 89 depends_on: t.List[MacroReference] 90 is_top_level: bool = False 91 92 93class MacroReturnVal(Exception): 94 def __init__(self, val: t.Any): 95 self.value = val 96 97 98class MacroExtractor(Parser): 99 def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: 100 """Extract a dictionary of macro definitions from a jinja string. 101 102 Args: 103 jinja: The jinja string to extract from. 104 dialect: The dialect of SQL. 105 106 Returns: 107 A dictionary of macro name to macro definition. 108 """ 109 self.reset() 110 self.sql = jinja 111 self._tokens = Dialect.get_or_raise(dialect).tokenize(jinja) 112 113 # guard for older sqlglot versions (before 30.0.3) 114 if hasattr(self, "_tokens_size"): 115 # keep the cached length in sync 116 self._tokens_size = len(self._tokens) 117 self._index = -1 118 self._advance() 119 120 macros: t.Dict[str, MacroInfo] = {} 121 122 while self._curr: 123 if self._curr.token_type == TokenType.BLOCK_START: 124 macro_start = self._curr 125 elif self._tag == "MACRO" and self._next: 126 name = self._next.text 127 while self._curr and self._curr.token_type != TokenType.BLOCK_END: 128 self._advance() 129 130 while self._curr and self._tag != "ENDMACRO": 131 self._advance() 132 133 macro_str = self._find_sql(macro_start, self._next) 134 macros[name] = MacroInfo( 135 definition=macro_str, 136 depends_on=list(extract_macro_references_and_variables(macro_str)[0]), 137 ) 138 139 self._advance() 140 141 return macros 142 143 def _advance(self, times: int = 1) -> None: 144 super()._advance(times) 145 self._tag = ( 146 self._curr.text.upper() 147 if self._curr and self._prev and self._prev.token_type == TokenType.BLOCK_START 148 else "" 149 ) 150 151 152def call_name(node: nodes.Expr) -> t.Tuple[str, ...]: 153 if isinstance(node, nodes.Name): 154 return (node.name,) 155 if isinstance(node, nodes.Const): 156 return (f"'{node.value}'",) 157 if isinstance(node, nodes.Getattr): 158 return call_name(node.node) + (node.attr,) 159 if isinstance(node, (nodes.Getitem, nodes.Call)): 160 return call_name(node.node) 161 return () 162 163 164def render_jinja(query: str, methods: t.Optional[t.Dict[str, t.Any]] = None) -> str: 165 return ENVIRONMENT.from_string(query).render(methods or {}) 166 167 168def find_call_names(node: nodes.Node, vars_in_scope: t.Set[str]) -> t.Iterator[CallNames]: 169 vars_in_scope = vars_in_scope.copy() 170 for child_node in node.iter_child_nodes(): 171 if "target" in child_node.fields: 172 # For nodes with assignment targets (Assign, AssignBlock, For, Import), 173 # the target name could shadow a reference in the right hand side. 174 # So we need to process the RHS before adding the target to scope. 175 # For example: {% set model = model.path %} should track model.path. 176 yield from find_call_names(child_node, vars_in_scope) 177 178 target = getattr(child_node, "target") 179 if isinstance(target, nodes.Name): 180 vars_in_scope.add(target.name) 181 elif isinstance(target, nodes.Tuple): 182 for item in target.items: 183 if isinstance(item, nodes.Name): 184 vars_in_scope.add(item.name) 185 elif isinstance(child_node, nodes.Macro): 186 for arg in child_node.args: 187 vars_in_scope.add(arg.name) 188 elif isinstance(child_node, nodes.Call) or ( 189 isinstance(child_node, nodes.Getattr) and not isinstance(child_node.node, nodes.Getattr) 190 ): 191 name = call_name(child_node) 192 if name[0][0] != "'" and name[0] not in vars_in_scope: 193 yield (name, child_node) 194 195 if "target" not in child_node.fields: 196 yield from find_call_names(child_node, vars_in_scope) 197 198 199def extract_call_names( 200 jinja_str: str, cache: t.Optional[t.Dict[str, t.Tuple[t.List[CallNames], bool]]] = None 201) -> t.List[CallNames]: 202 def parse() -> t.List[CallNames]: 203 return list(find_call_names(ENVIRONMENT.parse(jinja_str), set())) 204 205 if cache is not None: 206 key = str(zlib.crc32(jinja_str.encode("utf-8"))) 207 if key in cache: 208 names = cache[key][0] 209 else: 210 names = parse() 211 cache[key] = (names, True) 212 return names 213 return parse() 214 215 216def is_variable_node(n: nodes.Node) -> bool: 217 return ( 218 isinstance(n, nodes.Call) 219 and isinstance(n.node, nodes.Name) 220 and n.node.name in (c.VAR, c.BLUEPRINT_VAR) 221 ) 222 223 224def extract_macro_references_and_variables( 225 *jinja_strs: str, 226) -> t.Tuple[t.Set[MacroReference], t.Set[str]]: 227 macro_references = set() 228 variables = set() 229 for jinja_str in jinja_strs: 230 for call_name, node in extract_call_names(jinja_str): 231 if call_name[0] in (c.VAR, c.BLUEPRINT_VAR): 232 if not is_variable_node(node): 233 # Find the variable node which could be nested 234 for n in node.find_all(nodes.Call): 235 if is_variable_node(n): 236 node = n 237 break 238 else: 239 raise ValueError(f"Could not find variable name in {jinja_str}") 240 node = t.cast(nodes.Call, node) 241 args = [jinja_call_arg_name(arg) for arg in node.args] 242 if args and args[0]: 243 variables.add(args[0].lower()) 244 elif call_name[0] == c.GATEWAY: 245 variables.add(c.GATEWAY) 246 elif len(call_name) == 1: 247 macro_references.add(MacroReference(name=call_name[0])) 248 elif len(call_name) == 2: 249 macro_references.add(MacroReference(package=call_name[0], name=call_name[1])) 250 return macro_references, variables 251 252 253def sort_dict_recursive( 254 item: t.Dict[str, t.Any], 255) -> t.Dict[str, t.Any]: 256 sorted_dict: t.Dict[str, t.Any] = {} 257 for k, v in sorted(item.items()): 258 if isinstance(v, list): 259 sorted_dict[k] = sorted(v) 260 elif isinstance(v, dict): 261 sorted_dict[k] = sort_dict_recursive(v) 262 else: 263 sorted_dict[k] = v 264 return sorted_dict 265 266 267JinjaGlobalAttribute = t.Union[str, int, float, bool, AttributeDict] 268 269 270class JinjaMacroRegistry(PydanticModel): 271 """Registry for Jinja macros. 272 273 Args: 274 packages: The mapping from package name to a collection of macro definitions. 275 root_macros: The collection of top-level macro definitions. 276 global_objs: The global objects. 277 create_builtins_module: The name of a module which defines the `create_builtins` factory 278 function that will be used to construct builtin variables and functions. 279 root_package_name: The name of the root package. If specified root macros will be available 280 as both `root_package_name.macro_name` and `macro_name`. 281 top_level_packages: The list of top-level packages. Macros in this packages will be available 282 as both `package_name.macro_name` and `macro_name`. 283 """ 284 285 packages: t.Dict[str, t.Dict[str, MacroInfo]] = {} 286 root_macros: t.Dict[str, MacroInfo] = {} 287 global_objs: t.Dict[str, JinjaGlobalAttribute] = {} 288 create_builtins_module: t.Optional[str] = SQLMESH_JINJA_PACKAGE 289 root_package_name: t.Optional[str] = None 290 top_level_packages: t.List[str] = [] 291 292 _parser_cache: t.Dict[t.Tuple[t.Optional[str], str], Template] = {} 293 _trimmed: bool = False 294 __environment: t.Optional[Environment] = None 295 296 def __getstate__(self) -> t.Dict[t.Any, t.Any]: 297 state = super().__getstate__() 298 private = state[PRIVATE_FIELDS] 299 private["_parser_cache"] = {} 300 private["_JinjaMacroRegistry__environment"] = None 301 return state 302 303 @field_validator("global_objs", mode="before") 304 @classmethod 305 def _validate_global_objs(cls, value: t.Any) -> t.Any: 306 def _normalize(val: t.Any) -> t.Any: 307 if isinstance(val, dict): 308 return AttributeDict({k: _normalize(v) for k, v in val.items()}) 309 if isinstance(val, list): 310 return [_normalize(v) for v in val] 311 if isinstance(val, set): 312 return [_normalize(v) for v in sorted(val)] 313 if isinstance(val, Enum): 314 return val.value 315 return val 316 317 return _normalize(value) 318 319 @field_serializer("global_objs") 320 def _serialize_attribute_dict( 321 self, value: t.Dict[str, JinjaGlobalAttribute] 322 ) -> t.Dict[str, t.Any]: 323 # NOTE: This is called only when used with Pydantic V2. 324 def _convert( 325 val: t.Union[t.Dict[str, JinjaGlobalAttribute], t.Dict[str, t.Any]], 326 ) -> t.Dict[str, t.Any]: 327 return {k: _convert(v) if isinstance(v, AttributeDict) else v for k, v in val.items()} 328 329 return _convert(value) 330 331 @property 332 def trimmed(self) -> bool: 333 return self._trimmed 334 335 def add_macros(self, macros: t.Dict[str, MacroInfo], package: t.Optional[str] = None) -> None: 336 """Adds macros to the target package. 337 338 Args: 339 macros: Macros that should be added. 340 package: The name of the package the given macros belong to. If not specified, the provided 341 macros will be added to the root namespace. 342 """ 343 344 if package is not None: 345 package_macros = self.packages.get(package, {}) 346 package_macros.update(macros) 347 self.packages[package] = package_macros 348 else: 349 self.root_macros.update(macros) 350 351 def add_globals(self, globals: t.Dict[str, JinjaGlobalAttribute]) -> None: 352 """Adds global objects to the registry. 353 354 Args: 355 globals: The global objects that should be added. 356 """ 357 # Keep the registry lightweight when the graph is not needed 358 if not "graph" in self.packages: 359 globals.pop("flat_graph", None) 360 self.global_objs.update(**self._validate_global_objs(globals)) 361 362 def build_macro(self, reference: MacroReference, **kwargs: t.Any) -> t.Optional[t.Callable]: 363 """Builds a Python callable for a macro with the given reference. 364 365 Args: 366 reference: The macro reference. 367 Returns: 368 The macro as a Python callable or None if not found. 369 """ 370 env: Environment = self.build_environment(**kwargs) 371 if reference.package is not None: 372 package = env.globals.get(reference.package, {}) 373 return package.get(reference.name) # type: ignore 374 return env.globals.get(reference.name) # type: ignore 375 376 def build_environment(self, **kwargs: t.Any) -> Environment: 377 """Builds a new Jinja environment based on this registry.""" 378 379 context: t.Dict[str, t.Any] = {} 380 381 root_macros = { 382 name: self._MacroWrapper(name, None, self, context) 383 for name, macro in self.root_macros.items() 384 } 385 386 package_macros: t.Dict[str, t.Any] = defaultdict(AttributeDict) 387 for package_name, macros in self.packages.items(): 388 for macro_name, macro in macros.items(): 389 macro_wrapper = self._MacroWrapper(macro_name, package_name, self, context) 390 package_macros[package_name][macro_name] = macro_wrapper 391 if macro.is_top_level and macro_name not in root_macros: 392 root_macros[macro_name] = macro_wrapper 393 394 top_level_packages = self.top_level_packages.copy() 395 396 if self.root_package_name is not None: 397 package_macros[self.root_package_name].update(root_macros) 398 top_level_packages.append(self.root_package_name) 399 400 env = environment() 401 402 builtin_globals = self._create_builtin_globals(kwargs) 403 for top_level_package_name in top_level_packages: 404 # Make sure that the top-level package doesn't fully override the same builtin package. 405 package_macros[top_level_package_name] = AttributeDict( 406 { 407 **(builtin_globals.pop(top_level_package_name, None) or {}), 408 **(package_macros.get(top_level_package_name) or {}), 409 } 410 ) 411 root_macros.update(package_macros[top_level_package_name]) 412 413 context.update(builtin_globals) 414 context.update(root_macros) 415 context.update(package_macros) 416 context["render"] = lambda input: env.from_string(input).render() 417 418 env.globals.update(context) 419 env.filters.update(self._environment.filters) 420 return env 421 422 def trim( 423 self, dependencies: t.Iterable[MacroReference], package: t.Optional[str] = None 424 ) -> JinjaMacroRegistry: 425 """Trims the registry by keeping only macros with given references and their transitive dependencies. 426 427 Args: 428 dependencies: References to macros that should be kept. 429 package: The name of the package in the context of which the trimming should be performed. 430 431 Returns: 432 A new trimmed registry. 433 """ 434 dependencies_by_package: t.Dict[t.Optional[str], t.Set[str]] = defaultdict(set) 435 for dep in dependencies: 436 dependencies_by_package[dep.package or package].add(dep.name) 437 438 top_level_packages = self.top_level_packages.copy() 439 if package is not None: 440 top_level_packages.append(package) 441 442 result = JinjaMacroRegistry( 443 global_objs=self.global_objs.copy(), 444 create_builtins_module=self.create_builtins_module, 445 root_package_name=self.root_package_name, 446 top_level_packages=top_level_packages, 447 ) 448 for package, names in dependencies_by_package.items(): 449 result = result.merge(self._trim_macros(names, package)) 450 451 result._trimmed = True 452 453 return result 454 455 def merge(self, other: JinjaMacroRegistry) -> JinjaMacroRegistry: 456 """Returns a copy of the registry which contains macros from both this and `other` instances. 457 458 Args: 459 other: The other registry instance. 460 461 Returns: 462 A new merged registry. 463 """ 464 465 root_macros = { 466 **self.root_macros, 467 **other.root_macros, 468 } 469 470 packages = {} 471 for package in {*self.packages, *other.packages}: 472 packages[package] = { 473 **self.packages.get(package, {}), 474 **other.packages.get(package, {}), 475 } 476 477 global_objs = { 478 **self.global_objs, 479 **other.global_objs, 480 } 481 482 return JinjaMacroRegistry( 483 packages=packages, 484 root_macros=root_macros, 485 global_objs=global_objs, 486 create_builtins_module=self.create_builtins_module or other.create_builtins_module, 487 root_package_name=self.root_package_name or other.root_package_name, 488 top_level_packages=[*self.top_level_packages, *other.top_level_packages], 489 ) 490 491 def to_expressions(self) -> t.List[Expression]: 492 output: t.List[Expression] = [] 493 494 filtered_objs = { 495 k: v for k, v in self.global_objs.items() if k in ("refs", "sources", "vars") 496 } 497 if filtered_objs: 498 output.append( 499 d.PythonCode( 500 expressions=[ 501 f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}" 502 for k, v in sort_dict_recursive(filtered_objs).items() 503 ] 504 ) 505 ) 506 507 for macro_name, macro_info in sorted(self.root_macros.items()): 508 output.append(d.jinja_statement(macro_info.definition)) 509 510 for _, package in sorted(self.packages.items()): 511 for macro_name, macro_info in sorted(package.items()): 512 output.append(d.jinja_statement(macro_info.definition)) 513 514 return output 515 516 @property 517 def data_hash_values(self) -> t.List[str]: 518 data = [] 519 520 for macro_name, macro in sorted(self.root_macros.items()): 521 data.append(macro_name) 522 data.append(macro.definition) 523 524 for _, package in sorted(self.packages.items()): 525 for macro_name, macro in sorted(package.items()): 526 data.append(macro_name) 527 data.append(macro.definition) 528 529 trimmed_global_objs = { 530 k: self.global_objs[k] for k in ("refs", "sources", "vars") if k in self.global_objs 531 } 532 data.append(json.dumps(trimmed_global_objs, sort_keys=True)) 533 534 return data 535 536 def __deepcopy__(self, memo: t.Optional[t.Dict[int, t.Any]] = None) -> JinjaMacroRegistry: 537 return JinjaMacroRegistry.parse_obj(self.dict()) 538 539 def _parse_macro(self, name: str, package: t.Optional[str]) -> Template: 540 cache_key = (package, name) 541 if cache_key not in self._parser_cache: 542 macro = self._get_macro(name, package) 543 544 definition: nodes.Template = self._environment.parse(macro.definition) 545 if _is_private_macro(name): 546 # A workaround to expose private jinja macros. 547 definition = self._to_non_private_macro_def(name, definition) 548 549 self._parser_cache[cache_key] = self._environment.from_string(definition) 550 return self._parser_cache[cache_key] 551 552 @property 553 def _environment(self) -> Environment: 554 if self.__environment is None: 555 self.__environment = environment() 556 self.__environment.filters.update(self._create_builtin_filters()) 557 return self.__environment 558 559 def _trim_macros( 560 self, 561 names: t.Set[str], 562 package: t.Optional[str] = None, 563 visited: t.Optional[t.Dict[t.Optional[str], t.Set[str]]] = None, 564 ) -> JinjaMacroRegistry: 565 if visited is None: 566 visited = defaultdict(set) 567 568 macros = self.packages.get(package, {}) if package is not None else self.root_macros 569 trimmed_macros = {} 570 571 dependencies: t.Dict[t.Optional[str], t.Set[str]] = defaultdict(set) 572 573 for name in names: 574 if name in macros and name not in visited[package]: 575 macro = macros[name] 576 trimmed_macros[name] = macro 577 for dependency in macro.depends_on: 578 dependencies[dependency.package or package].add(dependency.name) 579 visited[package].add(name) 580 581 if package is not None: 582 result = JinjaMacroRegistry(packages={package: trimmed_macros}) 583 else: 584 result = JinjaMacroRegistry(root_macros=trimmed_macros) 585 586 for upstream_package, upstream_names in dependencies.items(): 587 result = result.merge( 588 self._trim_macros(upstream_names, upstream_package, visited=visited) 589 ) 590 591 return result 592 593 def _macro_exists(self, name: str, package: t.Optional[str]) -> bool: 594 return ( 595 name in self.packages.get(package, {}) 596 if package is not None 597 else name in self.root_macros 598 ) 599 600 def _get_macro(self, name: str, package: t.Optional[str]) -> MacroInfo: 601 return self.packages[package][name] if package is not None else self.root_macros[name] 602 603 def _to_non_private_macro_def(self, name: str, template: nodes.Template) -> nodes.Template: 604 for node in template.find_all((nodes.Macro, nodes.Call)): 605 if isinstance(node, nodes.Macro): 606 node.name = _non_private_name(name) 607 elif isinstance(node, nodes.Call) and isinstance(node.node, nodes.Name): 608 node.node.name = _non_private_name(name) 609 610 return template 611 612 def _create_builtin_globals(self, global_vars: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: 613 """Creates Jinja builtin globals using a factory function defined in the provided module.""" 614 engine_adapter = global_vars.pop("engine_adapter", None) 615 global_vars = {**self.global_objs, **global_vars} 616 if self.create_builtins_module is not None: 617 module = importlib.import_module(self.create_builtins_module) 618 if hasattr(module, "create_builtin_globals"): 619 return module.create_builtin_globals(self, global_vars, engine_adapter) 620 return global_vars 621 622 def _create_builtin_filters(self) -> t.Dict[str, t.Any]: 623 """Creates Jinja builtin filters using a factory function defined in the provided module.""" 624 if self.create_builtins_module is not None: 625 module = importlib.import_module(self.create_builtins_module) 626 if hasattr(module, "create_builtin_filters"): 627 return module.create_builtin_filters() 628 return {} 629 630 class _MacroWrapper: 631 def __init__( 632 self, 633 name: str, 634 package: t.Optional[str], 635 registry: JinjaMacroRegistry, 636 context: t.Dict[str, t.Any], 637 ): 638 self.name = name 639 self.package = package 640 self.context = context 641 self.registry = registry 642 643 def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: 644 context = self.context.copy() 645 if self.package is not None and self.package in context: 646 context.update(context[self.package]) 647 648 template = self.registry._parse_macro(self.name, self.package) 649 macro_callable = getattr( 650 template.make_module(vars=context), _non_private_name(self.name) 651 ) 652 try: 653 return macro_callable(*args, **kwargs) 654 except MacroReturnVal as ret: 655 return ret.value 656 657 658def _is_private_macro(name: str) -> bool: 659 return name.startswith("_") 660 661 662def _non_private_name(name: str) -> str: 663 return name.lstrip("_") 664 665 666JINJA_REGEX = re.compile(r"({{|{%)") 667 668 669def has_jinja(value: str) -> bool: 670 return JINJA_REGEX.search(value) is not None 671 672 673def jinja_call_arg_name(node: nodes.Node) -> str: 674 if isinstance(node, nodes.Const): 675 return node.value 676 return "" 677 678 679def create_var(variables: t.Dict[str, t.Any]) -> t.Callable: 680 def _var(var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]: 681 value = variables.get(var_name.lower(), default) 682 if isinstance(value, SqlValue): 683 return value.sql 684 return value 685 686 return _var 687 688 689def create_builtin_globals( 690 jinja_macros: JinjaMacroRegistry, global_vars: t.Dict[str, t.Any], *args: t.Any, **kwargs: t.Any 691) -> t.Dict[str, t.Any]: 692 global_vars.pop(c.GATEWAY, None) 693 variables = global_vars.pop(c.SQLMESH_VARS, None) or {} 694 blueprint_variables = global_vars.pop(c.SQLMESH_BLUEPRINT_VARS, None) or {} 695 return { 696 **global_vars, 697 c.VAR: create_var(variables), 698 c.GATEWAY: lambda: variables.get(c.GATEWAY, None), 699 c.BLUEPRINT_VAR: create_var(blueprint_variables), 700 } 701 702 703def make_jinja_registry( 704 jinja_macros: JinjaMacroRegistry, package_name: str, jinja_references: t.Set[MacroReference] 705) -> JinjaMacroRegistry: 706 """ 707 Creates a Jinja macro registry for a specific package. 708 709 This function takes an existing Jinja macro registry and returns a new 710 registry that includes only the macros associated with the specified 711 package and trims the registry to include only the macros referenced 712 in the provided set of macro references. 713 714 Args: 715 jinja_macros: The original Jinja macro registry containing all macros. 716 package_name: The name of the package for which to create the registry. 717 jinja_references: A set of macro references to retain in the new registry. 718 719 Returns: 720 A new JinjaMacroRegistry containing only the macros for the specified 721 package and the referenced macros. 722 """ 723 724 jinja_registry = jinja_macros.copy() 725 jinja_registry.root_macros = jinja_registry.packages.get(package_name) or {} 726 jinja_registry = jinja_registry.trim(jinja_references) 727 728 return jinja_registry 729 730 731def extract_error_details(ex: Exception) -> str: 732 """Extracts a readable message from a Jinja2 error, to include missing name and macro.""" 733 734 error_details = "" 735 if isinstance(ex, UndefinedError): 736 if match := re.search(r"'(\w+)'", str(ex)): 737 error_details += f"\nUndefined macro/variable: '{match.group(1)}'" 738 try: 739 _, _, exc_traceback = exc_info() 740 for frame, _ in walk_tb(exc_traceback): 741 if frame.f_code.co_name == "_invoke": 742 macro = frame.f_locals.get("self") 743 if isinstance(macro, Macro): 744 error_details += f" in macro: '{macro.name}'\n" 745 break 746 except: 747 # to fall back to the generic error message if frame analysis fails 748 pass 749 return error_details or str(ex)
33def b64decode(value: t.Union[str, bytes]) -> str: 34 """Decode a base64-encoded value and return it as UTF-8 text. 35 36 Intended for base64-encoded string/JSON secrets (for example a service-account 37 key stored in an environment variable), not arbitrary binary payloads. 38 """ 39 decoded = value.encode("utf-8") if isinstance(value, str) else value 40 return base64.b64decode(decoded).decode("utf-8")
Decode a base64-encoded value and return it as UTF-8 text.
Intended for base64-encoded string/JSON secrets (for example a service-account key stored in an environment variable), not arbitrary binary payloads.
43def b64encode(value: t.Union[str, bytes]) -> str: 44 """Base64-encode a value and return the encoding as UTF-8 text. 45 46 The input is treated as UTF-8 text, mirroring ``b64decode``; it is intended for 47 string/JSON secrets rather than arbitrary binary payloads. 48 """ 49 encoded = value.encode("utf-8") if isinstance(value, str) else value 50 return base64.b64encode(encoded).decode("utf-8")
Base64-encode a value and return the encoding as UTF-8 text.
The input is treated as UTF-8 text, mirroring b64decode; it is intended for
string/JSON secrets rather than arbitrary binary payloads.
72class MacroReference(PydanticModel, frozen=True): 73 package: t.Optional[str] = None 74 name: str 75 76 @property 77 def reference(self) -> str: 78 if self.package is None: 79 return self.name 80 return ".".join((self.package, self.name)) 81 82 def __str__(self) -> str: 83 return self.reference
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
- __class_vars__: The names of the class variables defined on the model.
- __private_attributes__: Metadata about the private attributes of the model.
- __signature__: The synthesized
__init__[Signature][inspect.Signature] of the model. - __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
- __pydantic_core_schema__: The core schema of the model.
- __pydantic_custom_init__: Whether the model has a custom
__init__function. - __pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces
Model.__validators__andModel.__root_validators__from Pydantic V1. - __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models.
The
originandargsitems map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and theparameteritem maps to the__parameter__attribute of generic classes. - __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
- __pydantic_post_init__: The name of the post-init method for the model, if defined.
- __pydantic_root_model__: Whether the model is a [
RootModel][pydantic.root_model.RootModel]. - __pydantic_serializer__: The
pydantic-coreSchemaSerializerused to dump instances of the model. - __pydantic_validator__: The
pydantic-coreSchemaValidatorused to validate instances of the model. - __pydantic_fields__: A dictionary of field names and their corresponding [
FieldInfo][pydantic.fields.FieldInfo] objects. - __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [
ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects. - __pydantic_extra__: A dictionary containing extra values, if [
extra][pydantic.config.ConfigDict.extra] is set to'allow'. - __pydantic_fields_set__: The names of fields explicitly set during instantiation.
- __pydantic_private__: Values of private attributes set on the model instance.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_fields
- model_computed_fields
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- parse_file
- from_orm
- construct
- schema
- schema_json
- validate
- update_forward_refs
86class MacroInfo(PydanticModel): 87 """Class to hold macro and its calls""" 88 89 definition: str 90 depends_on: t.List[MacroReference] 91 is_top_level: bool = False
Class to hold macro and its calls
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_fields
- model_computed_fields
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- parse_file
- from_orm
- construct
- schema
- schema_json
- validate
- update_forward_refs
Common base class for all non-exit exceptions.
Inherited Members
- builtins.BaseException
- with_traceback
- args
99class MacroExtractor(Parser): 100 def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: 101 """Extract a dictionary of macro definitions from a jinja string. 102 103 Args: 104 jinja: The jinja string to extract from. 105 dialect: The dialect of SQL. 106 107 Returns: 108 A dictionary of macro name to macro definition. 109 """ 110 self.reset() 111 self.sql = jinja 112 self._tokens = Dialect.get_or_raise(dialect).tokenize(jinja) 113 114 # guard for older sqlglot versions (before 30.0.3) 115 if hasattr(self, "_tokens_size"): 116 # keep the cached length in sync 117 self._tokens_size = len(self._tokens) 118 self._index = -1 119 self._advance() 120 121 macros: t.Dict[str, MacroInfo] = {} 122 123 while self._curr: 124 if self._curr.token_type == TokenType.BLOCK_START: 125 macro_start = self._curr 126 elif self._tag == "MACRO" and self._next: 127 name = self._next.text 128 while self._curr and self._curr.token_type != TokenType.BLOCK_END: 129 self._advance() 130 131 while self._curr and self._tag != "ENDMACRO": 132 self._advance() 133 134 macro_str = self._find_sql(macro_start, self._next) 135 macros[name] = MacroInfo( 136 definition=macro_str, 137 depends_on=list(extract_macro_references_and_variables(macro_str)[0]), 138 ) 139 140 self._advance() 141 142 return macros 143 144 def _advance(self, times: int = 1) -> None: 145 super()._advance(times) 146 self._tag = ( 147 self._curr.text.upper() 148 if self._curr and self._prev and self._prev.token_type == TokenType.BLOCK_START 149 else "" 150 )
Parser consumes a list of tokens produced by the Tokenizer and produces a parsed syntax tree.
Arguments:
- error_level: The desired error level. Default: ErrorLevel.IMMEDIATE
- error_message_context: The amount of context to capture from a query string when displaying the error message (in number of characters). Default: 100
- max_errors: Maximum number of error messages to include in a raised ParseError. This is only relevant if error_level is ErrorLevel.RAISE. Default: 3
- max_nodes: Maximum number of AST nodes to prevent memory exhaustion. Set to -1 (default) to disable the check.
100 def extract(self, jinja: str, dialect: str = "") -> t.Dict[str, MacroInfo]: 101 """Extract a dictionary of macro definitions from a jinja string. 102 103 Args: 104 jinja: The jinja string to extract from. 105 dialect: The dialect of SQL. 106 107 Returns: 108 A dictionary of macro name to macro definition. 109 """ 110 self.reset() 111 self.sql = jinja 112 self._tokens = Dialect.get_or_raise(dialect).tokenize(jinja) 113 114 # guard for older sqlglot versions (before 30.0.3) 115 if hasattr(self, "_tokens_size"): 116 # keep the cached length in sync 117 self._tokens_size = len(self._tokens) 118 self._index = -1 119 self._advance() 120 121 macros: t.Dict[str, MacroInfo] = {} 122 123 while self._curr: 124 if self._curr.token_type == TokenType.BLOCK_START: 125 macro_start = self._curr 126 elif self._tag == "MACRO" and self._next: 127 name = self._next.text 128 while self._curr and self._curr.token_type != TokenType.BLOCK_END: 129 self._advance() 130 131 while self._curr and self._tag != "ENDMACRO": 132 self._advance() 133 134 macro_str = self._find_sql(macro_start, self._next) 135 macros[name] = MacroInfo( 136 definition=macro_str, 137 depends_on=list(extract_macro_references_and_variables(macro_str)[0]), 138 ) 139 140 self._advance() 141 142 return macros
Extract a dictionary of macro definitions from a jinja string.
Arguments:
- jinja: The jinja string to extract from.
- dialect: The dialect of SQL.
Returns:
A dictionary of macro name to macro definition.
Inherited Members
- sqlglot.parser.Parser
- Parser
- FUNCTIONS
- NO_PAREN_FUNCTIONS
- STRUCT_TYPE_TOKENS
- NESTED_TYPE_TOKENS
- ENUM_TYPE_TOKENS
- AGGREGATE_TYPE_TOKENS
- TYPE_TOKENS
- SIGNED_TO_UNSIGNED_TYPE_TOKEN
- SUBQUERY_PREDICATES
- SUBQUERY_TOKENS
- RESERVED_TOKENS
- DB_CREATABLES
- CREATABLES
- TRIGGER_EVENTS
- ALTERABLES
- ID_VAR_TOKENS
- TABLE_ALIAS_TOKENS
- ALIAS_TOKENS
- COLON_PLACEHOLDER_TOKENS
- ARRAY_CONSTRUCTORS
- COMMENT_TABLE_ALIAS_TOKENS
- UPDATE_ALIAS_TOKENS
- TRIM_TYPES
- IDENTIFIER_TOKENS
- BRACKETS
- COLUMN_POSTFIX_TOKENS
- TABLE_POSTFIX_TOKENS
- FUNC_TOKENS
- CONJUNCTION
- ASSIGNMENT
- DISJUNCTION
- EQUALITY
- COMPARISON
- BITWISE
- TERM
- FACTOR
- EXPONENT
- TIMES
- TIMESTAMPS
- SET_OPERATIONS
- JOIN_METHODS
- JOIN_SIDES
- JOIN_KINDS
- JOIN_HINTS
- TABLE_TERMINATORS
- LAMBDAS
- TYPED_LAMBDA_ARGS
- LAMBDA_ARG_TERMINATORS
- COLUMN_OPERATORS
- CAST_COLUMN_OPERATORS
- EXPRESSION_PARSERS
- STATEMENT_PARSERS
- UNARY_PARSERS
- STRING_PARSERS
- NUMERIC_PARSERS
- PRIMARY_PARSERS
- PLACEHOLDER_PARSERS
- RANGE_PARSERS
- PIPE_SYNTAX_TRANSFORM_PARSERS
- PROPERTY_PARSERS
- CONSTRAINT_PARSERS
- ALTER_PARSERS
- ALTER_ALTER_PARSERS
- SCHEMA_UNNAMED_CONSTRAINTS
- NO_PAREN_FUNCTION_PARSERS
- INVALID_FUNC_NAME_TOKENS
- FUNCTIONS_WITH_ALIASED_ARGS
- KEY_VALUE_DEFINITIONS
- FUNCTION_PARSERS
- QUERY_MODIFIER_PARSERS
- QUERY_MODIFIER_TOKENS
- SET_PARSERS
- SHOW_PARSERS
- TYPE_LITERAL_PARSERS
- TYPE_CONVERTERS
- DDL_SELECT_TOKENS
- PRE_VOLATILE_TOKENS
- TRANSACTION_KIND
- TRANSACTION_CHARACTERISTICS
- CONFLICT_ACTIONS
- TRIGGER_TIMING
- TRIGGER_DEFERRABLE
- CREATE_SEQUENCE
- ISOLATED_LOADING_OPTIONS
- USABLES
- CAST_ACTIONS
- SCHEMA_BINDING_OPTIONS
- PROCEDURE_OPTIONS
- EXECUTE_AS_OPTIONS
- KEY_CONSTRAINT_OPTIONS
- WINDOW_EXCLUDE_OPTIONS
- INSERT_ALTERNATIVES
- CLONE_KEYWORDS
- HISTORICAL_DATA_PREFIX
- HISTORICAL_DATA_KIND
- OPCLASS_FOLLOW_KEYWORDS
- OPTYPE_FOLLOW_TOKENS
- TABLE_INDEX_HINT_TOKENS
- VIEW_ATTRIBUTES
- WINDOW_ALIAS_TOKENS
- WINDOW_BEFORE_PAREN_TOKENS
- WINDOW_SIDES
- JSON_KEY_VALUE_SEPARATOR_TOKENS
- FETCH_TOKENS
- ADD_CONSTRAINT_TOKENS
- DISTINCT_TOKENS
- UNNEST_OFFSET_ALIAS_TOKENS
- SELECT_START_TOKENS
- COPY_INTO_VARLEN_OPTIONS
- IS_JSON_PREDICATE_KIND
- ODBC_DATETIME_LITERALS
- ON_CONDITION_TOKENS
- PRIVILEGE_FOLLOW_TOKENS
- DESCRIBE_STYLES
- SET_ASSIGNMENT_DELIMITERS
- ANALYZE_STYLES
- ANALYZE_EXPRESSION_PARSERS
- PARTITION_KEYWORDS
- AMBIGUOUS_ALIAS_TOKENS
- OPERATION_MODIFIERS
- RECURSIVE_CTE_SEARCH_KIND
- SECURITY_PROPERTY_KEYWORDS
- MODIFIABLES
- STRICT_CAST
- PREFIXED_PIVOT_COLUMNS
- IDENTIFY_PIVOT_STRINGS
- LOG_DEFAULTS_TO_LN
- TABLESAMPLE_CSV
- DEFAULT_SAMPLING_METHOD
- SET_REQUIRES_ASSIGNMENT_DELIMITER
- TRIM_PATTERN_FIRST
- STRING_ALIASES
- MODIFIERS_ATTACHED_TO_SET_OP
- SET_OP_MODIFIERS
- NO_PAREN_IF_COMMANDS
- JSON_ARROWS_REQUIRE_JSON_TYPE
- COLON_IS_VARIANT_EXTRACT
- VALUES_FOLLOWED_BY_PAREN
- SUPPORTS_IMPLICIT_UNNEST
- INTERVAL_SPANS
- SUPPORTS_PARTITION_SELECTION
- WRAPPED_TRANSFORM_COLUMN_CONSTRAINT
- OPTIONAL_ALIAS_TOKEN_CTE
- ALTER_RENAME_REQUIRES_COLUMN
- ALTER_TABLE_PARTITIONS
- JOINS_HAVE_EQUAL_PRECEDENCE
- ZONE_AWARE_TIMESTAMP_CONSTRUCTOR
- MAP_KEYS_ARE_ARBITRARY_EXPRESSIONS
- JSON_EXTRACT_REQUIRES_JSON_EXPRESSION
- ADD_JOIN_ON_TRUE
- SUPPORTS_OMITTED_INTERVAL_SPAN_UNIT
- SHOW_TRIE
- SET_TRIE
- error_level
- error_message_context
- max_errors
- max_nodes
- dialect
- sql
- errors
- reset
- raise_error
- validate_expression
- parse
- parse_into
- check_errors
- expression
- parse_set_operation
- build_cast
153def call_name(node: nodes.Expr) -> t.Tuple[str, ...]: 154 if isinstance(node, nodes.Name): 155 return (node.name,) 156 if isinstance(node, nodes.Const): 157 return (f"'{node.value}'",) 158 if isinstance(node, nodes.Getattr): 159 return call_name(node.node) + (node.attr,) 160 if isinstance(node, (nodes.Getitem, nodes.Call)): 161 return call_name(node.node) 162 return ()
169def find_call_names(node: nodes.Node, vars_in_scope: t.Set[str]) -> t.Iterator[CallNames]: 170 vars_in_scope = vars_in_scope.copy() 171 for child_node in node.iter_child_nodes(): 172 if "target" in child_node.fields: 173 # For nodes with assignment targets (Assign, AssignBlock, For, Import), 174 # the target name could shadow a reference in the right hand side. 175 # So we need to process the RHS before adding the target to scope. 176 # For example: {% set model = model.path %} should track model.path. 177 yield from find_call_names(child_node, vars_in_scope) 178 179 target = getattr(child_node, "target") 180 if isinstance(target, nodes.Name): 181 vars_in_scope.add(target.name) 182 elif isinstance(target, nodes.Tuple): 183 for item in target.items: 184 if isinstance(item, nodes.Name): 185 vars_in_scope.add(item.name) 186 elif isinstance(child_node, nodes.Macro): 187 for arg in child_node.args: 188 vars_in_scope.add(arg.name) 189 elif isinstance(child_node, nodes.Call) or ( 190 isinstance(child_node, nodes.Getattr) and not isinstance(child_node.node, nodes.Getattr) 191 ): 192 name = call_name(child_node) 193 if name[0][0] != "'" and name[0] not in vars_in_scope: 194 yield (name, child_node) 195 196 if "target" not in child_node.fields: 197 yield from find_call_names(child_node, vars_in_scope)
200def extract_call_names( 201 jinja_str: str, cache: t.Optional[t.Dict[str, t.Tuple[t.List[CallNames], bool]]] = None 202) -> t.List[CallNames]: 203 def parse() -> t.List[CallNames]: 204 return list(find_call_names(ENVIRONMENT.parse(jinja_str), set())) 205 206 if cache is not None: 207 key = str(zlib.crc32(jinja_str.encode("utf-8"))) 208 if key in cache: 209 names = cache[key][0] 210 else: 211 names = parse() 212 cache[key] = (names, True) 213 return names 214 return parse()
225def extract_macro_references_and_variables( 226 *jinja_strs: str, 227) -> t.Tuple[t.Set[MacroReference], t.Set[str]]: 228 macro_references = set() 229 variables = set() 230 for jinja_str in jinja_strs: 231 for call_name, node in extract_call_names(jinja_str): 232 if call_name[0] in (c.VAR, c.BLUEPRINT_VAR): 233 if not is_variable_node(node): 234 # Find the variable node which could be nested 235 for n in node.find_all(nodes.Call): 236 if is_variable_node(n): 237 node = n 238 break 239 else: 240 raise ValueError(f"Could not find variable name in {jinja_str}") 241 node = t.cast(nodes.Call, node) 242 args = [jinja_call_arg_name(arg) for arg in node.args] 243 if args and args[0]: 244 variables.add(args[0].lower()) 245 elif call_name[0] == c.GATEWAY: 246 variables.add(c.GATEWAY) 247 elif len(call_name) == 1: 248 macro_references.add(MacroReference(name=call_name[0])) 249 elif len(call_name) == 2: 250 macro_references.add(MacroReference(package=call_name[0], name=call_name[1])) 251 return macro_references, variables
254def sort_dict_recursive( 255 item: t.Dict[str, t.Any], 256) -> t.Dict[str, t.Any]: 257 sorted_dict: t.Dict[str, t.Any] = {} 258 for k, v in sorted(item.items()): 259 if isinstance(v, list): 260 sorted_dict[k] = sorted(v) 261 elif isinstance(v, dict): 262 sorted_dict[k] = sort_dict_recursive(v) 263 else: 264 sorted_dict[k] = v 265 return sorted_dict
271class JinjaMacroRegistry(PydanticModel): 272 """Registry for Jinja macros. 273 274 Args: 275 packages: The mapping from package name to a collection of macro definitions. 276 root_macros: The collection of top-level macro definitions. 277 global_objs: The global objects. 278 create_builtins_module: The name of a module which defines the `create_builtins` factory 279 function that will be used to construct builtin variables and functions. 280 root_package_name: The name of the root package. If specified root macros will be available 281 as both `root_package_name.macro_name` and `macro_name`. 282 top_level_packages: The list of top-level packages. Macros in this packages will be available 283 as both `package_name.macro_name` and `macro_name`. 284 """ 285 286 packages: t.Dict[str, t.Dict[str, MacroInfo]] = {} 287 root_macros: t.Dict[str, MacroInfo] = {} 288 global_objs: t.Dict[str, JinjaGlobalAttribute] = {} 289 create_builtins_module: t.Optional[str] = SQLMESH_JINJA_PACKAGE 290 root_package_name: t.Optional[str] = None 291 top_level_packages: t.List[str] = [] 292 293 _parser_cache: t.Dict[t.Tuple[t.Optional[str], str], Template] = {} 294 _trimmed: bool = False 295 __environment: t.Optional[Environment] = None 296 297 def __getstate__(self) -> t.Dict[t.Any, t.Any]: 298 state = super().__getstate__() 299 private = state[PRIVATE_FIELDS] 300 private["_parser_cache"] = {} 301 private["_JinjaMacroRegistry__environment"] = None 302 return state 303 304 @field_validator("global_objs", mode="before") 305 @classmethod 306 def _validate_global_objs(cls, value: t.Any) -> t.Any: 307 def _normalize(val: t.Any) -> t.Any: 308 if isinstance(val, dict): 309 return AttributeDict({k: _normalize(v) for k, v in val.items()}) 310 if isinstance(val, list): 311 return [_normalize(v) for v in val] 312 if isinstance(val, set): 313 return [_normalize(v) for v in sorted(val)] 314 if isinstance(val, Enum): 315 return val.value 316 return val 317 318 return _normalize(value) 319 320 @field_serializer("global_objs") 321 def _serialize_attribute_dict( 322 self, value: t.Dict[str, JinjaGlobalAttribute] 323 ) -> t.Dict[str, t.Any]: 324 # NOTE: This is called only when used with Pydantic V2. 325 def _convert( 326 val: t.Union[t.Dict[str, JinjaGlobalAttribute], t.Dict[str, t.Any]], 327 ) -> t.Dict[str, t.Any]: 328 return {k: _convert(v) if isinstance(v, AttributeDict) else v for k, v in val.items()} 329 330 return _convert(value) 331 332 @property 333 def trimmed(self) -> bool: 334 return self._trimmed 335 336 def add_macros(self, macros: t.Dict[str, MacroInfo], package: t.Optional[str] = None) -> None: 337 """Adds macros to the target package. 338 339 Args: 340 macros: Macros that should be added. 341 package: The name of the package the given macros belong to. If not specified, the provided 342 macros will be added to the root namespace. 343 """ 344 345 if package is not None: 346 package_macros = self.packages.get(package, {}) 347 package_macros.update(macros) 348 self.packages[package] = package_macros 349 else: 350 self.root_macros.update(macros) 351 352 def add_globals(self, globals: t.Dict[str, JinjaGlobalAttribute]) -> None: 353 """Adds global objects to the registry. 354 355 Args: 356 globals: The global objects that should be added. 357 """ 358 # Keep the registry lightweight when the graph is not needed 359 if not "graph" in self.packages: 360 globals.pop("flat_graph", None) 361 self.global_objs.update(**self._validate_global_objs(globals)) 362 363 def build_macro(self, reference: MacroReference, **kwargs: t.Any) -> t.Optional[t.Callable]: 364 """Builds a Python callable for a macro with the given reference. 365 366 Args: 367 reference: The macro reference. 368 Returns: 369 The macro as a Python callable or None if not found. 370 """ 371 env: Environment = self.build_environment(**kwargs) 372 if reference.package is not None: 373 package = env.globals.get(reference.package, {}) 374 return package.get(reference.name) # type: ignore 375 return env.globals.get(reference.name) # type: ignore 376 377 def build_environment(self, **kwargs: t.Any) -> Environment: 378 """Builds a new Jinja environment based on this registry.""" 379 380 context: t.Dict[str, t.Any] = {} 381 382 root_macros = { 383 name: self._MacroWrapper(name, None, self, context) 384 for name, macro in self.root_macros.items() 385 } 386 387 package_macros: t.Dict[str, t.Any] = defaultdict(AttributeDict) 388 for package_name, macros in self.packages.items(): 389 for macro_name, macro in macros.items(): 390 macro_wrapper = self._MacroWrapper(macro_name, package_name, self, context) 391 package_macros[package_name][macro_name] = macro_wrapper 392 if macro.is_top_level and macro_name not in root_macros: 393 root_macros[macro_name] = macro_wrapper 394 395 top_level_packages = self.top_level_packages.copy() 396 397 if self.root_package_name is not None: 398 package_macros[self.root_package_name].update(root_macros) 399 top_level_packages.append(self.root_package_name) 400 401 env = environment() 402 403 builtin_globals = self._create_builtin_globals(kwargs) 404 for top_level_package_name in top_level_packages: 405 # Make sure that the top-level package doesn't fully override the same builtin package. 406 package_macros[top_level_package_name] = AttributeDict( 407 { 408 **(builtin_globals.pop(top_level_package_name, None) or {}), 409 **(package_macros.get(top_level_package_name) or {}), 410 } 411 ) 412 root_macros.update(package_macros[top_level_package_name]) 413 414 context.update(builtin_globals) 415 context.update(root_macros) 416 context.update(package_macros) 417 context["render"] = lambda input: env.from_string(input).render() 418 419 env.globals.update(context) 420 env.filters.update(self._environment.filters) 421 return env 422 423 def trim( 424 self, dependencies: t.Iterable[MacroReference], package: t.Optional[str] = None 425 ) -> JinjaMacroRegistry: 426 """Trims the registry by keeping only macros with given references and their transitive dependencies. 427 428 Args: 429 dependencies: References to macros that should be kept. 430 package: The name of the package in the context of which the trimming should be performed. 431 432 Returns: 433 A new trimmed registry. 434 """ 435 dependencies_by_package: t.Dict[t.Optional[str], t.Set[str]] = defaultdict(set) 436 for dep in dependencies: 437 dependencies_by_package[dep.package or package].add(dep.name) 438 439 top_level_packages = self.top_level_packages.copy() 440 if package is not None: 441 top_level_packages.append(package) 442 443 result = JinjaMacroRegistry( 444 global_objs=self.global_objs.copy(), 445 create_builtins_module=self.create_builtins_module, 446 root_package_name=self.root_package_name, 447 top_level_packages=top_level_packages, 448 ) 449 for package, names in dependencies_by_package.items(): 450 result = result.merge(self._trim_macros(names, package)) 451 452 result._trimmed = True 453 454 return result 455 456 def merge(self, other: JinjaMacroRegistry) -> JinjaMacroRegistry: 457 """Returns a copy of the registry which contains macros from both this and `other` instances. 458 459 Args: 460 other: The other registry instance. 461 462 Returns: 463 A new merged registry. 464 """ 465 466 root_macros = { 467 **self.root_macros, 468 **other.root_macros, 469 } 470 471 packages = {} 472 for package in {*self.packages, *other.packages}: 473 packages[package] = { 474 **self.packages.get(package, {}), 475 **other.packages.get(package, {}), 476 } 477 478 global_objs = { 479 **self.global_objs, 480 **other.global_objs, 481 } 482 483 return JinjaMacroRegistry( 484 packages=packages, 485 root_macros=root_macros, 486 global_objs=global_objs, 487 create_builtins_module=self.create_builtins_module or other.create_builtins_module, 488 root_package_name=self.root_package_name or other.root_package_name, 489 top_level_packages=[*self.top_level_packages, *other.top_level_packages], 490 ) 491 492 def to_expressions(self) -> t.List[Expression]: 493 output: t.List[Expression] = [] 494 495 filtered_objs = { 496 k: v for k, v in self.global_objs.items() if k in ("refs", "sources", "vars") 497 } 498 if filtered_objs: 499 output.append( 500 d.PythonCode( 501 expressions=[ 502 f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}" 503 for k, v in sort_dict_recursive(filtered_objs).items() 504 ] 505 ) 506 ) 507 508 for macro_name, macro_info in sorted(self.root_macros.items()): 509 output.append(d.jinja_statement(macro_info.definition)) 510 511 for _, package in sorted(self.packages.items()): 512 for macro_name, macro_info in sorted(package.items()): 513 output.append(d.jinja_statement(macro_info.definition)) 514 515 return output 516 517 @property 518 def data_hash_values(self) -> t.List[str]: 519 data = [] 520 521 for macro_name, macro in sorted(self.root_macros.items()): 522 data.append(macro_name) 523 data.append(macro.definition) 524 525 for _, package in sorted(self.packages.items()): 526 for macro_name, macro in sorted(package.items()): 527 data.append(macro_name) 528 data.append(macro.definition) 529 530 trimmed_global_objs = { 531 k: self.global_objs[k] for k in ("refs", "sources", "vars") if k in self.global_objs 532 } 533 data.append(json.dumps(trimmed_global_objs, sort_keys=True)) 534 535 return data 536 537 def __deepcopy__(self, memo: t.Optional[t.Dict[int, t.Any]] = None) -> JinjaMacroRegistry: 538 return JinjaMacroRegistry.parse_obj(self.dict()) 539 540 def _parse_macro(self, name: str, package: t.Optional[str]) -> Template: 541 cache_key = (package, name) 542 if cache_key not in self._parser_cache: 543 macro = self._get_macro(name, package) 544 545 definition: nodes.Template = self._environment.parse(macro.definition) 546 if _is_private_macro(name): 547 # A workaround to expose private jinja macros. 548 definition = self._to_non_private_macro_def(name, definition) 549 550 self._parser_cache[cache_key] = self._environment.from_string(definition) 551 return self._parser_cache[cache_key] 552 553 @property 554 def _environment(self) -> Environment: 555 if self.__environment is None: 556 self.__environment = environment() 557 self.__environment.filters.update(self._create_builtin_filters()) 558 return self.__environment 559 560 def _trim_macros( 561 self, 562 names: t.Set[str], 563 package: t.Optional[str] = None, 564 visited: t.Optional[t.Dict[t.Optional[str], t.Set[str]]] = None, 565 ) -> JinjaMacroRegistry: 566 if visited is None: 567 visited = defaultdict(set) 568 569 macros = self.packages.get(package, {}) if package is not None else self.root_macros 570 trimmed_macros = {} 571 572 dependencies: t.Dict[t.Optional[str], t.Set[str]] = defaultdict(set) 573 574 for name in names: 575 if name in macros and name not in visited[package]: 576 macro = macros[name] 577 trimmed_macros[name] = macro 578 for dependency in macro.depends_on: 579 dependencies[dependency.package or package].add(dependency.name) 580 visited[package].add(name) 581 582 if package is not None: 583 result = JinjaMacroRegistry(packages={package: trimmed_macros}) 584 else: 585 result = JinjaMacroRegistry(root_macros=trimmed_macros) 586 587 for upstream_package, upstream_names in dependencies.items(): 588 result = result.merge( 589 self._trim_macros(upstream_names, upstream_package, visited=visited) 590 ) 591 592 return result 593 594 def _macro_exists(self, name: str, package: t.Optional[str]) -> bool: 595 return ( 596 name in self.packages.get(package, {}) 597 if package is not None 598 else name in self.root_macros 599 ) 600 601 def _get_macro(self, name: str, package: t.Optional[str]) -> MacroInfo: 602 return self.packages[package][name] if package is not None else self.root_macros[name] 603 604 def _to_non_private_macro_def(self, name: str, template: nodes.Template) -> nodes.Template: 605 for node in template.find_all((nodes.Macro, nodes.Call)): 606 if isinstance(node, nodes.Macro): 607 node.name = _non_private_name(name) 608 elif isinstance(node, nodes.Call) and isinstance(node.node, nodes.Name): 609 node.node.name = _non_private_name(name) 610 611 return template 612 613 def _create_builtin_globals(self, global_vars: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: 614 """Creates Jinja builtin globals using a factory function defined in the provided module.""" 615 engine_adapter = global_vars.pop("engine_adapter", None) 616 global_vars = {**self.global_objs, **global_vars} 617 if self.create_builtins_module is not None: 618 module = importlib.import_module(self.create_builtins_module) 619 if hasattr(module, "create_builtin_globals"): 620 return module.create_builtin_globals(self, global_vars, engine_adapter) 621 return global_vars 622 623 def _create_builtin_filters(self) -> t.Dict[str, t.Any]: 624 """Creates Jinja builtin filters using a factory function defined in the provided module.""" 625 if self.create_builtins_module is not None: 626 module = importlib.import_module(self.create_builtins_module) 627 if hasattr(module, "create_builtin_filters"): 628 return module.create_builtin_filters() 629 return {} 630 631 class _MacroWrapper: 632 def __init__( 633 self, 634 name: str, 635 package: t.Optional[str], 636 registry: JinjaMacroRegistry, 637 context: t.Dict[str, t.Any], 638 ): 639 self.name = name 640 self.package = package 641 self.context = context 642 self.registry = registry 643 644 def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: 645 context = self.context.copy() 646 if self.package is not None and self.package in context: 647 context.update(context[self.package]) 648 649 template = self.registry._parse_macro(self.name, self.package) 650 macro_callable = getattr( 651 template.make_module(vars=context), _non_private_name(self.name) 652 ) 653 try: 654 return macro_callable(*args, **kwargs) 655 except MacroReturnVal as ret: 656 return ret.value
Registry for Jinja macros.
Arguments:
- packages: The mapping from package name to a collection of macro definitions.
- root_macros: The collection of top-level macro definitions.
- global_objs: The global objects.
- create_builtins_module: The name of a module which defines the
create_builtinsfactory function that will be used to construct builtin variables and functions. - root_package_name: The name of the root package. If specified root macros will be available
as both
root_package_name.macro_nameandmacro_name. - top_level_packages: The list of top-level packages. Macros in this packages will be available
as both
package_name.macro_nameandmacro_name.
336 def add_macros(self, macros: t.Dict[str, MacroInfo], package: t.Optional[str] = None) -> None: 337 """Adds macros to the target package. 338 339 Args: 340 macros: Macros that should be added. 341 package: The name of the package the given macros belong to. If not specified, the provided 342 macros will be added to the root namespace. 343 """ 344 345 if package is not None: 346 package_macros = self.packages.get(package, {}) 347 package_macros.update(macros) 348 self.packages[package] = package_macros 349 else: 350 self.root_macros.update(macros)
Adds macros to the target package.
Arguments:
- macros: Macros that should be added.
- package: The name of the package the given macros belong to. If not specified, the provided
- macros will be added to the root namespace.
352 def add_globals(self, globals: t.Dict[str, JinjaGlobalAttribute]) -> None: 353 """Adds global objects to the registry. 354 355 Args: 356 globals: The global objects that should be added. 357 """ 358 # Keep the registry lightweight when the graph is not needed 359 if not "graph" in self.packages: 360 globals.pop("flat_graph", None) 361 self.global_objs.update(**self._validate_global_objs(globals))
Adds global objects to the registry.
Arguments:
- globals: The global objects that should be added.
363 def build_macro(self, reference: MacroReference, **kwargs: t.Any) -> t.Optional[t.Callable]: 364 """Builds a Python callable for a macro with the given reference. 365 366 Args: 367 reference: The macro reference. 368 Returns: 369 The macro as a Python callable or None if not found. 370 """ 371 env: Environment = self.build_environment(**kwargs) 372 if reference.package is not None: 373 package = env.globals.get(reference.package, {}) 374 return package.get(reference.name) # type: ignore 375 return env.globals.get(reference.name) # type: ignore
Builds a Python callable for a macro with the given reference.
Arguments:
- reference: The macro reference.
Returns:
The macro as a Python callable or None if not found.
377 def build_environment(self, **kwargs: t.Any) -> Environment: 378 """Builds a new Jinja environment based on this registry.""" 379 380 context: t.Dict[str, t.Any] = {} 381 382 root_macros = { 383 name: self._MacroWrapper(name, None, self, context) 384 for name, macro in self.root_macros.items() 385 } 386 387 package_macros: t.Dict[str, t.Any] = defaultdict(AttributeDict) 388 for package_name, macros in self.packages.items(): 389 for macro_name, macro in macros.items(): 390 macro_wrapper = self._MacroWrapper(macro_name, package_name, self, context) 391 package_macros[package_name][macro_name] = macro_wrapper 392 if macro.is_top_level and macro_name not in root_macros: 393 root_macros[macro_name] = macro_wrapper 394 395 top_level_packages = self.top_level_packages.copy() 396 397 if self.root_package_name is not None: 398 package_macros[self.root_package_name].update(root_macros) 399 top_level_packages.append(self.root_package_name) 400 401 env = environment() 402 403 builtin_globals = self._create_builtin_globals(kwargs) 404 for top_level_package_name in top_level_packages: 405 # Make sure that the top-level package doesn't fully override the same builtin package. 406 package_macros[top_level_package_name] = AttributeDict( 407 { 408 **(builtin_globals.pop(top_level_package_name, None) or {}), 409 **(package_macros.get(top_level_package_name) or {}), 410 } 411 ) 412 root_macros.update(package_macros[top_level_package_name]) 413 414 context.update(builtin_globals) 415 context.update(root_macros) 416 context.update(package_macros) 417 context["render"] = lambda input: env.from_string(input).render() 418 419 env.globals.update(context) 420 env.filters.update(self._environment.filters) 421 return env
Builds a new Jinja environment based on this registry.
423 def trim( 424 self, dependencies: t.Iterable[MacroReference], package: t.Optional[str] = None 425 ) -> JinjaMacroRegistry: 426 """Trims the registry by keeping only macros with given references and their transitive dependencies. 427 428 Args: 429 dependencies: References to macros that should be kept. 430 package: The name of the package in the context of which the trimming should be performed. 431 432 Returns: 433 A new trimmed registry. 434 """ 435 dependencies_by_package: t.Dict[t.Optional[str], t.Set[str]] = defaultdict(set) 436 for dep in dependencies: 437 dependencies_by_package[dep.package or package].add(dep.name) 438 439 top_level_packages = self.top_level_packages.copy() 440 if package is not None: 441 top_level_packages.append(package) 442 443 result = JinjaMacroRegistry( 444 global_objs=self.global_objs.copy(), 445 create_builtins_module=self.create_builtins_module, 446 root_package_name=self.root_package_name, 447 top_level_packages=top_level_packages, 448 ) 449 for package, names in dependencies_by_package.items(): 450 result = result.merge(self._trim_macros(names, package)) 451 452 result._trimmed = True 453 454 return result
Trims the registry by keeping only macros with given references and their transitive dependencies.
Arguments:
- dependencies: References to macros that should be kept.
- package: The name of the package in the context of which the trimming should be performed.
Returns:
A new trimmed registry.
456 def merge(self, other: JinjaMacroRegistry) -> JinjaMacroRegistry: 457 """Returns a copy of the registry which contains macros from both this and `other` instances. 458 459 Args: 460 other: The other registry instance. 461 462 Returns: 463 A new merged registry. 464 """ 465 466 root_macros = { 467 **self.root_macros, 468 **other.root_macros, 469 } 470 471 packages = {} 472 for package in {*self.packages, *other.packages}: 473 packages[package] = { 474 **self.packages.get(package, {}), 475 **other.packages.get(package, {}), 476 } 477 478 global_objs = { 479 **self.global_objs, 480 **other.global_objs, 481 } 482 483 return JinjaMacroRegistry( 484 packages=packages, 485 root_macros=root_macros, 486 global_objs=global_objs, 487 create_builtins_module=self.create_builtins_module or other.create_builtins_module, 488 root_package_name=self.root_package_name or other.root_package_name, 489 top_level_packages=[*self.top_level_packages, *other.top_level_packages], 490 )
Returns a copy of the registry which contains macros from both this and other instances.
Arguments:
- other: The other registry instance.
Returns:
A new merged registry.
492 def to_expressions(self) -> t.List[Expression]: 493 output: t.List[Expression] = [] 494 495 filtered_objs = { 496 k: v for k, v in self.global_objs.items() if k in ("refs", "sources", "vars") 497 } 498 if filtered_objs: 499 output.append( 500 d.PythonCode( 501 expressions=[ 502 f"{k} = '{v}'" if isinstance(v, str) else f"{k} = {v}" 503 for k, v in sort_dict_recursive(filtered_objs).items() 504 ] 505 ) 506 ) 507 508 for macro_name, macro_info in sorted(self.root_macros.items()): 509 output.append(d.jinja_statement(macro_info.definition)) 510 511 for _, package in sorted(self.packages.items()): 512 for macro_name, macro_info in sorted(package.items()): 513 output.append(d.jinja_statement(macro_info.definition)) 514 515 return output
517 @property 518 def data_hash_values(self) -> t.List[str]: 519 data = [] 520 521 for macro_name, macro in sorted(self.root_macros.items()): 522 data.append(macro_name) 523 data.append(macro.definition) 524 525 for _, package in sorted(self.packages.items()): 526 for macro_name, macro in sorted(package.items()): 527 data.append(macro_name) 528 data.append(macro.definition) 529 530 trimmed_global_objs = { 531 k: self.global_objs[k] for k in ("refs", "sources", "vars") if k in self.global_objs 532 } 533 data.append(json.dumps(trimmed_global_objs, sort_keys=True)) 534 535 return data
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
365def init_private_attributes(self: BaseModel, context: Any, /) -> None: 366 """This function is meant to behave like a BaseModel method to initialize private attributes. 367 368 It takes context as an argument since that's what pydantic-core passes when calling it. 369 370 Args: 371 self: The BaseModel instance. 372 context: The context. 373 """ 374 if getattr(self, '__pydantic_private__', None) is None: 375 pydantic_private = {} 376 for name, private_attr in self.__private_attributes__.items(): 377 # Avoid needlessly creating a new dict for the validated data: 378 if private_attr.default_factory_takes_validated_data: 379 default = private_attr.get_default( 380 call_default_factory=True, validated_data={**self.__dict__, **pydantic_private} 381 ) 382 else: 383 default = private_attr.get_default(call_default_factory=True) 384 if default is not PydanticUndefined: 385 pydantic_private[name] = default 386 object_setattr(self, '__pydantic_private__', pydantic_private)
This function is meant to behave like a BaseModel method to initialize private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Arguments:
- self: The BaseModel instance.
- context: The context.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_fields
- model_computed_fields
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- parse_file
- from_orm
- construct
- schema
- schema_json
- validate
- update_forward_refs
690def create_builtin_globals( 691 jinja_macros: JinjaMacroRegistry, global_vars: t.Dict[str, t.Any], *args: t.Any, **kwargs: t.Any 692) -> t.Dict[str, t.Any]: 693 global_vars.pop(c.GATEWAY, None) 694 variables = global_vars.pop(c.SQLMESH_VARS, None) or {} 695 blueprint_variables = global_vars.pop(c.SQLMESH_BLUEPRINT_VARS, None) or {} 696 return { 697 **global_vars, 698 c.VAR: create_var(variables), 699 c.GATEWAY: lambda: variables.get(c.GATEWAY, None), 700 c.BLUEPRINT_VAR: create_var(blueprint_variables), 701 }
704def make_jinja_registry( 705 jinja_macros: JinjaMacroRegistry, package_name: str, jinja_references: t.Set[MacroReference] 706) -> JinjaMacroRegistry: 707 """ 708 Creates a Jinja macro registry for a specific package. 709 710 This function takes an existing Jinja macro registry and returns a new 711 registry that includes only the macros associated with the specified 712 package and trims the registry to include only the macros referenced 713 in the provided set of macro references. 714 715 Args: 716 jinja_macros: The original Jinja macro registry containing all macros. 717 package_name: The name of the package for which to create the registry. 718 jinja_references: A set of macro references to retain in the new registry. 719 720 Returns: 721 A new JinjaMacroRegistry containing only the macros for the specified 722 package and the referenced macros. 723 """ 724 725 jinja_registry = jinja_macros.copy() 726 jinja_registry.root_macros = jinja_registry.packages.get(package_name) or {} 727 jinja_registry = jinja_registry.trim(jinja_references) 728 729 return jinja_registry
Creates a Jinja macro registry for a specific package.
This function takes an existing Jinja macro registry and returns a new registry that includes only the macros associated with the specified package and trims the registry to include only the macros referenced in the provided set of macro references.
Arguments:
- jinja_macros: The original Jinja macro registry containing all macros.
- package_name: The name of the package for which to create the registry.
- jinja_references: A set of macro references to retain in the new registry.
Returns:
A new JinjaMacroRegistry containing only the macros for the specified package and the referenced macros.
732def extract_error_details(ex: Exception) -> str: 733 """Extracts a readable message from a Jinja2 error, to include missing name and macro.""" 734 735 error_details = "" 736 if isinstance(ex, UndefinedError): 737 if match := re.search(r"'(\w+)'", str(ex)): 738 error_details += f"\nUndefined macro/variable: '{match.group(1)}'" 739 try: 740 _, _, exc_traceback = exc_info() 741 for frame, _ in walk_tb(exc_traceback): 742 if frame.f_code.co_name == "_invoke": 743 macro = frame.f_locals.get("self") 744 if isinstance(macro, Macro): 745 error_details += f" in macro: '{macro.name}'\n" 746 break 747 except: 748 # to fall back to the generic error message if frame analysis fails 749 pass 750 return error_details or str(ex)
Extracts a readable message from a Jinja2 error, to include missing name and macro.