Skip to content

Commit 9f2b888

Browse files
Move lru cache from processor into parser with a few tests.
1 parent 3704372 commit 9f2b888

File tree

3 files changed

+131
-32
lines changed

3 files changed

+131
-32
lines changed

tdom/parser.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import sys
12
import typing as t
23
from string.templatelib import Template, Interpolation
34
from html.parser import HTMLParser
45
from dataclasses import dataclass, field
6+
from functools import lru_cache
57

68
from .nodes import (
79
VOID_ELEMENTS,
@@ -428,11 +430,35 @@ def get_comp_starttag(self, starttag_ip_index: int) -> str:
428430
)
429431

430432

431-
def parse_html(
432-
template: Template,
433+
@lru_cache(maxsize=0 if "pytest" in sys.modules else 512)
434+
def _parse_html(
435+
cached_template: CachedTemplate,
433436
) -> (
434437
TNode # @TODO: Might be more consistent for this to always be a container.
435438
):
439+
parser = TemplateParser()
440+
parser.feed_template(cached_template.template)
441+
parser.close()
442+
return parser.get_node()
443+
444+
445+
@dataclass
446+
class CachedTemplate:
447+
"""Attempt to cache template just by its strings."""
448+
449+
template: Template
450+
451+
def __hash__(self):
452+
return hash(self.template.strings)
453+
454+
def __eq__(self, other):
455+
return (
456+
isinstance(other, CachedTemplate)
457+
and self.template.strings == other.template.strings
458+
)
459+
460+
461+
def parse_html(template: Template) -> TNode:
436462
"""
437463
Parse a string, or sequence of HTML string chunks, into a Node tree.
438464
@@ -441,7 +467,4 @@ def parse_html(
441467
This is particularly useful if you want to keep specific text chunks
442468
separate in the resulting Node tree.
443469
"""
444-
parser = TemplateParser()
445-
parser.feed_template(template)
446-
parser.close()
447-
return parser.get_node()
470+
return _parse_html(CachedTemplate(template))

tdom/parser_test.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pytest
22
from string.templatelib import Template, Interpolation
3+
from functools import lru_cache
34

45
from .nodes import (
6+
TNode,
57
TComment,
68
TDocumentType,
79
TElement,
@@ -13,7 +15,7 @@
1315
StaticAttribute,
1416
SpreadAttribute,
1517
)
16-
from .parser import parse_html
18+
from .parser import _parse_html, parse_html, CachedTemplate
1719

1820

1921
class TestHelpers:
@@ -437,3 +439,101 @@ def DivWrapper(attrs, embedded_t):
437439
]
438440
),
439441
)
442+
443+
444+
@pytest.fixture()
445+
def parse_html_simulation():
446+
"""
447+
Simulate parse_html/_parse_html using the lru_cache.
448+
"""
449+
450+
@lru_cache(maxsize=512)
451+
def _simulated_parse_html(cached_template: CachedTemplate) -> TNode:
452+
return _parse_html.__wrapped__(cached_template)
453+
454+
def simulated_parse_html(template: Template) -> TNode:
455+
return _simulated_parse_html(CachedTemplate(template))
456+
457+
yield simulated_parse_html, _simulated_parse_html
458+
_simulated_parse_html.cache_clear()
459+
460+
461+
def hits_misses_helper(caching_func):
462+
"""Shorthand for unpacking a cached func's cache info over-and-over."""
463+
464+
def _hits_misses():
465+
info = caching_func.cache_info()
466+
return (info.hits, info.misses)
467+
468+
return _hits_misses
469+
470+
471+
def test_parse_html_cache_hit_after_first_parse(parse_html_simulation):
472+
simulated_parse_html, _simulated_parse_html = parse_html_simulation
473+
hits_misses = hits_misses_helper(_simulated_parse_html)
474+
assert hits_misses() == (0, 0)
475+
476+
in_template = t"<div>Parsing {'html'}!</div>"
477+
out_tnode = th.el("div", children=tuple([th.text("Parsing ", 0, "!")]))
478+
479+
assert simulated_parse_html(in_template) == out_tnode
480+
assert hits_misses() == (0, 1) # MISS!
481+
assert simulated_parse_html(in_template) == out_tnode
482+
assert hits_misses() == (1, 1) # HIT!
483+
484+
485+
def test_parse_html_cache_hit_same_strings(parse_html_simulation):
486+
simulated_parse_html, _simulated_parse_html = parse_html_simulation
487+
hits_misses = hits_misses_helper(_simulated_parse_html)
488+
assert hits_misses() == (0, 0)
489+
490+
in_template = t"<div>Parsing {'html'}!</div>"
491+
alt_template = t"<div>Parsing {100}!</div>"
492+
out_tnode = th.el("div", children=tuple([th.text("Parsing ", 0, "!")]))
493+
494+
# Strings equal but interpolation values are different
495+
assert (
496+
alt_template.strings == in_template.strings
497+
and alt_template.values != in_template.values
498+
)
499+
500+
assert simulated_parse_html(in_template) == out_tnode
501+
assert hits_misses() == (0, 1) # MISS!
502+
assert simulated_parse_html(in_template) == out_tnode
503+
assert hits_misses() == (1, 1) # HIT!
504+
assert simulated_parse_html(alt_template) == out_tnode
505+
assert hits_misses() == (2, 1) # HIT!
506+
507+
508+
def test_cached_template_eq():
509+
ct1 = CachedTemplate(t"<div>Parsing {'html'}!</div>")
510+
ct2 = CachedTemplate(t"<div>Parsing {100}!</div>")
511+
assert (
512+
ct1.template.strings == ct2.template.strings
513+
and ct1.template.values != ct2.template.values
514+
)
515+
assert ct1 == ct2 and ct1 == ct1
516+
517+
ct3 = CachedTemplate(t"<div>Still parsing {'html'}!</div>")
518+
assert (
519+
ct1.template.strings != ct3.template.strings
520+
and ct1.template.values == ct3.template.values
521+
)
522+
assert ct1 != ct3
523+
524+
525+
def test_cached_template_hash():
526+
ct1 = CachedTemplate(t"<div>Parsing {'html'}!</div>")
527+
ct2 = CachedTemplate(t"<div>Parsing {100}!</div>")
528+
assert (
529+
ct1.template.strings == ct2.template.strings
530+
and ct1.template.values != ct2.template.values
531+
)
532+
assert hash(ct1) == hash(ct2)
533+
534+
ct3 = CachedTemplate(t"<div>Still parsing {'html'}!</div>")
535+
assert (
536+
ct1.template.strings != ct3.template.strings
537+
and ct1.template.values == ct3.template.values
538+
)
539+
assert hash(ct1) != hash(ct3)

