@@ -75,26 +75,76 @@ def extract_keys_from_dict(node: ast.Dict) -> typing.Iterator[str]:
7575 raise ValueError ("Key in dictionary is not a string literal" )
7676 yield key .value
7777
78- # Extract keys from the main `context` dictionary assignment
79- context_assignment : ast .AnnAssign = next (
80- stmt
81- for stmt in fn_get_template_context .body
82- if isinstance (stmt , ast .AnnAssign )
83- and isinstance (stmt .target , ast .Attribute )
84- and isinstance (stmt .target .value , ast .Name )
85- and stmt .target .value .id == "self"
86- and stmt .target .attr == "_cached_template_context"
87- )
88-
89- if not isinstance (context_assignment .value , ast .BoolOp ):
90- raise TypeError ("Expected a BoolOp like 'self._cached_template_context or {...}'." )
78+ # Extract keys from the main `context` dictionary assignment.
79+ # Supports two patterns:
80+ # 1. Annotated assignment: self._cached_template_context: T = self._cached_template_context or {...}
81+ # 2. Guard block: if self._cached_template_context is None: self._cached_template_context = {...}
82+ context_dict : ast .Dict | None = None
9183
92- context_assignment_op = context_assignment .value
93- _ , context_assignment_value = context_assignment_op .values
94-
95- if not isinstance (context_assignment_value , ast .Dict ):
96- raise ValueError ("'context' is not assigned a dictionary literal" )
97- yield from extract_keys_from_dict (context_assignment_value )
84+ for stmt in fn_get_template_context .body :
85+ # Pattern 1: AnnAssign with BoolOp
86+ if (
87+ isinstance (stmt , ast .AnnAssign )
88+ and isinstance (stmt .target , ast .Attribute )
89+ and isinstance (stmt .target .value , ast .Name )
90+ and stmt .target .value .id == "self"
91+ and stmt .target .attr == "_cached_template_context"
92+ and isinstance (stmt .value , ast .BoolOp )
93+ ):
94+ _ , context_dict = stmt .value .values # type: ignore[assignment]
95+ break
96+ # Pattern 2: if self._cached_template_context is None: self._cached_template_context = {...}
97+ if (
98+ isinstance (stmt , ast .If )
99+ and isinstance (stmt .test , ast .Compare )
100+ and isinstance (stmt .test .left , ast .Attribute )
101+ and isinstance (stmt .test .left .value , ast .Name )
102+ and stmt .test .left .value .id == "self"
103+ and stmt .test .left .attr == "_cached_template_context"
104+ and len (stmt .test .ops ) == 1
105+ and isinstance (stmt .test .ops [0 ], ast .Is )
106+ and len (stmt .test .comparators ) == 1
107+ and isinstance (stmt .test .comparators [0 ], ast .Constant )
108+ and stmt .test .comparators [0 ].value is None
109+ ):
110+ inner = next (
111+ (
112+ s
113+ for s in stmt .body
114+ if isinstance (s , (ast .Assign , ast .AnnAssign ))
115+ and (
116+ (
117+ isinstance (s , ast .Assign )
118+ and any (
119+ isinstance (t , ast .Attribute )
120+ and isinstance (t .value , ast .Name )
121+ and t .value .id == "self"
122+ and t .attr == "_cached_template_context"
123+ for t in s .targets
124+ )
125+ )
126+ or (
127+ isinstance (s , ast .AnnAssign )
128+ and isinstance (s .target , ast .Attribute )
129+ and isinstance (s .target .value , ast .Name )
130+ and s .target .value .id == "self"
131+ and s .target .attr == "_cached_template_context"
132+ )
133+ )
134+ ),
135+ None ,
136+ )
137+ if inner is not None :
138+ val = inner .value if isinstance (inner , ast .AnnAssign ) else inner .value # type: ignore[union-attr]
139+ if isinstance (val , ast .Dict ):
140+ context_dict = val
141+ break
142+
143+ if context_dict is None :
144+ raise ValueError ("Could not find '_cached_template_context' dictionary assignment in get_template_context()" )
145+ if not isinstance (context_dict , ast .Dict ):
146+ raise ValueError ("'_cached_template_context' is not assigned a dictionary literal" )
147+ yield from extract_keys_from_dict (context_dict )
98148
99149 # Handle keys added conditionally in `if from_server`
100150 for stmt in fn_get_template_context .body :
0 commit comments