django-template-compiler 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-template-compiler
3
+ Version: 0.0.1
4
+ Summary: A drop-in replacement for Django's template engine, 100% compatible including custom tags and filters, but much faster
5
+ Project-URL: Homepage, https://github.com/samtregar/django-template-compiler
6
+ Project-URL: Repository, https://github.com/samtregar/django-template-compiler
7
+ Project-URL: Issues, https://github.com/samtregar/django-template-compiler/issues
8
+ Author-email: Sam Tregar <sam@tregar.com>
9
+ License-Expression: BSD-3-Clause
10
+ License-File: LICENSE
11
+ Keywords: compiler,django,performance,templates
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
22
+ Classifier: Topic :: Text Processing :: Markup :: HTML
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: django>=4.2
25
+ Provides-Extra: dev
26
+ Requires-Dist: build; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: twine; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # django-template-compiler
32
+
33
+ A drop-in replacement for Django's template engine, 100% compatible including custom tags and filters, but much faster.
34
+
35
+ **Status: pre-alpha, but substantially complete.** Every parseable template compiles: dedicated code generation for the core template language (variables, filters, control flow, inheritance, `simple_tag`/`inclusion_tag`, container tags), with anything else — arbitrary third-party tags included — running as-is against the live context. dtc passes Django's own template test suite (Django 4.2–5.2) in CI, plus a differential fuzzer. Typical speedups: 1.6–2.1x on template-bound rendering, with a ~1.0x floor when a template is dominated by bridged tags. Not yet exercised by production traffic — try it and report.
36
+
37
+ Two behaviors worth knowing:
38
+
39
+ - **`DEBUG=True` disables compilation** (per engine): Django's debug error page and exception annotation need the interpreted render path. Production configs get the compiled path; development keeps perfect debugging.
40
+ - **Django test instrumentation is honored**: when `setup_test_environment()` (the test runner / `assertTemplateUsed`) patches template rendering, dtc detects the patch and routes through it, so the `template_rendered` signal fires exactly as with stock Django.
41
+
42
+ ## How it works
43
+
44
+ Templates are parsed with Django's own lexer and parser, then compiled to Python code — a `{% for %}` loop becomes a real Python `for` loop, variable lookups become direct attribute/key access. Anything the compiler can't handle yet (including arbitrary custom tags) falls back to Django's interpreted render path, so output is always exactly what Django would produce.
45
+
46
+ ## Benchmarks
47
+
48
+ `benchmarks/bench.py`, Python 3.11, Django 5.2 (µs per render; higher speedup is better):
49
+
50
+ | scenario | django | dtc | speedup |
51
+ |---|---:|---:|---:|
52
+ | 40 plain variables | 52.2 | 28.6 | 1.8x |
53
+ | 100-row loop | 165.7 | 73.2 | 2.3x |
54
+ | 100-row loop with `forloop.counter` | 679.4 | 126.9 | **5.4x** |
55
+ | 50×4 table (nested loop + if) | 516.3 | 271.9 | 1.9x |
56
+ | with/if scopes | 206.8 | 91.2 | 2.3x |
57
+ | spaceless-wrapped table | 248.2 | 114.6 | 2.2x |
58
+ | inheritance + include in loop | 151.4 | 93.5 | 1.6x |
59
+ | bridged unknown tag (worst case) | 26.3 | 21.0 | 1.3x |
60
+
61
+ For reference, Jinja2 renders the table scenario in ~80µs — dtc closes about half the gap to Jinja2 while producing byte-identical Django output. The remaining distance is the price of Django's semantics themselves (silent variable failures, callable auto-invocation, the context stack), which dtc preserves exactly and Jinja2 deliberately dropped.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install django-template-compiler
67
+ ```
68
+
69
+ The import name is `dtc`.
70
+
71
+ ## Usage
72
+
73
+ Change one line in your `TEMPLATES` setting:
74
+
75
+ ```python
76
+ TEMPLATES = [
77
+ {
78
+ "BACKEND": "dtc.backend.DTCTemplates", # was django.template.backends.django.DjangoTemplates
79
+ "DIRS": [BASE_DIR / "templates"],
80
+ "APP_DIRS": True,
81
+ "OPTIONS": {
82
+ # all DjangoTemplates options work unchanged
83
+ "context_processors": [...],
84
+ },
85
+ },
86
+ ]
87
+ ```
88
+
89
+ Everything else — template syntax, custom tag libraries, context processors, `{% load %}`, filters — works unchanged.
90
+
91
+ ### Cold starts and the disk cache
92
+
93
+ Compiling costs roughly 9x Django's parse per template, paid once per process. If your deployment restarts processes often (serverless, aggressive autoscaling), enable the disk cache, which persists compiled code objects across processes and cuts that overhead by ~70%:
94
+
95
+ ```python
96
+ "OPTIONS": {
97
+ "dtc_disk_cache": True, # ~/.cache/dtc/..., or pass an explicit path
98
+ },
99
+ ```
100
+
101
+ Cache entries are keyed by a hash of the generated code, so stale entries are impossible by construction; corrupt or version-mismatched entries are silently recompiled. Point it only at a directory you trust — cached code is executed.
102
+
103
+ ### Declaring custom tags context-safe
104
+
105
+ A custom tag without dedicated codegen renders through its own `render()` against the live context, which is always exact — but because the compiler can't see what that `render()` does, one such tag disables the read optimizations around it: the flattened read snapshot (template-wide), scope locals (in every enclosing `{% for %}`/`{% with %}`), and compiled-function sharing across template instances (which matters without a cached loader). `takes_context` simple/inclusion tags pay the first two as well.
106
+
107
+ Most tags never write the context. If yours is one of them, declare it:
108
+
109
+ ```python
110
+ class BreadcrumbNode(Node):
111
+ dtc_context_safe = True # stock Django ignores this; dtc keeps its
112
+ ... # optimizations around the tag
113
+
114
+ # takes_context tags declare the *function*:
115
+ @register.simple_tag(takes_context=True)
116
+ def current_section(context):
117
+ return context.get("section", "home")
118
+ current_section.dtc_context_safe = True
119
+
120
+ # third-party tags you can't edit, e.g. in settings or AppConfig.ready():
121
+ import dtc
122
+ dtc.declare_safe(SomeThirdPartyNode)
123
+ ```
124
+
125
+ The declaration is a promise about every `render()` call: the context stack and its mappings are left exactly as found (balanced push/pop inside is fine); no state keyed on the node's identity (Django's `CycleNode`/`IfChangedNode` pattern); behavior depends only on the parsed source. *Reading* the context is always fine, as is setting `context.autoescape`. A container tag may render nested writers freely, provided every nodelist it renders is listed in the standard `child_nodelists` attribute — the compiler analyzes those children itself; a rendered-but-unlisted nodelist is the one thing that can silently break output. Subclasses inherit the declaration with the `render()` it describes (`dtc_context_safe = False` opts back out). See `help(dtc.declare_safe)` for the precise contract.
126
+
127
+ Tags that *do* write the context can declare **what** they write instead, as long as the target names are fixed at parse time — the common capture/setter shape:
128
+
129
+ ```python
130
+ class CaptureNode(Node):
131
+ # names the instance attributes holding the written context keys
132
+ dtc_context_writes = ("target",)
133
+
134
+ def __init__(self, nodelist, target):
135
+ self.nodelist = nodelist
136
+ self.target = target # {% capture NAME %}...{% endcapture %}
137
+
138
+ def render(self, context):
139
+ context[self.target] = self.nodelist.render(context)
140
+ return ""
141
+
142
+ # or for classes you can't edit:
143
+ dtc.declare_writes(SomeVendorSetterNode, "dest")
144
+ ```
145
+
146
+ The compiler routes reads of the declared names through the live context and keeps every optimization on for everything else — including scope locals: if a declared write shadows a `{% for %}`/`{% with %}` name, the generated code re-reads that local right after the tag runs. The rest of the contract matches `dtc_context_safe`; the declared keys may be *set* only (no deletions), and an attribute holding `None` means an optional target unused at that site. See `help(dtc.declare_writes)`.
147
+
148
+ Declared writes may target the normal top-of-stack (`context[key] = value`) **or** the root layer (`context.dicts[0][key] = value` — the pattern used by tags that persist a value across template boundaries, past every scope pop). Root-written names are never served from the read snapshot (it excludes the root layer by design), so they stay exact across template boundaries, includes, and re-writes.
149
+
150
+ A wrong declaration produces wrong output silently — so verify it: run your test suite with `DTC_CHECK_DECLARATIONS=1` and dtc checks every declared render, raising `dtc.ContextSafeViolation` on any write outside the declaration. (Containers wrapping legitimate writers are skipped by the checker; the source-determinism clause isn't mechanically checkable.)
151
+
152
+ Tags that just compute a value from their arguments — a formatter, a calculator, a lookup — are better rewritten as `@register.simple_tag`: those compile natively, declaration-free, with argument resolution inlined.
153
+
154
+ ### Limitation: tags that rewrite enclosing context layers
155
+
156
+ Within a single template, *any* custom tag is rendered exactly — the compiler disables its read optimizations around every tag it doesn't recognize. Across template boundaries there is one assumption: a tag's context effects that outlive an `{% include %}`/`{% block %}`/`{% extends %}` are either **scope-limited** (ordinary `context[key] = value` writes and balanced push/pop, which die with the layers that the include/block machinery pops) or **root-layer** (`context.dicts[0][key] = value`, which dtc handles as described above). Every Django built-in and every `simple_tag`/`inclusion_tag` satisfies this.
157
+
158
+ A tag that mutates an *intermediate* layer of the caller's stack — indexing `context.dicts[1]`, calling `Context.set_upward()`, deleting keys from enclosing layers, or leaking an unbalanced `push()` — from inside an included or extended template **can produce output that differs from stock Django**: the enclosing template was compiled without knowledge of that tag, and its read snapshot or scope locals may serve the pre-mutation value. `DTC_CHECK_DECLARATIONS` cannot catch this (the tag carries no declaration, and the effect surfaces in a different template than the tag).
159
+
160
+ If you have such a tag, the supported paths are: write the root layer instead (`dicts[0]` — fully supported and declarable), write the top of the stack, or confine the mutation to the template that renders the tag. Note that intermediate-layer writes are fragile under stock Django too — what `dicts[1]` *is* depends on the stack depth at the call site.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ pip install -e .[dev]
166
+ pytest
167
+ ```
168
+
169
+ ## License
170
+
171
+ BSD 3-Clause. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ dtc/__init__.py,sha256=SCu3XWMtuAIIWEHaSYtHpQUThdzYFVaP-Le4Md2VYrc,5620
2
+ dtc/autopatch.py,sha256=CqoP3e92fVAiK_8sNH00CWlgdBqVZeEypYcYPNh3JOE,2512
3
+ dtc/backend.py,sha256=_vuCIpBC-A4_ztG7QHdMj2r3Dx6_YKARVrAXwIKVks4,2791
4
+ dtc/compiler.py,sha256=0bjPwlqUzK5i9jkFI0hg_x3M7FwjvjT2nNU3i8xxHPA,65272
5
+ dtc/diskcache.py,sha256=p0WZclYTZF7iOCDx0WndcdHyooHTXpygwPBES4jSq_g,3541
6
+ dtc/runtime.py,sha256=30MFZXSM6Gz-ZsLtVmNRLJT0mWRW2tvbdf9WUlUzZ44,15408
7
+ django_template_compiler-0.0.1.dist-info/METADATA,sha256=4Usc0xiDupcO_5snK03OQmWD6k4cgift6EqVcTg3ujE,11008
8
+ django_template_compiler-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ django_template_compiler-0.0.1.dist-info/licenses/LICENSE,sha256=n74JfQDQIZ-eBz4adf7AMuwOqLwzu9PYy2cO3HaueBI,1497
10
+ django_template_compiler-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Sam Tregar
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
dtc/__init__.py ADDED
@@ -0,0 +1,124 @@
1
+ """dtc -- a drop-in, compiling replacement for Django's template engine."""
2
+
3
+ __version__ = "0.0.1"
4
+
5
+ __all__ = ["ContextSafeViolation", "declare_safe", "declare_writes", "__version__"]
6
+
7
+
8
+ class ContextSafeViolation(Exception):
9
+ """A node declared ``dtc_context_safe`` broke the declaration's contract.
10
+
11
+ Raised only under ``DTC_CHECK_DECLARATIONS=1`` (see ``declare_safe``);
12
+ without it a wrong declaration silently produces wrong output.
13
+ """
14
+
15
+
16
+ def declare_safe(obj):
17
+ """Declare a custom template tag context-safe, letting the compiler keep
18
+ its read optimizations around it.
19
+
20
+ ``obj`` is either a ``django.template.Node`` subclass (a raw
21
+ ``register.tag`` tag) or the function registered with
22
+ ``@simple_tag(takes_context=True)`` / ``@inclusion_tag(takes_context=True)``.
23
+ Sets ``obj.dtc_context_safe = True`` and returns ``obj``, so it works as a
24
+ decorator. Tags you own can skip this helper and set the class attribute
25
+ directly — stock Django ignores it, so no dtc import is needed.
26
+
27
+ The declaration is a promise about every ``render()`` call (for a
28
+ function, every call):
29
+
30
+ (a) The context stack and every mapping on it are left exactly as found:
31
+ no ``push``/``pop``/``__setitem__``/``del`` visible after return
32
+ (balanced internal push/pop is fine). Effects of rendering child
33
+ nodelists are exempt — see (d).
34
+ (b) No per-render state keyed by the node's identity — nothing like
35
+ ``context.render_context[self]`` or mutable state on ``self``
36
+ (Django's CycleNode/IfChangedNode pattern). Caches derived purely
37
+ from the parsed arguments are fine.
38
+ (c) Behavior depends only on the parsed source: node instances parsed
39
+ from identical source are interchangeable (dtc may render other
40
+ same-source template instances through one parse's node objects).
41
+ (d) Any nodelist the node renders is listed in Django's
42
+ ``child_nodelists`` attribute. The compiler analyzes those children
43
+ itself, so their effects (including context writes by nested tags)
44
+ are exempt from (a). A nodelist rendered but *not* listed hides
45
+ nested writers from the compiler and produces wrong output — this
46
+ is the one part of the contract nothing can check.
47
+
48
+ Reading the context is always fine, as is setting
49
+ ``context.autoescape`` (compiled code re-reads it after every bridged
50
+ call). Clauses (b) and (c) do not apply to takes_context functions —
51
+ only (a) does.
52
+
53
+ Subclasses inherit the declaration along with the ``render()`` it
54
+ covers; a subclass whose ``render()`` no longer qualifies must set
55
+ ``dtc_context_safe = False``.
56
+
57
+ Run your test suite with ``DTC_CHECK_DECLARATIONS=1`` to verify
58
+ declarations: declared-safe renders are then checked against clauses
59
+ (a) and (b) and raise ``ContextSafeViolation`` on violation.
60
+ """
61
+ if isinstance(obj, type):
62
+ from django.template.base import Node
63
+
64
+ if not issubclass(obj, Node):
65
+ raise TypeError(
66
+ f"declare_safe() expects a django.template.Node subclass or a "
67
+ f"takes_context tag function, got the class {obj!r}"
68
+ )
69
+ elif not callable(obj):
70
+ raise TypeError(
71
+ f"declare_safe() expects a django.template.Node subclass or a "
72
+ f"takes_context tag function, got {obj!r}"
73
+ )
74
+ obj.dtc_context_safe = True
75
+ return obj
76
+
77
+
78
+ def declare_writes(node_class, *attr_names):
79
+ """Declare that a Node subclass's ``render()`` writes exactly the
80
+ context keys held in the named *instance attributes* — and follows the
81
+ rest of the ``declare_safe`` contract for everything else.
82
+
83
+ The attributes are named (rather than the keys themselves) because
84
+ write targets are parse-time data::
85
+
86
+ class CaptureNode(Node):
87
+ def __init__(self, nodelist, target):
88
+ self.nodelist = nodelist
89
+ self.target = target # {% capture NAME %}...{% endcapture %}
90
+ def render(self, context):
91
+ context[self.target] = self.nodelist.render(context)
92
+ return ""
93
+
94
+ dtc.declare_writes(CaptureNode, "target")
95
+ # or, on a class you own, equivalently:
96
+ # dtc_context_writes = ("target",)
97
+
98
+ The promise, per ``render()`` call: the only context mutations are
99
+ setting the named keys — on the effective top of the stack
100
+ (``context[k] = value``) or on the root layer
101
+ (``context.dicts[0][k] = value``, the pattern of tags that persist a
102
+ value across template boundaries) — no other writes, no intermediate
103
+ layers, no
104
+ deletions, no net push/pop, and clauses (b)–(d) of the
105
+ ``declare_safe`` contract. An attribute holding None means an optional
106
+ target unused at that site. With no attributes this is equivalent to
107
+ ``declare_safe``. Only Node subclasses can declare writes (a
108
+ ``takes_context`` function's targets aren't inspectable); subclasses
109
+ inherit the declaration.
110
+
111
+ Verify with ``DTC_CHECK_DECLARATIONS=1``: writes outside the declared
112
+ keys raise ``ContextSafeViolation``.
113
+ """
114
+ from django.template.base import Node
115
+
116
+ if not (isinstance(node_class, type) and issubclass(node_class, Node)):
117
+ raise TypeError(
118
+ f"declare_writes() expects a django.template.Node subclass, "
119
+ f"got {node_class!r}"
120
+ )
121
+ if not all(isinstance(a, str) for a in attr_names):
122
+ raise TypeError("declare_writes() attribute names must be strings")
123
+ node_class.dtc_context_writes = attr_names
124
+ return node_class
dtc/autopatch.py ADDED
@@ -0,0 +1,65 @@
1
+ """Engine-level instrumentation: compile every ``django.template.base.Template``.
2
+
3
+ ``install()`` patches ``Template._render`` to try the dtc-compiled path with
4
+ lazy per-instance compilation (shared with the rest of dtc via
5
+ ``runtime.compiled_for``), falling back to Django's interpreted renderer.
6
+ This hooks the *engine* level rather than the BACKENDS proxy, so it also
7
+ covers templates constructed directly — which is how Django's own
8
+ ``template_tests`` suite builds them (see ``scripts/run_django_suite.py``).
9
+
10
+ It also substitutes ``django.test.utils.instrumented_test_render`` so that
11
+ when ``setup_test_environment()`` re-patches ``_render`` for test
12
+ instrumentation, the replacement still takes the compiled path: the
13
+ ``template_rendered`` signal is sent exactly as stock Django sends it, then
14
+ rendering proceeds compiled-or-fallback.
15
+
16
+ Experimental; the supported integration point is ``dtc.backend.DTCTemplates``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from . import runtime
22
+ from .runtime import compiled_for, stats
23
+
24
+ _installed = False
25
+
26
+
27
+ def install():
28
+ global _installed
29
+ if _installed:
30
+ return
31
+ _installed = True
32
+
33
+ import django.test.utils as test_utils
34
+ from django.template.base import Template
35
+ from django.test.signals import template_rendered
36
+
37
+ orig_render = Template._render
38
+
39
+ def _render(self, context):
40
+ compiled = compiled_for(self)
41
+ if compiled is None:
42
+ stats["renders_fallback"] += 1
43
+ return orig_render(self, context)
44
+ stats["renders_compiled"] += 1
45
+ return compiled(context)
46
+
47
+ Template._render = _render
48
+ # This replacement is compiled-aware (it only adds stats counting), so
49
+ # the literal-include fast path may route around it. The
50
+ # instrumented_test_render below must NOT be registered: it sends the
51
+ # template_rendered signal, which routing around would silence.
52
+ runtime._transparent_renders.append(_render)
53
+
54
+ def instrumented_test_render(self, context):
55
+ # Byte-for-byte what django.test.utils.instrumented_test_render does,
56
+ # with the nodelist render swapped for compiled-or-fallback.
57
+ template_rendered.send(sender=self, template=self, context=context)
58
+ compiled = compiled_for(self)
59
+ if compiled is None:
60
+ stats["renders_fallback"] += 1
61
+ return self.nodelist.render(context)
62
+ stats["renders_compiled"] += 1
63
+ return compiled(context)
64
+
65
+ test_utils.instrumented_test_render = instrumented_test_render
dtc/backend.py ADDED
@@ -0,0 +1,80 @@
1
+ """Django BACKENDS engine for dtc.
2
+
3
+ Usage in settings.py -- change only the BACKEND line of an existing
4
+ DjangoTemplates configuration:
5
+
6
+ TEMPLATES = [
7
+ {
8
+ "BACKEND": "dtc.backend.DTCTemplates",
9
+ "DIRS": [...],
10
+ "APP_DIRS": True,
11
+ "OPTIONS": {...},
12
+ },
13
+ ]
14
+
15
+ All OPTIONS accepted by django.template.backends.django.DjangoTemplates
16
+ (context_processors, libraries, builtins, autoescape, debug, loaders,
17
+ string_if_invalid, ...) are accepted here with identical meaning.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from django.template import TemplateDoesNotExist
23
+ from django.template.backends.django import (
24
+ DjangoTemplates,
25
+ Template as DjangoTemplateProxy,
26
+ reraise,
27
+ )
28
+ from django.template.context import make_context
29
+
30
+ from .runtime import compiled_for, template_render
31
+
32
+
33
+ class DTCTemplates(DjangoTemplates):
34
+ """Drop-in replacement for the DjangoTemplates backend.
35
+
36
+ Parsing, template loading, and configuration are inherited unchanged
37
+ from Django; only the render path differs. Templates that the compiler
38
+ can handle render through generated Python code, everything else falls
39
+ back to Django's interpreted renderer.
40
+ """
41
+
42
+ def __init__(self, params):
43
+ params = params.copy()
44
+ options = params.get("OPTIONS", {}).copy()
45
+ # dtc's own option must not reach Django's Engine (unknown kwargs
46
+ # raise TypeError there).
47
+ disk_cache = options.pop("dtc_disk_cache", None)
48
+ params["OPTIONS"] = options
49
+ super().__init__(params)
50
+ if disk_cache:
51
+ from .diskcache import resolve_dir
52
+
53
+ self.engine._dtc_disk_cache = resolve_dir(disk_cache)
54
+
55
+ def from_string(self, template_code):
56
+ return Template(self.engine.from_string(template_code), self)
57
+
58
+ def get_template(self, template_name):
59
+ try:
60
+ return Template(self.engine.get_template(template_name), self)
61
+ except TemplateDoesNotExist as exc:
62
+ reraise(exc, self)
63
+
64
+
65
+ class Template(DjangoTemplateProxy):
66
+ """Backend template proxy that prefers the compiled render path."""
67
+
68
+ def __init__(self, template, backend):
69
+ super().__init__(template, backend)
70
+ self._compiled = compiled_for(template) # instance-cached
71
+
72
+ def render(self, context=None, request=None):
73
+ context = make_context(
74
+ context, request, autoescape=self.backend.engine.autoescape
75
+ )
76
+ # template_render reproduces django.template.base.Template.render
77
+ # exactly around the compiled body (render_context push, template
78
+ # binding — which runs context processors on RequestContext), and
79
+ # falls back to Django's own render when not compiled.
80
+ return template_render(self.template, context)