| 1 |
"""Cobbler, the simplest Python templating system that could possibly work. |
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
http://www.geocities.com/baby-lemonade/safairy.html explains it all, really: |
|---|
| 5 |
|
|---|
| 6 |
At the foot of the bed, Otumba kept wogs for poisonous snacks |
|---|
| 7 |
such as the deadly cobbler and apply python. |
|---|
| 8 |
|
|---|
| 9 |
Features: |
|---|
| 10 |
|
|---|
| 11 |
* Dict-like interface; keys map to %(named)s format args. |
|---|
| 12 |
* Re-usable; modify any/all params and assemble() as needed. |
|---|
| 13 |
* str()-able, unicode() too |
|---|
| 14 |
* Encoding-aware; just set Template.encoding before load_template(). |
|---|
| 15 |
* Stray "%" chars auto-replaced with "%%". |
|---|
| 16 |
* Builtin subclasses for HTML Option, Checkbox, Hidden, and TableRows. |
|---|
| 17 |
* Custom subclasses are a snap! |
|---|
| 18 |
|
|---|
| 19 |
CherryPy example: |
|---|
| 20 |
|
|---|
| 21 |
import os.path |
|---|
| 22 |
localDir = os.path.dirname(__file__) |
|---|
| 23 |
|
|---|
| 24 |
import cobbler |
|---|
| 25 |
import myapp |
|---|
| 26 |
|
|---|
| 27 |
class Root: |
|---|
| 28 |
|
|---|
| 29 |
def default(self, city): |
|---|
| 30 |
main = cobbler.Template(localDir, 'templates', 'default.html') |
|---|
| 31 |
main[u'City'] = myapp.safe_html(city) |
|---|
| 32 |
main[u'ZipCodes'] = myapp.zips_for_city(city) |
|---|
| 33 |
return main |
|---|
| 34 |
default.exposed = True |
|---|
| 35 |
|
|---|
| 36 |
default.html: |
|---|
| 37 |
|
|---|
| 38 |
<html> |
|---|
| 39 |
<head><title>Zip Codes for %(City)s</title></head> |
|---|
| 40 |
<body>%(ZipCodes)s</body> |
|---|
| 41 |
</html> |
|---|
| 42 |
|
|---|
| 43 |
""" |
|---|
| 44 |
|
|---|
| 45 |
import codecs |
|---|
| 46 |
import os |
|---|
| 47 |
import re |
|---|
| 48 |
from xml.sax.saxutils import quoteattr as quote |
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 |
def warn(msg): |
|---|
| 52 |
|
|---|
| 53 |
import warnings |
|---|
| 54 |
warnings.warn(msg) |
|---|
| 55 |
|
|---|
| 56 |
|
|---|
| 57 |
class Template(object): |
|---|
| 58 |
"""Interpolates Python values with a block of source text. |
|---|
| 59 |
|
|---|
| 60 |
The source text is usually written in a markup language like HTML. |
|---|
| 61 |
String-formatting is used to substitute Python values into the text. |
|---|
| 62 |
For example, the source text "Hello, %(audience)s!" might be |
|---|
| 63 |
interpolated with {'audience': 'World'} to produce "Hello, World!". |
|---|
| 64 |
""" |
|---|
| 65 |
|
|---|
| 66 |
params = {} |
|---|
| 67 |
encoding = "ISO-8859-1" |
|---|
| 68 |
templateText = u'' |
|---|
| 69 |
|
|---|
| 70 |
def __init__(self, *fileparts): |
|---|
| 71 |
self.exhausted = False |
|---|
| 72 |
self.params = self.__class__.params.copy() |
|---|
| 73 |
self.templateText = self.__class__.templateText |
|---|
| 74 |
if fileparts: |
|---|
| 75 |
self.templateText = self.load_template(*fileparts) |
|---|
| 76 |
|
|---|
| 77 |
def load_template(self, *fileparts): |
|---|
| 78 |
"""Return the source text from a file.""" |
|---|
| 79 |
newFileName = os.path.join(*fileparts) |
|---|
| 80 |
infile = codecs.open(newFileName, "r", self.encoding) |
|---|
| 81 |
t = infile.read() |
|---|
| 82 |
infile.close() |
|---|
| 83 |
t = re.sub(r'\r\n', u'\n', t) |
|---|
| 84 |
|
|---|
| 85 |
t = re.sub(r'(?<!%)%(?![%\(])', u'%%', t) |
|---|
| 86 |
return t |
|---|
| 87 |
|
|---|
| 88 |
def assemble(self, final_params={}): |
|---|
| 89 |
"""Return the template text, interpolated with all params. |
|---|
| 90 |
|
|---|
| 91 |
final_params are discarded. They are NOT merged into self.params. |
|---|
| 92 |
""" |
|---|
| 93 |
all = final_params.copy() |
|---|
| 94 |
all.update(self.params) |
|---|
| 95 |
return self.templateText % all |
|---|
| 96 |
|
|---|
| 97 |
def __getitem__(self, key): |
|---|
| 98 |
return self.params[key] |
|---|
| 99 |
|
|---|
| 100 |
def __setitem__(self, key, value): |
|---|
| 101 |
self.params[key] = value |
|---|
| 102 |
|
|---|
| 103 |
def __delitem__(self, key): |
|---|
| 104 |
del self.params[key] |
|---|
| 105 |
|
|---|
| 106 |
def update(self, mapping): |
|---|
| 107 |
self.params.update(mapping) |
|---|
| 108 |
|
|---|
| 109 |
def __iter__(self): |
|---|
| 110 |
self.exhausted = False |
|---|
| 111 |
return self |
|---|
| 112 |
|
|---|
| 113 |
def next(self): |
|---|
| 114 |
if self.exhausted: |
|---|
| 115 |
raise StopIteration |
|---|
| 116 |
else: |
|---|
| 117 |
self.exhausted = True |
|---|
| 118 |
return self.assemble() |
|---|
| 119 |
|
|---|
| 120 |
def __str__(self): |
|---|
| 121 |
return self.assemble() |
|---|
| 122 |
|
|---|
| 123 |
def __unicode__(self): |
|---|
| 124 |
return self.assemble() |
|---|
| 125 |
|
|---|
| 126 |
|
|---|
| 127 |
class OptionElement(Template): |
|---|
| 128 |
"""A Template for generating HTML <option> elements. |
|---|
| 129 |
|
|---|
| 130 |
Usage: |
|---|
| 131 |
|
|---|
| 132 |
1. When the value is distinct from the display: |
|---|
| 133 |
opts = {} |
|---|
| 134 |
for eachKey, eachItem in aCollection.items(): |
|---|
| 135 |
opts[eachKey] = eachItem.value('Name') |
|---|
| 136 |
output = OptionElement().assemble_all(opts, selItem) |
|---|
| 137 |
|
|---|
| 138 |
2. When the value is the same as the display: |
|---|
| 139 |
opts = ['value1', 'value2', 'value3'] |
|---|
| 140 |
output = OptionElement().assemble_all(opts, selItem) |
|---|
| 141 |
""" |
|---|
| 142 |
|
|---|
| 143 |
templateText = (u'<option value="%(value)s"%(isSelected)s>' |
|---|
| 144 |
u'%(display)s</option>\n') |
|---|
| 145 |
|
|---|
| 146 |
def assemble_all(self, optionList, matchValue, sortByDisplay=True): |
|---|
| 147 |
"""Interpose supplied parameters into the predetermined template. |
|---|
| 148 |
|
|---|
| 149 |
If optionList is a sequence of strings, |
|---|
| 150 |
they will be interposed as the 'display' variables. |
|---|
| 151 |
If optionList is a sequence of tuples, |
|---|
| 152 |
they will be interposed as ('value', 'display'). |
|---|
| 153 |
If optionList is a dictionary of strings, |
|---|
| 154 |
it will be interposed as {'value': 'display'}. |
|---|
| 155 |
""" |
|---|
| 156 |
if optionList: |
|---|
| 157 |
if isinstance(optionList, dict): |
|---|
| 158 |
optionList = [(k, v) for k, v in optionList.iteritems()] |
|---|
| 159 |
if sortByDisplay: |
|---|
| 160 |
optionList.sort(lambda x, y: cmp(x[1], y[1])) |
|---|
| 161 |
elif isinstance(optionList, list): |
|---|
| 162 |
if isinstance(optionList[0], tuple): |
|---|
| 163 |
if sortByDisplay: |
|---|
| 164 |
optionList.sort(lambda x, y: cmp(x[1], y[1])) |
|---|
| 165 |
else: |
|---|
| 166 |
if sortByDisplay: |
|---|
| 167 |
optionList.sort() |
|---|
| 168 |
|
|---|
| 169 |
tParam = [] |
|---|
| 170 |
matchValue = "%s" % matchValue |
|---|
| 171 |
for eachValue in optionList: |
|---|
| 172 |
sel = "" |
|---|
| 173 |
|
|---|
| 174 |
|
|---|
| 175 |
if isinstance(eachValue, tuple): |
|---|
| 176 |
val = "%s" % eachValue[0] |
|---|
| 177 |
display = "%s" % eachValue[1] |
|---|
| 178 |
else: |
|---|
| 179 |
val = display = ("%s" % eachValue) |
|---|
| 180 |
|
|---|
| 181 |
if val == matchValue: |
|---|
| 182 |
sel = " selected='selected' " |
|---|
| 183 |
|
|---|
| 184 |
if display.startswith(" ") or display.endswith(" "): |
|---|
| 185 |
msg = ("Option element has leading or trailing spaces in " |
|---|
| 186 |
"display:\n'%s'.\nSome browsers strip such spaces " |
|---|
| 187 |
"on display and when submitting HTML forms." % val) |
|---|
| 188 |
warn(msg) |
|---|
| 189 |
|
|---|
| 190 |
tParam.append(self.assemble({u'isSelected': sel, |
|---|
| 191 |
u'value': val, |
|---|
| 192 |
u'display': display, |
|---|
| 193 |
})) |
|---|
| 194 |
return u"".join(tParam) |
|---|
| 195 |
|
|---|
| 196 |
|
|---|
| 197 |
class CheckboxElement(Template): |
|---|
| 198 |
"""A Template for generating HTML <input type='checkbox'> elements |
|---|
| 199 |
|
|---|
| 200 |
Usage: |
|---|
| 201 |
|
|---|
| 202 |
output = cobbler.CheckboxElement().assemble_all(name, checked) |
|---|
| 203 |
""" |
|---|
| 204 |
|
|---|
| 205 |
def __init__(self, value=None): |
|---|
| 206 |
Template.__init__(self) |
|---|
| 207 |
if value is None: |
|---|
| 208 |
self.templateText = (u'<input type="checkbox" ' |
|---|
| 209 |
u'name="%(name)s"%(checked)s />') |
|---|
| 210 |
else: |
|---|
| 211 |
self.templateText = (u'<input type="checkbox" name="%(name)s" ' |
|---|
| 212 |
u'value=' + quote(value) + '%(checked)s />') |
|---|
| 213 |
|
|---|
| 214 |
def assemble_all(self, name, checked): |
|---|
| 215 |
chk = u'' |
|---|
| 216 |
if checked: |
|---|
| 217 |
chk = u' checked="checked" ' |
|---|
| 218 |
return self.assemble({u'name': name, u'checked': chk}) |
|---|
| 219 |
|
|---|
| 220 |
|
|---|
| 221 |
class HiddenElement(Template): |
|---|
| 222 |
"""A Template for generating HTML <input type='hidden'> elements |
|---|
| 223 |
|
|---|
| 224 |
Usage: |
|---|
| 225 |
output = cobbler.HiddenElement().assemble_all(key) |
|---|
| 226 |
""" |
|---|
| 227 |
|
|---|
| 228 |
templateText = u'<input type="hidden" name=%(name)s value=%(value)s />' |
|---|
| 229 |
|
|---|
| 230 |
def assemble_all(self, name, value): |
|---|
| 231 |
return self.assemble({u'name': quote(name), u'value': quote(value)}) |
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 |
class TableRows(Template): |
|---|
| 235 |
"""Template for generating HTML <tr><td></td><td></td></tr> elements.""" |
|---|
| 236 |
|
|---|
| 237 |
def __init__(self, trClass=None): |
|---|
| 238 |
self.exhausted = False |
|---|
| 239 |
self.trClass = trClass |
|---|
| 240 |
self.rows = [] |
|---|
| 241 |
|
|---|
| 242 |
def add_row(self, *args): |
|---|
| 243 |
self.rows.append(args) |
|---|
| 244 |
|
|---|
| 245 |
def assemble(self, final_rows=[]): |
|---|
| 246 |
if self.trClass is None: |
|---|
| 247 |
tr = u'<tr>' |
|---|
| 248 |
else: |
|---|
| 249 |
tr = u'<tr class=' + quote(self.trClass) + u'>' |
|---|
| 250 |
|
|---|
| 251 |
output = [] |
|---|
| 252 |
for row in (self.rows + final_rows): |
|---|
| 253 |
output.append(tr) |
|---|
| 254 |
for col in row: |
|---|
| 255 |
output.append(u" <td>%s</td>" % col) |
|---|
| 256 |
output.append("</tr>") |
|---|
| 257 |
return u'\n'.join(output) |
|---|
| 258 |
|
|---|