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