piecutter

piecutter is a template rendering framework, written in Python [1].

Leitmotiv: render templates against data, wherever the templates, whatever the template engine.

Key features

Simple API: render(template, data).

Render files and directories, a.k.a. single templates and collections of templates.

Multiple template engines: Python’s format() [3], Jinja2 [4] and Django [5]... Additional engines such as Cheetah [6] or non-Python template engines such as Ruby’s ERB [7] could be supported.

Extensible template loading: text, bytes, file-like objects, files on local filesystem, remote resources over HTTP, remote resources on github.com... Additional storages could be supported.

Configurable post-processing pipeline: write to local filesystem, generate an archive... It’s easy to create your own.

Dynamic directory generation: generate one template multiple times with different data, exclude some files depending on context, include templates from external locations, use several template engines...

Examples

Hello world!

Let’s generate the traditional “Hello world!”:

>>> import piecutter
>>> template = u'Hello {who}!'  # Text is recognized as a template.
>>> data = {u'who': u'world'}  # Data can be any dictionary-like object.
>>> render = piecutter.Cutter()  # Default engine uses Python's format().
>>> output = render(template, data)  # Default output is a file-like object.
>>> print(output.read())
Hello world!

Note

piecutter.Cutter provides sane defaults. Then every part of the rendering pipeline can be customized in order to fit specific cases.

Load files

Let’s load and render a template located on local filesystem:

>>> location = u'file://demo/simple/hello.txt'
>>> output = render(location, data)
>>> print(output.read())
Hello world!

It works as well with a remote template over HTTP:

>>> location = u'https://raw.github.com/diecutter/piecutter/cutter-api-reloaded/demo/simple/hello.txt'
>>> output = render(location, data)
>>> print(output.read())
Hello world!

Note

