]> git.phdru.name Git - sqlconvert.git/commitdiff
Build(GHActions): Use `checkout@v4` instead of outdated `v2` master
authorOleg Broytman <phd@phdru.name>
Tue, 2 Jan 2024 01:14:49 +0000 (04:14 +0300)
committerOleg Broytman <phd@phdru.name>
Tue, 2 Jan 2024 01:14:49 +0000 (04:14 +0300)
24 files changed:
.coveragerc [deleted file]
.github/workflows/run-tests.yaml [new file with mode: 0644]
.gitignore
.travis.yml [deleted file]
README.rst
appveyor.yml [deleted file]
demo/demo-process.py
devscripts/CI/validators.py [deleted file]
devscripts/prerelease-tag [new file with mode: 0755]
devscripts/release
devscripts/requirements/requirements.txt
devscripts/requirements/requirements_tests.txt
devscripts/requirements/requirements_tox.txt
docs/index.rst
docs/news.rst
scripts/mysql2sql
setup.cfg
setup.py
sqlconvert/__version__.py
sqlconvert/process_tokens.py
tests/mysql2sql/test.out2 [moved from tests/mysql2sql/test.out with 100% similarity]
tests/mysql2sql/test.out3 [new file with mode: 0644]
tests/test_process_tokens.py
tox.ini

diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644 (file)
index bb87c9d..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-[run]
-omit =
-   sqlconvert/__init__.py
-   sqlconvert/__version__.py
diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
new file mode 100644 (file)
index 0000000..25c08a0
--- /dev/null
@@ -0,0 +1,91 @@
+name: Run tests
+
+on: [push, pull_request]
+
+jobs:
+  run-tests:
+    env:
+      not_in_conda: "[]"
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        include:
+        - os: ubuntu-latest
+          os-name: Linux
+          pip-cache-path: ~/.cache/pip
+        - os: windows-latest
+          os-name: w32
+          pip-cache-path: ~\AppData\Local\pip\Cache
+
+    name: Python ${{ matrix.python-version }} @ ${{ matrix.os-name }}
+    runs-on: ${{ matrix.os }}
+
+    steps:
+
+      # Setup PostgreSQL
+      - uses: ankane/setup-postgres@v1
+      - name: Setup Postgres user @ Linux
+        run: |
+          sudo -u postgres psql --command="ALTER USER runner CREATEDB PASSWORD 'test'"
+        if: ${{ runner.os == 'Linux' }}
+      - name: Setup Postgres user @ w32
+        run: |
+          psql --command="CREATE USER runner CREATEDB PASSWORD 'test'"
+        if: ${{ runner.os == 'Windows' }}
+
+      # Setup Python/pip
+      - uses: actions/checkout@v4
+      - uses: s-weigand/setup-conda@v1
+        with:
+          conda-channels: conda-forge
+          python-version: ${{ matrix.python-version }}
+        if: ${{ !contains(fromJSON(env.not_in_conda), matrix.python-version) }}
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+        if: ${{ contains(fromJSON(env.not_in_conda), matrix.python-version) }}
+      - name: Cache pip
+        uses: actions/cache@v3
+        with:
+          path: ${{ matrix.pip-cache-path }}
+          key: ${{ runner.os }}-pip
+
+      # Setup tox
+      - name: Install dependencies
+        run: |
+          python --version
+          python -m pip || python -m ensurepip --default-pip --upgrade
+          python -m pip install --upgrade pip setuptools wheel
+          pip --version
+          pip install --upgrade virtualenv "tox >= 3.15, < 4"
+      - name: Set TOXENV
+        run: |
+          import os, sys
+          ld_library_path = None
+          pyver = '%d%d' % tuple(sys.version_info[:2])
+          toxenv = 'py%s' % pyver
+          toxenv += ',py%s-sqlite' % pyver
+          if os.name == 'nt':
+            toxenv += '-w32'
+          toxenv += ',py%s-postgres' % pyver
+          if os.name == 'posix':
+            if pyver == '27':  # Python 2.7 on Linux requires `$LD_LIBRARY_PATH`
+              ld_library_path = os.path.join(
+                os.path.dirname(os.path.dirname(sys.executable)), 'lib')
+            toxenv += ',py%s-flake8' % pyver
+          elif os.name == 'nt':
+            toxenv += '-w32'
+          with open(os.environ['GITHUB_ENV'], 'a') as f:
+            if ld_library_path:
+              f.write('LD_LIBRARY_PATH=' + ld_library_path + '\n')
+            f.write('TOXENV=' + toxenv + '\n')
+            f.write('PGPASSWORD=test\n')
+        shell: python
+
+      - name: Run tox
+        run: |
+          python -c "import os; print(os.environ['TOXENV'])"
+          tox --version
+          tox
index 1c1f8db7861fe3f6a1a1428c7a0acfd82a03712a..41980274dc5655c30fa332013ba069057aab7388 100644 (file)
@@ -5,8 +5,6 @@
 /build/
 /data/
 /dist/
