[062892]: / Allura / allura / lib / package_path_loader.py  Maximize  Restore  History

Download this file

285 lines (227 with data), 10.2 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
A Jinja template loader which allows for:
- dotted-notation package loading
- search-path-based overriding of same
Dotted notation
---------------
- Allow a Tool implementer to use a dotted-notation module name
(as occuring in the ``PYTHONPATH``), then the given path within the
module::
@expose('jinja:<module.name>:<path/within/module.html>')
e.g.::
@expose('jinja:allura:templates/repo/file.html')
Overriding dotted notation
--------------------------
Allow a Tool implementer to override the theme baseline (or any
other Tool's) templates. This can be lighter-weight than subclassing
:class:`allura.plugin.ThemeProvider`, plus will allow for more fine-grained
changes.
This will also override ``extends`` and ``import`` Jinja tags.
This approach uses a:
- ``setup.py`` entry point to a class with...
- *magic* files and...
- (optionally) a class property to specify ordering
File Structure for Overriding dotted notation
=============================================
For the examples, assume the following directory structure::
NewTool/
|- setup.py <- entry point specified here
|- newtool/
|- app.py <- entry point target here
|- templates/
| |- index.html <- Tool's regular templates
|- override <- override_root
|- allura/ <- magic directory named after module
|- templates/
|- repo/
|- file.html <- actual template
To override the above example, a Tool implementer would
add the following line to their Tool's ``setup.py`` (assuming usage in Allura,
with the default ``app_cfg``)::
[allura.theme.override]
newtool = newtool.app:NewToolApp
Then, in the neighbor path (see below) for the file containing the
Tool class, add the following path/file::
override/allura/templates/repo/file.html
The template will be overridden. Note that after changing
``setup.py``, it would be required to re-initialize with setuptools::
python setup.py develop
Specifying search path order with template_path_rules
=====================================================
If a highly specific ordering is required, such as if multiple Tools
are trying to override the same template, the entry point target
class can also contain a class property template_path_rules::
class NewToolApp(Application):
template_path_rules = [
['>', 'old-tool'],
]
Each rule specifies a postioner and an entry point or "signpost".
If no rule is provided, the default is ``['>', 'allura']``.
The "signposts" are:
- Any other app's override entry point name
- ``site-theme``
- ``allura`` (you probably shouldn't do this)
- ``project-theme`` **NOT IMPLEMENTED**
- ``tool-theme`` **NOT IMPLEMENTED**
The positioners are:
>
This overrider will be found BEFORE the specified entry point
<
This overrider will be found AFTER the specified entry point
=
This will replace one of the "signpost" entry points (if multiple apps
try to do this for the same signpost, the result is undefined)
**TODO:** Support multiple partial themes
"""
import pkg_resources
import os
import jinja2
from tg import config
from paste.deploy.converters import asbool
from ming.utils import LazyProperty
from allura.lib.helpers import topological_sort, iter_entry_points
class PackagePathLoader(jinja2.BaseLoader):
def __init__(self, override_entrypoint='allura.theme.override',
default_paths=None,
override_root='override',
):
'''
Set up initial values... defaults are for Allura.
'''
# TODO: How does one handle project-theme?
if default_paths is None:
default_paths = [
#['project-theme', None],
['site-theme', None],
['allura', '/'],
]
self.override_entrypoint = override_entrypoint
self.default_paths = default_paths
self.override_root = override_root
@LazyProperty
def fs_loader(self):
return jinja2.FileSystemLoader(self.init_paths())
def _load_paths(self):
"""
Load all the paths to be processed, including defaults, in the default order.
"""
paths = self.default_paths[:] # copy default_paths
paths[-1:0] = [ # insert all eps just before last item, by default
[ep.name, pkg_resources.resource_filename(ep.module_name, "")]
for ep in iter_entry_points(self.override_entrypoint)
]
return paths
def _load_rules(self):
"""
Load and pre-process the rules from the entry points.
Rules are specified per-tool as a list of the form:
template_path_rules = [
['>', 'tool1'], # this tool must be resolved before tool1
['<', 'tool2'], # this tool must be resolved after tool2
['=', 'tool3'], # this tool replaces all of tool3's templates
]
Returns two lists of rules, order_rules and replacement_rules.
order_rules represents all of the '>' and '<' rules and are returned
as a list of pairs of the form ('a', 'b') indicating that path 'a' must
come before path 'b'.
replacement_rules represent all of the '=' rules and are returned as
a dictionary mapping the paths to replace to the paths to replace with.
"""
order_rules = []
replacement_rules = {}
for ep in iter_entry_points(self.override_entrypoint):
for rule in getattr(ep.load(), 'template_path_rules', []):
if rule[0] == '>':
order_rules.append((ep.name, rule[1]))
elif rule[0] == '=':
replacement_rules[rule[1]] = ep.name
elif rule[0] == '<':
order_rules.append((rule[1], ep.name))
else:
raise jinja2.TemplateError(
'Unknown template path rule in {}: {}'.format(
ep.name, ' '.join(rule)))
return order_rules, replacement_rules
def _sort_paths(self, paths, rules):
"""
Process all '>' and '<' rules, providing a partial ordering
of the paths based on the given rules.
The rules should already have been pre-processed by _load_rules
to a list of partial ordering pairs ('a', 'b') indicating that
path 'a' should come before path 'b'.
"""
names = [p[0] for p in paths]
# filter rules that reference non-existent paths to prevent "loops" in
# the graph
rules = [r for r in rules if r[0] in names and r[1] in names]
ordered_paths = topological_sort(names, rules)
if ordered_paths is None:
raise jinja2.TemplateError(
'Loop detected in ordering of overrides')
return paths.sort(key=lambda p: ordered_paths.index(p[0]))
def _replace_signposts(self, paths, rules):
"""
Process all '=' rules, replacing the rule target's path value with
the rule's entry's path value.
Multiple entries replacing the same signpost can cause indeterminate
behavior, as the order of the entries is not entirely defined.
However, if _sort_by_rules is called first, the partial ordering is
respected.
This mutates paths.
"""
p_idx = lambda n: [e[0] for e in paths].index(n)
for target, replacement in rules.items():
try:
removed = paths.pop(p_idx(replacement))
paths[p_idx(target)][1] = removed[1]
except ValueError:
# target or replacement missing (may not be installed)
pass
def init_paths(self):
'''
Set up the setuptools entry point-based paths.
'''
paths = self._load_paths()
order_rules, repl_rules = self._load_rules()
self._sort_paths(paths, order_rules)
self._replace_signposts(paths, repl_rules)
return [p[1] for p in paths if p[1] is not None]
def get_source(self, environment, template):
'''
Returns the source for jinja2 rendered templates. Can understand...
- path/to/template.html
- module:path/to/template.html
'''
# look in all of the customized search locations...
if not asbool(config.get('disable_template_overrides', False)):
try:
parts = [self.override_root] + template.split(':')
if len(parts) > 2:
parts[1:2] = parts[1].split('.')
return self.fs_loader.get_source(environment,
os.path.join(*parts))
except jinja2.TemplateNotFound:
# fall-back to attempt non-override loading
pass
if ':' in template:
package, path = template.split(':', 2)
filename = pkg_resources.resource_filename(package, path)
return self.fs_loader.get_source(environment, filename)
else:
return self.fs_loader.get_source(environment, template)