Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from __future__ import with_statement 

2 

3import os 

4import sys 

5import hashlib 

6import logging 

7import tempfile 

8import inspect 

9 

10try: 

11 RecursionError 

12except NameError: 

13 RecursionError = RuntimeError 

14 

15def get_package_versions(): 

16 try: 

17 import pkg_resources 

18 except ImportError: 

19 logging.info("Setuptools not installed. Unable to determine version.") 

20 return [] 

21 

22 versions = dict() 

23 for path in sys.path: 

24 for distribution in pkg_resources.find_distributions(path): 

25 if distribution.has_version(): 

26 versions.setdefault( 

27 distribution.project_name, 

28 distribution.version, 

29 ) 

30 

31 return sorted(versions.items()) 

32 

33 

34pkg_digest = hashlib.sha1(__name__.encode('utf-8')) 

35for name, version in get_package_versions(): 

36 pkg_digest.update(name.encode('utf-8')) 

37 pkg_digest.update(version.encode('utf-8')) 

38 

39 

40from .exc import RenderError 

41from .exc import TemplateError 

42from .exc import ExceptionFormatter 

43from .compiler import Compiler 

44from .config import DEBUG_MODE 

45from .config import AUTO_RELOAD 

46from .config import EAGER_PARSING 

47from .config import CACHE_DIRECTORY 

48from .loader import ModuleLoader 

49from .loader import MemoryLoader 

50from .nodes import Module 

51from .utils import DebuggingOutputStream 

52from .utils import Scope 

53from .utils import join 

54from .utils import mangle 

55from .utils import create_formatted_exception 

56from .utils import read_bytes 

57from .utils import raise_with_traceback 

58from .utils import byte_string 

59from .utils import value_repr 

60 

61 

62log = logging.getLogger('chameleon.template') 

63 

64 

65def _make_module_loader(): 

66 remove = False 

67 if CACHE_DIRECTORY: 

68 path = CACHE_DIRECTORY 

69 else: 

70 path = tempfile.mkdtemp() 

71 remove = True 

72 

73 return ModuleLoader(path, remove) 

74 

75 

76class BaseTemplate(object): 

77 """Template base class. 

78 

79 Takes a string input which must be one of the following: 

80 

81 - a unicode string (or string on Python 3); 

82 - a utf-8 encoded byte string; 

83 - a byte string for an XML document that defines an encoding 

84 in the document premamble; 

85 - an HTML document that specifies the encoding via the META tag. 

86 

87 Note that the template input is decoded, parsed and compiled on 

88 initialization. 

89 """ 

90 

91 default_encoding = "utf-8" 

92 

93 # This attribute is strictly informational in this template class 

94 # and is used in exception formatting. It may be set on 

95 # initialization using the optional ``filename`` keyword argument. 

96 filename = '<string>' 

97 

98 _cooked = False 

99 

100 if DEBUG_MODE or CACHE_DIRECTORY: 

101 loader = _make_module_loader() 

102 else: 

103 loader = MemoryLoader() 

104 

105 if DEBUG_MODE: 

106 output_stream_factory = DebuggingOutputStream 

107 else: 

108 output_stream_factory = list 

109 

110 debug = DEBUG_MODE 

111 

112 # The ``builtins`` dictionary can be used by a template class to 

113 # add symbols which may not be redefined and which are (cheaply) 

114 # available in the template variable scope 

115 builtins = {} 

116 

117 # The ``builtins`` dictionary is updated with this dictionary at 

118 # cook time. Note that it can be provided at class initialization 

119 # using the ``extra_builtins`` keyword argument. 

120 extra_builtins = {} 

121 

122 # Expression engine must be provided by subclass 

123 engine = None 

124 

125 # When ``strict`` is set, expressions must be valid at compile 

126 # time. When not set, this is only required at evaluation time. 

127 strict = True 

128 

129 # This should return a value string representation for exception 

130 # formatting. 

131 value_repr = staticmethod(value_repr) 

132 

133 def __init__(self, body=None, **config): 

134 self.__dict__.update(config) 

135 

136 if body is not None: 

137 self.write(body) 

138 

139 # This is only necessary if the ``debug`` flag was passed as a 

140 # keyword argument 

141 if self.__dict__.get('debug') is True: 

142 self.loader = _make_module_loader() 

143 

144 def __call__(self, **kwargs): 

145 return self.render(**kwargs) 

146 

147 def __repr__(self): 

148 return "<%s %s>" % (self.__class__.__name__, self.filename) 

149 

150 @property 

151 def keep_body(self): 

152 # By default, we only save the template body if we're 

153 # in debugging mode (to save memory). 

154 return self.__dict__.get('keep_body', DEBUG_MODE) 

155 

156 @property 

157 def keep_source(self): 

158 # By default, we only save the generated source code if we're 

159 # in debugging mode (to save memory). 

160 return self.__dict__.get('keep_source', DEBUG_MODE) 

161 

162 def cook(self, body): 

163 builtins_dict = self.builtins.copy() 

164 builtins_dict.update(self.extra_builtins) 

165 names, builtins = zip(*sorted(builtins_dict.items())) 

166 digest = self.digest(body, names) 

167 program = self._cook(body, digest, names) 

168 

169 initialize = program['initialize'] 

170 functions = initialize(*builtins) 

171 

172 for name, function in functions.items(): 

173 setattr(self, "_" + name, function) 

174 

175 self._cooked = True 

176 

177 if self.keep_body: 

178 self.body = body 

179 

180 def cook_check(self): 

181 assert self._cooked 

