Quantcast
Channel: Planet Plone - Where Developers And Integrators Write
Viewing all articles
Browse latest Browse all 3535

Makina Corpus: The world's simplest Python template engine

$
0
0

A template engine is a component able to render some data using a given string template.

We use it extensively in web development (that's not surprising because web development is mainly about reading some data and render them as strings, that's what I say when someone ask me about my job).

So there are already a lot of Python based template engines (Jinja2, Mako, Chameleon, ...).

The purpose of this article is to build a very basic template engine based only on the Python standard string class formatting capacities.

You probably know...

You probably know .format() replaces the old "%" based formatting solution:

>>> "My name is %s and I am %d" % ("Eric", 41)
'My name is Eric and I am 41'
>>> "My name is {name} and I am {age}".format(name="Eric", age=41)
'My name is Eric and I am 41'

It allows to perform all the typical formatting features, for instance:

>>> '{:^20}'.format('centered')
' centered '
>>> '{:^20}'.format('align right')
' align right '
>>> '{:>20}'.format('align right')
' align right'
>>> '{:<20}'.format('align left')
'align left '

and many other things (have a look to https://pyformat.info/).

Did you know?

format() is also able to access attributes or items of the parameters.

For instance with a dictionary, we can do this:

>>> 'Name: {person[name]}, age: {person[age]}'.format(person={'name': 'Eric', 'age': 41})
'Name: Eric, age: 41'

And the same goes with attributes:

>>> class Song(object):
... title = 'Where is my mind'
...
>>> 'My favorite song is: {song.title}'.format(song=Song())
'My favorite song is: Where is my mind'

That's really cool. It starts looking like a template engine, right?

Ok, but few things are missing

What we usually expect from a template engine is to be able to:

  • call methods,
  • make loops over iterables,
  • manage condition.

Let's see how we can handle that.

Calling methods

A method is an attribute of an object, we can call attribute, why couldn't wecall a method? Let's try:

>>> 'My name is {name.upper}'.format(name='eric')
'My name is <built-in method upper of str object at 0x7f67dc8d1630>'

Yeah, not exactly what we expected...

An interesting feature of format() is the format specification: instead of just inserting a field with {field}, we can specify a format like this: {field:spec}.

That's exactly what we do with float for instance:

>>> "{:.3}".format(3.14159)
'3.14'

Well, it is actually very easy to implement our own spec by derivating the Formatter class. So let's implement a ':call' spec in charge of calling the current field:

classSuperFormatter(string.Formatter):defformat_field(self,value,spec):ifspec=='call':returnvalue()else:returnsuper(SuperFormatter,self).format_field(value,spec)

We can use it that way:

>>> sf.format('My name is {name.upper:call}', name="eric")
'My name is ERIC'

Nice!

Loops

Similarly, we can implement a :repeat spec,

classSuperFormatter(string.Formatter):defformat_field(self,value,spec):ifspec.startswith('repeat'):template=spec.partition(':')[-1]iftype(value)isdict:value=value.items()return''.join([template.format(item=item)foriteminvalue])else:returnsuper(SuperFormatter,self).format_field(value,spec)

Here, we pass a parameter to the spec to provide the template to use when we iterate on the loop items. So the resulting format is: <field>:repeat:<template>.

This subtemplate is a regular format() template where we escape the curly brackets by doubling them, and where the only field is the loop variable (named item).

So we can use it like this:

>>> sf.format('''Table of contents:
... {chapters:repeat:Chapter {{item}}
... }''', chapters=["I", "II", "III", "IV"])
'''Table of contents:
Chapter I
Chapter II
Chapter III
Chapter IV
'''

Condition

Let's also implement a :if spec to test the field value, and then display or not the subtemplate:

classSuperFormatter(string.Formatter):defformat_field(self,value,spec):ifspec.startswith('if'):return(valueandspec.partition(':')[-1])or''else:returnsuper(SuperFormatter,self).format_field(value,spec)

At first, it seems stupid, because it looks like it will only be able to conditionnally display static portions, like this:

>>> sf.format('Action: Back / Logout {manager:if:/ Delete}', manager=False)
'Action: Back / Logout '
>>> sf.format('Action: Back / Logout {manager:if:/ Delete}', manager=True)
'Action: Back / Logout / Delete'

What if we want to render conditionnally a portion of template containing fields, like this:

>>> sf.format('Action: Back / Logout {manager:if:/ Delete {id}}', manager=False, id=34)
'Action: Back / Logout '
>>> sf.format('Action: Back / Logout {manager:if:/ Delete {id}}', manager=True, id=34)
'Action: Back / Logout / Delete 34'

Hey that works! That was unexpected. To be honest, I first wrote the test exactly like this, and I expected it to fail, but it was not! Why is that?

Because here the curly brackets are not escaped, so they are processed by the main format() call. So cool!

Here we go

That's it, we have a pretty nice and complete template engine.

The full code of our world's simplest Python template engine is here, the implementation itself is 10 lines long, Python is a really powerful language!

If you have any funny idea to improve it (remember, I want to keep it short), pull requests are welcome.

The objective is mostly to have fun with Python and demonstrate what the standard Formatter class is able to do, but, having a second though, I might intregate it in Rapido :).


Viewing all articles
Browse latest Browse all 3535

Trending Articles