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

Source Code for Module tdi.tools.template

  1  # -*- coding: ascii -*- 
  2  r""" 
  3  :Copyright: 
  4   
  5   Copyright 2013 - 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 Tools 
 24  ================ 
 25   
 26  Template Tools. 
 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   
 34  import pprint as _pprint 
 35   
 36  from .._exceptions import (  # noqa  # pylint: disable = unused-import 
 37      DependencyError, DependencyCycle 
 38  ) 
 39  from .. import _graph 
40 41 42 -class TemplateUndecided(DependencyError):
43 """ 44 Template could not be determined, because the decision is ambiguous 45 46 The exception argument is a dict mapping undecidable overlay names to 47 the respective set of template names 48 """
49
50 51 -class TemplateListProxy(object):
52 """ 53 Proxy `TemplateList` instances for lazy creation 54 55 :IVariables: 56 `_list` : ``callable`` 57 List creator 58 59 `__list` : ``list`` 60 List cache (if possible) 61 """ 62
63 - def __init__(self, creator, autoreload):
64 """ 65 Initialization 66 67 :Parameters: 68 `creator` : ``callable`` 69 List creator function 70 71 `autoreload` : ``bool`` 72 Autoreload possible? 73 """ 74 if autoreload: 75 create = creator 76 else: 77 self.__list = None 78 79 def create(): 80 """ self-destroying creator """ 81 if self.__list is None: 82 self.__list = creator() 83 self._list = lambda: self.__list 84 return self.__list
85 self._list = create
86
87 - def __getattr__(self, name):
88 return getattr(self._list(), name)
89
90 - def __len__(self):
91 return len(self._list())
92
93 - def __repr__(self):
94 return repr(self._list())
95
96 - def __str__(self):
97 return str(self._list())
98
99 - def __add__(self, other):
100 return self._list() + other
101
102 - def __mul__(self, other):
103 return self._list() * other
104
105 - def __getitem__(self, index):
106 return self._list()[index]
107
108 - def __contains__(self, item):
109 return item in self._list()
110
111 - def __hash__(self):
112 return hash(self._list())
113
114 - def __cmp__(self, other):
115 return cmp(self._list(), other)
116
117 - def __iter__(self):
118 return iter(self._list())
119
120 121 -class TemplateList(list):
122 """ 123 Container for template names 124 125 This class contains the resulting template list, generated by the 126 layout code. 127 128 :IVariables: 129 `missing` : ``list`` or ``None`` 130 Missing overlays 131 """ 132 missing = None 133
134 - def __init__(*args, **kwargs): # pylint: disable = no-method-argument
135 """ 136 Initialization 137 138 :Keywords: 139 `MISSING` : ``iterable`` 140 Missing overlay list 141 """ 142 self, args = args[0], args[1:] 143 missing = kwargs.pop('MISSING', None) 144 if kwargs: 145 raise TypeError("Unrecognized keywords") 146 super(TemplateList, self).__init__(*args) 147 self.missing = list(missing or ()) or None
148
149 - def __repr__(self):
150 """ 151 Debug representation 152 153 :Return: The debug string 154 :Rtype: ``str`` 155 """ 156 return "%s(%s%s,%sMISSING=%s%s)" % ( 157 self.__class__.__name__, 158 self and '\n' or '', 159 _pprint.pformat(list(self)), 160 self and '\n\n ' or ' ', 161 self.missing and '\n' or ' ', 162 _pprint.pformat(self.missing) 163 )
164 165 @classmethod
166 - def discover(cls, loader, names, use=None, ignore=None):
167 """ 168 Disover templates and create a new template list 169 170 :Parameters: 171 `loader` : `Loader` 172 Template loader 173 174 `names` : ``iterable`` 175 Base names. These templates are always added first, in order and 176 define the initial list of overlays to discover. 177 178 `use` : ``dict`` 179 Extra target mapping (overlay name -> template name). This is 180 used, before the global overlay mapping is asked. Pass ambiguous 181 overlay decisions here, or disable certain overlays by passing 182 ``None`` as name. 183 184 `ignore` : ``iterable`` 185 List of template names to ignore completely. 186 187 :Return: Template list 188 :Rtype: `TemplateList` 189 190 :Exceptions: 191 - `TemplateUndecided` : Ambiguous template decisions 192 - `DependencyCycle` : A dependency cycle occured 193 """ 194 result, missing, undecided = discover( 195 loader, names, use=use, ignore=ignore 196 ) 197 if undecided: 198 raise TemplateUndecided(undecided) 199 return cls(result, MISSING=missing)
200
201 202 -class Layout(object):
203 """ 204 Create template lists based on a start configuration 205 206 :IVariables: 207 `_base` : ``tuple`` 208 Base template list 209 210 `_use` : ``dict`` 211 extra overlay -> filename mapping 212 213 `_ignore` : ``frozenset`` 214 Template names to ignore 215 """ 216
217 - def __init__(self, loader, *base, **kwargs):
218 """ 219 Initialization 220 221 :Parameters: 222 `loader` : `Loader` 223 Template loader 224 225 `base` : ``tuple`` 226 Base template list 227 228 `kwargs` : ``dict`` 229 Keywords 230 231 :Keywords: 232 `use` : ``dict`` 233 extra overlay -> filename mapping 234 235 `ignore` : ``iterable`` 236 template names to ignore 237 238 `cls` : ``callable`` 239 template list factory. If omitted or ``None``, `TemplateList` is 240 used. 241 242 `lazy` : ``bool`` 243 Lazy loading? 244 """ 245 use = kwargs.pop('use', None) 246 ignore = kwargs.pop('ignore', None) 247 cls = kwargs.pop('cls', None) 248 lazy = kwargs.pop('lazy', None) 249 if kwargs: 250 raise TypeError("Unrecognized keywords") 251 self._base = base 252 self._use = dict(use or ()) 253 self._ignore = frozenset(ignore or ()) 254 self._loader = loader 255 self._cls = cls is None and TemplateList or cls 256 self._lazy = lazy is None and True or bool(lazy)
257
258 - def extend(self, *base, **kwargs):
259 """ 260 Extend the layout and create a new one. 261 262 :Parameters: 263 `base` : ``tuple`` 264 Base template list 265 266 `kwargs` : ``dict`` 267 Keywords 268 269 :Keywords: 270 `use` : ``dict`` 271 extra overlay -> filename mapping 272 273 `ignore` : ``iterable`` 274 template names to ignore 275 276 `consider` : ``iterable`` 277 Template names to "unignore" 278 279 :Return: New layout 280 :Rtype: `Layout` 281 """ 282 use = kwargs.pop('use', None) 283 ignore = kwargs.pop('ignore', None) 284 consider = kwargs.pop('consider', None) 285 if kwargs: 286 raise TypeError("Unrecognized keywords") 287 newbase = tuple(self._base) + base 288 newuse = self._use 289 if use: 290 newuse = dict(newuse) 291 newuse.update(use) 292 newignore = self._ignore 293 if ignore or consider: 294 newignore = set(newignore) 295 if ignore: 296 newignore.update(set(ignore)) 297 if consider: 298 newignore -= set(consider) 299 return self.__class__(self._loader, *newbase, **dict( 300 use=newuse, ignore=newignore, cls=self._cls, lazy=self._lazy 301 ))
302
303 - def __call__(self, *names, **kwargs):
304 """ 305 Create a template list from this layout 306 307 :Parameters: 308 `names` : ``tuple`` 309 Base template list 310 311 `kwargs` : ``dict`` 312 Keywords 313 314 :Keywords: 315 `use` : ``dict`` 316 extra overlay -> filename mapping 317 318 `ignore` : ``iterable`` 319 template names to ignore 320 321 `consider` : ``iterable`` 322 Template names to "unignore" 323 324 :Return: template list 325 :Rtype: `TemplateList` 326 """ 327 use_ = kwargs.pop('use', None) 328 ignore_ = kwargs.pop('ignore', None) 329 consider = kwargs.pop('consider', None) 330 if kwargs: 331 raise TypeError("Unrecognized keywords") 332 base = tuple(self._base) + names 333 use = dict(self._use) 334 if use_: 335 use.update(use_) 336 ignore = set(self._ignore) 337 if ignore_: 338 ignore.update(set(ignore_)) 339 if consider: 340 ignore -= set(consider) 341 342 lazy, autoreload = self._lazy, self._loader.autoreload() 343 344 def make_creator(base, use, ignore): 345 """ Make a new template list creator """ 346 cls, loader = self._cls, self._loader 347 348 def creator(): 349 """ Create """ 350 return cls.discover(loader, base, use=use, ignore=ignore)
351 return creator
352 creator = make_creator(base, use, ignore) 353 if not lazy and not autoreload: 354 return creator() 355 result = TemplateListProxy(creator, autoreload) 356 if not lazy: 357 iter(result) # trigger list creation 358 return result 359
360 361 -def distinct_overlays(tpl):
362 """ 363 Extract distinct overlay names of a template 364 365 Overlay names available both as target and source within the template 366 are discarded. 367 368 :Parameters: 369 `tpl` : `tdi.template.Template` 370 Template to inspect 371 372 :Return: set(targets), set(sources) 373 :Rtype: ``tuple`` 374 """ 375 targets = set(tpl.target_overlay_names) 376 sources = set(tpl.source_overlay_names) 377 return targets - sources, sources - targets
378
379 380 -def discover(loader, names, use=None, ignore=None):
381 """ 382 Find templates to use and order them topologically correct 383 384 :Parameters: 385 `loader` : `Loader` 386 Template loader 387 388 `names` : ``iterable`` 389 Base names. These templates are always added first, in order and 390 define the initial list of overlays to discover. 391 392 `use` : ``dict`` 393 Extra target mapping (overlay name -> template name). This is 394 used, before the global overlay mapping is asked. Pass ambiguous 395 overlay decisions here, or disable certain overlays by passing 396 ``None`` as name. 397 398 `ignore` : ``iterable`` 399 List of template names to ignore completely. 400 401 :Return: list(template names), set(missing overlays), 402 dict(undecidable overlays -> possible template names) 403 :Rtype: ``tuple`` 404 405 :Exceptions: 406 - `DependencyCycle` : A dependency cycle occured 407 """ 408 # pylint: disable = too-many-branches 409 410 names, missing, undecided = list(names), set(), {} 411 if not names: 412 return names, missing, undecided 413 414 overlays = lambda x: distinct_overlays(loader.load(x)) 415 available = loader.available() 416 417 names.reverse() 418 dep = names.pop() 419 use, ignore = dict(use or ()), set(ignore or ()) 420 targets, graph = set(overlays(dep)[0]), _graph.DependencyGraph() 421 422 # initial templates 423 while names: 424 tname = names.pop() 425 ttargets, tsources = overlays(tname) 426 targets -= tsources 427 targets |= ttargets 428 graph.add(dep, tname) 429 dep = tname 430 431 # automatic templates 432 targets = dict((target, set([dep])) for target in targets) 433 while targets: 434 target, deps = targets.popitem() 435 ttargets = None 436 437 if target in use: 438 tname = use[target] 439 if tname is None: 440 missing.add(target) 441 continue 442 else: 443 ttargets, tsources = overlays(tname) 444 if target not in tsources: 445 raise AssertionError('"use" source %r not in %r' % ( 446 target, tname 447 )) 448 else: 449 tnames = [ 450 tname 451 for tname in available.get(target) or () 452 if tname not in ignore 453 ] 454 if not tnames: 455 missing.add(target) 456 continue 457 elif len(tnames) > 1: 458 undecided[target] = tuple(sorted(tnames)) 459 continue 460 tname = tnames[0] 461 462 if ttargets is None: 463 ttargets = overlays(tname)[0] 464 for dep in deps: 465 graph.add(dep, tname) 466 for target in ttargets: 467 if target not in targets: 468 targets[target] = set() 469 targets[target].add(tname) 470 471 return graph.resolve(), missing, undecided
472
473 474 -class Loader(object):
475 """ 476 Find, load and select templates 477 478 :IVariables: 479 `_available` : ``dict`` or ``None`` 480 The mapping cache. This dict contains the overlay -> template mapping. 481 If ``None``, the dict is created during the next call of the 482 ``available`` method. 483 484 `_registered` : ``set`` 485 List of template names registered for autoreload 486 487 `_load` : callable 488 Loader 489 490 `_list` : callable 491 Lister 492 493 `_select` : callable 494 Selector 495 """ 496
497 - def __init__(self, list, load, select):
498 """ 499 Initialization 500 501 :Parameters: 502 `list` : callable 503 Template lister. This function is called without parameters and 504 expected to return a list of all template names available. 505 Template names are hashable tokens (such as strings) identifying 506 the templates. They are passed to the `load` function if the 507 template needs to be loaded. 508 509 `load` : callable 510 Template loader. This function is called with a template name as 511 parameter and expected to return the actual template object. 512 513 `select` : callable 514 Template selector. This function is called with two parameters: 515 The loader instance (self) and the template name. It is expected 516 to return a bool value, which decides whether this template is 517 in the pool for automatic templates or not. 518 """ 519 # pylint: disable = redefined-builtin 520 521 self._available = None 522 self._registered = set() 523 self._load = load 524 self._list = list 525 self._select = select
526
527 - def autoreload(self):
528 """ 529 Autoreload templates? 530 531 :Return: Autoreloading available? 532 :Rtype: ``bool`` 533 """ 534 return bool(self._registered)
535
536 - def _callback(self, _):
537 """ Autoupdate callback - reset the source mapping """ 538 self._available = None
539
540 - def load(self, name):
541 """ 542 Load a single template and register the autoupdate callback 543 544 :Parameters: 545 `name` : hashable 546 Template name 547 548 :Return: The template 549 :Rtype: `tdi.template.Template` 550 """ 551 tpl = self._load(name) 552 if name not in self._registered: 553 register = getattr(tpl, 'autoupdate_register_callback', None) 554 if register is not None: 555 register(self._callback) 556 self._registered.add(name) 557 return tpl
558
559 - def available(self):
560 """ 561 Determine automatic overlay -> template name mapping 562 563 This method should only list the automatic overlay mappings. 564 565 :Return: source overlay name -> set(template name) 566 :Rtype: ``dict`` 567 """ 568 result = self._available 569 if result is None: 570 result = {} 571 for name in self._list(): 572 if self._select(self, name): 573 tpl = self.load(name) 574 for source in distinct_overlays(tpl)[1]: 575 if source not in result: 576 result[source] = set() 577 result[source].add(name) 578 self._available = result 579 return result
580