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 = '&#x27;'
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