Avoid global variables; use function parameters or class attributes for better scope management
global _CURRENT_LINE
1"""Adapted from https://github.com/noahmorrison/chevron.23MIT License.4"""56from __future__ import annotations78import logging9from collections.abc import Iterator, Mapping, Sequence10from types import MappingProxyType11from typing import (12 TYPE_CHECKING,13 Any,14 Literal,15 cast,16)1718if TYPE_CHECKING:19 from typing import TypeAlias2021logger = logging.getLogger(__name__)222324Scopes: TypeAlias = list[Literal[False, 0] | Mapping[str, Any]]252627# Globals28_CURRENT_LINE = 129_LAST_TAG_LINE = None303132class ChevronError(SyntaxError):33 """Custom exception for Chevron errors."""343536#37# Helper functions38#394041def grab_literal(template: str, l_del: str) -> tuple[str, str]:42 """Parse a literal from the template.4344 Args:45 template: The template to parse.46 l_del: The left delimiter.4748 Returns:49 The literal and the template.50 """51 global _CURRENT_LINE5253 try:54 # Look for the next tag and move the template to it55 literal, template = template.split(l_del, 1)56 _CURRENT_LINE += literal.count("\n")5758 # There are no more tags in the template?59 except ValueError:60 # Then the rest of the template is a literal61 return (template, "")6263 return (literal, template)646566def l_sa_check(67 template: str, # noqa: ARG00168 literal: str,69 is_standalone: bool, # noqa: FBT00170) -> bool:71 """Do a preliminary check to see if a tag could be a standalone.7273 Args:74 template: The template. (Not used.)75 literal: The literal.76 is_standalone: Whether the tag is standalone.7778 Returns:79 Whether the tag could be a standalone.80 """81 # If there is a newline, or the previous tag was a standalone82 if literal.find("\n") != -1 or is_standalone:83 padding = literal.rsplit("\n", maxsplit=1)[-1]8485 # If all the characters since the last newline are spaces86 # Then the next tag could be a standalone87 # Otherwise it can't be88 return padding.isspace() or not padding89 return False909192def r_sa_check(93 template: str,94 tag_type: str,95 is_standalone: bool, # noqa: FBT00196) -> bool:97 """Do a final check to see if a tag could be a standalone.9899 Args:100 template: The template.101 tag_type: The type of the tag.102 is_standalone: Whether the tag is standalone.103104 Returns:105 Whether the tag could be a standalone.106 """107 # Check right side if we might be a standalone108 if is_standalone and tag_type not in {"variable", "no escape"}:109 on_newline = template.split("\n", 1)110111 # If the stuff to the right of us are spaces we're a standalone112 return on_newline[0].isspace() or not on_newline[0]113114 # If we're a tag can't be a standalone115 return False116117118def parse_tag(template: str, l_del: str, r_del: str) -> tuple[tuple[str, str], str]:119 """Parse a tag from a template.120121 Args:122 template: The template.123 l_del: The left delimiter.124 r_del: The right delimiter.125126 Returns:127 The tag and the template.128129 Raises:130 ChevronError: If the tag is unclosed.131 ChevronError: If the set delimiter tag is unclosed.132 """133 tag_types = {134 "!": "comment",135 "#": "section",136 "^": "inverted section",137 "/": "end",138 ">": "partial",139 "=": "set delimiter?",140 "{": "no escape?",141 "&": "no escape",142 }143144 # Get the tag145 try:146 tag, template = template.split(r_del, 1)147 except ValueError as e:148 msg = f"unclosed tag at line {_CURRENT_LINE}"149 raise ChevronError(msg) from e150151 # Check for empty tags152 if not tag.strip():153 msg = f"empty tag at line {_CURRENT_LINE}"154 raise ChevronError(msg)155156 # Find the type meaning of the first character157 tag_type = tag_types.get(tag[0], "variable")158159 # If the type is not a variable160 if tag_type != "variable":161 # Then that first character is not needed162 tag = tag[1:]163164 # If we might be a set delimiter tag165 if tag_type == "set delimiter?":166 # Double check to make sure we are167 if tag.endswith("="):168 tag_type = "set delimiter"169 # Remove the equal sign170 tag = tag[:-1]171172 # Otherwise we should complain173 else:174 msg = f"unclosed set delimiter tag\nat line {_CURRENT_LINE}"175 raise ChevronError(msg)176177 elif (178 # If we might be a no html escape tag179 tag_type == "no escape?"180 # And we have a third curly brace181 # (And are using curly braces as delimiters)182 and l_del == "{{"183 and r_del == "}}"184 and template.startswith("}")185 ):186 # Then we are a no html escape tag187 template = template[1:]188 tag_type = "no escape"189190 # Strip the whitespace off the key and return191 return ((tag_type, tag.strip()), template)192193194#195# The main tokenizing function196#197198199def tokenize(200 template: str, def_ldel: str = "{{", def_rdel: str = "}}"201) -> Iterator[tuple[str, str]]:202 """Tokenize a mustache template.203204 Tokenizes a mustache template in a generator fashion, using file-like objects. It205 also accepts a string containing the template.206207 Args:208 template: a file-like object, or a string of a mustache template209 def_ldel: The default left delimiter210 (`'{{'` by default, as in spec compliant mustache)211 def_rdel: The default right delimiter212 (`'}}'` by default, as in spec compliant mustache)213214 Yields:215 Mustache tags in the form of a tuple `(tag_type, tag_key)` where `tag_type` is216 one of:217218 * literal219 * section220 * inverted section221 * end222 * partial223 * no escape224225 ...and `tag_key` is either the key or in the case of a literal tag, the226 literal itself.227228 Raises:229 ChevronError: If there is a syntax error in the template.230 """231 global _CURRENT_LINE, _LAST_TAG_LINE232 _CURRENT_LINE = 1233 _LAST_TAG_LINE = None234235 is_standalone = True236 open_sections = []237 l_del = def_ldel238 r_del = def_rdel239240 while template:241 literal, template = grab_literal(template, l_del)242243 # If the template is completed244 if not template:245 # Then yield the literal and leave246 yield ("literal", literal)247 break248249 # Do the first check to see if we could be a standalone250 is_standalone = l_sa_check(template, literal, is_standalone)251252 # Parse the tag253 tag, template = parse_tag(template, l_del, r_del)254 tag_type, tag_key = tag255256 # Special tag logic257258 # If we are a set delimiter tag259 if tag_type == "set delimiter":260 # Then get and set the delimiters261 dels = tag_key.strip().split(" ")262 l_del, r_del = dels[0], dels[-1]263264 # If we are a section tag265 elif tag_type in {"section", "inverted section"}:266 # Then open a new section267 open_sections.append(tag_key)268 _LAST_TAG_LINE = _CURRENT_LINE269270 # If we are an end tag271 elif tag_type == "end":272 # Then check to see if the last opened section273 # is the same as us274 try:275 last_section = open_sections.pop()276 except IndexError as e:277 msg = (278 f'Trying to close tag "{tag_key}"\n'279 "Looks like it was not opened.\n"280 f"line {_CURRENT_LINE + 1}"281 )282 raise ChevronError(msg) from e283 if tag_key != last_section:284 # Otherwise we need to complain285 msg = (286 f'Trying to close tag "{tag_key}"\n'287 f'last open tag is "{last_section}"\n'288 f"line {_CURRENT_LINE + 1}"289 )290 raise ChevronError(msg)291292 # Do the second check to see if we're a standalone293 is_standalone = r_sa_check(template, tag_type, is_standalone)294295 # Which if we are296 if is_standalone:297 # Remove the stuff before the newline298 template = template.split("\n", 1)[-1]299300 # Partials need to keep the spaces on their left301 if tag_type != "partial":302 # But other tags don't303 literal = literal.rstrip(" ")304305 # Start yielding306 # Ignore literals that are empty307 if literal:308 yield ("literal", literal)309310 # Ignore comments and set delimiters311 if tag_type not in {"comment", "set delimiter?"}:312 yield (tag_type, tag_key)313314 # If there are any open sections when we're done315 if open_sections:316 # Then we need to complain317 msg = (318 "Unexpected EOF\n"319 f'the tag "{open_sections[-1]}" was never closed\n'320 f"was opened at line {_LAST_TAG_LINE}"321 )322 raise ChevronError(msg)323324325#326# Helper functions327#328329330def _html_escape(string: str) -> str:331 """Return the HTML-escaped string with these characters escaped: `" & < >`."""332 html_codes = {333 '"': """,334 "<": "<",335 ">": ">",336 }337338 # & must be handled first339 string = string.replace("&", "&")340 for char, code in html_codes.items():341 string = string.replace(char, code)342 return string343344345def _get_key(346 key: str,347 scopes: Scopes,348 *,349 warn: bool,350 keep: bool,351 def_ldel: str,352 def_rdel: str,353) -> Any:354 """Retrieve a value from the current scope using a dot-separated key path.355356 Traverses through nested dictionaries and lists using dot notation.357358 Supports special key `'.'` to return the current scope.359360 Args:361 key: Dot-separated key path (e.g., `'user.name'` or `'.'` for current scope).362 scopes: List of scope dictionaries to search through.363 warn: Whether to log a warning when a key is not found.364 keep: Whether to return the original template tag when key is not found.365 def_ldel: Left delimiter for template (used when keep is `True`).366 def_rdel: Right delimiter for template (used when keep is `True`).367368 Returns:369 The value found at the key path.370371 If not found, returns the original template tag when keep is `True`,372 otherwise returns an empty string.373 """374 # If the key is a dot375 if key == ".":376 # Then just return the current scope377 return scopes[0]378379 # Loop through the scopes380 for scope in scopes:381 try:382 # Return an empty string if falsy, with two exceptions383 # 0 should return 0, and False should return False384 if scope in (0, False):385 return scope386387 resolved_scope: Literal[False, 0] | Mapping[str, Any] | Sequence[Any] = (388 scope389 )390 # For every dot separated key391 for child in key.split("."):392 # Return an empty string if falsy, with two exceptions393 # 0 should return 0, and False should return False394 if resolved_scope in (0, False):395 return resolved_scope396 # Move into the scope397 if isinstance(resolved_scope, dict):398 try:399 resolved_scope = resolved_scope[child]400 except (KeyError, TypeError):401 # Key not found - will be caught by outer try-except402 msg = f"Key {child!r} not found in dict"403 raise KeyError(msg) from None404 elif isinstance(resolved_scope, (list, tuple)):405 try:406 resolved_scope = resolved_scope[int(child)]407 except (ValueError, IndexError, TypeError):408 # Invalid index - will be caught by outer try-except409 msg = f"Invalid index {child!r} for list/tuple"410 raise IndexError(msg) from None411 else:412 # Reject everything else for security413 # This prevents traversing into arbitrary Python objects414 msg = (415 f"Cannot traverse into {type(resolved_scope).__name__}. "416 "Mustache templates only support dict, list, and tuple. "417 f"Got: {type(resolved_scope)}"418 )419 raise TypeError(msg) # noqa: TRY301420421 try:422 # This allows for custom falsy data types423 # https://github.com/noahmorrison/chevron/issues/35424 if resolved_scope._CHEVRON_return_scope_when_falsy: # type: ignore[union-attr] # noqa: SLF001425 return resolved_scope426 except AttributeError:427 if resolved_scope in (0, False):428 return resolved_scope429 return resolved_scope or ""430 except (AttributeError, KeyError, IndexError, ValueError, TypeError):431 # We couldn't find the key in the current scope432 # TypeError: Attempted to traverse into non-dict/list type433 # We'll try again on the next pass434 pass435436 # We couldn't find the key in any of the scopes437438 if warn:439 logger.warning("Could not find key '%s'", key)440441 if keep:442 return f"{def_ldel} {key} {def_rdel}"443444 return ""445446447def _get_partial(name: str, partials_dict: Mapping[str, str]) -> str:448 """Load a partial.449450 Returns:451 The partial.452 """453 try:454 # Maybe the partial is in the dictionary455 return partials_dict[name]456 except KeyError:457 return ""458459460#461# The main rendering function462#463g_token_cache: dict[str, list[tuple[str, str]]] = {}464465EMPTY_DICT: MappingProxyType[str, str] = MappingProxyType({})466467468def render(469 template: str | list[tuple[str, str]] = "",470 data: Mapping[str, Any] = EMPTY_DICT,471 partials_dict: Mapping[str, str] = EMPTY_DICT,472 padding: str = "",473 def_ldel: str = "{{",474 def_rdel: str = "}}",475 scopes: Scopes | None = None,476 warn: bool = False, # noqa: FBT001,FBT002477 keep: bool = False, # noqa: FBT001,FBT002478) -> str:479 """Render a mustache template.480481 Renders a mustache template with a data scope and inline partial capability.482483 Args:484 template: A file-like object or a string containing the template.485 data: A python dictionary with your data scope.486 partials_dict: A python dictionary which will be search for partials487 before the filesystem is.488489 `{'include': 'foo'}` is the same as a file called include.mustache490 (defaults to `{}`).491 padding: This is for padding partials, and shouldn't be used492 (but can be if you really want to).493 def_ldel: The default left delimiter494495 (`'{{'` by default, as in spec compliant mustache).496 def_rdel: The default right delimiter497498 (`'}}'` by default, as in spec compliant mustache).499 scopes: The list of scopes that `get_key` will look through.500 warn: Log a warning when a template substitution isn't found in the data501 keep: Keep unreplaced tags when a substitution isn't found in the data.502503 Returns:504 A string containing the rendered template.505 """506 # If the template is a sequence but not derived from a string507 if isinstance(template, Sequence) and not isinstance(template, str):508 # Then we don't need to tokenize it509 # But it does need to be a generator510 tokens: Iterator[tuple[str, str]] = (token for token in template)511 elif template in g_token_cache:512 tokens = (token for token in g_token_cache[template])513 else:514 # Otherwise make a generator515 tokens = tokenize(template, def_ldel, def_rdel)516517 output = ""518519 if scopes is None:520 scopes = [data]521522 # Run through the tokens523 for tag, key in tokens:524 # Set the current scope525 current_scope = scopes[0]526527 # If we're an end tag528 if tag == "end":529 # Pop out of the latest scope530 del scopes[0]531532 # If the current scope is falsy and not the only scope533 elif not current_scope and len(scopes) != 1:534 if tag in {"section", "inverted section"}:535 # Set the most recent scope to a falsy value536 scopes.insert(0, False)537538 # If we're a literal tag539 elif tag == "literal":540 # Add padding to the key and add it to the output541 output += key.replace("\n", "\n" + padding)542543 # If we're a variable tag544 elif tag == "variable":545 # Add the html escaped key to the output546 thing = _get_key(547 key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel548 )549 if thing is True and key == ".":550 # if we've coerced into a boolean by accident551 # (inverted tags do this)552 # then get the un-coerced object (next in the stack)553 thing = scopes[1]554 if not isinstance(thing, str):555 thing = str(thing)556 output += _html_escape(thing)557558 # If we're a no html escape tag559 elif tag == "no escape":560 # Just lookup the key and add it561 thing = _get_key(562 key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel563 )564 if not isinstance(thing, str):565 thing = str(thing)566 output += thing567568 # If we're a section tag569 elif tag == "section":570 # Get the sections scope571 scope = _get_key(572 key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel573 )574575 # If the scope is a callable (as described in576 # https://mustache.github.io/mustache.5.html)577 if callable(scope):578 # Generate template text from tags579 text = ""580 tags: list[tuple[str, str]] = []581 for token in tokens:582 if token == ("end", key):583 break584585 tags.append(token)586 tag_type, tag_key = token587 if tag_type == "literal":588 text += tag_key589 elif tag_type == "no escape":590 text += f"{def_ldel}& {tag_key} {def_rdel}"591 else:592 text += "{}{} {}{}".format(593 def_ldel,594 {595 "comment": "!",596 "section": "#",597 "inverted section": "^",598 "end": "/",599 "partial": ">",600 "set delimiter": "=",601 "no escape": "&",602 "variable": "",603 }[tag_type],604 tag_key,605 def_rdel,606 )607608 g_token_cache[text] = tags609610 rend = scope(611 text,612 lambda template, data=None: render(613 template,614 data={},615 partials_dict=partials_dict,616 padding=padding,617 def_ldel=def_ldel,618 def_rdel=def_rdel,619 scopes=(data and [data, *scopes]) or scopes,620 warn=warn,621 keep=keep,622 ),623 )624625 output += rend626627 # If the scope is a sequence, an iterator or generator but not628 # derived from a string629 elif isinstance(scope, (Sequence, Iterator)) and not isinstance(scope, str):630 # Then we need to do some looping631632 # Gather up all the tags inside the section633 # (And don't be tricked by nested end tags with the same key)634 # TODO: This feels like it still has edge cases, no?635 tags = []636 tags_with_same_key = 0637 for token in tokens:638 if token == ("section", key):639 tags_with_same_key += 1640 if token == ("end", key):641 tags_with_same_key -= 1642 if tags_with_same_key < 0:643 break644 tags.append(token)645646 # For every item in the scope647 for thing in scope:648 # Append it as the most recent scope and render649 new_scope = [thing, *scopes]650 rend = render(651 template=tags,652 scopes=new_scope,653 padding=padding,654 partials_dict=partials_dict,655 def_ldel=def_ldel,656 def_rdel=def_rdel,657 warn=warn,658 keep=keep,659 )660661 output += rend662663 else:664 # Otherwise we're just a scope section665 scopes.insert(0, scope)666667 # If we're an inverted section668 elif tag == "inverted section":669 # Add the flipped scope to the scopes670 scope = _get_key(671 key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel672 )673 scopes.insert(0, cast("Literal[False]", not scope))674675 # If we're a partial676 elif tag == "partial":677 # Load the partial678 partial = _get_partial(key, partials_dict)679680 # Find what to pad the partial with681 left = output.rpartition("\n")[2]682 part_padding = padding683 if left.isspace():684 part_padding += left685686 # Render the partial687 part_out = render(688 template=partial,689 partials_dict=partials_dict,690 def_ldel=def_ldel,691 def_rdel=def_rdel,692 padding=part_padding,693 scopes=scopes,694 warn=warn,695 keep=keep,696 )697698 # If the partial was indented699 if left.isspace():700 # then remove the spaces from the end701 part_out = part_out.rstrip(" \t")702703 # Add the partials output to the output704 output += part_out705706 return output
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.