-/htmlcov/
-/.coverage
 /MANIFEST
 *.py[co]
 *.tmp
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644 (file)
index 1d1cdf6..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-os: linux
-
-dist: xenial
-
-language: python
-
-python:
-  - "2.7"
-
-cache: pip
-
-addons:
-  apt:
-    packages:
-      - python-psycopg2
-      - python3-psycopg2
-  postgresql: "9.4"
-
-matrix:
-  include:
-  - python: "2.7"
-    env: TOXENV=py27
-  - python: "3.4"
-    env: TOXENV=py34
-  - python: "3.5"
-    env: TOXENV=py35
-  - python: "3.6"
-    env: TOXENV=py36
-  - python: "3.7"
-    env: TOXENV=py37
-  - python: "3.8"
-    env: TOXENV=py38
-  - python: "2.7"
-    env: TOXENV=py27-postgres
-  - python: "3.4"
-    env: TOXENV=py34-postgres
-  - python: "3.5"
-    env: TOXENV=py35-postgres
-  - python: "3.6"
-    env: TOXENV=py36-postgres
-  - python: "3.7"
-    env: TOXENV=py37-postgres
-  - python: "3.8"
-    env: TOXENV=py38-postgres
-  - python: "2.7"
-    env: TOXENV=py27-sqlite
-  - python: "3.4"
-    env: TOXENV=py34-sqlite
-  - python: "3.5"
-    env: TOXENV=py35-sqlite
-  - python: "3.6"
-    env: TOXENV=py36-sqlite
-  - python: "3.7"
-    env: TOXENV=py37-sqlite
-  - python: "3.8"
-    env: TOXENV=py38-sqlite
-  - python: "2.7"
-    env: TOXENV=py27-flake8
-  - python: "3.8"
-    env: TOXENV=py38-flake8
-
-install:
-  - travis_retry pip install --upgrade "pip < 19.1" setuptools tox ppu
-
-script:
-  - tox
-
-before_cache:
-  - remove-old-files.py -o 180 ~/.cache/pip
index 8b2b081615bcdb128fe2c4e55b968f3258141c68..22113ca383a4c19075650d6e9c17edb4fc1b5c74 100644 (file)
@@ -2,7 +2,7 @@ SQL converter.
 
 Author: Oleg Broytman <phd@phdru.name>.
 
