Package: unplate
unplateA minimal Python templating engine for people who don't like templating engines.What?Think "templating engine", but instead ofinterfacing withPython, Unplate is ratherembedded withinPython.Unplate is on PyPI:python3 -m pip install unplate.Example: Template LiteralThe simplest type of template is atemplate literal. It is denoted withunplate.template(my_template), where the template is written in comments. Interpolation of Python expressions is supported with{{ double braces }}.importunplate# These first few lines are magic that's required for Unplate to workifunplate.true:exec(unplate.compile(__file__),globals(),locals())else:defmake_namecard(name):""" Simple template example. Return an ASCII-art namecard. """greeting=unplate.template(# /------------------------\# |   Hello, my name is:   |# |  {{ name.ljust(20) }}  |# \------------------------/)returngreetingThe above code is functionally equivalent to the following:defmake_namecard(name):greeting=("""/------------------------\\|   Hello, my name is:   ||   """+str(name.ljust(20))+"""   |\\------------------------/""")returngreetingNote that the above code is not exactly what is generated by Unplate, but is a fair replacement for illustrations' sake.Example: Template BuildersTemplates 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<<<.importunplateifunplate.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 inmy_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:Liquid has nonotoperator. 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 theentirecondition---expressing something likeif post.is_published and not post.is_hiddenisn't possible without resorting to tricks like:nestingifandunlesswriting it aspost.is_published and post.is_hidden != trueusing four statements to negate a variable.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 %}.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.Jinja2 doesn't/didn't support a templating interhiting from a template inheriting from a template. Again to their credit, this nowseems to be supported as of 2.11.Recursion in Jinja2 is really awkward(scroll to "It is also possible to use loops recursively").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 toYYYY-MM-DDand 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---alreadyhasall this functionality. Mainstream languages generally have anotoperator, 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 tore-implement this functionality, poorly, in the templating language, when we alreadyhaveit [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 ofseparation 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 stillcanseparate 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 ofexec) in order to work.We'll look at a short example.importunplateifunplate.true:exec(unplate.compile(__file__),globals(),locals()else:# art by Linda Ball from https://www.asciiart.eu/animals/dogsascii_dog=unplate.template(#    __    __# o-''))_____\\# "--__/ * * * )# c_c__/-c____/)The first thing to note is thatunplate.truealways evaluates toTrue. Thus, the code in theelse: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 toexec, which executes it.In this case, the result ofunplate.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.truehas been replaced withFalse. This is to prevent an infinite recursion of calls tounplate.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.