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()