CI(AppVeyor): Work around a strange problem with Py3.7
authorOleg Broytman <phd@phdru.name>
Thu, 31 Jan 2019 22:59:40 +0000 (01:59 +0300)
committerOleg Broytman <phd@phdru.name>
Thu, 31 Jan 2019 23:00:05 +0000 (02:00 +0300)
Work around a very strange problem with Python 3.7 at AppVeyor
by copying correct validators.py to formencode.

setup.cfg
tox.ini
validators.py [new file with mode: 0644]

index 708bf5e..9e6f371 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,5 +10,5 @@ tag_date = 0
 tag_svn_revision = 0
 
 [flake8]
-exclude = .git,.tox,docs/conf.py
+exclude = .git,.tox,docs/conf.py,validators.py
 
diff --git a/tox.ini b/tox.ini
index 030496a..a185a37 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -20,6 +20,7 @@ passenv = PGPASSWORD
 platform = linux
 # Don't fail or warn on uninstalled commands
 whitelist_externals =
+    cmd
     createdb
     dropdb
 
@@ -97,7 +98,9 @@ commands = {[postgres-w32]commands}
 
 [testenv:py37-postgres-w32]
 platform = win32
-commands = {[postgres-w32]commands}
+commands =
+    cmd /c "copy validators.py {envsitepackagesdir}\\formencode\\validators.py"
+    {[postgres-w32]commands}
 
 # SQLite test environments
 [sqlite]
@@ -148,7 +151,9 @@ commands = {[sqlite-w32]commands}
 
 [testenv:py37-sqlite-w32]
 platform = win32
-commands = {[sqlite-w32]commands}
+commands =
+    cmd /c "copy validators.py {envsitepackagesdir}\\formencode\\validators.py"
+    {[sqlite-w32]commands}
 
 # flake8
 [testenv:py27-flake8]
