From: Oleg Broytman Date: Tue, 2 Jan 2024 01:14:49 +0000 (+0300) Subject: Build(GHActions): Use `checkout@v4` instead of outdated `v2` X-Git-Url: https://git.phdru.name/?p=sqlconvert.git;a=commitdiff_plain;h=HEAD;hp=722b2423e29d39101e4e298856b8839d2a57fc17 Build(GHActions): Use `checkout@v4` instead of outdated `v2` --- diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index bb87c9d..0000000 --- a/.coveragerc +++ /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 index 0000000..25c08a0 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -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 diff --git a/.gitignore b/.gitignore index 1c1f8db..4198027 100644 --- a/.gitignore +++ b/.gitignore @@ -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 index ddc9395..0000000 --- a/.travis.yml +++ /dev/null @@ -1,63 +0,0 @@ -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" - dist: xenial - env: TOXENV=py37 - - 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" - dist: xenial - env: TOXENV=py37-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" - dist: xenial - env: TOXENV=py37-sqlite - - python: "2.7" - env: TOXENV=py27-flake8 - - python: "3.7" - dist: xenial - env: TOXENV=py37-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 diff --git a/README.rst b/README.rst index 7de23df..22113ca 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ SQL converter. Author: Oleg Broytman . -Copyright (C) 2016-2018 PhiloSoft Design. +Copyright (C) 2016-2024 PhiloSoft Design. License: GPL. @@ -23,3 +23,7 @@ standard SQL to load at least to PostgreSQL or SQLite. | GitHub repo: https://github.com/phdru/sqlconvert | Issue tracker: https://github.com/phdru/sqlconvert/issues | PyPI: https://pypi.org/project/sqlconvert/ + +:: + + pip install sqlconvert diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 34cc57e..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,71 +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: "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: "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: "py27-flake8" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "32" - PYTHON_HOME: "C:\\Python27" - - TOXENV: "py37-flake8" - PYTHON_VERSION: "3.7" - PYTHON_ARCH: "64" - PYTHON_HOME: "C:\\Python37-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" diff --git a/demo/demo-process.py b/demo/demo-process.py index a81f93e..1fe9809 100755 --- a/demo/demo-process.py +++ b/demo/demo-process.py @@ -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/prerelease-tag b/devscripts/prerelease-tag new file mode 100755 index 0000000..19531f2 --- /dev/null +++ b/devscripts/prerelease-tag @@ -0,0 +1,4 @@ +#! /bin/sh + +tag="`python setup.py --version`" && +exec git tag --message="Release $tag" --sign $tag diff --git a/devscripts/release b/devscripts/release index 81488ed..28ce198 100755 --- a/devscripts/release +++ b/devscripts/release @@ -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 diff --git a/devscripts/requirements/requirements.txt b/devscripts/requirements/requirements.txt index abe3d94..a5e54e3 100644 --- a/devscripts/requirements/requirements.txt +++ b/devscripts/requirements/requirements.txt @@ -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 diff --git a/devscripts/requirements/requirements_tests.txt b/devscripts/requirements/requirements_tests.txt index 973ffba..e9716a9 100644 --- a/devscripts/requirements/requirements_tests.txt +++ b/devscripts/requirements/requirements_tests.txt @@ -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 diff --git a/devscripts/requirements/requirements_tox.txt b/devscripts/requirements/requirements_tox.txt index 8c9b6ed..b72a322 100644 --- a/devscripts/requirements/requirements_tox.txt +++ b/devscripts/requirements/requirements_tox.txt @@ -1 +1 @@ -tox >= 2.0, < 3.1 +tox >= 3.15, < 4 diff --git a/docs/index.rst b/docs/index.rst index fb45437..a73e921 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ Credits Created by Oleg Broytman . -Copyright (C) 2016-2017 PhiloSoft Design. +Copyright (C) 2016-2024 PhiloSoft Design. License diff --git a/docs/news.rst b/docs/news.rst index 6196fee..3a49d74 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -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) -------------------------- diff --git a/scripts/mysql2sql b/scripts/mysql2sql index e63c1fd..e859f2a 100755 --- a/scripts/mysql2sql +++ b/scripts/mysql2sql @@ -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: diff --git a/setup.cfg b/setup.cfg index 9e6f371..708bf5e 100644 --- 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 diff --git a/setup.py b/setup.py index 112c2e9..3d74634 100755 --- 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', @@ -51,6 +51,11 @@ setup( 'Programming Language :: Python :: 3.5', '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'], @@ -58,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', ], ) diff --git a/sqlconvert/__version__.py b/sqlconvert/__version__.py index d93b5b2..0404d81 100644 --- a/sqlconvert/__version__.py +++ b/sqlconvert/__version__.py @@ -1 +1 @@ -__version__ = '0.2.3' +__version__ = '0.3.0' diff --git a/sqlconvert/process_mysql.py b/sqlconvert/process_mysql.py index ad4d422..c89d6d1 100644 --- a/sqlconvert/process_mysql.py +++ b/sqlconvert/process_mysql.py @@ -1,6 +1,6 @@ from sqlparse.sql import Comment, Function, Identifier, Parenthesis, \ - Statement, Token + Statement, Token, Values from sqlparse import tokens as T from .process_tokens import escape_strings, is_comment_or_space @@ -29,7 +29,7 @@ def is_directive_statement(statement): def remove_directive_tokens(statement): - """Remove /\*! directives \*/ from the first-level""" # noqa: W605: \* + """Remove /*! directives */ from the first-level""" new_tokens = [] for token in statement.tokens: if _is_directive_token(token): @@ -110,6 +110,14 @@ def split_ext_insert(statement): expected = 'VALUES' continue elif expected == 'VALUES': + if isinstance(token, Values): + for subtoken in token.tokens: + if isinstance(subtoken, Parenthesis): + values_tokens.append(subtoken) + insert_tokens.append(Token(T.Keyword, 'VALUES')) + insert_tokens.append(Token(T.Whitespace, ' ')) + expected = 'VALUES_OR_SEMICOLON' + continue if (token.ttype is T.Keyword) and (token.normalized == 'VALUES'): insert_tokens.append(token) expected = 'VALUES_OR_SEMICOLON' diff --git a/sqlconvert/process_tokens.py b/sqlconvert/process_tokens.py index cb14467..886f36d 100644 --- a/sqlconvert/process_tokens.py +++ b/sqlconvert/process_tokens.py @@ -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.out b/tests/mysql2sql/test.out2 similarity index 70% rename from tests/mysql2sql/test.out rename to tests/mysql2sql/test.out2 index 0026e62..1bd6b8f 100644 --- a/tests/mysql2sql/test.out +++ b/tests/mysql2sql/test.out2 @@ -14,10 +14,10 @@ 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 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'); +INSERT INTO mytable (id, name) VALUES (1, 'one'); +INSERT INTO mytable (id, name) VALUES (2, 'two'); -- The end diff --git a/tests/mysql2sql/test.out3 b/tests/mysql2sql/test.out3 new file mode 100644 index 0000000..462e2be --- /dev/null +++ b/tests/mysql2sql/test.out3 @@ -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 diff --git a/tests/test_process_mysql.py b/tests/test_process_mysql.py index 656292b..420a219 100644 --- a/tests/test_process_mysql.py +++ b/tests/test_process_mysql.py @@ -84,19 +84,19 @@ def test_split_ext_insert(): stiter = process_statement(parsed) statement = next(stiter) query = tlist2str(statement) - assert query == u"INSERT INTO test VALUES (1, 2);\n" + assert query == u"INSERT INTO test VALUES (1, 2);\n" statement = next(stiter) query = tlist2str(statement) - assert query == u"INSERT INTO test VALUES (3, 4);" + assert query == u"INSERT INTO test VALUES (3, 4);" parsed = parse("insert into test (age, salary) values (1, 2), (3, 4)")[0] stiter = process_statement(parsed) statement = next(stiter) query = tlist2str(statement) - assert query == u"INSERT INTO test (age, salary) VALUES (1, 2)\n" + assert query == u"INSERT INTO test (age, salary) VALUES (1, 2)\n" statement = next(stiter) query = tlist2str(statement) - assert query == u"INSERT INTO test (age, salary) VALUES (3, 4)" + assert query == u"INSERT INTO test (age, salary) VALUES (3, 4)" def test_process(): diff --git a/tests/test_process_tokens.py b/tests/test_process_tokens.py index 0cc431c..d74cb47 100644 --- a/tests/test_process_tokens.py +++ b/tests/test_process_tokens.py @@ -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 a185a37..b726b45 100644 --- a/tox.ini +++ b/tox.ini @@ -1,171 +1,70 @@ [tox] -minversion = 2.0 -envlist = py{27,34,35,36,37}{,-sqlite},py{27,37}-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} 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} + {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} + {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 validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[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} - -[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} - -[testenv:py34-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: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 validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[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:py37-flake8] +# flake8 +[testenv:py{27,34,35,36,37,38,39,310,311,312}-flake8] deps = flake8 + pytest < 7.0 commands = {[testenv]commands} flake8 diff --git a/validators.py b/validators.py deleted file mode 100644 index 233d5dd..0000000 --- a/validators.py +++ /dev/null @@ -1,3089 +0,0 @@ -## FormEncode, a Form processor -## Copyright (C) 2003, Ian Bicking - -""" -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 - >>> 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 , - >>> cint2 = ConfirmType(type=int) - >>> cint2(accept_python=False).from_python(True) - Traceback (most recent call last): - ... - Invalid: True must be of the type - """ - - 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 : 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 `__ 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 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(?:(?: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[a-z0-9][a-z0-9\-]{,62}\.)* # subdomain - (?P[a-z]{2,63}|xn--[a-z0-9\-]{2,59}) # top level domain - ) - (?::[0-9]{1,5})? # port - # files/delims/etc - (?P/[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:: - - - - - 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 = '
\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( - '
\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
- 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( - '
\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( - '
\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() -