clabate 0.5.0
Minimalistic class-based templates
Requires Python: >=3.6

=======================================================
Clabate: minimalistic class-based templates for Python.
=======================================================

Clabate is a minimalistic template system for Python language.

Clabate does not offer yet another mini-language for templates.
It is based on class inheritance and `PEP 3101 <https://www.python.org/dev/peps/pep-3101>`_
string formatting.

The basic idea is simple: declare template strings as class attributes
and render them in the right order in some container called ``context``.

Subclasses may re-define template strings and define new ones.

Dynamic content can be generated using class properties and values
passed as *kwargs* to the constructor and/or ``render`` method.

In the very basic layer Clabate implements bare textual templates
which can be used, for example, to generate configuration files.

And that basic layer is extended by ``MarkupTemplate``, which escapes
everything by default, trying to minimize chances to overlook
unescaped substitutions.

========
Examples
========

.. code:: python

    from clabate import MarkupTemplate, Markup
    from datetime import datetime

    class HtmlPage(MarkupTemplate):
        html = Markup('''
            <html>
            <head>
                <title>{title}</title>
            </head>
            <body>
                <header>
                    {header}
                </header>
                <main>
                    {main}
                </main>
                <footer>
                    {footer}
                </footer>
            </body>
            </html>
        ''')

    class MyPage(HtmlPage):
        title = 'My web page'
        header = 'Today is {now:%Y-%m-%d}'
        main = '<<<Hello, world!>>>'
        footer = Markup('<span style="color:grey">Here we go!</span>')

        @property
        def now(self, context):
            return datetime.now()

    my_page = MyPage()
    context = my_page.render()
    print(context.html)

Output:

.. code:: html

    <html>
    <head>
        <title>My web page</title>
    </head>
    <body>
        <header>
            Today is 2022-12-09
        </header>
        <main>
            &lt;&lt;&lt;Hello, world!&gt;&gt;&gt;
        </main>
        <footer>
            <span style="color:grey">Here we go!</span>
        </footer>
    </body>
    </html>

How escaping works, an example from
`markup_examples.py <https://declassed.art/repository/clabate/file/tip/clabate/examples/markup_examples.py>`_:

.. code:: python

    from types import SimpleNamespace
    from clabate import MarkupTemplate, Markup

    class MarkupExample(MarkupTemplate):
        '''
        All values are escaped unless they are Markup.
        This applies to everything: complex data, properties, kwargs, and missing values.
        '''
        attr = 'String attributes are <strong>escaped</strong>'
        markup_attr = Markup('Markup attributes are <strong>unchanged</strong>')
        snippet1 = Markup('''
            {attr}
            {markup_attr}
            {prop}
            {markup_prop}
            {constructor_kwarg}
            {markup_constructor_kwarg}
            {render_kwarg}
            {markup_render_kwarg}
            What is <a.b[name].c[1].d>? It's {a.b[name].c[1].d}!
            What is <a.b[name].c[1].e>? It's {a.b[name].c[1].e} and it is escaped!
            And what is foo.bar? {foo.bar}
            Wait, what is bar[foo][1]? {bar[foo][1]}
        ''')

        a = SimpleNamespace(
            b = dict(
                name = SimpleNamespace(
                    c = [
                        0,
                        SimpleNamespace(
                            d = Markup('<span style="color:green">wow</span>'),
                            e = '<span style="color:green">wow</span>'
                        )
                    ]
                )
            )
        )

        @property
        def prop(self, context):
            return 'Property values are <strong>escaped</strong>'

        @property
        def markup_prop(self, context):
            return Markup('Markup property values are <strong>unchanged</strong>')

        def missing(self, name):
            return Markup(f'<span style="color:red">{name}</span> is missing from the rendering context!')

        # And values are escaped only once!
        snippet2 = Markup('''
            Values are escaped only once:
            {snippet1}
            Otherwise we'd get lots of &amp; instead of bare &
        ''')

    # Render MarkupExample
    template = MarkupExample(
        constructor_kwarg='Constructor kwargs are <strong>escaped</strong>',
        markup_constructor_kwarg=Markup('Markup constructor kwargs are <strong>unchanged</strong>')
    )
    context = template.render(
        render_kwarg='Render kwargs are <strong>escaped</strong>',
        markup_render_kwarg=Markup('Markup render kwargs are <strong>unchanged</strong>')
    )
    print(context.snippet1)
    print()
    print(context.snippet2)

Output:

