2828 TokenType.DELIMITER,
2929)
3030
31+ # Pre-compute common space strings for indentation (avoids repeated multiplication)
32+ _SPACE_CACHE = [""] + [" " * i for i in range(1, 33)] # Cache up to 32 spaces
33+
34+
35+ def _get_spaces(n: int) -> str:
36+ """Get n spaces, using cache when possible."""
37+ if n < len(_SPACE_CACHE):
38+ return _SPACE_CACHE[n]
39+ return " " * n
40+
3141
3242class ContextNode:
3343 __slots__ = ("context", "parent_context_node")
@@ -47,6 +57,7 @@ def get(self, key: str) -> t.Any:
4757 if key == ".":
4858 return self.context
4959
60+ # Use iterator to avoid copying keys
5061 chain_iter = iter(key.split("."))
5162 curr_ctx = self.context
5263 parent_node = self.parent_context_node
@@ -97,17 +108,11 @@ def open_section(self, key: str) -> t.List[ContextNode]:
97108
98109 # In the case of the list, need a new context for each item
99110 if isinstance(new_context, list):
100- res_list = []
101-
102- for item in new_context:
103- new_stack = ContextNode(item, self)
104- # new_stack.parent_context_node = self
105- res_list.append(new_stack)
106-
111+ # Pre-allocate list for better performance
112+ res_list = [ContextNode(item, self) for item in new_context]
107113 return res_list
108114
109- new_stack = ContextNode(new_context, self)
110- return [new_stack]
115+ return [ContextNode(new_context, self)]
111116
112117
113118class TagType(enum.Enum):
@@ -186,6 +191,7 @@ def __repr__(self) -> str:
186191class MustacheRenderer:
187192 mustache_tree: MustacheTreeNode
188193 partials_dict: t.Dict[str, MustacheTreeNode]
194+ __slots__ = ("mustache_tree", "partials_dict")
189195
190196 def __init__(
191197 self,
@@ -209,7 +215,8 @@ def render(
209215 stringify: t.Callable[[t.Any], str] = str,
210216 html_escape_fn: t.Callable[[str], str] = html_escape,
211217 ) -> str:
212- res_list = []
218+ res_list: t.List[str] = []
219+ res_list_append = res_list.append # Cache method lookup
213220 starting_context = ContextNode(data)
214221 last_was_newline = True # Track if we're at the start of a line
215222
@@ -220,18 +227,20 @@ def render(
220227 (node, starting_context, self.mustache_tree.offset)
221228 for node in self.mustache_tree.children
222229 )
230+ work_deque_popleft = work_deque.popleft # Cache method lookup
231+ work_deque_appendleft = work_deque.appendleft # Cache method lookup
223232 while work_deque:
224- curr_node, curr_context, curr_offset = work_deque.popleft ()
233+ curr_node, curr_context, curr_offset = work_deque_popleft ()
225234 if curr_node.tag_type is TagType.LITERAL:
226- res_list.append (curr_node.data)
235+ res_list_append (curr_node.data)
227236 last_was_newline = curr_node.data.endswith("\n")
228237
229238 # Add offset for partials after newline, but only if there's
230239 # more content coming at the same offset level
231240 if last_was_newline and curr_offset > 0:
232241 # Check if the next node also has the same offset
233242 if work_deque and work_deque[0][2] == curr_offset:
234- res_list.append( curr_offset * " " )
243+ res_list_append(_get_spaces( curr_offset) )
235244
236245 elif (
237246 curr_node.tag_type is TagType.VARIABLE
@@ -246,7 +255,7 @@ def render(
246255 if curr_node.tag_type is TagType.VARIABLE:
247256 str_content = html_escape_fn(str_content)
248257
249- res_list.append (str_content)
258+ res_list_append (str_content)
250259 elif curr_node.tag_type is TagType.SECTION:
251260 new_context_stacks = curr_context.open_section(curr_node.data)
252261
@@ -255,7 +264,7 @@ def render(
255264 for new_context_stack in reversed(new_context_stacks):
256265 for child_node in reversed(curr_node.children):
257266 # No need to make a copy of the context per-child, it's immutable
258- work_deque.appendleft ((child_node, new_context_stack, 0))
267+ work_deque_appendleft ((child_node, new_context_stack, 0))
259268
260269 elif curr_node.tag_type is TagType.INVERTED_SECTION:
261270 # No need to add to the context stack, inverted sections
@@ -266,7 +275,7 @@ def render(
266275
267276 if not bool(lookup_data):
268277 for child_node in reversed(curr_node.children):
269- work_deque.appendleft ((child_node, curr_context, 0))
278+ work_deque_appendleft ((child_node, curr_context, 0))
270279
271280 elif curr_node.tag_type is TagType.PARTIAL:
272281 partial_tree = self.partials_dict.get(curr_node.data)
@@ -279,12 +288,12 @@ def render(
279288 # For standalone partials, add indentation at the beginning
280289 # Only add if we're at the start of a line (last output was newline)
281290 if curr_node.offset > 0 and last_was_newline:
282- res_list.append( curr_node.offset * " " )
291+ res_list_append(_get_spaces( curr_node.offset) )
283292 last_was_newline = False
284293
285294 # Propagate the combined offset through the partial content
286295 for child_node in reversed(partial_tree.children):
287- work_deque.appendleft (
296+ work_deque_appendleft (
288297 (child_node, curr_context, curr_offset + curr_node.offset)
289298 )
290299
@@ -337,6 +346,8 @@ def process_raw_token_list(
337346) -> t.List[TokenTuple]:
338347 indices_to_delete: t.Set[int] = set()
339348 res_token_list: t.List[TokenTuple] = []
349+ res_token_list_append = res_token_list.append # Cache method lookup
350+
340351 for i, token in enumerate(raw_token_list):
341352 token_type, token_data, token_offset = token
342353
@@ -402,67 +413,70 @@ def process_raw_token_list(
402413 if remove_double_prev:
403414 indices_to_delete.add(i - 2)
404415
405- res_token_list.append (token)
416+ res_token_list_append (token)
406417
407418 handle_final_line_clear(res_token_list, _STANDALONE_TOKENS)
408419 # Don't require a trailing newline to remove leading whitespace.
409420
410- res_token_list = [
411- elem for i, elem in enumerate(res_token_list) if i not in indices_to_delete
412- ]
421+ # Only filter if we have deletions to avoid unnecessary list creation
422+ if indices_to_delete:
423+ res_token_list = [
424+ elem for i, elem in enumerate(res_token_list) if i not in indices_to_delete
425+ ]
413426
414427 return res_token_list
415428
416429
417430def create_mustache_tree(thing: str) -> MustacheTreeNode:
418431 root = MustacheTreeNode(TagType.ROOT, "", 0)
419432 work_stack: t.Deque[MustacheTreeNode] = deque([root])
433+ work_stack_append = work_stack.append # Cache method lookup
434+ work_stack_pop = work_stack.pop # Cache method lookup
435+
420436 raw_token_list = mustache_tokenizer(thing)
421437 token_list = process_raw_token_list(raw_token_list)
438+
422439 for token_type, token_data, token_offset in token_list:
423- # token_data = token_data.decode("utf-8")
424440 if token_type is TokenType.LITERAL:
425- literal_node = MustacheTreeNode(TagType.LITERAL, token_data, token_offset)
426- work_stack[-1].add_child(literal_node)
441+ work_stack[-1].add_child(
442+ MustacheTreeNode(TagType.LITERAL, token_data, token_offset)
443+ )
427444
428- elif token_type in [TokenType.SECTION, TokenType.INVERTED_SECTION]:
445+ elif (
446+ token_type is TokenType.SECTION or token_type is TokenType.INVERTED_SECTION
447+ ):
429448 tag_type = (
430449 TagType.SECTION
431450 if token_type is TokenType.SECTION
432451 else TagType.INVERTED_SECTION
433452 )
434453 section_node = MustacheTreeNode(tag_type, token_data, token_offset)
435- # Add section to list of children
436454 work_stack[-1].add_child(section_node)
437-
438- # Add section to work stack and descend in on the next iteration.
439- work_stack.append(section_node)
455+ work_stack_append(section_node)
440456
441457 elif token_type is TokenType.END_SECTION:
442458 if work_stack[-1].data != token_data:
443459 raise StrayClosingTagError(f'Opening tag for "{token_data}" not found.')
460+ work_stack_pop()
444461
445- # Close the current section by popping off the end of the work stack.
446- work_stack.pop()
447-
448- elif token_type in [TokenType.VARIABLE, TokenType.RAW_VARIABLE]:
462+ elif token_type is TokenType.VARIABLE or token_type is TokenType.RAW_VARIABLE:
449463 tag_type = (
450464 TagType.VARIABLE
451465 if token_type is TokenType.VARIABLE
452466 else TagType.VARIABLE_RAW
453467 )
454- variable_node = MustacheTreeNode(tag_type, token_data, token_offset)
455- # Add section to list of children
468+ work_stack[-1].add_child(
469+ MustacheTreeNode(tag_type, token_data, token_offset)
470+ )
456471
457- work_stack[-1].add_child(variable_node)
458- elif token_type is TokenType.COMMENT:
459- pass
460- elif token_type is TokenType.DELIMITER:
461- # Delimiters don't add nodes to the tree, they're handled during tokenization
472+ elif token_type is TokenType.COMMENT or token_type is TokenType.DELIMITER:
473+ # Comments and delimiters don't add nodes to the tree
462474 pass
475+
463476 elif token_type is TokenType.PARTIAL:
464- partial_node = MustacheTreeNode(TagType.PARTIAL, token_data, token_offset)
465- work_stack[-1].add_child(partial_node)
477+ work_stack[-1].add_child(
478+ MustacheTreeNode(TagType.PARTIAL, token_data, token_offset)
479+ )
466480 else:
467481 raise MystaceError
468482
0 commit comments