-Copyright (C) 2016-2018 PhiloSoft Design.
+Copyright (C) 2016-2024 PhiloSoft Design.
 
 License: GPL.
 
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644 (file)
index 8895632..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-# Test on windows
-# Heavily inspired by Oliver Grisel's appveyor-demo (https://github.com/ogrisel/python-appveyor-demo)
-version: '{branch}-{build}'
-
-cache:
-  - '%LOCALAPPDATA%\pip\Cache'
-
-# Match travis
-clone_depth: 50
-
-services:
-  - postgresql
-
-environment:
-  PGUSER: "postgres"
-  PGPASSWORD: "Password12!"
-
-  matrix:
-    - TOXENV: "py27"
-      PYTHON_VERSION: "2.7"
-      PYTHON_ARCH: "32"
-      PYTHON_HOME: "C:\\Python27"
-    - TOXENV: "py37"
-      PYTHON_VERSION: "3.7"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python37-x64"
-    - TOXENV: "py38"
-      PYTHON_VERSION: "3.8"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python38-x64"
-    - TOXENV: "py27-postgres-w32"
-      PYTHON_VERSION: "2.7"
-      PYTHON_ARCH: "32"
-      PYTHON_HOME: "C:\\Python27"
-      db: postgresql
-    - TOXENV: "py37-postgres-w32"
-      PYTHON_VERSION: "3.7"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python37-x64"
-      db: postgresql
-    - TOXENV: "py38-postgres-w32"
-      PYTHON_VERSION: "3.8"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python38-x64"
-      db: postgresql
-    - TOXENV: "py27-sqlite-w32"
-      PYTHON_VERSION: "2.7"
-      PYTHON_ARCH: "32"
-      PYTHON_HOME: "C:\\Python27"
-    - TOXENV: "py37-sqlite-w32"
-      PYTHON_VERSION: "3.7"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python37-x64"
-    - TOXENV: "py38-sqlite-w32"
-      PYTHON_VERSION: "3.8"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python38-x64"
-    - TOXENV: "py27-flake8"
-      PYTHON_VERSION: "2.7"
-      PYTHON_ARCH: "32"
-      PYTHON_HOME: "C:\\Python27"
-    - TOXENV: "py38-flake8"
-      PYTHON_VERSION: "3.8"
-      PYTHON_ARCH: "64"
-      PYTHON_HOME: "C:\\Python38-x64"
-
-install:
-  # Ensure we use the right python version
-  - "SET PATH=%PYTHON_HOME%;%PYTHON_HOME%\\Scripts;C:\\Program Files\\PostgreSQL\\9.5\\bin;%PATH%"
-  - "SET TOXPYTHON=%PYTHON_HOME%\\python.exe"
-  - "python --version"
-  - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
-  - "python -m pip install --upgrade \"pip < 19.1\" setuptools"
-  - "pip install --upgrade \"tox < 3.1\" ppu"
-  - "pip --version"
-
-# No build step - we don't have C extensions
-build: false
-
-test_script:
-  - "tox"
-
-after_test:
-  - "remove-old-files.py -o 180 %LOCALAPPDATA%\\pip\\Cache"
index a81f93ef1787cf1bae3658fa1a82a1ac757e8365..1fe9809ffd59490f48616c8fa20e7b15950dd822 100755 (executable)
@@ -15,10 +15,10 @@ def process_lines(*lines):
             print("----- -----")
             if find_error(statement):
                 print("ERRORS IN QUERY")
-            for statement in process_statement(statement):
-                print_tokens(statement, encoding='utf-8')
+            for _statement in process_statement(statement):
+                print_tokens(_statement, encoding='utf-8')
                 print()
-                statement._pprint_tree()
+                _statement._pprint_tree()
             print("-----/-----")
     tokens = grouper.close()
     if tokens:
diff --git a/devscripts/CI/validators.py b/devscripts/CI/validators.py
deleted file mode 100644 (file)
index 233d5dd..0000000
+++ /dev/null
@@ -1,3089 +0,0 @@
-## 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()
-
diff --git a/devscripts/prerelease-tag b/devscripts/prerelease-tag
new file mode 100755 (executable)
index 0000000..19531f2
--- /dev/null
@@ -0,0 +1,4 @@
+#! /bin/sh
+
+tag="`python setup.py --version`" &&
+exec git tag --message="Release $tag" --sign $tag
index 81488ede5803aff2339344ebf0ce26b990fbd09b..28ce198b25c5a473d09c2951695670dfa893146f 100755 (executable)
@@ -12,5 +12,5 @@ python setup.py sdist &&
 find build -name '*.py[co]' -delete &&
 python setup.py bdist_wheel --universal &&
 
