1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 ======================
19 Node Tree Structures
20 ======================
21
22 This module provides node tree management.
23 """
24 __author__ = u"Andr\xe9 Malo"
25 __docformat__ = "restructuredtext en"
26
27 from tdi._exceptions import NodeNotFoundError, NodeTreeError
28 from tdi import _finalize
29 from tdi import _nodetree
30 from tdi import util as _util
31
32
34 """
35 Lightweight node for raw content and attribute assignment
36
37 :IVariables:
38 `_udict` : ``dict``
39 The dict containing node information
40 """
41 __slots__ = ['content', 'encoder', 'decoder', '_udict']
42
44 """
45 Initialization
46
47 :Parameters:
48 `node` : `Node`
49 The original node
50 """
51 self._udict = node._udict
52
54 """
55 Raw content
56
57 :Type: ``str``
58 """
59
60 unicode_, str_, isinstance_ = unicode, str, isinstance
61 def fset(self, content):
62 udict = self._udict
63 if isinstance_(content, unicode_):
64 cont = udict['encoder'].encode(content)
65 else:
66 cont = str_(content)
67 udict['content'] = (cont, cont)
68 udict['namedict'] = {}
69 def fget(self):
70 return self._udict['content'][0]
71 return locals()
72 content = _util.Property(content)
73
75 """
76 Output encoder
77
78 :Type: `EncoderInterface`
79 """
80
81 def fget(self):
82 return self._udict['encoder']
83 return locals()
84 encoder = _util.Property(encoder)
85
87 """
88 Input decoder
89
90 :Type: `DecoderInterface`
91 """
92
93 def fget(self):
94 return self._udict['decoder']
95 return locals()
96 decoder = _util.Property(decoder)
97
99 """
100 Set the attribute `name` to `value`
101
102 The value is *not* encoded according to the model.
103 The original case of `name` is preserved. If the attribute does not
104 occur in the original template, the case of the passed `name` is
105 taken over. Non-string values (including unicode, but not ``None``)
106 are converted to string using ``str()``.
107
108 :Parameters:
109 `name` : ``str``
110 The attribute name (case insensitive)
111
112 `value` : ``str``
113 The attribute value (may be ``None`` for short
114 attributes). Objects that are not ``None`` and and not
115 ``unicode`` are stored as their string representation.
116 """
117 udict = self._udict
118 if value is not None:
119 if isinstance(value, unicode):
120 value = udict['encoder'].encode(value)
121 else:
122 value = str(value)
123 attr = udict['attr']
124 name = udict['encoder'].name(name)
125 normname = udict['decoder'].normalize(name)
126 realname = attr.get(normname, (name,))[0]
127 attr[normname] = (realname, value)
128
130 """
131 Determine the value of attribute `name`
132
133 :Parameters:
134 `name` : ``str``
135 The attribute name
136
137 :Return: The attribute (``None`` for shorttags)
138 :Rtype: ``str``
139
140 :Exceptions:
141 - `KeyError` : The attribute does not exist
142 """
143 udict = self._udict
144 return udict['attr'][
145 udict['decoder'].normalize(udict['encoder'].name(name))
146 ][1]
147
149 """
150 Delete attribute `name`
151
152 If the attribute does not exist, no exception is raised.
153
154 :Parameters:
155 `name` : ``str``
156 The name of the attribute to delete (case insensitive)
157 """
158 udict = self._udict
159 try:
160 del udict['attr'][
161 udict['decoder'].normalize(udict['encoder'].name(name))
162 ]
163 except KeyError:
164
165 pass
166
167
169 """
170 User visible node object
171
172 :IVariables:
173 `ctx` : ``tuple``
174 The node context (``None`` if there isn't one). Node contexts
175 are created on repetitions for all (direct and no-direct) subnodes of
176 the repeated node. The context is a ``tuple``, which contains for
177 repeated nodes the position within the loop (starting with ``0``), the
178 actual item and a tuple of the fixed parameters. The last two are also
179 passed to the repeat callback function directly. For separator
180 nodes, ``ctx[1]`` is a tuple containing the items before the separator
181 and after it. Separator indices are starting with ``0``, too.
182
183 `_model` : `ModelAdapterInterface`
184 The template model object
185
186 `_udict` : ``dict``
187 The dict containing node information
188 """
189 _usernode = True
190 __slots__ = ['content', 'raw', 'ctx', '_model', '_udict']
191
193 """
194 Node content
195
196 The property can be set to a unicode or str value, which will be
197 escaped and encoded (in case of unicode). It replaces the content or
198 child nodes of the node completely.
199
200 The property can be read and will either return the *raw* content of
201 the node (it may even contain markup) - or ``None`` if the node has
202 subnodes.
203
204 :Type: ``basestring`` or ``None``
205 """
206
207 basestring_, isinstance_, str_ = basestring, isinstance, str
208 def fset(self, content):
209 if not isinstance_(content, basestring_):
210 content = str_(content)
211 udict = self._udict
212 udict['content'] = (udict['encoder'].content(content), None)
213 udict['namedict'] = {}
214 def fget(self):
215 return self._udict['content'][0]
216 return locals()
217 content = _util.Property(content)
218
220 """
221 Hidden node markup?
222
223 :Type: ``bool``
224 """
225
226 def fset(self, value):
227 self._udict['noelement'] = value and True or False
228 def fget(self):
229 return self._udict['noelement']
230 return locals()
231 hiddenelement = _util.Property(hiddenelement)
232
234 """
235 Self-closed element? (read-only)
236
237 :Type: ``bool``
238 """
239
240 def fget(self):
241 return self._udict['closed']
242 return locals()
243 closedelement = _util.Property(closedelement)
244
246 """
247 Raw node
248
249 :Type: `RawNode`
250 """
251
252 def fget(self):
253 return RawNode(self)
254 return locals()
255 raw = _util.Property(raw)
256
257 - def __new__(cls, node, model, ctx=None, light=False):
258 """
259 Construction
260
261 :Parameters:
262 `node` : `Node` or `TemplateNode`
263 The node to clone
264
265 `model` : `ModelAdapterInterface`
266 The template model instance
267
268 `ctx` : ``tuple``
269 The node context
270
271 `light` : ``bool``
272 Do a light copy? (In this case just the node context is
273 updated and the *original* node is returned). Do this only if
274 `node` is already a `Node` instance and you do not need another
275 copy!
276
277 :Return: The node instance
278 :Rtype: `Node`
279 """
280
281 if light:
282 if not node._udict.get('callback'):
283 node.ctx = ctx
284 return node
285
286 self = object.__new__(cls)
287 udict = node._udict.copy()
288 udict['attr'] = udict['attr'].copy()
289 udict['nodes'] = udict['nodes'][:]
290 self._udict = udict
291 self._model = model
292 if udict.get('callback'):
293 self.ctx = node.ctx
294 else:
295 self.ctx = ctx
296
297 return self
298
300 """
301 Determine direct subnodes by name
302
303 In contrast to `__getattr__` this works for all names. Also the
304 exception in case of a failed lookup is different.
305
306 :Parameters:
307 `name` : ``str``
308 The name looked for
309
310 :Return: The found node
311 :Rtype: `Node`
312
313 :Exceptions:
314 - `NodeNotFoundError` : The subnode was not found
315 """
316
317 udict = self._udict
318 try:
319 name = str(name)
320 idx = udict['namedict'][name]
321 except (UnicodeError, KeyError):
322 raise NodeNotFoundError(name)
323
324 while idx < 0:
325 kind, result = udict['nodes'][-1 - idx]
326 if not result._usernode:
327 result = Node(result, self._model, self.ctx)
328 udict['nodes'][-1 - idx] = (kind, result)
329 udict = result._udict
330 idx = udict['namedict'][name]
331
332 kind, result = udict['nodes'][idx]
333 if not result._usernode:
334 result = Node(result, self._model, self.ctx)
335 udict['nodes'][idx] = (kind, result)
336 else:
337 result.ctx = self.ctx
338
339 return result
340
342 """
343 Determine direct subnodes by name
344
345 :Parameters:
346 `name` : ``str``
347 The name looked for
348
349 :Return: The found subnode
350 :Rtype: `Node`
351
352 :Exceptions:
353 - `AttributeError` : The subnode was not found
354 """
355 try:
356 return self(name)
357 except NodeNotFoundError:
358 raise AttributeError("Attribute %s.%s not found" % (
359 self.__class__.__name__, name
360 ))
361
363 """
364 Set the attribute `name` to `value`
365
366 The value is encoded according to the model and the original case
367 of `name` is preserved. If the attribute does not occur in the
368 original template, the case of the passed `name` is taken over.
369 Non-string values are converted to string using ``str()``. Unicode
370 values are passed as-is to the model encoder.
371
372 :Parameters:
373 `name` : ``str``
374 The attribute name (case insensitive)
375
376 `value` : any
377 The attribute value (may be ``None`` for short
378 attributes). Objects that are not ``None`` and and not
379 ``unicode`` are stored as their string representation.
380 """
381 udict = self._udict
382 if value is not None:
383 if not isinstance(value, basestring):
384 value = str(value)
385 value = udict['encoder'].attribute(value)
386
387 attr = udict['attr']
388 name = udict['encoder'].name(name)
389 normname = udict['decoder'].normalize(name)
390 realname = attr.get(normname, [name])[0]
391 attr[normname] = (realname, value)
392
394 """
395 Determine the value of attribute `name`
396
397 :Parameters:
398 `name` : ``str``
399 The attribute name
400
401 :Return: The attribute (``None`` for shorttags)
402 :Rtype: ``str``
403
404 :Exceptions:
405 - `KeyError` : The attribute does not exist
406 """
407 udict = self._udict
408 value = udict['attr'][
409 udict['decoder'].normalize(udict['encoder'].name(name))
410 ][1]
411 if value and (value.startswith('"') or value.startswith("'")):
412 value = value[1:-1]
413
414 return value
415
417 """
418 Delete attribute `name`
419
420 If the attribute does not exist, no exception is raised.
421
422 :Parameters:
423 `name` : ``str``
424 The name of the attribute to delete (case insensitive)
425 """
426 udict = self._udict
427 try:
428 del udict['attr'][
429 udict['decoder'].normalize(udict['encoder'].name(name))
430 ]
431 except KeyError:
432
433 pass
434
435 - def repeat(self, callback, itemlist, *fixed, **kwargs):
436 """
437 Repeat the snippet ``len(list(itemlist))`` times
438
439 The actually supported signature is::
440
441 repeat(self, callback, itemlist, *fixed, separate=None)
442
443 Examples:
444
445 >>> def render_foo(self, node):
446 >>> def callback(node, item):
447 >>> ...
448 >>> node.repeat(callback, [1, 2, 3, 4])
449
450 >>> def render_foo(self, node):
451 >>> def callback(node, item):
452 >>> ...
453 >>> def sep(node):
454 >>> ...
455 >>> node.repeat(callback, [1, 2, 3, 4], separate=sep)
456
457 >>> def render_foo(self, node):
458 >>> def callback(node, item, foo, bar):
459 >>> ...
460 >>> node.repeat(callback, [1, 2, 3, 4], "foo", "bar")
461
462 >>> def render_foo(self, node):
463 >>> def callback(node, item, foo, bar):
464 >>> ...
465 >>> def sep(node):
466 >>> ...
467 >>> node.repeat(callback, [1, 2, 3, 4], "foo", "bar",
468 >>> separate=sep)
469
470 :Parameters:
471 `callback` : ``callable``
472 The callback function
473
474 `itemlist` : iterable
475 The items to iterate over
476
477 `fixed` : ``tuple``
478 Fixed parameters to be passed to the repeat methods
479
480 :Keywords:
481 `separate` : ``callable``
482 Alternative callback function for separator nodes. If omitted or
483 ``None``, ``self.separate_name`` is looked up and called if it
484 exists.
485 """
486 if 'separate' in kwargs:
487 if len(kwargs) > 1:
488 raise TypeError("Unrecognized keyword parameters")
489 separate = kwargs['separate']
490 elif kwargs:
491 raise TypeError("Unrecognized keyword parameters")
492 else:
493 separate = None
494 self._udict['repeated'] = (callback, iter(itemlist), fixed, separate)
495
497 """
498 Remove the node from the tree
499
500 Tells the system, that the node (and all of its subnodes) should
501 not be rendered.
502 """
503 self._udict['removed'] = True
504 self._udict['namedict'] = {}
505
506 - def iterate(self, itemlist, separate=None):
507 """
508 Iterate over repeated nodes
509
510 Iteration works by repeating the original node
511 ``len(list(iteritems))`` times, turning the original node into a
512 container node and appending the generated nodeset to that container.
513 That way, the iterated nodes are virtually indented by one level, but
514 the container node is completely hidden, so it won't be visible.
515
516 All repeated nodes are marked as ``DONE``, so they (and their
517 subnodes) are not processed any further (except explicit callbacks).
518 If there is a separator node assigned, it's put between the
519 repetitions and *not* marked as ``DONE``. The callbacks to them
520 (if any) are executed when the template system gets back to control.
521
522 :Parameters:
523 `itemlist` : iterable
524 The items to iterate over
525
526 `separate` : ``callable``
527 Alternative callback function for separator nodes. If omitted or
528 ``None``, ``self.separate_name`` is looked up and called if it
529 exists.
530
531 :Return: The repeated nodes and items (``[(node, item), ...]``)
532 :Rtype: iterable
533 """
534 itemlist = iter(itemlist)
535 node, nodelist = self.copy(), []
536
537
538
539
540 self._udict['content'] = (None, None)
541 self._udict['nodes'] = nodelist
542 self._udict['namedict'] = {}
543 self._udict['masked'] = True
544
545 return _nodetree.iterate(
546 node, nodelist, itemlist, separate, Node
547 )
548
549 - def replace(self, callback, other, *fixed):
550 """
551 Replace the node (and all subnodes) with the copy of another one
552
553 The replacement node is deep-copied, so use it with care
554 (performance-wise).
555
556 :Parameters:
557 `callback` : ``callable``
558 callback function
559
560 `other` : `Node`
561 The replacement node
562
563 `fixed` : ``tuple``
564 Fixed parameters for the callback
565
566 :Return: The replaced node (actually the node itself, but with
567 updated parameters)
568 :Rtype: `Node`
569 """
570
571
572 udict = other._udict.copy()
573 udict['attr'] = udict['attr'].copy()
574 ctx, deep, TEXT = self.ctx, _nodetree.copydeep, _nodetree.TEXT_NODE
575 model = self._model
576
577 udict['nodes'] = [(kind, (kind != TEXT and node._usernode) and
578 deep(node, model, ctx, Node) or node
579 ) for kind, node in udict['nodes']]
580
581 udict['name'] = self._udict['name']
582 udict['callback'] = callback
583 udict['complete'] = fixed
584
585 self._udict = udict
586 return self
587
589 """
590 Deep copy this node
591
592 :Return: The node copy
593 :Rtype: `Node`
594 """
595 return _nodetree.copydeep(self, self._model, self.ctx, Node)
596
597 - def render(self, *callback, **kwargs):
598 """
599 render(self, callback, params, decode=True, decode_errors='strict')
600
601 Render this node only and return the result as string
602
603 Note that callback and params are optional positional parameters::
604
605 render(self, decode=True, decode_errors='strict')
606 # or
607 render(self, callback, decode=True, decode_errors='strict')
608 # or
609 render(self, callback, param1, paramx, ... decode=True, ...)
610
611 is also possible.
612
613 :Parameters:
614 `callback` : callable or ``None``
615 Optional callback function and additional parameters
616
617 `params` : ``tuple``
618 Optional extra parameters for `callback`
619
620 `decode` : ``bool``
621 Decode the result back to unicode? This uses the encoding of the
622 template.
623
624 `decode_errors` : ``str``
625 Error handler if decode errors happen.
626
627 :Return: The rendered node, type depends on `decode` parameter
628 :Rtype: ``basestring``
629 """
630
631 decode = kwargs.pop('decode', True)
632 decode_errors = kwargs.pop('decode_errors', 'strict')
633 if kwargs:
634 raise TypeError("Unrecognized keyword parameters")
635 node = self.copy()
636 if callback and callback[0] is not None:
637 node.replace(callback[0], node, *callback[1:])
638 else:
639 node.replace(None, node)
640 res = ''.join(_nodetree.render(node, node._model, Node))
641 if not decode:
642 return res
643 return res.decode(self._udict['decoder'].encoding, decode_errors)
644
645
647 """
648 Template node
649
650 This is kind of a proto node. During rendering each template node is
651 turned into a user visible `Node` object, which implements the user
652 interface. `TemplateNode` objects provide a tree building interface
653 instead.
654
655 :IVariables:
656 `_udict` : ``dict``
657 The dict containing node information
658
659 `_finalized` : ``bool``
660 Was the tree finalized?
661 """
662 ctx = None
663 _usernode = False
664
666 """
667 End tag of the node
668
669 :Type: ``str``
670 """
671
672 def fset(self, data):
673 if self._finalized:
674 raise NodeTreeError("Tree was already finalized")
675 if self._udict['closed']:
676 raise NodeTreeError(
677 "Self-closing elements cannot have an endtag"
678 )
679 if not isinstance(data, str):
680 raise NodeTreeError("Endtag data must be a string")
681 self._udict['endtag'] = data
682 def fget(self):
683 return self._udict.get('endtag')
684 return locals()
685 endtag = _util.Property(endtag)
686
687 - def __init__(self, tagname, attr, special, closed):
688 """
689 Initialization
690
691 :Parameters:
692 `tagname` : ``str``
693 The name of the accompanying tag
694
695 `attr` : iterable
696 The attribute list (``((name, value), ...)``)
697
698 `special` : ``dict``
699 Special node information
700 """
701 scope = special.get('scope')
702 overlay = special.get('overlay')
703 tdi = special.get('attribute')
704 if tdi is None:
705 flags, name = '', None
706 else:
707 flags, name = tdi
708
709 if overlay is None:
710 overlay = False, False, False, None
711 else:
712 overlay = (
713 '-' in overlay[0],
714 '>' in overlay[0],
715 '<' in overlay[0],
716 overlay[1],
717 )
718
719 if scope is None:
720 scope = False, False, None
721 else:
722 scope = (
723 ('-' in scope[0]),
724 ('=' in scope[0]),
725 scope[1],
726 )
727 if not scope[0] and not scope[1] and not scope[2]:
728 scope = False, False, None
729
730 self._udict = {
731 'sep': None,
732 'nodes': [],
733 'content': (None, None),
734 'attr_': tuple(attr),
735 'removed': False,
736 'repeated': None,
737 'name': name or None,
738 'closed': closed,
739 'tagname': tagname,
740 'noelement': '-' in flags or overlay[0] or scope[0],
741 'noauto': '*' in flags,
742 'masked': False,
743 'overlay': overlay,
744 'scope': scope,
745 }
746 self._finalized = False
747
748 - def append_text(self, content):
749 """
750 Append a text node
751
752 :Parameters:
753 `content` : ``str``
754 The text node content
755
756 :Exceptions:
757 - `NodeTreeError` : The tree was already finalized
758 """
759 if self._finalized:
760 raise NodeTreeError("Tree was already finalized")
761
762 self._udict['nodes'].append((_nodetree.TEXT_NODE, (content, content)))
763
765 """
766 Append an escaped node
767
768 :Parameters:
769 `escaped` : ``str``
770 The escaped string (in unescaped form, i.e. the final result)
771
772 `content` : ``str``
773 The escape string (the whole sequence)
774
775 :Exceptions:
776 - `NodeTreeError` : The tree was already finalized
777 """
778 if self._finalized:
779 raise NodeTreeError("Tree was already finalized")
780
781 self._udict['nodes'].append((_nodetree.TEXT_NODE, (escaped, content)))
782
783 - def append_node(self, tagname, attr, special, closed):
784 """
785 Append processable node
786
787 :Parameters:
788 `tagname` : ``str``
789 The name of the accompanying tag
790
791 `attr` : iterable
792 The attribute list (``((name, value), ...)``)
793
794 `special` : ``dict``
795 Special attributes. If it's empty, something's wrong.
796
797 `closed` : ``bool``
798 Closed tag?
799
800 :Return: new `TemplateNode` instance
801 :Rtype: `TemplateNode`
802
803 :Exceptions:
804 - `NodeTreeError` : The tree was already finalized
805 - `AssertionError` : nothing special
806 """
807 if self._finalized:
808 raise NodeTreeError("Tree was already finalized")
809
810 assert len(special), "Nothing special about this node."
811
812 node = TemplateNode(tagname, attr, special, bool(closed))
813 tdi = special.get('attribute')
814 if tdi is not None and ':' in tdi[0]:
815 kind = _nodetree.SEP_NODE
816 else:
817 kind = _nodetree.PROC_NODE
818 self._udict['nodes'].append((kind, node))
819
820 return node
821
822
823 -class Root(TemplateNode):
824 """
825 Root Node class
826
827 This class has to be used as the initial root of the tree.
828 """
829 _sources, _targets = None, None
830
832 """
833 Output encoder
834
835 :Type: `EncoderInterface`
836 """
837
838 def fget(self):
839 return self._udict['encoder']
840 return locals()
841 encoder = _util.Property(encoder)
842
844 """
845 Input decoder
846
847 :Type: `DecoderInterface`
848 """
849
850 def fget(self):
851 return self._udict['decoder']
852 return locals()
853 decoder = _util.Property(decoder)
854
856 """
857 Source overlay names
858
859 :Type: iterable
860 """
861
862 def fget(self):
863 if self._sources is None:
864 return ()
865 return self._sources.iterkeys()
866 return locals()
867 source_overlay_names = _util.Property(source_overlay_names)
868
870 """
871 Target overlay names
872
873 :Type: iterable
874 """
875
876 def fget(self):
877 if self._targets is None:
878 return ()
879 return self._targets.iterkeys()
880 return locals()
881 target_overlay_names = _util.Property(target_overlay_names)
882
884 """ Initialization """
885 super(Root, self).__init__('', (), {}, False)
886 self.endtag = ''
887 self._udict['is_root'] = True
888
890 """ String representation of the tree """
891 return self.to_string(verbose=True)
892
894 """
895 String representation of the tree
896
897 :Parameters:
898 `verbose` : ``bool``
899 Show (shortened) text node content and separator nodes?
900
901 :Return: The string representation
902 :Rtype: ``str``
903 """
904 if not self._finalized:
905 raise NodeTreeError("Tree was not finalized yet")
906 return '\n'.join(list(
907 _nodetree.represent(self._udict, bool(verbose))
908 )) + '\n'
909
911 """
912 Finalize the tree
913
914 This method assigns separator nodes to their accompanying content
915 nodes, concatenates adjacent text nodes and tries to optimize
916 the tree a bit.
917
918 :Parameters:
919 `encoder` : `EncoderInterface`
920 Encoder instance
921
922 :Exceptions:
923 - `NodeTreeError` : The tree was already finalized or endtag was not
924 set
925 """
926 if self._finalized:
927 raise NodeTreeError("Tree was already finalized")
928 self._sources, self._targets = \
929 _finalize.finalize(self._udict, encoder, decoder)
930 self._finalized = True
931
933 """
934 Overlay this tree with another one
935
936 :Parameters:
937 `other` : `Root`
938 The tree to lay over
939
940 :Exceptions:
941 - `NodeTreeError` : Finalization error
942 """
943
944 if not self._finalized:
945 raise NodeTreeError("Tree was not finalized yet.")
946 if not other._finalized:
947 raise NodeTreeError("Overlay tree was not finalized yet.")
948 return _nodetree.overlay(
949 self._udict, other._sources, TemplateNode, Root
950 )
951
952 - def render(self, model, startnode=None):
953 """
954 Render the tree into chunks, calling `model` for input
955
956 :Parameters:
957 `model` : `ModelAdapterInterface`
958 The model object
959
960 `startnode` : ``str``
961 Only render this node (and all its children). The node
962 is addressed via a dotted string notation, like ``a.b.c`` (this
963 would render the ``c`` node.) The notation does not describe a
964 strict node chain, though. Between to parts of a node chain may
965 be gaps in the tree. The algorithm looks out for the first
966 matching node. It does no backtracking and so does not cover all
967 branches (yet?), but that works fine for realistic cases :). A
968 non-working example would be (searching for a.b.c)::
969
970 *
971 +- a
972 | `- b - d
973 `- a
974 `- b - c
975
976 :Return: Rendered chunks
977 :Rtype: iterable
978 """
979 return _nodetree.render(
980 _nodetree.findnode(self, startnode), model, Node
981 )
982
983
984 from tdi import c
985 c = c.load('impl')
986 if c is not None:
987 Root, Node, RawNode, TemplateNode = (
988 c.Root, c.Node, c.RawNode, c.TemplateNode
989 )
990 del _nodetree
991 del c
992