.. code:: text

    String attributes are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup attributes are <strong>unchanged</strong>
    Property values are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup property values are <strong>unchanged</strong>
    Constructor kwargs are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup constructor kwargs are <strong>unchanged</strong>
    Render kwargs are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup render kwargs are <strong>unchanged</strong>
    What is <a.b[name].c[1].d>? It's <span style="color:green">wow</span>!
    What is <a.b[name].c[1].e>? It's &lt;span style="color:green"&gt;wow&lt;/span&gt; and it is escaped!
    And what is foo.bar? <span style="color:red">foo</span> is missing from the rendering context!
    Wait, what is bar[foo][1]? <span style="color:red">bar</span> is missing from the rendering context!

    Values are escaped only once:
    String attributes are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup attributes are <strong>unchanged</strong>
    Property values are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup property values are <strong>unchanged</strong>
    Constructor kwargs are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup constructor kwargs are <strong>unchanged</strong>
    Render kwargs are &lt;strong&gt;escaped&lt;/strong&gt;
    Markup render kwargs are <strong>unchanged</strong>
    What is <a.b[name].c[1].d>? It's <span style="color:green">wow</span>!
    What is <a.b[name].c[1].e>? It's &lt;span style="color:green"&gt;wow&lt;/span&gt; and it is escaped!
    And what is foo.bar? <span style="color:red">foo</span> is missing from the rendering context!
    Wait, what is bar[foo][1]? <span style="color:red">bar</span> is missing from the rendering context!
    Otherwise we'd get lots of &amp; instead of bare &

Plain text example:

.. code:: python

    from clabate import Template
    import time
    from types import SimpleNamespace

    class ZoneFileBoilerplate(Template):

        zone_config = '''
            $TTL    3600
            @   IN  SOA (
                        {primary_ns.hostname}.{idna_domain}.  ; MNAME
                        {rname}  ; RNAME
                        {timestamp}  ; SERIAL
                        3600  ; REFRESH
                        60    ; RETRY
                        1W    ; EXPIRY
                        60    ; MINIMUM Negative Cache TTL
                        )
            {nameservers}
            {resource_records}
        '''

        @property
        def timestamp(self, context):
            return int(time.time())

        @property
        def nameservers(self, context):
            ns_template = self.dedent('''
                @  IN  NS  {ns.hostname}.{idna_domain}.
                {ns.hostname}  IN  A  {ns.ipv4_addr}
            ''')
            result = []
            for ns in [self.primary_ns, self.secondary_ns]:
                result.append(self.render_str(context, ns_template, ns=ns))
            return ''.join(result)

        resource_records = '''
            @  IN  A   {main_server_ipv4}
            *  IN  A   {main_server_ipv4}
        '''

    class DeclassedZone(ZoneFileBoilerplate):

        primary_ns = SimpleNamespace(hostname='ns1', ipv4_addr='1.2.3.4')
        secondary_ns = SimpleNamespace(hostname='ns2', ipv4_addr='5.6.7.8')


    my_zone = DeclassedZone(idna_domain='declassed.art', rname='axy.{idna_domain}.')
    context = my_zone.render(main_server_ipv4='9.10.11.12')
    print(context.zone_config)

Output:

.. code::

    $TTL    3600
    @   IN  SOA (
                ns1.declassed.art.  ; MNAME
                axy.declassed.art.  ; RNAME
                1656230266  ; SERIAL
                3600  ; REFRESH
                60    ; RETRY
                1W    ; EXPIRY
                60    ; MINIMUM Negative Cache TTL
                )
    @  IN  NS  ns1.declassed.art.
    ns1  IN  A  1.2.3.4
    @  IN  NS  ns2.declassed.art.
    ns2  IN  A  5.6.7.8
    @  IN  A   9.10.11.12
    *  IN  A   9.10.11.12

There are more examples in `clabate/examples <https://declassed.art/repository/clabate/file/tip/clabate/examples>`_:

* `Basic examples <https://declassed.art/repository/clabate/file/tip/clabate/examples/basic_examples.py>`_
* `Markup examples <https://declassed.art/repository/clabate/file/tip/clabate/examples/markup_examples.py>`_
* `Invoke <https://declassed.art/repository/clabate/file/tip/clabate/examples/invoke_examples.py>`_
* `Pygments <https://declassed.art/repository/clabate/file/tip/clabate/examples/pygments_examples.py>`_
* `Comments <https://declassed.art/repository/clabate/file/tip/clabate/examples/comments_examples.py>`_
* `Sequences <https://declassed.art/repository/clabate/file/tip/clabate/examples/sequence_examples.py>`_
* `File inclusion <https://declassed.art/repository/clabate/file/tip/clabate/examples/file_inclusion_examples.py>`_

See also: https://declassed.art/en/blog/2022/06/29/clabate-class-based-templates