1
2 r"""
3 :Copyright:
4
5 Copyright 2010 - 2015
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 if __doc__:
61
62 __doc__ = __doc__.encode('ascii').decode('unicode_escape')
63 __author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
64 __docformat__ = "restructuredtext en"
65
66 import errno as _errno
67 import itertools as _it
68 import os as _os
69 import posixpath as _posixpath
70 try:
71 import threading as _threading
72 except ImportError:
73 import dummy_threading as _threading
74
75 try:
76 from wtf import services as _wtf_services
77 except ImportError:
78 _wtf_services = None
79
80 from .. import factory as _factory
81 from .. import factory_memoize as _factory_memoize
82 from .. import interfaces as _interfaces
83 from .. import model_adapters as _model_adapters
84 from ..markup import factory as _markup_factory
85 from ..tools import htmlform as _htmlform
86
87
89 """
90 Load a dotted name
91
92 The dotted name can be anything, which is passively resolvable (i.e.
93 without the invocation of a class to get their attributes or the like).
94 For example, `name` could be 'tdi.integration.wtf_service._load_dotted'
95 and would return this very function. It's assumed that the first part of
96 the `name` is always is a module.
97
98 :Parameters:
99 `name` : ``str``
100 The dotted name to load
101
102 :Return: The loaded object
103 :Rtype: any
104
105 :Exceptions:
106 - `ImportError` : A module in the path could not be loaded
107 """
108 components = name.split('.')
109 path = [components.pop(0)]
110 obj = __import__(path[0])
111 while components:
112 comp = components.pop(0)
113 path.append(comp)
114 try:
115 obj = getattr(obj, comp)
116 except AttributeError:
117 __import__('.'.join(path))
118 try:
119 obj = getattr(obj, comp)
120 except AttributeError:
121 raise ImportError('.'.join(path))
122
123 return obj
124
125
127 """ Load resource service """
128 from __svc__.wtf import resource
129 return resource
130
131
133 """
134 HTMLForm parameter adapter from request.param
135
136 :IVariables:
137 `param` : ``wtf.request.Request.param``
138 request.param
139 """
140 __implements__ = [_htmlform.ParameterAdapterInterface]
141
153
154 - def getfirst(self, name, default=None):
155 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """
156 if name in self.param:
157 return self.param[name]
158 return default
159
161 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """
162 return self.param.multi(name)
163
164
166 """ Directory Template Lister """
167
168
169
170
171 DEFAULT_IGNORE = ('.svn', 'CVS', '.git', '.bzr', '.hg')
172
173 - def __init__(self, directories, extensions, ignore=None):
174 """
175 Initialization
176
177 :Parameters:
178 `directories` : ``iterable``
179 List of base directories to scan
180
181 `extensions` : ``iterable``
182 List of extensions to consider
183
184 `ignore` : ``iterable``
185 List of directory names to ignore. If omitted or ``None``,
186 `DEFAULT_IGNORE` is applied.
187 """
188 self._dirs = tuple(_it.imap(str, directories))
189 self._ext = tuple(_it.imap(str, extensions or ()))
190 self._ci = _os.path.normcase('aA') != 'aA'
191 if ignore is None:
192 ignore = self.DEFAULT_IGNORE
193 if self._ci:
194 self._ignore = frozenset((
195 _os.path.normcase(item) for item in (ignore or ())
196 ))
197 else:
198 self._ignore = frozenset(ignore or ())
199
201 """
202 Walk the directories and yield all template names
203
204 :Return: Iterator over template names
205 :Rtype: ``iterable``
206 """
207
208
209 seen = set()
210 if _os.path.sep == '/':
211 norm = lambda p: p
212 else:
213 norm = lambda p: p.replace(_os.path.sep, '/')
214
215 for base in self._dirs:
216 baselen = len(_os.path.join(base, ''))
217 reldir = lambda x, b=baselen: x[b:]
218
219 def onerror(_):
220 """ Error handler """
221 raise
222 for dirpath, dirs, files in _os.walk(base, onerror=onerror):
223
224 if self._ignore:
225 newdirs = []
226 for dirname in dirs:
227 if self._ci:
228 if _os.path.normcase(dirname) in self._ignore:
229 continue
230 elif dirname in self._ignore:
231 continue
232 newdirs.append(dirname)
233 if len(newdirs) != len(dirs):
234 dirs[:] = newdirs
235
236
237 dirpath = reldir(dirpath)
238 for name in files:
239 if not name.endswith(self._ext):
240 continue
241 if dirpath:
242 name = _posixpath.join(norm(dirpath), name)
243 if name in seen:
244 continue
245 yield name
246
247
256
257
259 """
260 Actual global template service object
261
262 :IVariables:
263 `_dirs` : ``list``
264 Template locations resolved to directories
265
266 `autoreload` : ``bool``
267 Automatically reload templates?
268
269 `require_scopes` : ``bool``
270 Require all scopes?
271
272 `require_methods` : ``bool``
273 Require all render methods?
274 """
275
276 - def __init__(self, locations, autoreload=False, require_scopes=False,
277 require_methods=False, filters=None):
278 """
279 Initialization
280
281 :Parameters:
282 `locations` : iterable
283 Resource locations (``['token', ...]``)
284
285 `autoreload` : ``bool``
286 Automatically reload templates?
287
288 `require_scopes` : ``bool``
289 Require all scopes?
290
291 `require_methods` : ``bool``
292 Require all render methods?
293
294 `filters` : ``dict``
295 Filter factories to apply
296 """
297 self._dirs = list(_it.chain(*[
298 _resource()[location] for location in locations
299 ]))
300 self.autoreload = autoreload
301 self.require_scopes = require_scopes
302 self.require_methods = require_methods
303
304 def streamopen(name):
305 """ Stream opener """
306
307
308
309
310
311
312
313
314 stream, filename = self.stream(name)
315 stream.close()
316 return (_factory.file_opener, filename), filename
317
318 def loader(which, post_load=None, **kwargs):
319 """ Template loader """
320 kwargs['autoupdate'] = autoreload
321 kwargs['memoizer'] = _Memoizer()
322 factory = _factory_memoize.MemoizedFactory(
323 getattr(_markup_factory, which).replace(**kwargs)
324 )
325 sfactory = factory.replace(overlay_eventfilters=[])
326
327 def load(names):
328 """ Actual loader """
329 res = factory.from_streams(names, streamopen=streamopen)
330 for item in post_load or ():
331 res = item(res)
332 return res
333
334 def single(name):
335 """ Single file loader """
336 return sfactory.from_opener(*streamopen(name)[0])
337 return load, single
338
339 def opt(option, args):
340 """ Find opt """
341 for arg in args:
342 try:
343 option = option[arg]
344 except (TypeError, KeyError):
345 return None
346 return [unicode(opt).encode('utf-8') for opt in option]
347
348 def load(*args):
349 """ Actually load factories """
350 return map(
351 _load_dotted, filter(None, opt(filters, args) or ())
352 ) or None
353
354 self.html, self.html_file = loader(
355 'html',
356 post_load=load('html', 'template'),
357 eventfilters=load('html', 'load'),
358 overlay_eventfilters=load('html', 'overlay'),
359 )
360 self.xml, self.xml_file = loader(
361 'xml',
362 post_load=load('xml', 'template'),
363 eventfilters=load('xml', 'load'),
364 overlay_eventfilters=load('xml', 'overlay'),
365 )
366 self.text, self.text_file = loader(
367 'text',
368 post_load=load('text', 'template'),
369 eventfilters=load('text', 'load'),
370 overlay_eventfilters=load('text', 'overlay'),
371 )
372
373 - def lister(self, extensions, ignore=None):
374 """
375 Create template lister from our own config
376
377 :Parameters:
378 `extensions` : ``iterable``
379 List of file extensions to consider (required)
380
381 `ignore` : ``iterable``
382 List of (simple) directory names to ignore. If omitted or
383 ``None``, a default list is applied
384 (`DirectoryTemplateLister.DEFAULT_IGNORE`)
385
386 :Return: a template lister
387 :Rtype: ``callable``
388 """
389 return DirectoryTemplateLister([
390 rsc.resolve('.').filename for rsc in self._dirs
391 ], extensions, ignore=ignore)
392
393 - def stream(self, name, mode='rb', buffering=-1, blockiter=0):
394 """
395 Locate file in the template directories and open a stream
396
397 :Parameters:
398 `name` : ``str``
399 The relative filename
400
401 `mode` : ``str``
402 The opening mode
403
404 `buffering` : ``int``
405 buffering spec
406
407 `blockiter` : ``int``
408 Iterator mode
409 (``1: Line, <= 0: Default chunk size, > 1: This chunk size``)
410
411 :Return: The resource stream
412 :Rtype: ``wtf.app.services.resources.ResourceStream``
413
414 :Exceptions:
415 - `IOError` : File not found
416 """
417 for location in self._dirs:
418 try:
419 loc = location.resolve(name)
420 return loc.open(
421 mode=mode, buffering=buffering, blockiter=blockiter
422 ), loc.filename
423 except IOError, e:
424 if e.args[0] == _errno.ENOENT:
425 continue
426 raise
427 raise IOError(_errno.ENOENT, name)
428
429
431 """
432 Response hint factory collection
433
434 :IVariables:
435 `_global` : `GlobalTemplate`
436 The global service
437 """
438
440 """
441 Initialization
442
443 :Parameters:
444 `global_template` : `GlobalTemplate`
445 The global template service
446 """
447 def adapter(model):
448 """ Adapter factory """
449 return _model_adapters.RenderAdapter(
450 model,
451 requiremethods=global_template.require_methods,
452 requirescopes=global_template.require_scopes,
453 )
454
455 def load_html(response):
456 """ Response factory for ``load_html`` """
457 def load_html(*names):
458 """
459 Load TDI template
460
461 :Parameters:
462 `names` : ``tuple``
463 The template names. If there's more than one name
464 given, the templates are overlayed.
465
466 :Return: The TDI template
467 :Rtype: ``tdi.template.Template``
468 """
469 return global_template.html(names)
470 return load_html
471
472 def render_html(response):
473 """ Response factory for ``render_html`` """
474 return self._render_factory(
475 response, global_template.html, adapter,
476 "render_html", 'text/html'
477 )
478
479 def pre_render_html(response):
480 """ Response factory for ``pre_render_html`` """
481 return self._render_factory(
482 response, global_template.html, adapter,
483 "pre_render_html", 'text/html', pre=True
484 )
485
486 def load_xml(response):
487 """ Response factory for ``load_xml`` """
488 def load_xml(*names):
489 """
490 Load TDI template
491
492 :Parameters:
493 `names` : ``tuple``
494 The template names. If there's more than one name
495 given, the templates are overlayed.
496
497 :Return: The TDI template
498 :Rtype: ``tdi.template.Template``
499 """
500 return global_template.xml(names)
501 return load_xml
502
503 def render_xml(response):
504 """ Response factory for ``render_xml`` """
505 return self._render_factory(
506 response, global_template.xml, adapter,
507 "render_xml", 'text/xml'
508 )
509
510 def pre_render_xml(response):
511 """ Response factory for ``pre_render_xml`` """
512 return self._render_factory(
513 response, global_template.xml, adapter,
514 "pre_render_xml", 'text/xml', pre=True,
515 )
516
517 def load_text(response):
518 """ Response factory for ``load_text`` """
519 def load_text(*names):
520 """
521 Load TDI template
522
523 :Parameters:
524 `names` : ``tuple``
525 The template names. If there's more than one name
526 given, the templates are overlayed.
527
528 :Return: The TDI template
529 :Rtype: ``tdi.template.Template``
530 """
531 return global_template.text(names)
532 return load_text
533
534 def render_text(response):
535 """ Response factory for ``render_text`` """
536 return self._render_factory(
537 response, global_template.text, adapter,
538 "render_text", 'text/plain'
539 )
540
541 def pre_render_text(response):
542 """ Response factory for ``pre_render_text`` """
543 return self._render_factory(
544 response, global_template.text, adapter,
545 "pre_render_text", 'text/plain', pre=True,
546 )
547
548 self.env = {
549 'wtf.response.load_html': load_html,
550 'wtf.response.render_html': render_html,
551 'wtf.response.pre_render_html': pre_render_html,
552 'wtf.response.load_xml': load_xml,
553 'wtf.response.render_xml': render_xml,
554 'wtf.response.pre_render_xml': pre_render_xml,
555 'wtf.response.load_text': load_text,
556 'wtf.response.render_text': render_text,
557 'wtf.response.pre_render_text': pre_render_text,
558 }
559
560 - def _render_factory(self, response, template_loader, adapter, func_name,
561 content_type_, pre=False):
562 """
563 Response factory for ``render_html/xml/text``
564
565 :Parameters:
566 `response` : ``wtf.response.Response``
567 The response object
568
569 `template_loader` : ``callable``
570 Template loader function
571
572 `adapter` : ``callable``
573 render adapter factory
574
575 `func_name` : ``str``
576 Name of the render function (only for introspection)
577
578 `content_type_` : ``str``
579 Default content type
580
581 `pre` : ``bool``
582 Prerender only?
583
584 :Return: The render callable
585 :Rtype: ``callable``
586 """
587 def render_func(model, *names, **kwargs):
588 """
589 Simplified TDI invocation
590
591 The following keyword arguments are recognized:
592
593 ``startnode`` : ``str``
594 The node to render. If omitted or ``None``, the whole
595 template is rendered.
596 ``prerender`` : any
597 Prerender-Model to apply
598 ``content_type`` : ``str``
599 Content type to set. If omitted, the default content type
600 will be set. If ``None``, the content type won't be set.
601
602 :Parameters:
603 `model` : any
604 The model instance
605
606 `names` : ``tuple``
607 The template names. If there's more than one name
608 given, the templates are overlayed.
609
610 `kwargs` : ``dict``
611 Additional keyword parameters
612
613 :Return: Iterable containing the rendered template
614 :Rtype: ``iterable``
615
616 :Exceptions:
617 - `Exception` : Anything what happened during rendering
618 """
619 startnode = kwargs.pop('startnode', None)
620 prerender = kwargs.pop('prerender', None)
621 content_type = kwargs.pop('content_type', content_type_)
622 if kwargs:
623 raise TypeError("Unrecognized kwargs: %r" % kwargs.keys())
624
625 tpl = template_loader(names)
626 if content_type is not None:
627 encoding = tpl.encoding
628 response.content_type(content_type, charset=encoding)
629 if pre and prerender is not None:
630 return [tpl.render_string(
631 prerender, startnode=startnode,
632 adapter=_model_adapters.RenderAdapter.for_prerender,
633 )]
634 return [tpl.render_string(
635 model,
636 adapter=adapter, startnode=startnode, prerender=prerender,
637 )]
638 try:
639 render_func.__name__ = func_name
640 except TypeError:
641 pass
642 return render_func
643
644
646 """
647 Template middleware
648
649 :IVariables:
650 `_func` : ``callable``
651 Next WSGI handler
652
653 `_env` : ``dict``
654 Environment update
655 """
656
657 - def __init__(self, global_template, func):
658 """
659 Initialization
660
661 :Parameters:
662 `global_template` : `GlobalTemplate`
663 The global template service
664
665 `func` : ``callable``
666 WSGI callable to wrap
667 """
668 self._func = func
669 self._env = ResponseFactory(global_template).env
670
671 - def __call__(self, environ, start_response):
672 """
673 Middleware handler
674
675 :Parameters:
676 `environ` : ``dict``
677 WSGI environment
678
679 `start_response` : ``callable``
680 Start response callable
681
682 :Return: WSGI response iterable
683 :Rtype: ``iterable``
684 """
685 environ.update(self._env)
686 return self._func(environ, start_response)
687
688
690 """
691 Template service
692
693 :IVariables:
694 `_global` : `GlobalTemplate`
695 The global service
696 """
697 if _wtf_services is not None:
698 __implements__ = [_wtf_services.ServiceInterface]
699
700 - def __init__(self, config, opts, args):
701 """ Initialization """
702
703
704 self._global = GlobalTemplate(
705 config.tdi.locations,
706 config.tdi('autoreload', False),
707 config.tdi('require_scopes', False),
708 config.tdi('require_methods', False),
709 config.tdi('filters', None),
710 )
711
713 """ :See: ``wtf.services.ServiceInterface.shutdown`` """
714 pass
715
717 """ :See: ``wtf.services.ServiceInterface.global_service`` """
718 return 'tdi', self._global
719
721 """ :See: ``wtf.services.ServiceInterface.middleware`` """
722 return Middleware(self._global, func)
723