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.
Resources¶
- Documentation: https://piecutter.readthedocs.io
- PyPI page: http://pypi.python.org/pypi/piecutter
- Bugtracker: https://github.com/diecutter/piecutter/issues
- Changelog: https://piecutter.readthedocs.io/en/latest/about/changelog.html
- Roadmap: https://github.com/diecutter/piecutter/milestones
- Code repository: https://github.com/diecutter/piecutter
- Continuous integration: https://travis-ci.org/diecutter/piecutter
Contents¶
Quickstart tutorial¶
Let’s discover piecutter with simple stories!
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.
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.
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) anddata
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.
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()
: andmatch()
.-
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 atlocation
is a file.
-
is_directory
(location)¶ Return
True
if ressource atlocation
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
iflocation
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.
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']
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¶
- Rémy Hubscher <hubscher.remy@gmail.com>
- Benoît Bryon <benoit@marmelune.net>
Contributors¶
Developers: https://github.com/diecutter/piecutter/graphs/contributors
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.
Resources¶
- Code repository: https://github.com/diecutter/piecutter
- Bugtracker: https://github.com/diecutter/piecutter/issues
- Continuous integration: https://travis-ci.org/diecutter/piecutter
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:
- Python [4] version 2.7 (in a virtualenv [5] if you like).
- make and wget to use the provided Makefile.
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
andmake 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/ |