diff --git a/validators.py b/validators.py
new file mode 100644 (file)
index 0000000..233d5dd
--- /dev/null
@@ -0,0 +1,3089 @@
+## FormEncode, a  Form processor
+## Copyright (C) 2003, Ian Bicking <ianb@colorstudy.com>
+
+"""
+Validator/Converters for use with FormEncode.
+"""
+
+import cgi
+import locale
+import re
+import warnings
+from encodings import idna
+
+try:  # import dnspython
+    import dns.resolver
+    import dns.exception
+except (IOError, ImportError):
+    have_dns = False
+else:
+    have_dns = True
+
+
+# These are only imported when needed
+httplib = None
+random = None
+sha1 = None
+socket = None
+urlparse = None
+
+from .api import (FancyValidator, Identity, Invalid, NoDefault, Validator,
+    deprecation_warning, is_empty)
+
+assert Identity and Invalid and NoDefault  # silence unused import warnings
+
+# Dummy i18n translation function, nothing is translated here.
+# Instead this is actually done in api.message.
+# The surrounding _('string') of the strings is only for extracting
+# the strings automatically.
+# If you run pygettext with this source comment this function out temporarily.
+_ = lambda s: s
+
+
+############################################################
+## Utility methods
+############################################################
+
+# These all deal with accepting both datetime and mxDateTime modules and types
+datetime_module = None
+mxDateTime_module = None
+
+
+def import_datetime(module_type):
+    global datetime_module, mxDateTime_module
+    module_type = module_type.lower() if module_type else 'datetime'
+    if module_type == 'datetime':
+        if datetime_module is None:
+            import datetime as datetime_module
+        return datetime_module
+    elif module_type == 'mxdatetime':
+        if mxDateTime_module is None:
+            from mx import DateTime as mxDateTime_module
+        return mxDateTime_module
+    else:
+        raise ImportError('Invalid datetime module %r' % module_type)
+
+
+def datetime_now(module):
+    if module.__name__ == 'datetime':
+        return module.datetime.now()
+    else:
+        return module.now()
+
+
+def datetime_makedate(module, year, month, day):
+    if module.__name__ == 'datetime':
+        return module.date(year, month, day)
+    else:
+        try:
+            return module.DateTime(year, month, day)
+        except module.RangeError as e:
+            raise ValueError(str(e))
+
+
+def datetime_time(module):
+    if module.__name__ == 'datetime':
+        return module.time
+    else:
+        return module.Time
+
+
+def datetime_isotime(module):
+    if module.__name__ == 'datetime':
+        return module.time.isoformat
+    else:
+        return module.ISO.Time
+
+
+############################################################
+## Wrapper Validators
+############################################################
+
+class ConfirmType(FancyValidator):
+    """
+    Confirms that the input/output is of the proper type.
+
+    Uses the parameters:
+
+    subclass:
+        The class or a tuple of classes; the item must be an instance
+        of the class or a subclass.
+    type:
+        A type or tuple of types (or classes); the item must be of
+        the exact class or type.  Subclasses are not allowed.
+
+    Examples::
+
+        >>> cint = ConfirmType(subclass=int)
+        >>> cint.to_python(True)
+        True
+        >>> cint.to_python('1')
+        Traceback (most recent call last):
+            ...
+        Invalid: '1' is not a subclass of <type 'int'>
+        >>> cintfloat = ConfirmType(subclass=(float, int))
+        >>> cintfloat.to_python(1.0), cintfloat.from_python(1.0)
+        (1.0, 1.0)
+        >>> cintfloat.to_python(1), cintfloat.from_python(1)
+        (1, 1)
+        >>> cintfloat.to_python(None)
+        Traceback (most recent call last):
+            ...
+        Invalid: None is not a subclass of one of the types <type 'float'>, <type 'int'>
+        >>> cint2 = ConfirmType(type=int)
+        >>> cint2(accept_python=False).from_python(True)
+        Traceback (most recent call last):
+            ...
+        Invalid: True must be of the type <type 'int'>
+    """
+
+    accept_iterator = True
+
+    subclass = None
+    type = None
+
+    messages = dict(
+        subclass=_('%(object)r is not a subclass of %(subclass)s'),
+        inSubclass=_('%(object)r is not a subclass of one of the types %(subclassList)s'),
+        inType=_('%(object)r must be one of the types %(typeList)s'),
+        type=_('%(object)r must be of the type %(type)s'))
+
+    def __init__(self, *args, **kw):
+        FancyValidator.__init__(self, *args, **kw)
+        if self.subclass:
+            if isinstance(self.subclass, list):
+                self.subclass = tuple(self.subclass)
+            elif not isinstance(self.subclass, tuple):
+                self.subclass = (self.subclass,)
+            self._validate_python = self.confirm_subclass
+        if self.type:
+            if isinstance(self.type, list):
+                self.type = tuple(self.type)
+            elif not isinstance(self.type, tuple):
+                self.type = (self.type,)
+            self._validate_python = self.confirm_type
+
+    def confirm_subclass(self, value, state):
+        if not isinstance(value, self.subclass):
+            if len(self.subclass) == 1:
+                msg = self.message('subclass', state, object=value,
+                                   subclass=self.subclass[0])
+            else:
+                subclass_list = ', '.join(map(str, self.subclass))
+                msg = self.message('inSubclass', state, object=value,
+                                   subclassList=subclass_list)
+            raise Invalid(msg, value, state)
+
+    def confirm_type(self, value, state):
+        for t in self.type:
+            if type(value) is t:
+                break
+        else:
+            if len(self.type) == 1:
+                msg = self.message('type', state, object=value,
+                                   type=self.type[0])
+            else:
+                msg = self.message('inType', state, object=value,
+                                   typeList=', '.join(map(str, self.type)))
+            raise Invalid(msg, value, state)
+        return value
+
+    def is_empty(self, value):
+        return False
+
+
+class Wrapper(FancyValidator):
+    """
+    Used to convert functions to validator/converters.
+
+    You can give a simple function for `_convert_to_python`,
+    `_convert_from_python`, `_validate_python` or `_validate_other`.
+    If that function raises an exception, the value is considered invalid.
+    Whatever value the function returns is considered the converted value.
+
+    Unlike validators, the `state` argument is not used.  Functions
+    like `int` can be used here, that take a single argument.
+
+    Note that as Wrapper will generate a FancyValidator, empty
+    values (those who pass ``FancyValidator.is_empty)`` will return ``None``.
+    To override this behavior you can use ``Wrapper(empty_value=callable)``.
+    For example passing ``Wrapper(empty_value=lambda val: val)`` will return
+    the value itself when is considered empty.
+
+    Examples::
+
+        >>> def downcase(v):
+        ...     return v.lower()
+        >>> wrap = Wrapper(convert_to_python=downcase)
+        >>> wrap.to_python('This')
+        'this'
+        >>> wrap.from_python('This')
+        'This'
+        >>> wrap.to_python('') is None
+        True
+        >>> wrap2 = Wrapper(
+        ...     convert_from_python=downcase, empty_value=lambda value: value)
+        >>> wrap2.from_python('This')
+        'this'
+        >>> wrap2.to_python('')
+        ''
+        >>> wrap2.from_python(1)
+        Traceback (most recent call last):
+          ...
+        Invalid: 'int' object has no attribute 'lower'
+        >>> wrap3 = Wrapper(validate_python=int)
+        >>> wrap3.to_python('1')
+        '1'
+        >>> wrap3.to_python('a') # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+          ...
+        Invalid: invalid literal for int()...
+    """
+
+    func_convert_to_python = None
+    func_convert_from_python = None
+    func_validate_python = None
+    func_validate_other = None
+
+    _deprecated_methods = (
+        ('func_to_python', 'func_convert_to_python'),
+        ('func_from_python', 'func_convert_from_python'))
+
+    def __init__(self, *args, **kw):
+        # allow old method names as parameters
+        if 'to_python' in kw and 'convert_to_python' not in kw:
+            kw['convert_to_python'] = kw.pop('to_python')
+        if 'from_python' in kw and 'convert_from_python' not in kw:
+            kw['convert_from_python'] = kw.pop('from_python')
+        for n in ('convert_to_python', 'convert_from_python',
+                  'validate_python', 'validate_other'):
+            if n in kw:
+                kw['func_%s' % n] = kw.pop(n)
+        FancyValidator.__init__(self, *args, **kw)
+        self._convert_to_python = self.wrap(self.func_convert_to_python)
+        self._convert_from_python = self.wrap(self.func_convert_from_python)
+        self._validate_python = self.wrap(self.func_validate_python)
+        self._validate_other = self.wrap(self.func_validate_other)
+
+    def wrap(self, func):
+        if not func:
+            return None
+
+        def result(value, state, func=func):
+            try:
+                return func(value)
+            except Exception as e:
+                raise Invalid(str(e), value, state)
+
+        return result
+
+
+class Constant(FancyValidator):
+    """
+    This converter converts everything to the same thing.
+
+    I.e., you pass in the constant value when initializing, then all
+    values get converted to that constant value.
+
+    This is only really useful for funny situations, like::
+
+      # Any evaluates sub validators in reverse order for to_python
+      fromEmailValidator = Any(
+                            Constant('unknown@localhost'),
+                               Email())
+
+    In this case, the if the email is not valid
+    ``'unknown@localhost'`` will be used instead.  Of course, you
+    could use ``if_invalid`` instead.
+
+    Examples::
+
+        >>> Constant('X').to_python('y')
+        'X'
+    """
+
+    __unpackargs__ = ('value',)
+
+    def _convert_to_python(self, value, state):
+        return self.value
+
+    _convert_from_python = _convert_to_python
+
+
+############################################################
+## Normal validators
+############################################################
+
+class MaxLength(FancyValidator):
+    """
+    Invalid if the value is longer than `maxLength`.  Uses len(),
+    so it can work for strings, lists, or anything with length.
+
+    Examples::
+
+        >>> max5 = MaxLength(5)
+        >>> max5.to_python('12345')
+        '12345'
+        >>> max5.from_python('12345')
+        '12345'
+        >>> max5.to_python('123456')
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value less than 5 characters long
+        >>> max5(accept_python=False).from_python('123456')
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value less than 5 characters long
+        >>> max5.to_python([1, 2, 3])
+        [1, 2, 3]
+        >>> max5.to_python([1, 2, 3, 4, 5, 6])
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value less than 5 characters long
+        >>> max5.to_python(5)
+        Traceback (most recent call last):
+          ...
+        Invalid: Invalid value (value with length expected)
+    """
+
+    __unpackargs__ = ('maxLength',)
+
+    messages = dict(
+        tooLong=_('Enter a value less than %(maxLength)i characters long'),
+        invalid=_('Invalid value (value with length expected)'))
+
+    def _validate_python(self, value, state):
+        try:
+            if value and len(value) > self.maxLength:
+                raise Invalid(
+                    self.message('tooLong', state,
+                        maxLength=self.maxLength), value, state)
+            else:
+                return None
+        except TypeError:
+            raise Invalid(
+                self.message('invalid', state), value, state)
+
+
+class MinLength(FancyValidator):
+    """
+    Invalid if the value is shorter than `minlength`.  Uses len(), so
+    it can work for strings, lists, or anything with length.  Note
+    that you **must** use ``not_empty=True`` if you don't want to
+    accept empty values -- empty values are not tested for length.
+
+    Examples::
+
+        >>> min5 = MinLength(5)
+        >>> min5.to_python('12345')
+        '12345'
+        >>> min5.from_python('12345')
+        '12345'
+        >>> min5.to_python('1234')
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value at least 5 characters long
+        >>> min5(accept_python=False).from_python('1234')
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value at least 5 characters long
+        >>> min5.to_python([1, 2, 3, 4, 5])
+        [1, 2, 3, 4, 5]
+        >>> min5.to_python([1, 2, 3])
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter a value at least 5 characters long
+        >>> min5.to_python(5)
+        Traceback (most recent call last):
+          ...
+        Invalid: Invalid value (value with length expected)
+
+    """
+
+    __unpackargs__ = ('minLength',)
+
+    messages = dict(
+        tooShort=_('Enter a value at least %(minLength)i characters long'),
+        invalid=_('Invalid value (value with length expected)'))
+
+    def _validate_python(self, value, state):
+        try:
+            if len(value) < self.minLength:
+                raise Invalid(
+                    self.message('tooShort', state,
+                        minLength=self.minLength), value, state)
+        except TypeError:
+            raise Invalid(
+                self.message('invalid', state), value, state)
+
+
+class NotEmpty(FancyValidator):
+    """
+    Invalid if value is empty (empty string, empty list, etc).
+
+    Generally for objects that Python considers false, except zero
+    which is not considered invalid.
+
+    Examples::
+
+        >>> ne = NotEmpty(messages=dict(empty='enter something'))
+        >>> ne.to_python('')
+        Traceback (most recent call last):
+          ...
+        Invalid: enter something
+        >>> ne.to_python(0)
+        0
+    """
+    not_empty = True
+
+    messages = dict(
+        empty=_('Please enter a value'))
+
+    def _validate_python(self, value, state):
+        if value == 0:
+            # This isn't "empty" for this definition.
+            return value
+        if not value:
+            raise Invalid(self.message('empty', state), value, state)
+
+
+class Empty(FancyValidator):
+    """
+    Invalid unless the value is empty.  Use cleverly, if at all.
+
+    Examples::
+
+        >>> Empty.to_python(0)
+        Traceback (most recent call last):
+          ...
+        Invalid: You cannot enter a value here
+    """
+
+    messages = dict(
+        notEmpty=_('You cannot enter a value here'))
+
+    def _validate_python(self, value, state):
+        if value or value == 0:
+            raise Invalid(self.message('notEmpty', state), value, state)
+
+
+class Regex(FancyValidator):
+    """
+    Invalid if the value doesn't match the regular expression `regex`.
+
+    The regular expression can be a compiled re object, or a string
+    which will be compiled for you.
+
+    Use strip=True if you want to strip the value before validation,
+    and as a form of conversion (often useful).
+
+    Examples::
+
+        >>> cap = Regex(r'^[A-Z]+$')
+        >>> cap.to_python('ABC')
+        'ABC'
+
+    Note that ``.from_python()`` calls (in general) do not validate
+    the input::
+
+        >>> cap.from_python('abc')
+        'abc'
+        >>> cap(accept_python=False).from_python('abc')
+        Traceback (most recent call last):
+          ...
+        Invalid: The input is not valid
+        >>> cap.to_python(1)
+        Traceback (most recent call last):
+          ...
+        Invalid: The input must be a string (not a <type 'int'>: 1)
+        >>> Regex(r'^[A-Z]+$', strip=True).to_python('  ABC  ')
+        'ABC'
+        >>> Regex(r'this', regexOps=('I',)).to_python('THIS')
+        'THIS'
+    """
+
+    regexOps = ()
+    strip = False
+    regex = None
+
+    __unpackargs__ = ('regex',)
+
+    messages = dict(
+        invalid=_('The input is not valid'))
+
+    def __init__(self, *args, **kw):
+        FancyValidator.__init__(self, *args, **kw)
+        if isinstance(self.regex, str):
+            ops = 0
+            assert not isinstance(self.regexOps, str), (
+                "regexOps should be a list of options from the re module "
+                "(names, or actual values)")
+            for op in self.regexOps:
+                if isinstance(op, str):
+                    ops |= getattr(re, op)
+                else:
+                    ops |= op
+            self.regex = re.compile(self.regex, ops)
+
+    def _validate_python(self, value, state):
+        self.assert_string(value, state)
+        if self.strip and isinstance(value, str):
+            value = value.strip()
+        if not self.regex.search(value):
+            raise Invalid(self.message('invalid', state), value, state)
+
+    def _convert_to_python(self, value, state):
+        if self.strip and isinstance(value, str):
+            return value.strip()
+        return value
+
+
+class PlainText(Regex):
+    """
+    Test that the field contains only letters, numbers, underscore,
+    and the hyphen.  Subclasses Regex.
+
+    Examples::
+
+        >>> PlainText.to_python('_this9_')
+        '_this9_'
+        >>> PlainText.from_python('  this  ')
+        '  this  '
+        >>> PlainText(accept_python=False).from_python('  this  ')
+        Traceback (most recent call last):
+          ...
+        Invalid: Enter only letters, numbers, or _ (underscore)
+        >>> PlainText(strip=True).to_python('  this  ')
+        'this'
+        >>> PlainText(strip=True).from_python('  this  ')
+        'this'
+    """
+
+    regex = r"^[a-zA-Z_\-0-9]*$"
+
+    messages = dict(
+        invalid=_('Enter only letters, numbers, or _ (underscore)'))
+
+
+class OneOf(FancyValidator):
+    """
+    Tests that the value is one of the members of a given list.
+
+    If ``testValueList=True``, then if the input value is a list or
+    tuple, all the members of the sequence will be checked (i.e., the
+    input must be a subset of the allowed values).
+
+    Use ``hideList=True`` to keep the list of valid values out of the
+    error message in exceptions.
+
+    Examples::
+
+        >>> oneof = OneOf([1, 2, 3])
+        >>> oneof.to_python(1)
+        1
+        >>> oneof.to_python(4)
+        Traceback (most recent call last):
+          ...
+        Invalid: Value must be one of: 1; 2; 3 (not 4)
+        >>> oneof(testValueList=True).to_python([2, 3, [1, 2, 3]])
+        [2, 3, [1, 2, 3]]
+        >>> oneof.to_python([2, 3, [1, 2, 3]])
+        Traceback (most recent call last):
+          ...
+        Invalid: Value must be one of: 1; 2; 3 (not [2, 3, [1, 2, 3]])
+    """
+
+    list = None
+    testValueList = False
+    hideList = False
+
+    __unpackargs__ = ('list',)
+
+    messages = dict(
+        invalid=_('Invalid value'),
+        notIn=_('Value must be one of: %(items)s (not %(value)r)'))
+
+    def _validate_python(self, value, state):
+        if self.testValueList and isinstance(value, (list, tuple)):
+            for v in value:
+                self._validate_python(v, state)
+        else:
+            if not value in self.list:
+                if self.hideList:
+                    raise Invalid(self.message('invalid', state), value, state)
+                else:
+                    try:
+                        items = '; '.join(map(str, self.list))
+                    except UnicodeError:
+                        items = '; '.join(map(str, self.list))
+                    raise Invalid(
+                        self.message('notIn', state,
+                            items=items, value=value), value, state)
+
+    @property
+    def accept_iterator(self):
+        return self.testValueList
+
+
+class DictConverter(FancyValidator):
+    """
+    Converts values based on a dictionary which has values as keys for
+    the resultant values.
+
+    If ``allowNull`` is passed, it will not balk if a false value
+    (e.g., '' or None) is given (it will return None in these cases).
+
+    to_python takes keys and gives values, from_python takes values and
+    gives keys.
+
+    If you give hideDict=True, then the contents of the dictionary
+    will not show up in error messages.
+
+    Examples::
+
+        >>> dc = DictConverter({1: 'one', 2: 'two'})
+        >>> dc.to_python(1)
+        'one'
+        >>> dc.from_python('one')
+        1
+        >>> dc.to_python(3)
+        Traceback (most recent call last):
+            ....
+        Invalid: Enter a value from: 1; 2
+        >>> dc2 = dc(hideDict=True)
+        >>> dc2.hideDict
+        True
+        >>> dc2.dict
+        {1: 'one', 2: 'two'}
+        >>> dc2.to_python(3)
+        Traceback (most recent call last):
+            ....
+        Invalid: Choose something
+        >>> dc.from_python('three')
+        Traceback (most recent call last):
+            ....
+        Invalid: Nothing in my dictionary goes by the value 'three'.  Choose one of: 'one'; 'two'
+    """
+
+    messages = dict(
+        keyNotFound=_('Choose something'),
+        chooseKey=_('Enter a value from: %(items)s'),
+        valueNotFound=_('That value is not known'),
+        chooseValue=_('Nothing in my dictionary goes by the value %(value)s.'
+            '  Choose one of: %(items)s'))
+
+    dict = None
+    hideDict = False
+
+    __unpackargs__ = ('dict',)
+
+    def _convert_to_python(self, value, state):
+        try:
+            return self.dict[value]
+        except KeyError:
+            if self.hideDict:
+                raise Invalid(self.message('keyNotFound', state), value, state)
+            else:
+                items = sorted(self.dict)
+                items = '; '.join(map(repr, items))
+                raise Invalid(self.message('chooseKey',
+                    state, items=items), value, state)
+
+    def _convert_from_python(self, value, state):
+        for k, v in self.dict.items():
+            if value == v:
+                return k
+        if self.hideDict:
+            raise Invalid(self.message('valueNotFound', state), value, state)
+        else:
+            items = '; '.join(map(repr, iter(self.dict.values())))
+            raise Invalid(
+                self.message('chooseValue', state,
+                    value=repr(value), items=items), value, state)
+
+
+class IndexListConverter(FancyValidator):
+    """
+    Converts a index (which may be a string like '2') to the value in
+    the given list.
+
+    Examples::
+
+        >>> index = IndexListConverter(['zero', 'one', 'two'])
+        >>> index.to_python(0)
+        'zero'
+        >>> index.from_python('zero')
+        0
+        >>> index.to_python('1')
+        'one'
+        >>> index.to_python(5)
+        Traceback (most recent call last):
+        Invalid: Index out of range
+        >>> index(not_empty=True).to_python(None)
+        Traceback (most recent call last):
+        Invalid: Please enter a value
+        >>> index.from_python('five')
+        Traceback (most recent call last):
+        Invalid: Item 'five' was not found in the list
+    """
+
+    list = None
+
+    __unpackargs__ = ('list',)
+
+    messages = dict(
+        integer=_('Must be an integer index'),
+        outOfRange=_('Index out of range'),
+        notFound=_('Item %(value)s was not found in the list'))
+
+    def _convert_to_python(self, value, state):
+        try:
+            value = int(value)
+        except (ValueError, TypeError):
+            raise Invalid(self.message('integer', state), value, state)
+        try:
+            return self.list[value]
+        except IndexError:
+            raise Invalid(self.message('outOfRange', state), value, state)
+
+    def _convert_from_python(self, value, state):
+        for i, v in enumerate(self.list):
+            if v == value:
+                return i
+        raise Invalid(
+            self.message('notFound', state, value=repr(value)), value, state)
+
+
+class DateValidator(FancyValidator):
+    """
+    Validates that a date is within the given range.  Be sure to call
+    DateConverter first if you aren't expecting mxDateTime input.
+
+    ``earliest_date`` and ``latest_date`` may be functions; if so,
+    they will be called each time before validating.
+
+    ``after_now`` means a time after the current timestamp; note that
+    just a few milliseconds before now is invalid!  ``today_or_after``
+    is more permissive, and ignores hours and minutes.
+
+    Examples::
+
+        >>> from datetime import datetime, timedelta
+        >>> d = DateValidator(earliest_date=datetime(2003, 1, 1))
+        >>> d.to_python(datetime(2004, 1, 1))
+        datetime.datetime(2004, 1, 1, 0, 0)
+        >>> d.to_python(datetime(2002, 1, 1))
+        Traceback (most recent call last):
+            ...
+        Invalid: Date must be after Wednesday, 01 January 2003
+        >>> d.to_python(datetime(2003, 1, 1))
+        datetime.datetime(2003, 1, 1, 0, 0)
+        >>> d = DateValidator(after_now=True)
+        >>> now = datetime.now()
+        >>> d.to_python(now+timedelta(seconds=5)) == now+timedelta(seconds=5)
+        True
+        >>> d.to_python(now-timedelta(days=1))
+        Traceback (most recent call last):
+            ...
+        Invalid: The date must be sometime in the future
+        >>> d.to_python(now+timedelta(days=1)) > now
+        True
+        >>> d = DateValidator(today_or_after=True)
+        >>> d.to_python(now) == now
+        True
+
+    """
+
+    earliest_date = None
+    latest_date = None
+    after_now = False
+    # Like after_now, but just after this morning:
+    today_or_after = False
+    # Use None or 'datetime' for the datetime module in the standard lib,
+    # or 'mxDateTime' to force the mxDateTime module
+    datetime_module = None
+
+    messages = dict(
+        after=_('Date must be after %(date)s'),
+        before=_('Date must be before %(date)s'),
+        # Double %'s, because this will be substituted twice:
+        date_format=_('%%A, %%d %%B %%Y'),
+        future=_('The date must be sometime in the future'))
+
+    def _validate_python(self, value, state):
+        date_format = self.message('date_format', state)
+        if (str is not str  # Python 2
+                and isinstance(date_format, str)):
+            # strftime uses the locale encoding, not Unicode
+            encoding = locale.getlocale(locale.LC_TIME)[1] or 'utf-8'
+            date_format = date_format.encode(encoding)
+        else:
+            encoding = None
+        if self.earliest_date:
+            if callable(self.earliest_date):
+                earliest_date = self.earliest_date()
+            else:
+                earliest_date = self.earliest_date
+            if value < earliest_date:
+                date_formatted = earliest_date.strftime(date_format)
+                if encoding:
+                    date_formatted = date_formatted.decode(encoding)
+                raise Invalid(
+                    self.message('after', state, date=date_formatted),
+                    value, state)
+        if self.latest_date:
+            if callable(self.latest_date):
+                latest_date = self.latest_date()
+            else:
+                latest_date = self.latest_date
+            if value > latest_date:
+                date_formatted = latest_date.strftime(date_format)
+                if encoding:
+                    date_formatted = date_formatted.decode(encoding)
+                raise Invalid(
+                    self.message('before', state, date=date_formatted),
+                    value, state)
+        if self.after_now:
+            dt_mod = import_datetime(self.datetime_module)
+            now = datetime_now(dt_mod)
+            if value < now:
+                date_formatted = now.strftime(date_format)
+                if encoding:
+                    date_formatted = date_formatted.decode(encoding)
+                raise Invalid(
+                    self.message('future', state, date=date_formatted),
+                    value, state)
+        if self.today_or_after:
+            dt_mod = import_datetime(self.datetime_module)
+            now = datetime_now(dt_mod)
+            today = datetime_makedate(dt_mod,
+                                      now.year, now.month, now.day)
+            value_as_date = datetime_makedate(
+                dt_mod, value.year, value.month, value.day)
+            if value_as_date < today:
+                date_formatted = now.strftime(date_format)
+                if encoding:
+                    date_formatted = date_formatted.decode(encoding)
+                raise Invalid(
+                    self.message('future', state, date=date_formatted),
+                    value, state)
+
+
+class Bool(FancyValidator):
+    """
+    Always Valid, returns True or False based on the value and the
+    existance of the value.
+
+    If you want to convert strings like ``'true'`` to booleans, then
+    use ``StringBool``.
+
+    Examples::
+
+        >>> Bool.to_python(0)
+        False
+        >>> Bool.to_python(1)
+        True
+        >>> Bool.to_python('')
+        False
+        >>> Bool.to_python(None)
+        False
+    """
+
+    if_missing = False
+
+    def _convert_to_python(self, value, state):
+        return bool(value)
+
+    _convert_from_python = _convert_to_python
+
+    def empty_value(self, value):
+        return False
+
+
+class RangeValidator(FancyValidator):
+    """This is an abstract base class for Int and Number.
+
+    It verifies that a value is within range.  It accepts min and max
+    values in the constructor.
+
+    (Since this is an abstract base class, the tests are in Int and Number.)
+
+    """
+
+    messages = dict(
+        tooLow=_('Please enter a number that is %(min)s or greater'),
+        tooHigh=_('Please enter a number that is %(max)s or smaller'))
+
+    min = None
+    max = None
+
+    def _validate_python(self, value, state):
+        if self.min is not None:
+            if value < self.min:
+                msg = self.message('tooLow', state, min=self.min)
+                raise Invalid(msg, value, state)
+        if self.max is not None:
+            if value > self.max:
+                msg = self.message('tooHigh', state, max=self.max)
+                raise Invalid(msg, value, state)
+
+
+class Int(RangeValidator):
+    """Convert a value to an integer.
+
+    Example::
+
+        >>> Int.to_python('10')
+        10
+        >>> Int.to_python('ten')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter an integer value
+        >>> Int(min=5).to_python('6')
+        6
+        >>> Int(max=10).to_python('11')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a number that is 10 or smaller
+
+    """
+
+    messages = dict(
+        integer=_('Please enter an integer value'))
+
+    def _convert_to_python(self, value, state):
+        try:
+            return int(value)
+        except (ValueError, TypeError):
+            raise Invalid(self.message('integer', state), value, state)
+
+    _convert_from_python = _convert_to_python
+
+
+class Number(RangeValidator):
+    """Convert a value to a float or integer.
+
+    Tries to convert it to an integer if no information is lost.
+
+    Example::
+
+        >>> Number.to_python('10')
+        10
+        >>> Number.to_python('10.5')
+        10.5
+        >>> Number.to_python('ten')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a number
+        >>> Number.to_python([1.2])
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a number
+        >>> Number(min=5).to_python('6.5')
+        6.5
+        >>> Number(max=10.5).to_python('11.5')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a number that is 10.5 or smaller
+
+    """
+
+    messages = dict(
+        number=_('Please enter a number'))
+
+    def _convert_to_python(self, value, state):
+        try:
+            value = float(value)
+            try:
+                int_value = int(value)
+            except OverflowError:
+                int_value = None
+            if value == int_value:
+                return int_value
+            return value
+        except (ValueError, TypeError):
+            raise Invalid(self.message('number', state), value, state)
+
+
+class ByteString(FancyValidator):
+    """Convert to byte string, treating empty things as the empty string.
+
+    Under Python 2.x you can also use the alias `String` for this validator.
+
+    Also takes a `max` and `min` argument, and the string length must fall
+    in that range.
+
+    Also you may give an `encoding` argument, which will encode any unicode
+    that is found.  Lists and tuples are joined with `list_joiner`
+    (default ``', '``) in ``from_python``.
+
+    ::
+
+        >>> ByteString(min=2).to_python('a')
+        Traceback (most recent call last):
+            ...
+        Invalid: Enter a value 2 characters long or more
+        >>> ByteString(max=10).to_python('xxxxxxxxxxx')
+        Traceback (most recent call last):
+            ...
+        Invalid: Enter a value not more than 10 characters long
+        >>> ByteString().from_python(None)
+        ''
+        >>> ByteString().from_python([])
+        ''
+        >>> ByteString().to_python(None)
+        ''
+        >>> ByteString(min=3).to_python(None)
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a value
+        >>> ByteString(min=1).to_python('')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a value
+
+    """
+
+    min = None
+    max = None
+    not_empty = None
+    encoding = None
+    list_joiner = ', '
+
+    messages = dict(
+        tooLong=_('Enter a value not more than %(max)i characters long'),
+        tooShort=_('Enter a value %(min)i characters long or more'))
+
+    def __initargs__(self, new_attrs):
+        if self.not_empty is None and self.min:
+            self.not_empty = True
+
+    def _convert_to_python(self, value, state):
+        if value is None:
+            value = ''
+        elif not isinstance(value, str):
+            try:
+                value = bytes(value)
+            except UnicodeEncodeError:
+                value = str(value)
+        if self.encoding is not None and isinstance(value, str):
+            value = value.encode(self.encoding)
+        return value
+
+    def _convert_from_python(self, value, state):
+        if value is None:
+            value = ''
+        elif not isinstance(value, str):
+            if isinstance(value, (list, tuple)):
+                value = self.list_joiner.join(
+                    self._convert_from_python(v, state) for v in value)
+            try:
+                value = str(value)
+            except UnicodeEncodeError:
+                value = str(value)
+        if self.encoding is not None and isinstance(value, str):
+            value = value.encode(self.encoding)
+        if self.strip:
+            value = value.strip()
+        return value
+
+    def _validate_other(self, value, state):
+        if self.max is None and self.min is None:
+            return
+        if value is None:
+            value = ''
+        elif not isinstance(value, str):
+            try:
+                value = str(value)
+            except UnicodeEncodeError:
+                value = str(value)
+        if self.max is not None and len(value) > self.max:
+            raise Invalid(
+                self.message('tooLong', state, max=self.max), value, state)
+        if self.min is not None and len(value) < self.min:
+            raise Invalid(
+                self.message('tooShort', state, min=self.min), value, state)
+
+    def empty_value(self, value):
+        return ''
+
+
+class UnicodeString(ByteString):
+    """Convert things to unicode string.
+
+    This is implemented as a specialization of the ByteString class.
+
+    Under Python 3.x you can also use the alias `String` for this validator.
+
+    In addition to the String arguments, an encoding argument is also
+    accepted. By default the encoding will be utf-8. You can overwrite
+    this using the encoding parameter. You can also set inputEncoding
+    and outputEncoding differently. An inputEncoding of None means
+    "do not decode", an outputEncoding of None means "do not encode".
+
+    All converted strings are returned as Unicode strings.
+
+    ::
+
+        >>> UnicodeString().to_python(None)
+        u''
+        >>> UnicodeString().to_python([])
+        u''
+        >>> UnicodeString(encoding='utf-7').to_python('Ni Ni Ni')
+        u'Ni Ni Ni'
+
+    """
+    encoding = 'utf-8'
+    inputEncoding = NoDefault
+    outputEncoding = NoDefault
+    messages = dict(
+        badEncoding=_('Invalid data or incorrect encoding'))
+
+    def __init__(self, **kw):
+        ByteString.__init__(self, **kw)
+        if self.inputEncoding is NoDefault:
+            self.inputEncoding = self.encoding
+        if self.outputEncoding is NoDefault:
+            self.outputEncoding = self.encoding
+
+    def _convert_to_python(self, value, state):
+        if not value:
+            return ''
+        if isinstance(value, str):
+            return value
+        if not isinstance(value, str):
+            if hasattr(value, '__unicode__'):
+                value = str(value)
+                return value
+            if not (str is str  # Python 3
+                    and isinstance(value, bytes) and self.inputEncoding):
+                value = str(value)
+        if self.inputEncoding and not isinstance(value, str):
+            try:
+                value = str(value, self.inputEncoding)
+            except UnicodeDecodeError:
+                raise Invalid(self.message('badEncoding', state), value, state)
+            except TypeError:
+                raise Invalid(
+                    self.message('badType', state,
+                        type=type(value), value=value), value, state)
+        return value
+
+    def _convert_from_python(self, value, state):
+        if not isinstance(value, str):
+            if hasattr(value, '__unicode__'):
+                value = str(value)
+            else:
+                value = str(value)
+        if self.outputEncoding and isinstance(value, str):
+            value = value.encode(self.outputEncoding)
+        return value
+
+    def empty_value(self, value):
+        return ''
+
+
+# Provide proper alias for native strings
+
+String = UnicodeString if str is str else ByteString
+
+
+class Set(FancyValidator):
+    """
+    This is for when you think you may return multiple values for a
+    certain field.
+
+    This way the result will always be a list, even if there's only
+    one result.  It's equivalent to ForEach(convert_to_list=True).
+
+    If you give ``use_set=True``, then it will return an actual
+    ``set`` object.
+
+    ::
+
+       >>> Set.to_python(None)
+       []
+       >>> Set.to_python('this')
+       ['this']
+       >>> Set.to_python(('this', 'that'))
+       ['this', 'that']
+       >>> s = Set(use_set=True)
+       >>> s.to_python(None)
+       set([])
+       >>> s.to_python('this')
+       set(['this'])
+       >>> s.to_python(('this',))
+       set(['this'])
+    """
+
+    use_set = False
+
+    if_missing = ()
+    accept_iterator = True
+
+    def _convert_to_python(self, value, state):
+        if self.use_set:
+            if isinstance(value, set):
+                return value
+            elif isinstance(value, (list, tuple)):
+                return set(value)
+            elif value is None:
+                return set()
+            else:
+                return set([value])
+        else:
+            if isinstance(value, list):
+                return value
+            elif isinstance(value, set):
+                return list(value)
+            elif isinstance(value, tuple):
+                return list(value)
+            elif value is None:
+                return []
+            else:
+                return [value]
+
+    def empty_value(self, value):
+        if self.use_set:
+            return set()
+        else:
+            return []
+
+
+class Email(FancyValidator):
+    r"""
+    Validate an email address.
+
+    If you pass ``resolve_domain=True``, then it will try to resolve
+    the domain name to make sure it's valid.  This takes longer, of
+    course. You must have the `dnspython <http://www.dnspython.org/>`__ modules
+    installed to look up DNS (MX and A) records.
+
+    ::
+
+        >>> e = Email()
+        >>> e.to_python(' test@foo.com ')
+        'test@foo.com'
+        >>> e.to_python('test')
+        Traceback (most recent call last):
+            ...
+        Invalid: An email address must contain a single @
+        >>> e.to_python('test@foobar')
+        Traceback (most recent call last):
+            ...
+        Invalid: The domain portion of the email address is invalid (the portion after the @: foobar)
+        >>> e.to_python('test@foobar.com.5')
+        Traceback (most recent call last):
+            ...
+        Invalid: The domain portion of the email address is invalid (the portion after the @: foobar.com.5)
+        >>> e.to_python('test@foo..bar.com')
+        Traceback (most recent call last):
+            ...
+        Invalid: The domain portion of the email address is invalid (the portion after the @: foo..bar.com)
+        >>> e.to_python('test@.foo.bar.com')
+        Traceback (most recent call last):
+            ...
+        Invalid: The domain portion of the email address is invalid (the portion after the @: .foo.bar.com)
+        >>> e.to_python('nobody@xn--m7r7ml7t24h.com')
+        'nobody@xn--m7r7ml7t24h.com'
+        >>> e.to_python('o*reilly@test.com')
+        'o*reilly@test.com'
+        >>> e = Email(resolve_domain=True)
+        >>> e.resolve_domain
+        True
+        >>> e.to_python('doesnotexist@colorstudy.com')
+        'doesnotexist@colorstudy.com'
+        >>> e.to_python('test@nyu.edu')
+        'test@nyu.edu'
+        >>> # NOTE: If you do not have dnspython installed this example won't work:
+        >>> e.to_python('test@thisdomaindoesnotexistithinkforsure.com')
+        Traceback (most recent call last):
+            ...
+        Invalid: The domain of the email address does not exist (the portion after the @: thisdomaindoesnotexistithinkforsure.com)
+        >>> e.to_python('test@google.com')
+        u'test@google.com'
+        >>> e = Email(not_empty=False)
+        >>> e.to_python('')
+
+    """
+
+    resolve_domain = False
+    resolve_timeout = 10  # timeout in seconds when resolving domains
+
+    usernameRE = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
+    domainRE = re.compile(r'''
+        ^(?:[a-z0-9][a-z0-9\-]{,62}\.)+        # subdomain
+        (?:[a-z]{2,63}|xn--[a-z0-9\-]{2,59})$  # top level domain
+    ''', re.I | re.VERBOSE)
+
+    messages = dict(
+        empty=_('Please enter an email address'),
+        noAt=_('An email address must contain a single @'),
+        badUsername=_('The username portion of the email address is invalid'
+            ' (the portion before the @: %(username)s)'),
+        socketError=_('An error occured when trying to connect to the server:'
+            ' %(error)s'),
+        badDomain=_('The domain portion of the email address is invalid'
+            ' (the portion after the @: %(domain)s)'),
+        domainDoesNotExist=_('The domain of the email address does not exist'
+            ' (the portion after the @: %(domain)s)'))
+
+    def __init__(self, *args, **kw):
+        FancyValidator.__init__(self, *args, **kw)
+        if self.resolve_domain:
+            if not have_dns:
+                warnings.warn(
+                    "dnspython <http://www.dnspython.org/> is not installed on"
+                    " your system (or the dns.resolver package cannot be found)."
+                    " I cannot resolve domain names in addresses")
+                raise ImportError("no module named dns.resolver")
+
+    def _validate_python(self, value, state):
+        if not value:
+            raise Invalid(self.message('empty', state), value, state)
+        value = value.strip()
+        splitted = value.split('@', 1)
+        try:
+            username, domain = splitted
+        except ValueError:
+            raise Invalid(self.message('noAt', state), value, state)
+        if not self.usernameRE.search(username):
+            raise Invalid(
+                self.message('badUsername', state, username=username),
+                value, state)
+        try:
+            idna_domain = [idna.ToASCII(p) for p in domain.split('.')]
+            if str is str:  # Python 3
+                idna_domain = [p.decode('ascii') for p in idna_domain]
+            idna_domain = '.'.join(idna_domain)
+        except UnicodeError:
+            # UnicodeError: label empty or too long
+            # This exception might happen if we have an invalid domain name part
+            # (for example test@.foo.bar.com)
+            raise Invalid(
+                self.message('badDomain', state, domain=domain),
+                value, state)
+        if not self.domainRE.search(idna_domain):
+            raise Invalid(
+                self.message('badDomain', state, domain=domain),
+                value, state)
+        if self.resolve_domain:
+            assert have_dns, "dnspython should be available"
+            global socket
+            if socket is None:
+                import socket
+            try:
+                try:
+                    dns.resolver.query(domain, 'MX')
+                except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
+                    try:
+                        dns.resolver.query(domain, 'A')
+                    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
+                        raise Invalid(
+                            self.message('domainDoesNotExist',
+                                state, domain=domain), value, state)
+            except (socket.error, dns.exception.DNSException) as e:
+                raise Invalid(
+                    self.message('socketError', state, error=e), value, state)
+
+    def _convert_to_python(self, value, state):
+        return value.strip()
+
+
+class URL(FancyValidator):
+    """
+    Validate a URL, either http://... or https://.  If check_exists
+    is true, then we'll actually make a request for the page.
+
+    If add_http is true, then if no scheme is present we'll add
+    http://
+
+    ::
+
+        >>> u = URL(add_http=True)
+        >>> u.to_python('foo.com')
+        'http://foo.com'
+        >>> u.to_python('http://hahaha.ha/bar.html')
+        'http://hahaha.ha/bar.html'
+        >>> u.to_python('http://xn--m7r7ml7t24h.com')
+        'http://xn--m7r7ml7t24h.com'
+        >>> u.to_python('http://xn--c1aay4a.xn--p1ai')
+        'http://xn--c1aay4a.xn--p1ai'
+        >>> u.to_python('http://foo.com/test?bar=baz&fleem=morx')
+        'http://foo.com/test?bar=baz&fleem=morx'
+        >>> u.to_python('http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest')
+        'http://foo.com/login?came_from=http%3A%2F%2Ffoo.com%2Ftest'
+        >>> u.to_python('http://foo.com:8000/test.html')
+        'http://foo.com:8000/test.html'
+        >>> u.to_python('http://foo.com/something\\nelse')
+        Traceback (most recent call last):
+            ...
+        Invalid: That is not a valid URL
+        >>> u.to_python('https://test.com')
+        'https://test.com'
+        >>> u.to_python('http://test')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must provide a full domain name (like test.com)
+        >>> u.to_python('http://test..com')
+        Traceback (most recent call last):
+            ...
+        Invalid: That is not a valid URL
+        >>> u = URL(add_http=False, check_exists=True)
+        >>> u.to_python('http://google.com')
+        'http://google.com'
+        >>> u.to_python('google.com')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must start your URL with http://, https://, etc
+        >>> u.to_python('http://www.formencode.org/does/not/exist/page.html')
+        Traceback (most recent call last):
+            ...
+        Invalid: The server responded that the page could not be found
+        >>> u.to_python('http://this.domain.does.not.exist.example.org/test.html')
+        ... # doctest: +ELLIPSIS
+        Traceback (most recent call last):
+            ...
+        Invalid: An error occured when trying to connect to the server: ...
+
+    If you want to allow addresses without a TLD (e.g., ``localhost``) you can do::
+
+        >>> URL(require_tld=False).to_python('http://localhost')
+        'http://localhost'
+
+    By default, internationalized domain names (IDNA) in Unicode will be
+    accepted and encoded to ASCII using Punycode (as described in RFC 3490).
+    You may set allow_idna to False to change this behavior::
+
+        >>> URL(allow_idna=True).to_python(
+        ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
+        'http://xn--c1aay4a.xn--p1ai'
+        >>> URL(allow_idna=True, add_http=True).to_python(
+        ... '\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
+        'http://xn--c1aay4a.xn--p1ai'
+        >>> URL(allow_idna=False).to_python(
+        ... 'http://\\u0433\\u0443\\u0433\\u043b.\\u0440\\u0444')
+        Traceback (most recent call last):
+        ...
+        Invalid: That is not a valid URL
+
+    """
+
+    add_http = True
+    allow_idna = True
+    check_exists = False
+    require_tld = True
+
+    url_re = re.compile(r'''
+        ^(http|https)://
+        (?:[%:\w]*@)?                              # authenticator
+        (?:                                        # ip or domain
+        (?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]?))|
+        (?P<domain>[a-z0-9][a-z0-9\-]{,62}\.)*     # subdomain
+        (?P<tld>[a-z]{2,63}|xn--[a-z0-9\-]{2,59})  # top level domain
+        )
+        (?::[0-9]{1,5})?                           # port
+        # files/delims/etc
+        (?P<path>/[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]*)?
+        $
+    ''', re.I | re.VERBOSE)
+
+    scheme_re = re.compile(r'^[a-zA-Z]+:')
+
+    messages = dict(
+        noScheme=_('You must start your URL with http://, https://, etc'),
+        badURL=_('That is not a valid URL'),
+        httpError=_('An error occurred when trying to access the URL:'
+            ' %(error)s'),
+        socketError=_('An error occured when trying to connect to the server:'
+            ' %(error)s'),
+        notFound=_('The server responded that the page could not be found'),
+        status=_('The server responded with a bad status code (%(status)s)'),
+        noTLD=_('You must provide a full domain name (like %(domain)s.com)'))
+
+    def _convert_to_python(self, value, state):
+        value = value.strip()
+        if self.add_http:
+            if not self.scheme_re.search(value):
+                value = 'http://' + value
+        if self.allow_idna:
+            value = self._encode_idna(value)
+        match = self.scheme_re.search(value)
+        if not match:
+            raise Invalid(self.message('noScheme', state), value, state)
+        value = match.group(0).lower() + value[len(match.group(0)):]
+        match = self.url_re.search(value)
+        if not match:
+            raise Invalid(self.message('badURL', state), value, state)
+        if self.require_tld and not match.group('domain'):
+            raise Invalid(
+                self.message('noTLD', state, domain=match.group('tld')),
+                value, state)
+        if self.check_exists and value.startswith(('http://', 'https://')):
+            self._check_url_exists(value, state)
+        return value
+
+    def _encode_idna(self, url):
+        global urlparse
+        if urlparse is None:
+            import urllib.parse
+        try:
+            scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
+                url)
+        except ValueError:
+            return url
+        try:
+            netloc = netloc.encode('idna')
+            if str is str:  # Python 3
+                netloc = netloc.decode('ascii')
+            return str(urllib.parse.urlunparse((scheme, netloc,
+                path, params, query, fragment)))
+        except UnicodeError:
+            return url
+
+    def _check_url_exists(self, url, state):
+        global httplib, urlparse, socket
+        if httplib is None:
+            import http.client
+        if urlparse is None:
+            import urllib.parse
+        if socket is None:
+            import socket
+        scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(
+            url, 'http')
+        if scheme == 'https':
+            ConnClass = http.client.HTTPSConnection
+        else:
+            ConnClass = http.client.HTTPConnection
+        try:
+            conn = ConnClass(netloc)
+            if params:
+                path += ';' + params
+            if query:
+                path += '?' + query
+            conn.request('HEAD', path)
+            res = conn.getresponse()
+        except http.client.HTTPException as e:
+            raise Invalid(
+                self.message('httpError', state, error=e), state, url)
+        except socket.error as e:
+            raise Invalid(
+                self.message('socketError', state, error=e), state, url)
+        else:
+            if res.status == 404:
+                raise Invalid(
+                    self.message('notFound', state), state, url)
+            if not 200 <= res.status < 500:
+                raise Invalid(
+                    self.message('status', state, status=res.status),
+                    state, url)
+
+
+class XRI(FancyValidator):
+    r"""
+    Validator for XRIs.
+
+    It supports both i-names and i-numbers, of the first version of the XRI
+    standard.
+
+    ::
+
+        >>> inames = XRI(xri_type="i-name")
+        >>> inames.to_python("   =John.Smith ")
+        '=John.Smith'
+        >>> inames.to_python("@Free.Software.Foundation")
+        '@Free.Software.Foundation'
+        >>> inames.to_python("Python.Software.Foundation")
+        Traceback (most recent call last):
+            ...
+        Invalid: The type of i-name is not defined; it may be either individual or organizational
+        >>> inames.to_python("http://example.org")
+        Traceback (most recent call last):
+            ...
+        Invalid: The type of i-name is not defined; it may be either individual or organizational
+        >>> inames.to_python("=!2C43.1A9F.B6F6.E8E6")
+        Traceback (most recent call last):
+            ...
+        Invalid: "!2C43.1A9F.B6F6.E8E6" is an invalid i-name
+        >>> iname_with_schema = XRI(True, xri_type="i-name")
+        >>> iname_with_schema.to_python("=Richard.Stallman")
+        'xri://=Richard.Stallman'
+        >>> inames.to_python("=John Smith")
+        Traceback (most recent call last):
+            ...
+        Invalid: "John Smith" is an invalid i-name
+        >>> inumbers = XRI(xri_type="i-number")
+        >>> inumbers.to_python("!!1000!de21.4536.2cb2.8074")
+        '!!1000!de21.4536.2cb2.8074'
+        >>> inumbers.to_python("@!1000.9554.fabd.129c!2847.df3c")
+        '@!1000.9554.fabd.129c!2847.df3c'
+
+    """
+
+    iname_valid_pattern = re.compile(r"""
+    ^
+    [\w]+                  # A global alphanumeric i-name
+    (\.[\w]+)*             # An i-name with dots
+    (\*[\w]+(\.[\w]+)*)*   # A community i-name
+    $
+    """, re.VERBOSE | re.UNICODE)
+
+    iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
+    """@cvar: These characters must not be at the beggining of the i-name"""
+
+    inumber_pattern = re.compile(r"""
+    ^
+    (
+    [=@]!       # It's a personal or organization i-number
+    |
+    !!          # It's a network i-number
+    )
+    [\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3}       # A global i-number
+    (![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})*   # Zero or more sub i-numbers
+    $
+    """, re.VERBOSE | re.IGNORECASE)
+
+    messages = dict(
+        noType=_('The type of i-name is not defined;'
+            ' it may be either individual or organizational'),
+        repeatedChar=_('Dots and dashes may not be repeated consecutively'),
+        badIname=_('"%(iname)s" is an invalid i-name'),
+        badInameStart=_('i-names may not start with numbers'
+            ' nor punctuation marks'),
+        badInumber=_('"%(inumber)s" is an invalid i-number'),
+        badType=_('The XRI must be a string (not a %(type)s: %(value)r)'),
+        badXri=_('"%(xri_type)s" is not a valid type of XRI'))
+
+    def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
+        """Create an XRI validator.
+
+        @param add_xri: Should the schema be added if not present?
+            Officially it's optional.
+        @type add_xri: C{bool}
+        @param xri_type: What type of XRI should be validated?
+            Possible values: C{i-name} or C{i-number}.
+        @type xri_type: C{str}
+
+        """
+        self.add_xri = add_xri
+        assert xri_type in ('i-name', 'i-number'), (
+            'xri_type must be "i-name" or "i-number"')
+        self.xri_type = xri_type
+        super(XRI, self).__init__(**kwargs)
+
+    def _convert_to_python(self, value, state):
+        """Prepend the 'xri://' schema if needed and remove trailing spaces"""
+        value = value.strip()
+        if self.add_xri and not value.startswith('xri://'):
+            value = 'xri://' + value
+        return value
+
+    def _validate_python(self, value, state=None):
+        """Validate an XRI
+
+        @raise Invalid: If at least one of the following conditions in met:
+            - C{value} is not a string.
+            - The XRI is not a personal, organizational or network one.
+            - The relevant validator (i-name or i-number) considers the XRI
+                is not valid.
+
+        """
+        if not isinstance(value, str):
+            raise Invalid(
+                self.message('badType', state,
+                    type=str(type(value)), value=value), value, state)
+
+        # Let's remove the schema, if any
+        if value.startswith('xri://'):
+            value = value[6:]
+
+        if not value[0] in ('@', '=') and not (
+                self.xri_type == 'i-number' and value[0] == '!'):
+            raise Invalid(self.message('noType', state), value, state)
+
+        if self.xri_type == 'i-name':
+            self._validate_iname(value, state)
+        else:
+            self._validate_inumber(value, state)
+
+    def _validate_iname(self, iname, state):
+        """Validate an i-name"""
+        # The type is not required here:
+        iname = iname[1:]
+        if '..' in iname or '--' in iname:
+            raise Invalid(self.message('repeatedChar', state), iname, state)
+        if self.iname_invalid_start.match(iname):
+            raise Invalid(self.message('badInameStart', state), iname, state)
+        if not self.iname_valid_pattern.match(iname) or '_' in iname:
+            raise Invalid(
+                self.message('badIname', state, iname=iname), iname, state)
+
+    def _validate_inumber(self, inumber, state):
+        """Validate an i-number"""
+        if not self.__class__.inumber_pattern.match(inumber):
+            raise Invalid(
+                self.message('badInumber', state,
+                    inumber=inumber, value=inumber), inumber, state)
+
+
+class OpenId(FancyValidator):
+    r"""
+    OpenId validator.
+
+    ::
+        >>> v = OpenId(add_schema=True)
+        >>> v.to_python(' example.net ')
+        'http://example.net'
+        >>> v.to_python('@TurboGears')
+        'xri://@TurboGears'
+        >>> w = OpenId(add_schema=False)
+        >>> w.to_python(' example.net ')
+        Traceback (most recent call last):
+        ...
+        Invalid: "example.net" is not a valid OpenId (it is neither an URL nor an XRI)
+        >>> w.to_python('!!1000')
+        '!!1000'
+        >>> w.to_python('look@me.com')
+        Traceback (most recent call last):
+        ...
+        Invalid: "look@me.com" is not a valid OpenId (it is neither an URL nor an XRI)
+
+    """
+
+    messages = dict(
+        badId=_('"%(id)s" is not a valid OpenId'
+            ' (it is neither an URL nor an XRI)'))
+
+    def __init__(self, add_schema=False, **kwargs):
+        """Create an OpenId validator.
+
+        @param add_schema: Should the schema be added if not present?
+        @type add_schema: C{bool}
+
+        """
+        self.url_validator = URL(add_http=add_schema)
+        self.iname_validator = XRI(add_schema, xri_type="i-name")
+        self.inumber_validator = XRI(add_schema, xri_type="i-number")
+
+    def _convert_to_python(self, value, state):
+        value = value.strip()
+        try:
+            return self.url_validator.to_python(value, state)
+        except Invalid:
+            try:
+                return self.iname_validator.to_python(value, state)
+            except Invalid:
+                try:
+                    return self.inumber_validator.to_python(value, state)
+                except Invalid:
+                    pass
+        # It's not an OpenId!
+        raise Invalid(self.message('badId', state, id=value), value, state)
+
+    def _validate_python(self, value, state):
+        self._convert_to_python(value, state)
+
+
+def StateProvince(*kw, **kwargs):
+    deprecation_warning("please use formencode.national.USStateProvince")
+    from formencode.national import USStateProvince
+    return USStateProvince(*kw, **kwargs)
+
+
+def PhoneNumber(*kw, **kwargs):
+    deprecation_warning("please use formencode.national.USPhoneNumber")
+    from formencode.national import USPhoneNumber
+    return USPhoneNumber(*kw, **kwargs)
+
+
+def IPhoneNumberValidator(*kw, **kwargs):
+    deprecation_warning(
+        "please use formencode.national.InternationalPhoneNumber")
+    from formencode.national import InternationalPhoneNumber
+    return InternationalPhoneNumber(*kw, **kwargs)
+
+
+class FieldStorageUploadConverter(FancyValidator):
+    """
+    Handles cgi.FieldStorage instances that are file uploads.
+
+    This doesn't do any conversion, but it can detect empty upload
+    fields (which appear like normal fields, but have no filename when
+    no upload was given).
+    """
+    def _convert_to_python(self, value, state=None):
+        if isinstance(value, cgi.FieldStorage):
+            if getattr(value, 'filename', None):
+                return value
+            raise Invalid('invalid', value, state)
+        else:
+            return value
+
+    def is_empty(self, value):
+        if isinstance(value, cgi.FieldStorage):
+            return not bool(getattr(value, 'filename', None))
+        return FancyValidator.is_empty(self, value)
+
+
+class FileUploadKeeper(FancyValidator):
+    """
+    Takes two inputs (a dictionary with keys ``static`` and
+    ``upload``) and converts them into one value on the Python side (a
+    dictionary with ``filename`` and ``content`` keys).  The upload
+    takes priority over the static value.  The filename may be None if
+    it can't be discovered.
+
+    Handles uploads of both text and ``cgi.FieldStorage`` upload
+    values.
+
+    This is basically for use when you have an upload field, and you
+    want to keep the upload around even if the rest of the form
+    submission fails.  When converting *back* to the form submission,
+    there may be extra values ``'original_filename'`` and
+    ``'original_content'``, which may want to use in your form to show
+    the user you still have their content around.
+
+    To use this, make sure you are using variabledecode, then use
+    something like::
+
+      <input type="file" name="myfield.upload">
+      <input type="hidden" name="myfield.static">
+
+    Then in your scheme::
+
+      class MyScheme(Scheme):
+          myfield = FileUploadKeeper()
+
+    Note that big file uploads mean big hidden fields, and lots of
+    bytes passed back and forth in the case of an error.
+    """
+
+    upload_key = 'upload'
+    static_key = 'static'
+
+    def _convert_to_python(self, value, state):
+        upload = value.get(self.upload_key)
+        static = value.get(self.static_key, '').strip()
+        filename = content = None
+        if isinstance(upload, cgi.FieldStorage):
+            filename = upload.filename
+            content = upload.value
+        elif isinstance(upload, str) and upload:
+            filename = None
+            # @@: Should this encode upload if it is unicode?
+            content = upload
+        if not content and static:
+            filename, content = static.split(None, 1)
+            filename = '' if filename == '-' else filename.decode('base64')
+            content = content.decode('base64')
+        return {'filename': filename, 'content': content}
+
+    def _convert_from_python(self, value, state):
+        filename = value.get('filename', '')
+        content = value.get('content', '')
+        if filename or content:
+            result = self.pack_content(filename, content)
+            return {self.upload_key: '',
+                    self.static_key: result,
+                    'original_filename': filename,
+                    'original_content': content}
+        else:
+            return {self.upload_key: '',
+                    self.static_key: ''}
+
+    def pack_content(self, filename, content):
+        enc_filename = self.base64encode(filename) or '-'
+        enc_content = (content or '').encode('base64')
+        result = '%s %s' % (enc_filename, enc_content)
+        return result
+
+
+class DateConverter(FancyValidator):
+    """
+    Validates and converts a string date, like mm/yy, dd/mm/yy,
+    dd-mm-yy, etc.  Using ``month_style`` you can support
+    the three general styles ``mdy`` = ``us`` = ``mm/dd/yyyy``,
+    ``dmy`` = ``euro`` = ``dd/mm/yyyy`` and
+    ``ymd`` = ``iso`` = ``yyyy/mm/dd``.
+
+    Accepts English month names, also abbreviated.  Returns value as a
+    datetime object (you can get mx.DateTime objects if you use
+    ``datetime_module='mxDateTime'``).  Two year dates are assumed to
+    be within 1950-2020, with dates from 21-49 being ambiguous and
+    signaling an error.
+
+    Use accept_day=False if you just want a month/year (like for a
+    credit card expiration date).
+
+    ::
+
+        >>> d = DateConverter()
+        >>> d.to_python('12/3/09')
+        datetime.date(2009, 12, 3)
+        >>> d.to_python('12/3/2009')
+        datetime.date(2009, 12, 3)
+        >>> d.to_python('2/30/04')
+        Traceback (most recent call last):
+            ...
+        Invalid: That month only has 29 days
+        >>> d.to_python('13/2/05')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a month from 1 to 12
+        >>> d.to_python('1/1/200')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a four-digit year after 1899
+
+    If you change ``month_style`` you can get European-style dates::
+
+        >>> d = DateConverter(month_style='dd/mm/yyyy')
+        >>> date = d.to_python('12/3/09')
+        >>> date
+        datetime.date(2009, 3, 12)
+        >>> d.from_python(date)
+        '12/03/2009'
+    """
+
+    # set to False if you want only month and year
+    accept_day = True
+    # allowed month styles: 'mdy' = 'us', 'dmy' = 'euro', 'ymd' = 'iso'
+    # also allowed: 'mm/dd/yyyy', 'dd/mm/yyyy', 'yyyy/mm/dd'
+    month_style = 'mdy'
+    # preferred separator for reverse conversion: '/', '.' or '-'
+    separator = '/'
+
+    # Use 'datetime' to force the Python datetime module, or
+    # 'mxDateTime' to force the mxDateTime module (None means use
+    # datetime, or if not present mxDateTime)
+    datetime_module = None
+
+    _month_names = {
+        'jan': 1, 'january': 1,
+        'feb': 2, 'febuary': 2,
+        'mar': 3, 'march': 3,
+        'apr': 4, 'april': 4,
+        'may': 5,
+        'jun': 6, 'june': 6,
+        'jul': 7, 'july': 7,
+        'aug': 8, 'august': 8,
+        'sep': 9, 'sept': 9, 'september': 9,
+        'oct': 10, 'october': 10,
+        'nov': 11, 'november': 11,
+        'dec': 12, 'december': 12,
+        }
+
+    _date_re = dict(
+        dmy=re.compile(
+            r'^\s*(\d\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
+                % '|'.join(_month_names), re.I),
+        mdy=re.compile(
+            r'^\s*(\d\d?|%s)[\-\./\\](\d\d?)[\-\./\\](\d\d\d?\d?)\s*$'
+                % '|'.join(_month_names), re.I),
+        ymd=re.compile(
+            r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)[\-\./\\](\d\d?)\s*$'
+                % '|'.join(_month_names), re.I),
+        my=re.compile(
+            r'^\s*(\d\d?|%s)[\-\./\\](\d\d\d?\d?)\s*$'
+                % '|'.join(_month_names), re.I),
+        ym=re.compile(
+            r'^\s*(\d\d\d?\d?)[\-\./\\](\d\d?|%s)\s*$'
+                % '|'.join(_month_names), re.I))
+
+    _formats = dict(d='%d', m='%m', y='%Y')
+
+    _human_formats = dict(d=_('DD'), m=_('MM'), y=_('YYYY'))
+
+    # Feb. should be leap-year aware (but mxDateTime does catch that)
+    _monthDays = {
+        1: 31, 2: 29, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31,
+        9: 30, 10: 31, 11: 30, 12: 31}
+
+    messages = dict(
+        badFormat=_('Please enter the date in the form %(format)s'),
+        monthRange=_('Please enter a month from 1 to 12'),
+        invalidDay=_('Please enter a valid day'),
+        dayRange=_('That month only has %(days)i days'),
+        invalidDate=_('That is not a valid day (%(exception)s)'),
+        unknownMonthName=_('Unknown month name: %(month)s'),
+        invalidYear=_('Please enter a number for the year'),
+        fourDigitYear=_('Please enter a four-digit year after 1899'),
+        wrongFormat=_('Please enter the date in the form %(format)s'))
+
+    def __init__(self, *args, **kw):
+        super(DateConverter, self).__init__(*args, **kw)
+        month_style = (self.month_style or DateConverter.month_style).lower()
+        accept_day = bool(self.accept_day)
+        self.accept_day = self.accept_day
+        if month_style in ('mdy',
+                'md', 'mm/dd/yyyy', 'mm/dd', 'us', 'american'):
+            month_style = 'mdy'
+        elif month_style in ('dmy',
+                'dm', 'dd/mm/yyyy', 'dd/mm', 'euro', 'european'):
+            month_style = 'dmy'
+        elif month_style in ('ymd',
+                'ym', 'yyyy/mm/dd', 'yyyy/mm', 'iso', 'china', 'chinese'):
+            month_style = 'ymd'
+        else:
+            raise TypeError('Bad month_style: %r' % month_style)
+        self.month_style = month_style
+        separator = self.separator
+        if not separator or separator == 'auto':
+            separator = dict(mdy='/', dmy='.', ymd='-')[month_style]
+        elif separator not in ('-', '.', '/', '\\'):
+            raise TypeError('Bad separator: %r' % separator)
+        self.separator = separator
+        self.format = separator.join(self._formats[part]
+            for part in month_style if part != 'd' or accept_day)
+        self.human_format = separator.join(self._human_formats[part]
+            for part in month_style if part != 'd' or accept_day)
+
+    def _convert_to_python(self, value, state):
+        self.assert_string(value, state)
+        month_style = self.month_style
+        if not self.accept_day:
+            month_style = 'ym' if month_style == 'ymd' else 'my'
+        match = self._date_re[month_style].search(value)
+        if not match:
+            raise Invalid(
+                self.message('badFormat', state,
+                    format=self.human_format), value, state)
+        groups = match.groups()
+        if self.accept_day:
+            if month_style == 'mdy':
+                month, day, year = groups
+            elif month_style == 'dmy':
+                day, month, year = groups
+            else:
+                year, month, day = groups
+            day = int(day)
+            if not 1 <= day <= 31:
+                raise Invalid(self.message('invalidDay', state), value, state)
+        else:
+            day = 1
+            if month_style == 'my':
+                month, year = groups
+            else:
+                year, month = groups
+        month = self.make_month(month, state)
+        if not 1 <= month <= 12:
+            raise Invalid(self.message('monthRange', state), value, state)
+        if self._monthDays[month] < day:
+            raise Invalid(
+                self.message('dayRange', state,
+                    days=self._monthDays[month]), value, state)
+        year = self.make_year(year, state)
+        dt_mod = import_datetime(self.datetime_module)
+        try:
+            return datetime_makedate(dt_mod, year, month, day)
+        except ValueError as v:
+            raise Invalid(
+                self.message('invalidDate', state,
+                    exception=str(v)), value, state)
+
+    def make_month(self, value, state):
+        try:
+            return int(value)
+        except ValueError:
+            try:
+                return self._month_names[value.lower().strip()]
+            except KeyError:
+                raise Invalid(
+                    self.message('unknownMonthName', state,
+                        month=value), value, state)
+
+    def make_year(self, year, state):
+        try:
+            year = int(year)
+        except ValueError:
+            raise Invalid(self.message('invalidYear', state), year, state)
+        if year <= 20:
+            year += 2000
+        elif 50 <= year < 100:
+            year += 1900
+        if 20 < year < 50 or 99 < year < 1900:
+            raise Invalid(self.message('fourDigitYear', state), year, state)
+        return year
+
+    def _convert_from_python(self, value, state):
+        if self.if_empty is not NoDefault and not value:
+            return ''
+        return value.strftime(self.format)
+
+
+class TimeConverter(FancyValidator):
+    """
+    Converts times in the format HH:MM:SSampm to (h, m, s).
+    Seconds are optional.
+
+    For ampm, set use_ampm = True.  For seconds, use_seconds = True.
+    Use 'optional' for either of these to make them optional.
+
+    Examples::
+
+        >>> tim = TimeConverter()
+        >>> tim.to_python('8:30')
+        (8, 30)
+        >>> tim.to_python('20:30')
+        (20, 30)
+        >>> tim.to_python('30:00')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must enter an hour in the range 0-23
+        >>> tim.to_python('13:00pm')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must enter an hour in the range 1-12
+        >>> tim.to_python('12:-1')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must enter a minute in the range 0-59
+        >>> tim.to_python('12:02pm')
+        (12, 2)
+        >>> tim.to_python('12:02am')
+        (0, 2)
+        >>> tim.to_python('1:00PM')
+        (13, 0)
+        >>> tim.from_python((13, 0))
+        '13:00:00'
+        >>> tim2 = tim(use_ampm=True, use_seconds=False)
+        >>> tim2.from_python((13, 0))
+        '1:00pm'
+        >>> tim2.from_python((0, 0))
+        '12:00am'
+        >>> tim2.from_python((12, 0))
+        '12:00pm'
+
+    Examples with ``datetime.time``::
+
+        >>> v = TimeConverter(use_datetime=True)
+        >>> a = v.to_python('18:00')
+        >>> a
+        datetime.time(18, 0)
+        >>> b = v.to_python('30:00')
+        Traceback (most recent call last):
+            ...
+        Invalid: You must enter an hour in the range 0-23
+        >>> v2 = TimeConverter(prefer_ampm=True, use_datetime=True)
+        >>> v2.from_python(a)
+        '6:00:00pm'
+        >>> v3 = TimeConverter(prefer_ampm=True,
+        ...                    use_seconds=False, use_datetime=True)
+        >>> a = v3.to_python('18:00')
+        >>> a
+        datetime.time(18, 0)
+        >>> v3.from_python(a)
+        '6:00pm'
+        >>> a = v3.to_python('18:00:00')
+        Traceback (most recent call last):
+            ...
+        Invalid: You may not enter seconds
+    """
+
+    use_ampm = 'optional'
+    prefer_ampm = False
+    use_seconds = 'optional'
+    use_datetime = False
+    # This can be set to make it prefer mxDateTime:
+    datetime_module = None
+
+    messages = dict(
+        noAMPM=_('You must indicate AM or PM'),
+        tooManyColon=_('There are too many :\'s'),
+        noSeconds=_('You may not enter seconds'),
+        secondsRequired=_('You must enter seconds'),
+        minutesRequired=_('You must enter minutes (after a :)'),
+        badNumber=_('The %(part)s value you gave is not a number: %(number)r'),
+        badHour=_('You must enter an hour in the range %(range)s'),
+        badMinute=_('You must enter a minute in the range 0-59'),
+        badSecond=_('You must enter a second in the range 0-59'))
+
+    def _convert_to_python(self, value, state):
+        result = self._to_python_tuple(value, state)
+        if self.use_datetime:
+            dt_mod = import_datetime(self.datetime_module)
+            time_class = datetime_time(dt_mod)
+            return time_class(*result)
+        else:
+            return result
+
+    def _to_python_tuple(self, value, state):
+        time = value.strip()
+        explicit_ampm = False
+        if self.use_ampm:
+            last_two = time[-2:].lower()
+            if last_two not in ('am', 'pm'):
+                if self.use_ampm != 'optional':
+                    raise Invalid(self.message('noAMPM', state), value, state)
+                offset = 0
+            else:
+                explicit_ampm = True
+                offset = 12 if last_two == 'pm' else 0
+                time = time[:-2]
+        else:
+            offset = 0
+        parts = time.split(':', 3)
+        if len(parts) > 3:
+            raise Invalid(self.message('tooManyColon', state), value, state)
+        if len(parts) == 3 and not self.use_seconds:
+            raise Invalid(self.message('noSeconds', state), value, state)
+        if (len(parts) == 2
+                and self.use_seconds and self.use_seconds != 'optional'):
+            raise Invalid(self.message('secondsRequired', state), value, state)
+        if len(parts) == 1:
+            raise Invalid(self.message('minutesRequired', state), value, state)
+        try:
+            hour = int(parts[0])
+        except ValueError:
+            raise Invalid(
+                self.message('badNumber', state,
+                    number=parts[0], part='hour'), value, state)
+        if explicit_ampm:
+            if not 1 <= hour <= 12:
+                raise Invalid(
+                    self.message('badHour', state,
+                        number=hour, range='1-12'), value, state)
+            if hour == 12 and offset == 12:
+                # 12pm == 12
+                pass
+            elif hour == 12 and offset == 0:
+                # 12am == 0
+                hour = 0
+            else:
+                hour += offset
+        else:
+            if not 0 <= hour < 24:
+                raise Invalid(
+                    self.message('badHour', state,
+                        number=hour, range='0-23'), value, state)
+        try:
+            minute = int(parts[1])
+        except ValueError:
+            raise Invalid(
+                self.message('badNumber', state,
+                    number=parts[1], part='minute'), value, state)
+        if not 0 <= minute < 60:
+            raise Invalid(
+                self.message('badMinute', state, number=minute),
+                value, state)
+        if len(parts) == 3:
+            try:
+                second = int(parts[2])
+            except ValueError:
+                raise Invalid(
+                    self.message('badNumber', state,
+                        number=parts[2], part='second'), value, state)
+            if not 0 <= second < 60:
+                raise Invalid(
+                    self.message('badSecond', state, number=second),
+                    value, state)
+        else:
+            second = None
+        if second is None:
+            return (hour, minute)
+        else:
+            return (hour, minute, second)
+
+    def _convert_from_python(self, value, state):
+        if isinstance(value, str):
+            return value
+        if hasattr(value, 'hour'):
+            hour, minute = value.hour, value.minute
+            second = value.second
+        elif len(value) == 3:
+            hour, minute, second = value
+        elif len(value) == 2:
+            hour, minute = value
+            second = 0
+        ampm = ''
+        if (self.use_ampm == 'optional' and self.prefer_ampm) or (
+                self.use_ampm and self.use_ampm != 'optional'):
+            ampm = 'am'
+            if hour > 12:
+                hour -= 12
+                ampm = 'pm'
+            elif hour == 12:
+                ampm = 'pm'
+            elif hour == 0:
+                hour = 12
+        if self.use_seconds:
+            return '%i:%02i:%02i%s' % (hour, minute, second, ampm)
+        else:
+            return '%i:%02i%s' % (hour, minute, ampm)
+
+
+def PostalCode(*kw, **kwargs):
+    deprecation_warning("please use formencode.national.USPostalCode")
+    from formencode.national import USPostalCode
+    return USPostalCode(*kw, **kwargs)
+
+
+class StripField(FancyValidator):
+    """
+    Take a field from a dictionary, removing the key from the dictionary.
+
+    ``name`` is the key.  The field value and a new copy of the dictionary
+    with that field removed are returned.
+
+    >>> StripField('test').to_python({'a': 1, 'test': 2})
+    (2, {'a': 1})
+    >>> StripField('test').to_python({})
+    Traceback (most recent call last):
+        ...
+    Invalid: The name 'test' is missing
+
+    """
+
+    __unpackargs__ = ('name',)
+
+    messages = dict(
+        missing=_('The name %(name)s is missing'))
+
+    def _convert_to_python(self, valueDict, state):
+        v = valueDict.copy()
+        try:
+            field = v.pop(self.name)
+        except KeyError:
+            raise Invalid(
+                self.message('missing', state, name=repr(self.name)),
+                valueDict, state)
+        return field, v
+
+    def is_empty(self, value):
+        # empty dictionaries don't really apply here
+        return False
+
+
+class StringBool(FancyValidator):  # originally from TurboGears 1
+    """
+    Converts a string to a boolean.
+
+    Values like 'true' and 'false' are considered True and False,
+    respectively; anything in ``true_values`` is true, anything in
+    ``false_values`` is false, case-insensitive).  The first item of
+    those lists is considered the preferred form.
+
+    ::
+
+        >>> s = StringBool()
+        >>> s.to_python('yes'), s.to_python('no')
+        (True, False)
+        >>> s.to_python(1), s.to_python('N')
+        (True, False)
+        >>> s.to_python('ye')
+        Traceback (most recent call last):
+            ...
+        Invalid: Value should be 'true' or 'false'
+    """
+
+    true_values = ['true', 't', 'yes', 'y', 'on', '1']
+    false_values = ['false', 'f', 'no', 'n', 'off', '0']
+
+    messages = dict(
+        string=_('Value should be %(true)r or %(false)r'))
+
+    def _convert_to_python(self, value, state):
+        if isinstance(value, str):
+            value = value.strip().lower()
+            if value in self.true_values:
+                return True
+            if not value or value in self.false_values:
+                return False
+            raise Invalid(
+                self.message('string', state,
+                    true=self.true_values[0], false=self.false_values[0]),
+                value, state)
+        return bool(value)
+
+    def _convert_from_python(self, value, state):
+        return (self.true_values if value else self.false_values)[0]
+
+# Should deprecate:
+StringBoolean = StringBool
+
+
+class SignedString(FancyValidator):
+    """
+    Encodes a string into a signed string, and base64 encodes both the
+    signature string and a random nonce.
+
+    It is up to you to provide a secret, and to keep the secret handy
+    and consistent.
+    """
+
+    messages = dict(
+        malformed=_('Value does not contain a signature'),
+        badsig=_('Signature is not correct'))
+
+    secret = None
+    nonce_length = 4
+
+    def _convert_to_python(self, value, state):
+        global sha1
+        if not sha1:
+            from hashlib import sha1
+        assert self.secret is not None, "You must give a secret"
+        parts = value.split(None, 1)
+        if not parts or len(parts) == 1:
+            raise Invalid(self.message('malformed', state), value, state)
+        sig, rest = parts
+        sig = sig.decode('base64')
+        rest = rest.decode('base64')
+        nonce = rest[:self.nonce_length]
+        rest = rest[self.nonce_length:]
+        expected = sha1(str(self.secret) + nonce + rest).digest()
+        if expected != sig:
+            raise Invalid(self.message('badsig', state), value, state)
+        return rest
+
+    def _convert_from_python(self, value, state):
+        global sha1
+        if not sha1:
+            from hashlib import sha1
+        nonce = self.make_nonce()
+        value = str(value)
+        digest = sha1(self.secret + nonce + value).digest()
+        return self.encode(digest) + ' ' + self.encode(nonce + value)
+
+    def encode(self, value):
+        return value.encode('base64').strip().replace('\n', '')
+
+    def make_nonce(self):
+        global random
+        if not random:
+            import random
+        return ''.join(chr(random.randrange(256))
+            for _i in range(self.nonce_length))
+
+
+class IPAddress(FancyValidator):
+    """
+    Formencode validator to check whether a string is a correct IP address.
+
+    Examples::
+
+        >>> ip = IPAddress()
+        >>> ip.to_python('127.0.0.1')
+        '127.0.0.1'
+        >>> ip.to_python('299.0.0.1')
+        Traceback (most recent call last):
+            ...
+        Invalid: The octets must be within the range of 0-255 (not '299')
+        >>> ip.to_python('192.168.0.1/1')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a valid IP address (a.b.c.d)
+        >>> ip.to_python('asdf')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a valid IP address (a.b.c.d)
+    """
+
+    messages = dict(
+        badFormat=_('Please enter a valid IP address (a.b.c.d)'),
+        leadingZeros=_('The octets must not have leading zeros'),
+        illegalOctets=_('The octets must be within the range of 0-255'
+            ' (not %(octet)r)'))
+
+    leading_zeros = False
+
+    def _validate_python(self, value, state=None):
+        try:
+            if not value:
+                raise ValueError
+            octets = value.split('.', 5)
+            # Only 4 octets?
+            if len(octets) != 4:
+                raise ValueError
+            # Correct octets?
+            for octet in octets:
+                if octet.startswith('0') and octet != '0':
+                    if not self.leading_zeros:
+                        raise Invalid(
+                            self.message('leadingZeros', state), value, state)
+                    # strip zeros so this won't be an octal number
+                    octet = octet.lstrip('0')
+                if not 0 <= int(octet) < 256:
+                    raise Invalid(
+                        self.message('illegalOctets', state, octet=octet),
+                        value, state)
+        # Splitting faild: wrong syntax
+        except ValueError:
+            raise Invalid(self.message('badFormat', state), value, state)
+
+
+class CIDR(IPAddress):
+    """
+    Formencode validator to check whether a string is in correct CIDR
+    notation (IP address, or IP address plus /mask).
+
+    Examples::
+
+        >>> cidr = CIDR()
+        >>> cidr.to_python('127.0.0.1')
+        '127.0.0.1'
+        >>> cidr.to_python('299.0.0.1')
+        Traceback (most recent call last):
+            ...
+        Invalid: The octets must be within the range of 0-255 (not '299')
+        >>> cidr.to_python('192.168.0.1/1')
+        Traceback (most recent call last):
+            ...
+        Invalid: The network size (bits) must be within the range of 8-32 (not '1')
+        >>> cidr.to_python('asdf')
+        Traceback (most recent call last):
+            ...
+        Invalid: Please enter a valid IP address (a.b.c.d) or IP network (a.b.c.d/e)
+    """
+
+    messages = dict(IPAddress._messages,
+        badFormat=_('Please enter a valid IP address (a.b.c.d)'
+            ' or IP network (a.b.c.d/e)'),
+        illegalBits=_('The network size (bits) must be within the range'
+            ' of 8-32 (not %(bits)r)'))
+
+    def _validate_python(self, value, state):
+        try:
+            # Split into octets and bits
+            if '/' in value:  # a.b.c.d/e
+                addr, bits = value.split('/')
+            else:  # a.b.c.d
+                addr, bits = value, 32
+            # Use IPAddress validator to validate the IP part
+            IPAddress._validate_python(self, addr, state)
+            # Bits (netmask) correct?
+            if not 8 <= int(bits) <= 32:
+                raise Invalid(
+                    self.message('illegalBits', state, bits=bits),
+                    value, state)
+        # Splitting faild: wrong syntax
+        except ValueError:
+            raise Invalid(self.message('badFormat', state), value, state)
+
+
+class MACAddress(FancyValidator):
+    """
+    Formencode validator to check whether a string is a correct hardware
+    (MAC) address.
+
+    Examples::
+
+        >>> mac = MACAddress()
+        >>> mac.to_python('aa:bb:cc:dd:ee:ff')
+        'aabbccddeeff'
+        >>> mac.to_python('aa:bb:cc:dd:ee:ff:e')
+        Traceback (most recent call last):
+            ...
+        Invalid: A MAC address must contain 12 digits and A-F; the value you gave has 13 characters
+        >>> mac.to_python('aa:bb:cc:dd:ee:fx')
+        Traceback (most recent call last):
+            ...
+        Invalid: MAC addresses may only contain 0-9 and A-F (and optionally :), not 'x'
+        >>> MACAddress(add_colons=True).to_python('aabbccddeeff')
+        'aa:bb:cc:dd:ee:ff'
+    """
+
+    strip = True
+    valid_characters = '0123456789abcdefABCDEF'
+    add_colons = False
+
+    messages = dict(
+        badLength=_('A MAC address must contain 12 digits and A-F;'
+            ' the value you gave has %(length)s characters'),
+        badCharacter=_('MAC addresses may only contain 0-9 and A-F'
+            ' (and optionally :), not %(char)r'))
+
+    def _convert_to_python(self, value, state):
+        address = value.replace(':', '').lower()  # remove colons
+        if len(address) != 12:
+            raise Invalid(
+                self.message('badLength', state,
+                    length=len(address)), address, state)
+        for char in address:
+            if char not in self.valid_characters:
+                raise Invalid(
+                    self.message('badCharacter', state,
+                        char=char), address, state)
+        if self.add_colons:
+            address = '%s:%s:%s:%s:%s:%s' % (
+                address[0:2], address[2:4], address[4:6],
+                address[6:8], address[8:10], address[10:12])
+        return address
+
+    _convert_from_python = _convert_to_python
+
+
+class FormValidator(FancyValidator):
+    """
+    A FormValidator is something that can be chained with a Schema.
+
+    Unlike normal chaining the FormValidator can validate forms that
+    aren't entirely valid.
+
+    The important method is .validate(), of course.  It gets passed a
+    dictionary of the (processed) values from the form.  If you have
+    .validate_partial_form set to True, then it will get the incomplete
+    values as well -- check with the "in" operator if the form was able
+    to process any particular field.
+
+    Anyway, .validate() should return a string or a dictionary.  If a
+    string, it's an error message that applies to the whole form.  If
+    not, then it should be a dictionary of fieldName: errorMessage.
+    The special key "form" is the error message for the form as a whole
+    (i.e., a string is equivalent to {"form": string}).
+
+    Returns None on no errors.
+    """
+
+    validate_partial_form = False
+
+    validate_partial_python = None
+    validate_partial_other = None
+
+    def is_empty(self, value):
+        return False
+
+    def field_is_empty(self, value):
+        return is_empty(value)
+
+
+class RequireIfMissing(FormValidator):
+    """
+    Require one field based on another field being present or missing.
+
+    This validator is applied to a form, not an individual field (usually
+    using a Schema's ``pre_validators`` or ``chained_validators``) and is
+    available under both names ``RequireIfMissing`` and ``RequireIfPresent``.
+
+    If you provide a ``missing`` value (a string key name) then
+    if that field is missing the field must be entered.
+    This gives you an either/or situation.
+
+    If you provide a ``present`` value (another string key name) then
+    if that field is present, the required field must also be present.
+
+    ::
+
+        >>> from formencode import validators
+        >>> v = validators.RequireIfPresent('phone_type', present='phone')
+        >>> v.to_python(dict(phone_type='', phone='510 420  4577'))
+        Traceback (most recent call last):
+            ...
+        Invalid: You must give a value for phone_type
+        >>> v.to_python(dict(phone=''))
+        {'phone': ''}
+
+    Note that if you have a validator on the optionally-required
+    field, you should probably use ``if_missing=None``.  This way you
+    won't get an error from the Schema about a missing value.  For example::
+
+        class PhoneInput(Schema):
+            phone = PhoneNumber()
+            phone_type = String(if_missing=None)
+            chained_validators = [RequireIfPresent('phone_type', present='phone')]
+    """
+
+    # Field that potentially is required:
+    required = None
+    # If this field is missing, then it is required:
+    missing = None
+    # If this field is present, then it is required:
+    present = None
+
+    __unpackargs__ = ('required',)
+
+    def _convert_to_python(self, value_dict, state):
+        is_empty = self.field_is_empty
+        if is_empty(value_dict.get(self.required)) and (
+                (self.missing and is_empty(value_dict.get(self.missing))) or
+                (self.present and not is_empty(value_dict.get(self.present)))):
+            raise Invalid(
+                _('You must give a value for %s') % self.required,
+                value_dict, state,
+                error_dict={self.required:
+                    Invalid(self.message('empty', state),
+                        value_dict.get(self.required), state)})
+        return value_dict
+
+RequireIfPresent = RequireIfMissing
+
+class RequireIfMatching(FormValidator):
+    """
+    Require a list of fields based on the value of another field.
+
+    This validator is applied to a form, not an individual field (usually
+    using a Schema's ``pre_validators`` or ``chained_validators``).
+
+    You provide a field name, an expected value and a list of required fields
+    (a list of string key names). If the value of the field, if present,
+    matches the value of ``expected_value``, then the validator will raise an
+    ``Invalid`` exception for every field in ``required_fields`` that is
+    missing.
+
+    ::
+
+        >>> from formencode import validators
+        >>> v = validators.RequireIfMatching('phone_type', expected_value='mobile', required_fields=['mobile'])
+        >>> v.to_python(dict(phone_type='mobile'))
+        Traceback (most recent call last):
+            ...
+        formencode.api.Invalid: You must give a value for mobile
+        >>> v.to_python(dict(phone_type='someothervalue'))
+        {'phone_type': 'someothervalue'}
+    """
+
+    # Field that we will check for its value:
+    field = None
+    # Value that the field shall have
+    expected_value = None
+    # If this field is present, then these fields are required:
+    required_fields = []
+
+    __unpackargs__ = ('field', 'expected_value')
+
+    def _convert_to_python(self, value_dict, state):
+        is_empty = self.field_is_empty
+
+        if self.field in value_dict and value_dict.get(self.field) == self.expected_value:
+            for required_field in self.required_fields:
+                if required_field not in value_dict or is_empty(value_dict.get(required_field)):
+                    raise Invalid(
+                        _('You must give a value for %s') % required_field,
+                        value_dict, state,
+                        error_dict={required_field:
+                            Invalid(self.message('empty', state),
+                                value_dict.get(required_field), state)})
+        return value_dict
+
+class FieldsMatch(FormValidator):
+    """
+    Tests that the given fields match, i.e., are identical.  Useful
+    for password+confirmation fields.  Pass the list of field names in
+    as `field_names`.
+
+    ::
+
+        >>> f = FieldsMatch('pass', 'conf')
+        >>> sorted(f.to_python({'pass': 'xx', 'conf': 'xx'}).items())
+        [('conf', 'xx'), ('pass', 'xx')]
+        >>> f.to_python({'pass': 'xx', 'conf': 'yy'})
+        Traceback (most recent call last):
+            ...
+        Invalid: conf: Fields do not match
+    """
+
+    show_match = False
+    field_names = None
+    validate_partial_form = True
+
+    __unpackargs__ = ('*', 'field_names')
+
+    messages = dict(
+        invalid=_('Fields do not match (should be %(match)s)'),
+        invalidNoMatch=_('Fields do not match'),
+        notDict=_('Fields should be a dictionary'))
+
+    def __init__(self, *args, **kw):
+        super(FieldsMatch, self).__init__(*args, **kw)
+        if len(self.field_names) < 2:
+            raise TypeError('FieldsMatch() requires at least two field names')
+
+    def validate_partial(self, field_dict, state):
+        for name in self.field_names:
+            if name not in field_dict:
+                return
+        self._validate_python(field_dict, state)
+
+    def _validate_python(self, field_dict, state):
+        try:
+            ref = field_dict[self.field_names[0]]
+        except TypeError:
+            # Generally because field_dict isn't a dict
+            raise Invalid(self.message('notDict', state), field_dict, state)
+        except KeyError:
+            ref = ''
+        errors = {}
+        for name in self.field_names[1:]:
+            if field_dict.get(name, '') != ref:
+                if self.show_match:
+                    errors[name] = self.message('invalid', state,
+                                                match=ref)
+                else:
+                    errors[name] = self.message('invalidNoMatch', state)
+        if errors:
+            error_list = sorted(errors.items())
+            error_message = '<br>\n'.join(
+                '%s: %s' % (name, value) for name, value in error_list)
+            raise Invalid(error_message, field_dict, state, error_dict=errors)
+
+
+class CreditCardValidator(FormValidator):
+    """
+    Checks that credit card numbers are valid (if not real).
+
+    You pass in the name of the field that has the credit card
+    type and the field with the credit card number.  The credit
+    card type should be one of "visa", "mastercard", "amex",
+    "dinersclub", "discover", "jcb".
+
+    You must check the expiration date yourself (there is no
+    relation between CC number/types and expiration dates).
+
+    ::
+
+        >>> cc = CreditCardValidator()
+        >>> sorted(cc.to_python({'ccType': 'visa', 'ccNumber': '4111111111111111'}).items())
+        [('ccNumber', '4111111111111111'), ('ccType', 'visa')]
+        >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111111'})
+        Traceback (most recent call last):
+            ...
+        Invalid: ccNumber: You did not enter a valid number of digits
+        >>> cc.to_python({'ccType': 'visa', 'ccNumber': '411111111111112'})
+        Traceback (most recent call last):
+            ...
+        Invalid: ccNumber: You did not enter a valid number of digits
+        >>> cc().to_python({})
+        Traceback (most recent call last):
+            ...
+        Invalid: The field ccType is missing
+    """
+
+    validate_partial_form = True
+
+    cc_type_field = 'ccType'
+    cc_number_field = 'ccNumber'
+
+    __unpackargs__ = ('cc_type_field', 'cc_number_field')
+
+    messages = dict(
+        notANumber=_('Please enter only the number, no other characters'),
+        badLength=_('You did not enter a valid number of digits'),
+        invalidNumber=_('That number is not valid'),
+        missing_key=_('The field %(key)s is missing'))
+
+    def validate_partial(self, field_dict, state):
+        if not field_dict.get(self.cc_type_field, None) \
+           or not field_dict.get(self.cc_number_field, None):
+            return None
+        self._validate_python(field_dict, state)
+
+    def _validate_python(self, field_dict, state):
+        errors = self._validateReturn(field_dict, state)
+        if errors:
+            error_list = sorted(errors.items())
+            raise Invalid(
+                '<br>\n'.join('%s: %s' % (name, value)
+                    for name, value in error_list),
+                field_dict, state, error_dict=errors)
+
+    def _validateReturn(self, field_dict, state):
+        for field in self.cc_type_field, self.cc_number_field:
+            if field not in field_dict:
+                raise Invalid(
+                    self.message('missing_key', state, key=field),
+                    field_dict, state)
+        ccType = field_dict[self.cc_type_field].lower().strip()
+        number = field_dict[self.cc_number_field].strip()
+        number = number.replace(' ', '')
+        number = number.replace('-', '')
+        try:
+            int(number)
+        except ValueError:
+            return {self.cc_number_field: self.message('notANumber', state)}
+        assert ccType in self._cardInfo, (
+            "I can't validate that type of credit card")
+        foundValid = False
+        validLength = False
+        for prefix, length in self._cardInfo[ccType]:
+            if len(number) == length:
+                validLength = True
+                if number.startswith(prefix):
+                    foundValid = True
+                    break
+        if not validLength:
+            return {self.cc_number_field: self.message('badLength', state)}
+        if not foundValid:
+            return {self.cc_number_field: self.message('invalidNumber', state)}
+        if not self._validateMod10(number):
+            return {self.cc_number_field: self.message('invalidNumber', state)}
+        return None
+
+    def _validateMod10(self, s):
+        """Check string with the mod 10 algorithm (aka "Luhn formula")."""
+        checksum, factor = 0, 1
+        for c in reversed(s):
+            for c in str(factor * int(c)):
+                checksum += int(c)
+            factor = 3 - factor
+        return checksum % 10 == 0
+
+    _cardInfo = {
+        "visa": [('4', 16),
+                 ('4', 13)],
+        "mastercard": [('51', 16),
+                       ('52', 16),
+                       ('53', 16),
+                       ('54', 16),
+                       ('55', 16)],
+        "discover": [('6011', 16)],
+        "amex": [('34', 15),
+                 ('37', 15)],
+        "dinersclub": [('300', 14),
+                       ('301', 14),
+                       ('302', 14),
+                       ('303', 14),
+                       ('304', 14),
+                       ('305', 14),
+                       ('36', 14),
+                       ('38', 14)],
+        "jcb": [('3', 16),
+                ('2131', 15),
+                ('1800', 15)],
+            }
+
+
+class CreditCardExpires(FormValidator):
+    """
+    Checks that credit card expiration date is valid relative to
+    the current date.
+
+    You pass in the name of the field that has the credit card
+    expiration month and the field with the credit card expiration
+    year.
+
+    ::
+
+        >>> ed = CreditCardExpires()
+        >>> sorted(ed.to_python({'ccExpiresMonth': '11', 'ccExpiresYear': '2250'}).items())
+        [('ccExpiresMonth', '11'), ('ccExpiresYear', '2250')]
+        >>> ed.to_python({'ccExpiresMonth': '10', 'ccExpiresYear': '2005'})
+        Traceback (most recent call last):
+            ...
+        Invalid: ccExpiresMonth: Invalid Expiration Date<br>
+        ccExpiresYear: Invalid Expiration Date
+    """
+
+    validate_partial_form = True
+
+    cc_expires_month_field = 'ccExpiresMonth'
+    cc_expires_year_field = 'ccExpiresYear'
+
+    __unpackargs__ = ('cc_expires_month_field', 'cc_expires_year_field')
+
+    datetime_module = None
+
+    messages = dict(
+        notANumber=_('Please enter numbers only for month and year'),
+        invalidNumber=_('Invalid Expiration Date'))
+
+    def validate_partial(self, field_dict, state):
+        if not field_dict.get(self.cc_expires_month_field, None) \
+           or not field_dict.get(self.cc_expires_year_field, None):
+            return None
+        self._validate_python(field_dict, state)
+
+    def _validate_python(self, field_dict, state):
+        errors = self._validateReturn(field_dict, state)
+        if errors:
+            error_list = sorted(errors.items())
+            raise Invalid(
+                '<br>\n'.join('%s: %s' % (name, value)
+                    for name, value in error_list),
+                field_dict, state, error_dict=errors)
+
+    def _validateReturn(self, field_dict, state):
+        ccExpiresMonth = str(field_dict[self.cc_expires_month_field]).strip()
+        ccExpiresYear = str(field_dict[self.cc_expires_year_field]).strip()
+
+        try:
+            ccExpiresMonth = int(ccExpiresMonth)
+            ccExpiresYear = int(ccExpiresYear)
+            dt_mod = import_datetime(self.datetime_module)
+            now = datetime_now(dt_mod)
+            today = datetime_makedate(dt_mod, now.year, now.month, now.day)
+            next_month = ccExpiresMonth % 12 + 1
+            next_month_year = ccExpiresYear
+            if next_month == 1:
+                next_month_year += 1
+            expires_date = datetime_makedate(
+                dt_mod, next_month_year, next_month, 1)
+            assert expires_date > today
+        except ValueError:
+            return {self.cc_expires_month_field:
+                        self.message('notANumber', state),
+                    self.cc_expires_year_field:
+                        self.message('notANumber', state)}
+        except AssertionError:
+            return {self.cc_expires_month_field:
+                        self.message('invalidNumber', state),
+                    self.cc_expires_year_field:
+                        self.message('invalidNumber', state)}
+
+
+class CreditCardSecurityCode(FormValidator):
+    """
+    Checks that credit card security code has the correct number
+    of digits for the given credit card type.
+
+    You pass in the name of the field that has the credit card
+    type and the field with the credit card security code.
+
+    ::
+
+        >>> code = CreditCardSecurityCode()
+        >>> sorted(code.to_python({'ccType': 'visa', 'ccCode': '111'}).items())
+        [('ccCode', '111'), ('ccType', 'visa')]
+        >>> code.to_python({'ccType': 'visa', 'ccCode': '1111'})
+        Traceback (most recent call last):
+            ...
+        Invalid: ccCode: Invalid credit card security code length
+    """
+
+    validate_partial_form = True
+
+    cc_type_field = 'ccType'
+    cc_code_field = 'ccCode'
+
+    __unpackargs__ = ('cc_type_field', 'cc_code_field')
+
+    messages = dict(
+        notANumber=_('Please enter numbers only for credit card security code'),
+        badLength=_('Invalid credit card security code length'))
+
+    def validate_partial(self, field_dict, state):
+        if (not field_dict.get(self.cc_type_field, None)
+                or not field_dict.get(self.cc_code_field, None)):
+            return None
+        self._validate_python(field_dict, state)
+
+    def _validate_python(self, field_dict, state):
+        errors = self._validateReturn(field_dict, state)
+        if errors:
+            error_list = sorted(errors.items())
+            raise Invalid(
+                '<br>\n'.join('%s: %s' % (name, value)
+                    for name, value in error_list),
+                field_dict, state, error_dict=errors)
+
+    def _validateReturn(self, field_dict, state):
+        ccType = str(field_dict[self.cc_type_field]).strip()
+        ccCode = str(field_dict[self.cc_code_field]).strip()
+        try:
+            int(ccCode)
+        except ValueError:
+            return {self.cc_code_field: self.message('notANumber', state)}
+        length = self._cardInfo[ccType]
+        if len(ccCode) != length:
+            return {self.cc_code_field: self.message('badLength', state)}
+
+    # key = credit card type, value = length of security code
+    _cardInfo = dict(visa=3, mastercard=3, discover=3, amex=4)
+
+
+def validators():
+    """Return the names of all validators in this module."""
+    return [name for name, value in globals().items()
+        if isinstance(value, type) and issubclass(value, Validator)]
+
+__all__ = ['Invalid'] + validators()
+