Skip to content

Commit 1258e2c

Browse files
authored
Merge pull request #3 from michalpokusa/syntax-error-prevention-and-traceback-readability
Syntax error prevention and traceback readability
2 parents 95f75b3 + 369d3b1 commit 1258e2c

File tree

1 file changed

+71
-23
lines changed

1 file changed

+71
-23
lines changed

adafruit_templateengine.py

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def _resolve_includes(template: str):
219219

220220
def _check_for_unsupported_nested_blocks(template: str):
221221
if _find_block(template) is not None:
222-
raise ValueError("Nested blocks are not supported")
222+
raise SyntaxError("Nested blocks are not supported")
223223

224224

225225
def _resolve_includes_blocks_and_extends(template: str):
@@ -248,7 +248,7 @@ def _resolve_includes_blocks_and_extends(template: str):
248248
endblock_match = _find_named_endblock(template, block_name)
249249

250250
if endblock_match is None:
251-
raise ValueError(r"Missing {% endblock %} for block: " + block_name)
251+
raise SyntaxError("Missing {% endblock %} for block: " + block_name)
252252

253253
block_content = template[block_match.end() : endblock_match.start()]
254254

@@ -366,13 +366,13 @@ def _token_is_on_own_line(text_before_token: str) -> bool:
366366
return _LSTRIP_BLOCK_PATTERN.search(text_before_token) is not None
367367

368368

