Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/chameleon/tal.py : 30%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1##############################################################################
2#
3# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4# All Rights Reserved.
5#
6# This software is subject to the provisions of the Zope Public License,
7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11# FOR A PARTICULAR PURPOSE.
12#
13##############################################################################
15import re
16import copy
18from .exc import LanguageError
19from .utils import descriptorint
20from .utils import descriptorstr
21from .namespaces import XMLNS_NS
22from .parser import groups
25try:
26 next
27except NameError:
28 from chameleon.py25 import next
30try:
31 # optional library: `zope.interface`
32 from chameleon import interfaces
33 import zope.interface
34except ImportError:
35 interfaces = None
38NAME = r"[a-zA-Z_][-a-zA-Z0-9_]*"
39DEFINE_RE = re.compile(r"(?s)\s*(?:(global|local)\s+)?" +
40 r"(%s|\(%s(?:,\s*%s)*\))\s+(.*)\Z" % (NAME, NAME, NAME),
41 re.UNICODE)
42SUBST_RE = re.compile(r"\s*(?:(text|structure)\s+)?(.*)\Z", re.S | re.UNICODE)
43ATTR_RE = re.compile(r"\s*([^\s{}'\"]+)\s+([^\s].*)\Z", re.S | re.UNICODE)
45ENTITY_RE = re.compile(r'(&(#?)(x?)(\d{1,5}|\w{1,8});)')
47WHITELIST = frozenset([
48 "define",
49 "comment",
50 "condition",
51 "content",
52 "replace",
53 "repeat",
54 "attributes",
55 "on-error",
56 "omit-tag",
57 "script",
58 "switch",
59 "case",
60 "xmlns",
61 "xml"
62 ])
65def split_parts(arg):
66 # Break in pieces at undoubled semicolons and
67 # change double semicolons to singles:
68 i = 0
69 while i < len(arg):
70 m = ENTITY_RE.search(arg[i:])
71 if m is None:
72 break
73 arg = arg[:i + m.end()] + ';' + arg[i + m.end():]
74 i += m.end()
76 arg = arg.replace(";;", "\0")
77 parts = arg.split(';')
78 parts = [p.replace("\0", ";") for p in parts]
79 if len(parts) > 1 and not parts[-1].strip():
80 del parts[-1] # It ended in a semicolon
82 return parts
85def parse_attributes(clause):
86 attrs = []
87 seen = set()
88 for part in split_parts(clause):
89 m = ATTR_RE.match(part)
90 if not m:
91 name, expr = None, part.strip()
92 else:
93 name, expr = groups(m, part)
95 if name in seen:
96 raise LanguageError(
97 "Duplicate attribute name in attributes.", part)
99 seen.add(name)
100 attrs.append((name, expr))
102 return attrs
105def parse_substitution(clause):
106 m = SUBST_RE.match(clause)
107 if m is None:
108 raise LanguageError(
109 "Invalid content substitution syntax.", clause)
111 key, expression = groups(m, clause)
112 if not key:
113 key = "text"
115 return key, expression
118def parse_defines(clause):
119 """
120 Parses a tal:define value.
122 # Basic syntax, implicit local
123 >>> parse_defines('hello lovely')
124 [('local', ('hello',), 'lovely')]
126 # Explicit local
127 >>> parse_defines('local hello lovely')
128 [('local', ('hello',), 'lovely')]
130 # With global
131 >>> parse_defines('global hello lovely')
132 [('global', ('hello',), 'lovely')]
134 # Multiple expressions
135 >>> parse_defines('hello lovely; tea time')
136 [('local', ('hello',), 'lovely'), ('local', ('tea',), 'time')]
138 # With multiple names
139 >>> parse_defines('(hello, howdy) lovely')
140 [('local', ['hello', 'howdy'], 'lovely')]
142 # With unicode whitespace
143 >>> try:
144 ... s = '\xc2\xa0hello lovely'.decode('utf-8')
145 ... except AttributeError:
146 ... s = '\xa0hello lovely'
147 >>> from chameleon.utils import unicode_string
148 >>> parse_defines(s) == [
149 ... ('local', ('hello',), 'lovely')
150 ... ]
151 True
153 """
154 defines = []
155 for part in split_parts(clause):
156 m = DEFINE_RE.match(part)
157 if m is None:
158 raise LanguageError("Invalid define syntax", part)
159 context, name, expr = groups(m, part)
160 context = context or "local"
162 if name.startswith('('):
163 names = [n.strip() for n in name.strip('()').split(',')]
164 else:
165 names = (name,)
167 defines.append((context, names, expr))
169 return defines
172def prepare_attributes(attrs, dyn_attributes, i18n_attributes,
173 ns_attributes, drop_ns):
174 drop = set([attribute['name'] for attribute, (ns, value)
175 in zip(attrs, ns_attributes)
176 if ns in drop_ns or (
177 ns == XMLNS_NS and
178 attribute['value'] in drop_ns
179 )
180 ])
182 attributes = []
183 normalized = {}
184 computed = []
186 for attribute in attrs:
187 name = attribute['name']
189 if name in drop:
190 continue
192 attributes.append((
193 name,
194 attribute['value'],
195 attribute['quote'],
196 attribute['space'],
197 attribute['eq'],
198 None,
199 ))
201 normalized[name.lower()] = len(attributes) - 1
203 for name, expr in dyn_attributes:
204 index = normalized.get(name.lower()) if name else None
206 if index is not None:
207 _, text, quote, space, eq, _ = attributes[index]
208 add = attributes.__setitem__
209 else:
210 text = None
211 quote = '"'
212 space = " "
213 eq = "="
214 index = len(attributes)
215 add = attributes.insert
216 if name is not None:
217 normalized[name.lower()] = len(attributes) - 1
219 attribute = name, text, quote, space, eq, expr
220 add(index, attribute)
222 for name in i18n_attributes:
223 attr = name.lower()
224 if attr not in normalized:
225 attributes.append((name, name, '"', " ", "=", None))
226 normalized[attr] = len(attributes) - 1
228 return attributes
231class RepeatItem(object):
232 __slots__ = "length", "_iterator"
234 __allow_access_to_unprotected_subobjects__ = True
236 def __init__(self, iterator, length):
237 self.length = length
238 self._iterator = iterator
240 def __iter__(self):
241 return self._iterator
243 try:
244 iter(()).__len__
245 except AttributeError:
246 @descriptorint
247 def index(self):
248 try:
249 remaining = self._iterator.__length_hint__()
250 except AttributeError:
251 remaining = len(tuple(copy.copy(self._iterator)))
252 return self.length - remaining - 1
253 else:
254 @descriptorint
255 def index(self):
256 remaining = self._iterator.__len__()
257 return self.length - remaining - 1
259 @descriptorint
260 def start(self):
261 return self.index == 0
263 @descriptorint
264 def end(self):
265 return self.index == self.length - 1
267 @descriptorint
268 def number(self):
269 return self.index + 1
271 @descriptorstr
272 def odd(self):
273 """Returns a true value if the item index is odd.
275 >>> it = RepeatItem(iter(("apple", "pear")), 2)
277 >>> next(it._iterator)
278 'apple'
279 >>> it.odd()
280 ''
282 >>> next(it._iterator)
283 'pear'
284 >>> it.odd()
285 'odd'
286 """
288 return self.index % 2 == 1 and 'odd' or ''
290 @descriptorstr
291 def even(self):
292 """Returns a true value if the item index is even.
294 >>> it = RepeatItem(iter(("apple", "pear")), 2)
296 >>> next(it._iterator)
297 'apple'
298 >>> it.even()
299 'even'
301 >>> next(it._iterator)
302 'pear'
303 >>> it.even()
304 ''
305 """
307 return self.index % 2 == 0 and 'even' or ''
309 @descriptorstr
310 def parity(self):
311 """Return 'odd' or 'even' depending on the position's parity
313 Useful for assigning CSS class names to table rows.
314 """
316 return self.index % 2 == 0 and 'even' or 'odd'
318 def next(self):
319 raise NotImplementedError(
320 "Method not implemented (can't update local variable).")
322 def _letter(self, base=ord('a'), radix=26):
323 """Get the iterator position as a lower-case letter
325 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3)
326 >>> next(it._iterator)
327 'apple'
328 >>> it.letter()
329 'a'
330 >>> next(it._iterator)
331 'pear'
332 >>> it.letter()
333 'b'
334 >>> next(it._iterator)
335 'orange'
336 >>> it.letter()
337 'c'
338 """
340 index = self.index
341 if index < 0:
342 raise TypeError("No iteration position")
343 s = ""
344 while 1:
345 index, off = divmod(index, radix)
346 s = chr(base + off) + s
347 if not index:
348 return s
350 letter = descriptorstr(_letter)
352 @descriptorstr
353 def Letter(self):
354 """Get the iterator position as an upper-case letter
356 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3)
357 >>> next(it._iterator)
358 'apple'
359 >>> it.Letter()
360 'A'
361 >>> next(it._iterator)
362 'pear'
363 >>> it.Letter()
364 'B'
365 >>> next(it._iterator)
366 'orange'
367 >>> it.Letter()
368 'C'
369 """
371 return self._letter(base=ord('A'))
373 @descriptorstr
374 def Roman(self, rnvalues=(
375 (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
376 (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
377 (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I'))):
378 """Get the iterator position as an upper-case roman numeral
380 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3)
381 >>> next(it._iterator)
382 'apple'
383 >>> it.Roman()
384 'I'
385 >>> next(it._iterator)
386 'pear'
387 >>> it.Roman()
388 'II'
389 >>> next(it._iterator)
390 'orange'
391 >>> it.Roman()
392 'III'
393 """
395 n = self.index + 1
396 s = ""
397 for v, r in rnvalues:
398 rct, n = divmod(n, v)
399 s = s + r * rct
400 return s
402 @descriptorstr
403 def roman(self):
404 """Get the iterator position as a lower-case roman numeral
406 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3)
407 >>> next(it._iterator)
408 'apple'
409 >>> it.roman()
410 'i'
411 >>> next(it._iterator)
412 'pear'
413 >>> it.roman()
414 'ii'
415 >>> next(it._iterator)
416 'orange'
417 >>> it.roman()
418 'iii'
419 """
421 return self.Roman().lower()
424if interfaces is not None:
425 zope.interface.classImplements(RepeatItem, interfaces.ITALESIterator)
428class RepeatDict(object):
429 """Repeat dictionary implementation.
431 >>> repeat = RepeatDict({})
432 >>> iterator, length = repeat('numbers', range(5))
433 >>> length
434 5
436 >>> repeat['numbers']
437 <chameleon.tal.RepeatItem object at ...>
439 >>> repeat.numbers
440 <chameleon.tal.RepeatItem object at ...>
442 >>> getattr(repeat, 'missing_key', None) is None
443 True
445 >>> try:
446 ... from chameleon import interfaces
447 ... interfaces.ITALESIterator(repeat,None) is None
448 ... except ImportError:
449 ... True
450 ...
451 True
452 """
454 __slots__ = "__setitem__", "__getitem__"
456 def __init__(self, d):
457 self.__setitem__ = d.__setitem__
458 self.__getitem__ = d.__getitem__
460 def __getattr__(self,key):
461 try:
462 return self[key]
463 except KeyError:
464 raise AttributeError(key)
467 def __call__(self, key, iterable):
468 """We coerce the iterable to a tuple and return an iterator
469 after registering it in the repeat dictionary."""
471 iterable = list(iterable) if iterable is not None else ()
473 length = len(iterable)
474 iterator = iter(iterable)
476 # Insert as repeat item
477 self[key] = RepeatItem(iterator, length)
479 return iterator, length
482class ErrorInfo(object):
483 """Information about an exception passed to an on-error handler."""
485 def __init__(self, err, position=(None, None)):
486 if isinstance(err, Exception):
487 self.type = err.__class__
488 self.value = err
489 else:
490 self.type = err
491 self.value = None
492 self.lineno = position[0]
493 self.offset = position[1]
496if interfaces is not None:
497 zope.interface.classImplements(ErrorInfo, interfaces.ITALExpressionErrorInfo)