2 # -*- coding: iso-8859-15 -*-
5 """This file contains "validators" that convert between string and python (database) representation
6 of properties used in the "Rodelbahnbox" and "Gasthausbox".
7 The "to_python" method has to get a unicode argument.
10 import formencode.national
13 import xml.dom.minidom as minidom
14 from xml.parsers.expat import ExpatError
17 class NoneValidator(formencode.FancyValidator):
18 """Takes a validator and makes it possible that empty strings are mapped to None."""
19 def __init__(self, validator, python_none=None):
20 self.validator = validator
21 self.python_none = python_none
23 def to_python(self, value):
24 self.assert_string(value, None)
25 if value == u'': return self.python_none
26 return self.validator.to_python(value)
28 def from_python(self, value):
29 if value == self.python_none: return u''
30 return self.validator.from_python(value)
33 class NeinValidator(formencode.FancyValidator):
34 """Take an arbitrary validator and adds the possibility that the
35 string can be u'Nein'.
36 Example together with an UnsignedNone validator:
37 >>> v = NeinValidator(UnsignedNone())
40 >>> v.to_python(u'34')
42 >>> v.to_python(u'Nein')
45 def __init__(self, validator, python_no=u'Nein'):
46 self.validator = validator
47 self.python_no = python_no
49 def to_python(self, value):
50 self.assert_string(value, None)
51 if value == u'Nein': return self.python_no
52 return self.validator.to_python(value)
54 def from_python(self, value):
55 if value == self.python_no: return u'Nein'
56 return self.validator.from_python(value)
59 class Unicode(formencode.FancyValidator):
60 """Converts an unicode string to an unicode string:
61 u'any string' <=> u'any string'"""
62 def to_python(self, value):
63 self.assert_string(value, None)
66 def from_python(self, value):
70 class UnicodeNone(NoneValidator):
71 """Converts an unicode string to an unicode string:
73 u'any string' <=> u'any string'"""
75 NoneValidator.__init__(self, Unicode())
78 class Unsigned(formencode.FancyValidator):
79 """Converts an unsigned number to a string and vice versa:
84 def __init__(self, max=None):
85 self.iv = formencode.validators.Int(min=0, max=max)
87 def to_python(self, value):
88 self.assert_string(value, None)
89 return self.iv.to_python(value)
91 def from_python(self, value):
95 class UnsignedNone(NoneValidator):
96 """Converts an unsigned number to a string and vice versa:
102 def __init__(self, max=None):
103 NoneValidator.__init__(self, Unsigned(max))
106 class UnsignedNeinNone(NoneValidator):
107 """ Translates a number of Nein to a number.
115 NoneValidator.__init__(self, UnsignedNone())
118 class Loop(formencode.FancyValidator):
119 """Takes a validator and calls from_python(to_python(value))."""
120 def __init__(self, validator):
121 self.validator = validator
123 def to_python(self, value):
124 self.assert_string(value, None)
125 return self.validator.from_python(self.validator.to_python(value))
127 def from_python(self, value):
128 # we don't call self.validator.to_python(self.validator.from_python(value))
129 # here because our to_python implementation basically leaves the input untouched
130 # and so should from_python do.
131 return self.validator.from_python(self.validator.to_python(value))
134 class DictValidator(formencode.FancyValidator):
135 """Translates strings to other values via a python directory.
136 >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
137 >>> boolValidator.to_python(u'')
139 >>> boolValidator.to_python(u'Ja')
142 def __init__(self, dict):
145 def to_python(self, value):
146 self.assert_string(value, None)
147 if not self.dict.has_key(value): raise formencode.Invalid("Key not found in dict.", value, None)
148 return self.dict[value]
150 def from_python(self, value):
151 for k, v in self.dict.iteritems():
152 if type(v) == type(value) and v == value: return k
153 raise formencode.Invalid('Invalid value', value, None)
156 class GermanBoolNone(DictValidator):
157 """Converts German bool values to the python bool type:
163 DictValidator.__init__(self, {u'': None, u'Ja': True, u'Nein': False})
166 class GermanTristateTuple(DictValidator):
167 """Does the following conversion:
169 u'Ja' <=> (True, False)
170 u'Teilweise' <=> (True, True)
171 u'Nein' <=> (False, True)"""
172 def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
173 DictValidator.__init__(self, {u'': none_python, u'Ja': yes_python, u'Nein': no_python, u'Teilweise': partly_python})
176 class GermanTristateFloat(GermanTristateTuple):
177 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
184 GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
187 class ValueComment(formencode.FancyValidator):
188 """Converts value with a potentially optional comment to a python tuple. If a comment is present, the
189 closing bracket has to be the rightmost character.
191 u'value' <=> (u'value', None)
192 u'value (comment)' <=> (u'value', u'comment')
193 u'[[link (linkcomment)]]' <=> (u'[[link (linkcomment)]]', None)
194 u'[[link (linkcomment)]] (comment)' <=> (u'[[link (linkcomment)]]', comment)
196 def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
197 self.value_validator = value_validator
198 self.comment_validator = comment_validator
199 self.comment_is_optional = comment_is_optional
201 def to_python(self, value):
202 self.assert_string(value, None)
207 right = value.rfind(')')
208 if right+1 != len(value):
209 if not self.comment_is_optional: raise formencode.Invalid(u'Mandatory comment not present', value, None)
213 left = value.rfind('(')
214 if left < 0: raise formencode.Invalid(u'Invalid format', value, None)
215 v = value[:left].strip()
216 c = value[left+1:right].strip()
217 return self.value_validator.to_python(v), self.comment_validator.to_python(c)
219 def from_python(self, value):
220 assert len(value) == 2
221 v = self.value_validator.from_python(value[0])
222 c = self.comment_validator.from_python(value[1])
224 if len(v) > 0: return u'%s (%s)' % (v, c)
225 else: return u'(%s)' % c
229 class SemicolonList(formencode.FancyValidator):
230 """Applies a given validator to a semicolon separated list of values and returns a python list.
231 For an empty string an empty list is returned."""
232 def __init__(self, validator=Unicode()):
233 self.validator = validator
235 def to_python(self, value):
236 self.assert_string(value, None)
237 return [self.validator.to_python(s.strip()) for s in value.split(';')]
239 def from_python(self, value):
240 return "; ".join([self.validator.from_python(s) for s in value])
243 class ValueCommentList(SemicolonList):
244 """A value-comment list looks like one of the following lines:
246 value (optional comment)
248 value1; value2 (optional comment)
249 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
250 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
251 This function returns the value-comment list as list of tuples:
252 [(u'value1', u'comment1'), (u'value2', None)]
253 If no comment is present, None is specified.
254 For an empty string, [] is returned."""
255 def __init__(self, value_validator=Unicode(), comments_are_optional=True):
256 SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
259 class GenericDateTime(formencode.FancyValidator):
260 """Converts a generic date/time information to a datetime class with a user defined format.
261 '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
263 def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
264 formencode.FancyValidator.__init__(self, **keywords)
265 self.date_time_format = date_time_format
267 def to_python(self, value, state=None):
268 self.assert_string(value, None)
269 try: return datetime.datetime.strptime(value, self.date_time_format)
270 except ValueError, e: raise formencode.Invalid(str(e), value, None)
272 def from_python(self, value, state=None):
273 return value.strftime(self.date_time_format)
276 class DateTimeNoSec(GenericDateTime):
277 def __init__(self, **keywords):
278 GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
281 class DateNone(NoneValidator):
282 """Converts date information to date classes with the format '%Y-%m-%d' or None."""
284 NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
287 class Geo(formencode.FancyValidator):
288 """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
289 def to_python(self, value):
290 self.assert_string(value, None)
291 r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', value)
292 if r is None: raise formencode.Invalid(u"Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, None)
293 return (float(r.groups()[0]), float(r.groups()[1]))
295 def from_python(self, value):
296 latitude, longitude = value
297 return u'%.6f N %.6f E' % (latitude, longitude)
300 class GeoNone(NoneValidator):
301 """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
303 NoneValidator.__init__(self, Geo(), (None, None))
306 class MultiGeo(formencode.FancyValidator):
307 "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
309 # Valid for input_format
310 FORMAT_GUESS = 0 # guesses the input format; default for input_format
311 FORMAT_NONE = -1 # indicates missing formats
313 # Valid for input_format and output_format
314 FORMAT_GEOCACHING = 1 # e.g. "N 47° 13.692 E 011° 25.535"
315 FORMAT_WINTERRODELN = 2 # e.g. "47.222134 N 11.467211 E"
316 FORMAT_GMAPPLUGIN = 3 # e.g. "47.232922, 11.452239"
317 FORMAT_GPX = 4 # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
319 input_format = FORMAT_GUESS
320 output_format = FORMAT_WINTERRODELN
321 last_input_format = FORMAT_NONE
323 def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
324 self.input_format = input_format
325 self.output_format = output_format
326 formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
328 def to_python(self, value):
329 self.assert_string(value, None)
330 input_format = self.input_format
331 if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
332 raise formencode.Invalid(u"input_format %d is not recognized" % input_format, value, None) # Shouldn't it be an other type of runtime error?
333 lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
337 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
338 r = re.match(u'N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
341 result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
342 last_input_format = self.FORMAT_WINTERRODELN
345 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
346 r = re.match(u'(\d+\.\d+) N (\d+\.\d+) E', line)
348 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
349 last_input_format = self.FORMAT_WINTERRODELN
352 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
353 r = re.match(u'(\d+\.\d+), ?(\d+\.\d+)', line)
355 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
356 last_input_format = self.FORMAT_GMAPPLUGIN
359 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
361 xml = minidom.parseString(line)
362 coord = xml.documentElement
363 lat = float(coord.getAttribute('lat'))
364 lon = float(coord.getAttribute('lon'))
365 try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
366 except (IndexError, ValueError): ele = None
367 result.append((lat, lon, ele))
368 last_input_format = self.FORMAT_GPX
370 except (ExpatError, IndexError, ValueError): pass
372 raise formencode.Invalid(u"Coordinates '%s' have no known format" % line, value, None)
376 def from_python(self, value):
377 output_format = self.output_format
379 for latitude, longitude, height in value:
380 if output_format == self.FORMAT_GEOCACHING:
382 result.append(u'N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
384 elif output_format == self.FORMAT_WINTERRODELN:
385 result.append(u'%.6f N %.6f E' % (latitude, longitude))
387 elif output_format == self.FORMAT_GMAPPLUGIN:
388 result.append(u'%.6f, %.6f' % (latitude, longitude))
390 elif output_format == self.FORMAT_GPX:
391 if not height is None: result.append(u'<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
392 else: result.append(u'<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
395 raise formencode.Invalid(u"output_format %d is not recognized" % output_format, value, None) # Shouldn't it be an other type of runtime error?
397 return "\n".join(result)
401 class AustrianPhoneNumber(formencode.FancyValidator):
403 Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension)
404 @param default_cc country code for prepending if none is provided, defaults to 43 (Austria)
406 >>> v = AustrianPhoneNumber()
407 >>> v.to_python(u'0512/12345678')
409 >>> v.to_python(u'+43/512/12345678')
411 >>> v.to_python(u'0512/1234567-89') # 89 is the extension
412 u'+43/512/1234567-89'
413 >>> v.to_python(u'+43/512/1234567-89')
414 u'+43/512/1234567-89'
415 >>> v.to_python(u'0512 / 12345678') # Exception
416 >>> v.to_python(u'0512-12345678') # Exception
418 # Inspired by formencode.national.InternationalPhoneNumber
420 default_cc = 43 # Default country code
421 messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."}
423 def to_python(self, value):
424 self.assert_string(value, None)
425 m = re.match(u'^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value)
427 # u'+43/512/1234567-89' => (u'43', u'512/1234567', u'89')
428 # u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89')
429 # u'+43/512/1234/567' => (u'43', u'512/1234/567', None)
430 # u'0512/1234567' => (None, u'0512/1234567', None)
431 if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
432 (country, phone, extension) = m.groups()
435 if phone.find(u'//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
439 if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
441 country = unicode(self.default_cc)
443 if extension is None: return '+%s/%s' % (country, phone)
444 return '+%s/%s-%s' % (country, phone, extension)
448 class AustrianPhoneNumberNone(NoneValidator):
450 NoneValidator.__init__(self, AustrianPhoneNumber())
454 class AustrianPhoneNumberCommentLoop(NoneValidator):
456 NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
459 class GermanDifficulty(DictValidator):
460 """Converts the difficulty represented in a number from 1 to 3 (or None)
461 to a German representation:
467 DictValidator.__init__(self, {u'': None, u'leicht': 1, u'mittel': 2, u'schwer': 3})
470 class GermanAvalanches(DictValidator):
471 """Converts the avalanches property represented as number from 1 to 4 (or None)
472 to a German representation:
476 u'gelegentlich' <=> 3
479 DictValidator.__init__(self, {u'': None, u'kaum': 1, u'selten': 2, u'gelegentlich': 3, u'häufig': 4})
482 class GermanPublicTransport(DictValidator):
483 """Converts the public_transport property represented as number from 1 to 6 (or None)
484 to a German representation:
493 DictValidator.__init__(self, {u'': None, u'Sehr gut': 1, u'Gut': 2, u'Mittelmäßig': 3, u'Schlecht': 4, u'Nein': 5, u'Ja': 6})
496 class GermanTristateFloatComment(ValueComment):
497 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
498 in parenthesis to a German text:
500 u'Ja' <=> (1.0, None)
501 u'Teilweise' <=> (0.5, None)
502 u'Nein' <=> (0.0, None)
503 u'Ja (aber schmal)' <=> (1.0, u'aber schmal')
504 u'Teilweise (oben)' <=> (0.5, u'oben')
505 u'Nein (aber breit)' <=> (0.0, u'aber breit')
508 ValueComment.__init__(self, GermanTristateFloat())
511 class UnsignedCommentNone(NoneValidator):
512 """Converts the a property with unsigned values an optional comment
513 in parenthesis to a text:
515 u'2 (Mo, Di)' <=> (2, u'Mo, Di')
519 def __init__(self, max=None):
520 NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
523 class GermanCachet(formencode.FancyValidator):
524 """Converts a "Gütesiegel":
527 u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'"""
528 def to_python(self, value):
529 self.assert_string(value, None)
530 if value == u'': return None
531 elif value == u'Nein': return value
532 elif value.startswith(u'Tiroler Naturrodelbahn-Gütesiegel '):
534 Unsigned().to_python(p[2]) # check if year can be parsed
535 if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, None)
537 else: raise formencode.Invalid(u"Unbekanntes Gütesiegel", value, None)
539 def from_python(self, value):
540 if value == None: return u''
542 return self.to_python(value)
545 class Url(formencode.FancyValidator):
546 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
547 urlv = formencode.validators.URL()
548 def to_python(self, value):
549 self.assert_string(value, None)
551 v = v.replace(u'ä', u'a')
552 v = v.replace(u'ö', u'o')
553 v = v.replace(u'ü', u'u')
554 v = v.replace(u'ß', u'ss')
555 v = self.urlv.to_python(v)
558 def from_python(self, value):
562 class UrlNeinNone(NoneValidator):
563 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
564 The special value u"Nein" is allowed."""
566 NoneValidator.__init__(self, NeinValidator(Url()))
569 class ValueCommentListNeinLoopNone(NoneValidator):
570 """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
571 An empty string is translated to None:
574 u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
576 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
579 class PhoneNumber(formencode.FancyValidator):
580 """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
581 def __init__(self, default_cc=43):
582 self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
584 def to_python(self, value):
585 return unicode(self.validator.to_python(value))
587 def from_python(self, value):
588 return self.validator.from_python(value)
591 class PhoneCommentListNeinLoopNone(NoneValidator):
592 """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
595 u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
597 def __init__(self, comments_are_optional):
598 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
601 class EmailCommentListNeinLoopNone(NoneValidator):
602 """Converts a semicolon-separated list of email addresses with optional comments to itself.
603 The special value of u'Nein' indicates that there are no email addresses.
604 The empty string translates to None:
607 u'first@example.com' <=> u'first@example.com'
608 u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
611 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(formencode.validators.Email()))))
614 class WikiPage(formencode.FancyValidator):
615 """Validates wiki page name like u'[[Birgitzer Alm]]'.
616 The page is not checked for existance.
617 An empty string is an error.
618 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
620 def to_python(self, value):
621 self.assert_string(value, None)
622 if not value.startswith('[[') or not value.endswith(']]'):
623 raise formencode.Invalid('No valid wiki page name', value, None)
626 def from_python(self, value):
630 class WikiPageList(SemicolonList):
631 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
632 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
633 u'[[Birgitzer Alm]]' <=> [u'[[Birgitzer Alm]]']
637 SemicolonList.__init__(self, WikiPage())
640 class WikiPageListLoopNone(NoneValidator):
641 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
642 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
643 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
647 NoneValidator.__init__(self, Loop(WikiPageList()))
650 class TupleSecondValidator(formencode.FancyValidator):
651 """Does not really validate anything but puts the string through
652 a validator in the second part of a tuple.
653 Examples with an Unsigned() validator and the True argument:
655 u'2' <=> (True, 2)"""
656 def __init__(self, first=True, validator=UnicodeNone()):
658 self.validator = validator
660 def to_python(self, value):
661 self.assert_string(value, None)
662 return self.first, self.validator.to_python(value)
664 def from_python(self, value):
665 assert value[0] == self.first
666 return self.validator.from_python(value[1])
669 class BoolUnicodeTupleValidator(NoneValidator):
670 """Translates an unparsed string or u'Nein' to a tuple:
672 u'Nein' <=> (False, None)
673 u'any text' <=> (True, u'any text')
675 def __init__(self, validator=UnicodeNone()):
676 NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
679 class GermanLift(BoolUnicodeTupleValidator):
680 """Checks a lift_details property. It is a value comment property with the following
687 Alternatively, the value u'Nein' is allowed.
688 An empty string maps to (None, None).
692 u'Nein' <=> (False, None)
693 u'Sessellift <=> (True, u'Sessellift')
694 u'Gondel (nur bis zur Hälfte)' <=> (True, u'Gondel (nur bis zur Hälfte)')
695 u'Sessellift; Taxi' <=> (True, u'Sessellift; Taxi')
696 u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
699 BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({u'Sessellift': u'Sessellift', u'Gondel': u'Gondel', u'Linienbus': u'Linienbus', u'Taxi': u'Taxi', u'Sonstige': u'Sonstige'}))))
702 class SledRental(BoolUnicodeTupleValidator):
703 """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments.
705 u'Nein' <=> (False, None)
706 u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
708 BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))