Skip to content

Commit 0252538

Browse files
committed
Added TemplateSyntaxError and moved underlining function to error class method
1 parent 58a58c8 commit 0252538

File tree

1 file changed

+108
-130
lines changed

1 file changed

+108
-130
lines changed

adafruit_templateengine.py

Lines changed: 108 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,71 @@ def __init__(self, template: str, start_position: int, end_position: int):
6868
self.content = template[start_position:end_position]
6969

7070

71+
class TemplateSyntaxError(SyntaxError):
72+
"""Raised when a syntax error is encountered in a template."""
73+
74+
def __init__(self, message: str, token: Token):
75+
super().__init__(f"{message}\n\n" + self._underline_token_in_template(token))
76+
77+
@staticmethod
78+
def _underline_token_in_template(
79+
token: Token, *, lines_around: int = 4, symbol: str = "^"
80+
) -> str:
81+
"""
82+
Return ``number_of_lines`` lines before and after the token, with the token content underlined
83+
with ``symbol`` e.g.:
84+
85+
```html
86+
[8 lines skipped]
87+
Shopping list:
88+
<ul>
89+
{% for item in context["items"] %}
90+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
91+
<li>{{ item["name"] }} - ${{ item["price"] }}</li>
92+
{% empty %}
93+
[5 lines skipped]
94+
```
95+
"""
96+
97+
template_before_token = token.template[: token.start_position]
98+
if (skipped_lines := template_before_token.count("\n") - lines_around) > 0:
99+
template_before_token = f"[{skipped_lines} lines skipped]\n" + "\n".join(
100+
template_before_token.split("\n")[-(lines_around + 1) :]
101+
)
102+
103+
template_after_token = token.template[token.end_position :]
104+
if (skipped_lines := template_after_token.count("\n") - lines_around) > 0:
105+
template_after_token = (
106+
"\n".join(template_after_token.split("\n")[: (lines_around + 1)])
107+
+ f"\n[{skipped_lines} lines skipped]"
108+
)
109+
110+
lines_before_line_with_token = template_before_token.rsplit("\n", 1)[0]
111+
112+
line_with_token = (
113+
template_before_token.rsplit("\n", 1)[-1]
114+
+ token.content
115+
+ template_after_token.split("\n")[0]
116+
)
117+
118+
line_with_underline = (
119+
" " * len(template_before_token.rsplit("\n", 1)[-1])
120+
+ symbol * len(token.content)
121+
+ " " * len(template_after_token.split("\n")[0])
122+
)
123+
124+
lines_after_line_with_token = template_after_token.split("\n", 1)[-1]
125+
126+
return "\n".join(
127+
[
128+
lines_before_line_with_token,
129+
line_with_token,
130+
line_with_underline,
131+
lines_after_line_with_token,
132+
]
133+
)
134+
135+
71136
def safe_html(value: Any) -> str:
72137
"""
73138
Encodes unsafe symbols in ``value`` to HTML entities and returns the string that can be safely
@@ -202,64 +267,6 @@ def _find_named_endblock(template: str, name: str):
202267
return re.search(r"{% endblock " + name + r" %}", template)
203268

204269

205-
def _underline_token_in_template(
206-
token: Token, *, lines_around: int = 5, symbol: str = "^"
207-
) -> str:
208-
"""
209-
Return ``number_of_lines`` lines before and after the token, with the token content underlined
210-
with ``symbol`` e.g.:
211-
212-
```html
213-
[8 lines skipped]
214-
Shopping list:
215-
<ul>
216-
{% for item in context["items"] %}
217-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
218-
<li>{{ item["name"] }} - ${{ item["price"] }}</li>
219-
{% empty %}
220-
[5 lines skipped]
221-
```
222-
"""
223-
224-
template_before_token = token.template[: token.start_position]
225-
if (skipped_lines := template_before_token.count("\n") - lines_around) > 0:
226-
template_before_token = f"[{skipped_lines} lines skipped]\n" + "\n".join(
227-
template_before_token.split("\n")[-(lines_around + 1) :]
228-
)
229-
230-
template_after_token = token.template[token.end_position :]
231-
if (skipped_lines := template_after_token.count("\n") - lines_around) > 0:
232-
template_after_token = (
233-
"\n".join(template_after_token.split("\n")[: (lines_around + 1)])
234-
+ f"\n[{skipped_lines} lines skipped]"
235-
)
236-
237-
lines_before_line_with_token = template_before_token.rsplit("\n", 1)[0]
238-
239-
line_with_token = (
240-
template_before_token.rsplit("\n", 1)[-1]
241-
+ token.content
242-
+ template_after_token.split("\n")[0]
243-
)
244-
245-
line_with_underline = (
246-
" " * len(template_before_token.rsplit("\n", 1)[-1])
247-
+ symbol * len(token.content)
248-
+ " " * len(template_after_token.split("\n")[0])
249-
)
250-
251-
lines_after_line_with_token = template_after_token.split("\n", 1)[-1]
252-
253-
return "\n".join(
254-
[
255-
lines_before_line_with_token,
256-
line_with_token,
257-
line_with_underline,
258-
lines_after_line_with_token,
259-
]
260-
)
261-
262-
263270
def _exists_and_is_file(path: str) -> bool:
264271
try:
265272
return (os.stat(path)[0] & 0b_11110000_00000000) == 0b_10000000_00000000
@@ -274,12 +281,7 @@ def _resolve_includes(template: str):
274281
# TODO: Restrict include to specific directory
275282

276283
if not _exists_and_is_file(template_path):
277-
raise OSError(
278-
f"Include template not found: {template_path}\n\n"
279-
+ _underline_token_in_template(
280-
Token(template, include_match.start(), include_match.end())
281-
)
282-
)
284+
raise OSError(f"Template file not found: {template_path}")
283285

284286
# Replace the include with the template content
285287
with open(template_path, "rt", encoding="utf-8") as template_file:
@@ -316,15 +318,13 @@ def _resolve_includes_blocks_and_extends(template: str):
316318
endblock_match = _find_named_endblock(template[offset:], block_name)
317319

318320
if endblock_match is None:
319-
raise SyntaxError(
320-
"Missing {% endblock %}:\n\n"
321-
+ _underline_token_in_template(
322-
Token(
323-
template,
324-
offset + block_match.start(),
325-
offset + block_match.end(),
326-
)
327-
)
321+
raise TemplateSyntaxError(
322+
"Missing {% endblock %}",
323+
Token(
324+
template,
325+
offset + block_match.start(),
326+
offset + block_match.end(),
327+
),
328328
)
329329

330330
block_content = template[
@@ -333,15 +333,13 @@ def _resolve_includes_blocks_and_extends(template: str):
333333

334334
# Check for unsupported nested blocks
335335
if (nested_block_match := _find_block(block_content)) is not None:
336-
raise SyntaxError(
337-
"Nested blocks are not supported:\n\n"
338-
+ _underline_token_in_template(
339-
Token(
340-
template,
341-
offset + block_match.end() + nested_block_match.start(),
342-
offset + block_match.end() + nested_block_match.end(),
343-
)
344-
)
336+
raise TemplateSyntaxError(
337+
"Nested blocks are not supported",
338+
Token(
339+
template,
340+
offset + block_match.end() + nested_block_match.start(),
341+
offset + block_match.end() + nested_block_match.end(),
342+
),
345343
)
346344

347345
if block_name in block_replacements:
@@ -382,15 +380,13 @@ def _replace_blocks_with_replacements(template: str, replacements: "dict[str, st
382380

383381
# Check for unsupported nested blocks
384382
if (nested_block_match := _find_block(block_content)) is not None:
385-
raise SyntaxError(
386-
"Nested blocks are not supported:\n\n"
387-
+ _underline_token_in_template(
388-
Token(
389-
template,
390-
block_match.end() + nested_block_match.start(),
391-
block_match.end() + nested_block_match.end(),
392-
)
393-
)
383+
raise TemplateSyntaxError(
384+
"Nested blocks are not supported",
385+
Token(
386+
template,
387+
block_match.end() + nested_block_match.start(),
388+
block_match.end() + nested_block_match.end(),
389+
),
394390
)
395391

396392
# No replacement for this block, use default content
@@ -552,23 +548,26 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
552548

553549
nested_if_statements.append(token)
554550
elif token.content.startswith(r"{% elif "):
551+
if not nested_if_statements:
552+
raise TemplateSyntaxError("Missing {% if ... %}", token)
553+
555554
indentation_level -= 1
556555
function_string += (
557556
indent * indentation_level + f"{token.content[3:-3]}:\n"
558557
)
559558
indentation_level += 1
560559
elif token.content == r"{% else %}":
560+
if not nested_if_statements:
561+
raise TemplateSyntaxError("Missing {% if ... %}", token)
562+
561563
indentation_level -= 1
562564
function_string += indent * indentation_level + "else:\n"
563565
indentation_level += 1
564566
elif token.content == r"{% endif %}":
565-
indentation_level -= 1
566-
567567
if not nested_if_statements:
568-
raise SyntaxError(
569-
"Missing {% if ... %}\n\n" + _underline_token_in_template(token)
570-
)
568+
raise TemplateSyntaxError("Missing {% if ... %}", token)
571569

570+
indentation_level -= 1
572571
nested_if_statements.pop()
573572

574573
# Token is a for loop
@@ -580,24 +579,22 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
580579

581580
nested_for_loops.append(token)
582581
elif token.content == r"{% empty %}":
582+
if not nested_for_loops:
583+
raise TemplateSyntaxError("Missing {% for ... %}", token)
584+
583585
indentation_level -= 1
584586
last_forloop_iterable = (
585587
nested_for_loops[-1].content[3:-3].split(" in ", 1)[1]
586588
)
587-
588589
function_string += (
589590
indent * indentation_level + f"if not {last_forloop_iterable}:\n"
590591
)
591592
indentation_level += 1
592593
elif token.content == r"{% endfor %}":
593-
indentation_level -= 1
594-
595594
if not nested_for_loops:
596-
raise SyntaxError(
597-
"Missing {% for ... %}\n\n"
598-
+ _underline_token_in_template(token)
599-
)
595+
raise TemplateSyntaxError("Missing {% for ... %}", token)
600596

597+
indentation_level -= 1
601598
nested_for_loops.pop()
602599

603600
# Token is a while loop
@@ -609,14 +606,10 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
609606

610607
nested_while_loops.append(token)
611608
elif token.content == r"{% endwhile %}":
612-
indentation_level -= 1
613-
614609
if not nested_while_loops:
615-
raise SyntaxError(
616-
"Missing {% while ... %}\n\n"
617-
+ _underline_token_in_template(token)
618-
)
610+
raise TemplateSyntaxError("Missing {% while ... %}", token)
619611

612+
indentation_level -= 1
620613
nested_while_loops.pop()
621614

622615
# Token is a Python code
@@ -634,46 +627,31 @@ def _create_template_rendering_function( # pylint: disable=,too-many-locals,too
634627

635628
elif token.content == r"{% endautoescape %}":
636629
if not nested_autoescape_modes:
637-
raise SyntaxError(
638-
"Missing {% autoescape ... %}\n\n"
639-
+ _underline_token_in_template(token)
640-
)
630+
raise TemplateSyntaxError("Missing {% autoescape ... %}", token)
641631

642632
nested_autoescape_modes.pop()
643633

644634
else:
645-
raise SyntaxError(
646-
f"Unknown token type: {token.content}\n\n"
647-
+ _underline_token_in_template(token)
648-
)
635+
raise TemplateSyntaxError(f"Unknown token type: {token.content}", token)
649636

650637
else:
651-
raise SyntaxError(
652-
f"Unknown token type: {token.content}\n\n"
653-
+ _underline_token_in_template(token)
654-
)
638+
raise TemplateSyntaxError(f"Unknown token type: {token.content}", token)
655639

656640
# Move offset to the end of the token
657641
offset += token_match.end()
658642

659643
# Checking for unclosed blocks
660644
if len(nested_if_statements) > 0:
661645
last_if_statement = nested_if_statements[-1]
662-
raise SyntaxError(
663-
"Missing {% endif %}\n\n" + _underline_token_in_template(last_if_statement)
664-
)
646+
raise TemplateSyntaxError("Missing {% endif %}", last_if_statement)
665647

666648
if len(nested_for_loops) > 0:
667649
last_for_loop = nested_for_loops[-1]
668-
raise SyntaxError(
669-
"Missing {% endfor %}\n\n" + _underline_token_in_template(last_for_loop)
670-
)
650+
raise TemplateSyntaxError("Missing {% endfor %}", last_for_loop)
671651

672652
if len(nested_while_loops) > 0:
673653
last_while_loop = nested_while_loops[-1]
674-
raise SyntaxError(
675-
"Missing {% endwhile %}\n\n" + _underline_token_in_template(last_while_loop)
676-
)
654+
raise TemplateSyntaxError("Missing {% endwhile %}", last_while_loop)
677655

678656
# No check for unclosed autoescape blocks, as they are optional and do not result in errors
679657

0 commit comments

Comments
 (0)