Tips and tricks --------------- This module contains some ustache integration tips alongside general Mustache syntax tricks which can be useful for other implementations too (as long they're spec compliant). Cache ===== Ustache implements a default cache storing the 1024 last used template tokenizations to skip that slower parsing step for the common templates, exposed as :py:attr:`ustache.default_cache`. Template pre-caching ~~~~~~~~~~~~~~~~~~~~ As templates are parsed and cached on their first render, that cache can be prepared in advance by simply rendering them with dummy scope. .. code:: python import ustache render('Hello {{.}}', None) Alternatively, consuming the template tokenization will have the same effect while skipping any rendering. .. code:: python import ustache template = 'Hello {{.}}' all(ustache.tokenize(template.encode())) Disable cache ~~~~~~~~~~~~~ Disabling **ustache** template cache, which can be specially useful for one-off templates, can be achieved by providing your own cache mapping. For example, by passing an empty disposable :py:class:`dict`, the compiled template will be discarded during garbage collection. .. code:: python import ustache print(ustache.render('Hello {{.}}', 'world!', cache={})) # Hello world Lambda examples =============== Lambda functions are not always straightforward, especially when requiring context data, as they're unable to directly access the current scope. The standard solution is using the render function, usually multiple times, to retrieve the required data from context, as string, and then performing some parsing. Datetime strformat ~~~~~~~~~~~~~~~~~~ In this example we implement the common-case-scenario of formatting a :py:class:`datetime.datetime` object using a lambda forwarding the custom format to :py:meth:`datetime.datetime.strftime`. .. code:: python import datetime import typing import ustache def strftime(text: str, render: typing.Callable[[str], str]) -> str: """Render date/datetime scope with given strftime format.""" iso = render('{{.}}').replace(' ', 'T') # __str__ is stable try: dt = ( datetime.datetime.fromisoformat(iso) if 'T' in iso else datetime.date.fromisoformat(iso) ) except ValueError: return '' return dt.strftime(render(text)) print( ustache.render( '{{#dt}}{{#_strftime}}%Y.%m.%d{{/_strftime}}{{/dt}}', { 'dt': datetime.datetime.now(), '_strftime': strftime, # prefixed to avoid collisions }, ), ) # 2021.03.25 Virtual properties ================== Sometimes lambda functions are not enough to get the job done, and you might find yourself recursively patching your entire rendering scope with new keys or properties. While in JavaScript would be way easier to temporarily patch the ``Object`` prototype instead, using a similar approach on Python is a very bad idea. To address this issue :py:func:`ustache.default_getter` exposes a `virtuals` argument you can use to include your custom virtual property implementations (just remember to include those already defined by :py:attr:`ustache.default_virtuals` as well). You can either use a custom getter wrapper or :py:func:`functools.partial` to pass a custom :py:func:`ustache.default_getter` including your virtuals to :py:func:`ustache.render`. .. code:: python import functools import ustache def word_count(text): return len(text.split()) print( ustache.render( '{{obj.word_count}} words', {'obj': 'virtual properties are cool'}, getter=functools.partial( ustache.default_getter, virtuals={ **ustache.default_virtuals, 'word_count': word_count, }, ), ), ) # 4 words Please note both :py:class:`AttributeError` and :py:class:`TypeError` exceptions raised from virtual property functions are appropriately handled by :py:func:`ustache.default_getter`. Streaming patterns ================== Streaming is a first-class citizen for **ustache**, enabling powerful patterns which are not only memory-efficient but more responsive than their buffered counterparts, especially over networks. You can easily integrate :py:func:`ustache.stream` on most common scenarios with just a tiny bit of preprocessing, here are some examples. JSON streaming ~~~~~~~~~~~~~~ In this example we stream enveloped JSON by preprocessing that envelope as our template and then serializing every generator item individually (using :py:func:`enumerate` to enable comma insertion logic) while rendering. .. code:: python import json import ustache # Given any row generator (like a database row cursor) rows = ( (i, 'a', 'b', 'c') for i in range(1000, 10000) ) # Serialize envelope with content placeholder placeholder = '%CONTENT%' envelope = json.dumps({'results': [placeholder]}) # Replace envelope placeholder with our mustache template template = envelope.replace( json.dumps(placeholder), '{{#rows}}{{#0}},{{/0}}{{&1}}{{/rows}}', # row mustache template ) # Stream enveloped JSON rows stream = ustache.stream( template, {'rows': enumerate(map(json.dumps, rows))}, ) for chunk in stream: print(chunk) # {"results": [ # [1000, "a", "b", "c"] # , # ... # , # [9999, "a", "b", "c"] # ]}