]> git.phdru.name Git - sqlconvert.git/blob - devscripts/CI/validators.py
CI: Remove branch limitations
[sqlconvert.git] / devscripts / CI / validators.py
1 ## FormEncode, a  Form processor
2 ## Copyright (C) 2003, Ian Bicking <ianb@colorstudy.com>
3
4 """
5 Validator/Converters for use with FormEncode.
6 """
7
8 import cgi
9 import locale
10 import re
11 import warnings
12 from encodings import idna
13
14 try:  # import dnspython
15     import dns.resolver
16     import dns.exception
17 except (IOError, ImportError):
18     have_dns = False
19 else:
20     have_dns = True
21
22
23 # These are only imported when needed
24 httplib = None
25 random = None
26 sha1 = None
27 socket = None
28 urlparse = None
29
30 from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator,
31     deprecation_warning, is_empty)
32
33 assert Identity and Invalid and NoDefault  # silence unused import warnings
34
35 # Dummy i18n translation function, nothing is translated here.
36 # Instead this is actually done in api.message.
37 # The surrounding _('string') of the strings is only for extracting
38 # the strings automatically.
39 # If you run pygettext with this source comment this function out temporarily.
40 _ = lambda s: s
41
42
43 ############################################################
44 ## Utility methods
45 ############################################################
46
47 # These all deal with accepting both datetime and mxDateTime modules and types
48 datetime_module = None
49 mxDateTime_module = None
50
51
52 def import_datetime(module_type):
53     global datetime_module, mxDateTime_module
54     module_type = module_type.lower() if module_type else 'datetime'
55     if module_type == 'datetime':
56         if datetime_module is None:
57             import datetime as datetime_module
58         return datetime_module
59     elif module_type == 'mxdatetime':
60         if mxDateTime_module is None:
61             from mx import DateTime as mxDateTime_module
62         return mxDateTime_module
63     else:
64         raise ImportError('Invalid datetime module %r' % module_type)
65
66
67 def datetime_now(module):
68     if module.__name__ == 'datetime':
69         return module.datetime.now()
70     else:
71         return module.now()
72
73
74 def datetime_makedate(module, year, month, day):
75     if module.__name__ == 'datetime':
76         return module.date(year, month, day)
77     else:
78         try:
79             return module.DateTime(year, month, day)
80         except module.RangeError as e:
81             raise ValueError(str(e))
82
83
84 def datetime_time(module):
85     if module.__name__ == 'datetime':
86         return module.time
87     else:
88         return module.Time
89
90
91 def datetime_isotime(module):
92     if module.__name__ == 'datetime':
93         return module.time.isoformat
94     else:
95         return module.ISO.Time
96
97
98 ############################################################
99 ## Wrapper Validators
100 ############################################################
101
102 class ConfirmType(FancyValidator):
103     """
104     Confirms that the input/output is of the proper type.
105
106     Uses the parameters:
107
108     subclass:
109         The class or a tuple of classes; the item must be an instance
110         of the class or a subclass.
111     type:
112         A type or tuple of types (or classes); the item must be of
113         the exact class or type.  Subclasses are not allowed.
114
115     Examples::
116
117         >>> cint = ConfirmType(subclass=int)
118         >>> cint.to_python(True)
119         True
120         >>> cint.to_python('1')
121         Traceback (most recent call last):
122             ...
123         Invalid: '1' is not a subclass of <type 'int'>
124         >>> cintfloat = ConfirmType(subclass=(float, int))
125         >>> cintfloat.to_python(1.0), cintfloat.from_python(1.0)
126         (1.0, 1.0)
127         >>> cintfloat.to_python(1), cintfloat.from_python(1)
128         (1, 1)
129         >>> cintfloat.to_python(None)
130         Traceback (most recent call last):
131             ...
132         Invalid: None is not a subclass of one of the types <type 'float'>, <type 'int'>
133         >>> cint2 = ConfirmType(type=int)
134         >>> cint2(accept_python=False).from_python(True)
135         Traceback (most recent call last):
136             ...
137         Invalid: True must be of the type <type 'int'>
138     """
139
140     accept_iterator = True
141
142     subclass = None
143     type = None
144
145     messages = dict(
146         subclass=_('%(object)r is not a subclass of %(subclass)s'),
147         inSubclass=_('%(object)r is not a subclass of one of the types %(subclassList)s'),
148         inType=_('%(object)r must be one of the types %(typeList)s'),
149         type=_('%(object)r must be of the type %(type)s'))
150
151     def __init__(self, *args, **kw):
152         FancyValidator.__init__(self, *args, **kw)
153         if self.subclass:
154             if isinstance(self.subclass, list):
155                 self.subclass = tuple(self.subclass)
156             elif not isinstance(self.subclass, tuple):
157                 self.subclass = (self.subclass,)
158             self._validate_python = self.confirm_subclass
159         if self.type:
160             if isinstance(self.type, list):
161                 self.type = tuple(self.type)
162             elif not isinstance(self.type, tuple):
163                 self.type = (self.type,)
164             self._validate_python = self.confirm_type
165
166     def confirm_subclass(self, value, state):
167         if not isinstance(value, self.subclass):
168             if len(self.subclass) == 1:
169                 msg = self.message('subclass', state, object=value,
170                                    subclass=self.subclass[0])
171             else:
172                 subclass_list = ', '.join(map(str, self.subclass))
173                 msg = self.message('inSubclass', state, object=value,
174                                    subclassList=subclass_list)
175             raise Invalid(msg, value, state)
176
177     def confirm_type(self, value, state):
178         for t in self.type:
179             if type(value) is t:
180                 break
181         else:
182             if len(self.type) == 1:
183                 msg = self.message('type', state, object=value,
184                                    type=self.type[0])
185             else:
186                 msg = self.message('inType', state, object=value,
187                                    typeList=', '.join(map(str, self.type)))
188             raise Invalid(msg, value, state)
189         return value
190
191     def is_empty(self, value):
192         return False
193
194
195 class Wrapper(FancyValidator):
196     """
197     Used to convert functions to validator/converters.
198
199     You can give a simple function for `_convert_to_python`,
200     `_convert_from_python`, `_validate_python` or `_validate_other`.
201     If that function raises an exception, the value is considered invalid.
202     Whatever value the function returns is considered the converted value.
203
204     Unlike validators, the `state` argument is not used.  Functions
205     like `int` can be used here, that take a single argument.
206
207     Note that as Wrapper will generate a FancyValidator, empty
208     values (those who pass ``FancyValidator.is_empty)`` will return ``None``.
209     To override this behavior you can use ``Wrapper(empty_value=callable)``.
210     For example passing ``Wrapper(empty_value=lambda val: val)`` will return
211     the value itself when is considered empty.
212
213     Examples::
214
215         >>> def downcase(v):
216         ...     return v.lower()
217         >>> wrap = Wrapper(convert_to_python=downcase)
218         >>> wrap.to_python('This')
219         'this'
220         >>> wrap.from_python('This')
221         'This'
222         >>> wrap.to_python('') is None
223         True
224         >>> wrap2 = Wrapper(
225         ...     convert_from_python=downcase, empty_value=lambda value: value)
226         >>> wrap2.from_python('This')
227         'this'
228         >>> wrap2.to_python('')
229         ''
230         >>> wrap2.from_python(1)
231         Traceback (most recent call last):
232           ...
233         Invalid: 'int' object has no attribute 'lower'
234         >>> wrap3 = Wrapper(validate_python=int)
235         >>> wrap3.to_python('1')
236         '1'
237         >>> wrap3.to_python('a') # doctest: +ELLIPSIS
238         Traceback (most recent call last):
239           ...
240         Invalid: invalid literal for int()...
241     """
242
243     func_convert_to_python = None
244     func_convert_from_python = None
245     func_validate_python = None
246     func_validate_other = None
247
248     _deprecated_methods = (
249         ('func_to_python', 'func_convert_to_python'),
250         ('func_from_python', 'func_convert_from_python'))
251
252     def __init__(self, *args, **kw):
253         # allow old method names as parameters
254         if 'to_python' in kw and 'convert_to_python' not in kw:
255             kw['convert_to_python'] = kw.pop('to_python')
256         if 'from_python' in kw and 'convert_from_python' not in kw:
257             kw['convert_from_python'] = kw.pop('from_python')
258         for n in ('convert_to_python', 'convert_from_python',
259                   'validate_python', 'validate_other'):
260             if n in kw:
261                 kw['func_%s' % n] = kw.pop(n)
262         FancyValidator.__init__(self, *args, **kw)
263         self._convert_to_python = self.wrap(self.func_convert_to_python)
264         self._convert_from_python = self.wrap(self.func_convert_from_python)
265         self._validate_python = self.wrap(self.func_validate_python)
266         self._validate_other = self.wrap(self.func_validate_other)
267
268     def wrap(self, func):
269         if not func:
270             return None
271
272         def result(value, state, func=func):
273             try:
274                 return func(value)
275             except Exception as e:
276                 raise Invalid(str(e), value, state)
277
278         return result
279
280
281 class Constant(FancyValidator):
282     """
283     This converter converts everything to the same thing.
284
285     I.e., you pass in the constant value when initializing, then all
286     values get converted to that constant value.
287
288     This is only really useful for funny situations, like::
289
290       # Any evaluates sub validators in reverse order for to_python
291       fromEmailValidator = Any(
292                             Constant('unknown@localhost'),
293                                Email())
294
295     In this case, the if the email is not valid
296     ``'unknown@localhost'`` will be used instead.  Of course, you
297     could use ``if_invalid`` instead.
298
299     Examples::
300
301         >>> Constant('X').to_python('y')
302         'X'
303     """
304
305     __unpackargs__ = ('value',)
306
307     def _convert_to_python(self, value, state):
308         return self.value
309
310     _convert_from_python = _convert_to_python
311
312
313 ############################################################
314 ## Normal validators
315 ############################################################
316
317 class MaxLength(FancyValidator):
318     """
319     Invalid if the value is longer than `maxLength`.  Uses len(),
320     so it can work for strings, lists, or anything with length.
321
322     Examples::
323
324         >>> max5 = MaxLength(5)
325         >>> max5.to_python('12345')
326         '12345'
327         >>> max5.from_python('12345')
328         '12345'
329         >>> max5.to_python('123456')
330         Traceback (most recent call last):
331           ...
332         Invalid: Enter a value less than 5 characters long
333         >>> max5(accept_python=False).from_python('123456')
334         Traceback (most recent call last):
335           ...
336         Invalid: Enter a value less than 5 characters long
337         >>> max5.to_python([1, 2, 3])
338         [1, 2, 3]
339         >>> max5.to_python([1, 2, 3, 4, 5, 6])
340         Traceback (most recent call last):
341           ...
342         Invalid: Enter a value less than 5 characters long
343         >>> max5.to_python(5)
344         Traceback (most recent call last):
345           ...
346         Invalid: Invalid value (value with length expected)
347     """
348
349     __unpackargs__ = ('maxLength',)
350
351     messages = dict(
352         tooLong=_('Enter a value less than %(maxLength)i characters long'),
353         invalid=_('Invalid value (value with length expected)'))
354
355     def _validate_python(self, value, state):
356         try:
357             if value and len(value) > self.maxLength:
358                 raise Invalid(
359                     self.message('tooLong', state,
360                         maxLength=self.maxLength), value, state)
361             else:
362                 return None
363         except TypeError:
364             raise Invalid(
365                 self.message('invalid', state), value, state)
366
367
368 class MinLength(FancyValidator):
369     """
370     Invalid if the value is shorter than `minlength`.  Uses len(), so
371     it can work for strings, lists, or anything with length.  Note
372     that you **must** use ``not_empty=True`` if you don't want to
373     accept empty values -- empty values are not tested for length.
374
375     Examples::
376
377         >>> min5 = MinLength(5)
378         >>> min5.to_python('12345')
379         '12345'
380         >>> min5.from_python('12345')
381         '12345'
382         >>> min5.to_python('1234')
383         Traceback (most recent call last):
384           ...
385         Invalid: Enter a value at least 5 characters long
386         >>> min5(accept_python=False).from_python('1234')
387         Traceback (most recent call last):
388           ...
389         Invalid: Enter a value at least 5 characters long
390         >>> min5.to_python([1, 2, 3, 4, 5])
391         [1, 2, 3, 4, 5]
392         >>> min5.to_python([1, 2, 3])
393         Traceback (most recent call last):
394           ...
395         Invalid: Enter a value at least 5 characters long
396         >>> min5.to_python(5)
397         Traceback (most recent call last):
398           ...
399         Invalid: Invalid value (value with length expected)
400
401     """
402
403     __unpackargs__ = ('minLength',)
404
405     messages = dict(
406         tooShort=_('Enter a value at least %(minLength)i characters long'),
407         invalid=_('Invalid value (value with length expected)'))
408
409     def _validate_python(self, value, state):
410         try:
411             if len(value) < self.minLength:
412                 raise Invalid(
413                     self.message('tooShort', state,
414                         minLength=self.minLength), value, state)
415         except TypeError:
416             raise Invalid(
417                 self.message('invalid', state), value, state)
418
419
420 class NotEmpty(FancyValidator):
421     """
422     Invalid if value is empty (empty string, empty list, etc).
423
424     Generally for objects that Python considers false, except zero
425     which is not considered invalid.
426
427     Examples::
428
429         >>> ne = NotEmpty(messages=dict(empty='enter something'))
430         >>> ne.to_python('')
431         Traceback (most recent call last):
432           ...
433         Invalid: enter something
434         >>> ne.to_python(0)
435         0
436     """
437     not_empty = True
438
439     messages = dict(
440         empty=_('Please enter a value'))
441
442     def _validate_python(self, value, state):
443         if value == 0:
444             # This isn't "empty" for this definition.
445             return value
446         if not value:
447             raise Invalid(self.message('empty', state), value, state)
448
449
450 class Empty(FancyValidator):
451     """
452     Invalid unless the value is empty.  Use cleverly, if at all.
453
454     Examples::
455
456         >>> Empty.to_python(0)
457         Traceback (most recent call last):
458           ...
459         Invalid: You cannot enter a value here
460     """
461
462     messages = dict(
463         notEmpty=_('You cannot enter a value here'))
464
465     def _validate_python(self, value, state):
466         if value or value == 0:
467             raise Invalid(self.message('notEmpty', state), value, state)
468
469
470 class Regex(FancyValidator):
471     """
472     Invalid if the value doesn't match the regular expression `regex`.
473
474     The regular expression can be a compiled re object, or a string
475     which will be compiled for you.
476
477     Use strip=True if you want to strip the value before validation,
478     and as a form of conversion (often useful).
479
480     Examples::
481
482         >>> cap = Regex(r'^[A-Z]+$')
483         >>> cap.to_python('ABC')
484         'ABC'
485
486     Note that ``.from_python()`` calls (in general) do not validate
487     the input::
488
489         >>> cap.from_python('abc')
490         'abc'
491         >>> cap(accept_python=False).from_python('abc')
492         Traceback (most recent call last):
493           ...
494         Invalid: The input is not valid
495         >>> cap.to_python(1)
496         Traceback (most recent call last):
497           ...
498         Invalid: The input must be a string (not a <type 'int'>: 1)
499         >>> Regex(r'^[A-Z]+$', strip=True).to_python('  ABC  ')
500         'ABC'
501         >>> Regex(r'this', regexOps=('I',)).to_python('THIS')
502         'THIS'
503     """
504
505     regexOps = ()
506     strip = False
507     regex = None
508
509     __unpackargs__ = ('regex',)
510
511     messages = dict(
512         invalid=_('The input is not valid'))
513
514     def __init__(self, *args, **kw):
515         FancyValidator.__init__(self, *args, **kw)
516         if isinstance(self.regex, str):
517             ops = 0
518             assert not isinstance(self.regexOps, str), (
519                 "regexOps should be a list of options from the re module "
520                 "(names, or actual values)")
521             for op in self.regexOps:
522                 if isinstance(op, str):
523                     ops |= getattr(re, op)
524                 else:
525                     ops |= op
526             self.regex = re.compile(self.regex, ops)
527
528     def _validate_python(self, value, state):
529         self.assert_string(value, state)
530         if self.strip and isinstance(value, str):
531             value = value.strip()
532         if not self.regex.search(value):
533             raise Invalid(self.message('invalid', state), value, state)
534
535     def _convert_to_python(self, value, state):
536         if self.strip and isinstance(value, str):
537             return value.strip()
538         return value
539
540
541 class PlainText(Regex):
542     """
543     Test that the field contains only letters, numbers, underscore,
544     and the hyphen.  Subclasses Regex.
545
546     Examples::
547
548         >>> PlainText.to_python('_this9_')
549         '_this9_'
550         >>> PlainText.from_python('  this  ')
551         '  this  '
552         >>> PlainText(accept_python=False).from_python('  this  ')
553         Traceback (most recent call last):
554           ...
555         Invalid: Enter only letters, numbers, or _ (underscore)
556         >>> PlainText(strip=True).to_python('  this  ')
557         'this'
558         >>> PlainText(strip=True).from_python('  this  ')
559         'this'
560     """
561
562     regex = r"^[a-zA-Z_\-0-9]*$"
563
564     messages = dict(
565         invalid=_('Enter only letters, numbers, or _ (underscore)'))
566
567
568 class OneOf(FancyValidator):
569     """
570     Tests that the value is one of the members of a given list.
571
572     If ``testValueList=True``, then if the input value is a list or
573     tuple, all the members of the sequence will be checked (i.e., the
574     input must be a subset of the allowed values).
575
576     Use ``hideList=True`` to keep the list of valid values out of the
577     error message in exceptions.
578
579     Examples::
580
581         >>> oneof = OneOf([1, 2, 3])
582         >>> oneof.to_python(1)
583         1
584         >>> oneof.to_python(4)
585         Traceback (most recent call last):
586           ...
587         Invalid: Value must be one of: 1; 2; 3 (not 4)
588         >>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
589         [2, 3, [1, 2, 3]]
590         >>> oneof.to_python([2, 3, [1, 2, 3]])
591         Traceback (most recent call last):
592           ...
593         Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
594     """
595
596     list = None
597     testValueList = False
598     hideList = False
599
600     __unpackargs__ = ('list',)
601
602     messages = dict(
603         invalid=_('Invalid value'),
604         notIn=_('Value must be one of: %(items)s (not %(value)r)'))
605
606     def _validate_python(self, value, state):
607         if self.testValueList and isinstance(value, (list, tuple)):
608             for v in value:
609                 self._validate_python(v, state)
610         else:
611             if not value in self.list:
612                 if self.hideList:
613                     raise Invalid(self.message('invalid', state), value, state)
614                 else:
615                     try:
616                         items = '; '.join(map(str, self.list))
617                     except UnicodeError:
618                         items = '; '.join(map(str, self.list))
619                     raise Invalid(
620                         self.message('notIn', state,
621                             items=items, value=value), value, state)
622
623     @property
624     def accept_iterator(self):
625         return self.testValueList
626
627
628 class DictConverter(FancyValidator):
629     """
630     Converts values based on a dictionary which has values as keys for
631     the resultant values.
632
633     If ``allowNull`` is passed, it will not balk if a false value
634     (e.g., '' or None) is given (it will return None in these cases).
635
636     to_python takes keys and gives values, from_python takes values and
637     gives keys.
638
639     If you give hideDict=True, then the contents of the dictionary
640     will not show up in error messages.
641
642     Examples::
643
644         >>> dc = DictConverter({1: 'one', 2: 'two'})
645         >>> dc.to_python(1)
646         'one'
647         >>> dc.from_python('one')
648         1
649         >>> dc.to_python(3)
650         Traceback (most recent call last):
651             ....
652         Invalid: Enter a value from: 1; 2
653         >>> dc2 = dc(hideDict=True)
654         >>> dc2.hideDict
655         True
656         >>> dc2.dict
657         {1: 'one', 2: 'two'}
658         >>> dc2.to_python(3)
659         Traceback (most recent call last):
660             ....
661         Invalid: Choose something
662         >>> dc.from_python('three')
663         Traceback (most recent call last):
664             ....
665         Invalid: Nothing in my dictionary goes by the value 'three'.  Choose one of: 'one'; 'two'
666     """
667
668     messages = dict(
669         keyNotFound=_('Choose something'),
670         chooseKey=_('Enter a value from: %(items)s'),
671         valueNotFound=_('That value is not known'),
672         chooseValue=_('Nothing in my dictionary goes by the value %(value)s.'
673             '  Choose one of: %(items)s'))
674
675     dict = None
676     hideDict = False
677
678     __unpackargs__ = ('dict',)
679
680     def _convert_to_python(self, value, state):
681         try:
682             return self.dict[value]
683         except KeyError:
684             if self.hideDict:
685                 raise Invalid(self.message('keyNotFound', state), value, state)
686             else:
687                 items = sorted(self.dict)
688                 items = '; '.join(map(repr, items))
689                 raise Invalid(self.message('chooseKey',
690                     state, items=items), value, state)
691
692     def _convert_from_python(self, value, state):
693         for k, v in self.dict.items():
694             if value == v:
695                 return k
696         if self.hideDict:
697             raise Invalid(self.message('valueNotFound', state), value, state)
698         else:
699             items = '; '.join(map(repr, iter(self.dict.values())))
700             raise Invalid(
701                 self.message('chooseValue', state,
702                     value=repr(value), items=items), value, state)
703
704
705 class IndexListConverter(FancyValidator):
706     """
707     Converts a index (which may be a string like '2') to the value in
708     the given list.
709
710     Examples::
711
712         >>> index = IndexListConverter(['zero', 'one', 'two'])
713         >>> index.to_python(0)
714         'zero'
715         >>> index.from_python('zero')
716         0
717         >>> index.to_python('1')
718         'one'
719         >>> index.to_python(5)
720         Traceback (most recent call last):
721         Invalid: Index out of range
722         >>> index(not_empty=True).to_python(None)
723         Traceback (most recent call last):
724         Invalid: Please enter a value
725         >>> index.from_python('five')
726         Traceback (most recent call last):
727         Invalid: Item 'five' was not found in the list
728     """
729
730     list = None
731
732     __unpackargs__ = ('list',)
733
734     messages = dict(
735         integer=_('Must be an integer index'),
736         outOfRange=_('Index out of range'),
737         notFound=_('Item %(value)s was not found in the list'))
738
739     def _convert_to_python(self, value, state):
740         try:
741             value = int(value)
742         except (ValueError, TypeError):
743             raise Invalid(self.message('integer', state), value, state)
744         try:
745             return self.list[value]
746         except IndexError:
747             raise Invalid(self.message('outOfRange', state), value, state)
748
749     def _convert_from_python(self, value, state):
750         for i, v in enumerate(self.list):
751             if v == value:
752                 return i
753         raise Invalid(
754             self.message('notFound', state, value=repr(value)), value, state)
755
756
757 class DateValidator(FancyValidator):
758     """
759     Validates that a date is within the given range.  Be sure to call
760     DateConverter first if you aren't expecting mxDateTime input.
761
762     ``earliest_date`` and ``latest_date`` may be functions; if so,
763     they will be called each time before validating.
764
765     ``after_now`` means a time after the current timestamp; note that
766     just a few milliseconds before now is invalid!  ``today_or_after``
767     is more permissive, and ignores hours and minutes.
768
769     Examples::
770
771         >>> from datetime import datetime, timedelta
772         >>> d = DateValidator(earliest_date=datetime(2003, 1, 1))
773         >>> d.to_python(datetime(2004, 1, 1))
774         datetime.datetime(2004, 1, 1, 0, 0)
775         >>> d.to_python(datetime(2002, 1, 1))
776         Traceback (most recent call last):
777             ...
778         Invalid: Date must be after Wednesday, 01 January 2003
779         >>> d.to_python(datetime(2003, 1, 1))
780         datetime.datetime(2003, 1, 1, 0, 0)
781         >>> d = DateValidator(after_now=True)
782         >>> now = datetime.now()
783         >>> d.to_python(now+timedelta(seconds=5)) == now+timedelta(seconds=5)
784         True
785         >>> d.to_python(now-timedelta(days=1))
786         Traceback (most recent call last):
787             ...
788         Invalid: The date must be sometime in the future
789         >>> d.to_python(now+timedelta(days=1)) > now
790         True
791         >>> d = DateValidator(today_or_after=True)
792         >>> d.to_python(now) == now
793         True
794
795     """
796
797     earliest_date = None
798     latest_date = None
799     after_now = False
800     # Like after_now, but just after this morning:
801     today_or_after = False
802     # Use None or 'datetime' for the datetime module in the standard lib,
803     # or 'mxDateTime' to force the mxDateTime module
804     datetime_module = None
805
806     messages = dict(
807         after=_('Date must be after %(date)s'),
808         before=_('Date must be before %(date)s'),
809         # Double %'s, because this will be substituted twice:
810         date_format=_('%%A, %%d %%B %%Y'),
811         future=_('The date must be sometime in the future'))
812
813     def _validate_python(self, value, state):
814         date_format = self.message('date_format', state)
815         if (str is not str  # Python 2
816                 and isinstance(date_format, str)):
817             # strftime uses the locale encoding, not Unicode
818             encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
819             date_format = date_format.encode(encoding)
820         else:
821             encoding = None
822         if self.earliest_date:
823             if callable(self.earliest_date):
824                 earliest_date = self.earliest_date()
825             else:
826                 earliest_date = self.earliest_date
827             if value < earliest_date:
828                 date_formatted = earliest_date.strftime(date_format)
829                 if encoding:
830                     date_formatted = date_formatted.decode(encoding)
831                 raise Invalid(
832                     self.message('after', state, date=date_formatted),
833                     value, state)
834         if self.latest_date:
835             if callable(self.latest_date):
836                 latest_date = self.latest_date()
837             else:
838                 latest_date = self.latest_date
839             if value > latest_date:
840                 date_formatted = latest_date.strftime(date_format)
841                 if encoding:
842                     date_formatted = date_formatted.decode(encoding)
843                 raise Invalid(
844                     self.message('before', state, date=date_formatted),
845                     value, state)
846         if self.after_now:
847             dt_mod = import_datetime(self.datetime_module)
848             now = datetime_now(dt_mod)
849             if value < now:
850                 date_formatted = now.strftime(date_format)
851                 if encoding:
852                     date_formatted = date_formatted.decode(encoding)
853                 raise Invalid(
854                     self.message('future', state, date=date_formatted),
855                     value, state)
856         if self.today_or_after:
857             dt_mod = import_datetime(self.datetime_module)
858             now = datetime_now(dt_mod)
859             today = datetime_makedate(dt_mod,
860                                       now.year, now.month, now.day)
861             value_as_date = datetime_makedate(
862                 dt_mod, value.year, value.month, value.day)
863             if value_as_date < today:
864                 date_formatted = now.strftime(date_format)
865                 if encoding:
866                     date_formatted = date_formatted.decode(encoding)
867                 raise Invalid(
868                     self.message('future', state, date=date_formatted),
869                     value, state)
870
871
872 class Bool(FancyValidator):
873     """
874     Always Valid, returns True or False based on the value and the
875     existance of the value.
876
877     If you want to convert strings like ``'true'`` to booleans, then
878     use ``StringBool``.
879
880     Examples::
881
882         >>> Bool.to_python(0)
883         False
884         >>> Bool.to_python(1)
885         True
886         >>> Bool.to_python('')
887         False
888         >>> Bool.to_python(None)
889         False
890     """
891
892     if_missing = False
893
894     def _convert_to_python(self, value, state):
895         return bool(value)
896
897     _convert_from_python = _convert_to_python
898
899     def empty_value(self, value):
900         return False
901
902
903 class RangeValidator(FancyValidator):
904     """This is an abstract base class for Int and Number.
905
906     It verifies that a value is within range.  It accepts min and max
907     values in the constructor.
908
909     (Since this is an abstract base class, the tests are in Int and Number.)
910
911     """
912
913     messages = dict(
914         tooLow=_('Please enter a number that is %(min)s or greater'),
915         tooHigh=_('Please enter a number that is %(max)s or smaller'))
916
917     min = None
918     max = None
919
920     def _validate_python(self, value, state):
921         if self.min is not None:
922             if value < self.min:
923                 msg = self.message('tooLow', state, min=self.min)
924                 raise Invalid(msg, value, state)
925         if self.max is not None:
926             if value > self.max:
927                 msg = self.message('tooHigh', state, max=self.max)
928                 raise Invalid(msg, value, state)
929
930
931 class Int(RangeValidator):
932     """Convert a value to an integer.
933
934     Example::
935
936         >>> Int.to_python('10')
937         10
938         >>> Int.to_python('ten')
939         Traceback (most recent call last):
940             ...
941         Invalid: Please enter an integer value
942         >>> Int(min=5).to_python('6')
943         6
944         >>> Int(max=10).to_python('11')
945         Traceback (most recent call last):
946             ...
947         Invalid: Please enter a number that is 10 or smaller
948
949     """
950
951     messages = dict(
952         integer=_('Please enter an integer value'))
953
954     def _convert_to_python(self, value, state):
955         try:
956             return int(value)
957         except (ValueError, TypeError):
958             raise Invalid(self.message('integer', state), value, state)
959
960     _convert_from_python = _convert_to_python
961
962
963 class Number(RangeValidator):
964     """Convert a value to a float or integer.
965
966     Tries to convert it to an integer if no information is lost.
967
968     Example::
969
970         >>> Number.to_python('10')
971         10
972         >>> Number.to_python('10.5')
973         10.5
974         >>> Number.to_python('ten')
975         Traceback (most recent call last):
976             ...
977         Invalid: Please enter a number
978         >>> Number.to_python([1.2])
979         Traceback (most recent call last):
980             ...
981         Invalid: Please enter a number
982         >>> Number(min=5).to_python('6.5')
983         6.5
984         >>> Number(max=10.5).to_python('11.5')
985         Traceback (most recent call last):
986             ...
987         Invalid: Please enter a number that is 10.5 or smaller
988
989     """
990
991     messages = dict(
992         number=_('Please enter a number'))
993
994     def _convert_to_python(self, value, state):
995         try:
996             value = float(value)
997             try:
998                 int_value = int(value)
999             except OverflowError:
1000                 int_value = None
1001             if value == int_value:
1002                 return int_value
1003             return value
1004         except (ValueError, TypeError):
1005             raise Invalid(self.message('number', state), value, state)
1006
1007
1008 class ByteString(FancyValidator):
1009     """Convert to byte string, treating empty things as the empty string.
1010
1011     Under Python 2.x you can also use the alias `String` for this validator.
1012
1013     Also takes a `max` and `min` argument, and the string length must fall
1014     in that range.
1015
1016     Also you may give an `encoding` argument, which will encode any unicode
1017     that is found.  Lists and tuples are joined with `list_joiner`
1018     (default ``', '``) in ``from_python``.
1019
1020     ::
1021
1022         >>> ByteString(min=2).to_python('a')
1023         Traceback (most recent call last):
1024             ...
1025         Invalid: Enter a value 2 characters long or more
1026         >>> ByteString(max=10).to_python('xxxxxxxxxxx')
1027         Traceback (most recent call last):
1028             ...
1029         Invalid: Enter a value not more than 10 characters long
1030         >>> ByteString().from_python(None)
1031         ''
1032         >>> ByteString().from_python([])
1033         ''
1034         >>> ByteString().to_python(None)
1035         ''
1036         >>> ByteString(min=3).to_python(None)
1037         Traceback (most recent call last):
1038             ...
1039         Invalid: Please enter a value
1040         >>> ByteString(min=1).to_python('')
1041         Traceback (most recent call last):
1042             ...
1043         Invalid: Please enter a value
1044
1045     """
1046
1047     min = None
1048     max = None
1049     not_empty = None
1050     encoding = None
1051     list_joiner = ', '
1052
1053     messages = dict(
1054         tooLong=_('Enter a value not more than %(max)i characters long'),
1055         tooShort=_('Enter a value %(min)i characters long or more'))
1056
1057     def __initargs__(self, new_attrs):
1058         if self.not_empty is None and self.min:
1059             self.not_empty = True
1060
1061     def _convert_to_python(self, value, state):
1062         if value is None:
1063             value = ''
1064         elif not isinstance(value, str):
1065             try:
1066                 value = bytes(value)
1067             except UnicodeEncodeError:
1068                 value = str(value)
1069         if self.encoding is not None and isinstance(value, str):
1070             value = value.encode(self.encoding)
1071         return value
1072
1073     def _convert_from_python(self, value, state):
1074         if value is None:
1075             value = ''
1076         elif not isinstance(value, str):
1077             if isinstance(value, (list, tuple)):
1078                 value = self.list_joiner.join(
1079                     self._convert_from_python(v, state) for v in value)
1080             try:
1081                 value = str(value)
1082             except UnicodeEncodeError:
1083                 value = str(value)
1084         if self.encoding is not None and isinstance(value, str):
1085             value = value.encode(self.encoding)
1086         if self.strip:
1087             value = value.strip()
1088         return value
1089
1090     def _validate_other(self, value, state):
1091         if self.max is None and self.min is None:
1092             return
1093         if value is None:
1094             value = ''
1095         elif not isinstance(value, str):
1096             try:
1097                 value = str(value)
1098             except UnicodeEncodeError:
1099                 value = str(value)
1100         if self.max is not None and len(value) > self.max:
1101             raise Invalid(
1102                 self.message('tooLong', state, max=self.max), value, state)
1103         if self.min is not None and len(value) < self.min:
1104             raise Invalid(
1105                 self.message('tooShort', state, min=self.min), value, state)
1106
1107     def empty_value(self, value):
1108         return ''
1109
1110
1111 class UnicodeString(ByteString):
1112     """Convert things to unicode string.
1113
1114     This is implemented as a specialization of the ByteString class.
1115
1116     Under Python 3.x you can also use the alias `String` for this validator.
1117
1118     In addition to the String arguments, an encoding argument is also
1119     accepted. By default the encoding will be utf-8. You can overwrite
1120     this using the encoding parameter. You can also set inputEncoding
1121     and outputEncoding differently. An inputEncoding of None means
1122     "do not decode", an outputEncoding of None means "do not encode".
1123
1124     All converted strings are returned as Unicode strings.
1125
1126     ::
1127
1128         >>> UnicodeString().to_python(None)
1129         u''
1130         >>> UnicodeString().to_python([])
1131         u''
1132         >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
1133         u'Ni Ni Ni'
1134
1135     """
1136     encoding = 'utf-8'
1137     inputEncoding = NoDefault
1138     outputEncoding = NoDefault
1139     messages = dict(
1140         badEncoding=_('Invalid data or incorrect encoding'))
1141
1142     def __init__(self, **kw):
1143         ByteString.__init__(self, **kw)
1144         if self.inputEncoding is NoDefault:
1145             self.inputEncoding = self.encoding
1146         if self.outputEncoding is NoDefault:
1147             self.outputEncoding = self.encoding
1148
1149     def _convert_to_python(self, value, state):
1150         if not value:
1151             return ''
1152         if isinstance(value, str):
1153             return value
1154         if not isinstance(value, str):
1155             if hasattr(value, '__unicode__'):
1156                 value = str(value)
1157                 return value
1158             if not (str is str  # Python 3
1159                     and isinstance(value, bytes) and self.inputEncoding):
1160                 value = str(value)
1161         if self.inputEncoding and not isinstance(value, str):
1162             try:
1163                 value = str(value, self.inputEncoding)
1164             except UnicodeDecodeError:
1165                 raise Invalid(self.message('badEncoding', state), value, state)
1166             except TypeError:
1167                 raise Invalid(
1168                     self.message('badType', state,
1169                         type=type(value), value=value), value, state)
1170         return value
1171
1172     def _convert_from_python(self, value, state):
1173         if not isinstance(value, str):
1174             if hasattr(value, '__unicode__'):
1175                 value = str(value)
1176             else:
1177                 value = str(value)
1178         if self.outputEncoding and isinstance(value, str):
1179             value = value.encode(self.outputEncoding)
1180         return value
1181
1182     def empty_value(self, value):
1183         return ''
1184
1185
1186 # Provide proper alias for native strings
1187
1188 String = UnicodeString if str is str else ByteString
1189
1190
1191 class Set(FancyValidator):
1192     """
1193     This is for when you think you may return multiple values for a
1194     certain field.
1195
1196     This way the result will always be a list, even if there's only
1197     one result.  It's equivalent to ForEach(convert_to_list=True).
1198
1199     If you give ``use_set=True``, then it will return an actual
1200     ``set`` object.
1201
1202     ::
1203
1204        >>> Set.to_python(None)
1205        []
1206        >>> Set.to_python('this')
1207        ['this']
1208        >>> Set.to_python(('this', 'that'))
1209        ['this', 'that']
1210        >>> s = Set(use_set=True)
1211        >>> s.to_python(None)
1212        set([])
1213        >>> s.to_python('this')
1214        set(['this'])
1215        >>> s.to_python(('this',))
1216        set(['this'])
1217     """
1218
1219     use_set = False
1220
1221     if_missing = ()
1222     accept_iterator = True
1223
1224     def _convert_to_python(self, value, state):
1225         if self.use_set:
1226             if isinstance(value, set):
1227                 return value
1228             elif isinstance(value, (list, tuple)):
1229                 return set(value)
1230             elif value is None:
1231                 return set()
1232             else:
1233                 return set([value])
1234         else:
1235             if isinstance(value, list):
1236                 return value
1237             elif isinstance(value, set):
1238                 return list(value)
1239             elif isinstance(value, tuple):
1240                 return list(value)
1241             elif value is None:
1242                 return []
1243             else:
1244                 return [value]
1245
1246     def empty_value(self, value):
1247         if self.use_set:
1248             return set()
1249         else:
1250             return []
1251
1252
1253 class Email(FancyValidator):
1254     r"""
1255     Validate an email address.
1256
1257     If you pass ``resolve_domain=True``, then it will try to resolve
1258     the domain name to make sure it's valid.  This takes longer, of
1259     course. You must have the `dnspython <http://www.dnspython.org/>`__ modules
1260     installed to look up DNS (MX and A) records.
1261
1262     ::
1263
1264         >>> e = Email()
1265         >>> e.to_python(' test@foo.com ')
1266         'test@foo.com'
1267         >>> e.to_python('test')
1268         Traceback (most recent call last):
1269             ...
1270         Invalid: An email address must contain a single @
1271         >>> e.to_python('test@foobar')
1272         Traceback (most recent call last):
1273             ...
1274         Invalid: The domain portion of the email address is invalid (the portion after the @: foobar)
1275         >>> e.to_python('test@foobar.com.5')
1276         Traceback (most recent call last):
1277             ...
1278         Invalid: The domain portion of the email address is invalid (the portion after the @: foobar.com.5)
1279         >>> e.to_python('test@foo..bar.com')
1280         Traceback (most recent call last):
1281             ...
1282         Invalid: The domain portion of the email address is invalid (the portion after the @: foo..bar.com)
1283         >>> e.to_python('test@.foo.bar.com')
1284         Traceback (most recent call last):
1285             ...
1286         Invalid: The domain portion of the email address is invalid (the portion after the @: .foo.bar.com)
1287         >>> e.to_python('nobody@xn--m7r7ml7t24h.com')
1288         'nobody@xn--m7r7ml7t24h.com'
1289         >>> e.to_python('o*reilly@test.com')
1290         'o*reilly@test.com'
1291         >>> e = Email(resolve_domain=True)
1292         >>> e.resolve_domain
1293         True
1294         >>> e.to_python('doesnotexist@colorstudy.com')
1295         'doesnotexist@colorstudy.com'
1296         >>> e.to_python('test@nyu.edu')
1297         'test@nyu.edu'
1298         >>> # NOTE: If you do not have dnspython installed this example won't work:
1299         >>> e.to_python('test@thisdomaindoesnotexistithinkforsure.com')
1300         Traceback (most recent call last):
1301             ...
1302         Invalid: The domain of the email address does not exist (the portion after the @: thisdomaindoesnotexistithinkforsure.com)
1303         >>> e.to_python('test@google.com')
1304         u'test@google.com'
1305         >>> e = Email(not_empty=False)
1306         >>> e.to_python('')
1307
1308     """
1309
1310     resolve_domain = False
1311     resolve_timeout = 10  # timeout in seconds when resolving domains
1312
1313     usernameRE = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
1314     domainRE = re.compile(r'''
1315         ^(?:[a-z0-9][a-z0-9\-]{,62}\.)+        # subdomain
1316         (?:[a-z]{2,63}|xn--[a-z0-9\-]{2,59})$  # top level domain
1317     ''', re.I | re.VERBOSE)
1318
1319     messages = dict(
1320         empty=_('Please enter an email address'),
1321         noAt=_('An email address must contain a single @'),
1322         badUsername=_('The username portion of the email address is invalid'
1323             ' (the portion before the @: %(username)s)'),
1324         socketError=_('An error occured when trying to connect to the server:'
1325             ' %(error)s'),
1326         badDomain=_('The domain portion of the email address is invalid'
1327             ' (the portion after the @: %(domain)s)'),
1328         domainDoesNotExist=_('The domain of the email address does not exist'
1329             ' (the portion after the @: %(domain)s)'))
1330
1331     def __init__(self, *args, **kw):
1332         FancyValidator.__init__(self, *args, **kw)
1333         if self.resolve_domain:
1334             if not have_dns:
1335                 warnings.warn(
1336                     "dnspython <http://www.dnspython.org/> is not installed on"
1337                     " your system (or the dns.resolver package cannot be found)."
1338                     " I cannot resolve domain names in addresses")
1339                 raise ImportError("no module named dns.resolver")
1340
1341     def _validate_python(self, value, state):
1342         if not value:
1343             raise Invalid(self.message('empty', state), value, state)
1344         value = value.strip()
1345         splitted = value.split('@', 1)
1346         try:
1347             username, domain = splitted
1348         except ValueError:
1349             raise Invalid(self.message('noAt', state), value, state)
1350         if not self.usernameRE.search(username):
1351             raise Invalid(
1352                 self.message('badUsername', state, username=username),
1353                 value, state)
1354         try:
1355             idna_domain = [idna.ToASCII(p) for p in domain.split('.')]
1356             if str is str:  # Python 3
1357                 idna_domain = [p.decode('ascii') for p in idna_domain]
1358             idna_domain = '.'.join(idna_domain)
1359         except UnicodeError:
1360             # UnicodeError: label empty or too long
1361             # This exception might happen if we have an invalid domain name part
1362             # (for example test@.foo.bar.com)
1363             raise Invalid(
1364                 self.message('badDomain', state, domain=domain),
1365                 value, state)
1366         if not self.domainRE.search(idna_domain):
1367             raise Invalid(
1368                 self.message('badDomain', state, domain=domain),
1369                 value, state)
1370         if self.resolve_domain:
1371             assert have_dns, "dnspython should be available"
1372             global socket
1373             if socket is None:
1374                 import socket
1375             try:
1376                 try:
1377                     dns.resolver.query(domain, 'MX')
1378                 except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
1379                     try:
1380                         dns.resolver.query(domain, 'A')
1381                     except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
1382                         raise Invalid(
1383                             self.message('domainDoesNotExist',
1384                                 state, domain=domain), value, state)
1385             except (socket.error, dns.exception.DNSException) as e:
1386                 raise Invalid(
1387                     self.message('socketError', state, error=e), value, state)
1388
1389     def _convert_to_python(self, value, state):
1390         return value.strip()
1391
1392
1393 class URL(FancyValidator):
1394     """
1395     Validate a URL, either http://... or https://.  If check_exists
1396     is true, then we'll actually make a request for the page.
1397
1398     If add_http is true, then if no scheme is present we'll add
1399     http://
1400
1401     ::
1402
1403         >>> u = URL(add_http=True)
1404         >>> u.to_python('foo.com')
1405         'http://foo.com'
1406         >>> u.to_python('http://hahaha.ha/bar.html')
1407         'http://hahaha.ha/bar.html'
1408         >>> u.to_python('http://xn--m7r7ml7t24h.com')
1409         'http://xn--m7r7ml7t24h.com'
1410         >>> u.to_python('http://xn--c1aay4a.xn--p1ai')
1411         'http://xn--c1aay4a.xn--p1ai'
1412         >>> u.to_python('http://foo.com/test?bar=baz&fleem=morx')
1413         'http://foo.com/test?bar=baz&fleem=morx'
1414         >>> u.to_python('http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest')
1415         'http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest'
1416         >>> u.to_python('http://foo.com:8000/test.html')
1417         'http://foo.com:8000/test.html'
1418         >>> u.to_python('http://foo.com/something\\nelse')
1419         Traceback (most recent call last):
1420             ...
1421         Invalid: That is not a valid URL
1422         >>> u.to_python('https://test.com')
1423         'https://test.com'
1424         >>> u.to_python('http://test')
1425         Traceback (most recent call last):
1426             ...
1427         Invalid: You must provide a full domain name (like test.com)
1428         >>> u.to_python('http://test..com')
1429         Traceback (most recent call last):
1430             ...
1431         Invalid: That is not a valid URL
1432         >>> u = URL(add_http=False, check_exists=True)
1433         >>> u.to_python('http://google.com')
1434         'http://google.com'
1435         >>> u.to_python('google.com')
1436         Traceback (most recent call last):
1437             ...
1438         Invalid: You must start your URL with http://, https://, etc
1439         >>> u.to_python('http://www.formencode.org/does/not/exist/page.html')
1440         Traceback (most recent call last):
1441             ...
1442         Invalid: The server responded that the page could not be found
1443         >>> u.to_python('http://this.domain.does.not.exist.example.org/test.html')
1444         ... # doctest: +ELLIPSIS
1445         Traceback (most recent call last):
1446             ...
1447         Invalid: An error occured when trying to connect to the server: ...
1448
1449     If you want to allow addresses without a TLD (e.g., ``localhost``) you can do::
1450
1451         >>> URL(require_tld=False).to_python('http://localhost')
1452         'http://localhost'
1453
1454     By default, internationalized domain names (IDNA) in Unicode will be
1455     accepted and encoded to ASCII using Punycode (as described in RFC 3490).
1456     You may set allow_idna to False to change this behavior::
1457
1458         >>> URL(allow_idna=True).to_python(
1459         ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
1460         'http://xn--c1aay4a.xn--p1ai'
1461         >>> URL(allow_idna=True, add_http=True).to_python(
1462         ... '\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
1463         'http://xn--c1aay4a.xn--p1ai'
1464         >>> URL(allow_idna=False).to_python(
1465         ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
1466         Traceback (most recent call last):
1467         ...
1468         Invalid: That is not a valid URL
1469
1470     """
1471
1472     add_http = True
1473     allow_idna = True
1474     check_exists = False
1475     require_tld = True
1476
1477     url_re = re.compile(r'''
1478         ^(http|https)://
1479         (?:[%:\w]*@)?                              # authenticator
1480         (?:                                        # ip or domain
1481         (?P<ip>(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|
1482         (?P<domain>[a-z0-9][a-z0-9\-]{,62}\.)*     # subdomain
1483         (?P<tld>[a-z]{2,63}|xn--[a-z0-9\-]{2,59})  # top level domain
1484         )
1485         (?::[0-9]{1,5})?                           # port
1486         # files/delims/etc
1487         (?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
1488         $
1489     ''', re.I | re.VERBOSE)
1490
1491     scheme_re = re.compile(r'^[a-zA-Z]+:')
1492
1493     messages = dict(
1494         noScheme=_('You must start your URL with http://, https://, etc'),
1495         badURL=_('That is not a valid URL'),
1496         httpError=_('An error occurred when trying to access the URL:'
1497             ' %(error)s'),
1498         socketError=_('An error occured when trying to connect to the server:'
1499             ' %(error)s'),
1500         notFound=_('The server responded that the page could not be found'),
1501         status=_('The server responded with a bad status code (%(status)s)'),
1502         noTLD=_('You must provide a full domain name (like %(domain)s.com)'))
1503
1504     def _convert_to_python(self, value, state):
1505         value = value.strip()
1506         if self.add_http:
1507             if not self.scheme_re.search(value):
1508                 value = 'http://' + value
1509         if self.allow_idna:
1510             value = self._encode_idna(value)
1511         match = self.scheme_re.search(value)
1512         if not match:
1513             raise Invalid(self.message('noScheme', state), value, state)
1514         value = match.group(0).lower() + value[len(match.group(0)):]
1515         match = self.url_re.search(value)
1516         if not match:
1517             raise Invalid(self.message('badURL', state), value, state)
1518         if self.require_tld and not match.group('domain'):
1519             raise Invalid(
1520                 self.message('noTLD', state, domain=match.group('tld')),
1521                 value, state)
1522         if self.check_exists and value.startswith(('http://', 'https://')):
1523             self._check_url_exists(value, state)
1524         return value
1525
1526     def _encode_idna(self, url):
1527         global urlparse
1528         if urlparse is None:
1529             import urllib.parse
1530         try:
1531             scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
1532                 url)
1533         except ValueError:
1534             return url
1535         try:
1536             netloc = netloc.encode('idna')
1537             if str is str:  # Python 3
1538                 netloc = netloc.decode('ascii')
1539             return str(urllib.parse.urlunparse((scheme, netloc,
1540                 path, params, query, fragment)))
1541         except UnicodeError:
1542             return url
1543
1544     def _check_url_exists(self, url, state):
1545         global httplib, urlparse, socket
1546         if httplib is None:
1547             import http.client
1548         if urlparse is None:
1549             import urllib.parse
1550         if socket is None:
1551             import socket
1552         scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
1553             url, 'http')
1554         if scheme == 'https':
1555             ConnClass = http.client.HTTPSConnection
1556         else:
1557             ConnClass = http.client.HTTPConnection
1558         try:
1559             conn = ConnClass(netloc)
1560             if params:
1561                 path += ';' + params
1562             if query:
1563                 path += '?' + query
1564             conn.request('HEAD', path)
1565             res = conn.getresponse()
1566         except http.client.HTTPException as e:
1567             raise Invalid(
1568                 self.message('httpError', state, error=e), state, url)
1569         except socket.error as e:
1570             raise Invalid(
1571                 self.message('socketError', state, error=e), state, url)
1572         else:
1573             if res.status == 404:
1574                 raise Invalid(
1575                     self.message('notFound', state), state, url)
1576             if not 200 <= res.status < 500:
1577                 raise Invalid(
1578                     self.message('status', state, status=res.status),
1579                     state, url)
1580
1581
1582 class XRI(FancyValidator):
1583     r"""
1584     Validator for XRIs.
1585
1586     It supports both i-names and i-numbers, of the first version of the XRI
1587     standard.
1588
1589     ::
1590
1591         >>> inames = XRI(xri_type="i-name")
1592         >>> inames.to_python("   =John.Smith ")
1593         '=John.Smith'
1594         >>> inames.to_python("@Free.Software.Foundation")
1595         '@Free.Software.Foundation'
1596         >>> inames.to_python("Python.Software.Foundation")
1597         Traceback (most recent call last):
1598             ...
1599         Invalid: The type of i-name is not defined; it may be either individual or organizational
1600         >>> inames.to_python("http://example.org")
1601         Traceback (most recent call last):
1602             ...
1603         Invalid: The type of i-name is not defined; it may be either individual or organizational
1604         >>> inames.to_python("=!2C43.1A9F.B6F6.E8E6")
1605         Traceback (most recent call last):
1606             ...
1607         Invalid: "!2C43.1A9F.B6F6.E8E6" is an invalid i-name
1608         >>> iname_with_schema = XRI(True, xri_type="i-name")
1609         >>> iname_with_schema.to_python("=Richard.Stallman")
1610         'xri://=Richard.Stallman'
1611         >>> inames.to_python("=John Smith")
1612         Traceback (most recent call last):
1613             ...
1614         Invalid: "John Smith" is an invalid i-name
1615         >>> inumbers = XRI(xri_type="i-number")
1616         >>> inumbers.to_python("!!1000!de21.4536.2cb2.8074")
1617         '!!1000!de21.4536.2cb2.8074'
1618         >>> inumbers.to_python("@!1000.9554.fabd.129c!2847.df3c")
1619         '@!1000.9554.fabd.129c!2847.df3c'
1620
1621     """
1622
1623     iname_valid_pattern = re.compile(r"""
1624     ^
1625     [\w]+                  # A global alphanumeric i-name
1626     (\.[\w]+)*             # An i-name with dots
1627     (\*[\w]+(\.[\w]+)*)*   # A community i-name
1628     $
1629     """, re.VERBOSE | re.UNICODE)
1630
1631     iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
1632     """@cvar: These characters must not be at the beggining of the i-name"""
1633
1634     inumber_pattern = re.compile(r"""
1635     ^
1636     (
1637     [=@]!       # It's a personal or organization i-number
1638     |
1639     !!          # It's a network i-number
1640     )
1641     [\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3}       # A global i-number
1642     (![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})*   # Zero or more sub i-numbers
1643     $
1644     """, re.VERBOSE | re.IGNORECASE)
1645
1646     messages = dict(
1647         noType=_('The type of i-name is not defined;'
1648             ' it may be either individual or organizational'),
1649         repeatedChar=_('Dots and dashes may not be repeated consecutively'),
1650         badIname=_('"%(iname)s" is an invalid i-name'),
1651         badInameStart=_('i-names may not start with numbers'
1652             ' nor punctuation marks'),
1653         badInumber=_('"%(inumber)s" is an invalid i-number'),
1654         badType=_('The XRI must be a string (not a %(type)s: %(value)r)'),
1655         badXri=_('"%(xri_type)s" is not a valid type of XRI'))
1656
1657     def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
1658         """Create an XRI validator.
1659
1660         @param add_xri: Should the schema be added if not present?
1661             Officially it's optional.
1662         @type add_xri: C{bool}
1663         @param xri_type: What type of XRI should be validated?
1664             Possible values: C{i-name} or C{i-number}.
1665         @type xri_type: C{str}
1666
1667         """
1668         self.add_xri = add_xri
1669         assert xri_type in ('i-name', 'i-number'), (
1670             'xri_type must be "i-name" or "i-number"')
1671         self.xri_type = xri_type
1672         super(XRI, self).__init__(**kwargs)
1673
1674     def _convert_to_python(self, value, state):
1675         """Prepend the 'xri://' schema if needed and remove trailing spaces"""
1676         value = value.strip()
1677         if self.add_xri and not value.startswith('xri://'):
1678             value = 'xri://' + value
1679         return value
1680
1681     def _validate_python(self, value, state=None):
1682         """Validate an XRI
1683
1684         @raise Invalid: If at least one of the following conditions in met:
1685             - C{value} is not a string.
1686             - The XRI is not a personal, organizational or network one.
1687             - The relevant validator (i-name or i-number) considers the XRI
1688                 is not valid.
1689
1690         """
1691         if not isinstance(value, str):
1692             raise Invalid(
1693                 self.message('badType', state,
1694                     type=str(type(value)), value=value), value, state)
1695
1696         # Let's remove the schema, if any
1697         if value.startswith('xri://'):
1698             value = value[6:]
1699
1700         if not value[0] in ('@', '=') and not (
1701                 self.xri_type == 'i-number' and value[0] == '!'):
1702             raise Invalid(self.message('noType', state), value, state)
1703
1704         if self.xri_type == 'i-name':
1705             self._validate_iname(value, state)
1706         else:
1707             self._validate_inumber(value, state)
1708
1709     def _validate_iname(self, iname, state):
1710         """Validate an i-name"""
1711         # The type is not required here:
1712         iname = iname[1:]
1713         if '..' in iname or '--' in iname:
1714             raise Invalid(self.message('repeatedChar', state), iname, state)
1715         if self.iname_invalid_start.match(iname):
1716             raise Invalid(self.message('badInameStart', state), iname, state)
1717         if not self.iname_valid_pattern.match(iname) or '_' in iname:
1718             raise Invalid(
1719                 self.message('badIname', state, iname=iname), iname, state)
1720
1721     def _validate_inumber(self, inumber, state):
1722         """Validate an i-number"""
1723         if not self.__class__.inumber_pattern.match(inumber):
1724             raise Invalid(
1725                 self.message('badInumber', state,
1726                     inumber=inumber, value=inumber), inumber, state)
1727
1728
1729 class OpenId(FancyValidator):
1730     r"""
1731     OpenId validator.
1732
1733     ::
1734         >>> v = OpenId(add_schema=True)
1735         >>> v.to_python(' example.net ')
1736         'http://example.net'
1737         >>> v.to_python('@TurboGears')
1738         'xri://@TurboGears'
1739         >>> w = OpenId(add_schema=False)
1740         >>> w.to_python(' example.net ')
1741         Traceback (most recent call last):
1742         ...
1743         Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
1744         >>> w.to_python('!!1000')
1745         '!!1000'
1746         >>> w.to_python('look@me.com')
1747         Traceback (most recent call last):
1748         ...
1749         Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
1750
1751     """
1752
1753     messages = dict(
1754         badId=_('"%(id)s" is not a valid OpenId'
1755             ' (it is neither an URL nor an XRI)'))
1756
1757     def __init__(self, add_schema=False, **kwargs):
1758         """Create an OpenId validator.
1759
1760         @param add_schema: Should the schema be added if not present?
1761         @type add_schema: C{bool}
1762
1763         """
1764         self.url_validator = URL(add_http=add_schema)
1765         self.iname_validator = XRI(add_schema, xri_type="i-name")
1766         self.inumber_validator = XRI(add_schema, xri_type="i-number")
1767
1768     def _convert_to_python(self, value, state):
1769         value = value.strip()
1770         try:
1771             return self.url_validator.to_python(value, state)
1772         except Invalid:
1773             try:
1774                 return self.iname_validator.to_python(value, state)
1775             except Invalid:
1776                 try:
1777                     return self.inumber_validator.to_python(value, state)
1778                 except Invalid:
1779                     pass
1780         # It's not an OpenId!
1781         raise Invalid(self.message('badId', state, id=value), value, state)
1782
1783     def _validate_python(self, value, state):
1784         self._convert_to_python(value, state)
1785
1786
1787 def StateProvince(*kw, **kwargs):
1788     deprecation_warning("please use formencode.national.USStateProvince")
1789     from formencode.national import USStateProvince
1790     return USStateProvince(*kw, **kwargs)
1791
1792
1793 def PhoneNumber(*kw, **kwargs):
1794     deprecation_warning("please use formencode.national.USPhoneNumber")
1795     from formencode.national import USPhoneNumber
1796     return USPhoneNumber(*kw, **kwargs)
1797
1798
1799 def IPhoneNumberValidator(*kw, **kwargs):
1800     deprecation_warning(
1801         "please use formencode.national.InternationalPhoneNumber")
1802     from formencode.national import InternationalPhoneNumber
1803     return InternationalPhoneNumber(*kw, **kwargs)
1804
1805
1806 class FieldStorageUploadConverter(FancyValidator):
1807     """
1808     Handles cgi.FieldStorage instances that are file uploads.
1809
1810     This doesn't do any conversion, but it can detect empty upload
1811     fields (which appear like normal fields, but have no filename when
1812     no upload was given).
1813     """
1814     def _convert_to_python(self, value, state=None):
1815         if isinstance(value, cgi.FieldStorage):
1816             if getattr(value, 'filename', None):
1817                 return value
1818             raise Invalid('invalid', value, state)
1819         else:
1820             return value
1821
1822     def is_empty(self, value):
1823         if isinstance(value, cgi.FieldStorage):
1824             return not bool(getattr(value, 'filename', None))
1825         return FancyValidator.is_empty(self, value)
1826
1827
1828 class FileUploadKeeper(FancyValidator):
1829     """
1830     Takes two inputs (a dictionary with keys ``static`` and
1831     ``upload``) and converts them into one value on the Python side (a
1832     dictionary with ``filename`` and ``content`` keys).  The upload
1833     takes priority over the static value.  The filename may be None if
1834     it can't be discovered.
1835
1836     Handles uploads of both text and ``cgi.FieldStorage`` upload
1837     values.
1838
1839     This is basically for use when you have an upload field, and you
1840     want to keep the upload around even if the rest of the form
1841     submission fails.  When converting *back* to the form submission,
1842     there may be extra values ``'original_filename'`` and
1843     ``'original_content'``, which may want to use in your form to show
1844     the user you still have their content around.
1845
1846     To use this, make sure you are using variabledecode, then use
1847     something like::
1848
1849       <input type="file" name="myfield.upload">
1850       <input type="hidden" name="myfield.static">
1851
1852     Then in your scheme::
1853
1854       class MyScheme(Scheme):
1855           myfield = FileUploadKeeper()
1856
1857     Note that big file uploads mean big hidden fields, and lots of
1858     bytes passed back and forth in the case of an error.
1859     """
1860
1861     upload_key = 'upload'
1862     static_key = 'static'
1863
1864     def _convert_to_python(self, value, state):
1865         upload = value.get(self.upload_key)
1866         static = value.get(self.static_key, '').strip()
1867         filename = content = None
1868         if isinstance(upload, cgi.FieldStorage):
1869             filename = upload.filename
1870             content = upload.value
1871         elif isinstance(upload, str) and upload:
1872             filename = None
1873             # @@: Should this encode upload if it is unicode?
1874             content = upload
1875         if not content and static:
1876             filename, content = static.split(None, 1)
1877             filename = '' if filename == '-' else filename.decode('base64')
1878             content = content.decode('base64')
1879         return {'filename': filename, 'content': content}
1880
1881     def _convert_from_python(self, value, state):
1882         filename = value.get('filename', '')
1883         content = value.get('content', '')
1884         if filename or content:
1885             result = self.pack_content(filename, content)
1886             return {self.upload_key: '',
1887                     self.static_key: result,
1888                     'original_filename': filename,
1889                     'original_content': content}
1890         else:
1891             return {self.upload_key: '',
1892                     self.static_key: ''}
1893
1894     def pack_content(self, filename, content):
1895         enc_filename = self.base64encode(filename) or '-'
1896         enc_content = (content or '').encode('base64')
1897         result = '%s %s' % (enc_filename, enc_content)
1898         return result
1899
1900
1901 class DateConverter(FancyValidator):
1902     """
1903     Validates and converts a string date, like mm/yy, dd/mm/yy,
1904     dd-mm-yy, etc.  Using ``month_style`` you can support
1905     the three general styles ``mdy`` = ``us`` = ``mm/dd/yyyy``,
1906     ``dmy`` = ``euro`` = ``dd/mm/yyyy`` and
1907     ``ymd`` = ``iso`` = ``yyyy/mm/dd``.
1908
1909     Accepts English month names, also abbreviated.  Returns value as a
1910     datetime object (you can get mx.DateTime objects if you use
1911     ``datetime_module='mxDateTime'``).  Two year dates are assumed to
1912     be within 1950-2020, with dates from 21-49 being ambiguous and
1913     signaling an error.
1914
1915     Use accept_day=False if you just want a month/year (like for a
1916     credit card expiration date).
1917
1918     ::
1919
1920         >>> d = DateConverter()
1921         >>> d.to_python('12/3/09')
1922         datetime.date(2009, 12, 3)
1923         >>> d.to_python('12/3/2009')
1924         datetime.date(2009, 12, 3)
1925         >>> d.to_python('2/30/04')
1926         Traceback (most recent call last):
1927             ...
1928         Invalid: That month only has 29 days
1929         >>> d.to_python('13/2/05')
1930         Traceback (most recent call last):
1931             ...
1932         Invalid: Please enter a month from 1 to 12
1933         >>> d.to_python('1/1/200')
1934         Traceback (most recent call last):
1935             ...
1936         Invalid: Please enter a four-digit year after 1899
1937
1938     If you change ``month_style`` you can get European-style dates::
1939
1940         >>> d = DateConverter(month_style='dd/mm/yyyy')
1941         >>> date = d.to_python('12/3/09')
1942         >>> date
1943         datetime.date(2009, 3, 12)
1944         >>> d.from_python(date)
1945         '12/03/2009'
1946     """
1947
1948     # set to False if you want only month and year
1949     accept_day = True
1950     # allowed month styles: 'mdy' = 'us', 'dmy' = 'euro', 'ymd' = 'iso'
1951     # also allowed: 'mm/dd/yyyy', 'dd/mm/yyyy', 'yyyy/mm/dd'
1952     month_style = 'mdy'
1953     # preferred separator for reverse conversion: '/', '.' or '-'
1954     separator = '/'
1955
1956     # Use 'datetime' to force the Python datetime module, or
1957     # 'mxDateTime' to force the mxDateTime module (None means use
1958     # datetime, or if not present mxDateTime)
1959     datetime_module = None
1960
1961     _month_names = {
1962         'jan': 1, 'january': 1,
1963         'feb': 2, 'febuary': 2,
1964         'mar': 3, 'march': 3,
1965         'apr': 4, 'april': 4,
1966         'may': 5,
1967         'jun': 6, 'june': 6,
1968         'jul': 7, 'july': 7,
1969         'aug': 8, 'august': 8,
1970         'sep': 9, 'sept': 9, 'september': 9,
1971         'oct': 10, 'october': 10,
1972         'nov': 11, 'november': 11,
1973         'dec': 12, 'december': 12,
1974         }
1975
1976     _date_re = dict(
1977         dmy=re.compile(
1978             r'^\s*(\d\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
1979                 % '|'.join(_month_names), re.I),
1980         mdy=re.compile(
1981             r'^\s*(\d\d?|%s)[\-\./\\](\d\d?)[\-\./\\](\d\d\d?\d?)\s*$'
1982                 % '|'.join(_month_names), re.I),
1983         ymd=re.compile(
1984             r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d?)\s*$'
1985                 % '|'.join(_month_names), re.I),
1986         my=re.compile(
1987             r'^\s*(\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
1988                 % '|'.join(_month_names), re.I),
1989         ym=re.compile(
1990             r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)\s*$'
1991                 % '|'.join(_month_names), re.I))
1992
1993     _formats = dict(d='%d', m='%m', y='%Y')
1994
1995     _human_formats = dict(d=_('DD'), m=_('MM'), y=_('YYYY'))
1996
1997     # Feb. should be leap-year aware (but mxDateTime does catch that)
1998     _monthDays = {
1999         1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
2000         9: 30, 10: 31, 11: 30, 12: 31}
2001
2002     messages = dict(
2003         badFormat=_('Please enter the date in the form %(format)s'),
2004         monthRange=_('Please enter a month from 1 to 12'),
2005         invalidDay=_('Please enter a valid day'),
2006         dayRange=_('That month only has %(days)i days'),
2007         invalidDate=_('That is not a valid day (%(exception)s)'),
2008         unknownMonthName=_('Unknown month name: %(month)s'),
2009         invalidYear=_('Please enter a number for the year'),
2010         fourDigitYear=_('Please enter a four-digit year after 1899'),
2011         wrongFormat=_('Please enter the date in the form %(format)s'))
2012
2013     def __init__(self, *args, **kw):
2014         super(DateConverter, self).__init__(*args, **kw)
2015         month_style = (self.month_style or DateConverter.month_style).lower()
2016         accept_day = bool(self.accept_day)
2017         self.accept_day = self.accept_day
2018         if month_style in ('mdy',
2019                 'md', 'mm/dd/yyyy', 'mm/dd', 'us', 'american'):
2020             month_style = 'mdy'
2021         elif month_style in ('dmy',
2022                 'dm', 'dd/mm/yyyy', 'dd/mm', 'euro', 'european'):
2023             month_style = 'dmy'
2024         elif month_style in ('ymd',
2025                 'ym', 'yyyy/mm/dd', 'yyyy/mm', 'iso', 'china', 'chinese'):
2026             month_style = 'ymd'
2027         else:
2028             raise TypeError('Bad month_style: %r' % month_style)
2029         self.month_style = month_style
2030         separator = self.separator
2031         if not separator or separator == 'auto':
2032             separator = dict(mdy='/', dmy='.', ymd='-')[month_style]
2033         elif separator not in ('-', '.', '/', '\\'):
2034             raise TypeError('Bad separator: %r' % separator)
2035         self.separator = separator
2036         self.format = separator.join(self._formats[part]
2037             for part in month_style if part != 'd' or accept_day)
2038         self.human_format = separator.join(self._human_formats[part]
2039             for part in month_style if part != 'd' or accept_day)
2040
2041     def _convert_to_python(self, value, state):
2042         self.assert_string(value, state)
2043         month_style = self.month_style
2044         if not self.accept_day:
2045             month_style = 'ym' if month_style == 'ymd' else 'my'
2046         match = self._date_re[month_style].search(value)
2047         if not match:
2048             raise Invalid(
2049                 self.message('badFormat', state,
2050                     format=self.human_format), value, state)
2051         groups = match.groups()
2052         if self.accept_day:
2053             if month_style == 'mdy':
2054                 month, day, year = groups
2055             elif month_style == 'dmy':
2056                 day, month, year = groups
2057             else:
2058                 year, month, day = groups
2059             day = int(day)
2060             if not 1 <= day <= 31:
2061                 raise Invalid(self.message('invalidDay', state), value, state)
2062         else:
2063             day = 1
2064             if month_style == 'my':
2065                 month, year = groups
2066             else:
2067                 year, month = groups
2068         month = self.make_month(month, state)
2069         if not 1 <= month <= 12:
2070             raise Invalid(self.message('monthRange', state), value, state)
2071         if self._monthDays[month] < day:
2072             raise Invalid(
2073                 self.message('dayRange', state,
2074                     days=self._monthDays[month]), value, state)
2075         year = self.make_year(year, state)
2076         dt_mod = import_datetime(self.datetime_module)
2077         try:
2078             return datetime_makedate(dt_mod, year, month, day)
2079         except ValueError as v:
2080             raise Invalid(
2081                 self.message('invalidDate', state,
2082                     exception=str(v)), value, state)
2083
2084     def make_month(self, value, state):
2085         try:
2086             return int(value)
2087         except ValueError:
2088             try:
2089                 return self._month_names[value.lower().strip()]
2090             except KeyError:
2091                 raise Invalid(
2092                     self.message('unknownMonthName', state,
2093                         month=value), value, state)
2094
2095     def make_year(self, year, state):
2096         try:
2097             year = int(year)
2098         except ValueError:
2099             raise Invalid(self.message('invalidYear', state), year, state)
2100         if year <= 20:
2101             year += 2000
2102         elif 50 <= year < 100:
2103             year += 1900
2104         if 20 < year < 50 or 99 < year < 1900:
2105             raise Invalid(self.message('fourDigitYear', state), year, state)
2106         return year
2107
2108     def _convert_from_python(self, value, state):
2109         if self.if_empty is not NoDefault and not value:
2110             return ''
2111         return value.strftime(self.format)
2112
2113
2114 class TimeConverter(FancyValidator):
2115     """
2116     Converts times in the format HH:MM:SSampm to (h, m, s).
2117     Seconds are optional.
2118
2119     For ampm, set use_ampm = True.  For seconds, use_seconds = True.
2120     Use 'optional' for either of these to make them optional.
2121
2122     Examples::
2123
2124         >>> tim = TimeConverter()
2125         >>> tim.to_python('8:30')
2126         (8, 30)
2127         >>> tim.to_python('20:30')
2128         (20, 30)
2129         >>> tim.to_python('30:00')
2130         Traceback (most recent call last):
2131             ...
2132         Invalid: You must enter an hour in the range 0-23
2133         >>> tim.to_python('13:00pm')
2134         Traceback (most recent call last):
2135             ...
2136         Invalid: You must enter an hour in the range 1-12
2137         >>> tim.to_python('12:-1')
2138         Traceback (most recent call last):
2139             ...
2140         Invalid: You must enter a minute in the range 0-59
2141         >>> tim.to_python('12:02pm')
2142         (12, 2)
2143         >>> tim.to_python('12:02am')
2144         (0, 2)
2145         >>> tim.to_python('1:00PM')
2146         (13, 0)
2147         >>> tim.from_python((13, 0))
2148         '13:00:00'
2149         >>> tim2 = tim(use_ampm=True, use_seconds=False)
2150         >>> tim2.from_python((13, 0))
2151         '1:00pm'
2152         >>> tim2.from_python((0, 0))
2153         '12:00am'
2154         >>> tim2.from_python((12, 0))
2155         '12:00pm'
2156
2157     Examples with ``datetime.time``::
2158
2159         >>> v = TimeConverter(use_datetime=True)
2160         >>> a = v.to_python('18:00')
2161         >>> a
2162         datetime.time(18, 0)
2163         >>> b = v.to_python('30:00')
2164         Traceback (most recent call last):
2165             ...
2166         Invalid: You must enter an hour in the range 0-23
2167         >>> v2 = TimeConverter(prefer_ampm=True, use_datetime=True)
2168         >>> v2.from_python(a)
2169         '6:00:00pm'
2170         >>> v3 = TimeConverter(prefer_ampm=True,
2171         ...                    use_seconds=False, use_datetime=True)
2172         >>> a = v3.to_python('18:00')
2173         >>> a
2174         datetime.time(18, 0)
2175         >>> v3.from_python(a)
2176         '6:00pm'
2177         >>> a = v3.to_python('18:00:00')
2178         Traceback (most recent call last):
2179             ...
2180         Invalid: You may not enter seconds
2181     """
2182
2183     use_ampm = 'optional'
2184     prefer_ampm = False
2185     use_seconds = 'optional'
2186     use_datetime = False
2187     # This can be set to make it prefer mxDateTime:
2188     datetime_module = None
2189
2190     messages = dict(
2191         noAMPM=_('You must indicate AM or PM'),
2192         tooManyColon=_('There are too many :\'s'),
2193         noSeconds=_('You may not enter seconds'),
2194         secondsRequired=_('You must enter seconds'),
2195         minutesRequired=_('You must enter minutes (after a :)'),
2196         badNumber=_('The %(part)s value you gave is not a number: %(number)r'),
2197         badHour=_('You must enter an hour in the range %(range)s'),
2198         badMinute=_('You must enter a minute in the range 0-59'),
2199         badSecond=_('You must enter a second in the range 0-59'))
2200
2201     def _convert_to_python(self, value, state):
2202         result = self._to_python_tuple(value, state)
2203         if self.use_datetime:
2204             dt_mod = import_datetime(self.datetime_module)
2205             time_class = datetime_time(dt_mod)
2206             return time_class(*result)
2207         else:
2208             return result
2209
2210     def _to_python_tuple(self, value, state):
2211         time = value.strip()
2212         explicit_ampm = False
2213         if self.use_ampm:
2214             last_two = time[-2:].lower()
2215             if last_two not in ('am', 'pm'):
2216                 if self.use_ampm != 'optional':
2217                     raise Invalid(self.message('noAMPM', state), value, state)
2218                 offset = 0
2219             else:
2220                 explicit_ampm = True
2221                 offset = 12 if last_two == 'pm' else 0
2222                 time = time[:-2]
2223         else:
2224             offset = 0
2225         parts = time.split(':', 3)
2226         if len(parts) > 3:
2227             raise Invalid(self.message('tooManyColon', state), value, state)
2228         if len(parts) == 3 and not self.use_seconds:
2229             raise Invalid(self.message('noSeconds', state), value, state)
2230         if (len(parts) == 2
2231                 and self.use_seconds and self.use_seconds != 'optional'):
2232             raise Invalid(self.message('secondsRequired', state), value, state)
2233         if len(parts) == 1:
2234             raise Invalid(self.message('minutesRequired', state), value, state)
2235         try:
2236             hour = int(parts[0])
2237         except ValueError:
2238             raise Invalid(
2239                 self.message('badNumber', state,
2240                     number=parts[0], part='hour'), value, state)
2241         if explicit_ampm:
2242             if not 1 <= hour <= 12:
2243                 raise Invalid(
2244                     self.message('badHour', state,
2245                         number=hour, range='1-12'), value, state)
2246             if hour == 12 and offset == 12:
2247                 # 12pm == 12
2248                 pass
2249             elif hour == 12 and offset == 0:
2250                 # 12am == 0
2251                 hour = 0
2252             else:
2253                 hour += offset
2254         else:
2255             if not 0 <= hour < 24:
2256                 raise Invalid(
2257                     self.message('badHour', state,
2258                         number=hour, range='0-23'), value, state)
2259         try:
2260             minute = int(parts[1])
2261         except ValueError:
2262             raise Invalid(
2263                 self.message('badNumber', state,
2264                     number=parts[1], part='minute'), value, state)
2265         if not 0 <= minute < 60:
2266             raise Invalid(
2267                 self.message('badMinute', state, number=minute),
2268                 value, state)
2269         if len(parts) == 3:
2270             try:
2271                 second = int(parts[2])
2272             except ValueError:
2273                 raise Invalid(
2274                     self.message('badNumber', state,
2275                         number=parts[2], part='second'), value, state)
2276             if not 0 <= second < 60:
2277                 raise Invalid(
2278                     self.message('badSecond', state, number=second),
2279                     value, state)
2280         else:
2281             second = None
2282         if second is None:
2283             return (hour, minute)
2284         else:
2285             return (hour, minute, second)
2286
2287     def _convert_from_python(self, value, state):
2288         if isinstance(value, str):
2289             return value
2290         if hasattr(value, 'hour'):
2291             hour, minute = value.hour, value.minute
2292             second = value.second
2293         elif len(value) == 3:
2294             hour, minute, second = value
2295         elif len(value) == 2:
2296             hour, minute = value
2297             second = 0
2298         ampm = ''
2299         if (self.use_ampm == 'optional' and self.prefer_ampm) or (
2300                 self.use_ampm and self.use_ampm != 'optional'):
2301             ampm = 'am'
2302             if hour > 12:
2303                 hour -= 12
2304                 ampm = 'pm'
2305             elif hour == 12:
2306                 ampm = 'pm'
2307             elif hour == 0:
2308                 hour = 12
2309         if self.use_seconds:
2310             return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
2311         else:
2312             return '%i:%02i%s' % (hour, minute, ampm)
2313
2314
2315 def PostalCode(*kw, **kwargs):
2316     deprecation_warning("please use formencode.national.USPostalCode")
2317     from formencode.national import USPostalCode
2318     return USPostalCode(*kw, **kwargs)
2319
2320
2321 class StripField(FancyValidator):
2322     """
2323     Take a field from a dictionary, removing the key from the dictionary.
2324
2325     ``name`` is the key.  The field value and a new copy of the dictionary
2326     with that field removed are returned.
2327
2328     >>> StripField('test').to_python({'a': 1, 'test': 2})
2329     (2, {'a': 1})
2330     >>> StripField('test').to_python({})
2331     Traceback (most recent call last):
2332         ...
2333     Invalid: The name 'test' is missing
2334
2335     """
2336
2337     __unpackargs__ = ('name',)
2338
2339     messages = dict(
2340         missing=_('The name %(name)s is missing'))
2341
2342     def _convert_to_python(self, valueDict, state):
2343         v = valueDict.copy()
2344         try:
2345             field = v.pop(self.name)
2346         except KeyError:
2347             raise Invalid(
2348                 self.message('missing', state, name=repr(self.name)),
2349                 valueDict, state)
2350         return field, v
2351
2352     def is_empty(self, value):
2353         # empty dictionaries don't really apply here
2354         return False
2355
2356
2357 class StringBool(FancyValidator):  # originally from TurboGears 1
2358     """
2359     Converts a string to a boolean.
2360
2361     Values like 'true' and 'false' are considered True and False,
2362     respectively; anything in ``true_values`` is true, anything in
2363     ``false_values`` is false, case-insensitive).  The first item of
2364     those lists is considered the preferred form.
2365
2366     ::
2367
2368         >>> s = StringBool()
2369         >>> s.to_python('yes'), s.to_python('no')
2370         (True, False)
2371         >>> s.to_python(1), s.to_python('N')
2372         (True, False)
2373         >>> s.to_python('ye')
2374         Traceback (most recent call last):
2375             ...
2376         Invalid: Value should be 'true' or 'false'
2377     """
2378
2379     true_values = ['true', 't', 'yes', 'y', 'on', '1']
2380     false_values = ['false', 'f', 'no', 'n', 'off', '0']
2381
2382     messages = dict(
2383         string=_('Value should be %(true)r or %(false)r'))
2384
2385     def _convert_to_python(self, value, state):
2386         if isinstance(value, str):
2387             value = value.strip().lower()
2388             if value in self.true_values:
2389                 return True
2390             if not value or value in self.false_values:
2391                 return False
2392             raise Invalid(
2393                 self.message('string', state,
2394                     true=self.true_values[0], false=self.false_values[0]),
2395                 value, state)
2396         return bool(value)
2397
2398     def _convert_from_python(self, value, state):
2399         return (self.true_values if value else self.false_values)[0]
2400
2401 # Should deprecate:
2402 StringBoolean = StringBool
2403
2404
2405 class SignedString(FancyValidator):
2406     """
2407     Encodes a string into a signed string, and base64 encodes both the
2408     signature string and a random nonce.
2409
2410     It is up to you to provide a secret, and to keep the secret handy
2411     and consistent.
2412     """
2413
2414     messages = dict(
2415         malformed=_('Value does not contain a signature'),
2416         badsig=_('Signature is not correct'))
2417
2418     secret = None
2419     nonce_length = 4
2420
2421     def _convert_to_python(self, value, state):
2422         global sha1
2423         if not sha1:
2424             from hashlib import sha1
2425         assert self.secret is not None, "You must give a secret"
2426         parts = value.split(None, 1)
2427         if not parts or len(parts) == 1:
2428             raise Invalid(self.message('malformed', state), value, state)
2429         sig, rest = parts
2430         sig = sig.decode('base64')
2431         rest = rest.decode('base64')
2432         nonce = rest[:self.nonce_length]
2433         rest = rest[self.nonce_length:]
2434         expected = sha1(str(self.secret) + nonce + rest).digest()
2435         if expected != sig:
2436             raise Invalid(self.message('badsig', state), value, state)
2437         return rest
2438
2439     def _convert_from_python(self, value, state):
2440         global sha1
2441         if not sha1:
2442             from hashlib import sha1
2443         nonce = self.make_nonce()
2444         value = str(value)
2445         digest = sha1(self.secret + nonce + value).digest()
2446         return self.encode(digest) + ' ' + self.encode(nonce + value)
2447
2448     def encode(self, value):
2449         return value.encode('base64').strip().replace('\n', '')
2450
2451     def make_nonce(self):
2452         global random
2453         if not random:
2454             import random
2455         return ''.join(chr(random.randrange(256))
2456             for _i in range(self.nonce_length))
2457
2458
2459 class IPAddress(FancyValidator):
2460     """
2461     Formencode validator to check whether a string is a correct IP address.
2462
2463     Examples::
2464
2465         >>> ip = IPAddress()
2466         >>> ip.to_python('127.0.0.1')
2467         '127.0.0.1'
2468         >>> ip.to_python('299.0.0.1')
2469         Traceback (most recent call last):
2470             ...
2471         Invalid: The octets must be within the range of 0-255 (not '299')
2472         >>> ip.to_python('192.168.0.1/1')
2473         Traceback (most recent call last):
2474             ...
2475         Invalid: Please enter a valid IP address (a.b.c.d)
2476         >>> ip.to_python('asdf')
2477         Traceback (most recent call last):
2478             ...
2479         Invalid: Please enter a valid IP address (a.b.c.d)
2480     """
2481
2482     messages = dict(
2483         badFormat=_('Please enter a valid IP address (a.b.c.d)'),
2484         leadingZeros=_('The octets must not have leading zeros'),
2485         illegalOctets=_('The octets must be within the range of 0-255'
2486             ' (not %(octet)r)'))
2487
2488     leading_zeros = False
2489
2490     def _validate_python(self, value, state=None):
2491         try:
2492             if not value:
2493                 raise ValueError
2494             octets = value.split('.', 5)
2495             # Only 4 octets?
2496             if len(octets) != 4:
2497                 raise ValueError
2498             # Correct octets?
2499             for octet in octets:
2500                 if octet.startswith('0') and octet != '0':
2501                     if not self.leading_zeros:
2502                         raise Invalid(
2503                             self.message('leadingZeros', state), value, state)
2504                     # strip zeros so this won't be an octal number
2505                     octet = octet.lstrip('0')
2506                 if not 0 <= int(octet) < 256:
2507                     raise Invalid(
2508                         self.message('illegalOctets', state, octet=octet),
2509                         value, state)
2510         # Splitting faild: wrong syntax
2511         except ValueError:
2512             raise Invalid(self.message('badFormat', state), value, state)
2513
2514
2515 class CIDR(IPAddress):
2516     """
2517     Formencode validator to check whether a string is in correct CIDR
2518     notation (IP address, or IP address plus /mask).
2519
2520     Examples::
2521
2522         >>> cidr = CIDR()
2523         >>> cidr.to_python('127.0.0.1')
2524         '127.0.0.1'
2525         >>> cidr.to_python('299.0.0.1')
2526         Traceback (most recent call last):
2527             ...
2528         Invalid: The octets must be within the range of 0-255 (not '299')
2529         >>> cidr.to_python('192.168.0.1/1')
2530         Traceback (most recent call last):
2531             ...
2532         Invalid: The network size (bits) must be within the range of 8-32 (not '1')
2533         >>> cidr.to_python('asdf')
2534         Traceback (most recent call last):
2535             ...
2536         Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
2537     """
2538
2539     messages = dict(IPAddress._messages,
2540         badFormat=_('Please enter a valid IP address (a.b.c.d)'
2541             ' or IP network (a.b.c.d/e)'),
2542         illegalBits=_('The network size (bits) must be within the range'
2543             ' of 8-32 (not %(bits)r)'))
2544
2545     def _validate_python(self, value, state):
2546         try:
2547             # Split into octets and bits
2548             if '/' in value:  # a.b.c.d/e
2549                 addr, bits = value.split('/')
2550             else:  # a.b.c.d
2551                 addr, bits = value, 32
2552             # Use IPAddress validator to validate the IP part
2553             IPAddress._validate_python(self, addr, state)
2554             # Bits (netmask) correct?
2555             if not 8 <= int(bits) <= 32:
2556                 raise Invalid(
2557                     self.message('illegalBits', state, bits=bits),
2558                     value, state)
2559         # Splitting faild: wrong syntax
2560         except ValueError:
2561             raise Invalid(self.message('badFormat', state), value, state)
2562
2563
2564 class MACAddress(FancyValidator):
2565     """
2566     Formencode validator to check whether a string is a correct hardware
2567     (MAC) address.
2568
2569     Examples::
2570
2571         >>> mac = MACAddress()
2572         >>> mac.to_python('aa:bb:cc:dd:ee:ff')
2573         'aabbccddeeff'
2574         >>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
2575         Traceback (most recent call last):
2576             ...
2577         Invalid: A MAC address must contain 12 digits and A-F; the value you gave has 13 characters
2578         >>> mac.to_python('aa:bb:cc:dd:ee:fx')
2579         Traceback (most recent call last):
2580             ...
2581         Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
2582         >>> MACAddress(add_colons=True).to_python('aabbccddeeff')
2583         'aa:bb:cc:dd:ee:ff'
2584     """
2585
2586     strip = True
2587     valid_characters = '0123456789abcdefABCDEF'
2588     add_colons = False
2589
2590     messages = dict(
2591         badLength=_('A MAC address must contain 12 digits and A-F;'
2592             ' the value you gave has %(length)s characters'),
2593         badCharacter=_('MAC addresses may only contain 0-9 and A-F'
2594             ' (and optionally :), not %(char)r'))
2595
2596     def _convert_to_python(self, value, state):
2597         address = value.replace(':', '').lower()  # remove colons
2598         if len(address) != 12:
2599             raise Invalid(
2600                 self.message('badLength', state,
2601                     length=len(address)), address, state)
2602         for char in address:
2603             if char not in self.valid_characters:
2604                 raise Invalid(
2605                     self.message('badCharacter', state,
2606                         char=char), address, state)
2607         if self.add_colons:
2608             address = '%s:%s:%s:%s:%s:%s' % (
2609                 address[0:2], address[2:4], address[4:6],
2610                 address[6:8], address[8:10], address[10:12])
2611         return address
2612
2613     _convert_from_python = _convert_to_python
2614
2615
2616 class FormValidator(FancyValidator):
2617     """
2618     A FormValidator is something that can be chained with a Schema.
2619
2620     Unlike normal chaining the FormValidator can validate forms that
2621     aren't entirely valid.
2622
2623     The important method is .validate(), of course.  It gets passed a
2624     dictionary of the (processed) values from the form.  If you have
2625     .validate_partial_form set to True, then it will get the incomplete
2626     values as well -- check with the "in" operator if the form was able
2627     to process any particular field.
2628
2629     Anyway, .validate() should return a string or a dictionary.  If a
2630     string, it's an error message that applies to the whole form.  If
2631     not, then it should be a dictionary of fieldName: errorMessage.
2632     The special key "form" is the error message for the form as a whole
2633     (i.e., a string is equivalent to {"form": string}).
2634
2635     Returns None on no errors.
2636     """
2637
2638     validate_partial_form = False
2639
2640     validate_partial_python = None
2641     validate_partial_other = None
2642
2643     def is_empty(self, value):
2644         return False
2645
2646     def field_is_empty(self, value):
2647         return is_empty(value)
2648
2649
2650 class RequireIfMissing(FormValidator):
2651     """
2652     Require one field based on another field being present or missing.
2653
2654     This validator is applied to a form, not an individual field (usually
2655     using a Schema's ``pre_validators`` or ``chained_validators``) and is
2656     available under both names ``RequireIfMissing`` and ``RequireIfPresent``.
2657
2658     If you provide a ``missing`` value (a string key name) then
2659     if that field is missing the field must be entered.
2660     This gives you an either/or situation.
2661
2662     If you provide a ``present`` value (another string key name) then
2663     if that field is present, the required field must also be present.
2664
2665     ::
2666
2667         >>> from formencode import validators
2668         >>> v = validators.RequireIfPresent('phone_type', present='phone')
2669         >>> v.to_python(dict(phone_type='', phone='510 420  4577'))
2670         Traceback (most recent call last):
2671             ...
2672         Invalid: You must give a value for phone_type
2673         >>> v.to_python(dict(phone=''))
2674         {'phone': ''}
2675
2676     Note that if you have a validator on the optionally-required
2677     field, you should probably use ``if_missing=None``.  This way you
2678     won't get an error from the Schema about a missing value.  For example::
2679
2680         class PhoneInput(Schema):
2681             phone = PhoneNumber()
2682             phone_type = String(if_missing=None)
2683             chained_validators = [RequireIfPresent('phone_type', present='phone')]
2684     """
2685
2686     # Field that potentially is required:
2687     required = None
2688     # If this field is missing, then it is required:
2689     missing = None
2690     # If this field is present, then it is required:
2691     present = None
2692
2693     __unpackargs__ = ('required',)
2694
2695     def _convert_to_python(self, value_dict, state):
2696         is_empty = self.field_is_empty
2697         if is_empty(value_dict.get(self.required)) and (
2698                 (self.missing and is_empty(value_dict.get(self.missing))) or
2699                 (self.present and not is_empty(value_dict.get(self.present)))):
2700             raise Invalid(
2701                 _('You must give a value for %s') % self.required,
2702                 value_dict, state,
2703                 error_dict={self.required:
2704                     Invalid(self.message('empty', state),
2705                         value_dict.get(self.required), state)})
2706         return value_dict
2707
2708 RequireIfPresent = RequireIfMissing
2709
2710 class RequireIfMatching(FormValidator):
2711     """
2712     Require a list of fields based on the value of another field.
2713
2714     This validator is applied to a form, not an individual field (usually
2715     using a Schema's ``pre_validators`` or ``chained_validators``).
2716
2717     You provide a field name, an expected value and a list of required fields
2718     (a list of string key names). If the value of the field, if present,
2719     matches the value of ``expected_value``, then the validator will raise an
2720     ``Invalid`` exception for every field in ``required_fields`` that is
2721     missing.
2722
2723     ::
2724
2725         >>> from formencode import validators
2726         >>> v = validators.RequireIfMatching('phone_type', expected_value='mobile', required_fields=['mobile'])
2727         >>> v.to_python(dict(phone_type='mobile'))
2728         Traceback (most recent call last):
2729             ...
2730         formencode.api.Invalid: You must give a value for mobile
2731         >>> v.to_python(dict(phone_type='someothervalue'))
2732         {'phone_type': 'someothervalue'}
2733     """
2734
2735     # Field that we will check for its value:
2736     field = None
2737     # Value that the field shall have
2738     expected_value = None
2739     # If this field is present, then these fields are required:
2740     required_fields = []
2741
2742     __unpackargs__ = ('field', 'expected_value')
2743
2744     def _convert_to_python(self, value_dict, state):
2745         is_empty = self.field_is_empty
2746
2747         if self.field in value_dict and value_dict.get(self.field) == self.expected_value:
2748             for required_field in self.required_fields:
2749                 if required_field not in value_dict or is_empty(value_dict.get(required_field)):
2750                     raise Invalid(
2751                         _('You must give a value for %s') % required_field,
2752                         value_dict, state,
2753                         error_dict={required_field:
2754                             Invalid(self.message('empty', state),
2755                                 value_dict.get(required_field), state)})
2756         return value_dict
2757
2758 class FieldsMatch(FormValidator):
2759     """
2760     Tests that the given fields match, i.e., are identical.  Useful
2761     for password+confirmation fields.  Pass the list of field names in
2762     as `field_names`.
2763
2764     ::
2765
2766         >>> f = FieldsMatch('pass', 'conf')
2767         >>> sorted(f.to_python({'pass': 'xx', 'conf': 'xx'}).items())
2768         [('conf', 'xx'), ('pass', 'xx')]
2769         >>> f.to_python({'pass': 'xx', 'conf': 'yy'})
2770         Traceback (most recent call last):
2771             ...
2772         Invalid: conf: Fields do not match
2773     """
2774
2775     show_match = False
2776     field_names = None
2777     validate_partial_form = True
2778
2779     __unpackargs__ = ('*', 'field_names')
2780
2781     messages = dict(
2782         invalid=_('Fields do not match (should be %(match)s)'),
2783         invalidNoMatch=_('Fields do not match'),
2784         notDict=_('Fields should be a dictionary'))
2785
2786     def __init__(self, *args, **kw):
2787         super(FieldsMatch, self).__init__(*args, **kw)
2788         if len(self.field_names) < 2:
2789             raise TypeError('FieldsMatch() requires at least two field names')
2790
2791     def validate_partial(self, field_dict, state):
2792         for name in self.field_names:
2793             if name not in field_dict:
2794                 return
2795         self._validate_python(field_dict, state)
2796
2797     def _validate_python(self, field_dict, state):
2798         try:
2799             ref = field_dict[self.field_names[0]]
2800         except TypeError:
2801             # Generally because field_dict isn't a dict
2802             raise Invalid(self.message('notDict', state), field_dict, state)
2803         except KeyError:
2804             ref = ''
2805         errors = {}
2806         for name in self.field_names[1:]:
2807             if field_dict.get(name, '') != ref:
2808                 if self.show_match:
2809                     errors[name] = self.message('invalid', state,
2810                                                 match=ref)
2811                 else:
2812                     errors[name] = self.message('invalidNoMatch', state)
2813         if errors:
2814             error_list = sorted(errors.items())
2815             error_message = '<br>\n'.join(
2816                 '%s: %s' % (name, value) for name, value in error_list)
2817             raise Invalid(error_message, field_dict, state, error_dict=errors)
2818
2819
2820 class CreditCardValidator(FormValidator):
2821     """
2822     Checks that credit card numbers are valid (if not real).
2823
2824     You pass in the name of the field that has the credit card
2825     type and the field with the credit card number.  The credit
2826     card type should be one of "visa", "mastercard", "amex",
2827     "dinersclub", "discover", "jcb".
2828
2829     You must check the expiration date yourself (there is no
2830     relation between CC number/types and expiration dates).
2831
2832     ::
2833
2834         >>> cc = CreditCardValidator()
2835         >>> sorted(cc.to_python({'ccType': 'visa', 'ccNumber': '4111111111111111'}).items())
2836         [('ccNumber', '4111111111111111'), ('ccType', 'visa')]
2837         >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111111'})
2838         Traceback (most recent call last):
2839             ...
2840         Invalid: ccNumber: You did not enter a valid number of digits
2841         >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111112'})
2842         Traceback (most recent call last):
2843             ...
2844         Invalid: ccNumber: You did not enter a valid number of digits
2845         >>> cc().to_python({})
2846         Traceback (most recent call last):
2847             ...
2848         Invalid: The field ccType is missing
2849     """
2850
2851     validate_partial_form = True
2852
2853     cc_type_field = 'ccType'
2854     cc_number_field = 'ccNumber'
2855
2856     __unpackargs__ = ('cc_type_field', 'cc_number_field')
2857
2858     messages = dict(
2859         notANumber=_('Please enter only the number, no other characters'),
2860         badLength=_('You did not enter a valid number of digits'),
2861         invalidNumber=_('That number is not valid'),
2862         missing_key=_('The field %(key)s is missing'))
2863
2864     def validate_partial(self, field_dict, state):
2865         if not field_dict.get(self.cc_type_field, None) \
2866            or not field_dict.get(self.cc_number_field, None):
2867             return None
2868         self._validate_python(field_dict, state)
2869
2870     def _validate_python(self, field_dict, state):
2871         errors = self._validateReturn(field_dict, state)
2872         if errors:
2873             error_list = sorted(errors.items())
2874             raise Invalid(
2875                 '<br>\n'.join('%s: %s' % (name, value)
2876                     for name, value in error_list),
2877                 field_dict, state, error_dict=errors)
2878
2879     def _validateReturn(self, field_dict, state):
2880         for field in self.cc_type_field, self.cc_number_field:
2881             if field not in field_dict:
2882                 raise Invalid(
2883                     self.message('missing_key', state, key=field),
2884                     field_dict, state)
2885         ccType = field_dict[self.cc_type_field].lower().strip()
2886         number = field_dict[self.cc_number_field].strip()
2887         number = number.replace(' ', '')
2888         number = number.replace('-', '')
2889         try:
2890             int(number)
2891         except ValueError:
2892             return {self.cc_number_field: self.message('notANumber', state)}
2893         assert ccType in self._cardInfo, (
2894             "I can't validate that type of credit card")
2895         foundValid = False
2896         validLength = False
2897         for prefix, length in self._cardInfo[ccType]:
2898             if len(number) == length:
2899                 validLength = True
2900                 if number.startswith(prefix):
2901                     foundValid = True
2902                     break
2903         if not validLength:
2904             return {self.cc_number_field: self.message('badLength', state)}
2905         if not foundValid:
2906             return {self.cc_number_field: self.message('invalidNumber', state)}
2907         if not self._validateMod10(number):
2908             return {self.cc_number_field: self.message('invalidNumber', state)}
2909         return None
2910
2911     def _validateMod10(self, s):
2912         """Check string with the mod 10 algorithm (aka "Luhn formula")."""
2913         checksum, factor = 0, 1
2914         for c in reversed(s):
2915             for c in str(factor * int(c)):
2916                 checksum += int(c)
2917             factor = 3 - factor
2918         return checksum % 10 == 0
2919
2920     _cardInfo = {
2921         "visa": [('4', 16),
2922                  ('4', 13)],
2923         "mastercard": [('51', 16),
2924                        ('52', 16),
2925                        ('53', 16),
2926                        ('54', 16),
2927                        ('55', 16)],
2928         "discover": [('6011', 16)],
2929         "amex": [('34', 15),
2930                  ('37', 15)],
2931         "dinersclub": [('300', 14),
2932                        ('301', 14),
2933                        ('302', 14),
2934                        ('303', 14),
2935                        ('304', 14),
2936                        ('305', 14),
2937                        ('36', 14),
2938                        ('38', 14)],
2939         "jcb": [('3', 16),
2940                 ('2131', 15),
2941                 ('1800', 15)],
2942             }
2943
2944
2945 class CreditCardExpires(FormValidator):
2946     """
2947     Checks that credit card expiration date is valid relative to
2948     the current date.
2949
2950     You pass in the name of the field that has the credit card
2951     expiration month and the field with the credit card expiration
2952     year.
2953
2954     ::
2955
2956         >>> ed = CreditCardExpires()
2957         >>> sorted(ed.to_python({'ccExpiresMonth': '11', 'ccExpiresYear': '2250'}).items())
2958         [('ccExpiresMonth', '11'), ('ccExpiresYear', '2250')]
2959         >>> ed.to_python({'ccExpiresMonth': '10', 'ccExpiresYear': '2005'})
2960         Traceback (most recent call last):
2961             ...
2962         Invalid: ccExpiresMonth: Invalid Expiration Date<br>
2963         ccExpiresYear: Invalid Expiration Date
2964     """
2965
2966     validate_partial_form = True
2967
2968     cc_expires_month_field = 'ccExpiresMonth'
2969     cc_expires_year_field = 'ccExpiresYear'
2970
2971     __unpackargs__ = ('cc_expires_month_field', 'cc_expires_year_field')
2972
2973     datetime_module = None
2974
2975     messages = dict(
2976         notANumber=_('Please enter numbers only for month and year'),
2977         invalidNumber=_('Invalid Expiration Date'))
2978
2979     def validate_partial(self, field_dict, state):
2980         if not field_dict.get(self.cc_expires_month_field, None) \
2981            or not field_dict.get(self.cc_expires_year_field, None):
2982             return None
2983         self._validate_python(field_dict, state)
2984
2985     def _validate_python(self, field_dict, state):
2986         errors = self._validateReturn(field_dict, state)
2987         if errors:
2988             error_list = sorted(errors.items())
2989             raise Invalid(
2990                 '<br>\n'.join('%s: %s' % (name, value)
2991                     for name, value in error_list),
2992                 field_dict, state, error_dict=errors)
2993
2994     def _validateReturn(self, field_dict, state):
2995         ccExpiresMonth = str(field_dict[self.cc_expires_month_field]).strip()
2996         ccExpiresYear = str(field_dict[self.cc_expires_year_field]).strip()
2997
2998         try:
2999             ccExpiresMonth = int(ccExpiresMonth)
3000             ccExpiresYear = int(ccExpiresYear)
3001             dt_mod = import_datetime(self.datetime_module)
3002             now = datetime_now(dt_mod)
3003             today = datetime_makedate(dt_mod, now.year, now.month, now.day)
3004             next_month = ccExpiresMonth % 12 + 1
3005             next_month_year = ccExpiresYear
3006             if next_month == 1:
3007                 next_month_year += 1
3008             expires_date = datetime_makedate(
3009                 dt_mod, next_month_year, next_month, 1)
3010             assert expires_date > today
3011         except ValueError:
3012             return {self.cc_expires_month_field:
3013                         self.message('notANumber', state),
3014                     self.cc_expires_year_field:
3015                         self.message('notANumber', state)}
3016         except AssertionError:
3017             return {self.cc_expires_month_field:
3018                         self.message('invalidNumber', state),
3019                     self.cc_expires_year_field:
3020                         self.message('invalidNumber', state)}
3021
3022
3023 class CreditCardSecurityCode(FormValidator):
3024     """
3025     Checks that credit card security code has the correct number
3026     of digits for the given credit card type.
3027
3028     You pass in the name of the field that has the credit card
3029     type and the field with the credit card security code.
3030
3031     ::
3032
3033         >>> code = CreditCardSecurityCode()
3034         >>> sorted(code.to_python({'ccType': 'visa', 'ccCode': '111'}).items())
3035         [('ccCode', '111'), ('ccType', 'visa')]
3036         >>> code.to_python({'ccType': 'visa', 'ccCode': '1111'})
3037         Traceback (most recent call last):
3038             ...
3039         Invalid: ccCode: Invalid credit card security code length
3040     """
3041
3042     validate_partial_form = True
3043
3044     cc_type_field = 'ccType'
3045     cc_code_field = 'ccCode'
3046
3047     __unpackargs__ = ('cc_type_field', 'cc_code_field')
3048
3049     messages = dict(
3050         notANumber=_('Please enter numbers only for credit card security code'),
3051         badLength=_('Invalid credit card security code length'))
3052
3053     def validate_partial(self, field_dict, state):
3054         if (not field_dict.get(self.cc_type_field, None)
3055                 or not field_dict.get(self.cc_code_field, None)):
3056             return None
3057         self._validate_python(field_dict, state)
3058
3059     def _validate_python(self, field_dict, state):
3060         errors = self._validateReturn(field_dict, state)
3061         if errors:
3062             error_list = sorted(errors.items())
3063             raise Invalid(
3064                 '<br>\n'.join('%s: %s' % (name, value)
3065                     for name, value in error_list),
3066                 field_dict, state, error_dict=errors)
3067
3068     def _validateReturn(self, field_dict, state):
3069         ccType = str(field_dict[self.cc_type_field]).strip()
3070         ccCode = str(field_dict[self.cc_code_field]).strip()
3071         try:
3072             int(ccCode)
3073         except ValueError:
3074             return {self.cc_code_field: self.message('notANumber', state)}
3075         length = self._cardInfo[ccType]
3076         if len(ccCode) != length:
3077             return {self.cc_code_field: self.message('badLength', state)}
3078
3079     # key = credit card type, value = length of security code
3080     _cardInfo = dict(visa=3, mastercard=3, discover=3, amex=4)
3081
3082
3083 def validators():
3084     """Return the names of all validators in this module."""
3085     return [name for name, value in globals().items()
3086         if isinstance(value, type) and issubclass(value, Validator)]
3087
3088 __all__ = ['Invalid'] + validators()
3089