-
Notifications
You must be signed in to change notification settings - Fork 57
/
__init__.py
495 lines (382 loc) · 16.4 KB
/
__init__.py
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
import inspect
import os
import posixpath
import sys
from jinja2 import Environment as _Jinja2Environment
from jinja2.exceptions import TemplateNotFound
from jinja2.loaders import FileSystemLoader
from jinja2.utils import open_if_exists
from pyramid.asset import abspath_from_asset_spec
from pyramid.path import DottedNameResolver
from zope.deprecation import deprecated
from zope.interface import Interface
from .compat import text_type
from .settings import (
parse_env_options_from_settings,
parse_loader_options_from_settings,
parse_multiline,
)
ENV_CONFIG_PHASE = 0
EXTRAS_CONFIG_PHASE = 1
PARENT_RELATIVE_DELIM = '@@FROM_PARENT@@'
class IJinja2Environment(Interface):
pass
class Environment(_Jinja2Environment):
def join_path(self, uri, parent):
if os.path.isabs(uri) or ':' in uri:
# we have an asset spec or absolute path
return uri
# uri may be relative to the parent, shuffle it through to the loader
return uri + PARENT_RELATIVE_DELIM + parent
class FileInfo(object):
open_if_exists = staticmethod(open_if_exists)
getmtime = staticmethod(os.path.getmtime)
def __init__(self, filename, encoding='utf-8'):
self.filename = filename
self.encoding = encoding
def _delay_init(self):
if '_mtime' in self.__dict__:
return
f = self.open_if_exists(self.filename)
if f is None:
raise TemplateNotFound(self.filename)
self._mtime = self.getmtime(self.filename)
data = ''
try:
data = f.read()
finally:
f.close()
if not isinstance(data, text_type):
data = data.decode(self.encoding)
self._contents = data
@property
def contents(self):
self._delay_init()
return self._contents
@property
def mtime(self):
self._delay_init()
return self._mtime
def uptodate(self):
try:
return os.path.getmtime(self.filename) == self.mtime
except OSError:
return False
class _PackageFinder(object):
inspect = staticmethod(inspect)
def caller_package(self, excludes=()):
"""A list of excluded patterns, optionally containing a `.` suffix.
For example, ``'pyramid.'`` would exclude exclude ``'pyramid.config'``
but not ``'pyramid'``.
"""
f = None
for t in self.inspect.stack():
f = t[0]
name = f.f_globals.get('__name__')
if name:
excluded = False
for pattern in excludes:
if pattern[-1] == '.' and name.startswith(pattern):
excluded = True
break
elif name == pattern:
excluded = True
break
if not excluded:
break
if f is None:
return None
pname = f.f_globals.get('__name__') or '__main__'
m = sys.modules[pname]
f = getattr(m, '__file__', '')
if (('__init__.py' in f) or ('__init__$py' in f)): # empty at >>>
return m
pname = m.__name__.rsplit('.', 1)[0]
return sys.modules[pname]
_caller_package = _PackageFinder().caller_package
class SmartAssetSpecLoader(FileSystemLoader):
'''A Jinja2 template loader that knows how to handle
asset specifications.
'''
def __init__(self, searchpath=(), encoding='utf-8', debug=False):
FileSystemLoader.__init__(self, searchpath, encoding)
self.debug = debug
def list_templates(self):
raise TypeError('this loader cannot iterate over all templates')
def _get_absolute_source(self, template):
filename = abspath_from_asset_spec(template)
fi = FileInfo(filename, self.encoding)
if os.path.isfile(fi.filename):
return fi.contents, fi.filename, fi.uptodate
def _relative_searchpath(self, chain):
""" Combine paths in the chain to construct search paths.
The precedence is for the most-specific paths to be tested first,
anchored at an absolute path or asset spec. From there, less-specific
paths are tested.
For example::
chain = [
'../forms.jinja2', 'sub/nav.jinja2',
'base.jinja2', 'myapp:templates/index.jinja2',
]
searchpath = ['myapp:templates/sub/..', 'sub/..', '..', '']
"""
# the initial empty string is important because not only does it allow
# the stack to always contain something join, but it allows the
# later for-loops to fallback to the original search path by
# joining to an empty string since os.path.join('', 'foo') == 'foo'
stack = ['']
for path in chain:
is_abspath = os.path.isabs(path)
is_spec = not is_abspath and ':' in path
if not is_abspath and is_spec:
ppkg, ppath = path.split(':', 1)
path = '{0}:{1}'.format(ppkg, posixpath.dirname(ppath))
else:
# this should split windows and posix paths
path = os.path.dirname(path)
if not path:
# skip empty directories
continue
subpath = stack[-1]
path = posixpath.join(path, subpath)
stack.append(path)
# do not continue further, all paths are relative to this
if is_abspath or is_spec:
break
return list(reversed(stack))
def get_source(self, environment, template):
# keep legacy asset: prefix checking that bypasses
# source path checking altogether
if template.startswith('asset:'):
template = template[6:]
# split the template into the chain of relative-imports
rel_chain = template.split(PARENT_RELATIVE_DELIM)
template, rel_chain = rel_chain[0], rel_chain[1:]
# load the template directly if it's an absolute path or asset spec
if os.path.isabs(template) or ':' in template:
src = self._get_absolute_source(template)
if src is not None:
return src
else:
# fallback to the search path just incase
return FileSystemLoader.get_source(self, environment, template)
# try to import the template as an asset spec or absolute path
# relative to its parents
rel_searchpath = self._relative_searchpath(rel_chain)
for parent in rel_searchpath:
if os.path.isabs(parent):
uri = os.path.join(parent, template)
# avoid recursive includes
if uri not in rel_chain:
src = self._get_absolute_source(uri)
if src is not None:
return src
# avoid doing "':' in" and then redundant "split"
parts = parent.split(':', 1)
if len(parts) > 1:
# parent is an asset spec
ppkg, ppath = parts
ppath = posixpath.join(ppath, template)
uri = '{0}:{1}'.format(ppkg, ppath)
# avoid recursive includes
if uri not in rel_chain:
src = self._get_absolute_source(uri)
if src is not None:
return src
# try to load the template from the default search path
for parent in rel_searchpath:
try:
uri = os.path.join(parent, template)
# avoid recursive includes
if uri not in rel_chain:
return FileSystemLoader.get_source(self, environment, uri)
except TemplateNotFound:
pass
# we're here because of an exception during the last step so extend
# the message and raise an appropriate error
# there should always be an exception because the rel_searchpath is
# guaranteed to contain at least one element ('')
searchpath = [p for p in rel_searchpath if p] + self.searchpath
message = '{0}; searchpath={1}'.format(template, searchpath)
raise TemplateNotFound(name=template, message=message)
class Jinja2TemplateRenderer(object):
'''Renderer for a jinja2 template'''
def __init__(self, template_loader):
self.template_loader = template_loader
def __call__(self, value, system):
try:
system.update(value)
except (TypeError, ValueError) as ex:
raise ValueError('renderer was passed non-dictionary '
'as value: %s' % str(ex))
template = self.template_loader()
return template.render(system)
class Jinja2RendererFactory(object):
environment = None
def __call__(self, info):
name, package = info.name, info.package
def template_loader():
# attempt to turn the name into a caller-relative asset spec
if ':' not in name and package is not None:
try:
name_with_package = '%s:%s' % (package.__name__, name)
return self.environment.get_template(name_with_package)
except TemplateNotFound:
pass
return self.environment.get_template(name)
return Jinja2TemplateRenderer(template_loader)
def renderer_factory(info):
registry = info.registry
env = registry.queryUtility(IJinja2Environment, name='.jinja2')
if env is None:
raise ValueError(
'As of pyramid_jinja2 2.3, the use of the '
'"pyramid_jinja2.renderer_factory" requires that pyramid_jinja2 '
'be configured via config.include("pyramid_jinja2") or the '
'equivalent "pyramid.includes" setting.')
factory = Jinja2RendererFactory()
factory.environment = env
return factory(info)
deprecated(
'renderer_factory',
'The pyramid_jinja2.renderer_factory was deprecated in version 2.0 and '
'will be removed in the future. You should upgrade to the newer '
'config.add_jinja2_renderer() API.')
def add_jinja2_search_path(config, searchpath, name='.jinja2', prepend=False):
"""
This function is added as a method of a :term:`Configurator`, and
should not be called directly. Instead it should be called like so after
``pyramid_jinja2`` has been passed to ``config.include``:
.. code-block:: python
config.add_jinja2_search_path('anotherpackage:templates/')
It will add the directory or :term:`asset specification` passed as
``searchpath`` to the current search path of the
:class:`jinja2.Environment` used by the renderer identified by ``name``.
By default the path is appended to the end of the search path. If
``prepend`` is set to ``True`` then the path will be inserted at the start
of the search path.
"""
def register():
env = get_jinja2_environment(config, name)
searchpaths = parse_multiline(searchpath)
for folder in searchpaths:
path = abspath_from_asset_spec(folder, config.package)
if prepend:
env.loader.searchpath.insert(0, path)
else:
env.loader.searchpath.append(path)
config.action(None, register, order=EXTRAS_CONFIG_PHASE)
def add_jinja2_extension(config, ext, name='.jinja2'):
"""
This function is added as a method of a :term:`Configurator`, and
should not be called directly. Instead it should be called like so after
``pyramid_jinja2`` has been passed to ``config.include``:
.. code-block:: python
config.add_jinja2_extension(myext)
It will add the Jinja2 extension passed as ``ext`` to the current
:class:`jinja2.Environment` used by the renderer named ``name``.
"""
ext = config.maybe_dotted(ext)
def register():
env = get_jinja2_environment(config, name)
env.add_extension(ext)
config.action(None, register, order=EXTRAS_CONFIG_PHASE)
def get_jinja2_environment(config, name='.jinja2'):
"""
This function is added as a method of a :term:`Configurator`, and
should not be called directly. Instead it should be called like so after
``pyramid_jinja2`` has been passed to ``config.include``:
.. code-block:: python
config.get_jinja2_environment()
It will return the configured ``jinja2.Environment`` for the
renderer named ``name``. The environment is created as an :term:`action`
which is deferred to allow users to override the configuration. In order
to get back the configured environment, you must either force a commit
via ``config.commit`` or schedule an action which can setup the
environment after it has been created:
.. code-block:: python
def setup_jinja2_env():
env = config.get_jinja2_environment()
# ...
config.action(None, setup_jinja2_env, order=999)
"""
registry = config.registry
return registry.queryUtility(IJinja2Environment, name=name)
def create_environment_from_options(env_opts, loader_opts):
loader = SmartAssetSpecLoader(**loader_opts)
newstyle = env_opts.pop('newstyle', False)
gettext = env_opts.pop('gettext', None)
filters = env_opts.pop('filters', {})
tests = env_opts.pop('tests', {})
globals = env_opts.pop('globals', {})
env = Environment(
loader=loader,
**env_opts
)
env.install_gettext_callables(
gettext.gettext, gettext.ngettext, newstyle=newstyle)
env.filters.update(filters)
env.tests.update(tests)
env.globals.update(globals)
return env
def add_jinja2_renderer(config, name, settings_prefix='jinja2.', package=None):
"""
This function is added as a method of a :term:`Configurator`, and
should not be called directly. Instead it should be called like so after
``pyramid_jinja2`` has been passed to ``config.include``:
.. code-block:: python
config.add_jinja2_renderer('.html', settings_prefix='jinja2.')
It will register a new renderer, loaded from settings at the specified
``settings_prefix`` prefix. This renderer will be active for files using
the specified extension ``name``.
"""
renderer_factory = Jinja2RendererFactory()
config.add_renderer(name, renderer_factory)
package = package or config.package
resolver = DottedNameResolver(package=package)
def register():
registry = config.registry
settings = config.get_settings()
loader_opts = parse_loader_options_from_settings(
settings,
settings_prefix,
resolver.maybe_resolve,
package,
)
env_opts = parse_env_options_from_settings(
settings,
settings_prefix,
resolver.maybe_resolve,
package,
)
env = create_environment_from_options(env_opts, loader_opts)
renderer_factory.environment = env
registry.registerUtility(env, IJinja2Environment, name=name)
config.action(
('jinja2-renderer', name), register, order=ENV_CONFIG_PHASE)
def includeme(config):
"""Set up standard configurator registrations. Use via:
.. code-block:: python
config = Configurator()
config.include('pyramid_jinja2')
Once this function has been invoked, the ``.jinja2`` renderer is
available for use in Pyramid and these new directives are available as
methods of the configurator:
- ``add_jinja2_renderer``: Add a new Jinja2 renderer, with a different
file extension and/or settings.
- ``add_jinja2_search_path``: Add a new location to the search path
for the specified renderer.
- ``add_jinja2_extension``: Add a list of extensions to the Jinja2
environment used by the specified renderer.
- ``get_jinja2_environment``: Return the :class:`jinja2.Environment`
used by the specified renderer.
"""
config.add_directive('add_jinja2_renderer', add_jinja2_renderer)
config.add_directive('add_jinja2_search_path', add_jinja2_search_path)
config.add_directive('add_jinja2_extension', add_jinja2_extension)
config.add_directive('get_jinja2_environment', get_jinja2_environment)
package = _caller_package(('pyramid', 'pyramid.', 'pyramid_jinja2'))
config.add_jinja2_renderer('.jinja2', package=package)
# always insert default search path relative to package
default_search_path = '%s:' % (package.__name__,)
config.add_jinja2_search_path(default_search_path, name='.jinja2')