From 04f598ed495c6b48d7df08a5f61c302e5b8d3438 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:23:22 +0000 Subject: [PATCH 1/5] Tracking if, for, while and autocomplete individually --- adafruit_templateengine.py | 68 ++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/adafruit_templateengine.py b/adafruit_templateengine.py index b3f4c40..6482b3f 100644 --- a/adafruit_templateengine.py +++ b/adafruit_templateengine.py @@ -387,8 +387,10 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran indent, indentation_level = " ", 1 # Keep track of the template state - forloop_iterables: "list[str]" = [] - autoescape_modes: "list[bool]" = ["default_on"] + nested_if_statements: "list[str]" = [] + nested_for_loops: "list[str]" = [] + nested_while_loops: "list[str]" = [] + nested_autoescape_modes: "list[str]" = [] last_token_was_block = False # Resolve tokens @@ -414,7 +416,10 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran if token.startswith(r"{{ "): last_token_was_block = False - autoescape = autoescape_modes[-1] in ("on", "default_on") + if nested_autoescape_modes: + autoescape = nested_autoescape_modes[-1][14:-3] == "on" + else: + autoescape = True # Expression should be escaped with language-specific function if autoescape: @@ -436,6 +441,8 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran if token.startswith(r"{% if "): function_string += indent * indentation_level + f"{token[3:-3]}:\n" indentation_level += 1 + + nested_if_statements.append(token) elif token.startswith(r"{% elif "): indentation_level -= 1 function_string += indent * indentation_level + f"{token[3:-3]}:\n" @@ -447,30 +454,51 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran elif token == r"{% endif %}": indentation_level -= 1 + if not nested_if_statements: + raise SyntaxError("No matching {% if ... %} block for {% endif %}") + + nested_if_statements.pop() + # Token is a for loop elif token.startswith(r"{% for "): function_string += indent * indentation_level + f"{token[3:-3]}:\n" indentation_level += 1 - forloop_iterables.append(token[3:-3].split(" in ", 1)[1]) + nested_for_loops.append(token) elif token == r"{% empty %}": indentation_level -= 1 + last_forloop_iterable = nested_for_loops[-1][3:-3].split(" in ", 1)[1] function_string += ( - indent * indentation_level + f"if not {forloop_iterables[-1]}:\n" + indent * indentation_level + f"if not {last_forloop_iterable}:\n" ) indentation_level += 1 elif token == r"{% endfor %}": indentation_level -= 1 - forloop_iterables.pop() + + if not nested_for_loops: + raise SyntaxError( + "No matching {% for ... %} block for {% endfor %}" + ) + + nested_for_loops.pop() # Token is a while loop elif token.startswith(r"{% while "): function_string += indent * indentation_level + f"{token[3:-3]}:\n" indentation_level += 1 + + nested_while_loops.append(token) elif token == r"{% endwhile %}": indentation_level -= 1 + if not nested_while_loops: + raise SyntaxError( + "No matching {% while ... %} block for {% endwhile %}" + ) + + nested_while_loops.pop() + # Token is a Python code elif token.startswith(r"{% exec "): expression = token[8:-3] @@ -481,11 +509,16 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran mode = token[14:-3] if mode not in ("on", "off"): raise ValueError(f"Unknown autoescape mode: {mode}") - autoescape_modes.append(mode) + + nested_autoescape_modes.append(token) + elif token == r"{% endautoescape %}": - if autoescape_modes == ["default_on"]: - raise ValueError("No autoescape mode to end") - autoescape_modes.pop() + if not nested_autoescape_modes: + raise SyntaxError( + "No matching {% autoescape ... %} block for {% endautoescape %}" + ) + + nested_autoescape_modes.pop() else: raise ValueError( @@ -498,6 +531,21 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran # Continue with the rest of the template template = template[token_match.end() :] + # Checking for unclosed blocks + if len(nested_if_statements) > 0: + last_if_statement = nested_if_statements[-1] + raise SyntaxError(f"Missing {{% endif %}} for {last_if_statement}") + + if len(nested_for_loops) > 0: + last_for_loop = nested_for_loops[-1] + raise SyntaxError(f"Missing {{% endfor %}} for {last_for_loop}") + + if len(nested_while_loops) > 0: + last_while_loop = nested_while_loops[-1] + raise SyntaxError(f"Missing {{% endwhile %}} for {last_while_loop}") + + # No check for unclosed autoescape blocks, as they are optional and do not result in errors + # Add the text after the last token (if any) text_after_last_token = template From 466a83b0f45c36140b9324f44c7c1a909d4b7730 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:30:18 +0000 Subject: [PATCH 2/5] Better error message for unknown tokens --- adafruit_templateengine.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/adafruit_templateengine.py b/adafruit_templateengine.py index 6482b3f..ba95d4b 100644 --- a/adafruit_templateengine.py +++ b/adafruit_templateengine.py @@ -521,12 +521,10 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran nested_autoescape_modes.pop() else: - raise ValueError( - f"Unknown token type: {token} at {token_match.start()}" - ) + raise SyntaxError(f"Unknown token type: {token}") else: - raise ValueError(f"Unknown token type: {token} at {token_match.start()}") + raise SyntaxError(f"Unknown token type: {token}") # Continue with the rest of the template template = template[token_match.end() :] From b073803e0a58d7f266dc4409ee9eb3c4f74c203d Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:53:11 +0000 Subject: [PATCH 3/5] Preventing indentation error on empty for loops, if statements etc. --- adafruit_templateengine.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/adafruit_templateengine.py b/adafruit_templateengine.py index ba95d4b..f822094 100644 --- a/adafruit_templateengine.py +++ b/adafruit_templateengine.py @@ -407,10 +407,12 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran if last_token_was_block and text_before_token.startswith("\n"): text_before_token = text_before_token[1:] - if text_before_token: - function_string += ( - indent * indentation_level + f"yield {repr(text_before_token)}\n" - ) + if text_before_token: + function_string += ( + indent * indentation_level + f"yield {repr(text_before_token)}\n" + ) + else: + function_string += indent * indentation_level + "pass\n" # Token is an expression if token.startswith(r"{{ "): From bed59fbba4e9a56c0b7280d98bfb7d8190f71483 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:58:13 +0000 Subject: [PATCH 4/5] More verbose template rendering function name for more readable traceback --- adafruit_templateengine.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/adafruit_templateengine.py b/adafruit_templateengine.py index f822094..c85cacb 100644 --- a/adafruit_templateengine.py +++ b/adafruit_templateengine.py @@ -366,13 +366,13 @@ def _token_is_on_own_line(text_before_token: str) -> bool: return _LSTRIP_BLOCK_PATTERN.search(text_before_token) is not None -def _create_template_function( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements +def _create_template_rendering_function( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements template: str, language: str = Language.HTML, *, trim_blocks: bool = True, lstrip_blocks: bool = True, - function_name: str = "_", + function_name: str = "__template_rendering_function", context_name: str = "context", dry_run: bool = False, ) -> "Generator[str] | str": @@ -605,7 +605,9 @@ def __init__(self, template_string: str, *, language: str = Language.HTML) -> No :param str template_string: String containing the template to be rendered :param str language: Language for autoescaping. Defaults to HTML """ - self._template_function = _create_template_function(template_string, language) + self._template_function = _create_template_rendering_function( + template_string, language + ) def render_iter( self, context: dict = None, *, chunk_size: int = None From 369d3b1b40ccbd5178f291b77da90f253641b5f7 Mon Sep 17 00:00:00 2001 From: michalpokusa <72110769+michalpokusa@users.noreply.github.com> Date: Tue, 5 Mar 2024 02:16:10 +0000 Subject: [PATCH 5/5] Unified SyntaxError messages --- adafruit_templateengine.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/adafruit_templateengine.py b/adafruit_templateengine.py index c85cacb..21234cf 100644 --- a/adafruit_templateengine.py +++ b/adafruit_templateengine.py @@ -219,7 +219,7 @@ def _resolve_includes(template: str): def _check_for_unsupported_nested_blocks(template: str): if _find_block(template) is not None: - raise ValueError("Nested blocks are not supported") + raise SyntaxError("Nested blocks are not supported") def _resolve_includes_blocks_and_extends(template: str): @@ -248,7 +248,7 @@ def _resolve_includes_blocks_and_extends(template: str): endblock_match = _find_named_endblock(template, block_name) if endblock_match is None: - raise ValueError(r"Missing {% endblock %} for block: " + block_name) + raise SyntaxError("Missing {% endblock %} for block: " + block_name) block_content = template[block_match.end() : endblock_match.start()] @@ -457,7 +457,7 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too indentation_level -= 1 if not nested_if_statements: - raise SyntaxError("No matching {% if ... %} block for {% endif %}") + raise SyntaxError("Missing {% if ... %} block for {% endif %}") nested_if_statements.pop() @@ -479,9 +479,7 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too indentation_level -= 1 if not nested_for_loops: - raise SyntaxError( - "No matching {% for ... %} block for {% endfor %}" - ) + raise SyntaxError("Missing {% for ... %} block for {% endfor %}") nested_for_loops.pop() @@ -496,7 +494,7 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too if not nested_while_loops: raise SyntaxError( - "No matching {% while ... %} block for {% endwhile %}" + "Missing {% while ... %} block for {% endwhile %}" ) nested_while_loops.pop() @@ -517,7 +515,7 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too elif token == r"{% endautoescape %}": if not nested_autoescape_modes: raise SyntaxError( - "No matching {% autoescape ... %} block for {% endautoescape %}" + "Missing {% autoescape ... %} block for {% endautoescape %}" ) nested_autoescape_modes.pop() @@ -534,15 +532,15 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too # Checking for unclosed blocks if len(nested_if_statements) > 0: last_if_statement = nested_if_statements[-1] - raise SyntaxError(f"Missing {{% endif %}} for {last_if_statement}") + raise SyntaxError("Missing {% endif %} for " + last_if_statement) if len(nested_for_loops) > 0: last_for_loop = nested_for_loops[-1] - raise SyntaxError(f"Missing {{% endfor %}} for {last_for_loop}") + raise SyntaxError("Missing {% endfor %} for " + last_for_loop) if len(nested_while_loops) > 0: last_while_loop = nested_while_loops[-1] - raise SyntaxError(f"Missing {{% endwhile %}} for {last_while_loop}") + raise SyntaxError("Missing {% endwhile %} for " + last_while_loop) # No check for unclosed autoescape blocks, as they are optional and do not result in errors