Coverage for jinja2_async_environment/compiler_old.py: 0%

1 import hashlib

2 import re

3 import typing as t

4

5 from jinja2 import nodes

6 from jinja2.compiler import (

7 CodeGenerator,

8 CompilerExit,

9 EvalContext,

10 Frame,

11 find_undeclared,

12)

13

14

15# Pre-compiled regex patterns for performance optimization

16 class CompiledPatterns:

17 """Pre-compiled regex patterns for template compilation optimization."""

18

19 # Pattern for async yield detection

20 ASYNC_YIELD_PATTERN = re.compile(

21 r"async for event in self\._async_yield_from\([^)]+\):\s*$", re.MULTILINE

22 )

23

24 # Pattern for undefined variable detection

25 UNDEFINED_VAR_PATTERN = re.compile(

26 r"undefined\(name='([^']+)'\) if l_0_\1 is missing else l_0_\1"

27 )

28

29 # Pattern for loop variable optimization

30 LOOP_VAR_PATTERN = re.compile(r"l_0_(\w+)")

31

32 # Pattern for context block detection

33 CONTEXT_BLOCK_PATTERN = re.compile(r"yield from context\.blocks")

34

35 # Additional performance patterns

36 HASATTR_CHECK_PATTERN = re.compile(r"hasattr\(([^,]+),\s*'__await__'\)")

37 AUTO_AITER_PATTERN = re.compile(r"auto_aiter\(([^)]+)\)")

38 TEMPLATE_RUNTIME_ERROR_PATTERN = re.compile(r'TemplateRuntimeError\("([^"]+)"\)')

39 DUPLICATE_IMPORT_PATTERN = re.compile(r"^from ([\w.]+) import (.+)$", re.MULTILINE)

40 REDUNDANT_ESCAPE_PATTERN = re.compile(r"escape\(escape\(([^)]+)\)\)")

41 CONSTANT_UNDEFINED_PATTERN = re.compile(r"undefined\(name=None\)")

42

43 # Cached import statements for faster code generation

44 _CACHED_IMPORTS = {

45 "runtime": "from jinja2.runtime import Undefined, Macro, missing, LoopContext, AsyncLoopContext, auto_aiter, Namespace, TemplateRuntimeError",

46 "markupsafe": "from markupsafe import escape",

47 "defaults": "from jinja2.defaults import DEFAULT_FILTERS",

48 }

49

50 @classmethod

51 def get_optimized_imports(cls) -> str:

52 """Return optimized import statements as a single string."""

53 return "\n".join(cls._CACHED_IMPORTS.values())

54

55 @classmethod

56 def optimize_generated_code(cls, code: str) -> str:

57 """Apply pattern-based optimizations to generated template code."""

58 optimized_code = code

59

60 # Fast string replacements for common patterns

61 optimizations = [

62 ("yield from context.blocks", "pass # yield from replaced"),

63 ("undefined(name='item') if l_0_item is missing else l_0_item", "item"),

64 ("undefined(name=None)", "Undefined()"),

65 ("escape(escape(", "escape("), # Remove double escaping

66 (

67 "if hasattr(value, '__await__'):\n return await value\nelse:\n return value",

68 "return await value if hasattr(value, '__await__') else value",

69 ),

70 ]

71

72 for pattern, replacement in optimizations:

73 optimized_code = optimized_code.replace(pattern, replacement)

74

75 # Use regex patterns for more complex optimizations

76 optimized_code = cls._optimize_with_regex(optimized_code)

77

78 # Remove duplicate imports

79 optimized_code = cls._deduplicate_imports(optimized_code)

80

81 return optimized_code

82

83 @classmethod

84 def _optimize_with_regex(cls, code: str) -> str:

85 """Apply regex-based optimizations."""

86

87 # Optimize hasattr checks

88 def optimize_hasattr(match):

89 var = match.group(1)

90 return f"getattr({var}, '__await__', None) is not None"

91

92 code = cls.HASATTR_CHECK_PATTERN.sub(optimize_hasattr, code)

93

94 # Remove redundant escape calls

95 code = cls.REDUNDANT_ESCAPE_PATTERN.sub(r"escape(\1)", code)

96

97 return code

98

99 @classmethod

100 def _deduplicate_imports(cls, code: str) -> str:

101 """Remove duplicate import statements."""

102 lines = code.split("\n")

103 seen_imports = set()

104 deduplicated_lines = []

105

106 for line in lines:

107 if line.strip().startswith("from ") and " import " in line:

108 if line not in seen_imports:

109 seen_imports.add(line)

110 deduplicated_lines.append(line)

111 else:

112 deduplicated_lines.append(line)

113

114 return "\n".join(deduplicated_lines)

115

116

117 class CompilationCache:

118 """Cache for compiled template code to avoid recompilation."""

119

120 def __init__(self, max_size: int = 1000):

121 self.max_size = max_size

122 self._cache: dict[str, str] = {}

123

124 def get_cache_key(self, source: str, environment_id: str) -> str:

125 """Generate a cache key for template source and environment."""

