Coverage for fastblocks/adapters/templates/advanced_manager.py: 27%
406 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-29 00:51 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-29 00:51 -0700
1"""Advanced Template Management System for FastBlocks Week 7-8.
3This module provides advanced Jinja2 template management with enhanced features:
4- Template syntax checking and validation with line-by-line error reporting
5- Fragment and partial template support for HTMX
6- Template variable autocomplete for adapter functions
7- Enhanced security with sandboxed environments
8- Performance optimization with advanced caching
9- Context-aware template suggestions
11Requirements:
12- jinja2>=3.1.6
13- jinja2-async-environment>=0.14.3
14- starlette-async-jinja>=1.12.4
16Author: lesleslie <les@wedgwoodwebworks.com>
17Created: 2025-01-12
18"""
20import asyncio
21import re
22import typing as t
23from contextlib import suppress
24from dataclasses import dataclass, field
25from enum import Enum
26from uuid import UUID
28from acb.adapters import AdapterStatus
29from acb.depends import depends
30from jinja2 import (
31 Environment,
32 StrictUndefined,
33 Template,
34 TemplateError,
35 TemplateNotFound,
36 TemplateSyntaxError,
37 UndefinedError,
38 meta,
39)
41try:
42 from jinja2.sandbox import SandboxedEnvironment
43except ImportError:
44 # Fallback for older Jinja2 versions
45 SandboxedEnvironment = Environment
46from jinja2.runtime import StrictUndefined as RuntimeStrictUndefined
48from .jinja2 import Templates, TemplatesSettings
51class ValidationLevel(Enum):
52 """Template validation levels."""
54 SYNTAX_ONLY = "syntax_only"
55 VARIABLES = "variables"
56 FULL = "full"
59class SecurityLevel(Enum):
60 """Template security levels."""
62 STANDARD = "standard"
63 RESTRICTED = "restricted"
64 SANDBOXED = "sandboxed"
67@dataclass
68class TemplateError:
69 """Represents a template validation error."""
71 message: str
72 line_number: int | None = None
73 column_number: int | None = None
74 error_type: str = "validation"
75 severity: str = "error" # error, warning, info
76 template_name: str | None = None
77 context: str | None = None # surrounding code context
80@dataclass
81class TemplateValidationResult:
82 """Result of template validation."""
84 is_valid: bool
85 errors: list[TemplateError] = field(default_factory=list)
86 warnings: list[TemplateError] = field(default_factory=list)
87 suggestions: list[str] = field(default_factory=list)
88 used_variables: set[str] = field(default_factory=set)
89 undefined_variables: set[str] = field(default_factory=set)
90 available_filters: set[str] = field(default_factory=set)
91 available_functions: set[str] = field(default_factory=set)
94@dataclass
95class FragmentInfo:
96 """Information about a template fragment."""
98 name: str
99 template_path: str
100 block_name: str | None = None
101 start_line: int | None = None
102 end_line: int | None = None
103 variables: set[str] = field(default_factory=set)
104 dependencies: set[str] = field(default_factory=set)
107@dataclass
108class AutocompleteItem:
109 """Autocomplete suggestion item."""
111 name: str
112 type: str # variable, filter, function, block
113 description: str | None = None
114 signature: str | None = None
115 adapter_source: str | None = None
116 example: str | None = None
119class AdvancedTemplatesSettings(TemplatesSettings):
120 """Advanced template settings with enhanced features."""
122 # Validation settings
123 validation_level: ValidationLevel = ValidationLevel.VARIABLES
124 validate_on_load: bool = True
125 strict_undefined: bool = True
127 # Security settings
128 security_level: SecurityLevel = SecurityLevel.STANDARD
129 sandbox_allowed_attributes: list[str] = field(
130 default_factory=lambda: [
131 "alt",
132 "class",
133 "id",
134 "src",
135 "href",
136 "title",
137 "width",
138 "height",
139 ]
140 )
141 sandbox_allowed_tags: list[str] = field(
142 default_factory=lambda: [
143 "div",
144 "span",
145 "p",
146 "a",
147 "img",
148 "h1",
149 "h2",
150 "h3",
151 "h4",
152 "h5",
153 "h6",
154 "ul",
155 "ol",
156 "li",
157 "strong",
158 "em",
159 "br",
160 "hr",
161 ]
162 )
164 # Fragment/Partial settings
165 enable_fragments: bool = True
166 fragment_prefix: str = "_"
167 auto_discover_fragments: bool = True
169 # Autocomplete settings
170 enable_autocomplete: bool = True
171 scan_adapter_functions: bool = True
172 cache_autocomplete: bool = True
174 # Performance settings
175 enable_template_cache: bool = True
176 template_cache_size: int = 1000
177 enable_compiled_cache: bool = True
178 precompile_templates: bool = False
180 # Advanced error handling
181 detailed_errors: bool = True
182 show_context_lines: int = 3
183 enable_error_suggestions: bool = True
185 def __init__(self, **data: t.Any) -> None:
186 super().__init__(**data)
189class AdvancedTemplateManager:
190 """Advanced template management with validation, fragments, and autocomplete."""
192 def __init__(self, settings: AdvancedTemplatesSettings | None = None) -> None:
193 self.settings = settings or AdvancedTemplatesSettings()
194 self.base_templates: Templates | None = None
195 self._validation_cache: dict[str, TemplateValidationResult] = {}
196 self._fragment_cache: dict[str, list[FragmentInfo]] = {}
197 self._autocomplete_cache: dict[str, list[AutocompleteItem]] = {}
198 self._template_dependencies: dict[str, set[str]] = {}
200 async def initialize(self) -> None:
201 """Initialize the advanced template manager."""
202 # Get base templates instance
203 try:
204 self.base_templates = depends.get("templates")
205 except Exception:
206 self.base_templates = Templates()
207 if not self.base_templates.app:
208 await self.base_templates.init()
210 # Setup advanced features
211 if self.settings.enable_fragments:
212 await self._discover_fragments()
214 if self.settings.enable_autocomplete:
215 await self._build_autocomplete_index()
217 def _get_template_environment(self, secure: bool = False) -> Environment:
218 """Get Jinja2 environment with appropriate security settings."""
219 if not self.base_templates or not self.base_templates.app:
220 raise RuntimeError("Base templates not initialized")
222 env = self.base_templates.app.env
224 if secure and self.settings.security_level == SecurityLevel.SANDBOXED:
225 # Create sandboxed environment
226 sandbox_env = SandboxedEnvironment(
227 loader=env.loader,
228 extensions=env.extensions,
229 undefined=StrictUndefined
230 if self.settings.strict_undefined
231 else RuntimeStrictUndefined,
232 )
234 # Apply security restrictions
235 sandbox_env.allowed_tags = set(self.settings.sandbox_allowed_tags)
236 sandbox_env.allowed_attributes = set(
237 self.settings.sandbox_allowed_attributes
238 )
240 return sandbox_env
242 return env
244 async def validate_template(
245 self,
246 template_source: str,
247 template_name: str = "unknown",
248 context: dict[str, t.Any] | None = None,
249 ) -> TemplateValidationResult:
250 """Validate template syntax and variables with detailed error reporting."""
251 # Check cache first
252 cache_key = f"{template_name}:{hash(template_source)}"
253 if cache_key in self._validation_cache:
254 return self._validation_cache[cache_key]
256 result = TemplateValidationResult(is_valid=True)
257 env = self._get_template_environment()
259 try:
260 # Parse template for syntax validation
261 parsed = env.parse(template_source, template_name)
263 # Extract variables and blocks
264 used_vars = meta.find_undeclared_variables(parsed)
265 result.used_variables = used_vars
267 # Get available variables from context and adapters
268 available_vars = self._get_available_variables(context)
269 result.undefined_variables = used_vars - available_vars
271 # Get available filters and functions
272 result.available_filters = set(env.filters.keys())
273 result.available_functions = set(env.globals.keys())
275 # Validate variables if required
276 if self.settings.validation_level in (
277 ValidationLevel.VARIABLES,
278 ValidationLevel.FULL,
279 ):
280 await self._validate_variables(result, template_source, template_name)
282 # Full validation includes template compilation
283 if self.settings.validation_level == ValidationLevel.FULL:
284 await self._validate_compilation(
285 result, template_source, template_name, env
286 )
288 except TemplateSyntaxError as e:
289 result.is_valid = False
290 error = TemplateError(
291 message=str(e),
292 line_number=e.lineno,
293 error_type="syntax",
294 template_name=template_name,
295 context=self._get_error_context(template_source, e.lineno),
296 )
297 result.errors.append(error)
299 except Exception as e:
300 result.is_valid = False
301 error = TemplateError(
302 message=f"Validation error: {e}",
303 error_type="general",
304 template_name=template_name,
305 )
306 result.errors.append(error)
308 # Add suggestions for improvements
309 if self.settings.enable_error_suggestions:
310 await self._add_suggestions(result, template_source)
312 # Cache result
313 self._validation_cache[cache_key] = result
314 return result
316 def _get_available_variables(
317 self, context: dict[str, t.Any] | None = None
318 ) -> set[str]:
319 """Get all available variables from context and adapters."""
320 available = set()
322 # Add context variables
323 if context:
324 available.update(context.keys())
326 # Add adapter variables
327 available.update(
328 ["config", "request", "models", "render_block", "render_component"]
329 )
331 # Add adapter functions
332 try:
333 for adapter_name in [
334 "images",
335 "icons",
336 "fonts",
337 "styles",
338 "cache",
339 "storage",
340 ]:
341 try:
342 adapter = depends.get(adapter_name)
343 if adapter:
344 available.add(adapter_name)
345 except Exception:
346 pass
347 except Exception:
348 pass
350 return available
352 async def _validate_variables(
353 self, result: TemplateValidationResult, template_source: str, template_name: str
354 ) -> None:
355 """Validate variable usage in template."""
356 lines = template_source.split("\n")
358 for line_num, line in enumerate(lines, 1):
359 # Find variable usage patterns
360 var_pattern = re.compile(r"\[\[\s*([^|\[\]]+?)(?:\s*\|[^|\[\]]*?)?\s*\]\]")
361 matches = var_pattern.finditer(line)
363 for match in matches:
364 var_expr = match.group(1).strip()
365 base_var = var_expr.split(".")[0].split("(")[0].strip()
367 if base_var in result.undefined_variables:
368 error = TemplateError(
369 message=f"Undefined variable: {base_var}",
370 line_number=line_num,
371 column_number=match.start(),
372 error_type="undefined_variable",
373 severity="warning"
374 if self._is_safe_undefined(base_var)
375 else "error",
376 template_name=template_name,
377 context=line.strip(),
378 )
380 if error.severity == "error":
381 result.errors.append(error)
382 result.is_valid = False
383 else:
384 result.warnings.append(error)
386 def _is_safe_undefined(self, var_name: str) -> bool:
387 """Check if undefined variable is potentially safe (like optional context)."""
388 safe_patterns = ["user", "session", "flash", "messages", "csrf_token"]
389 return any(pattern in var_name.lower() for pattern in safe_patterns)
391 async def _validate_compilation(
392 self,
393 result: TemplateValidationResult,
394 template_source: str,
395 template_name: str,
396 env: Environment,
397 ) -> None:
398 """Validate template compilation with mock context."""
399 try:
400 template = env.from_string(template_source, template_class=Template)
402 # Create mock context for testing
403 mock_context = self._create_mock_context(result.used_variables)
405 # Try to render with mock context
406 await asyncio.get_event_loop().run_in_executor(
407 None, template.render, mock_context
408 )
410 except UndefinedError:
411 # This is expected for some undefined variables
412 pass
413 except Exception as e:
414 result.is_valid = False
415 error = TemplateError(
416 message=f"Compilation error: {e}",
417 error_type="compilation",
418 template_name=template_name,
419 )
420 result.errors.append(error)
422 def _create_mock_context(self, variables: set[str]) -> dict[str, t.Any]:
423 """Create mock context for template validation."""
424 mock_context = {}
426 for var in variables:
427 if "." in var:
428 # Handle nested variables
429 parts = var.split(".")
430 current = mock_context
431 for part in parts[:-1]:
432 if part not in current:
433 current[part] = {}
434 current = current[part]
435 current[parts[-1]] = "mock_value"
436 else:
437 mock_context[var] = "mock_value"
439 return mock_context
441 def _get_error_context(
442 self, template_source: str, line_number: int | None
443 ) -> str | None:
444 """Get surrounding lines for error context."""
445 if not line_number or not self.settings.detailed_errors:
446 return None
448 lines = template_source.split("\n")
449 start = max(0, line_number - self.settings.show_context_lines - 1)
450 end = min(len(lines), line_number + self.settings.show_context_lines)
452 context_lines = []
453 for i in range(start, end):
454 marker = ">>> " if i == line_number - 1 else " "
455 context_lines.append(f"{marker}{i + 1:4d}: {lines[i]}")
457 return "\n".join(context_lines)
459 async def _add_suggestions(
460 self, result: TemplateValidationResult, template_source: str
461 ) -> None:
462 """Add helpful suggestions for template improvements."""
463 suggestions = []
465 # Suggest available alternatives for undefined variables
466 for undefined_var in result.undefined_variables:
467 available = result.used_variables - result.undefined_variables
469 # Simple fuzzy matching for suggestions
470 for var in available:
471 if self._is_similar(undefined_var, var):
472 suggestions.append(
473 f"Did you mean '{var}' instead of '{undefined_var}'?"
474 )
475 break
477 # Suggest filters for common patterns
478 if "| safe" not in template_source and any(
479 tag in template_source for tag in ["<", ">"]
480 ):
481 suggestions.append("Consider using the '| safe' filter for HTML content")
483 # Suggest async patterns for image operations
484 if "image_url(" in template_source and "await" not in template_source:
485 suggestions.append(
486 "Consider using 'await async_image_url()' for better performance"
487 )
489 result.suggestions.extend(suggestions)
491 def _is_similar(self, a: str, b: str, threshold: float = 0.6) -> bool:
492 """Simple string similarity check."""
493 if not a or not b:
494 return False
496 # Levenshtein distance approximation
497 longer = a if len(a) > len(b) else b
498 shorter = b if len(a) > len(b) else a
500 if len(longer) == 0:
501 return True
503 # Simple similarity based on common characters
504 common = sum(1 for char in shorter if char in longer)
505 similarity = common / len(longer)
507 return similarity >= threshold
509 async def _discover_fragments(self) -> None:
510 """Discover and index template fragments for HTMX support."""
511 if not self.base_templates:
512 return
514 # Get all template paths
515 env = self._get_template_environment()
516 if not env.loader:
517 return
519 try:
520 template_names = await asyncio.get_event_loop().run_in_executor(
521 None, env.loader.list_templates
522 )
523 except Exception:
524 return
526 for template_name in template_names:
527 if template_name.startswith(self.settings.fragment_prefix):
528 await self._analyze_fragment(template_name)
530 async def _analyze_fragment(self, template_name: str) -> None:
531 """Analyze a template fragment and extract metadata."""
532 try:
533 env = self._get_template_environment()
534 source, _ = env.loader.get_source(env, template_name)
536 # Parse template to find blocks
537 parsed = env.parse(source, template_name)
539 fragments = []
541 # Extract block information
542 for node in parsed.body:
543 if hasattr(node, "name") and node.name:
544 fragment = FragmentInfo(
545 name=node.name,
546 template_path=template_name,
547 block_name=node.name,
548 start_line=getattr(node, "lineno", None),
549 )
551 # Find variables used in this fragment
552 fragment.variables = meta.find_undeclared_variables(parsed)
553 fragments.append(fragment)
555 # If no blocks found, treat entire template as fragment
556 if not fragments:
557 fragment = FragmentInfo(
558 name=template_name.replace(".html", "").replace(
559 self.settings.fragment_prefix, ""
560 ),
561 template_path=template_name,
562 variables=meta.find_undeclared_variables(parsed),
563 )
564 fragments.append(fragment)
566 self._fragment_cache[template_name] = fragments
568 except Exception:
569 # Log error but continue
570 pass
572 async def _build_autocomplete_index(self) -> None:
573 """Build autocomplete index for template variables and functions."""
574 autocomplete_items = []
576 # Add built-in Jinja2 items
577 autocomplete_items.extend(self._get_builtin_autocomplete())
579 # Add adapter functions if enabled
580 if self.settings.scan_adapter_functions:
581 autocomplete_items.extend(await self._get_adapter_autocomplete())
583 # Add template-specific items
584 autocomplete_items.extend(self._get_template_autocomplete())
586 # Cache the results
587 cache_key = "global"
588 self._autocomplete_cache[cache_key] = autocomplete_items
590 def _get_builtin_autocomplete(self) -> list[AutocompleteItem]:
591 """Get autocomplete items for built-in Jinja2 features."""
592 items = []
594 # Built-in filters
595 builtin_filters = [
596 ("abs", "filter", "Return absolute value", "number|abs"),
597 ("attr", "filter", "Get attribute by name", "obj|attr('name')"),
598 ("batch", "filter", "Batch items into sublists", "items|batch(3)"),
599 ("capitalize", "filter", "Capitalize first letter", "text|capitalize"),
600 ("center", "filter", "Center text in field", "text|center(80)"),
601 (
602 "default",
603 "filter",
604 "Default value if undefined",
605 "var|default('fallback')",
606 ),
607 ("dictsort", "filter", "Sort dict by key/value", "dict|dictsort"),
608 ("escape", "filter", "Escape HTML characters", "text|escape"),
609 ("filesizeformat", "filter", "Format file size", "bytes|filesizeformat"),
610 ("first", "filter", "Get first item", "items|first"),
611 ("float", "filter", "Convert to float", "value|float"),
612 ("format", "filter", "String formatting", "'{0}'.format(value)"),
613 ("groupby", "filter", "Group by attribute", "items|groupby('category')"),
614 ("indent", "filter", "Indent text", "text|indent(4)"),
615 ("int", "filter", "Convert to integer", "value|int"),
616 ("join", "filter", "Join list with separator", "items|join(', ')"),
617 ("last", "filter", "Get last item", "items|last"),
618 ("length", "filter", "Get length", "items|length"),
619 ("list", "filter", "Convert to list", "value|list"),
620 ("lower", "filter", "Convert to lowercase", "text|lower"),
621 ("map", "filter", "Apply filter to each item", "items|map('upper')"),
622 ("max", "filter", "Get maximum value", "numbers|max"),
623 ("min", "filter", "Get minimum value", "numbers|min"),
624 ("random", "filter", "Get random item", "items|random"),
625 ("reject", "filter", "Reject items by test", "items|reject('odd')"),
626 ("replace", "filter", "Replace substring", "text|replace('old', 'new')"),
627 ("reverse", "filter", "Reverse order", "items|reverse"),
628 ("round", "filter", "Round number", "number|round(2)"),
629 ("safe", "filter", "Mark as safe HTML", "html|safe"),
630 ("select", "filter", "Select items by test", "items|select('even')"),
631 ("slice", "filter", "Slice sequence", "items|slice(3)"),
632 ("sort", "filter", "Sort items", "items|sort"),
633 ("string", "filter", "Convert to string", "value|string"),
634 ("striptags", "filter", "Remove HTML tags", "html|striptags"),
635 ("sum", "filter", "Sum numeric values", "numbers|sum"),
636 ("title", "filter", "Title case", "text|title"),
637 ("trim", "filter", "Strip whitespace", "text|trim"),
638 ("truncate", "filter", "Truncate text", "text|truncate(50)"),
639 ("unique", "filter", "Remove duplicates", "items|unique"),
640 ("upper", "filter", "Convert to uppercase", "text|upper"),
641 ("urlencode", "filter", "URL encode", "text|urlencode"),
642 ("urlize", "filter", "Convert URLs to links", "text|urlize"),
643 ("wordcount", "filter", "Count words", "text|wordcount"),
644 ("wordwrap", "filter", "Wrap text", "text|wordwrap(80)"),
645 ]
647 for name, item_type, description, example in builtin_filters:
648 items.append(
649 AutocompleteItem(
650 name=name,
651 type=item_type,
652 description=description,
653 example=example,
654 adapter_source="jinja2",
655 )
656 )
658 # Built-in functions
659 builtin_functions = [
660 (
661 "range",
662 "function",
663 "Generate sequence of numbers",
664 "range(10)",
665 "range(start, stop, step)",
666 ),
667 (
668 "lipsum",
669 "function",
670 "Generate lorem ipsum text",
671 "lipsum(5)",
672 "lipsum(n=5, html=True, min=20, max=100)",
673 ),
674 (
675 "dict",
676 "function",
677 "Create dictionary",
678 "dict(key='value')",
679 "dict(**kwargs)",
680 ),
681 (
682 "cycler",
683 "function",
684 "Create value cycler",
685 "cycler('odd', 'even')",
686 "cycler(*items)",
687 ),
688 (
689 "joiner",
690 "function",
691 "Create joiner helper",
692 "joiner(', ')",
693 "joiner(sep=', ')",
694 ),
695 ]
697 for name, item_type, description, example, signature in builtin_functions:
698 items.append(
699 AutocompleteItem(
700 name=name,
701 type=item_type,
702 description=description,
703 signature=signature,
704 example=example,
705 adapter_source="jinja2",
706 )
707 )
709 return items
711 async def _get_adapter_autocomplete(self) -> list[AutocompleteItem]:
712 """Get autocomplete items for adapter functions."""
713 items = []
715 # FastBlocks-specific filters from our filter modules
716 from .async_filters import FASTBLOCKS_ASYNC_FILTERS
717 from .filters import FASTBLOCKS_FILTERS
719 # Add sync filters
720 for name, func in FASTBLOCKS_FILTERS.items():
721 doc = func.__doc__ or ""
722 description = doc.split("\n")[0] if doc else f"FastBlocks {name} filter"
724 items.append(
725 AutocompleteItem(
726 name=name,
727 type="filter",
728 description=description,
729 adapter_source="fastblocks",
730 example=self._extract_example_from_doc(doc),
731 )
732 )
734 # Add async filters
735 for name, func in FASTBLOCKS_ASYNC_FILTERS.items():
736 doc = func.__doc__ or ""
737 description = (
738 doc.split("\n")[0] if doc else f"FastBlocks {name} async filter"
739 )
741 items.append(
742 AutocompleteItem(
743 name=name,
744 type="filter",
745 description=description,
746 adapter_source="fastblocks",
747 example=self._extract_example_from_doc(doc),
748 )
749 )
751 # Add adapter-specific functions
752 adapter_functions = {
753 "images": ["get_image_url", "get_img_tag", "get_placeholder_url"],
754 "icons": ["get_icon_tag", "get_icon_with_text"],
755 "fonts": ["get_font_import", "get_font_family"],
756 "styles": [
757 "get_component_class",
758 "get_utility_classes",
759 "build_component_html",
760 ],
761 }
763 for adapter_name, functions in adapter_functions.items():
764 try:
765 adapter = depends.get(adapter_name)
766 if adapter:
767 for func_name in functions:
768 if hasattr(adapter, func_name):
769 items.append(
770 AutocompleteItem(
771 name=f"{adapter_name}.{func_name}",
772 type="function",
773 description=f"{adapter_name.title()} adapter function",
774 adapter_source=adapter_name,
775 )
776 )
777 except Exception:
778 pass
780 return items
782 def _get_template_autocomplete(self) -> list[AutocompleteItem]:
783 """Get autocomplete items for template-specific variables."""
784 items = []
786 # Common template variables
787 common_vars = [
788 ("config", "variable", "Application configuration object"),
789 ("request", "variable", "Current HTTP request object"),
790 ("models", "variable", "Database models"),
791 ("render_block", "function", "Render template block"),
792 ("render_component", "function", "Render HTMY component"),
793 ]
795 for name, item_type, description in common_vars:
796 items.append(
797 AutocompleteItem(
798 name=name,
799 type=item_type,
800 description=description,
801 adapter_source="fastblocks",
802 )
803 )
805 return items
807 def _extract_example_from_doc(self, doc: str) -> str | None:
808 """Extract usage example from docstring."""
809 if not doc:
810 return None
812 lines = doc.split("\n")
813 in_example = False
814 example_lines = []
816 for line in lines:
817 line = line.strip()
818 if line.startswith("[[ ") and line.endswith(" ]]"):
819 return line
820 elif "Usage:" in line or "Example:" in line:
821 in_example = True
822 elif in_example and line.startswith("[["):
823 example_lines.append(line)
824 elif in_example and line and not line.startswith(" "):
825 break
827 return example_lines[0] if example_lines else None
829 async def get_fragments_for_template(
830 self, template_name: str
831 ) -> list[FragmentInfo]:
832 """Get fragments available for a specific template."""
833 if template_name in self._fragment_cache:
834 return self._fragment_cache[template_name]
836 # Try to discover fragments for this template
837 await self._analyze_fragment(template_name)
838 return self._fragment_cache.get(template_name, [])
840 async def get_autocomplete_suggestions(
841 self, context: str, cursor_position: int = 0, template_name: str = "unknown"
842 ) -> list[AutocompleteItem]:
843 """Get autocomplete suggestions for the given context."""
844 cache_key = "global"
845 if cache_key not in self._autocomplete_cache:
846 await self._build_autocomplete_index()
848 all_items = self._autocomplete_cache[cache_key]
850 # Extract the current word being typed
851 before_cursor = context[:cursor_position]
852 current_word = self._extract_current_word(before_cursor)
854 if not current_word:
855 return all_items[:20] # Return top 20 suggestions
857 # Filter suggestions based on current word
858 filtered = []
859 for item in all_items:
860 if current_word.lower() in item.name.lower():
861 filtered.append(item)
863 # Sort by relevance (exact matches first, then starts with, then contains)
864 filtered.sort(
865 key=lambda x: (
866 not x.name.lower().startswith(current_word.lower()),
867 not x.name.lower() == current_word.lower(),
868 x.name.lower(),
869 )
870 )
872 return filtered[:10] # Return top 10 matches
874 def _extract_current_word(self, text: str) -> str:
875 """Extract the current word being typed from template context."""
876 # Look for word characters at the end of the text
877 match = re.search(r"[\w.]+$", text)
878 return match.group(0) if match else ""
880 async def render_fragment(
881 self,
882 fragment_name: str,
883 context: dict[str, t.Any] | None = None,
884 template_name: str | None = None,
885 secure: bool = False,
886 ) -> str:
887 """Render a specific template fragment."""
888 if not self.base_templates:
889 raise RuntimeError("Templates not initialized")
891 # Find the fragment
892 fragment_info = await self._find_fragment(fragment_name, template_name)
893 if not fragment_info:
894 raise TemplateNotFound(f"Fragment '{fragment_name}' not found")
896 env = self._get_template_environment(secure=secure)
898 try:
899 if fragment_info.block_name:
900 # Render specific block
901 template = env.get_template(fragment_info.template_path)
902 return template.render_block(fragment_info.block_name, context or {})
903 else:
904 # Render entire template
905 template = env.get_template(fragment_info.template_path)
906 return template.render(context or {})
908 except Exception as e:
909 raise TemplateError(f"Error rendering fragment '{fragment_name}': {e}")
911 async def _find_fragment(
912 self, fragment_name: str, template_name: str | None = None
913 ) -> FragmentInfo | None:
914 """Find fragment by name, optionally within a specific template."""
915 # Search in specific template first
916 if template_name and template_name in self._fragment_cache:
917 for fragment in self._fragment_cache[template_name]:
918 if fragment.name == fragment_name:
919 return fragment
921 # Search across all fragments
922 for fragments in self._fragment_cache.values():
923 for fragment in fragments:
924 if fragment.name == fragment_name:
925 return fragment
927 return None
929 async def precompile_templates(self) -> dict[str, Template]:
930 """Precompile templates for performance optimization."""
931 if not self.settings.precompile_templates:
932 return {}
934 env = self._get_template_environment()
935 if not env.loader:
936 return {}
938 compiled_templates = {}
940 try:
941 template_names = await asyncio.get_event_loop().run_in_executor(
942 None, env.loader.list_templates
943 )
945 for template_name in template_names:
946 try:
947 template = env.get_template(template_name)
948 compiled_templates[template_name] = template
949 except Exception:
950 # Skip templates that can't be compiled
951 continue
953 except Exception:
954 pass
956 return compiled_templates
958 async def get_template_dependencies(self, template_name: str) -> set[str]:
959 """Get dependencies for a template (extends, includes, imports)."""
960 if template_name in self._template_dependencies:
961 return self._template_dependencies[template_name]
963 dependencies = set()
964 env = self._get_template_environment()
966 try:
967 source, _ = env.loader.get_source(env, template_name)
968 parsed = env.parse(source, template_name)
970 # Find extends, includes, and imports
971 for node in parsed.find_all(("Extends", "Include", "FromImport")):
972 if hasattr(node, "template") and hasattr(node.template, "value"):
973 dependencies.add(node.template.value)
975 self._template_dependencies[template_name] = dependencies
977 except Exception:
978 pass
980 return dependencies
982 def clear_caches(self) -> None:
983 """Clear all internal caches."""
984 self._validation_cache.clear()
985 self._fragment_cache.clear()
986 self._autocomplete_cache.clear()
987 self._template_dependencies.clear()
990MODULE_ID = UUID("01937d87-1234-7890-abcd-1234567890ab")
991MODULE_STATUS = AdapterStatus.EXPERIMENTAL
993# Register the advanced manager
994with suppress(Exception):
995 depends.set("advanced_template_manager", AdvancedTemplateManager)