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
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
1"""hods - home directory synchronization.
3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de>
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.
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.
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
22def load_environment_context():
23 """Load template context for the current environment."""
24 import platform
25 from hods.utils import get_hostname, pw_home
27 yield 'platform', platform
28 yield 'hostname', get_hostname()
30 data = os.environ.copy()
31 data['HOME'] = pw_home()
32 yield from data.items()
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()
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}
53TEMPLATE_ENGINE_REGISTRY = {}
56def get_template_engine(identifier):
57 """Get registered template engine class for the given identifier.
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
68class AlreadyRegisteredTemplateEngine(Exception):
69 """Exception raised when registering an identifier that already exists."""
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))
78class TemplateError(Exception):
79 """Base exception class for all user template (and engine) errors.
81 The `sourcefile` attribute is set in `TemplateEngine.render`.
82 """
84 def __init__(self, message):
85 """Initialize error."""
86 self.sourcefile = None
87 super().__init__(message)
90class UnknownTemplateEngine(TemplateError):
91 """Exception raised when requesting a non-existent template engine class.
93 All valid identifiers are stored in `TEMPLATE_ENGINE_REGISTRY`.
94 """
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))
102class TemplateEngineNotInstalled(TemplateError):
103 """Exception raised when registering an identifier that already exists."""
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))
111class TemplateEngineError(TemplateError):
112 """Exception class for all user template engine errors.
114 The `engine` attribute is set in `TemplateEngine.render`.
115 """
117 def __init__(self, message):
118 """Initialize error."""
119 self.engine = None
120 super().__init__(message)
123class TemplateRenderError(TemplateEngineError):
124 """Exception raised when rendering a template fails."""
126 def __init__(self, message, line_number=None):
127 """Initialize error."""
128 self.message = message
129 self.line_number = line_number
131 self.template = None
132 self.context = None
134 super().__init__(message)
137class TemplateEngine:
138 """Base class for all template engines."""
140 label = NotImplemented
141 engine_id = NotImplemented
143 description = ''
145 def __init__(self):
146 """Initialize template engine."""
147 if not self.is_installed():
148 raise TemplateEngineNotInstalled(self.__class__)
150 @classmethod
151 def is_installed(cls):
152 """Overwrite to check whether the template engine is available."""
153 raise NotImplementedError
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
162 def render(self, template, context):
163 """Render the given template.
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())
176 err.engine = self
177 err.template = template
178 err.context = context
179 raise err from ex
181 def render_error(self, message, **kwargs):
182 """Shortcut to raise a `TemplateRenderError`."""
183 raise TemplateRenderError(message, **kwargs)
185 def _render(self, template, context):
186 """Render the template string using the given context from `get_context`.
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
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
206class Jinja(TemplateEngine):
207 """The jinja2 template engine."""
209 label = 'jinja2'
210 engine_id = 'jinja'
211 description = '{% if var %}{{ var }}{% endif %}'
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
222 def _render(self, template, context):
223 from jinja2 import StrictUndefined, Template, TemplateSyntaxError
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)
233Jinja.register()
236class Mako(TemplateEngine):
237 """The mako template engine."""
239 label = 'mako'
240 engine_id = 'mako'
241 description = '<% if var %>${var}<% endif %>'
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
252 def _render(self, template, context):
253 from mako.template import Template
254 from mako.exceptions import text_error_template
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())
263Mako.register()