126 content = f"{source}:{environment_id}"

127 return hashlib.sha256(content.encode()).hexdigest()[:16]

128

129 def get(self, cache_key: str) -> str | None:

130 """Get compiled code from cache."""

131 return self._cache.get(cache_key)

132

133 def set(self, cache_key: str, compiled_code: str) -> None:

134 """Store compiled code in cache with size limit."""

135 if len(self._cache) >= self.max_size:

136 # Remove oldest entries (simple FIFO)

137 oldest_keys = list(self._cache.keys())[: self.max_size // 4]

138 for key in oldest_keys:

139 del self._cache[key]

140

141 self._cache[cache_key] = compiled_code

142

143 def clear(self) -> None:

144 """Clear the cache."""

145 self._cache.clear()

146

147

148# Global compilation cache instance

149 _compilation_cache = CompilationCache()

150

151

152 class DependencyResolver:

153 """Utility class for resolving template dependencies like filters and tests."""

154

155 def __init__(self, code_generator: "AsyncCodeGenerator") -> None:

156 self.code_generator = code_generator

157

158 def setup_filter_dependency(self, name: str) -> None:

159 """Set up a single filter dependency with error handling."""

160 if name in self.code_generator.filters:

161 return

162

163 self.code_generator.filters[name] = self.code_generator.temporary_identifier()

164 filter_var = self.code_generator.filters[name]

165

166 self.code_generator.writeline("try:")

167 self.code_generator.indent()

168 self.code_generator.writeline(f"{filter_var} = environment.filters[{name! r}]")

169 self.code_generator.outdent()

170 self.code_generator.writeline("except KeyError:")

171 self.code_generator.indent()

172 self.code_generator.writeline("@internalcode")

173 self.code_generator.writeline(f"def {filter_var}(*unused):")

174 self.code_generator.indent()

175 self.code_generator.writeline(

176 f'raise TemplateRuntimeError("No filter named {name! r} found.")'

177 )

178 self.code_generator.outdent()

179 self.code_generator.outdent()

180

181 def setup_test_dependency(self, name: str) -> None:

182 """Set up a single test dependency with error handling."""

183 if name in self.code_generator.tests:

184 return

185

186 self.code_generator.tests[name] = self.code_generator.temporary_identifier()

187 test_var = self.code_generator.tests[name]

188

189 self.code_generator.writeline("try:")

190 self.code_generator.indent()

191 self.code_generator.writeline(f"{test_var} = environment.tests[{name! r}]")

192 self.code_generator.outdent()

193 self.code_generator.writeline("except KeyError:")

194 self.code_generator.indent()

195 self.code_generator.writeline("@internalcode")

196 self.code_generator.writeline(f"def {test_var}(*unused):")

197 self.code_generator.indent()

198 self.code_generator.writeline(

199 f'raise TemplateRuntimeError("No test named {name! r} found.")'

200 )

201 self.code_generator.outdent()

202 self.code_generator.outdent()

203

204

205 class LoopCodeGenerator:

206 """Utility class for generating for-loop specific code patterns."""

207

208 def __init__(self, code_generator: "AsyncCodeGenerator") -> None:

209 self.code_generator = code_generator

210

211 def generate_async_for_header(

212 self, node: nodes.For, target: nodes.Node, frame: "AsyncFrame"

213 ) -> None:

214 """Generate the async for loop header with proper syntax."""

215 self.code_generator.writeline(

216 self.code_generator.choose_async("async for ", "for "), node

217 )

218 self.code_generator.visit(target, frame)

219

220 def generate_loop_iterator(

221 self,

222 iter_node: nodes.Node,

223 frame: "AsyncFrame",

224 extended_loop: bool,

225 loop_ref: str | None,

226 loop_filter_func: str | None,

227 ) -> None:

228 """Generate the iterator part of the for loop."""

229 if extended_loop and loop_ref:

230 self.code_generator.write(

231 f", {loop_ref} in {self.code_generator.choose_async('Async')}LoopContext("

232 )

233 else:

234 self.code_generator.write(" in ")

235

236 if loop_filter_func:

237 self.code_generator.write(f"{loop_filter_func}(")

238

239 if self.code_generator.environment.is_async and not extended_loop:

240 self.code_generator.write("auto_aiter(")

241 self.code_generator.visit(iter_node, frame)

242 if self.code_generator.environment.is_async and not extended_loop:

243 self.code_generator.write(")")

244

245 if loop_filter_func:

246 self.code_generator.write(")")

247

248 if extended_loop:

249 self.code_generator.write(", undefined):")

250 else:

251 self.code_generator.write(":")

252

253

254 class AsyncFrame(Frame):

255 block_frame: "AsyncFrame | None"

256 require_output_check: bool

257 has_known_extends: bool

258 toplevel: bool

259 rootlevel: bool

260 buffer: str | None

261 block_buffer: list[str]

262 extended_buffer: list[str] | None

263 require_yield: bool

264 buffer_count: int

265 is_async: bool

266

267 def __init__(self, eval_ctx: EvalContext | None = None) -> None:

268 if eval_ctx is None:

269 from jinja2.environment import Environment

270 from jinja2.nodes import EvalContext

271

272 eval_ctx = EvalContext(Environment(autoescape=True), "template")

273 super().__init__(eval_ctx)

274 self.buffer = None

275 self.block_buffer = []

276 self.extended_buffer = None

277 self.block_frame = None

278 self.require_output_check = False

279 self.has_known_extends = False

280 self.toplevel = False

281 self.rootlevel = False

282 self.require_yield = False

283 self.buffer_count = 0

284 self.is_async = False

285 self.block_counters: dict[str, int] = {}

286 self.block_frame_id = 0

287

288 def copy(self) -> t.Self:

289 rv = self.__class__(self.eval_ctx)

290 rv.symbols = self.symbols.copy() # noqa: FURB145

291 rv.buffer = self.buffer

292 rv.block_buffer = self.block_buffer

293 rv.extended_buffer = self.extended_buffer

294 rv.eval_ctx = self.eval_ctx

295 rv.parent = self

296 rv.require_output_check = self.require_output_check

297 rv.has_known_extends = self.has_known_extends

298 rv.toplevel = self.toplevel

299 rv.rootlevel = self.rootlevel

300 rv.block_frame = self.block_frame

301 rv.require_yield = self.require_yield

302 rv.buffer_count = self.buffer_count

303 rv.is_async = self.is_async

304 rv.block_counters = self.block_counters.copy() # noqa: FURB145

305 rv.block_frame_id = self.block_frame_id

306 return rv

307

308 def inspect(self, nodes: t.Any | None = None) -> None:

309 if nodes:

310 for node in nodes:

311 self.symbols.analyze_node(node)

312

313 def push_scope(self) -> None:

314 pass

315

316 def pop_scope(self) -> None:

317 pass

318

319 def find_break(self) -> bool:

320 return False

321

322 def find_continue(self) -> bool:

323 return False

324

325 def inner(self) -> "AsyncFrame":

326 """Create an inner frame."""

327 return self.copy()

328

329

330 class AsyncCodeGenerator(CodeGenerator):

331 environment: t.Any

332 name: str

333 filename: str

334 stream: t.Any

335 extends_so_far: int

336 has_known_extends: bool

337 root_frame_class: type[AsyncFrame] = AsyncFrame

338 eval_ctx: t.Any = None

339 is_async: bool = True

340 last_identifier: int = 0

341 identifiers: dict[str, t.Any] = {}

342 import_aliases: dict[str, t.Any] = {}

343 blocks: dict[str, t.Any] = {}

344 extends_buffer: t.Any = None

345 required_blocks: set[str] = set()

346 has_super: bool = False

347 macro_frames: list[AsyncFrame] = []

348

349 # Fast lookup cache for common variable names

350 _COMMON_VARS = frozenset(

351 [

352 "context",

353 "environment",

354 "eval_ctx",

355 "undefined",

356 "item",

357 "loop",

358 "block",

359 "value",

360 "name",

361 "key",

362 ]

363 )

364

365 def __init__(

366 self, environment: t.Any, name: str, filename: str, defer_init: bool = False

367 ) -> None:

368 super().__init__(

369 environment, name, filename, stream=None, defer_init=defer_init

370 )

371 self.extends_so_far = 0

372 self.has_known_extends = False

373 self.has_super = False

374 self.last_identifier = 0

375 self.identifiers = {}

376 self.import_aliases = {}

377 self.blocks = {}

378 self.extends_buffer = None

379 self.required_blocks = set()

380 self.is_async = True

381 self.macro_frames = []

382

383 # Initialize assignment tracking stack

384 self._assign_stack: list[set[str]] = []

385

386 # Initialize utility classes for better code organization

387 self._dependency_resolver = DependencyResolver(self)

388 self._loop_generator = LoopCodeGenerator(self)

389

390 # Note: Don't pre-populate self.filters here as it interferes with

391 # the pull_dependencies method that creates temporary identifiers

392

393 from jinja2.nodes import EvalContext

394

395 if self.eval_ctx is None:

396 self.eval_ctx = EvalContext(self.environment, self.name)

397

398 def choose_async(self, async_fmt: str = "async ", sync_fmt: str = "") -> str: # type: ignore[override]

399 return async_fmt if self.environment.enable_async else sync_fmt

400

401 def simple_write(self, value: str, frame: Frame) -> None: # type: ignore[override]

402 self.writeline(f"yield {value}")

403

404 def func_code_generator(self, frame: Frame) -> str:

405 async_frame = t.cast(AsyncFrame, frame)

406 return "async def" if async_frame.is_async else "def"

407

408 def func(self, name: str) -> str:

409 """Generate a function declaration for the given name."""

410 return f"def {name}"

411

412 def enter_frame(self, frame: Frame) -> None:

413 """Enter a new frame context."""

414 pass

415

416 def leave_frame(self, frame: Frame, with_python_scope: bool = False) -> None:

417 """Leave a frame context."""

418 pass

419

420 def return_buffer_contents(

421 self,

422 frame: Frame,

423 force_unescaped: bool = False, # noqa: ARG002

424 ) -> None:

425 _ = force_unescaped

426 if frame.buffer is not None:

427 self.writeline(f"return ''.join({frame.buffer})")

428

429 def visit_AsyncFor(self, node: t.Any, frame: Frame) -> None:

430 frame = t.cast(AsyncFrame, frame)

431 if hasattr(node, "recursive") and node.recursive:

432 raise NotImplementedError("Recursive loops not supported")

433 target = node.target

434 item = target.name if hasattr(target, "name") else "item"

435 frame.symbols.store(item)

436 self.writeline(f"{item} = None")

437 loop_filter = None

438 if hasattr(node, "test") and node.test:

439 loop_filter = self.temporary_identifier()

440 self.writeline(f"{loop_filter} = ", node.test)

441 self.visit(node.test, frame)

442 loop_var = self.temporary_identifier()

443 self.writeline(f"{loop_var} = -1", node)

444 self.writeline(f"async for {item} in ", node.iter)

445 self.visit(node.iter, frame)

446 self.write(":")

447 self.indent()

448 self.writeline(f"{loop_var} += 1")

449 if hasattr(node, "test") and node.test and loop_filter:

450 self.writeline(f"if {loop_filter}({item}):")

451 self.indent()

452 if hasattr(node, "body"):

453 self.blockvisit(node.body, frame)

454 if hasattr(node, "test") and node.test and loop_filter:

455 self.outdent()

456 self.outdent()

457 if hasattr(node, "else_") and node.else_:

458 self.writeline(f"if {loop_var} == -1:")

459 self.indent()

460 self.blockvisit(node.else_, frame)

461 self.outdent()

462

463 def visit_AsyncCall(self, node: t.Any, frame: Frame) -> None:

464 self.write("await ")

465 self.visit_Call(node, frame)

466

467 def visit_AsyncFilterBlock(self, node: t.Any, frame: Frame) -> None:

468 frame = t.cast(AsyncFrame, frame)

469 if not hasattr(node, "filter"):

470 return

471 if not hasattr(node, "body"):

472 return

473 filter_node = node.filter

474 buffer = self.temporary_identifier()

475 self.writeline(f"{buffer} = []")

476 asyncframe = frame.copy() # noqa: FURB145

477 asyncframe.buffer = buffer

478 asyncframe.toplevel = False

479 self.blockvisit(node.body, asyncframe)

480 self.writeline("await ", filter_node)

481 self.visit(filter_node, frame)

482 self.write(f"(''.join({buffer}))")

483

484 def visit_AsyncBlock(self, node: t.Any, frame: Frame) -> None:

485 frame = t.cast(AsyncFrame, frame)

486 if not hasattr(node, "name"):

487 return

488 if not hasattr(node, "body"):

489 return

490 block_name = node.name

491 self.writeline(f"blocks[{block_name! r}] = []")

492 block_func_name = f"block_{block_name}"

493 self.writeline(f"async def {block_func_name}(context):")

494 self.indent()

495 self.writeline("yield ''")

496 if node.body:

497 self.blockvisit(node.body, frame)

498 self.outdent()

499 self.writeline(f"blocks[{block_name! r}].append({block_func_name})")

500

501 def visit_Name(self, node: nodes.Name, frame: Frame) -> None:

502 frame = t.cast(AsyncFrame, frame)

503 self._handle_assignment_tracking(node, frame)

504 if self._handle_special_names(node):

505 return

506 self._handle_symbol_name(node, frame)

507

508 def _handle_assignment_tracking(self, node: nodes.Name, frame: AsyncFrame) -> None:

509 if node.ctx == "store":

510 # Store the variable in the frame's symbols

511 frame.symbols.store(node.name)

512 # Add to assignment tracking for context updates

513 if frame.toplevel or frame.loop_frame or frame.block_frame:

514 if hasattr(self, "_assign_stack") and self._assign_stack:

515 self._assign_stack[-1].add(node.name)

516

517 def _handle_special_names(self, node: nodes.Name) -> bool:

518 if node.name in ("blocks", "debug_info"):

519 self.write(node.name)

520 return True

521 return False

522

523 def _handle_symbol_name(self, node: nodes.Name, frame: AsyncFrame) -> None:

524 # Fast path for common variables

525 if node.name in self._COMMON_VARS and node.ctx == "load":

526 try:

527 ref = frame.symbols.ref(node.name)

528 self.write(ref)

529 return

530 except AssertionError:

531 self.write(f"context.get({node.name! r})")

532 return

533

534 # Standard path for other variables

535 try:

536 ref = frame.symbols.ref(node.name)

537 if node.ctx == "load" and self._should_use_undefined_check(ref, frame):

538 self.write(

539 f"(undefined(name={node.name! r}) if {ref} is missing else {ref})"

540 )

541 else:

542 self.write(ref)

543 except AssertionError:

544 if node.ctx == "load":

545 self.write(f"context.get({node.name! r})")

546 else:

547 self.write(f"context.vars[{node.name! r}]")

548

549 def _should_use_undefined_check(self, ref: str, frame: AsyncFrame) -> bool:

550 from jinja2.compiler import VAR_LOAD_PARAMETER

551

552 load = frame.symbols.find_load(ref)

553 return not (

554 load is not None

555 and load[0] == VAR_LOAD_PARAMETER

556 and hasattr(self, "parameter_is_undeclared")

557 and not self.parameter_is_undeclared(ref)

558 )

559

560 def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None:

561 """Find all filter and test names used in the template and assign them to variables."""

562 from jinja2.compiler import DependencyFinderVisitor

563

564 visitor = DependencyFinderVisitor()

565 for node in nodes:

566 visitor.visit(node)

567

568 # Set up filter dependencies using utility class

569 for name in sorted(visitor.filters):

570 self._dependency_resolver.setup_filter_dependency(name)

571

572 # Set up test dependencies using utility class

573 for name in sorted(visitor.tests):

574 self._dependency_resolver.setup_test_dependency(name)

575

576 def generate(self, node: nodes.Template) -> str:

577 self.writeline(f"name = {self.name! r}")

578 self.writeline("blocks = {}")

579 self.writeline("debug_info = None")

580

581 # Use optimized cached imports for better performance

582 for import_line in CompiledPatterns.get_optimized_imports().split("\n"):

583 self.writeline(import_line)

584 self.writeline("def undefined(name=None, **_):")

585 self.indent()

586 self.writeline("return Undefined(name=name)")

587 self.outdent()

588 self.writeline("async def auto_await(value):")

589 self.indent()

590 self.writeline("if hasattr(value, '__await__'):")

591 self.indent()

592 self.writeline("return await value")

593 self.outdent()

594 self.writeline("return value")

595 self.outdent()

596 self.writeline("filters = DEFAULT_FILTERS.copy()")

597 self.writeline("filters['escape'] = escape")

598 self.writeline("async def root(context):")

599 self.indent()

600 self.writeline("parent_template = None")

601 self.writeline("environment = context.environment")

602 self.writeline("eval_ctx = context.eval_ctx")

603 self.writeline("undefined = environment.undefined")

604 from jinja2.nodes import EvalContext

605

606 if self.eval_ctx is None:

607 self.eval_ctx = EvalContext(self.environment, self.name)

608 frame = self.root_frame_class(eval_ctx=self.eval_ctx)

609 frame.toplevel = frame.rootlevel = True

610 frame.require_output_check = False

611 frame.buffer = None

612 for macro in node.find_all(nodes.Macro):

613 frame.symbols.store(macro.name)

614

615 # Pull dependencies for filters and tests

616 self.pull_dependencies(node.body)

617

618 self.blockvisit(node.body, frame)

619 self.outdent()

620

621 # Apply pattern-based optimizations to generated code

622 generated_code = self.stream.getvalue()

623 return CompiledPatterns.optimize_generated_code(generated_code)

624

625 @classmethod

626 def compile_with_cache(

627 cls, environment: t.Any, source: str, name: str, filename: str

628 ) -> str:

629 """Compile template with caching support for improved performance."""

630 # Generate cache key

631 env_id = f"{id(environment)}:{environment.is_async}"

632 cache_key = _compilation_cache.get_cache_key(source, env_id)

633

634 # Check cache first

635 cached_code = _compilation_cache.get(cache_key)

636 if cached_code is not None:

637 return cached_code

638

639 # Compile and cache

640 generator = cls(environment, name, filename)

641 from jinja2 import parse

642

643 ast = parse(source, name, filename)

644 compiled_code = generator.generate(ast)

645

646 # Store in cache

647 _compilation_cache.set(cache_key, compiled_code)

648 return compiled_code

649

650 def visit_For(self, node: nodes.For, frame: Frame) -> None:

651 frame = t.cast(AsyncFrame, frame)

652 if node.recursive:

653 raise NotImplementedError("Recursive loops not supported")

654

655 # Create frames and setup

656 loop_frame, test_frame, else_frame = self._setup_for_frames(frame)

657 extended_loop, loop_ref = self._setup_for_loop_context(node, loop_frame)

658

659 # Analyze nodes for variable declarations

660 self._analyze_for_nodes(node, loop_frame, else_frame)

661

662 # Handle loop filter

663 loop_filter_func = self._setup_for_filter(node, test_frame, loop_frame)

664

665 # Setup loop variables and checks

666 self._setup_for_variables(node, extended_loop, loop_ref)

667

668 # Generate main loop

669 iteration_indicator = self._generate_for_loop(

670 node, frame, loop_frame, extended_loop, loop_ref, loop_filter_func

671 )

672

673 # Handle else clause

674 self._handle_for_else(node, else_frame, iteration_indicator)

675

676 # Cleanup

677 self._cleanup_for_assignments(loop_frame)

678

679 def _setup_for_frames(

680 self, frame: AsyncFrame

681 ) -> tuple[AsyncFrame, AsyncFrame, AsyncFrame]:

682 """Setup frames for different scopes in for loop."""

683 loop_frame = frame.inner()

684 loop_frame.loop_frame = True

685 test_frame = frame.inner()

686 else_frame = frame.inner()

687 return loop_frame, test_frame, else_frame

688

689 def _setup_for_loop_context(

690 self, node: nodes.For, loop_frame: AsyncFrame

691 ) -> tuple[bool, str | None]:

692 """Setup extended loop context and loop reference."""

693 extended_loop = (

694 node.recursive

695 or "loop"

696 in find_undeclared(node.iter_child_nodes(only=("body",)), ("loop",))

697 or any(block.scoped for block in node.find_all(nodes.Block))

698 )

699

700 loop_ref = None

701 if extended_loop:

702 loop_ref = loop_frame.symbols.declare_parameter("loop")

703

704 return extended_loop, loop_ref

705

706 def _analyze_for_nodes(

707 self, node: nodes.For, loop_frame: AsyncFrame, else_frame: AsyncFrame

708 ) -> None:

709 """Analyze nodes for variable declarations."""

710 loop_frame.symbols.analyze_node(node, for_branch="body")

711 if node.else_:

712 else_frame.symbols.analyze_node(node, for_branch="else")

713

714 def _setup_for_filter(

715 self, node: nodes.For, test_frame: AsyncFrame, loop_frame: AsyncFrame

716 ) -> str | None:

717 """Setup loop filter if present."""

718 if not node.test:

719 return None

720

721 loop_filter_func = self.temporary_identifier()

722 test_frame.symbols.analyze_node(node, for_branch="test")

723 self.writeline(f"{self.func(loop_filter_func)}(filter):", node.test)

724 self.indent()

725 self.enter_frame(test_frame)

726 self.writeline(self.choose_async("async for ", "for "))

727 self.visit(node.target, loop_frame)

728 self.write(" in ")

729 self.write(self.choose_async("auto_aiter(filter)", "filter"))

730 self.write(":")

731 self.indent()

732 self.writeline("if ", node.test)

733 self.visit(node.test, test_frame)

734 self.write(":")

735 self.indent()

736 self.writeline("yield ")

737 self.visit(node.target, loop_frame)

738 self.outdent(3)

739 self.leave_frame(test_frame, with_python_scope=True)

740 return loop_filter_func

741

742 def _setup_for_variables(

743 self, node: nodes.For, extended_loop: bool, loop_ref: str | None

744 ) -> None:

745 """Setup loop variables and check for conflicts."""

746 if extended_loop and loop_ref:

747 self.writeline(f"{loop_ref} = missing")

748

749 for name in node.find_all(nodes.Name):

750 if name.ctx == "store" and name.name == "loop":

751 self.fail(

752 "Can't assign to special loop variable in for-loop target",

753 name.lineno,

754 )

755

756 def _generate_for_loop(

757 self,

758 node: nodes.For,

759 frame: AsyncFrame,

760 loop_frame: AsyncFrame,

761 extended_loop: bool,

762 loop_ref: str | None,

763 loop_filter_func: str | None,

764 ) -> str | None:

765 """Generate the main for loop code."""

766 # Handle else clause iteration indicator

767 iteration_indicator = None

768 if node.else_:

769 iteration_indicator = self.temporary_identifier()

770 self.writeline(f"{iteration_indicator} = 1")

771

772 # Generate the main loop using utility class

773 self._loop_generator.generate_async_for_header(node, node.target, loop_frame)

774 self._loop_generator.generate_loop_iterator(

775 node.iter, frame, extended_loop, loop_ref, loop_filter_func

776 )

777

778 self.indent()

779 self.enter_frame(loop_frame)

780

781 self.writeline("_loop_vars = {}")

782 self.blockvisit(node.body, loop_frame)

783 if node.else_:

784 self.writeline(f"{iteration_indicator} = 0")

785 self.outdent()

786 self.leave_frame(loop_frame, with_python_scope=not node.else_)

787

788 return iteration_indicator

789

790 def _handle_for_else(

791 self, node: nodes.For, else_frame: AsyncFrame, iteration_indicator: str | None

792 ) -> None:

793 """Handle the else clause of for loop."""

794 if not node.else_ or not iteration_indicator:

795 return

796

797 self.writeline(f"if {iteration_indicator}:")

798 self.indent()

799 self.enter_frame(else_frame)

800 self.blockvisit(node.else_, else_frame)

801 self.leave_frame(else_frame)

802 self.outdent()

803

804 def _cleanup_for_assignments(self, loop_frame: AsyncFrame) -> None:

805 """Clear assignments made in the loop from the top level."""

806 if hasattr(self, "_assign_stack") and self._assign_stack:

807 self._assign_stack[-1].difference_update(loop_frame.symbols.stores)

808

809 def visit_Macro(self, node: nodes.Macro, frame: Frame) -> None:

810 """Visit a macro node and generate async-aware code."""

811 frame = t.cast(AsyncFrame, frame)

812

813 # For now, let's just use the base class implementation without modification

814 # This ensures macros work in sync mode, and we can enhance async support later

815 super().visit_Macro(node, frame)

816

817 def visit_Block(self, node: nodes.Block, frame: Frame) -> None:

818 frame = t.cast(AsyncFrame, frame)

819 block_name = node.name

820 self.writeline(f"blocks[{block_name! r}] = []")

821 block_func_name = f"block_{block_name}"

822 self.writeline(f"{self.choose_async()}def {block_func_name}(context):")

823 self.indent()

824 self.writeline("yield ''")

825 if node.body:

826 self.blockvisit(node.body, frame)

827 self.outdent()

828 self.writeline(f"blocks[{block_name! r}].append({block_func_name})")

829 level = 0

830 if frame.toplevel:

831 if self.has_known_extends:

832 return

833 if self.extends_so_far > 0:

834 self.writeline("if parent_template is None:")

835 self.indent()

836 level += 1

837 if node.scoped:

838 context = self.derive_context(frame)

839 else:

840 context = self.get_context_ref()

841 if node.required:

842 self.writeline(f"if len(context.blocks[{node.name! r}]) <= 1:", node)

843 self.indent()

844 self.writeline(

845 f'raise TemplateRuntimeError("Required block {node.name! r} not found")',

846 node,

847 )

848 self.outdent()

849 self.writeline("try:", node)

850 self.indent()

851 self.writeline(

852 f"async for event in context.blocks[{node.name! r}][0]({context}):",

853 node,

854 )

855 self.indent()

856 self.simple_write("event", frame)

857 self.outdent()

858 self.outdent()

859 self.writeline("except KeyError:", node)

860 self.indent()

861 self.writeline("yield ''")

862 self.outdent()

863 self.outdent(level)

864

865 def visit_Extends(self, node: nodes.Extends, frame: Frame) -> None:

866 frame = t.cast(AsyncFrame, frame)

867 if not frame.require_output_check:

868 raise CompilerExit()

869 if not frame.toplevel:

870 self.fail("cannot use extend from a non top-level scope", node.lineno)

871 if self.extends_so_far > 0:

872 if not self.has_known_extends:

873 self.writeline("if parent_template is not None:")

874 self.indent()

875 self.writeline('raise TemplateRuntimeError("extended multiple times")')

876 if self.has_known_extends:

877 raise CompilerExit()

878 else:

879 self.outdent()

880 self.writeline("parent_template = await environment.get_template_async(", node)

881 self.visit(node.template, frame)

882 self.write(f", {self.name! r})")

883 self.writeline("for name, parent_block in parent_template.blocks.items():")

884 self.indent()

885 self.writeline("context.blocks.setdefault(name, []).append(parent_block)")

886 self.outdent()

887 if frame.rootlevel:

888 self.has_known_extends = True

889 self.extends_so_far += 1

890

891 def visit_Include(self, node: nodes.Include, frame: Frame) -> None:

892 frame = t.cast(AsyncFrame, frame)

893 if node.ignore_missing:

894 self.writeline("try:")

895 self.indent()

896 self.writeline("template = await environment.get_template_async(", node)

897 self.visit(node.template, frame)

898 self.write(f", {self.name! r})")

899 if node.ignore_missing:

900 self.outdent()

901 self.writeline("except TemplateNotFound:")

902 self.indent()

903 self.writeline("pass")

904 self.outdent()

905 self.writeline("else:")

906 self.indent()

907 if node.with_context:

908 self.writeline(

909 f"async for event in template.root_render_func(template.new_context(context.get_all(), True, {self.dump_local_context(frame)})):"

910 )

911 else:

912 self.writeline(

913 "async for event in (await template._get_default_module_async())._body_stream:"

914 )

915 self.indent()

916 self.simple_write("event", frame)

917 self.outdent()

918 if node.ignore_missing:

919 self.outdent()

920

921 def _import_common(

922 self, node: nodes.Import | nodes.FromImport, frame: Frame

923 ) -> None:

924 frame = t.cast(AsyncFrame, frame)

925 self.writeline("template = await environment.get_template_async(", node)

926 self.visit(node.template, frame)

927 self.write(f", {self.name! r})")

928

929 def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None:

930 """Visit a filter node and generate async-aware code."""

931 frame = t.cast(AsyncFrame, frame)

932

933 filter_ref = self._get_filter_reference(node)

934 func = self.environment.filters.get(node.name)

935

936 if self.environment.is_async:

937 self.write("(await auto_await(")

938

939 self.write(f"{filter_ref}(")

940 self._write_filter_special_params(func)

941 self._write_filter_input(node, frame)

942 self._write_filter_arguments(node, frame)

943 self.write(")")

944

945 if self.environment.is_async:

946 self.write("))")

947

948 def _get_filter_reference(self, node: nodes.Filter) -> str:

949 """Get the filter reference from dependencies or fallback to environment."""

950 if node.name in self.filters:

951 return self.filters[node.name]

952 return f"environment.filters[{node.name! r}]"

953

954 def _write_filter_special_params(self, func: t.Any) -> None:

955 """Write special parameters that some filters need."""

956 from jinja2.compiler import _PassArg

957

958 pass_arg = None

959 if func:

960 pass_arg_type = _PassArg.from_obj(func)

961 if pass_arg_type:

962 pass_arg = {

963 _PassArg.context: "context",

964 _PassArg.eval_context: "context.eval_ctx",

965 _PassArg.environment: "environment",

966 }.get(pass_arg_type)

967

968 if pass_arg is not None:

969 self.write(f"{pass_arg}, ")

970

971 def _write_filter_input(self, node: nodes.Filter, frame: AsyncFrame) -> None:

972 """Write the filter input value."""

973 if node.node is not None:

974 self.visit(node.node, frame)

975 elif frame.buffer is not None:

976 self._write_buffer_content(frame)

977

978 def _write_buffer_content(self, frame: AsyncFrame) -> None:

979 """Write buffer content for filter blocks."""

980 if frame.eval_ctx.volatile:

981 self.write(

982 f"(Markup(concat({frame.buffer}))"

983 f" if context.eval_ctx.autoescape else concat({frame.buffer}))"

984 )

985 elif frame.eval_ctx.autoescape:

986 self.write(f"Markup(concat({frame.buffer}))")

987 else:

988 self.write(f"concat({frame.buffer})")

989

990 def _write_filter_arguments(self, node: nodes.Filter, frame: AsyncFrame) -> None:

991 """Write filter arguments and keyword arguments."""

992 for arg in node.args:

993 self.write(", ")

994 self.visit(arg, frame)

995

996 for kwarg in node.kwargs:

997 self.write(", ")

998 self.visit(kwarg, frame)

999

1000 if node.dyn_args:

1001 self.write(", *")

1002 self.visit(node.dyn_args, frame)

1003

1004 if node.dyn_kwargs:

1005 self.write(", **")

1006 self.visit(node.dyn_kwargs, frame)

1007

1008 def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:

1009 """Visit an assignment node ({% set %} statements)."""

1010 frame = t.cast(AsyncFrame, frame)

1011 self.push_assign_tracking()

1012

1013 # Check for namespace assignments like `ns.var = value`

1014 seen_refs: set[str] = set()

1015 for nsref in node.find_all(nodes.NSRef):

1016 if nsref.name in seen_refs:

1017 continue

1018 seen_refs.add(nsref.name)

1019 ref = frame.symbols.ref(nsref.name)

1020 self.writeline(f"if not isinstance({ref}, Namespace):")

1021 self.indent()

1022 self.writeline(

1023 "raise TemplateRuntimeError"

1024 '("cannot assign attribute on non-namespace object")'

1025 )

1026 self.outdent()

1027

1028 # Generate the assignment code

1029 self.newline(node)

1030 self.visit(node.target, frame)

1031 self.write(" = ")

1032 self.visit(node.node, frame)

1033 self.pop_assign_tracking(frame)

1034

1035 def push_assign_tracking(self) -> None:

1036 """Push a new layer for assignment tracking."""

1037 self._assign_stack.append(set())

1038

1039 def pop_assign_tracking(self, frame: Frame) -> None:

1040 """Pop the topmost level for assignment tracking and update context variables."""

1041 frame = t.cast(AsyncFrame, frame)

1042 vars_set = self._assign_stack.pop()

1043

1044 if (

1045 not frame.block_frame

1046 and not frame.loop_frame

1047 and not frame.toplevel

1048 or not vars_set

1049 ):

1050 return

1051

1052 public_names = [x for x in vars_set if x[:1] != "_"]

1053

1054 if len(vars_set) == 1:

1055 name = next(iter(vars_set))

1056 ref = frame.symbols.ref(name)

1057 if frame.loop_frame:

1058 self.writeline(f"_loop_vars[{name! r}] = {ref}")

1059 return

1060 if frame.block_frame:

1061 self.writeline(f"_block_vars[{name! r}] = {ref}")

1062 return

1063 self.writeline(f"context.vars[{name! r}] = {ref}")

1064 else:

1065 if frame.loop_frame:

1066 self.writeline("_loop_vars.update({")

1067 elif frame.block_frame:

1068 self.writeline("_block_vars.update({")

1069 else:

1070 self.writeline("context.vars.update({")

1071 for idx, name in enumerate(sorted(vars_set)):

1072 if idx:

1073 self.write(", ")

1074 ref = frame.symbols.ref(name)

1075 self.write(f"{name! r}: {ref}")

1076 self.write("})")

1077

1078 if not frame.block_frame and not frame.loop_frame and public_names:

1079 if len(public_names) == 1:

1080 self.writeline(f"context.exported_vars.add({public_names[0]!r})")

1081 else:

1082 names_str = ", ".join(map(repr, sorted(public_names)))

1083 self.writeline(f"context.exported_vars.update(({names_str}))")