piecutter.Cutter‘s default loader detects scheme (file:// and https:// in examples above) then delegates actual loading to specialized loader implementation.

Render directories

Given the following directory:

demo/simple/
├── hello.txt  # Contains "Hello {who}!\n"
└── {who}.txt  # Contains "Whatever the content.\n"

By default, directories are rendered as generator of rendered objects. So can iterate generated items and use their attributes and methods:

>>> for item in render(u'file://demo/simple', data):
...     if isinstance(item, piecutter.RenderedFile):
...         print('File: {}'.format(item.name))
...         print('Path: {}'.format(item.path))
...         print('Content: {}'.format(item.read()))
...     else:  # Is instance of ``piecutter.RenderedDirectory``
...         pass  # We may handle sub-directories recursively here.
File: hello.txt
Path: simple/hello.txt
Content: Hello world!

File: world.txt
Path: simple/world.txt
Content: Whatever the content.

Of course, you may want to write output to disk or to an archive. piecutter provides “writers” for that purpose!

Project status

Yesterday, piecutter was the core of diecutter [2].

As diecutter‘s authors, we think diecutter has great features related to templates and file generation. We wanted to share it with a larger audience. So we just packaged it as a standalone library, and this is piecutter.

In early versions, piecutter was tied to diecutter implementation. The API reflected diecutter‘s architecture and concepts, which may sound obscure for other usage.

Today, piecutter‘s API has been refactored, with simplicity in mind, independantly from diecutter.

Contents

Quickstart tutorial

Let’s discover piecutter with simple stories!

Install piecutter

pip install piecutter

See Install for details and alternatives.

Import piecutter

>>> import piecutter

This single import should be enough in most cases.

Hello world!

Let’s produce the traditional Hello world! with minimal code:

>>> render = piecutter.Cutter()
>>> template = u'Hello {who}!'
>>> data = {u'who': u'world'}
>>> output = render(template, data)
>>> print(output.read())
Hello world!

Notes about this example:

  • text datatype is recognized as a template
  • the template engine is Python’s format() [1] by default
  • data is a mapping
  • output is a file-like object
  • piecutter.Cutter encapsulates full rendering pipeline.

Setup a template engine

piecutter uses Engines to render the templates against data.

piecutter has builtin support for the following template engines: Python’s format() [1], Jinja2 [2] and Django [3]. Additional engines could be supported, including non-Python ones!

Learn more about template engines at Engines.

Jinja2

Let’s produce Hello world! using Jinja2 [2]:

>>> template = u'Hello {{ who }}!'
>>> render = piecutter.Cutter(engine=piecutter.Jinja2Engine())
>>> output = render(template, data)
>>> print(output.read())
Hello world!
Django

We can do the same using Django [3]:

>>> template = u'Hello {{ who }}!'
>>> render = piecutter.Cutter(engine=piecutter.DjangoEngine())
>>> output = render(template, data)
>>> print(output.read())
Hello world!

Load templates from various locations

piecutter uses Loaders to fetch templates from various locations and to distinguish files from directories.

See loaders for details.

File-like objects

If you pass a file-like object to piecutter‘s default loader, it will automatically use it as a template.

As an example, let’s render an in-memory file:

>>> from StringIO import StringIO
>>> render = piecutter.Cutter()
>>> template = StringIO(u'Hello {who}!')
>>> output = render(template, data)
>>> print(output.read())
Hello world!

Of course, we can render an opened file object:

>>> render = piecutter.Cutter()
>>> with open('demo/simple/hello.txt') as template:
...     output = render(template, data)
...     print(output.read())
Hello world!
Templates on local filesystem

Use file:// (or file:/// for absolute paths) to tell piecutter‘s default loader to read templates on local filesystem:

>>> template = u'file://demo/simple/hello.txt'
>>> output = render(template, data)
>>> print(output.read())
Hello world!
Templates over HTTP

Use http:// or https:// to tell piecutter‘s default loader to fetch templates from remote HTTP server:

>>> template = u'https://raw.github.com/diecutter/piecutter/cutter-api-reloaded/demo/simple/hello.txt'
>>> output = render(template, data)
>>> print(output.read())
Hello world!
Additional loaders

Feel free to write custom loaders in order to support additional locations!

Basically, if you can determine template type (file or directory), fetch file contents and list directory items, you can implement a loader.

Render directories

Collections of templates, a.ka. directories, are also supported. By default, they are rendered as generator of rendered items.

Given the following directory:

demo/simple/
├── hello.txt  # Contains "Hello {who}!\n"
└── {who}.txt  # Contains "Whatever the content.\n"

When we render the directory, we can iterate generated items and use their attributes and methods:

>>> for item in render(u'file://demo/simple', data):
...     print('Name: {}'.format(item.name))
...     print('Content: {}'.format(item.read()))
Name: hello.txt
Content: Hello world!

Name: world.txt
Content: Whatever the content.

Write generated files to disk

piecutter uses writers to post-process generated content.

As an example, piecutter.FileWriter writes generated files to disk.

Let’s setup some output directory and check it does not exist yet.

>>> import os
>>> output_directory = os.path.join(temp_dir, 'directory')
>>> os.path.exists(output_directory)
False

Now generate files in output directory:

>>> render = piecutter.Cutter(
...     writer=piecutter.FileWriter(target=output_directory),
... )
>>> written_files = render('file://demo/simple/', data)

... and inspect the results:

>>> sorted(os.listdir(output_directory))
['simple']
>>> sorted(os.listdir(os.path.join(output_directory, 'simple')))
['hello.txt', 'world.txt']
>>> print(open(os.path.join(output_directory, 'simple', 'hello.txt'), 'rb').read())
Hello world!

>>> written_files  # Contains absolute path to generated files.
['/.../directory/simple/hello.txt', '/.../directory/simple/world.txt']

Learn more at Writers.

Notes & references

[1](1, 2) https://docs.python.org/2.7/library/string.html#formatstrings
[2](1, 2) http://jinja.pocoo.org/
[3](1, 2) https://docs.djangoproject.com/en/1.8/topics/templates/

Dynamic directories

List of items in directories can be dynamically computed, using templates.

It is just as if directories were templates. In fact, piecutter actually implements this feature using templates. Here is the workflow when a cutter renders a directory:

  • given location, and given loader tells that resource is a directory...
  • try to get explicit tree listing, which can be either static or dynamic. At the moment, “explicit tree listing” means directory holds a file named .directory-tree:
    • handle this file as the “directory tree template”
    • render this file against data
    • the result is directory tree listing (typically as JSON)
  • else, get the implicit (and static) directory tree listing (all items in directory).
  • finally, render each item in directory tree listing.

Use cases

Since directories can be described using a template, you can use all features of template engines to render filenames, select templates or even alter data.

Here are some use cases:

  • skip or include files based on variables;
  • render a single template several times with different output filenames;
  • alter template context data for some templates;
  • include templates from third-party locations;
  • use loops, conditions, text modifiers... and all template-engine features!

Step by step tutorial

Let’s try to explain the dynamic tree templates with a story...

Given a directory

Let’s start with this simple directory:

/tmp/dynamic/
└── greeter.txt  # Contains "{{ greeter }} {{ name }}!"

First create the directory:

>>> template_dir = os.path.join(temp_dir, 'dynamic')
>>> os.mkdir(template_dir)

Then put a simple “greeter.txt” template in this directory:

>>> greeter_filename = os.path.join(template_dir, 'greeter.txt')
>>> greeter_content = "{{ greeter }} {{ name }}!"  # Jinja2
>>> open(greeter_filename, 'w').write(greeter_content)
Render directory as usual

As shown in Quickstart tutorial, we can render the directory.

First setup piecutter and data:

>>> import piecutter
>>> data = {u'greeter': u'Hello', u'name': u'world'}
>>> render = piecutter.Cutter(engine=piecutter.Jinja2Engine())
>>> render.loader.routes['file'].root = temp_dir

Then render directory. To check the result, we print attributes of each rendered file:

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))
Location: file://.../dynamic/greeter.txt
Name: greeter.txt
Path: dynamic/greeter.txt
Content: Hello world!
Introduce empty .directory-tree file

