Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: benhoyt/symplate
base: 2f894110b6
...
head fork: benhoyt/symplate
compare: 6f0dd7b971
Checking mergeability… Don't worry, you can still create the pull request.
  • 5 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
View
225 README.md
@@ -1,21 +1,37 @@
Symplate, the Simple pYthon teMPLATE renderer
=============================================
-Symplate is one of the simplest and fastest Python templating languages.
+Symplate is a very simple and very fast Python template language.
+
+* [Background](#background)
+* [Who uses Symplate?](#who-uses-symplate)
+* [Why use Symplate?](#why-use-symplate)
+* [Isn't worrying about performance silly?](#isnt-worrying-about-performance-silly)
+* [Basic usage](#basic-usage)
+* [Compiled Python output](#compiled-python-output)
+* [Directives](#directives)
+* [Filters](#filters)
+* [Including sub-templates](#including-sub-templates)
+* [Customizing the Renderer](#customizing-the-renderer)
+* [Unicode handling](#unicode-handling)
+* [Comments](#comments)
+* [Outputting a literal {{, }}, {%, or %}](#outputting-a-literal----or-)
+* [Command line usage](#command-line-usage)
+* [Hats off to bottle.py](#hats-off-to-bottlepy)
+* [TODO](#todo)
+* [Flames, comments, bug reports](#flames-comments-bug-reports)
Background
----------
-[*Skip the background, show me an example!*](#basic-usage)
-
When I got frustrated with the complexities and slow rendering speed of
[Cheetah](http://www.cheetahtemplate.org/), I started wondering just how
-simple a templating language could be.
+simple a template language could be.
-It's somewhat painful to write templates in pure Python -- code and text are
-hard to intersperse, and you don't get auto-escaping. But why not a KISS
-template-to-Python translator? Enter Symplate:
+You could write templates in pure Python, but that's somewhat painful -- code
+and text are hard to intersperse, and you don't get auto-escaping. But why not
+a KISS template-to-Python translator? Enter Symplate:
* `text` becomes `_write('text')`
* `{{ expr }}` becomes `_write(filt(expr))`
@@ -24,14 +40,14 @@ template-to-Python translator? Enter Symplate:
`{% for x in lst: %}`
* indentation decreases when you say `{% end %}`
-That's about all there is to it. All the rest is detail.
+That's about all there is to it. All the rest is [detail](#directives).
Who uses Symplate?
------------------
-Only me ... so far. It's my experiment. But there's no reason you can't: it's
-a proper library, and fairly well tested. I "ported" my
+Only me ... so far. It started as my experiment. That said, Symplate is now a
+proper library, and fairly well tested. I also "ported" my
[GiftyWeddings.com](http://giftyweddings.com/) website from Cheetah to
Symplate, and it's working very well.
@@ -39,17 +55,17 @@ Symplate, and it's working very well.
Why use Symplate?
-----------------
-Well, if you care about **raw performance** or **simplicity of
-implementation**, Symplate might be for you. I care about both, and I haven't
-needed some of the extra features other systems provide, such as sandboxed
-execution. If you want a Porshe, use Symplate. If you'd prefer a Volvo or BMW,
-I'd recommend [Jinja2](http://jinja.pocoo.org/docs/) or
+If you care about **raw performance** or **simplicity of implementation**,
+Symplate might be for you. I care about both, and I haven't needed some of the
+extra features other systems provide, such as sandboxed execution and template
+inheritance. If you want a Porsche, use Symplate. If you'd prefer a Volvo or
+BMW, I'd recommend [Jinja2](http://jinja.pocoo.org/docs/) or
[Mako](http://www.makotemplates.org/).
Symplate is dead simple: a couple of pages of code translate your templates to
-Python `.py` files, and `render()` imports and executes those.
+Python `.py` files, and `render()` imports and executes the compiled output.
-Symplate's also about as fast as a pure-Python templating language can be.
+Symplate's also about as fast as a pure-Python template language can be.
Partly *because* it's simple, it produces Python code as tight as you'd write
it by hand.
@@ -57,40 +73,40 @@ it by hand.
Isn't worrying about performance silly?
---------------------------------------
-Yes, [worrying about the performance of your template engine is
-silly](http://www.codeirony.com/?p=9). Well, sometimes. But when you're doing
-zero database requests and your "business logic" is pretty tight, template
-rendering is all that's left. And Cheetah (not to mention Django!) are
-particlarly slow.
+Yes, I know, [worrying about template performance is
+silly](http://www.codeirony.com/?p=9). *Some of the time.* But when you're
+caching everything to avoid database requests, and your "business logic" is
+pretty tight, template rendering is all that's left. And Cheetah (not to
+mention Django!) are particlarly slow.
If you're running a large-scale website and you're caching things so that
-template rendering *is* your bottleneck ... then if you can take your
-rendering time down from 100ms to 20ms, you can run your website on 1/5th the
-number of servers.
+template rendering *is* your bottleneck (yes, I've been there) ... then if you
+can take your render times down from 100ms to 20ms, you can run your website
+on 1/5th the number of servers.
-So how fast is Symplate? About as fast as you can hand-code Python. Here's the
-Symplate benchmark showing compile and render times for some of the fast or
-popular template languages.
+So how fast is Symplate? As mentioned, it's about as fast as you can hand-code
+Python. Here's the Symplate benchmark showing compile and render times for
+some of the fast or popular template languages.
-Times are normalized to the HandCoded render time (TODO):
+Times are normalized to the HandCoded render time:
- Engine compile (ms) render (ms)
- -----------------------------------
- HandCoded 0.000 0.107
- Symplate 1.385 0.120
- Wheezy 3.214 0.145
- Bottle 1.093 0.277
- Mako 6.567 0.415
- Jinja2 7.149 0.590
- Cheetah 13.299 0.644
- Django 0.839 2.451
+ engine compile render
+ ---------------------------
+ HandCoded 0.001 1.000
+ Symplate 12.622 1.153
+ Wheezy 30.158 1.387
+ Bottle 10.409 2.595
+ Mako 61.721 3.899
+ Jinja2 68.233 5.612
+ Cheetah 123.779 6.118
+ Django 7.924 22.899
Basic usage
-----------
Let's start with a simple example that uses more or less all the features of
-Symplate. Our main template is `blog.symp`:
+Symplate. Our main template file is `blog.symp`:
{% template entries, title='My Blog' %}
{{ !render('inc/header', title) }}
@@ -102,20 +118,17 @@ Symplate. Our main template is `blog.symp`:
</ul>
{{ !render('inc/footer') }}
-In Python fashion, everything's explicit. We explicitly specify the parameters
-this template takes in the `{% template ... %}` line, including the default
-parameter `title`.
+In true Python style, everything's explicit. We explicitly specify the
+parameters the template takes in the `{% template ... %}` line, including the
+default parameter `title`.
-For simplicity, there's no special "include" directive -- you just `render()`
-a sub-template -- usually with the `!` prefix to mean don't filter the
-rendered output. The arguments passed to `render()`ed sub-templates are
-specified explicitly, so there's no yucky setting of globals when rendering
-included templates. (Note: `render` is set to the current Renderer instance's
-`render` function.)
+For simplicity, there's no special "include" directive -- you just [`render()`
+a sub-template](#including-sub-templates) -- usually with the `!` prefix to
+mean don't filter the rendered output.
In this example, `entry.html_body` contains pre-rendered HTML, so this
-expression is also prefixed with `!` -- it will output the HTML body as a raw,
-unescaped string.
+expression is also prefixed with `!` -- it will output the HTML body as a
+[raw, unescaped string](#outputting-raw-strings).
Then `inc/header.symp` looks like this:
@@ -138,35 +151,38 @@ to a list of blog entries with the `url`, `title`, and `html_body` attributes,
and you're away:
renderer = symplate.Renderer(template_dir)
- output = renderer.render('blog', entries, title="Ben's Blog")
+
+ def homepage():
+ return renderer.render('blog', entries, title="Ben's Blog")
You can customize the Renderer to specify a different output directory, or to
turn on checking of template file mtimes for debugging. For example:
renderer = symplate.Renderer(template_dir, output_dir='out',
- check_mtime=DEBUG)
+ check_mtime=settings.DEBUG)
def homepage():
entries = load_blog_entries()
return renderer.render('blog', entries, title="Ben's Blog")
-See `Renderer.__init__`'s docstring or type `help(symplate.Renderer)` at a
-Python prompt for docs on the exact arguments for `Renderer()`.
-
Compiled Python output
----------------------
-Symplate is a [leaky abstraction](http://www.joelonsoftware.com/articles/LeakyAbstractions.html),
+Symplate is a [leaky
+abstraction](http://www.joelonsoftware.com/articles/LeakyAbstractions.html),
but is somewhat proud of that fact. I already knew Python well, so my goal was
to be as close to Python as possible -- I don't want to learn another language
-just to produce some HTML.
+just to produce some escaped HTML.
In any case, you're encouraged to look at the compiled Python output produced
-by the Symplate compiler. You might be surprised how clean it looks. Symplate
-tries to make the compiled template look much like it would if you were
-writing it by hand -- for example, short strings are output as `'shortstr'`,
-and long, multi-line strings as `"""long, multi-line strings"""`.
+by the Symplate compiler (usually placed in a `symplouts` directory at the
+same level as your template directory).
+
+You might be surprised how simple the compiled output is. Symplate tries to
+make the compiled template look much like it would if you were writing it by
+hand -- for example, short strings are output as `'shortstr'`, and long,
+multi-line strings as `"""long, multi-line strings"""`.
The `blog.symp` example above produces this in `blog.py`:
@@ -203,13 +219,16 @@ The `blog.symp` example above produces this in `blog.py`:
return u''.join(_output)
As you can see, apart from a tiny premable, it's about as fast and direct as
-it could possibly be (in pure Python).
+it could possibly be in pure Python.
Basic Symplate syntax errors like mismatched `{%`'s are raised as
`symplate.Error`s when the template is compiled. However, most Python
-expressions are copied directly to the Python output, so you only get a Python
-SyntaxError when the compiled template is imported at render time. (Yes, this
-is a minor drawback of Symplate's KISS approach.)
+expressions are copied directly to the Python output, so you'll only get a
+Python `SyntaxError` when the compiled template is imported at render time.
+
+(Yes, this is a minor drawback of Symplate's KISS approach. However, because
+Symplate is such a direct mapping to Python, it's usually easy to find errors
+in your templates.)
Directives
@@ -221,11 +240,12 @@ The only directives or keywords in Symplate are `template` and `end`. Oh, and
`{% template [args] %}` must appear at the start of a template before any
output. `args` is the argument specification including positional and
keyword/default arguments, just as if it were a function definition. In fact,
-it is -- `{% template [args] %}` gets compiled to
+it is -- `{% template [args] %}` gets compiled to
`def render(_renderer, args): ...`.
If you need to import other modules, do so at the top of your template, above
-the `template` directive (just like in Python you import before writing code).
+the `template` directive (just like how in Python you import before writing
+code).
`{% end [...] %}` ends a code indentation block. All it does is reduce the
indentation level in the compiled Python output. The `...` is optional, and
@@ -309,6 +329,39 @@ Note the modified `premable` so the compiled template has the `json` module
available.
+Including sub-templates
+-----------------------
+
+As mentioned above, there's no literal "include" directive. You simply call
+`render` in an output expression, like this:
+
+ {{ !render('sub_template_name', *args, **kwargs) }}
+
+`render` inside templates is set to the current Renderer instance's `render`
+function, so it uses the settings you expect. Note that you almost always use
+the `!` raw-output prefix, so that the rendered sub-template isn't
+HTML-escaped further.
+
+The arguments passed to `render()`ed sub-templates are specified explicitly,
+so there's no yucky setting of globals when rendering included templates.
+
+Symplate doesn't currently support template inheritance -- it prefers
+"composition over inheritance", if you will. For instance, if your header
+template has an ad in its sidebar that can vary by page, you could say:
+
+ {{ !render('header', title='My Page', ad_html=render('ad1')) }}
+
+When `check_mtimes` is off (the default), calling `render()` is super-fast,
+and after the first time when the module is imported, it basically amounts to
+a couple of dict lookups.
+
+
+Customizing the Renderer
+------------------------
+
+TODO
+
+
Unicode handling
----------------
@@ -337,7 +390,7 @@ One quirk is that Symplate determines when to indent the Python output based
on the `:` character being at the end of the line, so you can't add a comment
after the colon that begins an indentation block:
- {% for element in lst: # THIS WON'T WORK %}
+ {% for element in lst: # DON'T DO THIS %}
Outputting a literal {{, }}, {%, or %}
@@ -369,12 +422,26 @@ useful for pre-compiling one or more templates, which might be useful in a
constrained deployment environment where you can only upload Python code, and
not write to the file system.
-Simply specify your template directory and output directory and it'll compile
-all your templates to Python code. Straight from the command line help:
+Simply specify arguments as per your `Renderer`, and it'll compile all your
+templates to Python code. Quoting from the command line help:
+
+ Usage: symplate.py [-h] [options] template_dir [template_names]
- Usage: symplate.py [-h] [options] action [name|dir|glob]
+ Compile templates in specified template_dir, or all templates if
+ template_names not given
- TODO
+ Options:
+ --version show program's version number and exit
+ -h, --help show this help message and exit
+ -o OUTPUT_DIR, --output-dir=OUTPUT_DIR
+ compiled template output directory, default
+ {template_dir}/../symplouts
+ -e EXTENSION, --extension=EXTENSION
+ file extension for templates, default .symp
+ -p PREAMBLE, --preamble=PREAMBLE
+ template preamble (see docs), default ""
+ -q, --quiet don't print what we're doing
+ -n, --non-recursive don't recurse into subdirectories
Hats off to bottle.py
@@ -391,6 +458,18 @@ denote raw output. It seemed cleaner than my initial idea of passing
`raw=True` as a parameter to the filter, as in `{{ foo, raw=True }}`.
+TODO
+----
+
+Some things I'd like to do or look into when I get a chance:
+
+* Add Python 3 support. Shouldn't be hard, especially if we only care about
+ Python 2.6+.
+* Can we get original line numbers by outputting `# line: N` comments and then
+ reading those when an error occurs?
+* Investigate template inheritance, perhaps in the style of bottle.py.
+
+
Flames, comments, bug reports
-----------------------------
View
3  TODO.md
@@ -1,10 +1,7 @@
* ensure there's a simple way to get it to recompile all templates once on startup (or the first time they're used)
- perhaps just a force=False param to _get_module()
* are there tests for check_mtime stuff? and the above?
-* can we get original line numbers by outputting "# line: N" comments and then reading them?
* add setup.py and make into proper package
- http://docs.python.org/distutils/index.html
- http://guide.python-distribute.org/index.html
- add LICENSE.txt, CHANGES.txt
-* investigate inheritance in style of bottle.py?
-* Python 3 support?
View
24 benchmarks/run_benchmarks.py
@@ -17,7 +17,7 @@
BlogEntry(u'My life & story', None, u'<p>Once upon a time...</p>'),
BlogEntry(u'First \u201cpost\u201d', u'/first-post/', u'<p>This is the first post.</p>'),
]
-ENTRIES *= 10 # To give the render test a bit more to chew on
+ENTRIES *= 10 # to give the render test a bit more to chew on
def rel_dir(dirname):
@@ -42,12 +42,11 @@ def render(self):
raise NotImplementedError
def benchmark(self):
- timings = {}
self.setup_compile()
- timings['compile'] = min(timeit.repeat(self.compile, number=self.num_compiles)) / float(self.num_compiles)
+ compile_time = min(timeit.repeat(self.compile, number=self.num_compiles)) / float(self.num_compiles)
self.setup_render()
- timings['render'] = min(timeit.repeat(self.render, number=self.num_renders)) / float(self.num_renders)
- return timings
+ render_time = min(timeit.repeat(self.render, number=self.num_renders)) / float(self.num_renders)
+ return (compile_time, render_time)
try:
@@ -290,12 +289,11 @@ def main():
issubclass(cls, TemplateLanguage) and
cls is not TemplateLanguage]
- results = []
+ results = {}
output = None
for name, cls in language_classes:
language = cls()
- timings = language.benchmark()
- results.append((name, timings['compile'], timings['render']))
+ results[name] = language.benchmark()
output_dir = rel_dir('output')
if not os.path.exists(output_dir):
@@ -309,10 +307,12 @@ def main():
elif output[1] != rendering.strip():
print 'ERROR: output from %s and %s differ' % (name, output[0])
- print 'Engine compile (ms) render (ms)'
- print '-----------------------------------'
- for name, compile_time, render_time in sorted(results, key=lambda r: r[2]):
- print '%-10s %11.3f %12.3f' % (name, compile_time * 1000, render_time * 1000)
+ # show compiler and render times (normalized to HandCoded render time)
+ norm_time = results['HandCoded'][1]
+ print 'engine compile render'
+ print '---------------------------'
+ for name, timings in sorted(results.items(), key=lambda r: r[1][1]):
+ print '%-11s %7.3f %7.3f' % (name, timings[0] / norm_time, timings[1] / norm_time)
if __name__ == '__main__':
View
2  examples/blog.py
@@ -12,7 +12,7 @@
renderer = symplate.Renderer(
os.path.join(os.path.dirname(__file__), 'symplates'),
output_dir=os.path.join(os.path.dirname(__file__), 'symplouts'),
- check_mtime=True)
+ check_mtimes=True)
def main():
entries = [
View
10 symplate.py
@@ -56,7 +56,7 @@ class Renderer(object):
"""Symplate renderer class. See __init__'s docs for more info."""
def __init__(self, template_dir, output_dir=None, extension='.symp',
- check_mtime=False, modify_path=True, preamble='',
+ check_mtimes=False, modify_path=True, preamble='',
default_filter='symplate.html_filter'):
"""Initialize a Renderer instance.
@@ -66,7 +66,7 @@ def __init__(self, template_dir, output_dir=None, extension='.symp',
into, default is {template_dir}/../symplouts
* extension: file extension for templates (set to '' if you want
to specify explictly when calling render)
- * check_mtime: True means check template file's mtime on render(),
+ * check_mtimes: True means check template file's mtime on render(),
which is slower and usually only used for debugging
* modify_path: True means add output_dir/.. to sys.path for
importing compiled template
@@ -84,7 +84,7 @@ def __init__(self, template_dir, output_dir=None, extension='.symp',
'symplouts'))
self.output_dir = output_dir
self.extension = extension
- self.check_mtime = check_mtime
+ self.check_mtimes = check_mtimes
self.preamble = preamble
self.default_filter = default_filter
@@ -359,7 +359,7 @@ def compile_all(self, recursive=True, verbose=False):
def _get_module(self, name):
"""Import or compile and import named template and return module."""
names = self._get_filenames(name)
- if self.check_mtime:
+ if self.check_mtimes:
# compile the template source to .py if it has changed
try:
py_mtime = os.path.getmtime(names['py'])
@@ -390,7 +390,7 @@ def render(self, _name, *args, **kwargs):
else:
module = self._get_module(_name)
# only store in module cache if we're not checking mtimes
- if not self.check_mtime:
+ if not self.check_mtimes:
self._module_cache[_name] = module
return module._render(self, *args, **kwargs)
View
10 tests/test_renderer.py
@@ -25,17 +25,17 @@ def test_extension(self):
renderer = utils.Renderer(extension='.symp2')
self.assertEquals(self.render('{% template %}te', _renderer=renderer), 'te')
- def test_check_mtime_true(self):
- renderer = utils.Renderer(check_mtime=True)
+ def test_check_mtimes_true(self):
+ renderer = utils.Renderer(check_mtimes=True)
self.assertEquals(self.render('{% template %}cmt1', _renderer=renderer), 'cmt1')
time.sleep(0.02)
self.assertEquals(self.render('{% template %}cmt2', _renderer=renderer, _increment=0), 'cmt2')
- def test_check_mtime_false(self):
- renderer = utils.Renderer(check_mtime=True)
+ def test_check_mtimes_false(self):
+ renderer = utils.Renderer(check_mtimes=True)
self.assertEquals(self.render('{% template %}cmf1', _renderer=renderer), 'cmf1')
time.sleep(0.02)
- renderer = utils.Renderer(check_mtime=False)
+ renderer = utils.Renderer(check_mtimes=False)
self.assertEquals(self.render('{% template %}cmf2', _renderer=renderer, _increment=0), 'cmf1')
def test_modify_path(self):
View
2  tests/utils.py
@@ -15,7 +15,7 @@ class Renderer(symplate.Renderer):
def __init__(self, **kwargs):
template_dir = kwargs.pop('template_dir', TEMPLATE_DIR)
kwargs.setdefault('output_dir', OUTPUT_DIR)
- kwargs.setdefault('check_mtime', True)
+ kwargs.setdefault('check_mtimes', True)
super(Renderer, self).__init__(template_dir, **kwargs)
renderer = Renderer()

No commit comments for this range

Something went wrong with that request. Please try again.