1 #!/usr/bin/env python
  2
  3 """
  4 Use templates + yaml to generate files.
  5
  6 pork.py is a simple file generator which combines yaml and a templating
  7 engine in a very stripped down way.  Usage is simple:
  8
  9   ./pork.py <yaml file> [<yaml file> <yaml file> ...]
 10
 11 The YAML file should contain 2 YAML documents.  The first document is config
 12 information, and the second a structure which corresponds with the variables
 13 referenced in your template(s).
 14
 15 The config document must contain one key:
 16
 17   template -- full path to a template file on disk
 18
 19 By default pork.py sends the rendered results to standard output.  However,
 20 if the config document contains the optional ``target`` item, the value is
 21 used as the file to write on disk:
 22
 23   target -- full path to the target file (the file you are generating)
 24
 25 A third key the config document recognises is ``engine``.  This is the
 26 name of the template engine you want to use.  The default is django;
 27 other supported values are jinja2, mako, and python (string.Template
 28 interpolation).
 29
 30   engine -- name of the template engine to use (default 'django')
 31
 32 NB if you use django, no settings module is required, or used.
 33
 34 If your template refers to other templates (eg a django {% include %}),
 35 those others must be in the same directory as the original.
 36
 37 The second document is loaded into a python dictionary and passed to the
 38 chosen engine's rendering method (a dictionary, kwargs, etc).
 39
 40 pork.py only really does sanity checking on its own input.  Any errors
 41 caused by bad YAML, bad filenames, bad templates, etc, are dealt with
 42 by python, yaml, or the engine itself.
 43
 44 See the YAML spec or the PyYAML docs for how to write YAML documents and
 45 files.  Here's a super-trivial example:
 46
 47 hello.yml:
 48
 49   ---
 50   template: hello.html
 51   ---
 52   hello: world
 53
 54 hello.html:
 55
 56     Hello {{hello}}
 57
 58 $ ./pork.py hello.yml
 59 Hello world
 60
 61 """
 62
 63 # standard library
 64 import os, sys
 65
 66 # 3rd parties
 67 import yaml
 68
 69 _django_configured = False
 70
 71 engines = []
 72
 73 # decorator to maintain the list of engines from renderer methods
 74 def engine(func):
 75     if func.__name__ not in engines:
 76         engines.append(func.__name__)
 77     return func
 78
 79 class Renderer:
 80     def __init__(self, config, values):
 81         # split the path into a directory and a filename and get it rendered
 82         self.head, self.tail = os.path.split(config["template"])
 83         self.values = values
 84         self.config = config
 85         self.engine = config.get('engine','django')
 86     def render(self):
 87         return eval("self.%s()" % self.engine)
 88     def spit(self, output=None):
 89         if output is None:
 90             output = self.render()
 91         if self.config.has_key("target"):
 92             os.makedirs(os.path.split(self.config["target"])[0])
 93             fh = open(self.config["target"],'w')
 94             fh.write(output)
 95             fh.close()
 96             print "### generated [%s]" % self.config["target"]
 97         else:
 98             print output
 99     # renderers
100     @engine
101     def django(self):
102         # django
103         from django.conf import settings
104         from django.template.loader import render_to_string
105         # use of django's templates without settings requires this
106         # configury-pokery
107         global _django_configured
108         if not _django_configured:
109             settings.configure()
110             _django_configured=True
111         settings.TEMPLATE_DIRS=[self.head]
112         return render_to_string(self.tail, self.values)
113
114     @engine
115     def jinja2(self):
116         # jinja2
117         from jinja2 import Environment, FileSystemLoader
118         env = Environment(loader=FileSystemLoader(self.head))
119         template = env.get_template(self.tail)
120         values = self.values
121         return template.render(**values)
122
123     @engine
124     def mako(self):
125         # mako
126         from mako.lookup import TemplateLookup
127         lookup = TemplateLookup(directories=[self.head], collection_size=1)
128         template = lookup.get_template(self.tail)
129         values = self.values
130         return template.render(**values)
131
132     @engine
133     def python(self):
134         # string.Template
135         import string
136         file = "%s/%s" % (self.head, self.tail)
137         values=self.values
138         return string.Template(open(file).read()).safe_substitute(values)
139
140
141 def usage():
142     sys.stderr.write("Usage: pork.py <yaml file> [<yaml file> ...]\n")
143     sys.exit()
144
145 def main():
146     assert len(sys.argv) > 1, usage()
147     for file in sys.argv[1:]:
148         try:
149             generate(file)
150         except Exception, e:
151             sys.stderr.write("### Problem with file [%s], error was:\n" % file)
152             sys.stderr.write("###  %s\n" % e)
153
154 def generate(file):
155     # if this stuff barfs (bad yaml, no file, etc) we're just going to
156     # let python + yaml throw their errors
157     # NB: load_all returns a generator that can't be cast to a list, but I
158     # want it as a list damn it, hence the comprehension
159     docs = [doc for doc in yaml.load_all(open(file))]
160
161     # now do all our assertions in the hope that we have clean data
162     # all other errors can be caught by the template engines or python;
163     # we only care about pork.py's requirements
164     assert len(docs) == 2, "yaml file must contain only 2 documents"
165     config, values = docs
166     assert isinstance(config,dict), "config document must be dictionary"
167     assert isinstance(values,dict), "values document must be dictionary"
168     assert config.has_key("template"), "config document must contain template"
169     assert config.get('engine','django') in engines, \
170             "supported engines are %s" % engines
171
172     # the class does everything now
173     # initialises with the documents, .spit does the rendering and the
174     # stdout/file output
175     Renderer(config,values).spit()
176
177 if __name__ == '__main__':
178     main()