Let’s update the directory to introduce a blank .directory-tree file in directory:

>>> tree_filename = os.path.join(template_dir, '.directory-tree')
>>> open(tree_filename, 'w').write("")

So directory now looks like this:

/tmp/dynamic/
├── .directory-tree  # Blank file.
└── greeter.txt  # Contains "{{ greeter }} {{ name }}!"

What happens if we render the directory again?

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))

No output! greeter.txt file has not been rendered.

piecutter found the .directory-tree file and used it as an explicit list of files to render. Since the file is empty, nothing was rendered.

Register greeter.txt

Let’s register greeter.txt in .directory-tree file:

>>> import json
>>> items_to_render = [
...     {
...         "template": "greeter.txt"
...     }
... ]
>>> open(tree_filename, 'w').write(json.dumps(items_to_render))

And check “greeter.txt” now gets rendered:

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))
Location: file://.../dynamic/greeter.txt
Name: greeter.txt
Path: dynamic/greeter.txt
Content: Hello world!
Render greeter.txt multiple times

Each item in directory tree must tell template, and accepts optional filename and data. Let’s take advantage of those additional options to render “greeter.txt” multiple times with different filenames and different content:

>>> items_to_render = [
...     {
...         "template": "greeter.txt",
...         "filename": "hello.txt",  # Explicitely change filename.
...     },
...     {
...         "template": "greeter.txt",
...         "filename": "goodbye.txt",
...         "data": {"greeter": "Goodbye"}  # Alter context data.
...     }
... ]
>>> open(tree_filename, 'w').write(json.dumps(items_to_render))

And check the result:

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))
...     print('----')
Location: file://.../dynamic/greeter.txt
Name: hello.txt
Path: dynamic/hello.txt
Content: Hello world!
----
Location: file://.../dynamic/greeter.txt
Name: goodbye.txt
Path: dynamic/goodbye.txt
Content: Goodbye world!
----

As you can see, location attribute refers to template’s source, whereas name and path refer to rendered object.

.directory-tree is a template

.directory-tree itself is handled as a template! So we can use context data and all engine’s features to dynamically generate the items to render.

>>> items_template = """
...     [
...       {% for greeter in greeting_list|default(['hello', 'goodbye']) %}
...         {
...           "template": "greeter.txt",
...           "filename": "{{ greeter }}.txt",
...           "data": {"greeter": "{{ greeter|capitalize }}"}
...         }{% if not loop.last %},{% endif %}
...       {% endfor %}
...     ]"""
>>> open(tree_filename, 'w').write(items_template)

If we render single file .directory-tree, we get data in JSON format:

>>> tree = render('file://' + template_dir + '/.directory-tree', data)
>>> print(tree.read())  
[
    {
        "template": "greeter.txt",
        "filename": "hello.txt",
        "data": {"greeter": "Hello"}
    },
    {
        "template": "greeter.txt",
        "filename": "goodbye.txt",
        "data": {"greeter": "Goodbye"}
    }
]

And now the directory is dynamically rendered:

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))
...     print('----')
Location: file://.../dynamic/greeter.txt
Name: hello.txt
Path: dynamic/hello.txt
Content: Hello world!
----
Location: file://.../dynamic/greeter.txt
Name: goodbye.txt
Path: dynamic/goodbye.txt
Content: Goodbye world!
----
Include locations

In dynamic tree templates, items’ template is a location. By default, it is considered relative to parent directory. But it can be absolute.

Let’s update .directory-tree file so that it calls local “greeter.txt” and a remote resource (see also loaders about remote locations):

