1import time
2
3from mako import lookup
4from mako.cache import CacheImpl
5from mako.cache import register_plugin
6from mako.lookup import TemplateLookup
7from mako.template import Template
8from mako.testing.assertions import eq_
9from mako.testing.config import config
10from mako.testing.exclusions import requires_beaker
11from mako.testing.exclusions import requires_dogpile_cache
12from mako.testing.helpers import result_lines
13
14
15module_base = str(config.module_base)
16
17
18class SimpleBackend:
19    def __init__(self):
20        self.cache = {}
21
22    def get(self, key, **kw):
23        return self.cache[key]
24
25    def invalidate(self, key, **kw):
26        self.cache.pop(key, None)
27
28    def put(self, key, value, **kw):
29        self.cache[key] = value
30
31    def get_or_create(self, key, creation_function, **kw):
32        if key in self.cache:
33            return self.cache[key]
34
35        self.cache[key] = value = creation_function()
36        return value
37
38
39class MockCacheImpl(CacheImpl):
40    realcacheimpl = None
41
42    def __init__(self, cache):
43        self.cache = cache
44
45    def set_backend(self, cache, backend):
46        if backend == "simple":
47            self.realcacheimpl = SimpleBackend()
48        else:
49            self.realcacheimpl = cache._load_impl(backend)
50
51    def _setup_kwargs(self, kw):
52        self.kwargs = kw.copy()
53        self.kwargs.pop("regions", None)
54        self.kwargs.pop("manager", None)
55        if self.kwargs.get("region") != "myregion":
56            self.kwargs.pop("region", None)
57
58    def get_or_create(self, key, creation_function, **kw):
59        self.key = key
60        self._setup_kwargs(kw)
61        return self.realcacheimpl.get_or_create(key, creation_function, **kw)
62
63    def put(self, key, value, **kw):
64        self.key = key
65        self._setup_kwargs(kw)
66        self.realcacheimpl.put(key, value, **kw)
67
68    def get(self, key, **kw):
69        self.key = key
70        self._setup_kwargs(kw)
71        return self.realcacheimpl.get(key, **kw)
72
73    def invalidate(self, key, **kw):
74        self.key = key
75        self._setup_kwargs(kw)
76        self.realcacheimpl.invalidate(key, **kw)
77
78
79register_plugin("mock", __name__, "MockCacheImpl")
80
81
82class CacheTest:
83    real_backend = "simple"
84
85    def _install_mock_cache(self, template, implname=None):
86        template.cache_impl = "mock"
87        impl = template.cache.impl
88        impl.set_backend(template.cache, implname or self.real_backend)
89        return impl
90
91    def test_def(self):
92        t = Template(
93            """
94        <%!
95            callcount = [0]
96        %>
97        <%def name="foo()" cached="True">
98            this is foo
99            <%
100            callcount[0] += 1
101            %>
102        </%def>
103
104        ${foo()}
105        ${foo()}
106        ${foo()}
107        callcount: ${callcount}
108"""
109        )
110        m = self._install_mock_cache(t)
111        assert result_lines(t.render()) == [
112            "this is foo",
113            "this is foo",
114            "this is foo",
115            "callcount: [1]",
116        ]
117        assert m.kwargs == {}
118
119    def test_cache_enable(self):
120        t = Template(
121            """
122            <%!
123                callcount = [0]
124            %>
125            <%def name="foo()" cached="True">
126                <% callcount[0] += 1 %>
127            </%def>
128            ${foo()}
129            ${foo()}
130            callcount: ${callcount}
131        """,
132            cache_enabled=False,
133        )
134        self._install_mock_cache(t)
135
136        eq_(t.render().strip(), "callcount: [2]")
137
138    def test_nested_def(self):
139        t = Template(
140            """
141        <%!
142            callcount = [0]
143        %>
144        <%def name="foo()">
145            <%def name="bar()" cached="True">
146                this is foo
147                <%
148                callcount[0] += 1
149                %>
150            </%def>
151            ${bar()}
152        </%def>
153
154        ${foo()}
155        ${foo()}
156        ${foo()}
157        callcount: ${callcount}
158"""
159        )
160        m = self._install_mock_cache(t)
161        assert result_lines(t.render()) == [
162            "this is foo",
163            "this is foo",
164            "this is foo",
165            "callcount: [1]",
166        ]
167        assert m.kwargs == {}
168
169    def test_page(self):
170        t = Template(
171            """
172        <%!
173            callcount = [0]
174        %>
175        <%page cached="True"/>
176        this is foo
177        <%
178        callcount[0] += 1
179        %>
180        callcount: ${callcount}
181"""
182        )
183        m = self._install_mock_cache(t)
184        t.render()
185        t.render()
186        assert result_lines(t.render()) == ["this is foo", "callcount: [1]"]
187        assert m.kwargs == {}
188
189    def test_dynamic_key_with_context(self):
190        t = Template(
191            """
192            <%block name="foo" cached="True" cache_key="${mykey}">
193                some block
194            </%block>
195        """
196        )
197        m = self._install_mock_cache(t)
198        t.render(mykey="thekey")
199        t.render(mykey="thekey")
200        eq_(result_lines(t.render(mykey="thekey")), ["some block"])
201        eq_(m.key, "thekey")
202
203        t = Template(
204            """
205            <%def name="foo()" cached="True" cache_key="${mykey}">
206                some def
207            </%def>
208            ${foo()}
209        """
210        )
211        m = self._install_mock_cache(t)
212        t.render(mykey="thekey")
213        t.render(mykey="thekey")
214        eq_(result_lines(t.render(mykey="thekey")), ["some def"])
215        eq_(m.key, "thekey")
216
217    def test_dynamic_key_with_funcargs(self):
218        t = Template(
219            """
220            <%def name="foo(num=5)" cached="True" cache_key="foo_${str(num)}">
221             hi
222            </%def>
223
224            ${foo()}
225        """
226        )
227        m = self._install_mock_cache(t)
228        t.render()
229        t.render()
230        assert result_lines(t.render()) == ["hi"]
231        assert m.key == "foo_5"
232
233        t = Template(
234            """
235            <%def name="foo(*args, **kwargs)" cached="True"
236             cache_key="foo_${kwargs['bar']}">
237             hi
238            </%def>
239
240            ${foo(1, 2, bar='lala')}
241        """
242        )
243        m = self._install_mock_cache(t)
244        t.render()
245        assert result_lines(t.render()) == ["hi"]
246        assert m.key == "foo_lala"
247
248        t = Template(
249            """
250        <%page args="bar='hi'" cache_key="foo_${bar}" cached="True"/>
251         hi
252        """
253        )
254        m = self._install_mock_cache(t)
255        t.render()
256        assert result_lines(t.render()) == ["hi"]
257        assert m.key == "foo_hi"
258
259    def test_dynamic_key_with_imports(self):
260        lookup = TemplateLookup()
261        lookup.put_string(
262            "foo.html",
263            """
264        <%!
265            callcount = [0]
266        %>
267        <%namespace file="ns.html" import="*"/>
268        <%page cached="True" cache_key="${foo}"/>
269        this is foo
270        <%
271        callcount[0] += 1
272        %>
273        callcount: ${callcount}
274""",
275        )
276        lookup.put_string("ns.html", """""")
277        t = lookup.get_template("foo.html")
278        m = self._install_mock_cache(t)
279        t.render(foo="somekey")
280        t.render(foo="somekey")
281        assert result_lines(t.render(foo="somekey")) == [
282            "this is foo",
283            "callcount: [1]",
284        ]
285        assert m.kwargs == {}
286
287    def test_fileargs_implicit(self):
288        l = lookup.TemplateLookup(module_directory=module_base)
289        l.put_string(
290            "test",
291            """
292                <%!
293                    callcount = [0]
294                %>
295                <%def name="foo()" cached="True" cache_type='dbm'>
296                    this is foo
297                    <%
298                    callcount[0] += 1
299                    %>
300                </%def>
301
302                ${foo()}
303                ${foo()}
304                ${foo()}
305                callcount: ${callcount}
306        """,
307        )
308
309        m = self._install_mock_cache(l.get_template("test"))
310        assert result_lines(l.get_template("test").render()) == [
311            "this is foo",
312            "this is foo",
313            "this is foo",
314            "callcount: [1]",
315        ]
316        eq_(m.kwargs, {"type": "dbm"})
317
318    def test_fileargs_deftag(self):
319        t = Template(
320            """
321        <%%!
322            callcount = [0]
323        %%>
324        <%%def name="foo()" cached="True" cache_type='file' cache_dir='%s'>
325            this is foo
326            <%%
327            callcount[0] += 1
328            %%>
329        </%%def>
330
331        ${foo()}
332        ${foo()}
333        ${foo()}
334        callcount: ${callcount}
335"""
336            % module_base
337        )
338        m = self._install_mock_cache(t)
339        assert result_lines(t.render()) == [
340            "this is foo",
341            "this is foo",
342            "this is foo",
343            "callcount: [1]",
344        ]
345        assert m.kwargs == {"type": "file", "dir": module_base}
346
347    def test_fileargs_pagetag(self):
348        t = Template(
349            """
350        <%%page cache_dir='%s' cache_type='dbm'/>
351        <%%!
352            callcount = [0]
353        %%>
354        <%%def name="foo()" cached="True">
355            this is foo
356            <%%
357            callcount[0] += 1
358            %%>
359        </%%def>
360
361        ${foo()}
362        ${foo()}
363        ${foo()}
364        callcount: ${callcount}
365"""
366            % module_base
367        )
368        m = self._install_mock_cache(t)
369        assert result_lines(t.render()) == [
370            "this is foo",
371            "this is foo",
372            "this is foo",
373            "callcount: [1]",
374        ]
375        eq_(m.kwargs, {"dir": module_base, "type": "dbm"})
376
377    def test_args_complete(self):
378        t = Template(
379            """
380        <%%def name="foo()" cached="True" cache_timeout="30" cache_dir="%s"
381         cache_type="file" cache_key='somekey'>
382            this is foo
383        </%%def>
384
385        ${foo()}
386"""
387            % module_base
388        )
389        m = self._install_mock_cache(t)
390        t.render()
391        eq_(m.kwargs, {"dir": module_base, "type": "file", "timeout": 30})
392
393        t2 = Template(
394            """
395        <%%page cached="True" cache_timeout="30" cache_dir="%s"
396         cache_type="file" cache_key='somekey'/>
397        hi
398        """
399            % module_base
400        )
401        m = self._install_mock_cache(t2)
402        t2.render()
403        eq_(m.kwargs, {"dir": module_base, "type": "file", "timeout": 30})
404
405    def test_fileargs_lookup(self):
406        l = lookup.TemplateLookup(cache_dir=module_base, cache_type="file")
407        l.put_string(
408            "test",
409            """
410                <%!
411                    callcount = [0]
412                %>
413                <%def name="foo()" cached="True">
414                    this is foo
415                    <%
416                    callcount[0] += 1
417                    %>
418                </%def>
419
420                ${foo()}
421                ${foo()}
422                ${foo()}
423                callcount: ${callcount}
424        """,
425        )
426
427        t = l.get_template("test")
428        m = self._install_mock_cache(t)
429        assert result_lines(l.get_template("test").render()) == [
430            "this is foo",
431            "this is foo",
432            "this is foo",
433            "callcount: [1]",
434        ]
435        eq_(m.kwargs, {"dir": module_base, "type": "file"})
436
437    def test_buffered(self):
438        t = Template(
439            """
440        <%!
441            def a(text):
442                return "this is a " + text.strip()
443        %>
444        ${foo()}
445        ${foo()}
446        <%def name="foo()" cached="True" buffered="True">
447            this is a test
448        </%def>
449        """,
450            buffer_filters=["a"],
451        )
452        self._install_mock_cache(t)
453        eq_(
454            result_lines(t.render()),
455            ["this is a this is a test", "this is a this is a test"],
456        )
457
458    def test_load_from_expired(self):
459        """test that the cache callable can be called safely after the
460        originating template has completed rendering.
461
462        """
463        t = Template(
464            """
465        ${foo()}
466        <%def name="foo()" cached="True" cache_timeout="1">
467            foo
468        </%def>
469        """
470        )
471        self._install_mock_cache(t)
472
473        x1 = t.render()
474        time.sleep(1.2)
475        x2 = t.render()
476        assert x1.strip() == x2.strip() == "foo"
477
478    def test_namespace_access(self):
479        t = Template(
480            """
481            <%def name="foo(x)" cached="True">
482                foo: ${x}
483            </%def>
484
485            <%
486                foo(1)
487                foo(2)
488                local.cache.invalidate_def('foo')
489                foo(3)
490                foo(4)
491            %>
492        """
493        )
494        self._install_mock_cache(t)
495        eq_(result_lines(t.render()), ["foo: 1", "foo: 1", "foo: 3", "foo: 3"])
496
497    def test_lookup(self):
498        l = TemplateLookup(cache_impl="mock")
499        l.put_string(
500            "x",
501            """
502            <%page cached="True" />
503            ${y}
504        """,
505        )
506        t = l.get_template("x")
507        self._install_mock_cache(t)
508        assert result_lines(t.render(y=5)) == ["5"]
509        assert result_lines(t.render(y=7)) == ["5"]
510        assert isinstance(t.cache.impl, MockCacheImpl)
511
512    def test_invalidate(self):
513        t = Template(
514            """
515            <%%def name="foo()" cached="True">
516                foo: ${x}
517            </%%def>
518
519            <%%def name="bar()" cached="True" cache_type='dbm' cache_dir='%s'>
520                bar: ${x}
521            </%%def>
522            ${foo()} ${bar()}
523        """
524            % module_base
525        )
526        self._install_mock_cache(t)
527        assert result_lines(t.render(x=1)) == ["foo: 1", "bar: 1"]
528        assert result_lines(t.render(x=2)) == ["foo: 1", "bar: 1"]
529        t.cache.invalidate_def("foo")
530        assert result_lines(t.render(x=3)) == ["foo: 3", "bar: 1"]
531        t.cache.invalidate_def("bar")
532        assert result_lines(t.render(x=4)) == ["foo: 3", "bar: 4"]
533
534        t = Template(
535            """
536            <%%page cached="True" cache_type="dbm" cache_dir="%s"/>
537
538            page: ${x}
539        """
540            % module_base
541        )
542        self._install_mock_cache(t)
543        assert result_lines(t.render(x=1)) == ["page: 1"]
544        assert result_lines(t.render(x=2)) == ["page: 1"]
545        t.cache.invalidate_body()
546        assert result_lines(t.render(x=3)) == ["page: 3"]
547        assert result_lines(t.render(x=4)) == ["page: 3"]
548
549    def test_custom_args_def(self):
550        t = Template(
551            """
552            <%def name="foo()" cached="True" cache_region="myregion"
553                    cache_timeout="50" cache_foo="foob">
554            </%def>
555            ${foo()}
556        """
557        )
558        m = self._install_mock_cache(t, "simple")
559        t.render()
560        eq_(m.kwargs, {"region": "myregion", "timeout": 50, "foo": "foob"})
561
562    def test_custom_args_block(self):
563        t = Template(
564            """
565            <%block name="foo" cached="True" cache_region="myregion"
566                    cache_timeout="50" cache_foo="foob">
567            </%block>
568        """
569        )
570        m = self._install_mock_cache(t, "simple")
571        t.render()
572        eq_(m.kwargs, {"region": "myregion", "timeout": 50, "foo": "foob"})
573
574    def test_custom_args_page(self):
575        t = Template(
576            """
577            <%page cached="True" cache_region="myregion"
578                    cache_timeout="50" cache_foo="foob"/>
579        """
580        )
581        m = self._install_mock_cache(t, "simple")
582        t.render()
583        eq_(m.kwargs, {"region": "myregion", "timeout": 50, "foo": "foob"})
584
585    def test_pass_context(self):
586        t = Template(
587            """
588            <%page cached="True"/>
589        """
590        )
591        m = self._install_mock_cache(t)
592        t.render()
593        assert "context" not in m.kwargs
594
595        m.pass_context = True
596        t.render(x="bar")
597        assert "context" in m.kwargs
598        assert m.kwargs["context"].get("x") == "bar"
599
600
601class RealBackendMixin:
602    def test_cache_uses_current_context(self):
603        t = Template(
604            """
605        ${foo()}
606        <%def name="foo()" cached="True" cache_timeout="1">
607            foo: ${x}
608        </%def>
609        """
610        )
611        self._install_mock_cache(t)
612
613        x1 = t.render(x=1)
614        time.sleep(1.2)
615        x2 = t.render(x=2)
616        eq_(x1.strip(), "foo: 1")
617        eq_(x2.strip(), "foo: 2")
618
619    def test_region(self):
620        t = Template(
621            """
622            <%block name="foo" cached="True" cache_region="short">
623                short term ${x}
624            </%block>
625            <%block name="bar" cached="True" cache_region="long">
626                long term ${x}
627            </%block>
628            <%block name="lala">
629                none ${x}
630            </%block>
631        """
632        )
633
634        self._install_mock_cache(t)
635        r1 = result_lines(t.render(x=5))
636        time.sleep(1.2)
637        r2 = result_lines(t.render(x=6))
638        r3 = result_lines(t.render(x=7))
639        eq_(r1, ["short term 5", "long term 5", "none 5"])
640        eq_(r2, ["short term 6", "long term 5", "none 6"])
641        eq_(r3, ["short term 6", "long term 5", "none 7"])
642
643
644@requires_beaker
645class BeakerCacheTest(RealBackendMixin, CacheTest):
646    real_backend = "beaker"
647
648    def _install_mock_cache(self, template, implname=None):
649        template.cache_args["manager"] = self._regions()
650        return super()._install_mock_cache(template, implname)
651
652    def _regions(self):
653        import beaker
654
655        return beaker.cache.CacheManager(
656            cache_regions={
657                "short": {"expire": 1, "type": "memory"},
658                "long": {"expire": 60, "type": "memory"},
659            }
660        )
661
662
663@requires_dogpile_cache
664class DogpileCacheTest(RealBackendMixin, CacheTest):
665    real_backend = "dogpile.cache"
666
667    def _install_mock_cache(self, template, implname=None):
668        template.cache_args["regions"] = self._regions()
669        template.cache_args.setdefault("region", "short")
670        return super()._install_mock_cache(template, implname)
671
672    def _regions(self):
673        from dogpile.cache import make_region
674
675        my_regions = {
676            "short": make_region().configure(
677                "dogpile.cache.memory", expiration_time=1
678            ),
679            "long": make_region().configure(
680                "dogpile.cache.memory", expiration_time=60
681            ),
682            "myregion": make_region().configure(
683                "dogpile.cache.memory", expiration_time=60
684            ),
685        }
686
687        return my_regions
688