Coverage for src/hods/config/template.py: 100.00%

Shortcuts 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

131 statements  

1"""hods - home directory synchronization. 

2 

3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de> 

4 

5hods is free software: you can redistribute it and/or modify 

6it under the terms of the GNU General Public License as published by 

7the Free Software Foundation, either version 3 of the License, or 

8(at your option) any later version. 

9 

10hods is distributed in the hope that it will be useful, 

11but WITHOUT ANY WARRANTY; without even the implied warranty of 

12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13GNU General Public License for more details. 

14 

15You should have received a copy of the GNU General Public License 

16along with this program. If not, see <http://www.gnu.org/licenses/>. 

17""" 

18import os 

19import traceback 

20 

21 

22def load_environment_context(): 

23 """Load template context for the current environment.""" 

24 import platform 

25 from hods.utils import get_hostname, pw_home 

26 

27 yield 'platform', platform 

28 yield 'hostname', get_hostname() 

29 

30 data = os.environ.copy() 

31 data['HOME'] = pw_home() 

32 yield from data.items() 

33 

34 

35def load_settings_context(settings): 

36 """Load template context for the given `~hods.config.base.Settings` instance.""" 

37 yield 'version', settings.version 

38 yield 'server', settings.server 

39 yield 'is_proxy', settings.is_proxy 

40 yield 'is_server', settings.is_server 

41 yield 'server_is_proxy', settings.server_is_proxy 

42 yield 'cascade', settings.cascade 

43 yield from settings.variables.items() 

44 

45 

46def load_context(tree): 

47 """Load template context for the given `~hods.config.tree.SourceTree`.""" 

48 yield from load_environment_context() 

49 yield from load_settings_context(tree.settings) 

50 yield 'features', {f.name: f for f in tree.features} 

51 

52 

53TEMPLATE_ENGINE_REGISTRY = {} 

54 

55 

56def get_template_engine(identifier): 

57 """Get registered template engine class for the given identifier. 

58 

59 Raises: 

60 UnknownTemplateEngine: If given identifier is not registered. 

61 """ 

62 try: 

63 return TEMPLATE_ENGINE_REGISTRY[identifier] 

64 except KeyError as e: 

65 raise UnknownTemplateEngine(identifier) from e 

66 

67 

68class AlreadyRegisteredTemplateEngine(Exception): 

69 """Exception raised when registering an identifier that already exists.""" 

70 

71 def __init__(self, engine): 

72 """Initialize error.""" 

73 self.engine = engine 

74 super().__init__('Failed to register {}. Template engine with identifier "{}" already exists.'.format( 

75 engine.__name__, engine.engine_id)) 

76 

77 

78class TemplateError(Exception): 

79 """Base exception class for all user template (and engine) errors. 

80 

81 The `sourcefile` attribute is set in `TemplateEngine.render`. 

82 """ 

83 

84 def __init__(self, message): 

85 """Initialize error.""" 

86 self.sourcefile = None 

87 super().__init__(message) 

88 

89 

90class UnknownTemplateEngine(TemplateError): 

91 """Exception raised when requesting a non-existent template engine class. 

92 

93 All valid identifiers are stored in `TEMPLATE_ENGINE_REGISTRY`. 

94 """ 

95 

96 def __init__(self, engine_id): 

97 """Initialize error.""" 

98 self.engine_id = engine_id 

99 super().__init__('No template engine with identifier "{}" found'.format(engine_id)) 

100 

101 

102class TemplateEngineNotInstalled(TemplateError): 

103 """Exception raised when registering an identifier that already exists.""" 

104 

105 def __init__(self, engine_class): 

106 """Initialize error.""" 

107 self.engine_class = engine_class 

108 super().__init__('Template engine "{}" is not installed!'.format(engine_class.label)) 

109 

110 

111class TemplateEngineError(TemplateError): 

112 """Exception class for all user template engine errors. 

113 

114 The `engine` attribute is set in `TemplateEngine.render`. 

115 """ 

116 

117 def __init__(self, message): 

118 """Initialize error.""" 

119 self.engine = None 

120 super().__init__(message) 

121 

122 

123class TemplateRenderError(TemplateEngineError): 