>>> items_to_render = [
...     {
...         "template": "greeter.txt",
...         "filename": "local.txt",
...     },
...     {
...         "template": "https://raw.githubusercontent.com/diecutter/diecutter/0.7/demo/templates/greetings.txt",
...         "filename": "remote.txt",
...     }
... ]
>>> open(tree_filename, 'w').write(json.dumps(items_to_render))

Then render the directory again and check both local and remote files are rendered:

>>> for item in render(u'file://' + template_dir, data):
...     print('Location: {}'.format(item.location))
...     print('Name: {}'.format(item.name))
...     print('Path: {}'.format(item.path))
...     print('Content: {}'.format(item.read()))
...     print('----')
Location: file://.../dynamic/greeter.txt
Name: local.txt
Path: dynamic/local.txt
Content: Hello world!
----
Location: https://raw.githubusercontent.com/diecutter/diecutter/0.7/demo/templates/greetings.txt
Name: remote.txt
Path: dynamic/remote.txt
Content: Hello world!
----

This feature allows you to render a template directory that includes parts of third-party templates.

Internals

The loader is the one who knows that a resource is a directory, how to get the directory tree template and how to get the static listing of files in a directory.

The cutter drives the loader, then uses the engine to render the directory tree template, and finally converts result (JSON) into actual list of locations.

Install

piecutter is open-source software, published under BSD license. See License for details.

If you want to install a development environment, you should go to Contributing documentation.

Prerequisites

As a library

In most cases, you will use piecutter as a dependency of another project. In such a case, you should add piecutter in your main project’s requirements. Typically in setup.py:

from setuptools import setup

setup(
    install_requires=[
        'piecutter',
        #...
    ]
    # ...
)

Then when you install your main project with your favorite package manager (like pip [2]), piecutter will automatically installed.

Standalone

You can install piecutter with your favorite Python package manager. As an example with pip [2]:

pip install piecutter

Check

Check piecutter has been installed:

python -c "import piecutter;print(piecutter.__version__)"

You should get piecutter‘s version.

References

[1]https://www.python.org
[2](1, 2) https://pypi.python.org/pypi/pip/

Overview

This document quickly describes piecutter‘s core concepts and components.

See also Vision about motivations. See also Quickstart tutorial for a tutorial.

Cutters

Cutters are highest-level components in piecutter. They are the glue around all the features of piecutter. They orchestrate full template rendering workflow:

  • cutters are callables that take template (template object or location) and data as input
  • they use loaders [1] to fetch templates and distinguish files from directories
  • they use engines to render templates against data
  • they run writers to post-process output.

Learn more in Cutters.

Engines

Engines are the core components of piecutter. They use third-party template engines to generate output using templates and context data.

Engines are callables that accept template (template object only) and data as input then return generated output as an iterable file-like object.

piecutter‘s initial concept is to provide a single API to handle multiple template engines. piecutter‘s core currently supports the following third-party engines:

Of course, you can implement custom engines.

piecutter also provides a special ProxyEngine that tries to guess the best engine to use depending on template.

Learn more in Engines.

Loaders

Loaders load templates from Python objects or from locations.

Loaders are callables that accept location as input argument then return a template object.

piecutter provides builtin support for various locations:

  • Python builtins (text and file-like objects)
  • files on local filesystem
  • remote files over HTTP
  • remote files on Github.

Of course, you can write your own loaders!

See loaders for details.

Templates

piecutter handles template objects. They are, basically, Python objects whose content can be read.

Templates can represent either single units (files) or collections (directories).

Single units rendered as file-like object.

Collections are rendered as generator of file-like objects.

Loaders make the difference between single units and collections.

Learn more at Template objects.

Data

piecutter uses mappings as context data. Any dictionary-like object can be used.

During rendering, additional contextual data is added to the original, such as piecutter.engine, which represents template engine name.

See Context data for details.

Writers

piecutter uses writers to post-process template rendering output.

Learn more at Writers.

Dispatchers

piecutter renders templates using processing pipelines. As an example, writers can be chained to perform several operations. Everywhere processing could be done by either one or several functions, you can use dispatchers: they look like one function but encapsulate several calls.

Note

piecutter‘s initial scope doesn’t include dispatchers. So this feature may be moved to a third-party library.

See loaders for details.

Notes & references

[1]loaders
[2]https://docs.python.org/2.7/library/string.html#formatstrings
[3]http://jinja.pocoo.org/
[4]https://docs.djangoproject.com/en/1.8/topics/templates/

Cutters

piecutter‘s cutters encapsulate full template rendering workflow, from template loading to output post-processing, via template rendering of course.

Cutters are callable

Cutters are callables that take location and data as input and write output somewhere.

