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

Source Code for Module tdi.template

  1  # -*- coding: ascii -*- 
  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      # pylint: disable = redefined-builtin 
 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 
42 43 44 -class Template(object):
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
69 - def tree():
70 """ 71 Prepared node tree 72 73 :Type: `tdi.nodetree.Root` 74 """ 75 # pylint: disable = no-method-argument, unused-variable 76 # pylint: disable = protected-access, missing-docstring 77 78 def fget(self): 79 return self._prerender(None, None)
80 return locals()
81 82 @_util.Property
83 - def virgin_tree():
84 """ 85 The node tree without overlay filters 86 87 :Type: `tdi.nodetree.Root` 88 """ 89 # pylint: disable = no-method-argument, unused-variable 90 # pylint: disable = protected-access, missing-docstring 91 92 def fget(self): 93 return self._tree[0]
94 return locals() 95 96 @_util.Property
97 - def encoding():
98 """ 99 The template encoding 100 101 :Type: ``str`` 102 """ 103 # pylint: disable = no-method-argument, unused-variable 104 # pylint: disable = missing-docstring 105 106 def fget(self): 107 return self.virgin_tree.encoder.encoding
108 return locals() 109 110 @_util.Property
111 - def source_overlay_names():
112 """ 113 Source overlay names 114 115 :Type: iterable 116 """ 117 # pylint: disable = no-method-argument, unused-variable 118 # pylint: disable = missing-docstring 119 120 def fget(self): 121 return self.virgin_tree.source_overlay_names
122 return locals() 123 124 @_util.Property
125 - def target_overlay_names():
126 """ 127 Target overlay names 128 129 :Type: iterable 130 """ 131 # pylint: disable = no-method-argument, unused-variable 132 # pylint: disable = missing-docstring 133 134 def fget(self): 135 return self.virgin_tree.target_overlay_names
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
164 - def __str__(self):
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
173 - def template(self):
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
203 - def update_available(self):
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
214 - def _prepare(self):
215 """ 216 Prepare the tree 217 218 This is a hook, which is run before prerendering is checked and 219 executed. By default, this applies the overlay filters. 220 221 :Return: prepared tree 222 :Rtype: `tdi.nodetree.Root` 223 """ 224 tree, factory = self._tree[0], self.factory 225 if factory.overlay_filters is None: 226 return tree 227 ffactory = factory.replace(**factory.overlay_filters) 228 return ffactory.from_string(''.join(list(tree.render( 229 _model_adapters.RenderAdapter.for_prerender(None, attr=dict( 230 scope=ffactory.builder().analyze.scope, 231 tdi=ffactory.builder().analyze.attribute, 232 )) 233 ))), encoding=tree.encoder.encoding).virgin_tree
234
235 - def _prerender(self, model, adapter):
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 # 1st) Prepare the tree (overlay filters and possibly more) 252 ftree = otree[1] 253 if ftree is None: 254 otree[1] = ftree = self._prepare() 255 256 # Without a model *never* return a prerendered tree 257 if model is None: 258 return ftree 259 260 # 2nd) check if prerendering is actually needed. 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 # The prerendered tree has a version, but the passed 271 # prerendermodel does not contain a prerender_version 272 # method. This is obviously different from the model used 273 # for prerendering (and providing the version) in the first 274 # place. We re-pre-render the template and reset the version 275 # to None. This seems to be the least surprising way. 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 # 3rd) actually prerender 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
418 - def overlay(self, other):
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
431 432 -class OverlayTemplate(Template):
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
466 - def template(self):
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
494 - def update_available(self):
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
508 509 -class AutoUpdate(object):
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
526 - def __getattr__(self, name):
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 # pylint: disable = protected-access 543 return getattr(self.reload(force=False)._template, name)
544
545 - def autoupdate_factory(self, new):
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
560 - def autoupdate_register_callback(self, callback):
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
574 - def overlay(self, other):
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
589 - def template(self):
590 """ Return a clean template (without any wrapper) """ 591 return self._template.template()
592
593 - def update_available(self):
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
626 627 -class WrappedTemplate(Template):
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
682 - def overlay(self, other):
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
695 - def update_available(self):
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