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

Source Code for Module tdi.integration.wtf_service

  1  # -*- coding: ascii -*- 
  2  u""" 
  3  :Copyright: 
  4   
  5   Copyright 2010 - 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   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  __docformat__ = "restructuredtext en" 
 61  __author__ = u"Andr\xe9 Malo" 
 62   
 63  import errno as _errno 
 64  import itertools as _it 
 65  import os as _os 
 66  import posixpath as _posixpath 
 67  try: 
 68      import threading as _threading 
 69  except ImportError: 
 70      import dummy_threading as _threading 
 71   
 72  try: 
 73      from wtf import services as _wtf_services 
 74  except ImportError: 
 75      _wtf_services = None 
 76   
 77  from tdi import factory as _factory 
 78  from tdi import factory_memoize as _factory_memoize 
 79  from tdi import interfaces as _interfaces 
 80  from tdi import model_adapters as _model_adapters 
 81  from tdi import util as _util 
 82  from tdi.markup import factory as _markup_factory 
 83  from tdi.tools import htmlform as _htmlform 
 84   
 85   
86 -def _resource():
87 """ Load resource service """ 88 from __svc__.wtf import resource 89 return resource
90 91
92 -class RequestParameterAdapter(object):
93 """ 94 HTMLForm parameter adapter from request.param 95 96 :IVariables: 97 `param` : ``wtf.request.Request.param`` 98 request.param 99 """ 100 __implements__ = [_htmlform.ParameterAdapterInterface] 101
102 - def __init__(self, param):
103 """ 104 Initialization 105 106 :Parameters: 107 `param` : ``wtf.request.Request.param`` 108 request.param 109 """ 110 self.param = param 111 if self.__class__ is RequestParameterAdapter: 112 self.getlist = param.multi
113
114 - def getfirst(self, name, default=None):
115 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """ 116 if name in self.param: 117 return self.param[name] 118 return default
119
120 - def getlist(self, name): # pylint: disable = E0202
121 """ :See: ``tdi.tools.htmlform.ParameterAdapterInterface`` """ 122 return self.param.multi(name)
123 124
125 -class DirectoryTemplateLister(object):
126 """ Directory Template Lister """ 127 128 #: Default list of directory names to ignore 129 #: 130 #: :Type: ``tuple`` 131 DEFAULT_IGNORE = ('.svn', 'CVS', '.git', '.bzr', '.hg') 132
133 - def __init__(self, directories, extensions, ignore=None):
134 """ 135 Initialization 136 137 :Parameters: 138 `directories` : ``iterable`` 139 List of base directories to scan 140 141 `extensions` : ``iterable`` 142 List of extensions to consider 143 144 `ignore` : ``iterable`` 145 List of directory names to ignore. If omitted or ``None``, 146 `DEFAULT_IGNORE` is applied. 147 """ 148 self._dirs = tuple(_it.imap(str, directories)) 149 self._ext = tuple(_it.imap(str, extensions or ())) 150 self._ci = _os.path.normcase('aA') != 'aA' 151 if ignore is None: 152 ignore = self.DEFAULT_IGNORE 153 if self._ci: 154 self._ignore = frozenset(( 155 _os.path.normcase(item) for item in (ignore or ()) 156 )) 157 else: 158 self._ignore = frozenset(ignore or ())
159
160 - def __call__(self):
161 """ 162 Walk the directories and yield all template names 163 164 :Return: Iterator over template names 165 :Rtype: ``iterable`` 166 """ 167 # pylint: disable = R0912 168 169 seen = set() 170 if _os.path.sep == '/': 171 norm = lambda p: p 172 else: 173 norm = lambda p: p.replace(_os.path.sep, '/') 174 175 for base in self._dirs: 176 baselen = len(_os.path.join(base, '')) 177 reldir = lambda x: x[baselen:] 178 def onerror(_): 179 """ Error handler """ 180 raise
181 for dirpath, dirs, files in _os.walk(base, onerror=onerror): 182 # fixup directories to recurse 183 if self._ignore: 184 newdirs = [] 185 for dirname in dirs: 186 if self._ci: 187 if _os.path.normcase(dirname) in self._ignore: 188 continue 189 elif dirname in self._ignore: 190 continue 191 newdirs.append(dirname) 192 if len(newdirs) != len(dirs): 193 dirs[:] = newdirs 194 195 # find names 196 dirpath = reldir(dirpath) 197 for name in files: 198 if not name.endswith(self._ext): 199 continue 200 if dirpath: 201 name = _posixpath.join(norm(dirpath), name) 202 if name in seen: 203 continue 204 yield name
205 206
207 -class _Memoizer(dict):
208 """ Memoizer storage """ 209 __implements__ = [_interfaces.MemoizerInterface] 210
211 - def __init__(self, *args, **kwargs):
212 """ Initialize """ 213 super(_Memoizer, self).__init__(*args, **kwargs) 214 self.lock = _threading.Lock()
215 216
217 -class GlobalTemplate(object):
218 """ 219 Actual global template service object 220 221 :IVariables: 222 `_dirs` : ``list`` 223 Template locations resolved to directories 224 225 `autoreload` : ``bool`` 226 Automatically reload templates? 227 228 `require_scopes` : ``bool`` 229 Require all scopes? 230 231 `require_methods` : ``bool`` 232 Require all render methods? 233 """ 234
235 - def __init__(self, locations, autoreload=False, require_scopes=False, 236 require_methods=False, filters=None):
237 """ 238 Initialization 239 240 :Parameters: 241 `locations` : iterable 242 Resource locations (``['token', ...]``) 243 244 `autoreload` : ``bool`` 245 Automatically reload templates? 246 247 `require_scopes` : ``bool`` 248 Require all scopes? 249 250 `require_methods` : ``bool`` 251 Require all render methods? 252 253 `filters` : ``dict`` 254 Filter factories to apply 255 """ 256 self._dirs = list(_it.chain(*[_resource()[location] 257 for location in locations])) 258 self.autoreload = autoreload 259 self.require_scopes = require_scopes 260 self.require_methods = require_methods 261 262 def streamopen(name): 263 """ Stream opener """ 264 # while getting the stream and closing it immediately seems 265 # silly... 266 # the logic in self.stream() looks for the file in more than one 267 # directory and needs to try to open it in order to check if it's 268 # possible to open. However, if we want to autoreload our 269 # templates in TDI, TDI doesn't need an open stream, but a 270 # function that can open the stream (the so called stream opener) 271 # so it (TDI) can re-open the stream any time. 272 stream, filename = self.stream(name) 273 stream.close() 274 return (_factory.file_opener, filename), filename
275 276 def loader(which, post_load=None, **kwargs): 277 """ Template loader """ 278 kwargs['autoupdate'] = autoreload 279 kwargs['memoizer'] = _Memoizer() 280 factory = _factory_memoize.MemoizedFactory( 281 getattr(_markup_factory, which).replace(**kwargs) 282 ) 283 sfactory = factory.replace(overlay_eventfilters=[]) 284 def load(names): 285 """ Actual loader """ 286 res = factory.from_streams(names, streamopen=streamopen) 287 for item in post_load or (): 288 res = item(res) 289 return res
290 291 def single(name): 292 """ Single file loader """ 293 return sfactory.from_opener(*streamopen(name)[0]) 294 return load, single 295 296 def opt(option, args): 297 """ Find opt """ 298 for arg in args: 299 try: 300 option = option[arg] 301 except (TypeError, KeyError): 302 return None 303 return [unicode(opt).encode('utf-8') for opt in option] 304 305 def load(*args): 306 """ Actually load factories """ 307 return map( 308 _util.load_dotted, filter(None, opt(filters, args) or ()) 309 ) or None 310 311 self.html, self.html_file = loader('html', 312 post_load=load('html', 'template'), 313 eventfilters=load('html', 'load'), 314 overlay_eventfilters=load('html', 'overlay'), 315 ) 316 self.xml, self.xml_file = loader('xml', 317 post_load=load('xml', 'template'), 318 eventfilters=load('xml', 'load'), 319 overlay_eventfilters=load('xml', 'overlay'), 320 ) 321 self.text, self.text_file = loader('text', 322 post_load=load('text', 'template'), 323 eventfilters=load('text', 'load'), 324 overlay_eventfilters=load('text', 'overlay'), 325 ) 326
327 - def lister(self, extensions, ignore=None):
328 """ 329 Create template lister from our own config 330 331 :Parameters: 332 `extensions` : ``iterable`` 333 List of file extensions to consider (required) 334 335 `ignore` : ``iterable`` 336 List of (simple) directory names to ignore. If omitted or 337 ``None``, a default list is applied 338 (`DirectoryTemplateLister.DEFAULT_IGNORE`) 339 340 :Return: a template lister 341 :Rtype: ``callable`` 342 """ 343 return DirectoryTemplateLister([ 344 rsc.resolve('.').filename for rsc in self._dirs 345 ], extensions, ignore=ignore)
346
347 - def stream(self, name, mode='rb', buffering=-1, blockiter=0):
348 """ 349 Locate file in the template directories and open a stream 350 351 :Parameters: 352 `name` : ``str`` 353 The relative filename 354 355 `mode` : ``str`` 356 The opening mode 357 358 `buffering` : ``int`` 359 buffering spec 360 361 `blockiter` : ``int`` 362 Iterator mode 363 (``1: Line, <= 0: Default chunk size, > 1: This chunk size``) 364 365 :Return: The resource stream 366 :Rtype: ``wtf.app.services.resources.ResourceStream`` 367 368 :Exceptions: 369 - `IOError` : File not found 370 """ 371 for location in self._dirs: 372 try: 373 loc = location.resolve(name) 374 return loc.open( 375 mode=mode, buffering=buffering, blockiter=blockiter 376 ), loc.filename 377 except IOError, e: 378 if e[0] == _errno.ENOENT: 379 continue 380 raise 381 raise IOError(_errno.ENOENT, name)
382 383
384 -class ResponseFactory(object):
385 """ 386 Response hint factory collection 387 388 :IVariables: 389 `_global` : `GlobalTemplate` 390 The global service 391 """ 392
393 - def __init__(self, global_template):
394 """ 395 Initialization 396 397 :Parameters: 398 `global_template` : `GlobalTemplate` 399 The global template service 400 """ 401 # pylint: disable = R0912 402 # (too many branches) 403 404 def adapter(model): 405 """ Adapter factory """ 406 return _model_adapters.RenderAdapter(model, 407 requiremethods=global_template.require_methods, 408 requirescopes=global_template.require_scopes, 409 )
410 411 def load_html(response): 412 """ Response factory for ``load_html`` """ 413 # pylint: disable = W0613 414 def load_html(*names): 415 """ 416 Load TDI template 417 418 :Parameters: 419 `names` : ``tuple`` 420 The template names. If there's more than one name 421 given, the templates are overlayed. 422 423 :Return: The TDI template 424 :Rtype: ``tdi.template.Template`` 425 """ 426 return global_template.html(names)
427 return load_html 428 def render_html(response): 429 """ Response factory for ``render_html`` """ 430 return self._render_factory( 431 response, global_template.html, adapter, 432 "render_html", 'text/html' 433 ) 434 def pre_render_html(response): 435 """ Response factory for ``pre_render_html`` """ 436 return self._render_factory( 437 response, global_template.html, adapter, 438 "pre_render_html", 'text/html', pre=True 439 ) 440 441 def load_xml(response): 442 """ Response factory for ``load_xml`` """ 443 # pylint: disable = W0613 444 def load_xml(*names): 445 """ 446 Load TDI template 447 448 :Parameters: 449 `names` : ``tuple`` 450 The template names. If there's more than one name 451 given, the templates are overlayed. 452 453 :Return: The TDI template 454 :Rtype: ``tdi.template.Template`` 455 """ 456 return global_template.xml(names) 457 return load_xml 458 def render_xml(response): 459 """ Response factory for ``render_xml`` """ 460 return self._render_factory( 461 response, global_template.xml, adapter, 462 "render_xml", 'text/xml' 463 ) 464 def pre_render_xml(response): 465 """ Response factory for ``pre_render_xml`` """ 466 return self._render_factory( 467 response, global_template.xml, adapter, 468 "pre_render_xml", 'text/xml', pre=True, 469 ) 470 471 def load_text(response): 472 """ Response factory for ``load_text`` """ 473 # pylint: disable = W0613 474 def load_text(*names): 475 """ 476 Load TDI template 477 478 :Parameters: 479 `names` : ``tuple`` 480 The template names. If there's more than one name 481 given, the templates are overlayed. 482 483 :Return: The TDI template 484 :Rtype: ``tdi.template.Template`` 485 """ 486 return global_template.text(names) 487 return load_text 488 def render_text(response): 489 """ Response factory for ``render_text`` """ 490 return self._render_factory( 491 response, global_template.text, adapter, 492 "render_text", 'text/plain' 493 ) 494 def pre_render_text(response): 495 """ Response factory for ``pre_render_text`` """ 496 return self._render_factory( 497 response, global_template.text, adapter, 498 "pre_render_text", 'text/plain', pre=True, 499 ) 500 501 self.env = { 502 'wtf.response.load_html': load_html, 503 'wtf.response.render_html': render_html, 504 'wtf.response.pre_render_html': pre_render_html, 505 'wtf.response.load_xml': load_xml, 506 'wtf.response.render_xml': render_xml, 507 'wtf.response.pre_render_xml': pre_render_xml, 508 'wtf.response.load_text': load_text, 509 'wtf.response.render_text': render_text, 510 'wtf.response.pre_render_text': pre_render_text, 511 } 512
513 - def _render_factory(self, response, template_loader, adapter, func_name, 514 content_type_, pre=False):
515 """ 516 Response factory for ``render_html/xml/text`` 517 518 :Parameters: 519 `response` : ``wtf.response.Response`` 520 The response object 521 522 `template_loader` : ``callable`` 523 Template loader function 524 525 `adapter` : ``callable`` 526 render adapter factory 527 528 `func_name` : ``str`` 529 Name of the render function (only for introspection) 530 531 `content_type_` : ``str`` 532 Default content type 533 534 `pre` : ``bool`` 535 Prerender only? 536 537 :Return: The render callable 538 :Rtype: ``callable`` 539 """ 540 def render_func(model, *names, **kwargs): 541 """ 542 Simplified TDI invocation 543 544 The following keyword arguments are recognized: 545 546 ``startnode`` : ``str`` 547 The node to render. If omitted or ``None``, the whole 548 template is rendered. 549 ``prerender`` : any 550 Prerender-Model to apply 551 ``content_type`` : ``str`` 552 Content type to set. If omitted, the default content type 553 will be set. If ``None``, the content type won't be set. 554 555 :Parameters: 556 `model` : any 557 The model instance 558 559 `names` : ``tuple`` 560 The template names. If there's more than one name 561 given, the templates are overlayed. 562 563 `kwargs` : ``dict`` 564 Additional keyword parameters 565 566 :Return: Iterable containing the rendered template 567 :Rtype: ``iterable`` 568 569 :Exceptions: 570 - `Exception` : Anything what happened during rendering 571 """ 572 startnode = kwargs.pop('startnode', None) 573 prerender = kwargs.pop('prerender', None) 574 content_type = kwargs.pop('content_type', content_type_) 575 if kwargs: 576 raise TypeError("Unrecognized kwargs: %r" % kwargs.keys()) 577 578 tpl = template_loader(names) 579 if content_type is not None: 580 encoding = tpl.encoding 581 response.content_type(content_type, charset=encoding) 582 if pre and prerender is not None: 583 return [tpl.render_string( 584 prerender, startnode=startnode, 585 adapter=_model_adapters.RenderAdapter.for_prerender, 586 )] 587 return [tpl.render_string(model, adapter=adapter, 588 startnode=startnode, prerender=prerender, 589 )]
590 try: 591 render_func.__name__ = func_name # pylint: disable = W0622 592 except TypeError: # python 2.3 doesn't allow changing names 593 pass 594 return render_func 595 596
597 -class Middleware(object):
598 """ 599 Template middleware 600 601 :IVariables: 602 `_func` : ``callable`` 603 Next WSGI handler 604 605 `_env` : ``dict`` 606 Environment update 607 """ 608
609 - def __init__(self, global_template, func):
610 """ 611 Initialization 612 613 :Parameters: 614 `global_template` : `GlobalTemplate` 615 The global template service 616 617 `func` : ``callable`` 618 WSGI callable to wrap 619 """ 620 self._func = func 621 self._env = ResponseFactory(global_template).env
622
623 - def __call__(self, environ, start_response):
624 """ 625 Middleware handler 626 627 :Parameters: 628 `environ` : ``dict`` 629 WSGI environment 630 631 `start_response` : ``callable`` 632 Start response callable 633 634 :Return: WSGI response iterable 635 :Rtype: ``iterable`` 636 """ 637 environ.update(self._env) 638 return self._func(environ, start_response)
639 640
641 -class TDIService(object):
642 """ 643 Template service 644 645 :IVariables: 646 `_global` : `GlobalTemplate` 647 The global service 648 """ 649 if _wtf_services is not None: 650 __implements__ = [_wtf_services.ServiceInterface] 651
652 - def __init__(self, config, opts, args):
653 """ Initialization """ 654 # pylint: disable = W0613 655 self._global = GlobalTemplate( 656 config.tdi.locations, 657 config.tdi('autoreload', False), 658 config.tdi('require_scopes', False), 659 config.tdi('require_methods', False), 660 config.tdi('filters', None), 661 )
662
663 - def shutdown(self):
664 """ :See: ``wtf.services.ServiceInterface.shutdown`` """ 665 pass
666
667 - def global_service(self):
668 """ :See: ``wtf.services.ServiceInterface.global_service`` """ 669 return 'tdi', self._global
670
671 - def middleware(self, func):
672 """ :See: ``wtf.services.ServiceInterface.middleware`` """ 673 return Middleware(self._global, func)
674