-twine upload --skip-existing dist/* &&
+twine upload --disable-progress-bar --skip-existing dist/* &&
 exec rm -rf build dist sqlconvert.egg-info
index abe3d944ad92404eba46bb869f69722dcde87511..a5e54e3b25e7f6dbf43fe58c61fe162afcf53afd 100644 (file)
@@ -1,7 +1,4 @@
---install-option=-O2
-
 sqlparse
 SQLObject>=2.2.1; python_version >= '2.7' and python_version < '3.0'
 SQLObject>=3.0.0; python_version >= '3.4'
-m_lib.defenc>=1.0
-m_lib>=3.1
+m_lib.full>=1.0
index 973ffbaca0ae703070c4568c68c0ab24b50e9f0d..e9716a919a87793abf46b8e99a2906244b01d4ed 100644 (file)
@@ -1,6 +1,5 @@
 -r requirements.txt
 
 pytest < 5.0; python_version == '2.7' or python_version == '3.4'
-pytest; python_version >= '3.5'
-pytest-cov
+pytest < 7.0; python_version >= '3.5'
 ppu
index 8c9b6ed3baf9d7daf3562380b33d6a8a0485294a..b72a322f2a34aab552ca614a876db66d6de3ee5b 100644 (file)
@@ -1 +1 @@
-tox >= 2.0, < 3.1
+tox >= 3.15, < 4
index fb4543708a39f3c1c4623deb6e3dbf64e12c14cd..a73e9212e84beed903ffcd9f396ea66e8a819ea6 100644 (file)
@@ -41,7 +41,7 @@ Credits
 
 Created by Oleg Broytman <phd@phdru.name>.
 
-Copyright (C) 2016-2017 PhiloSoft Design.
+Copyright (C) 2016-2024 PhiloSoft Design.
 
 
 License
index 6196feecd4b7675702e64ddb5f2b35dbb6855f20..3a49d74791565a67af3cf9c8808e5f0074f9131d 100644 (file)
@@ -1,6 +1,24 @@
 News
 ====
 
+Version 0.3.1 (in development)
+------------------------------
+
+* Python 3.10, 3.11, 3.12.
+
+* CI(GHActions): Install all Python and PyPy versions from ``conda-forge``.
+
+Version 0.3.0 (2021-09-24)
+--------------------------
+
+* Python 3.8, Python 3.9.
+
+* GitHub Actions.
+
+* Stop testing at Travis CI.
+
+* Stop testing at AppVeyor.
+
 Version 0.2.3 (2019-02-01)
 --------------------------
 
index e63c1fd8f5942f0735256d5aae12e51e3ebd318a..e859f2a533df665041d6320902efb1c3245b0cc2 100755 (executable)
@@ -6,7 +6,6 @@ from io import open
 import os
 import sys
 
-from sqlparse.compat import text_type
 from sqlconvert.print_tokens import print_tokens
 from sqlconvert.process_mysql import is_directive_statement, process_statement
 from sqlconvert.process_tokens import is_newline_statement, StatementGrouper
@@ -14,6 +13,11 @@ from sqlconvert.process_tokens import is_newline_statement, StatementGrouper
 from m_lib.defenc import default_encoding
 from m_lib.pbar.tty_pbar import ttyProgressBar
 
+try:
+    text_type = unicode
+except NameError:
+    text_type = str
+
 
 def get_fsize(fp):
     try:
@@ -60,8 +64,8 @@ def main(infile, encoding, outfile, output_encoding, use_pbar, quoting_style):
             got_directive = is_directive_statement(statement)
             if got_directive:
                 continue
-            for statement in process_statement(statement, quoting_style):
-                print_tokens(statement, outfile=outfile,
+            for _statement in process_statement(statement, quoting_style):
+                print_tokens(_statement, outfile=outfile,
                              encoding=output_encoding)
     tokens = grouper.close()
     if tokens:
index 9e6f371d8f62f7451555447bb8bcf8bf92242f6a..708bf5e4044d63124ea3ffd0297e65eab9cab314 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,validators.py
+exclude = .git,.tox,docs/conf.py
 
index 6d0632c551ed87b76ce3ffc1967379c98e9ebded..3d74634341080352de9c0b8228b23f464449d02b 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@ if sys.version_info[:2] == (2, 7):
     execfile(versionpath, sqlconvert_version)  # noqa: F821 'execfile' Py3
 
 elif sys.version_info >= (3, 4):
-    exec(open(versionpath, 'rU').read(), sqlconvert_version)
+    exec(open(versionpath, 'r').read(), sqlconvert_version)
 
 else:
     raise ImportError("sqlconvert requires Python 2.7 or 3.4+")
@@ -20,7 +20,7 @@ setup(
     name='sqlconvert',
     version=sqlconvert_version['__version__'],
     description='Broytman sqlconvert',
-    long_description=open('README.rst', 'rU').read(),
+    long_description=open('README.rst', 'r').read(),
     long_description_content_type="text/x-rst",
     author='Oleg Broytman',
     author_email='phd@phdru.name',
@@ -52,6 +52,10 @@ setup(
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
+        'Programming Language :: Python :: 3.11',
+        'Programming Language :: Python :: 3.12',
     ],
     packages=['sqlconvert'],
     scripts=['scripts/mysql2sql'],
@@ -59,8 +63,7 @@ setup(
     install_requires=[
         'SQLObject>=2.2.1; python_version=="2.7"',
         'SQLObject>=3.0.0; python_version>="3.4"',
-        'm_lib.defenc>=1.0',
-        'm_lib>=3.1',
+        'm_lib.full>=1.0',
         'sqlparse',
     ],
 )
index d93b5b24297cfd540ea837e02e1b6c4dedf0819d..0404d81037f66e8dfa8fa4e18458619876f083bc 100644 (file)
@@ -1 +1 @@
-__version__ = '0.2.3'
+__version__ = '0.3.0'
index cb14467ccf958b478d888714ebaa6240e10fede2..886f36d46c02d1f824486593f3263bd06b3cb3b3 100644 (file)
@@ -2,9 +2,13 @@
 from sqlparse.sql import Comment
 from sqlobject.converters import sqlrepr
 from sqlparse import parse
-from sqlparse.compat import PY3
 from sqlparse import tokens as T
 
+try:
+    xrange
+except NameError:
+    xrange = range
+
 
 def find_error(token_list):
     """Find an error"""
@@ -36,10 +40,6 @@ def escape_strings(token_list, dbname):
             token.normalized = token.value = value
 
 
-if PY3:
-    xrange = range
-
-
 class StatementGrouper(object):
     """Collect lines and reparse until the last statement is complete"""
 
@@ -54,6 +54,8 @@ class StatementGrouper(object):
 
     def process_lines(self):
         statements = parse(''.join(self.lines), encoding=self.encoding)
+        if not statements:
+            return
         last_stmt = statements[-1]
         for i in xrange(len(last_stmt.tokens) - 1, 0, -1):
             token = last_stmt.tokens[i]
diff --git a/tests/mysql2sql/test.out3 b/tests/mysql2sql/test.out3
new file mode 100644 (file)
index 0000000..462e2be
--- /dev/null
@@ -0,0 +1,20 @@
+CREATE TABLE mytable (
+  id int(10) unsigned NOT NULL AUTO_INCREMENT,
+  date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  flag tinyint(4) NOT NULL DEFAULT '0',
+  PRIMARY KEY (id),
+  UNIQUE KEY date (date)
+) ENGINE="InnoDB" DEFAULT CHARSET=utf8;
+INSERT INTO /* inline comment */ mytable VALUES (1, 'ั‚ะตัั‚');
+SELECT * FROM mytable; -- line-comment"
+
+;
+INSERT INTO "MyTable" ("Id", "Name")
+VALUES (1, 'one');
+INSERT INTO mytable VALUES (1, 'one');
+
+INSERT INTO mytable VALUES (2, 'two');
+INSERT INTO mytable (id, name) VALUES (1, 'one');
+
+INSERT INTO mytable (id, name) VALUES (2, 'two');
+-- The end
index 0cc431ccb0614098ce5fd0c5ec72e99c508db2e6..d74cb47a0bd5fcfac108d3163fa1e2a9fad94ccc 100644 (file)
@@ -8,8 +8,8 @@ from sqlconvert.process_tokens import is_newline_statement, StatementGrouper
 
 
 def test_newline_statement():