182 

183 def parse(self, body): 

184 raise NotImplementedError("Must be implemented by subclass.") 

185 

186 def render(self, **__kw): 

187 econtext = Scope(__kw) 

188 rcontext = {} 

189 self.cook_check() 

190 stream = self.output_stream_factory() 

191 try: 

192 self._render(stream, econtext, rcontext) 

193 except RecursionError: 

194 raise 

195 except: 

196 cls, exc, tb = sys.exc_info() 

197 errors = rcontext.get('__error__') 

198 if errors: 

199 formatter = exc.__str__ 

200 if isinstance(formatter, ExceptionFormatter): 

201 if errors is not formatter._errors: 

202 formatter._errors.extend(errors) 

203 raise 

204 

205 formatter = ExceptionFormatter(errors, econtext, rcontext, self.value_repr) 

206 

207 try: 

208 exc = create_formatted_exception( 

209 exc, cls, formatter, RenderError 

210 ) 

211 except TypeError: 

212 pass 

213 

214 raise_with_traceback(exc, tb) 

215 

216 raise 

217 

218 return join(stream) 

219 

220 def write(self, body): 

221 if isinstance(body, byte_string): 

222 body, encoding, content_type = read_bytes( 

223 body, self.default_encoding 

224 ) 

225 else: 

226 content_type = body.startswith('<?xml') 

227 encoding = None 

228 

229 self.content_type = content_type 

230 self.content_encoding = encoding 

231 

232 self.cook(body) 

233 

234 def _get_module_name(self, name): 

235 return "%s.py" % name 

236 

237 def _cook(self, body, name, builtins): 

238 filename = self._get_module_name(name) 

239 cooked = self.loader.get(filename) 

240 if cooked is None: 

241 try: 

242 source = self._compile(body, builtins) 

243 if self.debug: 

244 source = "# template: %s\n#\n%s" % ( 

245 self.filename, source) 

246 if self.keep_source: 

247 self.source = source 

248 cooked = self.loader.build(source, filename) 

249 except TemplateError: 

250 exc = sys.exc_info()[1] 

251 exc.token.filename = self.filename 

252 raise 

253 elif self.keep_source: 

254 module = sys.modules.get(cooked.get('__name__')) 

255 if module is not None: 

256 self.source = inspect.getsource(module) 

257 else: 

258 self.source = None 

259 return cooked 

260 

261 def digest(self, body, names): 

262 class_name = type(self).__name__.encode('utf-8') 

263 sha = pkg_digest.copy() 

264 sha.update(body.encode('utf-8', 'ignore')) 

265 sha.update(class_name) 

266 digest = sha.hexdigest() 

267 

268 if self.filename is not BaseTemplate.filename: 

269 digest = os.path.splitext(self.filename)[0] + '-' + digest 

270 

271 return digest 

272 

273 def _compile(self, body, builtins): 

274 program = self.parse(body) 

275 module = Module("initialize", program) 

276 compiler = Compiler( 

277 self.engine, module, self.filename, body, 

278 builtins, strict=self.strict 

279 ) 

280 return compiler.code 

281 

282 

283class BaseTemplateFile(BaseTemplate): 

284 """File-based template base class. 

285 

286 Relative path names are supported only when a template loader is 

287 provided as the ``loader`` parameter. 

288 """ 

289 

290 # Auto reload is not enabled by default because it's a significant 

291 # performance hit 

292 auto_reload = AUTO_RELOAD 

293 

294 def __init__(self, filename, auto_reload=None, **config): 

295 # Normalize filename 

296 filename = os.path.abspath( 

297 os.path.normpath(os.path.expanduser(filename)) 

298 ) 

299 

300 self.filename = filename 

301 

302 # Override reload setting only if value is provided explicitly 

303 if auto_reload is not None: 

304 self.auto_reload = auto_reload 

305 

306 super(BaseTemplateFile, self).__init__(**config) 

307 

308 if EAGER_PARSING: 

309 self.cook_check() 

310 

311 def cook_check(self): 

312 if self.auto_reload: 

313 mtime = self.mtime() 

314 

315 if mtime != self._v_last_read: 

316 self._v_last_read = mtime 

317 self._cooked = False 

318 

319 if self._cooked is False: 

320 body = self.read() 

321 log.debug("cooking %r (%d bytes)..." % (self.filename, len(body))) 

322 self.cook(body) 

323 

324 def mtime(self): 

325 try: 

326 return os.path.getmtime(self.filename) 

327 except (IOError, OSError): 

328 return 0 

329 

330 def read(self): 

331 with open(self.filename, "rb") as f: 

332 data = f.read() 

333 

334 body, encoding, content_type = read_bytes( 

335 data, self.default_encoding 

336 ) 

337 

338 # In non-XML mode, we support various platform-specific line 

339 # endings and convert them to the UNIX newline character 

340 if content_type != "text/xml" and '\r' in body: 

341 body = body.replace('\r\n', '\n').replace('\r', '\n') 

342 

343 self.content_type = content_type 

344 self.content_encoding = encoding 

345 

346 return body 

347 

348 def _get_module_name(self, name): 

349 filename = os.path.basename(self.filename) 

350 mangled = mangle(filename) 

351 return "%s_%s.py" % (mangled, name) 

352 

353 def _get_filename(self): 

354 return self.__dict__.get('filename') 

355 

356 def _set_filename(self, filename): 

357 self.__dict__['filename'] = filename 

358 self._v_last_read = None 

359 self._cooked = False 

360 

361 filename = property(_get_filename, _set_filename)