libs/core/langchain_core/utils/mustache.py PYTHON 707 lines View on github.com → Search inside
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        '"': "&quot;",334        "<": "&lt;",335        ">": "&gt;",336    }337338    # & must be handled first339    string = string.replace("&", "&amp;")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

Code quality findings 14

Avoid global variables; use function parameters or class attributes for better scope management
global-variable
global _CURRENT_LINE
Ensure functions have docstrings for documentation
missing-docstring
def l_sa_check(
Ensure functions have docstrings for documentation
missing-docstring
def r_sa_check(
Ensure functions have docstrings for documentation
missing-docstring
def tokenize(
Avoid global variables; use function parameters or class attributes for better scope management
global-variable
global _CURRENT_LINE, _LAST_TAG_LINE
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(resolved_scope, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(resolved_scope, (list, tuple)):
Ensure functions have docstrings for documentation
missing-docstring
def render(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(template, Sequence) and not isinstance(template, str):
Avoid unless necessary; Python's garbage collector typically handles object deletion
unnecessary-del
del scopes[0]
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(thing, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(thing, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(scope, (Sequence, Iterator)) and not isinstance(scope, str):

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.