-    parsed = parse("\n")[0]
-    assert is_newline_statement(parsed)
+    parsed = parse("\n")
+    assert not parsed or is_newline_statement(parsed[0])
 
 
 def test_encoding():
diff --git a/tox.ini b/tox.ini
index 696e97cf5a3ef8ae658cf89b0405cd19bdbbf205..b726b45888cd78a2f07ff82fa5de20c9078235d9 100644 (file)
--- a/tox.ini
+++ b/tox.ini
 [tox]
-minversion = 2.0
-envlist = py27,py3{4,5,6,7,8}{,-sqlite},py{27,38}-flake8
+minversion = 3.15
+envlist = py27,py3{4,5,6,7,8,9,10,11,12}{,-m2s,-sqlite},py{27,36,312}-flake8
 
 # Base test environment settings
 [testenv]
-basepython =
-    py27: {env:TOXPYTHON:python2.7}
-    py34: {env:TOXPYTHON:python3.4}
-    py35: {env:TOXPYTHON:python3.5}
-    py36: {env:TOXPYTHON:python3.6}
-    py37: {env:TOXPYTHON:python3.7}
-    py38: {env:TOXPYTHON:python3.8}
 commands =
     {envpython} --version
     {envpython} -c "import struct; print(struct.calcsize('P') * 8)"
