Package tdi :: Module template
[frames] | no frames]

Source Code for Module tdi.template

  1  # -*- coding: ascii -*- 
  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   
42 -class Template(object):
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
66 - def tree():
67 """ 68 Prepared node tree 69 70 :Type: `tdi.nodetree.Root` 71 """ 72 # pylint: disable = E0211, W0212, W0612, C0111 73 def fget(self): 74 return self._prerender(None, None)
75 return locals()
76 tree = _util.Property(tree) 77
78 - def virgin_tree():
79 """ 80 The node tree without overlay filters 81 82 :Type: `tdi.nodetree.Root` 83 """ 84 # pylint: disable = E0211, W0212, W0612, C0111 85 def fget(self): 86 return self._tree[0]
87 return locals() 88 virgin_tree = _util.Property(virgin_tree) 89
90 - def encoding():
91 """ 92 The template encoding 93 94 :Type: ``str`` 95 """ 96 # pylint: disable = E0211, W0612, C0111 97 def fget(self): 98 return self.virgin_tree.encoder.encoding
99 return locals() 100 encoding = _util.Property(encoding) 101
102 - def source_overlay_names():
103 """ 104 Source overlay names 105 106 :Type: iterable 107 """ 108 # pylint: disable = E0211, W0612, C0111 109 def fget(self): 110 return self.virgin_tree.source_overlay_names
111 return locals() 112 source_overlay_names = _util.Property(source_overlay_names) 113
114 - def target_overlay_names():
115 """ 116 Target overlay names 117 118 :Type: iterable 119 """ 120 # pylint: disable = E0211, W0612, C0111 121 def fget(self): 122 return self.virgin_tree.target_overlay_names
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
152 - def __str__(self):
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
161 - def template(self):
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
191 - def update_available(self):
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
202 - def _prepare(self):
203 """ 204 Prepare the tree 205 206 This is a hook, which is run before prerendering is checked and 207 executed. By default, this applies the overlay filters. 208 209 :Return: prepared tree 210 :Rtype: `tdi.nodetree.Root` 211 """ 212 tree, factory = self._tree[0], self.factory 213 if factory.overlay_filters is None: 214 return tree 215 ffactory = factory.replace(**factory.overlay_filters) 216 return ffactory.from_string(''.join(list(tree.render( 217 _model_adapters.RenderAdapter.for_prerender(None, 218 attr=dict( 219 scope=ffactory.builder().analyze.scope, 220 tdi=ffactory.builder().analyze.attribute, 221 ) 222 ) 223 ))), encoding=tree.encoder.encoding).virgin_tree
224
225 - def _prerender(self, model, adapter):
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 # 1st) Prepare the tree (overlay filters and possibly more) 242 ftree = otree[1] 243 if ftree is None: 244 otree[1] = ftree = self._prepare() 245 246 # Without a model *never* return a prerendered tree 247 if model is None: 248 return ftree 249 250 # 2nd) check if prerendering is actually needed. 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 # The prerendered tree has a version, but the passed 261 # prerendermodel does not contain a prerender_version 262 # method. This is obviously different from the model used 263 # for prerendering (and providing the version) in the first 264 # place. We re-pre-render the template and reset the version 265 # to None. This seems to be the least surprising way. 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 # 3rd) actually prerender 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
407 - def overlay(self, other):
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
421 -class OverlayTemplate(Template):
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
455 - def template(self):
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
483 - def update_available(self):
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
498 -class AutoUpdate(object):
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
515 - def __getattr__(self, name):
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 # pylint: disable = W0212 532 return getattr(self.reload(force=False)._template, name)
533
534 - def autoupdate_factory(self, new):
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
549 - def autoupdate_register_callback(self, callback):
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
563 - def overlay(self, other):
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
578 - def template(self):
579 """ Return a clean template (without any wrapper) """ 580 return self._template.template()
581
582 - def update_available(self):
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
616 -class WrappedTemplate(Template):
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
671 - def overlay(self, other):
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
684 - def update_available(self):
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