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