+    {envpython} -m pytest --version
 deps =
     -rdevscripts/requirements/requirements_tests.txt
     postgres: psycopg2-binary
 passenv = PGPASSWORD
-platform = linux
+platform = linux|win32
 # Don't fail or warn on uninstalled commands
 whitelist_externals =
-    cmd
     createdb
     dropdb
 
-[general]
+[testenv:py{27,34,35,36,37,38,39,310,311,312}]
 commands =
     {[testenv]commands}
-    {envpython} -m pytest --cov=sqlconvert
-    {envpython} {envbindir}/mysql2sql -P demo/demo.sql test.out
-    cmp.py -i tests/mysql2sql/test.out test.out
-    rm.py -f test.out
-
-[testenv:py27]
-commands = {[general]commands}
-
-[testenv:py34]
-commands = {[general]commands}
-
-[testenv:py35]
-commands = {[general]commands}
-
-[testenv:py36]
-commands = {[general]commands}
-
-[testenv:py37]
-commands = {[general]commands}
-
-[testenv:py38]
-commands = {[general]commands}
+    {envpython} -m pytest
 
-# PostgreSQL test environments
-[postgresql]
+[testenv:py{27,34}-m2s]
 commands =
     {[testenv]commands}
-    -dropdb -U postgres -w test
-    createdb -U postgres -w test
-    {envpython} -m pytest --cov=sqlconvert -D postgres://postgres:@localhost/test
-    dropdb -U postgres -w test
-
-[testenv:py27-postgres]
-commands = {[postgresql]commands}
-
-[testenv:py34-postgres]
-commands = {[postgresql]commands}
-
-[testenv:py35-postgres]
-commands = {[postgresql]commands}
-
-[testenv:py36-postgres]
-commands = {[postgresql]commands}
-
-[testenv:py37-postgres]
-commands = {[postgresql]commands}
-
-[testenv:py38-postgres]
-commands = {[postgresql]commands}
+    {envpython} {envbindir}/mysql2sql -P demo/demo.sql test.out
+    cmp.py -i tests/mysql2sql/test.out2 test.out
+    rm.py -f test.out
 
-[postgres-w32]
-platform = win32
+[testenv:py{35,36,37,38,39,310,311,312}-m2s]
 commands =
     {[testenv]commands}
