@@ -13,42 +13,48 @@ def __init__(self, base_dir: str) -> None:
1313 self ._env = NativeEnvironment (
1414 trim_blocks = True , lstrip_blocks = True , autoescape = False
1515 )
16+ self ._env .globals ["load" ] = self ._load_file
17+ self ._variables : dict [str , Any ] = {}
1618
1719 def resolve (
1820 self , data : dict [str , Any ], extra_vars : dict [str , Any ]
1921 ) -> dict [str , Any ]:
2022 """Resolve all Jinja templates in data using iterative passes."""
21- resolved = {** copy .deepcopy (data ), ** extra_vars }
22- context = [resolved ] # Mutable wrapper for closure access
23+ self ._variables = {** copy .deepcopy (data ), ** extra_vars }
2324
2425 for _ in range (10 ):
25- previous = copy .deepcopy (resolved )
26- context [0 ] = resolved
27- self ._env .globals ["load" ] = self ._make_loader (context )
28- resolved = self ._resolve_recursive (resolved , context )
29- if resolved == previous :
30- return resolved
26+ self ._variables , changed = self ._resolve_value (self ._variables )
27+ if not changed :
28+ return self ._variables
3129
3230 raise RuntimeError ("Template resolution did not converge" )
3331
34- def _resolve_recursive (self , obj : Any , context : list [ dict ] ) -> Any :
32+ def _resolve_value (self , obj : Any ) -> tuple [ Any , bool ] :
3533 if isinstance (obj , dict ):
36- return {k : self ._resolve_recursive (v , context ) for k , v in obj .items ()}
34+ changed = False
35+ result : dict [str , Any ] = {}
36+ for k , v in obj .items ():
37+ new_v , v_changed = self ._resolve_value (v )
38+ result [k ] = new_v
39+ changed = changed or v_changed
40+ return result , changed
3741 if isinstance (obj , list ):
38- return [self ._resolve_recursive (i , context ) for i in obj ]
42+ changed = False
43+ items : list [Any ] = []
44+ for item in obj :
45+ new_item , item_changed = self ._resolve_value (item )
46+ items .append (new_item )
47+ changed = changed or item_changed
48+ return items , changed
3949 if isinstance (obj , str ) and "{{" in obj and "}}" in obj :
40- template = self ._env .from_string (obj )
41- return template .render (** context [0 ])
42- return obj
43-
44- def _make_loader (self , context : list [dict ]):
45- def load (filepath : str ) -> str | Any :
46- full_path = os .path .abspath (os .path .join (self ._base_dir , filepath ))
47- with open (full_path ) as f :
48- if filepath .endswith ((".yml" , ".yaml" )):
49- return yaml .load (f )
50- content = f .read ()
51- template = self ._env .from_string (content )
52- return template .render (** context [0 ])
53-
54- return load
50+ rendered = self ._env .from_string (obj ).render (** self ._variables )
51+ return rendered , rendered != obj
52+ return obj , False
53+
54+ def _load_file (self , filepath : str ) -> str | Any :
55+ full_path = os .path .abspath (os .path .join (self ._base_dir , filepath ))
56+ with open (full_path ) as f :
57+ if filepath .endswith ((".yml" , ".yaml" )):
58+ return yaml .load (f )
59+ content = f .read ()
60+ return self ._env .from_string (content ).render (** self ._variables )
0 commit comments