1
2 u"""
3 :Copyright:
4
5 Copyright 2006 - 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 Template Objects
24 ==================
25
26 Template Objects.
27 """
28 __author__ = u"Andr\xe9 Malo"
29 __docformat__ = "restructuredtext en"
30 __all__ = ['Template', 'OverlayTemplate', 'AutoUpdate']
31
32 import sys as _sys
33
34 from tdi._exceptions import Error
35 from tdi._exceptions import TemplateReloadError
36 from tdi._exceptions import AutoUpdateWarning
37 from tdi._exceptions import OverlayError
38 from tdi import model_adapters as _model_adapters
39 from tdi import util as _util
40
41
43 """
44 Template class
45
46 :IVariables:
47 `filename` : ``str``
48 The filename of the template
49
50 `mtime` : any
51 Last modification time key (``None`` means n/a)
52
53 `factory` : `Factory`
54 Template factory
55
56 `loader` : ``callable``
57 Loader
58
59 `_tree` : ``list``
60 The nodetree, the overlay-filtered tree and the prerendered tree
61 """
62 __slots__ = (
63 '__weakref__', 'filename', 'mtime', 'factory', 'loader', '_tree'
64 )
65
67 """
68 Prepared node tree
69
70 :Type: `tdi.nodetree.Root`
71 """
72
73 def fget(self):
74 return self._prerender(None, None)
75 return locals()
76 tree = _util.Property(tree)
77
79 """
80 The node tree without overlay filters
81
82 :Type: `tdi.nodetree.Root`
83 """
84
85 def fget(self):
86 return self._tree[0]
87 return locals()
88 virgin_tree = _util.Property(virgin_tree)
89
99 return locals()
100 encoding = _util.Property(encoding)
101
111 return locals()
112 source_overlay_names = _util.Property(source_overlay_names)
113
123 return locals()
124 target_overlay_names = _util.Property(target_overlay_names)
125
126 - def __init__(self, tree, filename, mtime, factory, loader=None):
127 """
128 Initialization
129
130 :Parameters:
131 `tree` : `tdi.nodetree.Root`
132 The template tree
133
134 `filename` : ``str``
135 Filename of the template
136
137 `mtime` : ``int``
138 Last modification time of the template (maybe ``None``)
139
140 `factory` : `Factory`
141 Template factory
142
143 `loader` : ``callable``
144 Template loader
145 """
146 self._tree = [tree, None, None]
147 self.filename = filename
148 self.mtime = mtime
149 self.factory = factory
150 self.loader = loader
151
153 """
154 String representation of the node tree
155
156 :Return: The string representation
157 :Rtype: ``str``
158 """
159 return self.tree.to_string(verbose=False)
160
162 """ Return a clean template (without any wrapper) """
163 return self
164
165 - def reload(self, force=False):
166 """
167 Reload template(s) if possible and needed
168
169 :Parameters:
170 `force` : ``bool``
171 Force reload? (only if there's a loader present)
172
173 :Return: The reloaded (new) template or self
174 :Rtype: `Template`
175 """
176 if self.loader is not None:
177 if force:
178 mtime = None
179 else:
180 mtime = self.mtime
181 try:
182 tree, mtime = self.loader(mtime)
183 except (AttributeError, IOError, OSError, Error), e:
184 raise TemplateReloadError(str(e))
185 if tree is not None:
186 return self.__class__(
187 tree, self.filename, mtime, self.factory, self.loader
188 )
189 return self
190
192 """
193 Check for update
194
195 :Return: Update available?
196 :Rtype: ``bool``
197 """
198 if self.loader is not None:
199 return self.loader(self.mtime, check_only=True)[0]
200 return False
201
224
226 """
227 Possibly prerender the tree
228
229 :Parameters:
230 `model` : any
231 Prerender-Model
232
233 `adapter` : `ModelAdapterInterface`
234 Prerender-adapter
235
236 :Return: The tree to finally render (either prerendered or not)
237 :Rtype: `tdi.nodetree.Root`
238 """
239 otree, factory = self._tree, self.factory
240
241
242 ftree = otree[1]
243 if ftree is None:
244 otree[1] = ftree = self._prepare()
245
246
247 if model is None:
248 return ftree
249
250
251 ptree = otree[2]
252 if ptree is None:
253 version = None
254 else:
255 version = ptree[1]
256 checker = getattr(model, 'prerender_version', None)
257 if checker is not None:
258 rerender, version = checker(version)
259 elif version is not None:
260
261
262
263
264
265
266 rerender, version = True, None
267 else:
268 rerender = False
269 if ptree is not None and not rerender:
270 otree[2] = ptree[0], version
271 return ptree[0]
272
273
274 filters = getattr(model, 'prerender_filters', None)
275 if filters is not None:
276 filters = filters().get
277 factory = factory.replace(
278 eventfilters=filters('eventfilters'),
279 default_eventfilters=filters(
280 'default_eventfilters', True
281 ),
282 streamfilters=filters('streamfilters'),
283 default_streamfilters=filters(
284 'default_streamfilters', True
285 ),
286 )
287 if adapter is None:
288 adapter = _model_adapters.RenderAdapter.for_prerender
289 adapted = adapter(model, attr=dict(
290 scope=factory.builder().analyze.scope,
291 tdi=factory.builder().analyze.attribute,
292 ))
293 tree = factory.from_string(''.join(list(ftree.render(adapted))),
294 encoding=ftree.encoder.encoding
295 ).tree
296
297 otree[2] = tree, version
298 return tree
299
300 - def render(self, model=None, stream=None, flush=False,
301 startnode=None, adapter=None, prerender=None, preadapter=None):
302 """
303 Render the template into `stream` using `model`
304
305 :Parameters:
306 `model` : any
307 The model object
308
309 `stream` : ``file``
310 The stream to render to. If ``None``, ``sys.stdout`` is
311 taken.
312
313 `flush` : ``bool``
314 flush after each write? The stream needs a ``flush``
315 method in order to do this (If it doesn't have one, `flush`
316 being ``True`` is silently ignored)
317
318 `startnode` : ``str``
319 Only render this node (and all its children). The node
320 is addressed via a dotted string notation, like ``a.b.c`` (this
321 would render the ``c`` node.) The notation does not describe a
322 strict node chain, though. Between to parts of a node chain may
323 be gaps in the tree. The algorithm looks out for the first
324 matching node. It does no backtracking and so does not cover all
325 branches (yet?), but that works fine for realistic cases :).
326 A non-working example would be (searching for a.b.c)::
327
328 *
329 +- a
330 | `- b - d
331 `- a
332 `- b - c
333
334 `adapter` : ``callable``
335 Usermodel adapter factory (takes the model). This
336 adapter is responsible for method and attribute resolving in the
337 actual user model. If omitted or ``None``, the standard
338 `model_adapters.RenderAdapter` is applied.
339
340 `prerender` : any
341 Prerender-Model. If omitted or ``None``, no prerendering will
342 happen and the original template is rendered.
343
344 `preadapter` : `ModelAdapterInterface`
345 Prerender-model adapter factory (takes the model and an attribute
346 specification dict). If omitted or ``None``, the standard
347 `model_adapters.RenderAdapter.for_prerender` is applied.
348 """
349 if stream is None:
350 stream = _sys.stdout
351 if adapter is None:
352 adapter = _model_adapters.RenderAdapter
353 result = self._prerender(prerender, preadapter).render(
354 adapter(model), startnode
355 )
356 if flush == -1:
357 stream.write(''.join(list(result)))
358 else:
359 write = stream.write
360 if flush:
361 try:
362 flush = stream.flush
363 except AttributeError:
364 pass
365 else:
366 for chunk in result:
367 write(chunk)
368 flush()
369 return
370 for chunk in result:
371 write(chunk)
372
373 - def render_string(self, model=None, startnode=None, adapter=None,
374 prerender=None, preadapter=None):
375 """
376 Render the template as string using `model`
377
378 :Parameters:
379 `model` : any
380 The model object
381
382 `startnode` : ``str``
383 Only render this node (and all its children). See
384 `render` for details.
385
386 `adapter` : ``callable``
387 Usermodel adapter factory (takes the model). This
388 adapter is responsible for method and attribute resolving in the
389 actual user model. If omitted or ``None``, the standard
390 `model_adapters.RenderAdapter` is applied.
391
392 `prerender` : any
393 Prerender-Model
394
395 `preadapter` : `ModelAdapterInterface`
396 Prerender-adapter
397
398 :Return: The rendered document
399 :Rtype: ``str``
400 """
401 if adapter is None:
402 adapter = _model_adapters.RenderAdapter
403 return ''.join(list(self._prerender(prerender, preadapter).render(
404 adapter(model), startnode
405 )))
406
408 """
409 Overlay this template with another one
410
411 :Parameters:
412 `other` : `Template`
413 The template layed over self
414
415 :Return: The combined template
416 :Rtype: `Template`
417 """
418 return OverlayTemplate(self, other)
419
420
422 """ Overlay template representation """
423 __slots__ = ('_left', '_right')
424
425 - def __init__(self, original, overlay, keep=False):
426 """
427 Initialization
428
429 :Parameters:
430 `original` : `Template`
431 Original template
432
433 `overlay` : `Template`
434 Overlay template
435
436 `keep` : `Template`
437 Keep original templates?
438 """
439 tree1, tree2 = original.virgin_tree, overlay.virgin_tree
440 if tree1.encoder.encoding != tree2.encoder.encoding:
441 raise OverlayError("Incompatible templates: encoding mismatch")
442 if keep:
443 self._left, self._right = original, overlay
444 else:
445 self._left, self._right = None, None
446 filename = "<overlay>"
447 if original.mtime is not None and overlay.mtime is not None:
448 mtime = max(original.mtime, overlay.mtime)
449 else:
450 mtime = None
451 super(OverlayTemplate, self).__init__(
452 tree1.overlay(tree2), filename, mtime, original.factory
453 )
454
456 """ Return a clean template """
457 if self._left is not None:
458 original = self._left.template()
459 overlay = self._right.template()
460 if original is not self._left or overlay is not self._right:
461 return self.__class__(original, overlay, keep=True)
462 return self
463
464 - def reload(self, force=False):
465 """
466 Reload template(s) if possible and needed
467
468 :Parameters:
469 `force` : ``bool``
470 Force reload (if possible)?
471
472 :Return: The reloaded template or self
473 :Rtype: `Template`
474 """
475 if self._left is not None:
476 original = self._left.reload(force=force)
477 overlay = self._right.reload(force=force)
478 if force or \
479 original is not self._left or overlay is not self._right:
480 return self.__class__(original, overlay, keep=True)
481 return self
482
484 """
485 Check for update
486
487 :Return: Update available?
488 :Rtype: ``bool``
489 """
490 if self._left is not None:
491 return (
492 self._left.update_available() or
493 self._right.update_available()
494 )
495 return False
496
497
499 """ Autoupdate wrapper """
500
501 - def __init__(self, template, _cb=None):
502 """
503 Initialization
504
505 :Parameters:
506 `template` : `Template`
507 The template to autoupdate
508 """
509 self._template = template.template()
510 if _cb is None:
511 self._cb = []
512 else:
513 self._cb = list(_cb)
514
516 """
517 Pass through every request to the original template
518
519 The template is checked before and possibly replaced if it changed.
520
521 :Parameters:
522 `name` : ``str``
523 Name to lookup
524
525 :Return: The requested value
526 :Rtype: any
527
528 :Exceptions:
529 - `AttributeError` : not found
530 """
531
532 return getattr(self.reload(force=False)._template, name)
533
535 """
536 Create new autoupdate wrapper from instance.
537
538 This is needed, when adding template wrappers.
539
540 :Parameters:
541 `new` : any
542 Template object
543
544 :Return: Autoupdated-wrapped template
545 :Rtype: `AutoUpdate`
546 """
547 return self.__class__(new, _cb=self._cb)
548
550 """
551 Register an autoupdate callback function
552
553 The function will be called, every time a template is reloaded
554 automatically. The template is passed as only argument to the callback
555 function.
556
557 :Parameters:
558 `callback` : ``callable``
559 The callback function.
560 """
561 self._cb.append(callback)
562
564 """
565 Overlay this template with another one
566
567 :Parameters:
568 `other` : `Template`
569 The template layed over self
570
571 :Return: The combined template
572 :Rtype: `Template`
573 """
574 return self.__class__(
575 OverlayTemplate(self, other, keep=True), _cb=self._cb
576 )
577
579 """ Return a clean template (without any wrapper) """
580 return self._template.template()
581
583 """
584 Check for update
585
586 :Return: Update available?
587 :Rtype: ``bool``
588 """
589 return self._template.update_available()
590
591 - def reload(self, force=False):
592 """
593 Reload template(s) if possible and needed
594
595 :Parameters:
596 `force` : ``bool``
597 Force reload (if possible)?
598
599 :Return: The reloaded (new) template or self
600 :Rtype: `Template`
601 """
602 template = self._template
603 if force or template.update_available():
604 try:
605 self._template = template.reload(force=force)
606 except TemplateReloadError, e:
607 AutoUpdateWarning.emit(
608 'Template autoupdate failed: %s' % str(e)
609 )
610 for func in list(self._cb):
611 func(self)
612
613 return self
614
615
617 """
618 Wrapped template base class
619
620 This class can be used to extend the hooks provided by the regular
621 template class. Inherit from it and overwrite the methods you need. This
622 class just defines the basics.
623
624 :IVariables:
625 `_template` : `Template`
626 Original template instance
627
628 `_opts` : any
629 Options passed via constructor
630 """
631 __slots__ = ('_opts', '_original')
632
633 - def __new__(cls, template, opts=None):
634 """
635 Construct
636
637 We may return an ``AutoUpdate`` instance here, wrapping the actual
638 instance.
639
640 :Parameters:
641 `template` : `Template`
642 Original template instance
643
644 `opts` : any
645 Options
646 """
647 self = super(WrappedTemplate, cls).__new__(cls)
648 factory = getattr(template, 'autoupdate_factory', None)
649 if factory is not None:
650 self.__init__(template, opts=opts)
651 return factory(self)
652 return self
653
654 - def __init__(self, template, opts=None):
655 """
656 Initialization
657
658 :Parameters:
659 `template` : `Template`
660 Original template instance
661
662 `opts` : any
663 Options
664 """
665 self._original = tpl = template.template()
666 self._opts = opts
667 super(WrappedTemplate, self).__init__(
668 tpl.virgin_tree, tpl.filename, tpl.mtime, tpl.factory, tpl.loader
669 )
670
672 """
673 Overlay this template with another one
674
675 :Parameters:
676 `other` : `Template`
677 The template layed over self
678
679 :Return: The combined template
680 :Rtype: `Template`
681 """
682 return self.__class__(self._original.overlay(other), opts=self._opts)
683
685 """
686 Check for update
687
688 :Return: Update available?
689 :Rtype: ``bool``
690 """
691 return self._original.update_available()
692
693 - def reload(self, force=False):
694 """
695 Reload template(s) if possible and needed
696
697 :Parameters:
698 `force` : ``bool``
699 Force reload (if possible)?
700
701 :Return: The reloaded (new) template or self
702 :Rtype: `Template`
703 """
704 if force or self.update_available():
705 self.__init__(self._original.reload(force=force), self._opts)
706 return self
707