1# coding: utf-8 2 3""" 4Unit tests of template.py. 5 6""" 7 8import codecs 9import os 10import sys 11import unittest 12 13from .examples.simple import Simple 14from pystache import Renderer 15from pystache import TemplateSpec 16from pystache.common import TemplateNotFoundError 17from pystache.context import ContextStack, KeyNotFoundError 18from pystache.loader import Loader 19 20from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin 21from pystache.tests.data.views import SayHello 22 23 24def _make_renderer(): 25 """ 26 Return a default Renderer instance for testing purposes. 27 28 """ 29 renderer = Renderer(string_encoding='ascii', file_encoding='ascii') 30 return renderer 31 32 33def mock_unicode(b, encoding=None): 34 if encoding is None: 35 encoding = 'ascii' 36 u = str(b, encoding=encoding) 37 return u.upper() 38 39 40class RendererInitTestCase(unittest.TestCase): 41 42 """ 43 Tests the Renderer.__init__() method. 44 45 """ 46 47 def test_partials__default(self): 48 """ 49 Test the default value. 50 51 """ 52 renderer = Renderer() 53 self.assertTrue(renderer.partials is None) 54 55 def test_partials(self): 56 """ 57 Test that the attribute is set correctly. 58 59 """ 60 renderer = Renderer(partials={'foo': 'bar'}) 61 self.assertEqual(renderer.partials, {'foo': 'bar'}) 62 63 def test_escape__default(self): 64 escape = Renderer().escape 65 66 self.assertEqual(escape(">"), ">") 67 self.assertEqual(escape('"'), """) 68 # Single quotes are escaped only in Python 3.2 and later. 69 if sys.version_info < (3, 2): 70 expected = "'" 71 else: 72 expected = ''' 73 self.assertEqual(escape("'"), expected) 74 75 def test_escape(self): 76 escape = lambda s: "**" + s 77 renderer = Renderer(escape=escape) 78 self.assertEqual(renderer.escape("bar"), "**bar") 79 80 def test_decode_errors__default(self): 81 """ 82 Check the default value. 83 84 """ 85 renderer = Renderer() 86 self.assertEqual(renderer.decode_errors, 'strict') 87 88 def test_decode_errors(self): 89 """ 90 Check that the constructor sets the attribute correctly. 91 92 """ 93 renderer = Renderer(decode_errors="foo") 94 self.assertEqual(renderer.decode_errors, "foo") 95 96 def test_file_encoding__default(self): 97 """ 98 Check the file_encoding default. 99 100 """ 101 renderer = Renderer() 102 self.assertEqual(renderer.file_encoding, renderer.string_encoding) 103 104 def test_file_encoding(self): 105 """ 106 Check that the file_encoding attribute is set correctly. 107 108 """ 109 renderer = Renderer(file_encoding='foo') 110 self.assertEqual(renderer.file_encoding, 'foo') 111 112 def test_file_extension__default(self): 113 """ 114 Check the file_extension default. 115 116 """ 117 renderer = Renderer() 118 self.assertEqual(renderer.file_extension, 'mustache') 119 120 def test_file_extension(self): 121 """ 122 Check that the file_encoding attribute is set correctly. 123 124 """ 125 renderer = Renderer(file_extension='foo') 126 self.assertEqual(renderer.file_extension, 'foo') 127 128 def test_missing_tags(self): 129 """ 130 Check that the missing_tags attribute is set correctly. 131 132 """ 133 renderer = Renderer(missing_tags='foo') 134 self.assertEqual(renderer.missing_tags, 'foo') 135 136 def test_missing_tags__default(self): 137 """ 138 Check the missing_tags default. 139 140 """ 141 renderer = Renderer() 142 self.assertEqual(renderer.missing_tags, 'ignore') 143 144 def test_search_dirs__default(self): 145 """ 146 Check the search_dirs default. 147 148 """ 149 renderer = Renderer() 150 self.assertEqual(renderer.search_dirs, [os.curdir]) 151 152 def test_search_dirs__string(self): 153 """ 154 Check that the search_dirs attribute is set correctly when a string. 155 156 """ 157 renderer = Renderer(search_dirs='foo') 158 self.assertEqual(renderer.search_dirs, ['foo']) 159 160 def test_search_dirs__list(self): 161 """ 162 Check that the search_dirs attribute is set correctly when a list. 163 164 """ 165 renderer = Renderer(search_dirs=['foo']) 166 self.assertEqual(renderer.search_dirs, ['foo']) 167 168 def test_string_encoding__default(self): 169 """ 170 Check the default value. 171 172 """ 173 renderer = Renderer() 174 self.assertEqual(renderer.string_encoding, sys.getdefaultencoding()) 175 176 def test_string_encoding(self): 177 """ 178 Check that the constructor sets the attribute correctly. 179 180 """ 181 renderer = Renderer(string_encoding="foo") 182 self.assertEqual(renderer.string_encoding, "foo") 183 184 185class RendererTests(unittest.TestCase, AssertStringMixin): 186 187 """Test the Renderer class.""" 188 189 def _renderer(self): 190 return Renderer() 191 192 ## Test Renderer.unicode(). 193 194 def test_unicode__string_encoding(self): 195 """ 196 Test that the string_encoding attribute is respected. 197 198 """ 199 renderer = self._renderer() 200 b = "é".encode('utf-8') 201 202 renderer.string_encoding = "ascii" 203 self.assertRaises(UnicodeDecodeError, renderer.str, b) 204 205 renderer.string_encoding = "utf-8" 206 self.assertEqual(renderer.str(b), "é") 207 208 def test_unicode__decode_errors(self): 209 """ 210 Test that the decode_errors attribute is respected. 211 212 """ 213 renderer = self._renderer() 214 renderer.string_encoding = "ascii" 215 b = "déf".encode('utf-8') 216 217 renderer.decode_errors = "ignore" 218 self.assertEqual(renderer.str(b), "df") 219 220 renderer.decode_errors = "replace" 221 # U+FFFD is the official Unicode replacement character. 222 self.assertEqual(renderer.str(b), u'd\ufffd\ufffdf') 223 224 ## Test the _make_loader() method. 225 226 def test__make_loader__return_type(self): 227 """ 228 Test that _make_loader() returns a Loader. 229 230 """ 231 renderer = self._renderer() 232 loader = renderer._make_loader() 233 234 self.assertEqual(type(loader), Loader) 235 236 def test__make_loader__attributes(self): 237 """ 238 Test that _make_loader() sets all attributes correctly.. 239 240 """ 241 unicode_ = lambda x: x 242 243 renderer = self._renderer() 244 renderer.file_encoding = 'enc' 245 renderer.file_extension = 'ext' 246 renderer.str = unicode_ 247 248 loader = renderer._make_loader() 249 250 self.assertEqual(loader.extension, 'ext') 251 self.assertEqual(loader.file_encoding, 'enc') 252 self.assertEqual(loader.to_unicode, unicode_) 253 254 ## Test the render() method. 255 256 def test_render__return_type(self): 257 """ 258 Check that render() returns a string of type unicode. 259 260 """ 261 renderer = self._renderer() 262 rendered = renderer.render('foo') 263 self.assertEqual(type(rendered), str) 264 265 def test_render__unicode(self): 266 renderer = self._renderer() 267 actual = renderer.render('foo') 268 self.assertEqual(actual, 'foo') 269 270 def test_render__str(self): 271 renderer = self._renderer() 272 actual = renderer.render('foo') 273 self.assertEqual(actual, 'foo') 274 275 def test_render__non_ascii_character(self): 276 renderer = self._renderer() 277 actual = renderer.render('Poincaré') 278 self.assertEqual(actual, 'Poincaré') 279 280 def test_render__context(self): 281 """ 282 Test render(): passing a context. 283 284 """ 285 renderer = self._renderer() 286 self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom') 287 288 def test_render__context_and_kwargs(self): 289 """ 290 Test render(): passing a context and **kwargs. 291 292 """ 293 renderer = self._renderer() 294 template = 'Hi {{person1}} and {{person2}}' 295 self.assertEqual(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad') 296 297 def test_render__kwargs_and_no_context(self): 298 """ 299 Test render(): passing **kwargs and no context. 300 301 """ 302 renderer = self._renderer() 303 self.assertEqual(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom') 304 305 def test_render__context_and_kwargs__precedence(self): 306 """ 307 Test render(): **kwargs takes precedence over context. 308 309 """ 310 renderer = self._renderer() 311 self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad') 312 313 def test_render__kwargs_does_not_modify_context(self): 314 """ 315 Test render(): passing **kwargs does not modify the passed context. 316 317 """ 318 context = {} 319 renderer = self._renderer() 320 renderer.render('Hi {{person}}', context=context, foo="bar") 321 self.assertEqual(context, {}) 322 323 def test_render__nonascii_template(self): 324 """ 325 Test passing a non-unicode template with non-ascii characters. 326 327 """ 328 renderer = _make_renderer() 329 template = "déf".encode("utf-8") 330 331 # Check that decode_errors and string_encoding are both respected. 332 renderer.decode_errors = 'ignore' 333 renderer.string_encoding = 'ascii' 334 self.assertEqual(renderer.render(template), "df") 335 336 renderer.string_encoding = 'utf_8' 337 self.assertEqual(renderer.render(template), "déf") 338 339 def test_make_resolve_partial(self): 340 """ 341 Test the _make_resolve_partial() method. 342 343 """ 344 renderer = Renderer() 345 renderer.partials = {'foo': 'bar'} 346 resolve_partial = renderer._make_resolve_partial() 347 348 actual = resolve_partial('foo') 349 self.assertEqual(actual, 'bar') 350 self.assertEqual(type(actual), str, "RenderEngine requires that " 351 "resolve_partial return unicode strings.") 352 353 def test_make_resolve_partial__unicode(self): 354 """ 355 Test _make_resolve_partial(): that resolve_partial doesn't "double-decode" Unicode. 356 357 """ 358 renderer = Renderer() 359 360 renderer.partials = {'partial': 'foo'} 361 resolve_partial = renderer._make_resolve_partial() 362 self.assertEqual(resolve_partial("partial"), "foo") 363 364 # Now with a value that is already unicode. 365 renderer.partials = {'partial': 'foo'} 366 resolve_partial = renderer._make_resolve_partial() 367 # If the next line failed, we would get the following error: 368 # TypeError: decoding Unicode is not supported 369 self.assertEqual(resolve_partial("partial"), "foo") 370 371 def test_render_name(self): 372 """Test the render_name() method.""" 373 data_dir = get_data_path() 374 renderer = Renderer(search_dirs=data_dir) 375 actual = renderer.render_name("say_hello", to='foo') 376 self.assertString(actual, "Hello, foo") 377 378 def test_render_path(self): 379 """ 380 Test the render_path() method. 381 382 """ 383 renderer = Renderer() 384 path = get_data_path('say_hello.mustache') 385 actual = renderer.render_path(path, to='foo') 386 self.assertEqual(actual, "Hello, foo") 387 388 def test_render__object(self): 389 """ 390 Test rendering an object instance. 391 392 """ 393 renderer = Renderer() 394 395 say_hello = SayHello() 396 actual = renderer.render(say_hello) 397 self.assertEqual('Hello, World', actual) 398 399 actual = renderer.render(say_hello, to='Mars') 400 self.assertEqual('Hello, Mars', actual) 401 402 def test_render__template_spec(self): 403 """ 404 Test rendering a TemplateSpec instance. 405 406 """ 407 renderer = Renderer() 408 409 class Spec(TemplateSpec): 410 template = "hello, {{to}}" 411 to = 'world' 412 413 spec = Spec() 414 actual = renderer.render(spec) 415 self.assertString(actual, 'hello, world') 416 417 def test_render__view(self): 418 """ 419 Test rendering a View instance. 420 421 """ 422 renderer = Renderer() 423 424 view = Simple() 425 actual = renderer.render(view) 426 self.assertEqual('Hi pizza!', actual) 427 428 def test_custom_string_coercion_via_assignment(self): 429 """ 430 Test that string coercion can be customized via attribute assignment. 431 432 """ 433 renderer = self._renderer() 434 def to_str(val): 435 if not val: 436 return '' 437 else: 438 return str(val) 439 440 self.assertEqual(renderer.render('{{value}}', value=None), 'None') 441 renderer.str_coerce = to_str 442 self.assertEqual(renderer.render('{{value}}', value=None), '') 443 444 def test_custom_string_coercion_via_subclassing(self): 445 """ 446 Test that string coercion can be customized via subclassing. 447 448 """ 449 class MyRenderer(Renderer): 450 def str_coerce(self, val): 451 if not val: 452 return '' 453 else: 454 return str(val) 455 renderer1 = Renderer() 456 renderer2 = MyRenderer() 457 458 self.assertEqual(renderer1.render('{{value}}', value=None), 'None') 459 self.assertEqual(renderer2.render('{{value}}', value=None), '') 460 461 462# By testing that Renderer.render() constructs the right RenderEngine, 463# we no longer need to exercise all rendering code paths through 464# the Renderer. It suffices to test rendering paths through the 465# RenderEngine for the same amount of code coverage. 466class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin): 467 468 """ 469 Check the RenderEngine returned by Renderer._make_render_engine(). 470 471 """ 472 473 def _make_renderer(self): 474 """ 475 Return a default Renderer instance for testing purposes. 476 477 """ 478 return _make_renderer() 479 480 ## Test the engine's resolve_partial attribute. 481 482 def test__resolve_partial__returns_unicode(self): 483 """ 484 Check that resolve_partial returns unicode (and not a subclass). 485 486 """ 487 class MyUnicode(str): 488 pass 489 490 renderer = Renderer() 491 renderer.string_encoding = 'ascii' 492 renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')} 493 494 engine = renderer._make_render_engine() 495 496 actual = engine.resolve_partial('str') 497 self.assertEqual(actual, "foo") 498 self.assertEqual(type(actual), str) 499 500 # Check that unicode subclasses are not preserved. 501 actual = engine.resolve_partial('subclass') 502 self.assertEqual(actual, "abc") 503 self.assertEqual(type(actual), str) 504 505 def test__resolve_partial__not_found(self): 506 """ 507 Check that resolve_partial returns the empty string when a template is not found. 508 509 """ 510 renderer = Renderer() 511 512 engine = renderer._make_render_engine() 513 resolve_partial = engine.resolve_partial 514 515 self.assertString(resolve_partial('foo'), '') 516 517 def test__resolve_partial__not_found__missing_tags_strict(self): 518 """ 519 Check that resolve_partial provides a nice message when a template is not found. 520 521 """ 522 renderer = Renderer() 523 renderer.missing_tags = 'strict' 524 525 engine = renderer._make_render_engine() 526 resolve_partial = engine.resolve_partial 527 528 self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']", 529 resolve_partial, "foo") 530 531 def test__resolve_partial__not_found__partials_dict(self): 532 """ 533 Check that resolve_partial returns the empty string when a template is not found. 534 535 """ 536 renderer = Renderer() 537 renderer.partials = {} 538 539 engine = renderer._make_render_engine() 540 resolve_partial = engine.resolve_partial 541 542 self.assertString(resolve_partial('foo'), '') 543 544 def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self): 545 """ 546 Check that resolve_partial provides a nice message when a template is not found. 547 548 """ 549 renderer = Renderer() 550 renderer.missing_tags = 'strict' 551 renderer.partials = {} 552 553 engine = renderer._make_render_engine() 554 resolve_partial = engine.resolve_partial 555 556 # Include dict directly since str(dict) is different in Python 2 and 3: 557 # <type 'dict'> versus <class 'dict'>, respectively. 558 self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict, 559 resolve_partial, "foo") 560 561 ## Test the engine's literal attribute. 562 563 def test__literal__uses_renderer_unicode(self): 564 """ 565 Test that literal uses the renderer's unicode function. 566 567 """ 568 renderer = self._make_renderer() 569 renderer.str = mock_unicode 570 571 engine = renderer._make_render_engine() 572 literal = engine.literal 573 574 b = "foo".encode("ascii") 575 self.assertEqual(literal(b), "FOO") 576 577 def test__literal__handles_unicode(self): 578 """ 579 Test that literal doesn't try to "double decode" unicode. 580 581 """ 582 renderer = Renderer() 583 renderer.string_encoding = 'ascii' 584 585 engine = renderer._make_render_engine() 586 literal = engine.literal 587 588 self.assertEqual(literal("foo"), "foo") 589 590 def test__literal__returns_unicode(self): 591 """ 592 Test that literal returns unicode (and not a subclass). 593 594 """ 595 renderer = Renderer() 596 renderer.string_encoding = 'ascii' 597 598 engine = renderer._make_render_engine() 599 literal = engine.literal 600 601 self.assertEqual(type(literal("foo")), str) 602 603 class MyUnicode(str): 604 pass 605 606 s = MyUnicode("abc") 607 608 self.assertEqual(type(s), MyUnicode) 609 self.assertTrue(isinstance(s, str)) 610 self.assertEqual(type(literal(s)), str) 611 612 ## Test the engine's escape attribute. 613 614 def test__escape__uses_renderer_escape(self): 615 """ 616 Test that escape uses the renderer's escape function. 617 618 """ 619 renderer = Renderer() 620 renderer.escape = lambda s: "**" + s 621 622 engine = renderer._make_render_engine() 623 escape = engine.escape 624 625 self.assertEqual(escape("foo"), "**foo") 626 627 def test__escape__uses_renderer_unicode(self): 628 """ 629 Test that escape uses the renderer's unicode function. 630 631 """ 632 renderer = Renderer() 633 renderer.str = mock_unicode 634 635 engine = renderer._make_render_engine() 636 escape = engine.escape 637 638 b = "foo".encode('ascii') 639 self.assertEqual(escape(b), "FOO") 640 641 def test__escape__has_access_to_original_unicode_subclass(self): 642 """ 643 Test that escape receives strings with the unicode subclass intact. 644 645 """ 646 renderer = Renderer() 647 renderer.escape = lambda s: str(type(s).__name__) 648 649 engine = renderer._make_render_engine() 650 escape = engine.escape 651 652 class MyUnicode(str): 653 pass 654 655 self.assertEqual(escape("foo".encode('ascii')), str.__name__) 656 self.assertEqual(escape("foo"), str.__name__) 657 self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__) 658 659 def test__escape__returns_unicode(self): 660 """ 661 Test that literal returns unicode (and not a subclass). 662 663 """ 664 renderer = Renderer() 665 renderer.string_encoding = 'ascii' 666 667 engine = renderer._make_render_engine() 668 escape = engine.escape 669 670 self.assertEqual(type(escape("foo")), str) 671 672 # Check that literal doesn't preserve unicode subclasses. 673 class MyUnicode(str): 674 pass 675 676 s = MyUnicode("abc") 677 678 self.assertEqual(type(s), MyUnicode) 679 self.assertTrue(isinstance(s, str)) 680 self.assertEqual(type(escape(s)), str) 681 682 ## Test the missing_tags attribute. 683 684 def test__missing_tags__unknown_value(self): 685 """ 686 Check missing_tags attribute: setting an unknown value. 687 688 """ 689 renderer = Renderer() 690 renderer.missing_tags = 'foo' 691 692 self.assertException(Exception, "Unsupported 'missing_tags' value: 'foo'", 693 renderer._make_render_engine) 694 695 ## Test the engine's resolve_context attribute. 696 697 def test__resolve_context(self): 698 """ 699 Check resolve_context(): default arguments. 700 701 """ 702 renderer = Renderer() 703 704 engine = renderer._make_render_engine() 705 706 stack = ContextStack({'foo': 'bar'}) 707 708 self.assertEqual('bar', engine.resolve_context(stack, 'foo')) 709 self.assertString('', engine.resolve_context(stack, 'missing')) 710 711 def test__resolve_context__missing_tags_strict(self): 712 """ 713 Check resolve_context(): missing_tags 'strict'. 714 715 """ 716 renderer = Renderer() 717 renderer.missing_tags = 'strict' 718 719 engine = renderer._make_render_engine() 720 721 stack = ContextStack({'foo': 'bar'}) 722 723 self.assertEqual('bar', engine.resolve_context(stack, 'foo')) 724 self.assertException(KeyNotFoundError, "Key 'missing' not found: first part", 725 engine.resolve_context, stack, 'missing') 726