Skip to content

Commit b6a5405

Browse files
add component registry for caching lookups (#63)
* add component registry for caching lookups * add cachetools dep directly * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rename library-wide registry instance to components * update changelog * add cachetools typeshed stubs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * be more specific on exception --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7f50704 commit b6a5405

File tree

7 files changed

+124
-8
lines changed

7 files changed

+124
-8
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added component caching with LRU (Least Recently Used) strategy via global `components` registry.
24+
- `cachetools>=5.5.0` is now a dependency of the library to support this new cache strategy
25+
2126
### Changed
2227

2328
- **Internal**: Flattened package structure by moving files from `components/` subdirectory to root level. No public API changes.
29+
- **Internal**: `BirdNode` now uses cached components instead of creating new ones each time.
2430

2531
## [0.4.0]
2632

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ docs = [
3131
types = [
3232
"django-stubs>=5.1.0",
3333
"django-stubs-ext>=5.1.0",
34-
"mypy>=1.11.2"
34+
"mypy>=1.11.2",
35+
"types-cachetools>=5.5.0.20240820"
3536
]
3637

3738
[project]
@@ -56,6 +57,7 @@ classifiers = [
5657
"Programming Language :: Python :: Implementation :: CPython"
5758
]
5859
dependencies = [
60+
"cachetools>=5.5.0",
5961
"django>=4.2"
6062
]
6163
description = "High-flying components for perfectionists with deadlines"

src/django_bird/components.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4-
from typing import TYPE_CHECKING
54
from typing import Any
65

6+
from cachetools import LRUCache
77
from django.template.backends.django import Template
88
from django.template.loader import select_template
99

1010
from .templates import get_template_names
1111

12-
if TYPE_CHECKING:
13-
pass
14-
1512

1613
@dataclass(frozen=True, slots=True)
1714
class Component:
@@ -30,3 +27,23 @@ def from_name(cls, name: str):
3027
template_names = get_template_names(name)
3128
template = select_template(template_names)
3229
return cls(name=name, template=template)
30+
31+
32+
class Registry:
33+
def __init__(self, maxsize: int = 100):
34+
self._cache: LRUCache[str, Component] = LRUCache(maxsize=maxsize)
35+
36+
def get_component(self, name: str) -> Component:
37+
try:
38+
return self._cache[name]
39+
except KeyError:
40+
component = Component.from_name(name)
41+
self._cache[name] = component
42+
return component
43+
44+
def clear(self) -> None:
45+
"""Clear the component cache. Mainly useful for testing."""
46+
self._cache.clear()
47+
48+
49+
components = Registry()

src/django_bird/templatetags/tags/bird.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django_bird._typing import TagBits
1313
from django_bird._typing import override
1414
from django_bird.components import Component
15+
from django_bird.components import components
1516
from django_bird.params import Params
1617
from django_bird.slots import DEFAULT_SLOT
1718
from django_bird.slots import Slots
@@ -59,7 +60,7 @@ def __init__(self, name: str, params: Params, nodelist: NodeList | None) -> None
5960
@override
6061
def render(self, context: Context) -> str:
6162
component_name = self.get_component_name(context)
62-
component = Component.from_name(component_name)
63+
component = components.get_component(component_name)
6364
component_context = self.get_component_context_data(component, context)
6465
return component.render(component_context)
6566

tests/conftest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def override_templates_settings(base_dir):
7272
def create_bird_dir(base_dir):
7373
def func(name):
7474
bird_template_dir = base_dir / name
75-
bird_template_dir.mkdir()
75+
bird_template_dir.mkdir(exist_ok=True)
7676
return bird_template_dir
7777

7878
return func
@@ -118,3 +118,12 @@ def func(text):
118118
return text.strip()
119119

120120
return func
121+
122+
123+
@pytest.fixture(autouse=True)
124+
def clear_registry():
125+
from django_bird.components import components
126+
127+
components.clear()
128+
yield
129+
components.clear()

tests/test_components.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
from django.template.backends.django import Template
5+
from django.template.exceptions import TemplateDoesNotExist
6+
7+
from django_bird.components import Component
8+
from django_bird.components import Registry
9+
10+
11+
class TestComponent:
12+
def test_from_name(self, create_bird_template):
13+
create_bird_template("button", "<button>Click me</button>")
14+
15+
comp = Component.from_name("button")
16+
17+
assert comp.name == "button"
18+
assert isinstance(comp.template, Template)
19+
assert comp.render({}) == "<button>Click me</button>"
20+
21+
22+
class TestRegistry:
23+
@pytest.fixture
24+
def registry(self):
25+
return Registry(maxsize=2)
26+
27+
def test_get_component_caches(self, registry, create_bird_template):
28+
create_bird_template(name="button", content="<button>Click me</button>")
29+
30+
component1 = registry.get_component("button")
31+
component2 = registry.get_component("button")
32+
33+
assert component1 is component2
34+
35+
def test_lru_cache_behavior(self, registry, create_bird_template):
36+
create_bird_template(name="button1", content="1")
37+
create_bird_template(name="button2", content="2")
38+
create_bird_template(name="button3", content="3")
39+
40+
button1 = registry.get_component("button1")
41+
button2 = registry.get_component("button2")
42+
button3 = registry.get_component("button3")
43+
44+
new_button1 = registry.get_component("button1")
45+
assert new_button1 is not button1
46+
47+
cached_button2 = registry.get_component("button2")
48+
assert cached_button2.name == button2.name
49+
assert cached_button2.render({}) == button2.render({})
50+
51+
cached_button3 = registry.get_component("button3")
52+
assert cached_button3.name == button3.name
53+
assert cached_button3.render({}) == button3.render({})
54+
55+
def test_component_not_found(self, registry):
56+
with pytest.raises(TemplateDoesNotExist):
57+
registry.get_component("nonexistent")

uv.lock

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)