2 # -*- coding: iso-8859-15 -*-
3 """This file contains "validators" that convert between string and python (database) representation
4 of properties used in the "Rodelbahnbox" and "Gasthausbox".
5 The "to_python" method has to get a unicode argument.
7 >>> nosetests --with-pylons=test.ini
9 TODO: Replace by wrpylib.wrvalidators
12 import formencode.national
15 import xml.dom.minidom as minidom
16 from xml.parsers.expat import ExpatError
19 class NoneValidator(formencode.FancyValidator):
20 """Takes a validator and makes it possible that empty strings are mapped to None."""
21 def __init__(self, validator, python_none=None):
22 self.validator = validator
23 self.python_none = python_none
25 def to_python(self, value):
26 self.assert_string(value, None)
27 if value == '': return self.python_none
28 return self.validator.to_python(value)
30 def from_python(self, value):
31 if value == self.python_none: return ''
32 return self.validator.from_python(value)
35 class NeinValidator(formencode.FancyValidator):
36 """Take an arbitrary validator and adds the possibility that the
37 string can be u'Nein'.
38 Example together with an UnsignedNone validator:
39 >>> v = NeinValidator(UnsignedNone())
42 >>> v.to_python(u'34')
44 >>> v.to_python(u'Nein')
47 def __init__(self, validator, python_no='Nein'):
48 self.validator = validator
49 self.python_no = python_no
51 def to_python(self, value):
52 self.assert_string(value, None)
53 if value == 'Nein': return self.python_no
54 return self.validator.to_python(value)
56 def from_python(self, value):
57 if value == self.python_no: return 'Nein'
58 return self.validator.from_python(value)
61 class Unicode(formencode.FancyValidator):
62 """Converts an unicode string to an unicode string:
63 u'any string' <=> u'any string'"""
64 def to_python(self, value):
65 self.assert_string(value, None)
68 def from_python(self, value):
72 class UnicodeNone(NoneValidator):
73 """Converts an unicode string to an unicode string:
75 u'any string' <=> u'any string'"""
77 NoneValidator.__init__(self, Unicode())
80 class Unsigned(formencode.FancyValidator):
81 """Converts an unsigned number to a string and vice versa:
86 def __init__(self, max=None):
87 self.iv = formencode.validators.Int(min=0, max=max)
89 def to_python(self, value):
90 self.assert_string(value, None)
91 return self.iv.to_python(value)
93 def from_python(self, value):
97 class UnsignedNone(NoneValidator):
98 """Converts an unsigned number to a string and vice versa:
104 def __init__(self, max=None):
105 NoneValidator.__init__(self, Unsigned(max))
108 class UnsignedNeinNone(NoneValidator):
109 """ Translates a number of Nein to a number.
117 NoneValidator.__init__(self, UnsignedNone())
120 class Loop(formencode.FancyValidator):
121 """Takes a validator and calls from_python(to_python(value))."""
122 def __init__(self, validator):
123 self.validator = validator
125 def to_python(self, value):
126 self.assert_string(value, None)
127 return self.validator.from_python(self.validator.to_python(value))
129 def from_python(self, value):
130 # we don't call self.validator.to_python(self.validator.from_python(value))
131 # here because our to_python implementation basically leaves the input untouches
132 # and so should from_python do.
133 return self.validator.from_python(self.validator.to_python(value))
136 class DictValidator(formencode.FancyValidator):
137 """Translates strings to other values via a python directory.
138 >>> boolValidator = DictValidator({u'': None, u'Ja': True, u'Nein': False})
139 >>> boolValidator.to_python(u'')
141 >>> boolValidator.to_python(u'Ja')
144 def __init__(self, dict):
147 def to_python(self, value):
148 self.assert_string(value, None)
149 if value not in self.dict: raise formencode.Invalid("Key not found in dict.", value, None)
150 return self.dict[value]
152 def from_python(self, value):
153 for k, v in self.dict.items():
154 if type(v) == type(value) and v == value: return k
155 raise formencode.Invalid('Invalid value', value, None)
158 class GermanBoolNone(DictValidator):
159 """Converts German bool values to the python bool type:
165 DictValidator.__init__(self, {'': None, 'Ja': True, 'Nein': False})
168 class GermanTristateTuple(DictValidator):
169 """Does the following conversion:
171 u'Ja' <=> (True, False)
172 u'Teilweise' <=> (True, True)
173 u'Nein' <=> (False, True)"""
174 def __init__(self, yes_python = (True, False), no_python = (False, True), partly_python = (True, True), none_python = (None, None)):
175 DictValidator.__init__(self, {'': none_python, 'Ja': yes_python, 'Nein': no_python, 'Teilweise': partly_python})
178 class GermanTristateFloat(GermanTristateTuple):
179 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None
186 GermanTristateTuple.__init__(self, yes_python=1.0, no_python=0.0, partly_python=0.5, none_python=None)
189 class ValueComment(formencode.FancyValidator):
190 """Converts value with a potentially optional comment to a python tuple:
192 u'value' <=> (u'value', None)
193 u'value (comment)' <=> (u'value', u'comment')"""
194 def __init__(self, value_validator=UnicodeNone(), comment_validator=UnicodeNone(), comment_is_optional=True):
195 self.value_validator = value_validator
196 self.comment_validator = comment_validator
197 self.comment_is_optional = comment_is_optional
199 def to_python(self, value):
200 self.assert_string(value, None)
205 left = value.find('(')
206 right = value.rfind(')')
207 if left < 0 and right < 0:
208 if not self.comment_is_optional: raise formencode.Invalid('Mandatory comment not present', value, None)
211 elif left >= 0 and right >= 0 and left < right:
212 v = value[:left].strip()
213 c = value[left+1:right].strip()
214 else: raise formencode.Invalid('Invalid format', value, None)
215 return self.value_validator.to_python(v), self.comment_validator.to_python(c)
217 def from_python(self, value):
218 assert len(value) == 2
219 v = self.value_validator.from_python(value[0])
220 c = self.comment_validator.from_python(value[1])
222 if len(v) > 0: return '%s (%s)' % (v, c)
223 else: return '(%s)' % c
227 class SemicolonList(formencode.FancyValidator):
228 """Applies a given validator to a semicolon separated list of values and returns a python list.
229 For an empty string an empty list is returned."""
230 def __init__(self, validator=Unicode()):
231 self.validator = validator
233 def to_python(self, value):
234 self.assert_string(value, None)
235 return [self.validator.to_python(s.strip()) for s in value.split(';')]
237 def from_python(self, value):
238 return "; ".join([self.validator.from_python(s) for s in value])
241 class ValueCommentList(SemicolonList):
242 """A value-comment list looks like one of the following lines:
244 value (optional comment)
246 value1; value2 (optional comment)
247 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
248 value1 (optional comment1); value2 (optional comment2); value3 (otional comment3)
249 This function returns the value-comment list as list of tuples:
250 [(u'value1', u'comment1'), (u'value2', None)]
251 If no comment is present, None is specified.
252 For an empty string, [] is returned."""
253 def __init__(self, value_validator=Unicode(), comments_are_optional=True):
254 SemicolonList.__init__(self, ValueComment(value_validator, comment_is_optional=comments_are_optional))
257 class GenericDateTime(formencode.FancyValidator):
258 """Converts a generic date/time information to a datetime class with a user defined format.
259 '2009-03-22 20:36:15' would be specified as '%Y-%m-%d %H:%M:%S'."""
261 def __init__(self, date_time_format = '%Y-%m-%d %H:%M:%S', **keywords):
262 formencode.FancyValidator.__init__(self, **keywords)
263 self.date_time_format = date_time_format
265 def to_python(self, value, state=None):
266 self.assert_string(value, None)
267 try: return datetime.datetime.strptime(value, self.date_time_format)
268 except ValueError as e: raise formencode.Invalid(str(e), value, None)
270 def from_python(self, value, state=None):
271 return value.strftime(self.date_time_format)
274 class DateTimeNoSec(GenericDateTime):
275 def __init__(self, **keywords):
276 GenericDateTime.__init__(self, '%Y-%m-%d %H:%M', **keywords)
279 class DateNone(NoneValidator):
280 """Converts date information to date classes with the format '%Y-%m-%d' or None."""
282 NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
285 class Geo(formencode.FancyValidator):
286 """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
287 def to_python(self, value):
288 self.assert_string(value, None)
289 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
290 if r is None: raise formencode.Invalid("Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, None)
291 return (float(r.groups()[0]), float(r.groups()[1]))
293 def from_python(self, value):
294 latitude, longitude = value
295 return '%.6f N %.6f E' % (latitude, longitude)
298 class GeoNone(NoneValidator):
299 """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
301 NoneValidator.__init__(self, Geo(), (None, None))
304 class MultiGeo(formencode.FancyValidator):
305 "Formats multiple coordinates, even in multiple lines to [(latitude, longitude, elevation), ...] or [(latitude, longitude, None), ...] tuplets."
307 # Valid for input_format
308 FORMAT_GUESS = 0 # guesses the input format; default for input_format
309 FORMAT_NONE = -1 # indicates missing formats
311 # Valid for input_format and output_format
312 FORMAT_GEOCACHING = 1 # e.g. "N 47° 13.692 E 011° 25.535"
313 FORMAT_WINTERRODELN = 2 # e.g. "47.222134 N 11.467211 E"
314 FORMAT_GMAPPLUGIN = 3 # e.g. "47.232922, 11.452239"
315 FORMAT_GPX = 4 # e.g. "<trkpt lat="47.181289" lon="11.408827"><ele>1090.57</ele></trkpt>"
317 input_format = FORMAT_GUESS
318 output_format = FORMAT_WINTERRODELN
319 last_input_format = FORMAT_NONE
321 def __init__(self, input_format = FORMAT_GUESS, output_format = FORMAT_WINTERRODELN, **keywords):
322 self.input_format = input_format
323 self.output_format = output_format
324 formencode.FancyValidator.__init__(self, if_empty = (None, None, None), **keywords)
326 def to_python(self, value):
327 self.assert_string(value, None)
328 input_format = self.input_format
329 if not input_format in [self.FORMAT_GUESS, self.FORMAT_GEOCACHING, self.FORMAT_WINTERRODELN, self.FORMAT_GMAPPLUGIN, self.FORMAT_GPX]:
330 raise formencode.Invalid("input_format %d is not recognized" % input_format, value, None) # Shouldn't it be an other type of runtime error?
331 lines = [line.strip() for line in value.split("\n") if len(line.strip()) > 0]
335 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GEOCACHING:
336 r = re.match('N ?(\d+)° ?(\d+\.\d+) +E ?(\d+)° ?(\d+\.\d+)', line)
339 result.append((float(g[0]) + float(g[1])/60, float(g[2]) + float(g[3])/60, None))
340 last_input_format = self.FORMAT_WINTERRODELN
343 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_WINTERRODELN:
344 r = re.match('(\d+\.\d+) N (\d+\.\d+) E', line)
346 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
347 last_input_format = self.FORMAT_WINTERRODELN
350 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GMAPPLUGIN:
351 r = re.match('(\d+\.\d+), ?(\d+\.\d+)', line)
353 result.append((float(r.groups()[0]), float(r.groups()[1]), None))
354 last_input_format = self.FORMAT_GMAPPLUGIN
357 if input_format == self.FORMAT_GUESS or input_format == self.FORMAT_GPX:
359 xml = minidom.parseString(line)
360 coord = xml.documentElement
361 lat = float(coord.getAttribute('lat'))
362 lon = float(coord.getAttribute('lon'))
363 try: ele = float(coord.childNodes[0].childNodes[0].nodeValue)
364 except (IndexError, ValueError): ele = None
365 result.append((lat, lon, ele))
366 last_input_format = self.FORMAT_GPX
368 except (ExpatError, IndexError, ValueError): pass
370 raise formencode.Invalid("Coordinates '%s' have no known format" % line, value, None)
374 def from_python(self, value):
375 output_format = self.output_format
377 for latitude, longitude, height in value:
378 if output_format == self.FORMAT_GEOCACHING:
380 result.append('N %02d° %02.3f E %03d° %02.3f' % (latitude, latitude % 1 * 60, longitude, longitude % 1 * 60))
382 elif output_format == self.FORMAT_WINTERRODELN:
383 result.append('%.6f N %.6f E' % (latitude, longitude))
385 elif output_format == self.FORMAT_GMAPPLUGIN:
386 result.append('%.6f, %.6f' % (latitude, longitude))
388 elif output_format == self.FORMAT_GPX:
389 if not height is None: result.append('<trkpt lat="%.6f" lon="%.6f"><ele>%.2f</ele></trkpt>' % (latitude, longitude, height))
390 else: result.append('<trkpt lat="%.6f" lon="%.6f"/>' % (latitude, longitude))
393 raise formencode.Invalid("output_format %d is not recognized" % output_format, value, None) # Shouldn't it be an other type of runtime error?
395 return "\n".join(result)
399 class AustrianPhoneNumber(formencode.FancyValidator):
401 Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension)
402 @param default_cc country code for prepending if none is provided, defaults to 43 (Austria)
404 >>> v = AustrianPhoneNumber()
405 >>> v.to_python(u'0512/12345678')
407 >>> v.to_python(u'+43/512/12345678')
409 >>> v.to_python(u'0512/1234567-89') # 89 is the extension
410 u'+43/512/1234567-89'
411 >>> v.to_python(u'+43/512/1234567-89')
412 u'+43/512/1234567-89'
413 >>> v.to_python(u'0512 / 12345678') # Exception
414 >>> v.to_python(u'0512-12345678') # Exception
416 # Inspired by formencode.national.InternationalPhoneNumber
418 default_cc = 43 # Default country code
419 messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."}
421 def to_python(self, value):
422 self.assert_string(value, None)
423 m = re.match('^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value)
425 # u'+43/512/1234567-89' => (u'43', u'512/1234567', u'89')
426 # u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89')
427 # u'+43/512/1234/567' => (u'43', u'512/1234/567', None)
428 # u'0512/1234567' => (None, u'0512/1234567', None)
429 if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
430 (country, phone, extension) = m.groups()
433 if phone.find('//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
437 if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, None)
439 country = str(self.default_cc)
441 if extension is None: return '+%s/%s' % (country, phone)
442 return '+%s/%s-%s' % (country, phone, extension)
446 class AustrianPhoneNumberNone(NoneValidator):
448 NoneValidator.__init__(self, AustrianPhoneNumber())
452 class AustrianPhoneNumberCommentLoop(NoneValidator):
454 NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
457 class GermanDifficulty(DictValidator):
458 """Converts the difficulty represented in a number from 1 to 3 (or None)
459 to a German representation:
465 DictValidator.__init__(self, {'': None, 'leicht': 1, 'mittel': 2, 'schwer': 3})
468 class GermanAvalanches(DictValidator):
469 """Converts the avalanches property represented as number from 1 to 4 (or None)
470 to a German representation:
474 u'gelegentlich' <=> 3
477 DictValidator.__init__(self, {'': None, 'kaum': 1, 'selten': 2, 'gelegentlich': 3, 'häufig': 4})
480 class GermanPublicTransport(DictValidator):
481 """Converts the public_transport property represented as number from 1 to 6 (or None)
482 to a German representation:
491 DictValidator.__init__(self, {'': None, 'Sehr gut': 1, 'Gut': 2, 'Mittelmäßig': 3, 'Schlecht': 4, 'Nein': 5, 'Ja': 6})
494 class GermanTristateFloatComment(ValueComment):
495 """Converts the a property with the possible values 0.0, 0.5, 1.0 or None and an optional comment
496 in parenthesis to a German text:
498 u'Ja' <=> (1.0, None)
499 u'Teilweise' <=> (0.5, None)
500 u'Nein' <=> (0.0, None)
501 u'Ja (aber schmal)' <=> (1.0, u'aber schmal')
502 u'Teilweise (oben)' <=> (0.5, u'oben')
503 u'Nein (aber breit)' <=> (0.0, u'aber breit')
506 ValueComment.__init__(self, GermanTristateFloat())
509 class UnsignedCommentNone(NoneValidator):
510 """Converts the a property with unsigned values an optional comment
511 in parenthesis to a text:
513 u'2 (Mo, Di)' <=> (2, u'Mo, Di')
517 def __init__(self, max=None):
518 NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
521 class GermanCachet(formencode.FancyValidator):
522 """Converts a "Gütesiegel":
525 u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'"""
526 def to_python(self, value):
527 self.assert_string(value, None)
528 if value == '': return None
529 elif value == 'Nein': return value
530 elif value.startswith('Tiroler Naturrodelbahn-Gütesiegel '):
532 Unsigned().to_python(p[2]) # check if year can be parsed
533 if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, None)
535 else: raise formencode.Invalid("Unbekanntes Gütesiegel", value, None)
537 def from_python(self, value):
538 if value == None: return ''
540 return self.to_python(self, value)
543 class Url(formencode.FancyValidator):
544 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed."""
545 urlv = formencode.validators.URL()
546 def to_python(self, value):
547 self.assert_string(value, None)
549 v = v.replace('ä', 'a')
550 v = v.replace('ö', 'o')
551 v = v.replace('ü', 'u')
552 v = v.replace('ß', 'ss')
553 v = self.urlv.to_python(v)
556 def from_python(self, value):
560 class UrlNeinNone(NoneValidator):
561 """Validates an URL. In contrast to fromencode.validators.URL, umlauts are allowed.
562 The special value u"Nein" is allowed."""
564 NoneValidator.__init__(self, NeinValidator(Url()))
567 class ValueCommentListNeinLoopNone(NoneValidator):
568 """Translates a semicolon separated list of values with optional comments in paranthesis or u'Nein' to itself.
569 An empty string is translated to None:
572 u'T-Mobile (gut); A1' <=> u'T-Mobile (gut); A1'"""
574 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
577 class PhoneNumber(formencode.FancyValidator):
578 """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
579 def __init__(self, default_cc=43):
580 self.validator = formencode.national.InternationalPhoneNumber(default_cc=lambda: default_cc)
582 def to_python(self, value):
583 return str(self.validator.to_python(value))
585 def from_python(self, value):
586 return self.validator.from_python(value)
589 class PhoneCommentListNeinLoopNone(NoneValidator):
590 """List with semicolon-separated phone numbers in international format with optional comment or 'Nein' as string:
593 u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456' <=> u'+43-699-1234567 (nicht nach 20:00 Uhr); +43-512-123456'
595 def __init__(self, comments_are_optional):
596 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(PhoneNumber(default_cc=43), comments_are_optional=comments_are_optional))))
599 class EmailCommentListNeinLoopNone(NoneValidator):
600 """Converts a semicolon-separated list of email addresses with optional comments to itself.
601 The special value of u'Nein' indicates that there are no email addresses.
602 The empty string translates to None:
605 u'first@example.com' <=> u'first@example.com'
606 u'first@example.com (Nur Winter); second@example.com' <=> u'first@example.com (Nur Winter); second@example.com'
609 NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList(formencode.validators.Email()))))
612 class WikiPage(formencode.FancyValidator):
613 """Validates wiki page name like u'[[Birgitzer Alm]]'.
614 The page is not checked for existance.
615 An empty string is an error.
616 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
618 def to_python(self, value):
619 self.assert_string(value, None)
620 if not value.startswith('[[') or not value.endswith(']]'):
621 raise formencode.Invalid('No valid wiki page name', value, None)
624 def from_python(self, value):
628 class WikiPageList(SemicolonList):
629 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]'.
630 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> [u'[[Birgitzer Alm]]', u'[[Kemater Alm]]']
631 u'[[Birgitzer Alm]]' <=> [u'[[Birgitzer Alm]]']
635 SemicolonList.__init__(self, WikiPage())
638 class WikiPageListLoopNone(NoneValidator):
639 """Validates a list of wiki pages like u'[[Birgitzer Alm]]; [[Kemater Alm]]' as string.
640 u'[[Birgitzer Alm]]; [[Kemater Alm]]' <=> u'[[Birgitzer Alm]]; [[Kemater Alm]]'
641 u'[[Birgitzer Alm]]' <=> u'[[Birgitzer Alm]]'
645 NoneValidator.__init__(self, Loop(WikiPageList()))
648 class TupleSecondValidator(formencode.FancyValidator):
649 """Does not really validate anything but puts the string through
650 a validator in the second part of a tuple.
651 Examples with an Unsigned() validator and the True argument:
653 u'2' <=> (True, 2)"""
654 def __init__(self, first=True, validator=UnicodeNone()):
656 self.validator = validator
658 def to_python(self, value):
659 self.assert_string(value, None)
660 return self.first, self.validator.to_python(value)
662 def from_python(self, value):
663 assert value[0] == self.first
664 return self.validator.from_python(value[1])
667 class BoolUnicodeTupleValidator(NoneValidator):
668 """Translates an unparsed string or u'Nein' to a tuple:
670 u'Nein' <=> (False, None)
671 u'any text' <=> (True, u'any text')
673 def __init__(self, validator=UnicodeNone()):
674 NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
677 class GermanLift(BoolUnicodeTupleValidator):
678 """Checks a lift_details property. It is a value comment property with the following
685 Alternatively, the value u'Nein' is allowed.
686 An empty string maps to (None, None).
690 u'Nein' <=> (False, None)
691 u'Sessellift <=> (True, u'Sessellift')
692 u'Gondel (nur bis zur Hälfte)' <=> (True, u'Gondel (nur bis zur Hälfte)')
693 u'Sessellift; Taxi' <=> (True, u'Sessellift; Taxi')
694 u'Sessellift (Wochenende); Taxi (6 Euro)' <=> (True, u'Sessellift (Wochenende); Taxi (6 Euro)')
697 BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'}))))
700 class SledRental(BoolUnicodeTupleValidator):
701 """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments.
703 u'Nein' <=> (False, None)
704 u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
706 BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))