1
2 r"""
3 :Copyright:
4
5 Copyright 2006 - 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 Factories
24 ====================
25
26 Template Factories.
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 os as _os
35 import sys as _sys
36 try:
37 import cStringIO as _string_io
38 except ImportError:
39 import StringIO as _string_io
40 try:
41 import threading as _threading
42 except ImportError:
43 import dummy_threading as _threading
44
45 from ._exceptions import TemplateFactoryError
46 from . import filters as _filters
47 from . import template as _template
48 from . import _util
52 """
53 Template loader
54
55 :IVariables:
56 `_args` : ``dict``
57 Initialization arguments
58
59 `_new_parser` : ``callable``
60 Parser factory
61
62 `_streamfilters` : iterable
63 List of stream filter factories (``(file, ...)``)
64
65 `_chunksize` : ``int``
66 Chunk size when reading templates
67 """
68
69 - def __init__(self, parser, builder, encoder, decoder,
70 eventfilters=None, streamfilters=None,
71 default_eventfilter_list=None,
72 default_streamfilter_list=None,
73 default_eventfilters=True, default_streamfilters=True,
74 chunksize=None):
75 """
76 Initialization
77
78 :Parameters:
79 `parser` : ``callable``
80 Parser factory (takes a builder instance and the decoder,
81 returns `ParserInterface`)
82
83 `builder` : ``callable``
84 Tree builder factory (takes the `encoder` and the decoder)
85
86 `encoder` : ``callable``
87 Encoder factory (takes the target encoding)
88
89 `decoder` : ``callable``
90 Decoder factory (takes the source encoding)
91
92 `eventfilters` : iterable
93 List of event filter factories
94 (``(BuildingListenerInterface, ...)``)
95
96 `streamfilters` : iterable
97 List of stream filter factories (``(file, ...)``)
98
99 `default_eventfilter_list` : ``list``
100 Default eventfilter list
101
102 `default_streamfilter_list` : ``list``
103 Default streamfilter list
104
105 `default_eventfilters` : ``bool``
106 Apply default eventfilters (before this list)?
107
108 `default_streamfilters` : ``bool``
109 Apply default streamfilters (before this list)?
110
111 `chunksize` : ``int``
112 Chunk size when reading templates
113 """
114
115
116 self._args = dict(locals())
117 del self._args['self']
118
119 default = list(default_streamfilter_list or ())
120 if streamfilters is None:
121 streamfilters = default
122 elif default_streamfilters:
123 streamfilters = default + list(streamfilters)
124 streamfilters.reverse()
125 self._streamfilters = tuple(streamfilters)
126
127 if chunksize is None:
128 chunksize = 8192
129 self._chunksize = chunksize
130
131 default = tuple(default_eventfilter_list or ())
132 if eventfilters is None:
133 eventfilters = default
134 elif default_eventfilters:
135 eventfilters = default + tuple(eventfilters)
136
137 def new_builder():
138 """
139 Make builder instance
140
141 :Return: Builder instance
142 :Rtype: `TreeBuildInterface`
143 """
144 return builder(encoder, decoder)
145
146 self.builder = new_builder
147
148 def make_parser(filename, encoding):
149 """
150 Make parser instance
151
152 :Return: Parser and tree returner
153 :Rtype: ``tuple``
154 """
155 this_builder = _filters.FilterFilename(new_builder(), filename)
156 for item in eventfilters:
157 this_builder = item(this_builder)
158 this_builder.handle_encoding(encoding)
159 return parser(this_builder), this_builder.finalize
160
161 self._new_parser = make_parser
162
163 @_util.Property
165 """
166 The initialization arguments
167
168 :Type: ``dict``
169 """
170
171
172
173 def fget(self):
174 return dict(self._args)
175 return locals()
176
177 - def persist(self, filename, encoding, opener):
178 """
179 Persist loader
180
181 :Parameters:
182 `filename` : ``str``
183 Filename in question
184
185 `encoding` : ``str``
186 Initial template encoding
187
188 `opener` : ``callable``
189 Stream opener, returns stream and mtime
190
191 :Return: persisted loader (takes stream and optional mtime)
192 :Rtype: ``callable``
193 """
194 def load(mtime=None, check_only=False):
195 """
196 Load the template and build the tree
197
198 :Parameters:
199 `mtime` : ``int``
200 Optional mtime key, which is passed to the opener, which can
201 compare it with the current mtime key.
202 """
203 stream, mtime = opener(filename, mtime, check_only=check_only)
204 try:
205 if check_only or stream is None:
206 return stream, mtime
207 return self(stream, filename, encoding), mtime
208 finally:
209 if not check_only and stream is not None:
210 stream.close()
211
212 return load
213
214 - def __call__(self, stream, filename, encoding):
215 """
216 Actually load the template and build the tree
217
218 :Parameters:
219 `stream` : ``file``
220 The stream to read from
221
222 `filename` : ``str``
223 The template filename
224
225 `encoding` : ``str``
226 Initial template encoding
227
228 :Return: The tree
229 :Rtype: `tdi.nodetree.Root`
230 """
231 stream = _filters.StreamFilename(stream, filename)
232 for item in self._streamfilters:
233 stream = item(stream)
234
235 parser, make_tree = self._new_parser(filename, encoding)
236 feed, size, read = parser.feed, self._chunksize, stream.read
237 while True:
238 chunk = read(size)
239 if not chunk:
240 break
241 feed(chunk)
242 parser.finalize()
243 return make_tree()
244
245
246 -def file_opener(filename, mtime, check_only=False):
247 """
248 File stream opener
249
250 :Parameters:
251 `filename` : ``str``
252 Filename
253
254 `mtime` : ``int``
255 mtime to check. If it equals the file's mtime, stream is returned as
256 ``None``
257
258 `check_only` : ``bool``
259 Only check? In this case the returned "stream" is either True (update
260 available) or False (mtime didn't change)
261
262 :Return: The stream and its mtime
263 :Rtype: ``tuple``
264 """
265 if check_only:
266 try:
267 xtime = _os.stat(filename).st_mtime
268 except OSError:
269 xtime = None
270 update = mtime is None or xtime is None or mtime != xtime
271 return update, xtime
272
273 stream = open(filename, 'rb')
274 try:
275 try:
276 xtime = _os.fstat(stream.fileno()).st_mtime
277 except (OSError, AttributeError):
278 xtime = None
279 if mtime is not None and xtime is not None and mtime == xtime:
280 stream, _ = None, stream.close()
281 return stream, xtime
282 except:
283 e = _sys.exc_info()
284 try:
285 stream.close()
286 finally:
287 try:
288 raise e[0], e[1], e[2]
289 finally:
290 del e
291
294 """
295 Overlay a list of templates from left to right
296
297 :Parameters:
298 `templates` : iterable
299 Template list
300
301 :Return: The final template
302 :Rtype: `tdi.template.Template`
303 """
304 templates = list(templates)
305 templates.reverse()
306 try:
307 result = templates.pop()
308 except IndexError:
309 raise TemplateFactoryError("Need at least one template")
310 while templates:
311 result = result.overlay(templates.pop())
312 return result
313
314
315
316
317
318 _global_lock = _threading.Lock()
322 """
323 Decorate a factory method call to possibly memoize the result
324
325 :Parameters:
326 `func` : ``callable``
327 Method's function
328
329 :Return: Decorated function
330 :Rtype: ``callable``
331 """
332 name = func.__name__
333
334 def proxy(*args, **kwargs):
335 """ Proxy """
336 self, key = args[0], kwargs.pop('key', None)
337 cache = self._cache
338 if cache is None or key is None:
339 return func(*args, **kwargs)
340 lock, key = getattr(cache, 'lock', None), (name, key)
341 if lock is None:
342 lock = _global_lock
343 lock.acquire()
344 try:
345 if key in cache:
346 return cache[key]
347 finally:
348 lock.release()
349 res = func(*args, **kwargs)
350 lock.acquire()
351 try:
352 if key in cache:
353 return cache[key]
354 else:
355 cache[key] = res
356 return res
357 finally:
358 lock.release()
359 return _util.decorating(func, extra=dict(key=None))(proxy)
360
363 """
364 Template builder/loader factory
365
366 The method calls are memoized, if:
367
368 - a memoizer is given on instantiation (like ``dict``)
369 - a key is supplied to the method (as keyword argument ``key``). The key
370 must be hashable. You can wrap an automatic key supplier around the
371 factory instance, for example `tdi.factory_memoize.MemoizedFactory`.
372
373 :IVariables:
374 `_loader` : `Loader`
375 Template loader
376
377 `_autoupdate` : ``bool``
378 Should the templates be automatically updated when
379 they change?
380
381 `_cache` : `MemoizerInterface`
382 Memoizer or ``None``
383
384 `overlay_filters` : ``dict``
385 Overlay filters
386
387 `_default_encoding` : ``str``
388 Default encoding
389 """
390
391 - def __init__(self, parser, builder, encoder, decoder,
392 autoupdate=False, eventfilters=None, streamfilters=None,
393 default_eventfilters=True, default_streamfilters=True,
394 default_eventfilter_list=None,
395 default_streamfilter_list=None,
396 overlay_eventfilters=None, overlay_streamfilters=None,
397 overlay_default_eventfilters=True,
398 overlay_default_streamfilters=True,
399 default_encoding='ascii', chunksize=None, memoizer=None):
400 """
401 Initialization
402
403 :Parameters:
404 `parser` : ``callable``
405 Parser factory (takes a builder instance and the decoder,
406 returns `ParserInterface`)
407
408 `builder` : ``callable``
409 Tree builder factory (takes the `encoder` and the decoder)
410
411 `encoder` : ``callable``
412 Encoder factory (takes the target encoding)
413
414 `decoder` : ``callable``
415 Decoder factory (takes the source encoding)
416
417 `autoupdate` : ``bool``
418 Should the templates be automatically updated when
419 they change?
420
421 `eventfilters` : iterable
422 List of event filter factories
423 (``(BuildingListenerInterface, ...)``)
424
425 `streamfilters` : iterable
426 List of stream filter factories (``(file, ...)``)
427
428 `default_eventfilters` : ``bool``
429 Apply default eventfilters (before this list)?
430
431 `default_streamfilters` : ``bool``
432 Apply default streamfilters (before this list)?
433
434 `default_eventfilter_list` : ``iterable``
435 List of default eventfilters
436
437 `default_streamfilter_list` : ``iterable``
438 List of default streamfilters
439
440 `overlay_eventfilters` : iterable
441 List of event filter factories
442 (``(BuildingListenerInterface, ...)``) to apply after all
443 overlaying being done (right before (pre)rendering)
444
445 `overlay_streamfilters` : iterable
446 List of stream filter factories (``(file, ...)``)
447 to apply after all overlaying being done (right before
448 (pre)rendering)
449
450 `overlay_default_eventfilters` : ``bool``
451 Apply default eventfilters (before this list)?
452
453 `overlay_default_streamfilters` : ``bool``
454 Apply default streamfilters (before this list)?
455
456 `default_encoding` : ``str``
457 Default encoding
458
459 `chunksize` : ``int``
460 Chunk size when reading templates
461
462 `memoizer` : `MemoizerInterface`
463 Memoizer to use. If omitted or ``None``, memoization is turned
464 off.
465 """
466
467
468 self._loader = Loader(
469 parser=parser,
470 decoder=decoder,
471 eventfilters=eventfilters,
472 streamfilters=streamfilters,
473 default_eventfilters=default_eventfilters,
474 default_streamfilters=default_streamfilters,
475 default_eventfilter_list=list(default_eventfilter_list or ()),
476 default_streamfilter_list=list(default_streamfilter_list or ()),
477 builder=builder,
478 encoder=encoder,
479 chunksize=chunksize,
480 )
481 if overlay_eventfilters is None and overlay_streamfilters is None:
482 self.overlay_filters = None
483 else:
484 self.overlay_filters = dict(
485 eventfilters=overlay_eventfilters,
486 streamfilters=overlay_streamfilters,
487 default_eventfilters=overlay_default_eventfilters,
488 default_streamfilters=overlay_default_streamfilters,
489 )
490 self._autoupdate = autoupdate
491 self._cache = memoizer
492 self._default_encoding = default_encoding
493
495 """
496 Return a tree builder instance as configured by this factory
497
498 The purpose of the method is mostly internal. It's used to get the
499 builder in order to inspect it.
500
501 :Return: Builder
502 :Rtype: `BuilderInterface`
503 """
504 return self._loader.builder()
505
506 - def replace(self, autoupdate=None, eventfilters=None, streamfilters=None,
507 default_eventfilters=None, default_streamfilters=None,
508 overlay_eventfilters=None, overlay_streamfilters=None,
509 overlay_default_eventfilters=None,
510 overlay_default_streamfilters=None, default_encoding=None,
511 memoizer=None):
512 """
513 Create a new factory instance with replaced values
514
515 :Parameters:
516 `autoupdate` : ``bool``
517 Should the templates be automatically updated when
518 they change? If omitted or ``None``, the current setting is
519 applied.
520
521 `eventfilters` : iterable
522 List of event filter factories
523 (``(BuildingListenerInterface, ...)``)
524
525 `streamfilters` : iterable
526 List of stream filter factories (``(file, ...)``)
527
528 `default_eventfilters` : ``bool``
529 Apply default eventfilters (before this list)?
530
531 `default_streamfilters` : ``bool``
532 Apply default streamfilters (before this list)?
533
534 `overlay_eventfilters` : iterable
535 List of overlay event filter factories
536 (``(BuildingListenerInterface, ...)``)
537
538 `overlay_streamfilters` : iterable
539 List of overlay stream filter factories (``(file, ...)``)
540
541 `overlay_default_eventfilters` : ``bool``
542 Apply overlay default eventfilters (before this list)?
543
544 `overlay_default_streamfilters` : ``bool``
545 Apply overlay default streamfilters (before this list)?
546
547 `default_encoding` : ``str``
548 Default encoding
549
550 `memoizer` : `MemoizerInterface`
551 New memoizer. If omitted or ``None``, the new factory will be
552 initialized without memoizing.
553
554 :Return: New factory instance
555 :Rtype: `Factory`
556 """
557
558
559 args = self._loader.args
560 if autoupdate is None:
561 autoupdate = self._autoupdate
562 args['autoupdate'] = autoupdate
563 if eventfilters is not None:
564 args['eventfilters'] = eventfilters
565 if default_eventfilters is not None:
566 args['default_eventfilters'] = default_eventfilters
567 if streamfilters is not None:
568 args['streamfilters'] = streamfilters
569 if default_streamfilters is not None:
570 args['default_streamfilters'] = default_streamfilters
571
572 if self.overlay_filters:
573 for key, value in self.overlay_filters.iteritems():
574 args['overlay_' + key] = value
575 if overlay_eventfilters is not None:
576 args['overlay_eventfilters'] = overlay_eventfilters
577 if overlay_default_eventfilters is not None:
578 args['overlay_default_eventfilters'] = \
579 overlay_default_eventfilters
580 if overlay_streamfilters is not None:
581 args['overlay_streamfilters'] = overlay_streamfilters
582 if overlay_default_streamfilters is not None:
583 args['overlay_default_streamfilters'] = \
584 overlay_default_streamfilters
585
586 if default_encoding is None:
587 args['default_encoding'] = self._default_encoding
588 else:
589 args['default_encoding'] = default_encoding
590
591 if memoizer is not None:
592 args['memoizer'] = memoizer
593
594 return self.__class__(**args)
595
596 - def from_file(self, filename, encoding=None):
597 """
598 Build template from file
599
600 :Parameters:
601 `filename` : ``str``
602 The filename to read the template from
603
604 `encoding` : ``str``
605 The initial template encoding. If omitted or ``None``, the default
606 encoding is applied.
607
608 :Return: A new `Template` instance
609 :Rtype: `Template`
610
611 :Exceptions:
612 - `Error` : An error occured while loading the template
613 - `IOError` : Error while opening/reading the file
614 """
615 if encoding is None:
616 encoding = self._default_encoding
617 return self.from_opener(file_opener, filename, encoding=encoding)
618 from_file = _memoize(from_file)
619
620 - def from_opener(self, opener, filename, encoding=None):
621 """
622 Build template from stream as returned by stream opener
623
624 :Parameters:
625 `opener` : ``callable``
626 Stream opener, returns stream and mtime.
627
628 `filename` : ``str``
629 "Filename" of the template. It's passed to the opener, so it knows
630 what to open.
631
632 `encoding` : ``str``
633 Initial template encoding. If omitted or ``None``, the default
634 encoding is applied.
635
636 :Return: The new `Template` instance
637 :Rtype: `Template`
638 """
639 if encoding is None:
640 encoding = self._default_encoding
641 loader = self._loader.persist(filename, encoding, opener)
642 tree, mtime = loader()
643 result = _template.Template(tree, filename, mtime, self, loader)
644 if self._autoupdate:
645 result = _template.AutoUpdate(result)
646 return result
647 from_opener = _memoize(from_opener)
648
649 - def from_stream(self, stream, encoding=None, filename=None,
650 mtime=None, opener=None):
651 """
652 Build template from stream
653
654 :Parameters:
655 `stream` : ``file``
656 The stream to read from
657
658 `encoding` : ``str``
659 Initial template encoding. If omitted or ``None``, the default
660 encoding is applied.
661
662 `filename` : ``str``
663 Optional fake filename of the template. If not set,
664 it's taken from ``stream.name``. If this is not possible,
665 it's ``<stream>``.
666
667 `mtime` : ``int``
668 Optional fake mtime
669
670 `opener`
671 Deprecated. Don't use it anymore.
672
673 :Return: The new `Template` instance
674 :Rtype: `Template`
675 """
676 if encoding is None:
677 encoding = self._default_encoding
678 if filename is None:
679 try:
680 filename = stream.name
681 except AttributeError:
682 filename = '<stream>'
683 tree = self._loader(stream, filename, encoding)
684 if opener is not None:
685 import warnings as _warnings
686 _warnings.warn(
687 "opener argument is deprecated. Use the from_opener "
688 "method instead.",
689 category=DeprecationWarning, stacklevel=2
690 )
691 loader = self._loader.persist(filename, encoding, opener)
692 else:
693 loader = None
694 result = _template.Template(tree, filename, mtime, self, loader)
695 if self._autoupdate and loader is not None:
696 result = _template.AutoUpdate(result)
697 return result
698 from_stream = _memoize(from_stream)
699
700 - def from_string(self, data, encoding=None, filename=None, mtime=None):
701 """
702 Build template from from string
703
704 :Parameters:
705 `data` : ``str``
706 The string to process
707
708 `encoding` : ``str``
709 The initial template encoding. If omitted or ``None``, the default
710 encoding is applied.
711
712 `filename` : ``str``
713 Optional fake filename of the template. If not set,
714 it's ``<string>``
715
716 `mtime` : ``int``
717 Optional fake mtime
718
719 :Return: The new `Template` instance
720 :Rtype: `Template`
721 """
722 if encoding is None:
723 encoding = self._default_encoding
724 if filename is None:
725 filename = '<string>'
726 stream = _string_io.StringIO(data)
727 tree = self._loader(stream, filename, encoding)
728 return _template.Template(tree, filename, mtime, self)
729 from_string = _memoize(from_string)
730
731 - def from_files(self, names, encoding=None, basedir=None):
732 """
733 Load templates from files and overlay them
734
735 :Parameters:
736 `names` : iterable
737 List of filenames, possibly relative to basedir
738
739 `encoding` : ``str``
740 Initial template encoding for all files. If omitted or ``None``,
741 the default encoding is applied.
742
743 `basedir` : ``basestring``
744 Directory, all filenames are relative to. If omitted or ``None``
745 the names are applied as-is.
746
747 :Return: The final template
748 :Rtype: `Template`
749 """
750 if encoding is None:
751 encoding = self._default_encoding
752 if basedir is not None:
753 names = [_os.path.join(basedir, name) for name in names]
754 return overlay([
755 self.from_file(name, encoding=encoding, key=name)
756 for name in names
757 ])
758 from_files = _memoize(from_files)
759
760 - def from_streams(self, streams, encoding=None, streamopen=None):
761 """
762 Load templates from streams and overlay them
763
764 :Parameters:
765 `streams` : iterable
766 List of items identifying the streams. If `streamopen` is omitted
767 or ``None`` the streams are assumed to be regular filenames.
768
769 `encoding` : ``str``
770 Initial template encoding for all streams. If omitted or ``None``,
771 the default encoding is applied.
772
773 `streamopen` : ``callable``
774 Function taking the passed item (of streams) and returning a
775 tuple:
776
777 - the stream specification. This itself is either a 2-tuple or a
778 3-tuple. A 2-tuple contains a stream opener and a filename
779 and is passed to `from_opener`. A 3-tuple contains the open
780 stream, the filename and the mtime and is passed to
781 `from_stream`. (filename and mtime may be ``None``.)
782 - the memoization key, may be ``None``
783
784 If omitted or ``None``, the items are assumed to be file names.
785
786 :Return: The final template
787 :Rtype: `Template`
788 """
789 if encoding is None:
790 encoding = self._default_encoding
791 if streamopen is None:
792 streamopen = lambda x: ((file_opener, x), x)
793
794 def tpls():
795 """ Get templates """
796 for item in streams:
797 tup = streamopen(item)
798 if len(tup) == 4:
799
800 filename, stream, mtime, opener = tup
801 try:
802 import warnings as _warnings
803 _warnings.warn(
804 "streamopen returning a 4-tuple is deprecated. "
805 "Return a 2-tuple instead (streamspec, key).",
806 category=DeprecationWarning, stacklevel=2
807 )
808 tpl = self.from_stream(
809 stream, encoding, filename, mtime, opener
810 )
811 finally:
812 stream.close()
813 yield tpl
814 continue
815
816 tup, key = tup
817 if len(tup) == 3:
818
819 stream, filename, mtime = tup
820 try:
821 tpl = self.from_stream(
822 stream, encoding=encoding, filename=filename,
823 mtime=mtime, key=key,
824 )
825 finally:
826 stream.close()
827 yield tpl
828 continue
829
830 opener, filename = tup
831 yield self.from_opener(
832 opener, filename, encoding=encoding, key=key,
833 )
834 return overlay(tpls())
835 from_streams = _memoize(from_streams)
836