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