-    -dropdb -U postgres -w test
-    createdb -U postgres -w test
-    pytest --cov=sqlconvert -D "postgres://postgres:Password12!@localhost/test"
-    dropdb -U postgres -w test
-
-[testenv:py27-postgres-w32]
-platform = win32
-commands = {[postgres-w32]commands}
-
-[testenv:py34-postgres-w32]
-platform = win32
-commands = {[postgres-w32]commands}
-
-[testenv:py35-postgres-w32]
-platform = win32
-commands = {[postgres-w32]commands}
-
-[testenv:py36-postgres-w32]
-platform = win32
-commands = {[postgres-w32]commands}
-
-[testenv:py37-postgres-w32]
-platform = win32
-commands =
-    cmd /c "copy devscripts\\CI\\validators.py {envsitepackagesdir}\\formencode\\validators.py"
-    {[postgres-w32]commands}
-
-[testenv:py38-postgres-w32]
-platform = win32
-commands =
-    {[postgres-w32]commands}
+    {envpython} {envbindir}/mysql2sql -P demo/demo.sql test.out
+    cmp.py -i tests/mysql2sql/test.out3 test.out
+    rm.py -f test.out
 
-# SQLite test environments
-[sqlite]
+[testenv:py{27,34,35,36,37,38,39,310,311,312}-sqlite]
 commands =
     {[testenv]commands}
     -rm.py -f /tmp/test.sqdb
-    {envpython} -m pytest --cov=sqlconvert -D sqlite:///tmp/test.sqdb
+    {envpython} -m pytest -D sqlite:///tmp/test.sqdb
     rm.py -f /tmp/test.sqdb
 
-[testenv:py27-sqlite]
-commands = {[sqlite]commands}
-
-[testenv:py34-sqlite]
-commands = {[sqlite]commands}
-
-[testenv:py35-sqlite]
-commands = {[sqlite]commands}
-
-[testenv:py36-sqlite]
-commands = {[sqlite]commands}
-
-[testenv:py37-sqlite]
-commands = {[sqlite]commands}
-
-[testenv:py38-sqlite]
-commands = {[sqlite]commands}
-
-[sqlite-w32]
+[testenv:py{27,34,35,36,37,38,39,310,311,312}-sqlite-w32]
 platform = win32
 commands =
     {[testenv]commands}
-    -rm.py -f C:/projects/sqlconvert/test.sqdb
-    pytest --cov=sqlconvert -D sqlite:/C:/projects/sqlconvert/test.sqdb?debug=1
-    rm.py -f C:/projects/sqlconvert/test.sqdb
-
-[testenv:py27-sqlite-w32]
-platform = win32
-commands = {[sqlite-w32]commands}
+    -rm.py -f {env:TEMP}/test.sqdb
+    pytest -D sqlite:/{env:TEMP}/test.sqdb?debug=1
+    rm.py -f {env:TEMP}/test.sqdb
 
-[testenv:py34-sqlite-w32]
-platform = win32
-commands = {[sqlite-w32]commands}
-
-[testenv:py35-sqlite-w32]
-platform = win32
-commands = {[sqlite-w32]commands}
-
-[testenv:py36-sqlite-w32]
-platform = win32
-commands = {[sqlite-w32]commands}
-
-[testenv:py37-sqlite-w32]
-platform = win32
-commands =
-    cmd /c "copy devscripts\\CI\\validators.py {envsitepackagesdir}\\formencode\\validators.py"
-    {[sqlite-w32]commands}
-
-[testenv:py38-sqlite-w32]
-platform = win32
-commands =
-    {[sqlite-w32]commands}
-
-# flake8
-[testenv:py27-flake8]
-deps =
-    flake8
+[testenv:py{27,34,35,36,37,38,39,310,311,312}-postgres{,-w32}]
 commands =
     {[testenv]commands}
-    flake8
+    -dropdb --username=runner test
+    createdb --username=runner test
+    {envpython} -m pytest -D postgres://runner:test@localhost/test
+    dropdb --username=runner test
 
-[testenv:py38-flake8]
+# flake8
+[testenv:py{27,34,35,36,37,38,39,310,311,312}-flake8]
 deps =
     flake8
+    pytest < 7.0
 commands =
     {[testenv]commands}
     flake8