Skip to main content

A templating language embedded into Python

Project description

unplate

A minimal Python templating engine for people who don't like templating engines.

What?

Think "templating engine", but instead of interfacing with Python, Unplate is rather embedded within Python.

Unplate is on PyPI: python3 -m pip install unplate.

Example: Template Literal

The simplest type of template is a template literal. It is denoted with unplate.template(my_template), where the template is written in comments. Interpolation of Python expressions is supported with {{ double braces }}.

import unplate

# These first few lines are magic that's required for Unplate to work
if unplate.true:
  exec(unplate.compile(__file__), globals(), locals())
else:

  def make_namecard(name):
    """ Simple template example. Return an ASCII-art namecard. """
    greeting = unplate.template(
      # /------------------------\
      # |   Hello, my name is:   |
      # |  {{ name.ljust(20) }}  |
      # \------------------------/
    )
    return greeting

The above code is functionally equivalent to the following:

def make_namecard(name):
  greeting = (
"""/------------------------\\
|   Hello, my name is:   |
|   """ + str(name.ljust(20)) + """   |
\\------------------------/
""")
  return greeting

Note that the above code is not exactly what is generated by Unplate, but is a fair replacement for illustrations' sake.

Example: Template Builders

Templates may also contain logic, e.g. for-loops. These are supported by "template builders" which are opened with [unplate.begin(template_name)] and closed with [unplate.end]. Python statements can interpolated into template builders by beginning the line with >>>. Dedentation must be explicitly denoted, using <<<.

import unplate
if unplate.true:
  exec(unplate.compile(__file__), globals(), locals())
else:

  [unplate.begin(my_template)]
  # One line
  # Two line
  # >>> for color in ['red', 'blue']:
    # >>> capitalized = color.capitalize()
    # {{ capitalized }} line
  # <<<
  [unplate.end]

gives the following result in my_template:

One line
Two line
Red line
Blue line

(Note the trailing newline)

Why?

Essentially, because I got frustrated.

Across the board, my experiences with templating engines has been the same. They look fantastic at the beginning, but always turn out to have pretty severely limited functionality or questionable design decisions.

For instance:

  1. Liquid has no not operator. What the hell? So you can't say {% if not post.is_hidden %}. You can instead say {% unless post.is_hidden %}, but that only works if you're trying to negate the entire condition---expressing something like if post.is_published and not post.is_hidden isn't possible without resorting to tricks like:
  2. On a similar line, Liquid has no support for nesting experessions with parentheses: "You cannot change the order of operations using parentheses — parentheses are invalid characters in Liquid [...]". You can't write {% if (A or B) and C %}.
  3. By default, Jinja2 templates fail silently on an undefined variable: "If a variable or attribute does not exist [...] the default behavior is to evaluate to an empty string if printed or iterated over, and to fail for every other operation". I actually understand this choice, but only for use in production (generally better to serve something incomplete than nothing at all). For development, though, it makes no sense. To their credit, the behavior can be changed.
  4. Jinja2 doesn't/didn't support a templating interhiting from a template inheriting from a template. Again to their credit, this now seems to be supported as of 2.11.
  5. Recursion in Jinja2 is really awkward (scroll to "It is also possible to use loops recursively").
  6. I once was building a blog rendered with Liquid and wanted to compare the dates of two posts. However, the date objects themselves could not be compared. I was able to do it by formatting both dates to YYYY-MM-DD and using lexicographical string comparison on those results. It worked, but it was ugly, and ridiculous that I had to go through such a hoop in order to tell if one date came after another or not.

So, in my experience, templating engines tend to have limited functionality and doing anything even kinda difficult requires weird tricks. But usually the host language---the programming language rendering the template---already has all this functionality. Mainstream languages generally have a not operator, yell at you upon referencing an undefined variable, support clean recursion, and can compare dates! So why, I figured, are we doing all this work to re-implement this functionality, poorly, in the templating language, when we already have it [1]?

This is the idea of Unplate. Instead of separating ourselves from the host language, integrate into it. This way, we get all the functionality of that language for free. No need to reimplement anything.

I'd also like to note that although I'm picking on Liquid and Jinja2, these issues don't seem to be limited to just these two engines. I spent quite a while looking for a suitable templating engine to use for my personal website, rejecting engine after engine for problems similar to these, before settling on Jinja2 as "least bad". Unplate is my attempt to create a genuine solution.

[1]: The answer, I think, is actually simple: what we gain is agnosticism to the host language; Jinja2 and Liquid can both, in theory, be rendered via multiple different programming languages. This is the main disadvantage of using Unplate---it's Python-only---but one that I posit is not actually much of a problem in most situations.

What about separation of data and display?

Surely the situation isn't this way for no reason. I think probably the reason that templating engines do this---separate themselves so much from the host language---is in the name of separation of data and display: you should process your data in one area, and display it in another. This is perfectly reasonable, and valid!

However, need it be so painful?

I say no! With Unplate, you still can separate data and display: just keep your template logic to a minimum. Unplate does make it easier to break this rule, but it's still possible---and encouraged---to follow it.

How?

Long story short, being really naughty. Unplate abuses the extremely dynamic nature of Python (in particular, the existence of exec) in order to work.

We'll look at a short example.

import unplate
if unplate.true:
  exec(unplate.compile(__file__), globals(), locals()
else:
  # art by Linda Ball from https://www.asciiart.eu/animals/dogs
  ascii_dog = unplate.template(
    #    __    __
    # o-''))_____\\
    # "--__/ * * * )
    # c_c__/-c____/ 
  )

The first thing to note is that unplate.true always evaluates to True. Thus, the code in the else: block never gets executed.

Instead, unplate.compile(__file__) reads your source code, turns all the Unplate comments into functional Python code, and returns this Python code. This is then passed to exec, which executes it.

In this case, the result of unplate.compile(__file__) will look something like this:

import unplate
if False:
  exec(unplate.compile(__file__), globals(), locals()
else:
  # art by Linda Ball from https://www.asciiart.eu/animals/dogs
  ascii_dog = (
"""   __    __
o-''))_____\\
"--__/ * * * )
c_c__/-c____/ 
""")

Two changes have been made. Firstly, unplate.true has been replaced with False. This is to prevent an infinite recursion of calls to unplate.compile. Secondly, the template has been transformed into a native Python multiline string. This code is now executed, doing what you wanted---making an ascii dog!

This, in short, is how Unplate works. Template builders are, of course, somewhat more complex---but they rely on the same principles.

Unplate attempts to preserve line numbers---this is why the string literal is surrounded by some awkward parentheses---but column numbers for code within templates is not necessarily preserved.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for unplate, version 0.2.8
Filename, size File type Python version Upload date Hashes
Filename, size unplate-0.2.8-py3-none-any.whl (14.0 kB) File type Wheel Python version py3 Upload date Hashes View
Filename, size unplate-0.2.8.tar.gz (11.8 kB) File type Source Python version None Upload date Hashes View

Supported by

Pingdom Pingdom Monitoring Google Google Object Storage and Download Analytics Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page