124 """Exception raised when rendering a template fails.""" 

125 

126 def __init__(self, message, line_number=None): 

127 """Initialize error.""" 

128 self.message = message 

129 self.line_number = line_number 

130 

131 self.template = None 

132 self.context = None 

133 

134 super().__init__(message) 

135 

136 

137class TemplateEngine: 

138 """Base class for all template engines.""" 

139 

140 label = NotImplemented 

141 engine_id = NotImplemented 

142 

143 description = '' 

144 

145 def __init__(self): 

146 """Initialize template engine.""" 

147 if not self.is_installed(): 

148 raise TemplateEngineNotInstalled(self.__class__) 

149 

150 @classmethod 

151 def is_installed(cls): 

152 """Overwrite to check whether the template engine is available.""" 

153 raise NotImplementedError 

154 

155 @classmethod 

156 def register(cls): 

157 """Register this template engine class.""" 

158 if cls.engine_id in TEMPLATE_ENGINE_REGISTRY: 

159 raise AlreadyRegisteredTemplateEngine(cls) 

160 TEMPLATE_ENGINE_REGISTRY[cls.engine_id] = cls 

161 

162 def render(self, template, context): 

163 """Render the given template. 

164 

165 Catch *any* exception that is raised and re-raise it 

166 as TemplateRenderError. All subclasses should catch engine related 

167 exceptions and do the same! 

168 """ 

169 try: 

170 return self._render(template, context) 

171 except TemplateRenderError as e: 

172 ex, err = None, e 

173 except BaseException as e: 

174 ex, err = e, TemplateRenderError(traceback.format_exc()) 

175 

176 err.engine = self 

177 err.template = template 

178 err.context = context 

179 raise err from ex 

180 

181 def render_error(self, message, **kwargs): 

182 """Shortcut to raise a `TemplateRenderError`.""" 

183 raise TemplateRenderError(message, **kwargs) 

184 

185 def _render(self, template, context): 

186 """Render the template string using the given context from `get_context`. 

187 

188 Overwrite this method in subclasses. Any exception is caught 

189 by the `render` wrapper. Catch custom exceptions yourself and 

190 call `error` to provide more descriptive error messages. 

191 """ 

192 raise NotImplementedError 

193 

194 

195def find_string(value, text): 

196 """Search in a multiline text and return first occurrence coordinates.""" 

197 for line_index, line in enumerate(text.splitlines()): 

198 try: 

199 index = line.index(value) 

200 except ValueError: 

201 continue 

202 return line_index + 1, index + 1 

203 return None, None 

204 

205 

206class Jinja(TemplateEngine): 

207 """The jinja2 template engine.""" 

208 

209 label = 'jinja2' 

210 engine_id = 'jinja' 

211 description = '{% if var %}{{ var }}{% endif %}' 

212 

213 @classmethod 

214 def is_installed(cls): 

215 """Check whether `jinja2` can be imported.""" 

216 try: 

217 from jinja2 import Template # noqa: F401 

218 except ImportError: 

219 return False 

220 return True 

221 

222 def _render(self, template, context): 

223 from jinja2 import StrictUndefined, Template, TemplateSyntaxError 

224 

225 try: 

226 t = Template(template, undefined=StrictUndefined) 

227 return t.render(context) 

228 except TemplateSyntaxError as e: 

229 e.translated = False 

230 self.render_error(e) 

231 

232 

233Jinja.register() 

234 

235 

236class Mako(TemplateEngine): 

237 """The mako template engine.""" 

238 

239 label = 'mako' 

240 engine_id = 'mako' 

241 description = '<% if var %>${var}<% endif %>' 

242 

243 @classmethod 

244 def is_installed(cls): 

245 """Check whether `mako` can be imported.""" 

246 try: 

247 from mako.template import Template # noqa: F401 

248 except ImportError: 

249 return False 

250 return True 

251 

252 def _render(self, template, context): 

253 from mako.template import Template 

254 from mako.exceptions import text_error_template 

255 

256 try: 

257 t = Template(template, strict_undefined=True) 

258 return t.render(**context) 

259 except: # let mako raise *anything* # noqa: E722 

260 self.render_error(text_error_template().render()) 

261 

262 

263Mako.register()