tdom/processor.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import sys
21
import typing as t
32
from collections.abc import Iterable
4-
from dataclasses import dataclass
53
from collections import OrderedDict
6-
from functools import lru_cache
74
from string.templatelib import Interpolation, Template
85

96
from markupsafe import Markup
@@ -410,27 +407,6 @@ def _substitute_node(tnode: TNode, interpolations: tuple[Interpolation, ...]) ->
410407
return DocumentType(tnode.text)
411408

412409

413-
@dataclass
414-
class CachedTemplate:
415-
"""Attempt to cache template just by its strings."""
416-
417-
template: Template
418-
419-
def __hash__(self):
420-
return hash(self.template.strings)
421-
422-
def __eq__(self, other):
423-
return (
424-
isinstance(other, CachedTemplate)
425-
and self.template.strings == other.template.strings
426-
)
427-
428-
429-
@lru_cache(maxsize=0 if "pytest" in sys.modules else 512)
430-
def _parse_html(cached_template):
431-
return parse_html(cached_template.template)
432-
433-
434410
# --------------------------------------------------------------------------
435411
# Public API
436412
# --------------------------------------------------------------------------
@@ -440,5 +416,5 @@ def html(template: Template) -> Node:
440416
"""Parse a t-string and return a tree of Nodes."""
441417
# Parse the HTML, returning a tree of nodes with placeholders
442418
# where interpolations go.
443-
tnode = _parse_html(CachedTemplate(template))
419+
tnode = parse_html(template)
444420
return _substitute_node(tnode, template.interpolations)

0 commit comments

Comments
 (0)