>>> import piecutter
>>> render = piecutter.Cutter()
>>> output = render(u'Hello {who}!', {u'who': u'world'})
>>> print(output.read())
Hello world!

Cutters are configurable

Cutters are objects that can be configured with loaders, engines and writers.

>>> render = piecutter.Cutter(
...     loader=piecutter.HttpLoader(),
...     engine=piecutter.Jinja2Engine(),
...     writer=piecutter.PrintWriter(),
... )
>>> render(
...     'https://raw.githubusercontent.com/diecutter/diecutter/0.7/demo/templates/greetings.txt',
...     {'name': 'world'})
Hello world!

Tip

If you want loader, engine or writer to perform multiple tasks, then you may be interested in dispatchers. Dispatchers help you create pipelines of callables.

Engines

piecutter‘s engines use third-party template engines to generate output using template and data.

Engines are callables that accept template, data as input then return generated output as an iterable file-like object.

Warning

Engines support only single templates. Rendering collections of templates involve interactions with loaders. This feature is implemented by cutters level.

Python format

>>> import piecutter
>>> render = piecutter.PythonFormatEngine()
>>> output = render(u'Hello {who}!', {u'who': u'world'})
>>> print(output.read())
Hello world!

Jinja2

>>> import piecutter
>>> render = piecutter.Jinja2Engine()
>>> output = render(u'Hello {{ who }}!', {u'who': u'world'})
>>> print(output.read())
Hello world!

Django

>>> import piecutter
>>> render = piecutter.DjangoEngine()
>>> output = render(u'Hello {{ who }}!', {u'who': u'world'})
>>> print(output.read())
Hello world!

Proxy

This is a special renderer that tries to detect best engine matching template.

>>> import piecutter
>>> render = piecutter.ProxyEngine(
...     engines={
...         'django': piecutter.DjangoEngine(),
...         'jinja2': piecutter.Jinja2Engine(),
...         'pythonformat': piecutter.PythonFormatEngine(),
...     },
... )

>>> data = {u'who': u'world'}

>>> template = piecutter.TextTemplate("{# Jinja2 #}Hello {{ who }}!")
>>> print(render(template, data).read())
Hello world!

>>> template = piecutter.TextTemplate("{# Django #}Hello {{ who }}!")
>>> print(render(template, data).read())
Hello world!

>>> template = piecutter.TextTemplate("Hello {who}!")
>>> print(render(template, data).read())
Hello world!

Warning

piecutter.ProxyEngine is experimental and not yet optimized: it loads template content in memory in order to guess engine. Better implementations or alternatives (such as using template filename’s extension) are welcome!

Custom engines

Engines typically are classes that inherit from piecutter.Engine. Feel free to write your own!

class piecutter.engines.Engine

Bases: object

Engines render single template against data.

Subclasses MUST implement do_render(): and match().

render(template, context)

Return the rendered template against context.

match(template, context)

Return probability that template uses engine (experimental).

If a template is not written in engine’s syntax, the probability should be 0.0.

If there is no doubt the template has been written for engine, the probability should be 1.0.

Else, the probability should be strictly between 0.0 and 1.0.

As an example, here are two ways to be sure template has been written for a specific template engine:

  • template’s name uses specific file extension
  • there is an explicit shebang at the beginning of the template.

Loaders

Loaders are callables that accept location as input argument then return a template object:

>>> import piecutter
>>> load = piecutter.LocalLoader(root=u'demo/simple')
>>> with load(u'hello.txt') as template:
...     print(template)
Hello {who}!

Loaders encapsulate communication with file storages, either local or remote. They can:

  • distinguish single files from directories
  • read contents of single files
  • get a dynamic tree template out of a directory
  • or get the static list of templates in a directory.

The default loader of piecutter.Cutter is a piecutter.ProxyLoader.

LocalLoader

piecutter.LocalLoader handles files in local filesystem.

>>> import piecutter
>>> load = piecutter.LocalLoader(root='demo/simple')

>>> with load('hello.txt') as template:
...     print(template)
Hello {who}!


>>> with load('i-dont-exist.txt') as template:  # Doctest: +ELLIPSIS
...     print(template)
Traceback (most recent call last):
  ...
TemplateNotFound: ...

And local directories:

>>> import piecutter
>>> load = piecutter.LocalLoader(root='demo/simple')
>>> print(load.tree('.'))
[u'hello.txt', u'{who}.txt']

HttpLoader

piecutter.HttpLoader handles files over HTTP.

