1
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
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 (
37 DependencyError, DependencyCycle
38 )
39 from .. import _graph
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
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
88 return getattr(self._list(), name)
89
91 return len(self._list())
92
94 return repr(self._list())
95
97 return str(self._list())
98
100 return self._list() + other
101
103 return self._list() * other
104
106 return self._list()[index]
107
109 return item in self._list()
110
112 return hash(self._list())
113
115 return cmp(self._list(), other)
116
118 return iter(self._list())
119
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
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
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
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
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)
358 return result
359
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
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
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
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
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
520
521 self._available = None
522 self._registered = set()
523 self._load = load
524 self._list = list
525 self._select = select
526
528 """
529 Autoreload templates?
530
531 :Return: Autoreloading available?
532 :Rtype: ``bool``
533 """
534 return bool(self._registered)
535
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
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