369-
def _create_template_function( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements
369+
def _create_template_rendering_function( # pylint: disable=,too-many-locals,too-many-branches,too-many-statements
370370
template: str,
371371
language: str = Language.HTML,
372372
*,
373373
trim_blocks: bool = True,
374374
lstrip_blocks: bool = True,
375-
function_name: str = "_",
375+
function_name: str = "__template_rendering_function",
376376
context_name: str = "context",
377377
dry_run: bool = False,
378378
) -> "Generator[str] | str":
@@ -387,8 +387,10 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
387387
indent, indentation_level = " ", 1
388388

389389
# Keep track of the template state
390-
forloop_iterables: "list[str]" = []
391-
autoescape_modes: "list[bool]" = ["default_on"]
390+
nested_if_statements: "list[str]" = []
391+
nested_for_loops: "list[str]" = []
392+
nested_while_loops: "list[str]" = []
393+
nested_autoescape_modes: "list[str]" = []
392394
last_token_was_block = False
393395

394396
# Resolve tokens
@@ -405,16 +407,21 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
405407
if last_token_was_block and text_before_token.startswith("\n"):
406408
text_before_token = text_before_token[1:]
407409

408-
if text_before_token:
409-
function_string += (
410-
indent * indentation_level + f"yield {repr(text_before_token)}\n"
411-
)
410+
if text_before_token:
411+
function_string += (
412+
indent * indentation_level + f"yield {repr(text_before_token)}\n"
413+
)
414+
else:
415+
function_string += indent * indentation_level + "pass\n"
412416

413417
# Token is an expression
414418
if token.startswith(r"{{ "):
415419
last_token_was_block = False
416420

417-
autoescape = autoescape_modes[-1] in ("on", "default_on")
421+
if nested_autoescape_modes:
422+
autoescape = nested_autoescape_modes[-1][14:-3] == "on"
423+
else:
424+
autoescape = True
418425

419426
# Expression should be escaped with language-specific function
420427
if autoescape:
@@ -436,6 +443,8 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
436443
if token.startswith(r"{% if "):
437444
function_string += indent * indentation_level + f"{token[3:-3]}:\n"
438445
indentation_level += 1
446+
447+
nested_if_statements.append(token)
439448
elif token.startswith(r"{% elif "):
440449
indentation_level -= 1
441450
function_string += indent * indentation_level + f"{token[3:-3]}:\n"
@@ -447,30 +456,49 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
447456
elif token == r"{% endif %}":
448457
indentation_level -= 1
449458

459+
if not nested_if_statements:
460+
raise SyntaxError("Missing {% if ... %} block for {% endif %}")
461+
462+
nested_if_statements.pop()
463+
450464
# Token is a for loop
451465
elif token.startswith(r"{% for "):
452466
function_string += indent * indentation_level + f"{token[3:-3]}:\n"
453467
indentation_level += 1
454468

455-
forloop_iterables.append(token[3:-3].split(" in ", 1)[1])
469+
nested_for_loops.append(token)
456470
elif token == r"{% empty %}":
457471
indentation_level -= 1
472+
last_forloop_iterable = nested_for_loops[-1][3:-3].split(" in ", 1)[1]
458473

459474
function_string += (
460-
indent * indentation_level + f"if not {forloop_iterables[-1]}:\n"
475+
indent * indentation_level + f"if not {last_forloop_iterable}:\n"
461476
)
462477
indentation_level += 1
463478
elif token == r"{% endfor %}":
464479
indentation_level -= 1
465-
forloop_iterables.pop()
480+
481+
if not nested_for_loops:
482+
raise SyntaxError("Missing {% for ... %} block for {% endfor %}")
483+
484+
nested_for_loops.pop()
466485

467486
# Token is a while loop
468487
elif token.startswith(r"{% while "):
469488
function_string += indent * indentation_level + f"{token[3:-3]}:\n"
470489
indentation_level += 1
490+
491+
nested_while_loops.append(token)
471492
elif token == r"{% endwhile %}":
472493
indentation_level -= 1
473494

495+
if not nested_while_loops:
496+
raise SyntaxError(
497+
"Missing {% while ... %} block for {% endwhile %}"
498+
)
499+
500+
nested_while_loops.pop()
501+
474502
# Token is a Python code
475503
elif token.startswith(r"{% exec "):
476504
expression = token[8:-3]
@@ -481,23 +509,41 @@ def _create_template_function( # pylint: disable=,too-many-locals,too-many-bran
481509
mode = token[14:-3]
482510
if mode not in ("on", "off"):
483511
raise ValueError(f"Unknown autoescape mode: {mode}")
484-
autoescape_modes.append(mode)
512+
513+
nested_autoescape_modes.append(token)
514+
485515
elif token == r"{% endautoescape %}":
486-
if autoescape_modes == ["default_on"]:
487-
raise ValueError("No autoescape mode to end")
488-
autoescape_modes.pop()
516+
if not nested_autoescape_modes:
517+
raise SyntaxError(
518+
"Missing {% autoescape ... %} block for {% endautoescape %}"
519+
)
520+
521+
nested_autoescape_modes.pop()
489522

490523
else:
491-
raise ValueError(
492-
f"Unknown token type: {token} at {token_match.start()}"
493-
)
524+
raise SyntaxError(f"Unknown token type: {token}")
494525

495526
else:
496-
raise ValueError(f"Unknown token type: {token} at {token_match.start()}")
527+
raise SyntaxError(f"Unknown token type: {token}")
497528

498529
# Continue with the rest of the template
499530
template = template[token_match.end() :]
500531

532+
# Checking for unclosed blocks
533+
if len(nested_if_statements) > 0:
534+
last_if_statement = nested_if_statements[-1]
535+
raise SyntaxError("Missing {% endif %} for " + last_if_statement)
536+
537+
if len(nested_for_loops) > 0:
538+
last_for_loop = nested_for_loops[-1]
539+
raise SyntaxError("Missing {% endfor %} for " + last_for_loop)
540+
541+
if len(nested_while_loops) > 0:
542+
last_while_loop = nested_while_loops[-1]
543+
raise SyntaxError("Missing {% endwhile %} for " + last_while_loop)
544+
545+
# No check for unclosed autoescape blocks, as they are optional and do not result in errors
546+
501547
# Add the text after the last token (if any)
502548
text_after_last_token = template
503549

@@ -557,7 +603,9 @@ def __init__(self, template_string: str, *, language: str = Language.HTML) -> No
557603
:param str template_string: String containing the template to be rendered
558604
:param str language: Language for autoescaping. Defaults to HTML
559605
"""
560-
self._template_function = _create_template_function(template_string, language)
606+
self._template_function = _create_template_rendering_function(
607+
template_string, language
608+
)
561609

562610
def render_iter(
563611
self, context: dict = None, *, chunk_size: int = None

0 commit comments

Comments
 (0)