1
2 u"""
3 :Copyright:
4
5 Copyright 2010 - 2013
6 Andr\xe9 Malo or his licensors, as applicable
7
8 :License:
9
10 Licensed under the Apache License, Version 2.0 (the "License");
11 you may not use this file except in compliance with the License.
12 You may obtain a copy of the License at
13
14 http://www.apache.org/licenses/LICENSE-2.0
15
16 Unless required by applicable law or agreed to in writing, software
17 distributed under the License is distributed on an "AS IS" BASIS,
18 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 See the License for the specific language governing permissions and
20 limitations under the License.
21
22 =============
23 TDI Service
24 =============
25
26 This module implements the tdi template locating and caching service.
27
28 Configuration
29 ~~~~~~~~~~~~~
30
31 ::
32
33 [resources]
34 # example: templates directory parallel to the app package.
35 template_site = app:../templates
36
37 [tdi]
38 # locations is a ResourceService location
39 locations = templates_site
40 #autoreload = False
41 #require_scopes = False
42 #require_methods = False
43 #filters.html.load =
44 #filters.html.overlay =
45 #filters.html.template =
46 #filters.xml.load =
47 #filters.xml.overlay =
48 #filters.xml.template=
49 #filters.text.load =
50 #filters.text.overlay =
51 #filters.text.template=
52
53 # load + overlay Filters are lists of (event) filter factories, for example
54 #
55 #filters.html.load =
56 # tdi.tools.html.MinifyFilter
57 #
58 # template filters work on the final template object
59 """
60 __docformat__ = "restructuredtext en"
61 __author__ = u"Andr\xe9 Malo"
62
63 import errno as _errno
64 import itertools as _it
65 import os as _os
66 import posixpath as _posixpath
67 try:
68 import threading as _threading
69 except ImportError:
70 import dummy_threading as _threading
71
72 try:
73 from wtf import services as _wtf_services
74 except ImportError:
75 _wtf_services = None
76
77 from tdi import factory as _factory
78 from tdi import factory_memoize as _factory_memoize
79 from tdi import interfaces as _interfaces
80 from tdi import model_adapters as _model_adapters
81 from tdi import util as _util
82 from tdi.markup import factory as _markup_factory
83 from tdi.tools import htmlform as _htmlform
84
85
87 """ Load resource service """
88 from __svc__.wtf import resource
89 return resource
90
91
93 """
94 HTMLForm parameter adapter from request.param
95
96 :IVariables:
97 `param` : ``wtf.request.Request.param``
98 request.param
99 """
100 __implements__ = [_htmlform.ParameterAdapterInterface]
101
113
114 - def getfirst(self, name, default=None):
115 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """
116 if name in self.param:
117 return self.param[name]
118 return default
119
121 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """
122 return self.param.multi(name)
123
124
126 """ Directory Template Lister """
127
128
129
130
131 DEFAULT_IGNORE = ('.svn', 'CVS', '.git', '.bzr', '.hg')
132
133 - def __init__(self, directories, extensions, ignore=None):
134 """
135 Initialization
136
137 :Parameters:
138 `directories` : ``iterable``
139 List of base directories to scan
140
141 `extensions` : ``iterable``
142 List of extensions to consider
143
144 `ignore` : ``iterable``
145 List of directory names to ignore. If omitted or ``None``,
146 `DEFAULT_IGNORE` is applied.
147 """
148 self._dirs = tuple(_it.imap(str, directories))
149 self._ext = tuple(_it.imap(str, extensions or ()))
150 self._ci = _os.path.normcase('aA') != 'aA'
151 if ignore is None:
152 ignore = self.DEFAULT_IGNORE
153 if self._ci:
154 self._ignore = frozenset((
155 _os.path.normcase(item) for item in (ignore or ())
156 ))
157 else:
158 self._ignore = frozenset(ignore or ())
159
161 """
162 Walk the directories and yield all template names
163
164 :Return: Iterator over template names
165 :Rtype: ``iterable``
166 """
167
168
169 seen = set()
170 if _os.path.sep == '/':
171 norm = lambda p: p
172 else:
173 norm = lambda p: p.replace(_os.path.sep, '/')
174
175 for base in self._dirs:
176 baselen = len(_os.path.join(base, ''))
177 reldir = lambda x: x[baselen:]
178 def onerror(_):
179 """ Error handler """
180 raise
181 for dirpath, dirs, files in _os.walk(base, onerror=onerror):
182
183 if self._ignore:
184 newdirs = []
185 for dirname in dirs:
186 if self._ci:
187 if _os.path.normcase(dirname) in self._ignore:
188 continue
189 elif dirname in self._ignore:
190 continue
191 newdirs.append(dirname)
192 if len(newdirs) != len(dirs):
193 dirs[:] = newdirs
194
195
196 dirpath = reldir(dirpath)
197 for name in files:
198 if not name.endswith(self._ext):
199 continue
200 if dirpath:
201 name = _posixpath.join(norm(dirpath), name)
202 if name in seen:
203 continue
204 yield name
205
206
215
216
218 """
219 Actual global template service object
220
221 :IVariables:
222 `_dirs` : ``list``
223 Template locations resolved to directories
224
225 `autoreload` : ``bool``
226 Automatically reload templates?
227
228 `require_scopes` : ``bool``
229 Require all scopes?
230
231 `require_methods` : ``bool``
232 Require all render methods?
233 """
234
235 - def __init__(self, locations, autoreload=False, require_scopes=False,
236 require_methods=False, filters=None):
237 """
238 Initialization
239
240 :Parameters:
241 `locations` : iterable
242 Resource locations (``['token', ...]``)
243
244 `autoreload` : ``bool``
245 Automatically reload templates?
246
247 `require_scopes` : ``bool``
248 Require all scopes?
249
250 `require_methods` : ``bool``
251 Require all render methods?
252
253 `filters` : ``dict``
254 Filter factories to apply
255 """
256 self._dirs = list(_it.chain(*[_resource()[location]
257 for location in locations]))
258 self.autoreload = autoreload
259 self.require_scopes = require_scopes
260 self.require_methods = require_methods
261
262 def streamopen(name):
263 """ Stream opener """
264
265
266
267
268
269
270
271
272 stream, filename = self.stream(name)
273 stream.close()
274 return (_factory.file_opener, filename), filename
275
276 def loader(which, post_load=None, **kwargs):
277 """ Template loader """
278 kwargs['autoupdate'] = autoreload
279 kwargs['memoizer'] = _Memoizer()
280 factory = _factory_memoize.MemoizedFactory(
281 getattr(_markup_factory, which).replace(**kwargs)
282 )
283 sfactory = factory.replace(overlay_eventfilters=[])
284 def load(names):
285 """ Actual loader """
286 res = factory.from_streams(names, streamopen=streamopen)
287 for item in post_load or ():
288 res = item(res)
289 return res
290
291 def single(name):
292 """ Single file loader """
293 return sfactory.from_opener(*streamopen(name)[0])
294 return load, single
295
296 def opt(option, args):
297 """ Find opt """
298 for arg in args:
299 try:
300 option = option[arg]
301 except (TypeError, KeyError):
302 return None
303 return [unicode(opt).encode('utf-8') for opt in option]
304
305 def load(*args):
306 """ Actually load factories """
307 return map(
308 _util.load_dotted, filter(None, opt(filters, args) or ())
309 ) or None
310
311 self.html, self.html_file = loader('html',
312 post_load=load('html', 'template'),
313 eventfilters=load('html', 'load'),
314 overlay_eventfilters=load('html', 'overlay'),
315 )
316 self.xml, self.xml_file = loader('xml',
317 post_load=load('xml', 'template'),
318 eventfilters=load('xml', 'load'),
319 overlay_eventfilters=load('xml', 'overlay'),
320 )
321 self.text, self.text_file = loader('text',
322 post_load=load('text', 'template'),
323 eventfilters=load('text', 'load'),
324 overlay_eventfilters=load('text', 'overlay'),
325 )
326
327 - def lister(self, extensions, ignore=None):
328 """
329 Create template lister from our own config
330
331 :Parameters:
332 `extensions` : ``iterable``
333 List of file extensions to consider (required)
334
335 `ignore` : ``iterable``
336 List of (simple) directory names to ignore. If omitted or
337 ``None``, a default list is applied
338 (`DirectoryTemplateLister.DEFAULT_IGNORE`)
339
340 :Return: a template lister
341 :Rtype: ``callable``
342 """
343 return DirectoryTemplateLister([
344 rsc.resolve('.').filename for rsc in self._dirs
345 ], extensions, ignore=ignore)
346
347 - def stream(self, name, mode='rb', buffering=-1, blockiter=0):
348 """
349 Locate file in the template directories and open a stream
350
351 :Parameters:
352 `name` : ``str``
353 The relative filename
354
355 `mode` : ``str``
356 The opening mode
357
358 `buffering` : ``int``
359 buffering spec
360
361 `blockiter` : ``int``
362 Iterator mode
363 (``1: Line, <= 0: Default chunk size, > 1: This chunk size``)
364
365 :Return: The resource stream
366 :Rtype: ``wtf.app.services.resources.ResourceStream``
367
368 :Exceptions:
369 - `IOError` : File not found
370 """
371 for location in self._dirs:
372 try:
373 loc = location.resolve(name)
374 return loc.open(
375 mode=mode, buffering=buffering, blockiter=blockiter
376 ), loc.filename
377 except IOError, e:
378 if e[0] == _errno.ENOENT:
379 continue
380 raise
381 raise IOError(_errno.ENOENT, name)
382
383
385 """
386 Response hint factory collection
387
388 :IVariables:
389 `_global` : `GlobalTemplate`
390 The global service
391 """
392
394 """
395 Initialization
396
397 :Parameters:
398 `global_template` : `GlobalTemplate`
399 The global template service
400 """
401
402
403
404 def adapter(model):
405 """ Adapter factory """
406 return _model_adapters.RenderAdapter(model,
407 requiremethods=global_template.require_methods,
408 requirescopes=global_template.require_scopes,
409 )
410
411 def load_html(response):
412 """ Response factory for ``load_html`` """
413
414 def load_html(*names):
415 """
416 Load TDI template
417
418 :Parameters:
419 `names` : ``tuple``
420 The template names. If there's more than one name
421 given, the templates are overlayed.
422
423 :Return: The TDI template
424 :Rtype: ``tdi.template.Template``
425 """
426 return global_template.html(names)
427 return load_html
428 def render_html(response):
429 """ Response factory for ``render_html`` """
430 return self._render_factory(
431 response, global_template.html, adapter,
432 "render_html", 'text/html'
433 )
434 def pre_render_html(response):
435 """ Response factory for ``pre_render_html`` """
436 return self._render_factory(
437 response, global_template.html, adapter,
438 "pre_render_html", 'text/html', pre=True
439 )
440
441 def load_xml(response):
442 """ Response factory for ``load_xml`` """
443
444 def load_xml(*names):
445 """
446 Load TDI template
447
448 :Parameters:
449 `names` : ``tuple``
450 The template names. If there's more than one name
451 given, the templates are overlayed.
452
453 :Return: The TDI template
454 :Rtype: ``tdi.template.Template``
455 """
456 return global_template.xml(names)
457 return load_xml
458 def render_xml(response):
459 """ Response factory for ``render_xml`` """
460 return self._render_factory(
461 response, global_template.xml, adapter,
462 "render_xml", 'text/xml'
463 )
464 def pre_render_xml(response):
465 """ Response factory for ``pre_render_xml`` """
466 return self._render_factory(
467 response, global_template.xml, adapter,
468 "pre_render_xml", 'text/xml', pre=True,
469 )
470
471 def load_text(response):
472 """ Response factory for ``load_text`` """
473
474 def load_text(*names):
475 """
476 Load TDI template
477
478 :Parameters:
479 `names` : ``tuple``
480 The template names. If there's more than one name
481 given, the templates are overlayed.
482
483 :Return: The TDI template
484 :Rtype: ``tdi.template.Template``
485 """
486 return global_template.text(names)
487 return load_text
488 def render_text(response):
489 """ Response factory for ``render_text`` """
490 return self._render_factory(
491 response, global_template.text, adapter,
492 "render_text", 'text/plain'
493 )
494 def pre_render_text(response):
495 """ Response factory for ``pre_render_text`` """
496 return self._render_factory(
497 response, global_template.text, adapter,
498 "pre_render_text", 'text/plain', pre=True,
499 )
500
501 self.env = {
502 'wtf.response.load_html': load_html,
503 'wtf.response.render_html': render_html,
504 'wtf.response.pre_render_html': pre_render_html,
505 'wtf.response.load_xml': load_xml,
506 'wtf.response.render_xml': render_xml,
507 'wtf.response.pre_render_xml': pre_render_xml,
508 'wtf.response.load_text': load_text,
509 'wtf.response.render_text': render_text,
510 'wtf.response.pre_render_text': pre_render_text,
511 }
512
513 - def _render_factory(self, response, template_loader, adapter, func_name,
514 content_type_, pre=False):
515 """
516 Response factory for ``render_html/xml/text``
517
518 :Parameters:
519 `response` : ``wtf.response.Response``
520 The response object
521
522 `template_loader` : ``callable``
523 Template loader function
524
525 `adapter` : ``callable``
526 render adapter factory
527
528 `func_name` : ``str``
529 Name of the render function (only for introspection)
530
531 `content_type_` : ``str``
532 Default content type
533
534 `pre` : ``bool``
535 Prerender only?
536
537 :Return: The render callable
538 :Rtype: ``callable``
539 """
540 def render_func(model, *names, **kwargs):
541 """
542 Simplified TDI invocation
543
544 The following keyword arguments are recognized:
545
546 ``startnode`` : ``str``
547 The node to render. If omitted or ``None``, the whole
548 template is rendered.
549 ``prerender`` : any
550 Prerender-Model to apply
551 ``content_type`` : ``str``
552 Content type to set. If omitted, the default content type
553 will be set. If ``None``, the content type won't be set.
554
555 :Parameters:
556 `model` : any
557 The model instance
558
559 `names` : ``tuple``
560 The template names. If there's more than one name
561 given, the templates are overlayed.
562
563 `kwargs` : ``dict``
564 Additional keyword parameters
565
566 :Return: Iterable containing the rendered template
567 :Rtype: ``iterable``
568
569 :Exceptions:
570 - `Exception` : Anything what happened during rendering
571 """
572 startnode = kwargs.pop('startnode', None)
573 prerender = kwargs.pop('prerender', None)
574 content_type = kwargs.pop('content_type', content_type_)
575 if kwargs:
576 raise TypeError("Unrecognized kwargs: %r" % kwargs.keys())
577
578 tpl = template_loader(names)
579 if content_type is not None:
580 encoding = tpl.encoding
581 response.content_type(content_type, charset=encoding)
582 if pre and prerender is not None:
583 return [tpl.render_string(
584 prerender, startnode=startnode,
585 adapter=_model_adapters.RenderAdapter.for_prerender,
586 )]
587 return [tpl.render_string(model, adapter=adapter,
588 startnode=startnode, prerender=prerender,
589 )]
590 try:
591 render_func.__name__ = func_name
592 except TypeError:
593 pass
594 return render_func
595
596
598 """
599 Template middleware
600
601 :IVariables:
602 `_func` : ``callable``
603 Next WSGI handler
604
605 `_env` : ``dict``
606 Environment update
607 """
608
609 - def __init__(self, global_template, func):
610 """
611 Initialization
612
613 :Parameters:
614 `global_template` : `GlobalTemplate`
615 The global template service
616
617 `func` : ``callable``
618 WSGI callable to wrap
619 """
620 self._func = func
621 self._env = ResponseFactory(global_template).env
622
623 - def __call__(self, environ, start_response):
624 """
625 Middleware handler
626
627 :Parameters:
628 `environ` : ``dict``
629 WSGI environment
630
631 `start_response` : ``callable``
632 Start response callable
633
634 :Return: WSGI response iterable
635 :Rtype: ``iterable``
636 """
637 environ.update(self._env)
638 return self._func(environ, start_response)
639
640
642 """
643 Template service
644
645 :IVariables:
646 `_global` : `GlobalTemplate`
647 The global service
648 """
649 if _wtf_services is not None:
650 __implements__ = [_wtf_services.ServiceInterface]
651
652 - def __init__(self, config, opts, args):
653 """ Initialization """
654
655 self._global = GlobalTemplate(
656 config.tdi.locations,
657 config.tdi('autoreload', False),
658 config.tdi('require_scopes', False),
659 config.tdi('require_methods', False),
660 config.tdi('filters', None),
661 )
662
664 """ :See: ``wtf.services.ServiceInterface.shutdown`` """
665 pass
666
668 """ :See: ``wtf.services.ServiceInterface.global_service`` """
669 return 'tdi', self._global
670
672 """ :See: ``wtf.services.ServiceInterface.middleware`` """
673 return Middleware(self._global, func)
674