Package tdi :: Package integration :: Module wtf_service
[frames] | no frames]

Source Code for Module tdi.integration.wtf_service

  1  # -*- coding: ascii -*- 
  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      # pylint: disable = redefined-builtin 
 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   
88 -def _load_dotted(name):
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
126 -def _resource():
127 """ Load resource service """ 128 from __svc__.wtf import resource # pylint: disable = import-error 129 return resource
130 131
132 -class RequestParameterAdapter(object):
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
142 - def __init__(self, param):
143 """ 144 Initialization 145 146 :Parameters: 147 `param` : ``wtf.request.Request.param`` 148 request.param 149 """ 150 self.param = param 151 if self.__class__ is RequestParameterAdapter: 152 self.getlist = param.multi
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
160 - def getlist(self, name): # pylint: disable = method-hidden
161 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """ 162 return self.param.multi(name)
163 164
165 -class DirectoryTemplateLister(object):
166 """ Directory Template Lister """ 167 168 #: Default list of directory names to ignore 169 #: 170 #: :Type: ``tuple`` 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
200 - def __call__(self):
201 """ 202 Walk the directories and yield all template names 203 204 :Return: Iterator over template names 205 :Rtype: ``iterable`` 206 """ 207 # pylint: disable = too-many-branches 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 # fixup directories to recurse 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 # find names 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
248 -class _Memoizer(dict):
249 """ Memoizer storage """ 250 __implements__ = [_interfaces.MemoizerInterface] 251
252 - def __init__(self, *args, **kwargs):
253 """ Initialize """ 254 super(_Memoizer, self).__init__(*args, **kwargs) 255 self.lock = _threading.Lock()
256 257
258 -class GlobalTemplate(object):
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 # while getting the stream and closing it immediately seems 307 # silly... 308 # the logic in self.stream() looks for the file in more than one 309 # directory and needs to try to open it in order to check if it's 310 # possible to open. However, if we want to autoreload our 311 # templates in TDI, TDI doesn't need an open stream, but a 312 # function that can open the stream (the so called stream opener) 313 # so it (TDI) can re-open the stream any time. 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
430 -class ResponseFactory(object):
431 """ 432 Response hint factory collection 433 434 :IVariables: 435 `_global` : `GlobalTemplate` 436 The global service 437 """ 438
439 - def __init__(self, global_template):
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): # pylint: disable = unused-argument 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): # pylint: disable = unused-argument 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): # pylint: disable = unused-argument 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: # python 2.3 doesn't allow changing names 641 pass 642 return render_func 643 644
645 -class Middleware(object):
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
689 -class TDIService(object):
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 # pylint: disable = unused-argument 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
712 - def shutdown(self):
713 """ :See: ``wtf.services.ServiceInterface.shutdown`` """ 714 pass
715
716 - def global_service(self):
717 """ :See: ``wtf.services.ServiceInterface.global_service`` """ 718 return 'tdi', self._global
719
720 - def middleware(self, func):
721 """ :See: ``wtf.services.ServiceInterface.middleware`` """ 722 return Middleware(self._global, func)
723