>>> load = piecutter.HttpLoader()
>>> location = 'https://raw.githubusercontent.com/diecutter/diecutter/0.7/demo/templates/greetings.txt'
>>> with load(location) as template:
...     print(template)
{{ greetings|default('Hello') }} {{ name }}!

GithubLoader

piecutter.GithubLoader handles files in Github repository.

>>> checkout_dir = os.path.join(temp_dir, 'github-checkout')
>>> load = piecutter.GithubLoader(checkout_dir)
>>> location = 'diecutter/diecutter/0.7/demo/templates/greetings.txt'
>>> with load(location) as template:
...     print(template)
{{ greetings|default('Hello') }} {{ name }}!

ProxyLoader

piecutter.ProxyLoader delegates loading to specific implementation depending on location:

  • if location is a Template object, just return it.
  • if location is Python’s text, try to detect scheme (fallback to “text://”) then route to specific loader matching this scheme.
  • if location is file-like object, route to specific loader matching “fileobj://” scheme.

Let’s initialize a proxy with routes:

>>> load = piecutter.ProxyLoader(
...     routes={  # The following are defaults.
...         'text': piecutter.TextLoader(),
...         'fileobj': piecutter.FileObjLoader(),
...         'file': piecutter.LocalLoader(),
...         'http': piecutter.HttpLoader(),
...         'https': piecutter.HttpLoader(),
...     })

Then load templates from various locations:

>>> location = u'Just raw text\n'
>>> with load(location) as template:
...     print(template)
Just raw text
>>> from StringIO import StringIO
>>> location = StringIO('Content in some file-like object\n')
>>> with load(location) as template:
...     print(template)
Content in some file-like object
>>> location = 'file://demo/simple/hello.txt'
>>> with load(location) as template:
...     print(template)
Hello {who}!
>>> location = 'https://raw.githubusercontent.com/diecutter/diecutter/0.7/demo/templates/greetings.txt'
>>> with load(location) as template:
...     print(template)
{{ greetings|default('Hello') }} {{ name }}!

Custom loaders

Loaders typically are classes that inherit from piecutter.Loader. Feel free to write your own!

class piecutter.loaders.Loader

Bases: object

Loader implements access to locations.

open(location)

Return template object (file or directory) from location.

is_file(location)

Return True if ressource at location is a file.

is_directory(location)

Return True if ressource at location is a directory.

tree_template(location)

Return location of dynamic tree template if location is a dir.

Whenever possible, dynamic tree template file should be named ”.directory-tree”.

Raise exception if location is not a directory.

Raise TemplateNotFound if location has no tree template.

tree(location)

Return static list of templates, given location is a directory.

As an example a “local filesystem” implementation should just return the list of items in directory, except special dynamic tree template.

Raise exception if location is not a directory.

Template objects

piecutter‘s templates are Python representations of content to be rendered against data.

Note

You may not care about template objects, except you enter piecutter internals in order to write custom stuff such as loaders, cutters or writers. In other cases, using locations with loaders should be enough.

Single units VS collections

piecutter handles two types of templates:

  • files: they are rendered as single units;
  • directories: they are rendered as collections of files.

Loaders make the difference between single units and collections:

>>> import piecutter
>>> loader = piecutter.LocalLoader(root=u'demo/simple')

>>> loader.is_file(u'hello.txt')
True
>>> with loader(u'hello.txt') as template:
...     template.is_file
True

>>> loader.is_dir(u'.')
True
>>> with loader(u'.') as template:
...     template.is_dir
True

Typically, single units will be rendered as file-like object, and collections will be rendered as generator of file-like objects.

Create template from text

You can instantiate templates from text:

>>> import piecutter

>>> template = piecutter.TextTemplate("I'm a template")
>>> template  # Doctest: +ELLIPSIS
<piecutter.templates.TextTemplate object at 0x...>
>>> print(template)
I'm a template

Create template from file

You can create templates from file-like objects:

>>> with open('demo/simple/hello.txt') as template_file:
...     template = piecutter.FileTemplate(template_file)
...     template  # Doctest: +ELLIPSIS
...     print(template)
<piecutter.templates.FileTemplate object at 0x...>
Hello {who}!

Create template from custom locations

You can use loaders to instantiate templates from custom locations:

>>> import pathlib
>>> loader = piecutter.LocalLoader(root=pathlib.Path('demo/simple'))
>>> with loader.open('hello.txt') as template:
...     print(template)
Hello {who}!

You should be able to load files from almost everywhere, provided you have the right loaders. See Loaders for details.

Create template from directory

You can create templates from directories using loaders:

>>> loader = piecutter.LocalLoader(root=pathlib.Path('demo/simple'))
>>> print(loader.tree('.'))
[u'hello.txt', u'{who}.txt']

Context data

This document describes how piecutter handles data to render templates.

Mappings

piecutter uses mappings as context data. Any dictionary-like object can be used.

Default context data

piecutter registers a special piecutter variable in context, with values about environment and execution.

Here is a sample template using all piecutter‘s special context:

{# jinja2 -#}
{
  {%- for key, value in piecutter.iteritems() %}
  "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %}
  {%- endfor %}
}

And here is the expected output:

>>> import piecutter
>>> render = piecutter.Cutter(engine=piecutter.Jinja2Engine())
>>> print(render('file://tests/default_context.json', {}).read())
{
  "engine": "jinja2"
}

Writers

Writers post-process generated content.

The default writer in piecutter.Cutter is piecutter.TransparentWriter.

TransparentWriter

piecutter.TransparentWriter just returns result as is:

>>> import piecutter
>>> write = piecutter.TransparentWriter()
>>> write('Hello') is 'Hello'
True

With this writer in a Cutter, you get the output of the engine.

StreamWriter

piecutter.StreamWriter writes generated content to a stream:

>>> import sys
>>> render = piecutter.Cutter()
>>> render.writer = piecutter.StreamWriter(stream=sys.stdout)
>>> render('file://demo/simple/hello.txt', {u'who': u'world'})
Hello world!

PrintWriter

piecutter.PrintWriter sends generated content to builtin print function:

>>> import sys
>>> render = piecutter.Cutter()
>>> render.writer = piecutter.PrintWriter()
>>> render('file://demo/simple/hello.txt', {u'who': u'world'})
Hello world!

FileWriter

piecutter.FileWriter writes generated files to disk and return list of written filenames:

>>> # Let's setup some output directory.
>>> output_directory = os.path.join(temp_dir, 'directory')
>>> os.path.exists(output_directory)
False

>>> # Generate files in output directory.
>>> render = piecutter.Cutter()
>>> render.writer = piecutter.FileWriter(target=output_directory)
>>> written_files = render('file://demo/simple', {u'who': u'world'})

>>> # Inspect the results.
>>> sorted(os.listdir(output_directory))
['simple']
>>> sorted(os.listdir(os.path.join(output_directory, 'simple')))
['hello.txt', 'world.txt']
>>> print(open(os.path.join(output_directory, 'simple', 'hello.txt'), 'rb').read())
Hello world!

>>> written_files  # Contains absolute path to generated files.
['/.../directory/simple/hello.txt', '/.../directory/simple/world.txt']

Custom writers

Writers typically are classes that inherit from piecutter.Writer. Feel free to implement your own!

class piecutter.writers.Writer

Bases: object

Writers post-process generated content.

Subclasses MUST implement write().

write(content)

Post-process template-rendering result.

Dispatchers

piecutter‘s dispatchers are made to configure pipelines. As a summary, use a dispatcher wherever you could use a single callable.

Dispatchers are configurable callables.

Note

In future releases, dispatchers may be distributed in their own library, because they provide features that could be useful apart from piecutter, i.e. piecutter would depend on this external library.

FirstResultDispatcher

Iterate over callables in pipeline, calling each one with original arguments. As soon as one returns “non empty” result, break and return this result.

>>> from piecutter.utils import dispatchers

>>> zero = lambda x: None
>>> one = lambda x: dispatchers.NO_RESULT
>>> two = lambda x: 'two {thing}'.format(thing=x)
>>> three = lambda x: 'three {thing}'.format(thing=x)
>>> callable = dispatchers.FirstResultDispatcher([zero, one, two, three])
>>> print(callable('sheep'))
two sheep

LastResultDispatcher

Iterate over callables in pipeline, call each one with original arguments. Only remember last “non empty” result. At the end, return this result.

>>> one = lambda x: 'one {thing}'.format(thing=x)
>>> two = lambda x: 'two {thing}'.format(thing=x)
>>> three = lambda x: 'three {thing}'.format(thing=x)
>>> callable = dispatchers.LastResultDispatcher([zero, one, two, three])
>>> print(callable('sheep'))
three sheep

ChainDispatcher

Iterate over callables in pipeline, call the first one with original arguments, then use the current output as input for the next callable... Return the value returned by the last callable in the pipeline.

>>> strip = lambda x: x.strip()
>>> upper = lambda x: x.upper()
>>> strong = lambda x: '<strong>{thing}</strong>'.format(thing=x)
>>> callable = dispatchers.ChainDispatcher([strip, upper, strong])
>>> print(callable('    sheep           '))
<strong>SHEEP</strong>

About piecutter

This section is about piecutter project itself.

Vision

piecutter is about file generation. Its primary goal is to provide a generic API to render templates against data.

Render template against data

piecutter has been created for the following pattern:

  • you have a template. To its simplest expression, it is content with placeholders ;
  • you have data, typically a mapping ;
  • you want to render the template against the data.

The important point here is that, as an user you want to focus on templates and data, because they are your content, the bits that you own and manage.

As an user, you do not want to bother with the rendering process. And that is piecutter‘s primary goal: encapsulate content generation, whatever the template and the data you provide.

Wherever the templates

Templates can theorically live anywhere: on local filesystem, on remote places, or they could be generated in some way... As an user, I do not want to bother with template loading, I just want templates to be loaded and rendered against data.

One could say templates are just strings and loading could be done by the user, i.e. the feature could be simplified to “render string against data”. But templates often take advantage of features like “includes” or “extends”. Such features require loaders.

Of course piecutter cannot implement all template storages. It provides implementation for simplest ones (string, local filesystem) and an API for third-parties to implement additional loaders.

Whatever the template engine

As a matter of fact, templates are written using the syntax of one template engine. But whatever this syntax, you basically want it rendered.

Data is dictionary-like

piecutter supports neither loading of various data formats nor loading from various locations. The Python [1] language has nice libraries for that purpose.

piecutter expects a structured data input, i.e. a dictionary-like object. And it should be enough.

A framework

piecutter is a framework. It is built with flexibility in mind. It is a library to build other software. It provides material to connect to third-party tools. It is easy to extend.

Notes & references

[1]https://www.python.org

License

Copyright (c) 2014, Rémy Hubscher. See Authors & contributors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the piecutter software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Authors & contributors

Development lead

Changelog

This document describes changes between each past release. For information about future releases, check milestones [1] and Vision.

0.2 (unreleased)

API simplification.

  • Feature #9 - piecutter.Cutter class encapsulates template rendering process. Setup a cutter as you like, then use it to render templates.
  • Bug #11 - On PyPI, README is rendered as HTML (was not, because of some raw HTML). Badges were removed from PyPI page.
0.1.1 (2014-04-09)

Fixes around distribution of release 0.1.

  • Bug #12 - piecutter archive on PyPI was missing many files.
0.1 (2014-04-08)

Initial release.

  • Feature #6 - Imported stuff from diecutter: loaders, writers, resources and engines.

Notes & references

[1]https://github.com/diecutter/piecutter/milestones

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

This document provides guidelines for people who want to contribute to the project.

Create tickets: bugs, features, feedback...

The best way to send feedback is to file an issue in the bugtracker [1].

Please use the bugtracker [1] before starting some work:

  • check if the bug or feature request has already been filed. It may have been answered too!
  • else create a new ticket.
  • if you plan to contribute, tell us, so that we are given an opportunity to give feedback as soon as possible.
  • in your commit messages, reference the ticket with some refs #TICKET-ID syntax.

Use topic branches

  • Work in branches.
  • Prefix your branch with the ticket ID corresponding to the issue. As an example, if you are working on ticket #23 which is about contribute documentation, name your branch like 23-contribute-doc.
  • If you work in a development branch and want to refresh it with changes from master, please rebase [2] or merge-based rebase [3], i.e. do not merge master.

Fork, clone

Clone piecutter repository (adapt to use your own fork):

git clone git@github.com:<your-github-username-here>/piecutter.git
cd piecutter/

Setup a development environment

System requirements:

Execute:

make develop

Usual actions

The Makefile is the reference card for usual actions in development environment:

  • Install development toolkit with pip [6]: make develop.
  • Run tests with tox [7]: make test.
  • Build documentation: make documentation. It builds Sphinx [8] documentation in var/docs/html/index.html.
  • Release piecutter project with zest.releaser [9]: make release.
  • Cleanup local repository: make clean, make distclean and make maintainer-clean.

Notes & references

[1](1, 2) https://github.com/diecutter/piecutter/issues
[2]http://git-scm.com/book/en/Git-Branching-Rebasing
[3]http://tech.novapost.fr/psycho-rebasing-en.html
[4]https://www.python.org
[5]https://pypi.python.org/pypi/virtualenv/
[6]https://pypi.python.org/pypi/pip/
[7]https://pypi.python.org/pypi/tox/
[8]https://pypi.python.org/pypi/Sphinx/
[9]https://pypi.python.org/pypi/zest.releaser/