diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 00000000..5c2b60c0 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -0,0 +1,111 @@ +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", "3.13", "3.14"] + exclude: + - os: windows-latest + python-version: "2.7" + 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 MySQL + - uses: ankane/setup-mysql@v1 + + # Setup PostgreSQL + - uses: ankane/setup-postgres@v1 + - name: Setup Postgres user @ Linux + run: | + sudo -u postgres psql --command="ALTER USER runner CREATEDB ENCRYPTED PASSWORD 'test'" + if: ${{ runner.os == 'Linux' }} + - name: Setup Postgres user @ w32 + run: | + psql --command="CREATE USER runner CREATEDB ENCRYPTED PASSWORD 'test'" + if: ${{ runner.os == 'Windows' }} + + # Setup Python/pip + - uses: actions/checkout@v5 + - uses: conda-incubator/setup-miniconda@v3.2.0 + with: + channels: conda-forge, conda-forge/label/python_rc + miniforge-version: latest + python-version: ${{ matrix.python-version }} + if: ${{ !contains(fromJSON(env.not_in_conda), matrix.python-version) }} + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + if: ${{ contains(fromJSON(env.not_in_conda), matrix.python-version) }} + - uses: actions/cache@v4 + with: + path: ~/conda_pkgs_dir + key: ${{ runner.os }}-conda + - name: Cache pip + uses: actions/cache@v4 + 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" + shell: bash -el {0} + - name: Set PYVER + run: | + python -c " + import os, sys + ld_library_path = None + pyver = '%d%d' % tuple(sys.version_info[:2]) + 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') + with open(os.environ['GITHUB_ENV'], 'a') as f: + if ld_library_path: + f.write('LD_LIBRARY_PATH=' + ld_library_path + '\n') + f.write('PYVER=' + pyver + '\n') + f.write('PGPASSWORD=test\n') + " + shell: bash -el {0} + + - name: tox version + run: | + tox --version + shell: bash -el {0} + - name: Run tox @ Linux + run: | + devscripts/tox-select-envs $PYVER-mysql && + devscripts/tox-select-envs $PYVER-postgres && + devscripts/tox-select-envs $PYVER-sqlite && + devscripts/tox-select-envs $PYVER-flake8 + if: ${{ runner.os == 'Linux' }} + shell: bash -el {0} + - name: Run tox @ w32 + run: > + devscripts\\tox-select-envs.cmd %PYVER%-mysql && + devscripts\\tox-select-envs.cmd %PYVER%-postgres && + devscripts\\tox-select-envs.cmd %PYVER%-sqlite + if: ${{ runner.os == 'Windows' }} + shell: cmd /C CALL {0} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index acd3e873..00000000 --- a/.travis.yml +++ /dev/null @@ -1,182 +0,0 @@ -language: python - -python: - - "2.7" - -cache: pip - -services: - - mysql - - postgres - -addons: - apt: - packages: - - python-egenix-mxdatetime - - python-mysqldb - - python-psycopg2 - - python3-psycopg2 - - firebird2.5-super - postgresql: "9.4" - -matrix: - include: - - python: "2.7" - env: TOXENV=py27-mysqldb - - python: "3.4" - env: TOXENV=py34-mysqlclient - - python: "3.5" - env: TOXENV=py35-mysqlclient - - python: "3.6" - env: TOXENV=py36-mysqlclient - - python: "3.7" - dist: xenial - env: TOXENV=py37-mysqlclient - - python: "2.7" - env: TOXENV=py27-mysql-connector - - python: "3.4" - env: TOXENV=py34-mysql-connector - - python: "3.5" - env: TOXENV=py35-mysql-connector - - python: "3.6" - env: TOXENV=py36-mysql-connector - - python: "3.7" - dist: xenial - env: TOXENV=py37-mysql-connector - - python: "2.7" - env: TOXENV=py27-mysql-oursql - - python: "3.4" - env: TOXENV=py34-mysql-oursql3 - - python: "3.5" - env: TOXENV=py35-mysql-oursql3 - - python: "3.6" - env: TOXENV=py36-mysql-oursql3 - - python: "3.7" - dist: xenial - env: TOXENV=py37-mysql-oursql3 - - python: "2.7" - env: TOXENV=py27-pymysql - - python: "3.4" - env: TOXENV=py34-pymysql - - python: "3.5" - env: TOXENV=py35-pymysql - - python: "3.6" - env: TOXENV=py36-pymysql - - python: "3.7" - dist: xenial - env: TOXENV=py37-pymysql - - python: "2.7" - env: TOXENV=py27-postgres-psycopg - - python: "3.4" - env: TOXENV=py34-postgres-psycopg - - python: "3.5" - env: TOXENV=py35-postgres-psycopg - - python: "3.6" - env: TOXENV=py36-postgres-psycopg - - python: "3.7" - dist: xenial - env: TOXENV=py37-postgres-psycopg - - python: "2.7" - env: TOXENV=py27-postgres-pygresql - - python: "3.4" - env: TOXENV=py34-postgres-pygresql - - python: "3.5" - env: TOXENV=py35-postgres-pygresql - - python: "3.6" - env: TOXENV=py36-postgres-pygresql - - python: "3.7" - dist: xenial - env: TOXENV=py37-postgres-pygresql - - python: "3.4" - env: TOXENV=py34-pypostgresql - - python: "3.5" - env: TOXENV=py35-pypostgresql - - python: "3.6" - env: TOXENV=py36-pypostgresql - - python: "3.7" - dist: xenial - env: TOXENV=py37-pypostgresql - - python: "2.7" - env: TOXENV=py27-postgres-pg8000 - - python: "3.4" - env: TOXENV=py34-postgres-pg8000 - - python: "3.5" - env: TOXENV=py35-postgres-pg8000 - - python: "3.6" - env: TOXENV=py36-postgres-pg8000 - - python: "3.7" - dist: xenial - env: TOXENV=py37-postgres-pg8000 - - 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-sqlite-memory - - python: "3.4" - env: TOXENV=py34-sqlite-memory - - python: "3.5" - env: TOXENV=py35-sqlite-memory - - python: "3.6" - env: TOXENV=py36-sqlite-memory - - python: "3.7" - dist: xenial - env: TOXENV=py37-sqlite-memory - - python: "2.7" - env: TOXENV=py27-flake8 - - python: "3.7" - dist: xenial - env: TOXENV=py37-flake8 - - python: "2.7" - env: TOXENV=py27-firebird-fdb - - python: "3.6" - env: TOXENV=py36-firebird-fdb - - python: "2.7" - env: TOXENV=py27-firebirdsql - - python: "3.6" - env: TOXENV=py36-firebirdsql - - allow_failures: - - env: TOXENV=py27-firebird-fdb - - env: TOXENV=py36-firebird-fdb - - env: TOXENV=py27-firebirdsql - - env: TOXENV=py36-firebirdsql - - fast_finish: true - -before_install: - # Start the firebird database server. - # We use firebird-super, so there's none of the inetd configuration - # required by firebird-classic. - # We also create a test user for the firebird test and - # create a script that can be fed into isql-fb - # to create the test database. - # Copied password initializtion from - # https://github.com/xdenser/node-firebird-libfbclient/blob/master/.travis.yml - - if [[ $TOXENV = *firebird* ]]; then - sudo sed -i /etc/default/firebird2.5 -e 's/=no/=yes/' && - sudo /etc/init.d/firebird2.5-super start && sleep 5 && - sudo /bin/bash -c '(export FB_VER="2.5"; export FB_FLAVOUR="super";source /usr/share/firebird2.5-common/functions.sh; writeNewPassword masterkey)' && - sudo gsec -user sysdba -pass masterkey -add test -pw test && - sudo /bin/bash -c "echo \"CREATE DATABASE 'localhost:/tmp/test.fdb';\" > /var/lib/firebird/create_test_db" && - sudo chmod 644 /var/lib/firebird/create_test_db; - fi - -install: travis_retry pip install --upgrade "pip < 19.1" setuptools tox coveralls codecov ppu - -script: tox - -after_success: - - cd sqlobject - - coveralls - - codecov - -before_cache: - - remove-old-files.py -o 180 ~/.cache/pip diff --git a/ANNOUNCE.rst b/ANNOUNCE.rst index 84b0dcbe..b1d6645f 100644 --- a/ANNOUNCE.rst +++ b/ANNOUNCE.rst @@ -1,28 +1,14 @@ Hello! -I'm pleased to announce version 3.8.0a1, the first alpha of the upcoming -release of branch 3.8 of SQLObject. - -I'm pleased to announce version 3.8.0a2, the second alpha of the upcoming -release of branch 3.8 of SQLObject. - -I'm pleased to announce version 3.8.0b1, the first beta of the upcoming -release of branch 3.8 of SQLObject. - -I'm pleased to announce version 3.8.0rc1, the first release candidate -of the upcoming release of branch 3.8 of SQLObject. - -I'm pleased to announce version 3.8.0, the first stable release of branch -3.8 of SQLObject. - -I'm pleased to announce version 3.8.1, the first bugfix release of branch -3.8 of SQLObject. +I'm pleased to announce version 3.13.2a0, the 2nd bugfix of the +branch 3.13 of SQLObject. What's new in SQLObject ======================= -Contributors for this release are +The contributors for this release are: + For a more complete list, please see the news: http://sqlobject.org/News.html @@ -31,12 +17,17 @@ http://sqlobject.org/News.html What is SQLObject ================= -SQLObject is an object-relational mapper. Your database tables are described -as classes, and rows are instances of those classes. SQLObject is meant to be -easy to use and quick to get started with. +SQLObject is a free and open-source (LGPL) Python object-relational +mapper. Your database tables are described as classes, and rows are +instances of those classes. SQLObject is meant to be easy to use and +quick to get started with. -SQLObject supports a number of backends: MySQL, PostgreSQL, SQLite, -Firebird, Sybase, MSSQL and MaxDB (also known as SAPDB). +SQLObject supports a number of backends: MySQL/MariaDB (with a number of +DB API drivers: ``MySQLdb``, ``mysqlclient``, ``mysql-connector``, +``PyMySQL``, ``mariadb``), PostgreSQL (``psycopg``, ``psycopg2``, ``PyGreSQL``, +partially ``pg8000``), SQLite (builtin ``sqlite3``); +connections to other backends - Firebird, Sybase, MSSQL and MaxDB (also +known as SAPDB) - are less debugged). Python 2.7 or 3.4+ is required. @@ -47,14 +38,8 @@ Where is SQLObject Site: http://sqlobject.org -Development: -http://sqlobject.org/devel/ - -Mailing list: -https://lists.sourceforge.net/mailman/listinfo/sqlobject-discuss - Download: -https://pypi.org/project/SQLObject/3.8.0a0.dev20190202/ +https://pypi.org/project/SQLObject/3.13.2a0.dev20251208/ News and changes: http://sqlobject.org/News.html @@ -62,10 +47,23 @@ http://sqlobject.org/News.html StackOverflow: https://stackoverflow.com/questions/tagged/sqlobject +Mailing lists: +https://sourceforge.net/p/sqlobject/mailman/ + +Development: +http://sqlobject.org/devel/ + +Developer Guide: +http://sqlobject.org/DeveloperGuide.html + Example ======= +Install:: + + $ pip install sqlobject + Create a simple class that wraps a table:: >>> from sqlobject import * diff --git a/MANIFEST.in b/MANIFEST.in index cb6c6a70..c3330428 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ global-include *.py *.rst *.txt recursive-include docs *.css *.html *.js *.gif *.png -include LICENSE MANIFEST.in .travis.yml tox.ini -include debian/* sqlobject/.coveragerc +include LICENSE MANIFEST.in .tox.ini +include debian/* include docs/Makefile docs/genapidocs docs/rebuild recursive-exclude devscripts * recursive-exclude docs/_build * diff --git a/README.rst b/README.rst index 3e96729c..cf8a985e 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,85 @@ -SQLObject 3.8.0 -=============== +SQLObject 3.13.2a0 +================== -Thanks for looking at SQLObject. SQLObject is an object-relational -mapper, i.e., a library that will wrap your database tables in Python -classes, and your rows in Python instances. +SQLObject is a free and open-source (LGPL) Python object-relational +mapper. Your database tables are described as classes, and rows are +instances of those classes. SQLObject is meant to be easy to use and +quick to get started with. -It currently supports MySQL through the `MySQLdb` package, PostgreSQL -through the `psycopg` package, SQLite, Firebird, MaxDB (SAP DB), MS SQL, -Sybase and Rdbhost. Python 2.7 or 3.4+ is required. +SQLObject supports a number of backends: MySQL/MariaDB (with a number of +DB API drivers: ``MySQLdb``, ``mysqlclient``, ``mysql-connector``, +``PyMySQL``, ``mariadb``), PostgreSQL (``psycopg``, ``psycopg2``, ``PyGreSQL``, +partially ``pg8000``), SQLite (builtin ``sqlite3``); +connections to other backends - Firebird, Sybase, MSSQL and MaxDB (also +known as SAPDB) - are less debugged). -For more information please see the documentation in -``_, or online at http://sqlobject.org/ +Python 2.7 or 3.4+ is required. + + +Where is SQLObject +================== + +Site: +http://sqlobject.org + +Download: +https://pypi.org/project/SQLObject/ + +News and changes: +http://sqlobject.org/News.html + +StackOverflow: +https://stackoverflow.com/questions/tagged/sqlobject + +Mailing lists: +https://sourceforge.net/p/sqlobject/mailman/ + +Development: +http://sqlobject.org/devel/ + +Developer Guide: +http://sqlobject.org/DeveloperGuide.html + + +Example +======= + +Install:: + + $ pip install sqlobject + +Create a simple class that wraps a table:: + + >>> from sqlobject import * + >>> + >>> sqlhub.processConnection = connectionForURI('sqlite:/:memory:') + >>> + >>> class Person(SQLObject): + ... fname = StringCol() + ... mi = StringCol(length=1, default=None) + ... lname = StringCol() + ... + >>> Person.createTable() + +Use the object:: + + >>> p = Person(fname="John", lname="Doe") + >>> p + + >>> p.fname + 'John' + >>> p.mi = 'Q' + >>> p2 = Person.get(1) + >>> p2 + + >>> p is p2 + True + +Queries:: + + >>> p3 = Person.selectBy(lname="Doe")[0] + >>> p3 + + >>> pc = Person.select(Person.q.lname=="Doe").count() + >>> pc + 1 diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 953cc8ac..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,171 +0,0 @@ -# Install SQLObject on windows and test against MS SQL server and postgres -# Heavily inspired by Oliver Grisel's appveyor-demo (https://github.com/ogrisel/python-appveyor-demo) -# and Michael Sverdlik's appveyor-utils (https://github.com/cloudify-cosmo/appveyor-utils) -version: '{branch}-{build}' - -cache: - - '%LOCALAPPDATA%\pip\Cache' - -# Match travis -clone_depth: 50 - -services: - - mysql - - postgresql - -environment: - MYSQL_PWD: "Password12!" - PGUSER: "postgres" - PGPASSWORD: "Password12!" - - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 - CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\run_with_env.cmd" - - matrix: - - TOXENV: "py27-mysql-connector-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - db: mysql - - TOXENV: "py36-mysql-connector-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: mysql - - TOXENV: "py37-mysql-connector-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: mysql - - TOXENV: "py27-pymysql-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - db: mysql - - TOXENV: "py36-pymysql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: mysql - - TOXENV: "py37-pymysql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: mysql - - TOXENV: "py27-postgres-psycopg-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - db: postgresql - - TOXENV: "py36-postgres-psycopg-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: postgresql - - TOXENV: "py37-postgres-psycopg-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: postgresql - - TOXENV: "py27-postgres-pygresql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27-x64" - db: postgresql - - TOXENV: "py36-postgres-pygresql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: postgresql - - TOXENV: "py37-postgres-pygresql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: postgresql - - TOXENV: "py36-pypostgresql-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36" - db: postgresql - - TOXENV: "py37-pypostgresql-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37" - db: postgresql - - TOXENV: "py36-pypostgresql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: postgresql - - TOXENV: "py37-pypostgresql-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: postgresql - - TOXENV: "py27-postgres-pg8000-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - db: postgresql - - TOXENV: "py36-postgres-pg8000-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - db: postgresql - - TOXENV: "py37-postgres-pg8000-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - db: postgresql - - TOXENV: "py27-sqlite-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - - TOXENV: "py36-sqlite-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - - TOXENV: "py37-sqlite-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - - TOXENV: "py27-sqlite-memory-w32" - PYTHON_ARCH: "32" - PYTHON_VERSION: "2.7" - PYTHON_HOME: "C:\\Python27" - - TOXENV: "py36-sqlite-memory-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.6" - PYTHON_HOME: "C:\\Python36-x64" - - TOXENV: "py37-sqlite-memory-w32" - PYTHON_ARCH: "64" - PYTHON_VERSION: "3.7" - PYTHON_HOME: "C:\\Python37-x64" - -matrix: - fast_finish: true - -install: - # Ensure we use the right python version - - "SET PATH=%PYTHON_HOME%;%PYTHON_HOME%\\Scripts;C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin;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" - # List ODBC drivers - - ps: Get-OdbcDriver -Platform 32-bit | Select-Object -ExpandProperty Name - - ps: Get-OdbcDriver -Platform 64-bit | Select-Object -ExpandProperty Name - -# Not a C project, so no build step -build: false - -test_script: - - "%CMD_IN_ENV% tox" - -after_test: - - "remove-old-files.py -o 180 %LOCALAPPDATA%\\pip\\Cache" diff --git a/devscripts/BRANCH-CHECKLIST b/devscripts/BRANCH-CHECKLIST index da9c16ac..29899043 100644 --- a/devscripts/BRANCH-CHECKLIST +++ b/devscripts/BRANCH-CHECKLIST @@ -9,7 +9,7 @@ setup.cfg in the branch edit section [publish] - uncomment doc-dest for stable branch. In setup.py in the branch edit URL (remove '/devel') and download URLs. In setup.py and DeveloperGuide.rst edit - Travis CI build status image URL (change branch). Commit. + CI build status image URL (change branch). Commit. 1b. If the branching point was master the script checks out master and calls editor again; edit README.rst, __version__.py and News.rst in diff --git a/devscripts/RELEASE-CHECKLIST b/devscripts/RELEASE-CHECKLIST index e85de5e2..a7909b4f 100644 --- a/devscripts/RELEASE-CHECKLIST +++ b/devscripts/RELEASE-CHECKLIST @@ -1,57 +1,49 @@ 0. Run full test suite in all branches and in master. Continue if all tests passed. -1. Check out the release branch. If it is a stable release - edit - docs/News.rst to set release date. Commit. - -2. If release branch is not master - run devscripts/prerelease $NEW_TAG; if +1. If release branch is not master - run devscripts/prerelease $NEW_TAG; if it's master - run devscripts/prerelease $NEW_TAG master. - The script checks out the release branch and calls editor; update - version, the list of contributors, the list of changes and download - URL in ANNOUNCE.rst; edit __version__.py and README.rst in the release - branch - fix versions. Edit section [egg_info] in setup.cfg - set if - it is a stable or development release. In setup.py edit "Development - Status" in trove classifiers; edit download URLs: if a non-stable - version - append 'dev' and date stamp, for a stable version remove - 'dev' and date stamp). - - If it's the first stable release of the branch - edit build-all-docs, - advance stable branch. Commit. If it's not master - merge to all higher - branches and master. + The script checks out the release branch and calls editor; if it's the + first stable release of the branch - edit build-all-docs, advance stable + branch; if it is a stable release - edit docs/News.rst to set release + date; update version, the list of contributors, the list of changes and + download URL in ANNOUNCE.rst; edit __version__.py and README.rst in the + release branch - fix versions. Edit section [egg_info] in setup.cfg - + set if it is a stable or development release. In setup.py edit + "Development Status" in trove classifiers; edit download URLs: if a + non-stable version - append 'dev' and date stamp, for a stable version + remove 'dev' and date stamp). Commit. Verify. - Commit. Verify. +2. If it's not master - null-merge to the next higher branch. -3. If it's not master - null-merge to the next higher branch. - -4. If release branch is not master - run devscripts/prerelease-tag +3. If release branch is not master - run devscripts/prerelease-tag $NEW_TAG; if it's master - run devscripts/prerelease-tag $NEW_TAG master. This checks out the release branch and creates the new tag at the head of the release branch. -5. Run devscripts/release. This generates and uploads new archives to PyPI +4. Run devscripts/release. This generates and uploads new archives to PyPI and if it is a stable release - uploads archives and release - announcement (ANNOUNCE.rst) to SourceForge. - -6. Move old releases at SourceForge to subdirectory OldFiles. + announcement (ANNOUNCE.rst) to SourceForge. Move old releases at + SourceForge to subdirectory OldFiles. -7. Run devscripts/push-all in the development repository to push all - branches and tags to the public repositories. - -8. Run devscripts/postrelease. The script restores ANNOUNCE.rst and +5. Run devscripts/postrelease. The script restores ANNOUNCE.rst and setup.cfg from the previous commit (HEAD~). It calls editor; update next version, remove the list of contributors and the list of changes, edit download URL in ANNOUNCE.rst. Edit README.rst and docs/News.rst - add new version. -9. Generate new docs using devscripts/build-all-docs. Upload docs using +6. Run devscripts/push-all in the development repository to push all + branches and tags to the public repositories. + +7. Generate new docs using devscripts/build-all-docs. Upload docs using devscripts/publish-docs. -10. Send announcement to the SQLObject mailing list. For a stable - release send announcements to python, python-announce and python-db - mailing lists. +8. Send announcement to the SQLObject mailing list. For a stable + release send announcements to python, python-announce and python-db + mailing lists. -10. Announce new release(s) at Twitter (https://twitter.com/SQLObject) and - Wikipedia (https://en.wikipedia.org/wiki/SQLObject). If it is a stable - release - announce it at - https://en.wikipedia.org/wiki/Comparison_of_object-relational_mapping_software. +9. Announce new release(s) at Twitter (https://twitter.com/SQLObject) and + Wikipedia (https://en.wikipedia.org/wiki/SQLObject). If it is a stable + release - announce it at + https://en.wikipedia.org/wiki/Comparison_of_object-relational_mapping_software. diff --git a/devscripts/branch b/devscripts/branch index 908e5fb3..dfcd6df1 100755 --- a/devscripts/branch +++ b/devscripts/branch @@ -8,7 +8,7 @@ fi branch="$1" treeish="$2" -. `dirname $0`/split.sh && +. `dirname $0`/split_tag.sh && branch="$1" if [ -z "$treeish" ]; then @@ -42,7 +42,7 @@ micro = 0 release_level = 'trunk' serial = 0 version_info = (major, minor, micro, release_level, serial)" > sqlobject/__version__.py && - `git var GIT_EDITOR` README.rst sqlobject/__version__.py docs/News.rst appveyor.yml setup.py && + `git var GIT_EDITOR` README.rst sqlobject/__version__.py docs/News.rst setup.py && git commit --message="Next branch will be $major.$next_minor" README.rst sqlobject/__version__.py docs/News.rst setup.py && exec sed -i /"$major\.$minor"/"$major.$next_minor"/ ANNOUNCE.rst diff --git a/devscripts/build-all-docs b/devscripts/build-all-docs index 746018a3..62020c9d 100755 --- a/devscripts/build-all-docs +++ b/devscripts/build-all-docs @@ -3,14 +3,14 @@ build_docs() { git checkout "$1" && devscripts/build-docs && - rsync -ahP --del --exclude=/robots.txt docs/html/ ../SQLObject-docs/"$2"/ + rsync -ahPv --del --exclude=/robots.txt docs/html/ ../SQLObject-docs/"$2"/ } cd "`dirname \"$0\"`" && PROG_DIR="`pwd`" && cd .. && -build_docs 3.7.1 && +build_docs 3.13.1 && build_docs master devel && rm -rf docs/html && diff --git a/devscripts/git-hooks/post-checkout b/devscripts/git-hooks/post-checkout index 2bd5be00..7b065f5a 100755 --- a/devscripts/git-hooks/post-checkout +++ b/devscripts/git-hooks/post-checkout @@ -11,12 +11,7 @@ if [ "$new_branch" = 1 ]; then # empty directories and outdated docs find . -name '*.py[co]' -delete && for d in sqlobject/include/pydispatch sqlobject/include/tests; do - if [ "`echo $d/*`" = "$d/*" ]; then rm -rf $d; fi + if [ "`echo $d/*`" = "$d/*" ]; then rmdir $d; fi done && - rm -rf docs/html -fi && - -python -m compileall -q -x '\.tox/.+' . && -python -O -m compileall -q -x '\.tox/.+' . - -exit 0 + rm -rf docs/_build/html docs/html +fi diff --git a/devscripts/git-hooks/post-merge b/devscripts/git-hooks/post-merge deleted file mode 100755 index 633a1d0d..00000000 --- a/devscripts/git-hooks/post-merge +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -# post-merge hook that compiles python files to byte code - -python -m compileall -q -x '\.tox/.+' . && -python -O -m compileall -q -x '\.tox/.+' . - -exit 0 diff --git a/devscripts/git-hooks/post-rewrite b/devscripts/git-hooks/post-rewrite deleted file mode 100755 index df284420..00000000 --- a/devscripts/git-hooks/post-rewrite +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -# post-rewrite hook that compiles python files to byte code - -python -m compileall -q -x '\.tox/.+' . && -python -O -m compileall -q -x '\.tox/.+' . - -exit 0 diff --git a/devscripts/git-svn/SQLObject-gitignore/.gitignore b/devscripts/git-svn/SQLObject-gitignore/.gitignore deleted file mode 100644 index 52d4a58a..00000000 --- a/devscripts/git-svn/SQLObject-gitignore/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*~ -*.tmp -*.pyc -*.pyo -/MANIFEST -/SQLObject.egg-info -/build -/data -/dist -/temp diff --git a/devscripts/git-svn/SQLObject-gitignore/docs/.gitignore b/devscripts/git-svn/SQLObject-gitignore/docs/.gitignore deleted file mode 100644 index 59370209..00000000 --- a/devscripts/git-svn/SQLObject-gitignore/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/*.html -/data.db -/html diff --git a/devscripts/git-svn/svn2git b/devscripts/git-svn/svn2git deleted file mode 100755 index e93e4f6f..00000000 --- a/devscripts/git-svn/svn2git +++ /dev/null @@ -1,55 +0,0 @@ -#! /bin/sh - -if [ -z "$1" ]; then - echo "Usage: $0 svn_url [dir]" >&2 - exit 1 -fi - -url="$1" - -if [ ! -f authors.txt ]; then - echo "Run \"get-authors $1\" first" >&2 - exit 2 -fi - -if [ -z "$2" ]; then - dir="`basename $url`" -else - dir="$2" -fi - -if [ -z "$dir" ]; then - echo "Usage: $0 $url dir" >&2 - exit 1 -fi - -# init + fetch -git svn clone "$url" --authors-file=authors.txt --prefix=svn/ --stdlayout "$dir" && -cd "$dir" && - -# Convert tags and branches - -# See http://blog.jessitron.com/2013/08/converting-from-svn-to-git.html - -git for-each-ref --format="%(refname:short)" refs/remotes/svn | - sed 's#svn/##' | grep -v '^tags' | - while read aBranch; do git branch $aBranch svn/$aBranch || exit 1; done - -# See http://thomasrast.ch/git/git-svn-conversion.html - -git for-each-ref --format="%(refname:short)" refs/remotes/svn/tags/ | -while read tag; do - GIT_COMMITTER_DATE="`git log -1 --pretty=format:\"%ad\" \"$tag\"`" \ - GIT_COMMITTER_EMAIL="`git log -1 --pretty=format:\"%ce\" \"$tag\"`" \ - GIT_COMMITTER_NAME="`git log -1 --pretty=format:\"%cn\" \"$tag\"`" \ - git tag -a -m "`git for-each-ref --format=\"%(contents)\" \"$tag\"`" \ - "`echo \"$tag\" | sed 's#svn/tags/##'`" "$tag" || exit 1 -done - -# preserve authors.txt -cp -p ../authors.txt .git/info && -git config --local --path svn.authorsfile .git/info/authors.txt - -git svn gc && -git gc --aggressive && -echo "Cloned from $url using git-svn" >.git/description diff --git a/devscripts/git-svn/svn2git-fullhistory b/devscripts/git-svn/svn2git-fullhistory deleted file mode 100755 index bc74b897..00000000 --- a/devscripts/git-svn/svn2git-fullhistory +++ /dev/null @@ -1,33 +0,0 @@ -#! /bin/sh - -"`dirname \"$0\"`"/svn2git \ - http://svn.colorstudy.com/SQLObject SQLObject-fullhistory && - -git init --bare SQLObject.git && -cd SQLObject.git && -echo "Bare SQLObject repository" >description && -git fetch ../SQLObject-fullhistory master:master 1.5:1.5 1.6:1.6 1.7:1.7 tag 1.5.0b1 tag 1.5.0rc1 tag 1.5.0 tag 1.5.1 tag 1.5.2 tag 1.6.0a1 tag 1.6.0b1 tag 1.6.0 tag 1.7.0b1 && - -cd .. && -git clone SQLObject.git SQLObject && rm -rf SQLObject.git && -cd SQLObject && git remote rm origin && rmdir .git/refs/remotes/origin && - -echo "Development SQLObject repository" >.git/description && -git branch --track 1.5 origin/1.5 && -git branch --track 1.6 origin/1.6 && -git branch --track 1.7 origin/1.7 && - -# Null merges -git checkout 1.6 && git merge --no-commit -s ours 1.5 && -git checkout HEAD sqlobject/main.py && git commit && - -git checkout 1.7 && git merge --no-commit -s ours 1.6 && -git checkout HEAD sqlobject/col.py && git commit && - -git checkout master && git merge --no-commit -s ours 1.7 && -git checkout HEAD setup.cfg setup.py \ - sqlobject/__version__.py sqlobject/converters.py \ - sqlobject/tests/test_converters.py sqlobject/tests/test_datetime.py && -git commit && - -exec vim .git/config diff --git a/devscripts/postrelease b/devscripts/postrelease index 55dfe674..a799b553 100755 --- a/devscripts/postrelease +++ b/devscripts/postrelease @@ -2,5 +2,8 @@ git checkout HEAD~ ANNOUNCE.rst setup.cfg && +trove_cls='3 - Alpha' && +sed -Ei "s/Development Status :: .+\",\$/Development Status :: $trove_cls\",/" setup.py && + `git var GIT_EDITOR` ANNOUNCE.rst setup.cfg README.rst docs/News.rst && -exec git commit --message="Prepare for the next release" ANNOUNCE.rst setup.cfg README.rst docs/News.rst +exec git commit --message="Build: Prepare for the next release" --message="[skip ci]" ANNOUNCE.rst setup.cfg README.rst docs/News.rst setup.py diff --git a/devscripts/prerelease b/devscripts/prerelease index edd7358e..7fe520db 100755 --- a/devscripts/prerelease +++ b/devscripts/prerelease @@ -10,7 +10,7 @@ else branch="$2" fi -. `dirname $0`/split.sh && +. `dirname $0`/split_tag.sh && split_tag $tag $branch git checkout "$branch" && @@ -22,5 +22,27 @@ micro = $micro release_level = '$state' serial = $serial version_info = (major, minor, micro, release_level, serial)" > sqlobject/__version__.py && -`git var GIT_EDITOR` devscripts/build-all-docs docs/News.rst ANNOUNCE.rst sqlobject/__version__.py README.rst setup.cfg setup.py && + +sqlo_tag="SQLObject $tag" && +sqlo_tag_len=${#sqlo_tag} && +sed -Ei "1s/^SQLObject [1-9].+\$/$sqlo_tag/" README.rst && +sed -Ei "2s/^==========+\$/`python -c \"print('='*$sqlo_tag_len)\"`/" README.rst && + +if [ "$state" = alpha ]; then + trove_cls='3 - Alpha' +elif [ "$state" = beta -o "$state" = 'release candidate' ]; then + trove_cls='4 - Beta' +elif [ "$state" = final -o "$state" = post ]; then + trove_cls='5 - Production\/Stable' +else + echo "Error: unknown state $state" >&2 + exit 1 +fi && +sed -Ei "s/Development Status :: .+\",\$/Development Status :: $trove_cls\",/" setup.py && + +if [ "$state" = final -o "$state" = post ]; then + dbad=devscripts/build-all-docs +fi && + +`git var GIT_EDITOR` $dbad docs/News.rst ANNOUNCE.rst sqlobject/__version__.py README.rst setup.cfg setup.py && exec git commit --message="Release $tag" devscripts/build-all-docs docs/News.rst ANNOUNCE.rst sqlobject/__version__.py README.rst setup.cfg setup.py diff --git a/devscripts/prerelease-tag b/devscripts/prerelease-tag index b7f10175..46ac39e7 100755 --- a/devscripts/prerelease-tag +++ b/devscripts/prerelease-tag @@ -10,7 +10,7 @@ else branch="$2" fi -. `dirname $0`/split.sh && +. `dirname $0`/split_tag.sh && split_tag $tag $branch git checkout "$branch" && diff --git a/devscripts/release b/devscripts/release index 35154c95..44ad33a1 100755 --- a/devscripts/release +++ b/devscripts/release @@ -3,6 +3,7 @@ cd "`dirname \"$0\"`"/.. && umask 022 && chmod -R a+rX . && +find .git/objects -type f -exec chmod u=r,go= '{}' \+ && set-commit-date.py && devscripts/build-docs && @@ -15,14 +16,14 @@ find build -name '*.py[co]' -delete && python setup.py bdist_wheel --universal && version=`python setup.py --version` -. `dirname $0`/split.sh && +. devscripts/split_tag.sh && split_tag $version if [ "$state" = final ]; then - rsync -ahP4 dist/* frs.sourceforge.net:/home/frs/project/sqlobject/sqlobject/"$version"/ && - rsync -ahP4 ANNOUNCE.rst frs.sourceforge.net:/home/frs/project/sqlobject/sqlobject/"$version"/README.rst || exit 1 + rsync -ahPv dist/* frs.sourceforge.net:/home/frs/project/sqlobject/sqlobject/"$version"/ && + rsync -ahPv ANNOUNCE.rst frs.sourceforge.net:/home/frs/project/sqlobject/sqlobject/"$version"/README.rst || exit 1 devscripts/sftp-frs fi && -twine upload --skip-existing dist/* && -exec rm -rf build dist docs/html SQLObject.egg-info +twine upload --disable-progress-bar --skip-existing dist/* && +exec rm -rf build dist docs/html sqlobject.egg-info diff --git a/devscripts/requirements/requirements.txt b/devscripts/requirements/requirements.txt index b8113c5c..02833990 100644 --- a/devscripts/requirements/requirements.txt +++ b/devscripts/requirements/requirements.txt @@ -1,5 +1,8 @@ ---install-option=-O2 +# DateTime from Zope +DateTime -FormEncode >= 1.1.1, != 1.3.0; python_version >= '2.7' and python_version < '3.0' +FormEncode >= 1.1.1, != 1.3.0; python_version == '2.7' FormEncode >= 1.3.1; python_version >= '3.4' +FormEncode >= 2.1.1; python_version >= '3.13' + PyDispatcher >= 2.0.4 diff --git a/devscripts/requirements/requirements_connector_python.txt b/devscripts/requirements/requirements_connector_python.txt new file mode 100644 index 00000000..0cee2ac1 --- /dev/null +++ b/devscripts/requirements/requirements_connector_python.txt @@ -0,0 +1,10 @@ +mysql-connector-python <= 8.0.23; python_version == '2.7' +protobuf < 3.19; python_version == '3.4' +mysql-connector-python <= 8.0.22, > 2.0; python_version == '3.4' +mysql-connector-python <= 8.0.23, >= 8.0.5; python_version == '3.5' +mysql-connector-python <= 8.0.28, >= 8.0.6; python_version == '3.6' +mysql-connector-python <= 8.0.29, >= 8.0.13; python_version == '3.7' +mysql-connector-python <= 8.0.29, >= 8.0.19; python_version == '3.8' +mysql-connector-python <= 8.0.29, >= 8.0.24; python_version == '3.9' +mysql-connector-python <= 8.0.29, >= 8.0.28; python_version == '3.10' +mysql-connector-python >= 8.0.29; python_version >= '3.11' diff --git a/devscripts/requirements/requirements_mysqlclient.txt b/devscripts/requirements/requirements_mysqlclient.txt new file mode 100644 index 00000000..53611366 --- /dev/null +++ b/devscripts/requirements/requirements_mysqlclient.txt @@ -0,0 +1,9 @@ +mysqlclient == 2.0.3; python_version == '3.6' and sys_platform == 'win32' +mysqlclient == 2.0.3; python_version == '3.7' and sys_platform == 'win32' +mysqlclient == 2.1.1; python_version == '3.8' and sys_platform == 'win32' +mysqlclient >= 2.2.7; python_version == '3.9' and sys_platform == 'win32' +mysqlclient >= 2.2.7; python_version == '3.10' and sys_platform == 'win32' +mysqlclient >= 2.2.7; python_version == '3.11' and sys_platform == 'win32' +mysqlclient >= 2.2.7; python_version == '3.12' and sys_platform == 'win32' +mysqlclient >= 2.2.7; python_version == '3.13' and sys_platform == 'win32' +mysqlclient; sys_platform != 'win32' diff --git a/devscripts/requirements/requirements_pg8000.txt b/devscripts/requirements/requirements_pg8000.txt new file mode 100644 index 00000000..06cd178f --- /dev/null +++ b/devscripts/requirements/requirements_pg8000.txt @@ -0,0 +1,3 @@ +pg8000 < 1.13; python_version == '2.7' +pg8000 < 1.12.4; python_version == '3.4' +pg8000; python_version >= '3.5' diff --git a/devscripts/requirements/requirements_pygresql.txt b/devscripts/requirements/requirements_pygresql.txt new file mode 100644 index 00000000..5fbb3514 --- /dev/null +++ b/devscripts/requirements/requirements_pygresql.txt @@ -0,0 +1,2 @@ +pygresql < 5.2; python_version == '3.4' +pygresql; python_version != '3.4' diff --git a/devscripts/requirements/requirements_pymysql.txt b/devscripts/requirements/requirements_pymysql.txt new file mode 100644 index 00000000..4333b47c --- /dev/null +++ b/devscripts/requirements/requirements_pymysql.txt @@ -0,0 +1,4 @@ +pymysql < 1.0; python_version == '2.7' or python_version == '3.5' +pymysql < 0.10.0; python_version == '3.4' +pymysql < 1.0.3; python_version == '3.6' +pymysql; python_version >= '3.7' diff --git a/devscripts/requirements/requirements_tests.txt b/devscripts/requirements/requirements_tests.txt index 9d647535..c328e74a 100644 --- a/devscripts/requirements/requirements_tests.txt +++ b/devscripts/requirements/requirements_tests.txt @@ -1,5 +1,9 @@ -r requirements.txt +setuptools 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' and python_version <= '3.11' +pytest; python_version >= '3.12' + +pendulum < 2.1; python_version == '3.4' +pendulum; python_version == '2.7' or (python_version >= '3.5' and python_version <= '3.11') diff --git a/devscripts/requirements/requirements_tox.txt b/devscripts/requirements/requirements_tox.txt index 8c9b6ed3..b72a322f 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/devscripts/setup b/devscripts/setup deleted file mode 100755 index 04ed1b78..00000000 --- a/devscripts/setup +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/sh - -umask 022 # -rwxr-xr-x -cd "`dirname \"$0\"`"/.. && - -for py_ver in 2.7 3.4 3.5 3.6 3.7; do - python$py_ver -m pip install --install-option=-O2 --upgrade . -done && - -exec rm -rf build dist SQLObject.egg-info MANIFEST *.egg diff --git a/devscripts/split.sh b/devscripts/split_tag.sh similarity index 81% rename from devscripts/split.sh rename to devscripts/split_tag.sh index ce5c3c5e..d0024f04 100644 --- a/devscripts/split.sh +++ b/devscripts/split_tag.sh @@ -1,6 +1,6 @@ split_tag() { branch=$2 - set -- `echo $1 | sed -e 's/\./ /g' -e 's/a/ alpha /' -e 's/b/ beta /' -e 's/rc/ rc /' -e 's/\([0-9]\)c/\1 rc /'` + set -- `echo $1 | sed -e 's/\./ /g' -e 's/a/ alpha /' -e 's/b/ beta /' -e 's/rc/ rc /' -e 's/\([0-9]\)c/\1 rc /' -e 's/post\([0-9]\+\)/ post \1/'` major=$1 minor=$2 micro=$3 diff --git a/devscripts/test-split.sh b/devscripts/test-split_tag.sh similarity index 69% rename from devscripts/test-split.sh rename to devscripts/test-split_tag.sh index a5f17495..40b9d3d4 100755 --- a/devscripts/test-split.sh +++ b/devscripts/test-split_tag.sh @@ -1,6 +1,6 @@ #! /bin/sh -. `dirname $0`/split.sh && +. `dirname $0`/split_tag.sh && test_eq() { if [ "$1" != "$2" ]; then @@ -23,3 +23,11 @@ test_eq "$minor" 12 test_eq "$micro" 42 test_eq "$state" "release candidate" test_eq "$serial" 4 + +split_tag 21.12.42.post1 +test_eq "$branch" 21.12 +test_eq "$major" 21 +test_eq "$minor" 12 +test_eq "$micro" 42 +test_eq "$state" "post" +test_eq "$serial" 1 diff --git a/devscripts/tox-select-envs b/devscripts/tox-select-envs index 34ff4b2c..f1e9a817 100755 --- a/devscripts/tox-select-envs +++ b/devscripts/tox-select-envs @@ -2,7 +2,7 @@ pattern="$1" shift -envs="`tox --listenvs-all | grep -F $pattern | sed 's/$/,/'`" +envs="`tox --listenvs-all | grep -F $pattern | grep -v 'noauto\|w32' | sed 's/$/,/'`" if [ -n "$envs" ]; then exec tox -e "$envs" "$@" diff --git a/devscripts/tox-select-envs.cmd b/devscripts/tox-select-envs.cmd new file mode 100644 index 00000000..1aa15195 --- /dev/null +++ b/devscripts/tox-select-envs.cmd @@ -0,0 +1,19 @@ +@echo off +SetLocal EnableDelayedExpansion + +set "pattern=%1" +shift + +set "envs=" +for /f "usebackq" %%e in ( + `tox --listenvs-all ^| find "%pattern%" ^| find "-w32" ^| find /v "noauto"` +) do ( + if defined envs (set "envs=!envs!,%%e") else (set "envs=%%e") +) + +if not "%envs%"=="" ( + tox -e "%envs%" %* +) else ( + echo "No environments match %pattern%" >&2 + exit /b 1 +) diff --git a/docs/Authors.rst b/docs/Authors.rst index 92f936ea..46d00465 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -22,7 +22,6 @@ Contributions have been made by: * Dan Pascu * Diez B. Roggisch * Christopher Singley -* David Keeney * Daniel Fetchinson * Neil Muller * Petr Jakes @@ -37,6 +36,12 @@ Contributions have been made by: * Shailesh Mungikar * Michael S. Root * Scott Stahl +* Markus Elfring +* James Hudson +* Juergen Gmach +* Hugo van Kemenade +* Igor Yudytskiy +* Dave Mulligan (https://github.com/DaveMulligan95060) * Oleg Broytman .. image:: https://sourceforge.net/sflogo.php?group_id=74338&type=10 diff --git a/docs/DeveloperGuide.rst b/docs/DeveloperGuide.rst index 966c379d..777aea14 100644 --- a/docs/DeveloperGuide.rst +++ b/docs/DeveloperGuide.rst @@ -14,13 +14,13 @@ Development Installation First install `FormEncode `_:: - $ git clone git://github.com/formencode/formencode.git + $ git clone https://github.com/formencode/formencode.git $ cd formencode $ sudo python setup.py develop Then do the same for SQLObject:: - $ git clone git clone git://github.com/sqlobject/sqlobject.git + $ git clone git clone https://github.com/sqlobject/sqlobject.git $ cd sqlobject $ sudo python setup.py develop @@ -125,6 +125,9 @@ Python Style Guide. Some things to take particular note of: .. _PEP 8: http://www.python.org/dev/peps/pep-0008/ +* With some exceptions sources must be pure ASCII. Including string + literals and comments. + * With a few exceptions sources must be `flake8`_-clean (and hence pep8-clean). Please consider using pre-commit hook installed by running ``flake8 --install-hook``. @@ -196,6 +199,9 @@ Python Style Guide. Some things to take particular note of: Don't use single quotes ('''). Don't bother trying make the string less vertically compact. + Not strictly required but ``reStructuredText`` format for docstrings is + very much recommended. + * Comments go right before the thing they are commenting on. * Methods never, ever, ever start with capital letters. Generally @@ -270,36 +276,33 @@ forced to write the test. That's no fun for us, to just be writing tests. So please, write tests; everything at least needs to be exercised, even if the tests are absolutely complete. -We now use Travis CI and AppVeyor to run tests. See the statuses: +We now use `Github Actions `_ +to run tests. -.. image:: https://travis-ci.org/sqlobject/sqlobject.svg?branch=master - :target: https://travis-ci.org/sqlobject/sqlobject +.. image:: https://github.com/sqlobject/sqlobject/actions/workflows/run-tests.yaml/badge.svg?branch=github-actions + :target: https://github.com/sqlobject/sqlobject/actions/workflows/run-tests.yaml -.. image:: https://ci.appveyor.com/api/projects/status/github/sqlobject/sqlobject?branch=master - :target: https://ci.appveyor.com/project/phdru/sqlobject +Documentation +============= -To avoid triggering unnecessary test run at CI services add text `[skip ci] -`_ or -``[ci skip]`` anywhere in your commit messages for commits that don't change -code (documentation updates and such). +Please write documentation. Documentation should live in the docs/ +directory in ``reStructuredText`` format. We use Sphinx to convert docs to +HTML. -We use `coverage.py `_ -to measures code coverage by tests and upload the result for analyzis to -`Coveralls `_ and -`Codecov `_: +Contributing +============ -.. image:: https://coveralls.io/repos/github/sqlobject/sqlobject/badge.svg?branch=master - :target: https://coveralls.io/github/sqlobject/sqlobject?branch=master +* Now de-facto `stadard for good commit messages + `_ is required. -.. image:: https://codecov.io/gh/sqlobject/sqlobject/branch/master/graph/badge.svg - :target: https://codecov.io/gh/sqlobject/sqlobject +* `Conventional commit subject liness + `_ are recommended. -Documentation -============= +* ``Markdown`` format for commit message bodies is recommended. + `Github-flavored Markdown `_ is allowed. -Please write documentation. Documentation should live in the docs/ -directory in reStructuredText format. We use Sphinx to convert docs to -HTML. +* Commit messages must be pure ASCII. No fancy Unicode emojies, + quotes, etc. .. image:: https://sourceforge.net/sflogo.php?group_id=74338&type=10 :target: https://sourceforge.net/projects/sqlobject diff --git a/docs/News.rst b/docs/News.rst index 1b968833..e01b1376 100644 --- a/docs/News.rst +++ b/docs/News.rst @@ -5,402 +5,293 @@ News .. contents:: Contents: :backlinks: none -SQLObject 3.8.0 (master) -======================== +SQLObject development (master) +============================== -SQLObject 3.7.1 -=============== +SQLObject 3.13.1 +================ -Released 2 Feb 2019. +Released 2025 Dec 08. Bug fixes --------- -* Fixed a unicode problem in the latest mysqlclient. +* ``UuidValidator.from_python()`` now accepts strings as a valid input. + This fixes #199. -Documentation -------------- - -* Exclude sqlmeta members from some of the api docs. - The inclusion of of these sqlmeta members in these files breaks - reproducible builds. - -Development ------------ - -* Source code was made flake8-clean using the latest flake8. - -CI --- - -* Run tests with Python 3.7. - -SQLObject 3.7.0 -=============== - -Released 6 June 2018. - -Features --------- - -* Add signals on commit and rollback; pull request by Scott Stahl. +* Fixed #197: a bug in ``dbconnection.ConnectionURIOpener.registerConnection`` + triggered by non-empty instance's ``name``. The bug was inserted in 2004 so + it seems nobody ever used named instances. Fixed anyway. -Bug fixes ---------- - -* Fix SSL-related parameters for MySQL-connector (connector uses - a different param style). Bug reported by Christophe Popov. - -Drivers -------- - -* Remove psycopg1. Driver ``psycopg`` is now just an alias for ``psycopg2``. +* Fixed #195: Minor ``NameError`` in ``pgconnection.py`` + when using ``psycopg`` version 1 with a non-default port. Tests ----- -* Install psycopg2 from `psycopg2-binary`_ package. - -.. _`psycopg2-binary`: https://pypi.org/project/psycopg2-binary/ +* Tested with Python 3.14. -SQLObject 3.6.0 -=============== +* Run tests with source-only (non-binary) ``psycopg`` and ``psycopg2``. -Released 24 Feb 2018. - -Minor features --------------- - -* Close cursors after using to free resources immediately - instead of waiting for gc. - -Bug fixes ---------- +SQLObject 3.13.0 +================ -* Fix for TypeError using selectBy on a BLOBCol. PR by Michael S. Root. +Released 2025 Mar 07. Drivers ------- -* Extend support for oursql and Python 3 (requires our fork of the driver). +* Extended default list of MySQL drivers to ``mysqldb``, ``mysqlclient``, + ``mysql-connector``, ``mysql-connector-python``, ``pymysql``. -* Fix cursor.arraysize - pymssql doesn't have arraysize. +* Extended default list of PostgreSQL drivers to ``psycopg``, ``psycopg2``, + ``pygresql``, ``pg8000``. -* Set timeout for ODBC with MSSQL. +* Fixed outstanding problems with ``psycopg``. It's now the first class driver. -* Fix _setAutoCommit for MSSQL. +* Fixed all problems with ``pg8000``. It's now the first class driver. -Documentation -------------- +* Dropped support for ``CyMySQL``; + its author refused to fix unicode-related problems. -* Document extras that are available for installation. +* Dropped support for ``py-postgresql``; it's completely broken + with debianized ``Postgres`` and the authors reject fixes. -Build +Tests ----- -* Use ``python_version`` environment marker in ``setup.py`` to make - ``install_requires`` and ``extras_require`` declarative. This makes - the universal wheel truly universal. - -* Use ``python_requires`` keyword in ``setup.py``. +* Added tests for ``mysqldb`` (aka ``mysql-python``) + and ``mysqlclient`` on w32. -SQLObject 3.5.0 -=============== +* Improved tests of ``mysql-connector`` and ``mysql-connector-python``. -Released 15 Nov 2017. - -Minor features --------------- +CI +-- -* Add Python3 special methods for division to SQLExpression. - Pull request by Michael S. Root. +* Tests(GHActions): Fixed old bugs in the workflow on w32. -Drivers -------- +* Run tests with ``psycopg[c]``. -* Add support for `pg8000 `_ - PostgreSQL driver. +SQLObject 3.12.0.post2 +====================== -* Fix autoreconnect with pymysql driver. Contributed by Shailesh Mungikar. +Released 2025 Feb 01. -Documentation -------------- +Installation/dependencies +------------------------- -* Remove generated HTML from eggs/wheels (docs are installed into wrong - place). Generated docs are still included in the source distribution. +* Use ``FormEncode`` 2.1.1 for Python 3.13. -Tests ------ +SQLObject 3.12.0 +================ -* Add tests for PyGreSQL, py-postgresql and pg8000 at AppVeyor. +Released 2024 Dec 20. -* Fixed bugs in py-postgresql at AppVeyor. SQLObject requires - the latest version of the driver from our fork. +Drivers +------- -SQLObject 3.4.0 -=============== +* Add support for CyMySQL; there're some problems with unicode yet. -Released 5 Aug 2017. +* Separate ``psycopg`` and ``psycopg2``; + ``psycopg`` is actually ``psycopg3`` now; not all tests pass. -Features --------- +* Minor fix in getting error code from PyGreSQL. -* Python 2.6 is no longer supported. The minimal supported version is - Python 2.7. +* Dropped ``oursql``. It wasn't updated in years. -Drivers (work in progress) --------------------------- +* Dropped ``PySQLite2``. Only builtin ``sqlite3`` is supported. -* Encode binary values for py-postgresql driver. This fixes the - last remaining problems with the driver. - -* Encode binary values for PyGreSQL driver using the same encoding as for - py-postgresql driver. This fixes the last remaining problems with the driver. +Tests +----- - Our own encoding is needed because unescape_bytea(escape_bytea()) is not - idempotent. See the comment for PQunescapeBytea at - https://www.postgresql.org/docs/9.6/static/libpq-exec.html: +* Run tests with Python 3.13. - This conversion is not exactly the inverse of PQescapeBytea, because the - string is not expected to be "escaped" when received from PQgetvalue. In - particular this means there is no need for string quoting considerations. +* Run tests with ``psycopg-c``; not all tests pass. -* List all drivers in extras_require in setup.py. +* Fix ``test_exceptions.py`` under MariaDB, PostgreSQL and SQLite. -Minor features --------------- +* ``py-postgres``: Set ``sslmode`` to ``allow``; + upstream changed default to ``prefer``. -* Use base64.b64encode/b64decode instead of deprecated - encodestring/decodestring. +CI +-- -Tests ------ +* Run tests with ``PyGreSQL`` on w32, do not ignore errors. -* Fix a bug with sqlite-memory: rollback transaction and close connection. - The solution was found by Dr. Neil Muller. +* Skip tests with ``pg8000`` on w32. -* Use remove-old-files.py from ppu to cleanup pip cache - at Travis and AppVeyor. +* GHActions: Switch to ``setup-miniconda``. -* Add test_csvimport.py more as an example how to use load_csv - from sqlobject.util.csvimport. +* GHActions: Python 3.13. -SQLObject 3.3.0 -=============== +SQLObject 3.11.0 +================ -Released 7 May 2017. +Released 2023 Nov 11. Features -------- -* Support for Python 2.6 is declared obsolete and will be removed - in the next release. +* Continue working on ``SQLRelatedJoin`` aliasing introduced in 3.10.2. + When a table joins with itself calling + ``relJoinCol.filter(thisClass.q.column)`` raises ``ValueError`` + hinting that an alias is required for filtering. -Minor features --------------- +* Test that ``idType`` is either ``int`` or ``str``. -* Convert scripts repository to devscripts subdirectory. - Some of thses scripts are version-dependent so it's better to have them - in the main repo. +* Added ``sqlmeta.idSize``. This sets the size of integer column ``id`` + for MySQL and PostgreSQL. Allowed values are ``'TINY'``, ``'SMALL'``, + ``'MEDIUM'``, ``'BIG'``, ``None``; default is ``None``. For Postgres + mapped to ``smallserial``/``serial``/``bigserial``. For other backends + it's currently ignored. Feature request by Meet Gujrathi at + https://stackoverflow.com/q/77360075/7976758 -* Test for __nonzero__ under Python 2, __bool__ under Python 3 in BoolCol. +SQLObject 3.10.3 +================ -Drivers (work in progress) --------------------------- +Released 2023 Oct 25. -* Add support for PyODBC and PyPyODBC (pure-python ODBC DB API driver) for - MySQL, PostgreSQL and MS SQL. Driver names are ``pyodbc``, ``pypyodbc`` - or ``odbc`` (try ``pyodbc`` and ``pypyodbc``). There are some problems - with pyodbc and many problems with pypyodbc. +Bug fixes +--------- -Documentation -------------- +* Relaxed aliasing in ``SQLRelatedJoin`` introduced in 3.10.2 - aliasing + is required only when the table joins with itself. When there're two + tables to join aliasing prevents filtering -- wrong SQL is generated + in ``relJoinCol.filter(thisClass.q.column)``. + +Drivers +------- -* Stop updating http://sqlobject.readthedocs.org/ - it's enough to have - http://sqlobject.org/ +* Fix(SQLiteConnection): Release connections from threads that are + no longer active. This fixes memory leak in multithreaded programs + in Windows. + + ``SQLite`` requires different connections per thread so + ``SQLiteConnection`` creates and stores a connection per thread. + When a thread finishes its connections should be closed. + But if a program doesn't cooperate and doesn't close connections at + the end of a thread SQLObject leaks memory as connection objects are + stuck in ``SQLiteConnection``. On Linux the leak is negligible as + Linux reuses thread IDs so new connections replace old ones and old + connections are garbage collected. But Windows doesn't reuse thread + IDs so old connections pile and never released. To fix the problem + ``SQLiteConnection`` now enumerates threads and releases connections + from non-existing threads. + +* Dropped ``supersqlite``. It seems abandoned. + The last version 0.0.78 was released in 2018. Tests ----- -* Run tests at Travis CI and AppVeyor with Python 3.6, x86 and x64. +* Run tests with Python 3.12. -* Stop running tests at Travis with Python 2.6. +CI +-- -* Stop running tests at AppVeyor with pymssql - too many timeouts and - problems. +* GHActions: Ensure ``pip`` only if needed -SQLObject 3.2.0 -=============== + This is to work around a problem in conda with Python 3.7 - + it brings in wrong version of ``setuptools`` incompatible with Python 3.7. -Released 11 Mar 2017. +SQLObject 3.10.2 +================ + +Released 2023 Aug 09. Minor features -------------- -* Drop table name from ``VACUUM`` command in SQLiteConnection: SQLite - doesn't vacuum a single table and SQLite 3.15 uses the supplied name as - the name of the attached database to vacuum. - -* Remove ``driver`` keyword from RdbhostConnection as it allows one driver - ``rdbhdb``. - -* Add ``driver`` keyword for FirebirdConnection. Allowed values are 'fdb', - 'kinterbasdb' and 'pyfirebirdsql'. Default is to test 'fdb' and - 'kinterbasdb' in that order. pyfirebirdsql is supported but has problems. - -* Add ``driver`` keyword for MySQLConnection. Allowed values are 'mysqldb', - 'connector', 'oursql' and 'pymysql'. Default is to test for mysqldb only. - -* Add support for `MySQL Connector - `_ (pure python; `binary - packages `_ are not at - PyPI and hence are hard to install and test). - -* Add support for `oursql `_ MySQL - driver (only Python 2.6 and 2.7 until oursql author fixes Python 3 - compatibility). - -* Add support for `PyMySQL `_ - pure - python mysql interface). - -* Add parameter ``timeout`` for MSSQLConnection (usable only with pymssql - driver); timeouts are in seconds. - -* Remove deprecated ez_setup.py. - -Drivers (work in progress) --------------------------- - -* Extend support for PyGreSQL driver. There are still some problems. - -* Add support for `py-postgresql - `_ PostgreSQL driver. There - are still problems with the driver. - -* Add support for `pyfirebirdsql - `_.There are still problems with - the driver. +* Class ``Alias`` grows a method ``.select()`` to match ``SQLObject.select()``. Bug fixes --------- -* Fix MSSQLConnection.columnsFromSchema: remove `(` and `)` from default - value. +* Fixed a bug in ``SQLRelatedJoin`` in the case where the table joins with + itself; in the resulting SQL two instances of the table must use different + aliases. -* Fix MSSQLConnection and SybaseConnection: insert default values into a table - with just one IDENTITY column. +CI +-- -* Remove excessive NULLs from ``CREATE TABLE`` for MSSQL/Sybase. +* Install all Python and PyPy versions from ``conda-forge``. -* Fix concatenation operator for MSSQL/Sybase (it's ``+``, not ``||``). +SQLObject 3.10.1 +================ -* Fix MSSQLConnection.server_version() under Py3 (decode version to str). +Released 2022 Dec 22. -Documentation -------------- +Minor features +-------------- -* The docs are now generated with Sphinx. +* Use ``module_loader.exec_module(module_loader.create_module())`` + instead of ``module_loader.load_module()`` when available. -* Move ``docs/LICENSE`` to the top-level directory so that Github - recognizes it. +Drivers +------- + +* Added ``mysql-connector-python``. Tests ----- -* Rename ``py.test`` -> ``pytest`` in tests and docs. - -* Great Renaming: fix ``pytest`` warnings by renaming ``TestXXX`` classes - to ``SOTestXXX`` to prevent ``pytest`` to recognize them as test classes. - -* Fix ``pytest`` warnings by converting yield tests to plain calls: yield - tests were deprecated in ``pytest``. +* Run tests with Python 3.11. -* Tests are now run at CIs with Python 3.5. - -* Drop ``Circle CI``. - -* Run at Travis CI tests with Firebird backend (server version 2.5; - drivers fdb and firebirdsql). There are problems with tests. +CI +-- -* Run tests at AppVeyor for windows testing. Run tests with MS SQL, - MySQL, Postgres and SQLite backends; use Python 2.7, 3.4 and 3.5, - x86 and x64. There are problems with MS SQL and MySQL. +* Ubuntu >= 22 and ``setup-python`` dropped Pythons < 3.7. + Use ``conda`` via ``s-weigand/setup-conda`` instead of ``setup-python`` + to install older Pythons on Linux. -SQLObject 3.1.0 -=============== +SQLObject 3.10.0 +================ -Released 16 Aug 2016. +Released 2022 Sep 20. Features -------- -* Add UuidCol. - -* Add JsonbCol. Only for PostgreSQL. - Requires psycopg2 >= 2.5.4 and PostgreSQL >= 9.2. - -* Add JSONCol, a universal json column. - -* For Python >= 3.4 minimal FormEncode version is now 1.3.1. - -* If mxDateTime is in use, convert timedelta (returned by MySQL) to - mxDateTime.Time. - -Documentation -------------- +* Allow connections in ``ConnectionHub`` to be strings. + This allows to open a new connection in every thread. -* Developer's Guide is extended to explain SQLObject architecture - and how to create a new column type. - -* Fix URLs that can be found; remove missing links. - -* Rename reStructuredText files from \*.txt to \*.rst. - -Source code ------------ - -* Fix all `import *` using https://github.com/zestyping/star-destroyer. +* Add compatibility with ``Pendulum``. Tests ----- -* Tests are now run at Circle CI. +* Run tests with Python 3.10. -* Use pytest-cov for test coverage. Report test coverage - via coveralls.io and codecov.io. +CI +-- -* Install mxDateTime to run date/time tests with it. +* GitHub Actions. -SQLObject 3.0.0 -=============== +* Stop testing at Travis CI. -Released 1 Jun 2016. +* Stop testing at AppVeyor. -Features --------- +Documentation +------------- -* Support for Python 2 and Python 3 with one codebase! - (Python version >= 3.4 currently required.) +* DevGuide: source code must be pure ASCII. -Minor features --------------- +* DevGuide: ``reStructuredText`` format for docstrings is recommended. -* PyDispatcher (>=2.0.4) was made an external dependency. +* DevGuide: de-facto good commit message format is required: + subject/body/trailers. -Development ------------ +* DevGuide: ``conventional commit`` format for commit message subject lines + is recommended. -* Source code was made flake8-clean. +* DevGuide: ``Markdown`` format for commit message bodies is recommended. -Documentation -------------- +* DevGuide: commit messages must be pure ASCII. -* Documentation is published at http://sqlobject.readthedocs.org/ in - Sphinx format. `Older news`__ -.. __: News5.html +.. __: News6.html .. image:: https://sourceforge.net/sflogo.php?group_id=74338&type=10 :target: https://sourceforge.net/projects/sqlobject diff --git a/docs/News6.rst b/docs/News6.rst new file mode 100644 index 00000000..313c62b3 --- /dev/null +++ b/docs/News6.rst @@ -0,0 +1,566 @@ +++++ +News +++++ + +.. contents:: Contents: + :backlinks: none + +SQLObject 3.9.1 +=============== + +Released 2021 Feb 27. + +Drivers +------- + +* Adapt to the latest ``pg8000``. + +* Protect ``getuser()`` - it can raise ``ImportError`` on w32 + due to absent of ``pwd`` module. + +Build +----- + +* Change URLs for ``oursql`` in ``extras_require`` in ``setup.py``. + Provide separate URLs for Python 2.7 and 3.4+. + +* Add ``mariadb`` in ``extras_require`` in ``setup.py``. + +CI +-- + +* For tests with Python 3.4 run ``tox`` under Python 3.5. + +Tests +----- + +* Refactor ``tox.ini``. + +SQLObject 3.9.0 +=============== + +Released 2020 Dec 15. + +Features +-------- + +* Add ``JSONCol``: a universal json column that converts simple Python objects + (None, bool, int, float, long, dict, list, str/unicode to/from JSON using + json.dumps/loads. A subclass of StringCol. Requires ``VARCHAR``/``TEXT`` + columns at backends, doesn't work with ``JSON`` columns. + +* Extend/fix support for ``DateTime`` from ``Zope``. + +* Drop support for very old version of ``mxDateTime`` + without ``mx.`` namespace. + +Drivers +------- + +* Support `mariadb `_. + +CI +-- + +* Run tests with Python 3.9 at Travis and AppVeyor. + +SQLObject 3.8.1 +=============== + +Released 2020 Oct 01. + +Documentation +------------- + +* Use conf.py options to exclude sqlmeta options. + +Tests +----- + +* Fix ``PyGreSQL`` version for Python 3.4. + +CI +-- + +* Run tests with Python 3.8 at AppVeyor. + +SQLObject 3.8.0 +=============== + +Released 7 Dec 2019. + +Features +-------- + +* Add driver ``supersqlite``. Not all tests are passing + so the driver isn't added to the list of default drivers. + +Minor features +-------------- + +* Improve sqlrepr'ing ``ALL/ANY/SOME()``: always put the expression + at the right side of the comparison operation. + +Bug fixes +--------- + +* Fixed a bug in cascade deletion/nullification. + +* Fixed a bug in ``PostgresConnection.columnsFromSchema``: + PostgreSQL 12 removed outdated catalog attribute + ``pg_catalog.pg_attrdef.adsrc``. + +* Fixed a bug working with microseconds in Time columns. + +CI +-- + +* Run tests with Python 3.8 at Travis CI. + +SQLObject 3.7.3 +=============== + +Released 22 Sep 2019. + +Bug fixes +--------- + +* Avoid excessive parentheses around ``ALL/ANY/SOME()``. + +Tests +----- + +* Add tests for cascade deletion. + +* Add tests for ``sqlbuilder.ALL/ANY/SOME()``. + +* Fix calls to ``pytest.mark.skipif`` - make conditions bool instead of str. + +* Fix module-level calls to ``pytest.mark.skip`` - add reasons. + +* Fix escape sequences ``'\%'`` -> ``'\\%'``. + +CI +-- + +* Reduce the number of virtual machines/containers: + one OS, one DB, one python version, many drivers per VM. + +* Fix sqlite test under Python 3.7+ at AppVeyor. + +SQLObject 3.7.2 +=============== + +Released 1 May 2019. + +Minor features +-------------- + +* Adapt Postgres exception handling to ``psycopg2`` version ``2.8``: + in the recent ``psycopg2`` errors are in ``psycopg2.errors`` module. + +* Removed RdbhostConnection: David Keeney and rdbhost seem to be unavailable + since 2017. + +SQLObject 3.7.1 +=============== + +Released 2 Feb 2019. + +Bug fixes +--------- + +* Fixed a unicode problem in the latest mysqlclient. + +Documentation +------------- + +* Exclude sqlmeta members from some of the api docs. + The inclusion of of these sqlmeta members in these files breaks + reproducible builds. + +Development +----------- + +* Source code was made flake8-clean using the latest flake8. + +CI +-- + +* Run tests with Python 3.7. + +SQLObject 3.7.0 +=============== + +Released 6 June 2018. + +Features +-------- + +* Add signals on commit and rollback; pull request by Scott Stahl. + +Bug fixes +--------- + +* Fix SSL-related parameters for MySQL-connector (connector uses + a different param style). Bug reported by Christophe Popov. + +Drivers +------- + +* Remove psycopg1. Driver ``psycopg`` is now just an alias for ``psycopg2``. + +Tests +----- + +* Install psycopg2 from `psycopg2-binary`_ package. + +.. _`psycopg2-binary`: https://pypi.org/project/psycopg2-binary/ + +SQLObject 3.6.0 +=============== + +Released 24 Feb 2018. + +Minor features +-------------- + +* Close cursors after using to free resources immediately + instead of waiting for gc. + +Bug fixes +--------- + +* Fix for TypeError using selectBy on a BLOBCol. PR by Michael S. Root. + +Drivers +------- + +* Extend support for oursql and Python 3 (requires our fork of the driver). + +* Fix cursor.arraysize - pymssql doesn't have arraysize. + +* Set timeout for ODBC with MSSQL. + +* Fix _setAutoCommit for MSSQL. + +Documentation +------------- + +* Document extras that are available for installation. + +Build +----- + +* Use ``python_version`` environment marker in ``setup.py`` to make + ``install_requires`` and ``extras_require`` declarative. This makes + the universal wheel truly universal. + +* Use ``python_requires`` keyword in ``setup.py``. + +SQLObject 3.5.0 +=============== + +Released 15 Nov 2017. + +Minor features +-------------- + +* Add Python3 special methods for division to SQLExpression. + Pull request by Michael S. Root. + +Drivers +------- + +* Add support for `pg8000 `_ + PostgreSQL driver. + +* Fix autoreconnect with pymysql driver. Contributed by Shailesh Mungikar. + +Documentation +------------- + +* Remove generated HTML from eggs/wheels (docs are installed into wrong + place). Generated docs are still included in the source distribution. + +Tests +----- + +* Add tests for PyGreSQL, py-postgresql and pg8000 at AppVeyor. + +* Fixed bugs in py-postgresql at AppVeyor. SQLObject requires + the latest version of the driver from our fork. + +SQLObject 3.4.0 +=============== + +Released 5 Aug 2017. + +Features +-------- + +* Python 2.6 is no longer supported. The minimal supported version is + Python 2.7. + +Drivers (work in progress) +-------------------------- + +* Encode binary values for py-postgresql driver. This fixes the + last remaining problems with the driver. + +* Encode binary values for PyGreSQL driver using the same encoding as for + py-postgresql driver. This fixes the last remaining problems with the driver. + + Our own encoding is needed because unescape_bytea(escape_bytea()) is not + idempotent. See the comment for PQunescapeBytea at + https://www.postgresql.org/docs/9.6/static/libpq-exec.html: + + This conversion is not exactly the inverse of PQescapeBytea, because the + string is not expected to be "escaped" when received from PQgetvalue. In + particular this means there is no need for string quoting considerations. + +* List all drivers in extras_require in setup.py. + +Minor features +-------------- + +* Use base64.b64encode/b64decode instead of deprecated + encodestring/decodestring. + +Tests +----- + +* Fix a bug with sqlite-memory: rollback transaction and close connection. + The solution was found by Dr. Neil Muller. + +* Use remove-old-files.py from ppu to cleanup pip cache + at Travis and AppVeyor. + +* Add test_csvimport.py more as an example how to use load_csv + from sqlobject.util.csvimport. + +SQLObject 3.3.0 +=============== + +Released 7 May 2017. + +Features +-------- + +* Support for Python 2.6 is declared obsolete and will be removed + in the next release. + +Minor features +-------------- + +* Convert scripts repository to devscripts subdirectory. + Some of thses scripts are version-dependent so it's better to have them + in the main repo. + +* Test for __nonzero__ under Python 2, __bool__ under Python 3 in BoolCol. + +Drivers (work in progress) +-------------------------- + +* Add support for PyODBC and PyPyODBC (pure-python ODBC DB API driver) for + MySQL, PostgreSQL and MS SQL. Driver names are ``pyodbc``, ``pypyodbc`` + or ``odbc`` (try ``pyodbc`` and ``pypyodbc``). There are some problems + with pyodbc and many problems with pypyodbc. + +Documentation +------------- + +* Stop updating http://sqlobject.readthedocs.org/ - it's enough to have + http://sqlobject.org/ + +Tests +----- + +* Run tests at Travis CI and AppVeyor with Python 3.6, x86 and x64. + +* Stop running tests at Travis with Python 2.6. + +* Stop running tests at AppVeyor with pymssql - too many timeouts and + problems. + +SQLObject 3.2.0 +=============== + +Released 11 Mar 2017. + +Minor features +-------------- + +* Drop table name from ``VACUUM`` command in SQLiteConnection: SQLite + doesn't vacuum a single table and SQLite 3.15 uses the supplied name as + the name of the attached database to vacuum. + +* Remove ``driver`` keyword from RdbhostConnection as it allows one driver + ``rdbhdb``. + +* Add ``driver`` keyword for FirebirdConnection. Allowed values are 'fdb', + 'kinterbasdb' and 'pyfirebirdsql'. Default is to test 'fdb' and + 'kinterbasdb' in that order. pyfirebirdsql is supported but has problems. + +* Add ``driver`` keyword for MySQLConnection. Allowed values are 'mysqldb', + 'connector', 'connector-python', 'oursql' and 'pymysql'. Default is to + test for mysqldb only. + +* Add support for `MySQL Connector + `_ (pure python; `binary + packages `_ are not at + PyPI and hence are hard to install and test). + +* Add support for `oursql `_ MySQL + driver (only Python 2.6 and 2.7 until oursql author fixes Python 3 + compatibility). + +* Add support for `PyMySQL `_ - pure + python mysql interface). + +* Add parameter ``timeout`` for MSSQLConnection (usable only with pymssql + driver); timeouts are in seconds. + +* Remove deprecated ez_setup.py. + +Drivers (work in progress) +-------------------------- + +* Extend support for PyGreSQL driver. There are still some problems. + +* Add support for `py-postgresql + `_ PostgreSQL driver. There + are still problems with the driver. + +* Add support for `pyfirebirdsql + `_.There are still problems with + the driver. + +Bug fixes +--------- + +* Fix MSSQLConnection.columnsFromSchema: remove `(` and `)` from default + value. + +* Fix MSSQLConnection and SybaseConnection: insert default values into a table + with just one IDENTITY column. + +* Remove excessive NULLs from ``CREATE TABLE`` for MSSQL/Sybase. + +* Fix concatenation operator for MSSQL/Sybase (it's ``+``, not ``||``). + +* Fix MSSQLConnection.server_version() under Py3 (decode version to str). + +Documentation +------------- + +* The docs are now generated with Sphinx. + +* Move ``docs/LICENSE`` to the top-level directory so that Github + recognizes it. + +Tests +----- + +* Rename ``py.test`` -> ``pytest`` in tests and docs. + +* Great Renaming: fix ``pytest`` warnings by renaming ``TestXXX`` classes + to ``SOTestXXX`` to prevent ``pytest`` to recognize them as test classes. + +* Fix ``pytest`` warnings by converting yield tests to plain calls: yield + tests were deprecated in ``pytest``. + +* Tests are now run at CIs with Python 3.5. + +* Drop ``Circle CI``. + +* Run at Travis CI tests with Firebird backend (server version 2.5; + drivers fdb and firebirdsql). There are problems with tests. + +* Run tests at AppVeyor for windows testing. Run tests with MS SQL, + MySQL, Postgres and SQLite backends; use Python 2.7, 3.4 and 3.5, + x86 and x64. There are problems with MS SQL and MySQL. + +SQLObject 3.1.0 +=============== + +Released 16 Aug 2016. + +Features +-------- + +* Add UuidCol. + +* Add JsonbCol. Only for PostgreSQL. + Requires psycopg2 >= 2.5.4 and PostgreSQL >= 9.2. + +* Add JSONCol, a universal json column. + +* For Python >= 3.4 minimal FormEncode version is now 1.3.1. + +* If mxDateTime is in use, convert timedelta (returned by MySQL) to + mxDateTime.Time. + +Documentation +------------- + +* Developer's Guide is extended to explain SQLObject architecture + and how to create a new column type. + +* Fix URLs that can be found; remove missing links. + +* Rename reStructuredText files from \*.txt to \*.rst. + +Source code +----------- + +* Fix all `import *` using https://github.com/zestyping/star-destroyer. + +Tests +----- + +* Tests are now run at Circle CI. + +* Use pytest-cov for test coverage. Report test coverage + via coveralls.io and codecov.io. + +* Install mxDateTime to run date/time tests with it. + +SQLObject 3.0.0 +=============== + +Released 1 Jun 2016. + +Features +-------- + +* Support for Python 2 and Python 3 with one codebase! + (Python version >= 3.4 currently required.) + +Minor features +-------------- + +* PyDispatcher (>=2.0.4) was made an external dependency. + +Development +----------- + +* Source code was made flake8-clean. + +Documentation +------------- + +* Documentation is published at http://sqlobject.readthedocs.org/ in + Sphinx format. + +`Older news`__ + +.. __: News5.html + +.. image:: https://sourceforge.net/sflogo.php?group_id=74338&type=10 + :target: https://sourceforge.net/projects/sqlobject + :class: noborder + :align: center + :height: 15 + :width: 80 + :alt: Get SQLObject at SourceForge.net. Fast, secure and Free Open Source software downloads diff --git a/docs/SQLObject.rst b/docs/SQLObject.rst index 46d820ee..1dd4dcd7 100644 --- a/docs/SQLObject.rst +++ b/docs/SQLObject.rst @@ -46,30 +46,35 @@ used with the same query syntax. Requirements ============ -Currently SQLObject supports MySQL_ via MySQLdb_ aka MySQL-python (called -mysqlclient_ for Python 3), `MySQL Connector`_, oursql_, PyMySQL_, PyODBC_ -and PyPyODBC_. For PostgreSQL_ psycopg2_ is recommended; -PyGreSQL_, py-postgresql_ and pg8000_ are supported; SQLite_ has a -built-in driver or PySQLite_. Firebird_ is supported via fdb_ or -kinterbasdb_; pyfirebirdsql_ is supported but has problems. `MAX DB`_ -(also known as SAP DB) is supported via sapdb_. Sybase via Sybase_. `MSSQL -Server`_ via pymssql_ (+ FreeTDS_) or adodbapi_ (Win32). PyODBC_ and -PyPyODBC_ are supported for MySQL, PostgreSQL and MSSQL but have -problems (not all tests passed). +Currently SQLObject supports MySQL_ and MariaDB_ via MySQLdb_ aka +MySQL-python (called mysqlclient_ for Python 3), `MySQL Connector`_, +PyMySQL_, `mariadb connector`_, PyODBC_ and PyPyODBC_. For +PostgreSQL_ psycopg_ and psycopg2_ are recommended, especially their +precompiled wheels psycopg-binary_ and psycopg2-binary_; see also optimized +psycopg-c_; PyGreSQL_ and pg8000_ are supported; SQLite_ +has a built-in driver. Firebird_ is supported via fdb_ or kinterbasdb_; +pyfirebirdsql_ is supported but has problems. `MAX DB`_ (also known as SAP +DB) is supported via sapdb_. Sybase via Sybase_. `MSSQL Server`_ via +pymssql_ (+ FreeTDS_) or adodbapi_ (Win32). PyODBC_ and PyPyODBC_ are +supported for MySQL, PostgreSQL and MSSQL but have problems (not all tests +passed). .. _MySQL: https://www.mysql.com/ +.. _MariaDB: https://mariadb.org/ .. _MySQLdb: https://sourceforge.net/projects/mysql-python/ .. _mysqlclient: https://pypi.org/project/mysqlclient/ .. _`MySQL Connector`: https://pypi.org/project/mysql-connector/ -.. _oursql: https://github.com/python-oursql/oursql -.. _PyMySQL: https://github.com/PyMySQL/PyMySQL/ +.. _PyMySQL: https://pypi.org/project/PyMySQL/ +.. _mariadb connector: https://pypi.org/project/mariadb/ .. _PostgreSQL: https://postgresql.org -.. _psycopg2: http://initd.org/psycopg/ +.. _psycopg: https://pypi.org/project/psycopg/ +.. _psycopg-binary: https://pypi.org/project/psycopg-binary/ +.. _psycopg-c: https://pypi.org/project/psycopg-c/ +.. _psycopg2: https://www.psycopg.org/ +.. _psycopg2-binary: https://pypi.org/project/psycopg2-binary/ .. _PyGreSQL: http://www.pygresql.org/ -.. _py-postgresql: https://pypi.org/project/py-postgresql/ .. _pg8000: https://pypi.org/project/pg8000/ .. _SQLite: https://sqlite.org/ -.. _PySQLite: https://github.com/ghaering/pysqlite .. _Firebird: http://www.firebirdsql.org/en/python-driver/ .. _fdb: http://www.firebirdsql.org/en/devel-python-driver/ .. _kinterbasdb: http://kinterbasdb.sourceforge.net/ @@ -261,7 +266,7 @@ To create a new object (and row), use class instantiation, like:: .. note:: In SQLObject NULL/None does *not* mean default. NULL is a funny - thing; it mean very different things in different contexts and to + thing; it means very different things in different contexts and to different people. Sometimes it means "default", sometimes "not applicable", sometimes "unknown". If you want a default, NULL or otherwise, you always have to be explicit in your class @@ -301,7 +306,7 @@ Here's a longer example of using the class:: >>> p is p2 True -Columns are accessed like attributes. (This uses the ``property`` +Columns are accessed like attributes (This uses the ``property`` feature of Python, so that retrieving and setting these attributes executes code). Also note that objects are unique -- there is generally only one ``Person`` instance of a particular id in memory at @@ -458,7 +463,7 @@ slicing, this makes batched queries easy to write: the entire result set to sort the items (so it knows which the first ten are), and depending on your query may need to scan through the entire table (depending on your use of indexes). - Indexes are probably the most important way to improve importance + Indexes are probably the most important way to improve performance in a case like this, and you may find caching to be more effective than slicing. @@ -543,8 +548,8 @@ addresses, of course:: Note the column ``person = ForeignKey("Person")``. This is a reference to a `Person` object. We refer to other classes by name -(with a string). In the database there will be a ``person_id`` -column, type ``INT``, which points to the ``person`` column. +(with a string). In the address table there will be a ``person_id`` +column, type ``INT``, which points to the ``person`` table. .. note:: @@ -581,7 +586,7 @@ in-place:: the class definition is equivalent to calling certain class methods (like ``addColumn()``). -Now we can get the backreference with ``aPerson.addresses``, which +Now we can get the backreference with ``Person.addresses``, which returns a list. An example:: >>> p.addresses @@ -762,8 +767,16 @@ values are: is ``id``. `idType`: - A function that coerces/normalizes IDs when setting IDs. This - is ``int`` by default (all IDs are normalized to integers). + A type that coerces/normalizes IDs when setting IDs. Must be ``int`` + or ``str``. This is ``int`` by default (all IDs are normalized to + integers). + +`idSize`: + This sets the size of integer column ``id`` for MySQL and PostgreSQL. + Allowed values are ``'TINY'``, ``'SMALL'``, ``'MEDIUM'``, ``'BIG'``, + ``None``; default is ``None``. For Postgres mapped to + ``smallserial``/``serial``/``bigserial``. For other backends it's + currently ignored. `style`: A style object -- this object allows you to use other algorithms @@ -1243,7 +1256,8 @@ different types of columns, when SQLObject creates your tables. `JSONCol`: A universal json column that converts simple Python objects (None, bool, int, float, long, dict, list, str/unicode to/from JSON using - json.dumps/loads. A subclass of StringCol. + json.dumps/loads. A subclass of StringCol. Requires ``VARCHAR``/``TEXT`` + columns at backends, doesn't work with ``JSON`` columns. `PickleCol`: An extension of BLOBCol; this column can store/retrieve any Python object; @@ -1353,7 +1367,7 @@ Several keyword arguments are allowed to the `MultipleJoin` constructor: have a table ``Product``, and another table has a column ``ProductNo`` that points to this table, then you'd use ``joinColumn="ProductNo"``. WARNING: the argument you pass must - conform to the column name in the database, not to the column in the + conform to the column name in the database, not to the attribute in the class. So, if you have a SQLObject containing the ``ProductNo`` column, this will probably be translated into ``product_no_id`` in the DB (``product_no`` is the normal uppercase- to-lowercase + @@ -1786,9 +1800,10 @@ MySQLConnection supports all the features, though MySQL only supports transactions_ when using the InnoDB backend; SQLObject can explicitly define the backend using ``sqlmeta.createSQL``. -Supported drivers are ``mysqldb``, ``connector``, ``oursql`` and -``pymysql``, ``pyodbc``, ``pypyodbc`` or ``odbc`` (try ``pyodbc`` and -``pypyodbc``); defualt is ``mysqldb``. +Supported drivers are ``mysqldb``, ``connector``, ``pymysql``, +``mariadb``, ``pyodbc``, ``pypyodbc`` or ``odbc`` (try ``pyodbc`` and +``pypyodbc``); default are ``mysqldb``, ``mysqlclient``, +``mysql-connector``, ``mysql-connector-python``, ``pymysql``. Keyword argument ``conv`` allows to pass a list of custom converters. @@ -1832,10 +1847,10 @@ PostgresConnection supports transactions and all other features. The user can choose a DB API driver for PostgreSQL by using a ``driver`` parameter in DB URI or PostgresConnection that can be a comma-separated -list of driver names. Possible drivers are: ``psycopg2``, -``psycopg`` (alias for ``psycopg2``), ``pygresql``, ``pypostgresql``, -``pg8000``, ``pyodbc``, ``pypyodbc`` or ``odbc`` (try ``pyodbc`` and -``pypyodbc``). Default is ``psycopg``. +list of driver names. Possible drivers are: ``psycopg``, ``psycopg2``, +``pygresql``, ``pg8000``, ``pyodbc``, ``pypyodbc`` or ``odbc`` (try +``pyodbc`` and ``pypyodbc``). Default are ``psycopg``, ``psycopg2``, +``pygresql``. Connection-specific parameters are: ``sslmode``, ``unicodeCols``, ``schema``, ``charset``. @@ -1854,12 +1869,6 @@ column -- strings can go in integer columns, dates in integers, etc. SQLite may have concurrency issues, depending on your usage in a multi-threaded environment. -The user can choose a DB API driver for SQLite by using a ``driver`` -parameter in DB URI or SQLiteConnection that can be a comma-separated list -of driver names. Possible drivers are: ``pysqlite2`` (alias ``sqlite2``), -``sqlite3``, ``sqlite`` (alias ``sqlite1``). Default is to test pysqlite2, -sqlite3 and sqlite in that order. - Connection-specific parameters are: ``encoding``, ``mode``, ``timeout``, ``check_same_thread``, ``use_table_info``. diff --git a/docs/TODO.rst b/docs/TODO.rst index 106a5996..0d2c9423 100644 --- a/docs/TODO.rst +++ b/docs/TODO.rst @@ -1,9 +1,7 @@ TODO ---- -* Fix test ordering problem with Postgres. - -* Fix unicode problems with pyodbc at AppVeyor. +* Fix unicode problems with pyodbc. * Resolve timeout problems with MSSQL. diff --git a/docs/api/sqlobject.boundattributes.rst b/docs/api/sqlobject.boundattributes.rst index ebc0ab5a..1518f8da 100644 --- a/docs/api/sqlobject.boundattributes.rst +++ b/docs/api/sqlobject.boundattributes.rst @@ -1,5 +1,5 @@ -sqlobject\.boundattributes module -================================= +sqlobject.boundattributes module +================================ .. automodule:: sqlobject.boundattributes :members: diff --git a/docs/api/sqlobject.cache.rst b/docs/api/sqlobject.cache.rst index 657e1e86..6c74d5be 100644 --- a/docs/api/sqlobject.cache.rst +++ b/docs/api/sqlobject.cache.rst @@ -1,5 +1,5 @@ -sqlobject\.cache module -======================= +sqlobject.cache module +====================== .. automodule:: sqlobject.cache :members: diff --git a/docs/api/sqlobject.classregistry.rst b/docs/api/sqlobject.classregistry.rst index 80ed01a7..08824917 100644 --- a/docs/api/sqlobject.classregistry.rst +++ b/docs/api/sqlobject.classregistry.rst @@ -1,5 +1,5 @@ -sqlobject\.classregistry module -=============================== +sqlobject.classregistry module +============================== .. automodule:: sqlobject.classregistry :members: diff --git a/docs/api/sqlobject.col.rst b/docs/api/sqlobject.col.rst index 4c649dc9..5282fc2f 100644 --- a/docs/api/sqlobject.col.rst +++ b/docs/api/sqlobject.col.rst @@ -1,5 +1,5 @@ -sqlobject\.col module -===================== +sqlobject.col module +==================== .. automodule:: sqlobject.col :members: diff --git a/docs/api/sqlobject.compat.rst b/docs/api/sqlobject.compat.rst index 4191c0cf..cdb4f6dd 100644 --- a/docs/api/sqlobject.compat.rst +++ b/docs/api/sqlobject.compat.rst @@ -1,5 +1,5 @@ -sqlobject\.compat module -======================== +sqlobject.compat module +======================= .. automodule:: sqlobject.compat :members: diff --git a/docs/api/sqlobject.conftest.rst b/docs/api/sqlobject.conftest.rst index e29d059d..8e564281 100644 --- a/docs/api/sqlobject.conftest.rst +++ b/docs/api/sqlobject.conftest.rst @@ -1,5 +1,5 @@ -sqlobject\.conftest module -========================== +sqlobject.conftest module +========================= .. automodule:: sqlobject.conftest :members: diff --git a/docs/api/sqlobject.constraints.rst b/docs/api/sqlobject.constraints.rst index 88a736b3..ad39272b 100644 --- a/docs/api/sqlobject.constraints.rst +++ b/docs/api/sqlobject.constraints.rst @@ -1,5 +1,5 @@ -sqlobject\.constraints module -============================= +sqlobject.constraints module +============================ .. automodule:: sqlobject.constraints :members: diff --git a/docs/api/sqlobject.converters.rst b/docs/api/sqlobject.converters.rst index 38f7777f..82b5cff7 100644 --- a/docs/api/sqlobject.converters.rst +++ b/docs/api/sqlobject.converters.rst @@ -1,5 +1,5 @@ -sqlobject\.converters module -============================ +sqlobject.converters module +=========================== .. automodule:: sqlobject.converters :members: diff --git a/docs/api/sqlobject.dbconnection.rst b/docs/api/sqlobject.dbconnection.rst index 6ab39d10..6a6adc35 100644 --- a/docs/api/sqlobject.dbconnection.rst +++ b/docs/api/sqlobject.dbconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.dbconnection module -============================== +sqlobject.dbconnection module +============================= .. automodule:: sqlobject.dbconnection :members: diff --git a/docs/api/sqlobject.dberrors.rst b/docs/api/sqlobject.dberrors.rst index e8be2dbf..3035746a 100644 --- a/docs/api/sqlobject.dberrors.rst +++ b/docs/api/sqlobject.dberrors.rst @@ -1,5 +1,5 @@ -sqlobject\.dberrors module -========================== +sqlobject.dberrors module +========================= .. automodule:: sqlobject.dberrors :members: diff --git a/docs/api/sqlobject.declarative.rst b/docs/api/sqlobject.declarative.rst index f89a88b4..9cc7e9bd 100644 --- a/docs/api/sqlobject.declarative.rst +++ b/docs/api/sqlobject.declarative.rst @@ -1,5 +1,5 @@ -sqlobject\.declarative module -============================= +sqlobject.declarative module +============================ .. automodule:: sqlobject.declarative :members: diff --git a/docs/api/sqlobject.events.rst b/docs/api/sqlobject.events.rst index 18ddcc78..8c33239d 100644 --- a/docs/api/sqlobject.events.rst +++ b/docs/api/sqlobject.events.rst @@ -1,5 +1,5 @@ -sqlobject\.events module -======================== +sqlobject.events module +======================= .. automodule:: sqlobject.events :members: diff --git a/docs/api/sqlobject.firebird.firebirdconnection.rst b/docs/api/sqlobject.firebird.firebirdconnection.rst index e517e981..6fdf26e9 100644 --- a/docs/api/sqlobject.firebird.firebirdconnection.rst +++ b/docs/api/sqlobject.firebird.firebirdconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.firebird\.firebirdconnection module -============================================== +sqlobject.firebird.firebirdconnection module +============================================ .. automodule:: sqlobject.firebird.firebirdconnection :members: diff --git a/docs/api/sqlobject.firebird.rst b/docs/api/sqlobject.firebird.rst index a1f5b0ae..78cec9ae 100644 --- a/docs/api/sqlobject.firebird.rst +++ b/docs/api/sqlobject.firebird.rst @@ -1,5 +1,5 @@ -sqlobject\.firebird package -=========================== +sqlobject.firebird package +========================== .. automodule:: sqlobject.firebird :members: diff --git a/docs/api/sqlobject.include.hashcol.rst b/docs/api/sqlobject.include.hashcol.rst index 7abe8d54..76396aa4 100644 --- a/docs/api/sqlobject.include.hashcol.rst +++ b/docs/api/sqlobject.include.hashcol.rst @@ -1,5 +1,5 @@ -sqlobject\.include\.hashcol module -================================== +sqlobject.include.hashcol module +================================ .. automodule:: sqlobject.include.hashcol :members: diff --git a/docs/api/sqlobject.include.rst b/docs/api/sqlobject.include.rst index aebc9e47..62086bfc 100644 --- a/docs/api/sqlobject.include.rst +++ b/docs/api/sqlobject.include.rst @@ -1,5 +1,5 @@ -sqlobject\.include package -========================== +sqlobject.include package +========================= .. automodule:: sqlobject.include :members: diff --git a/docs/api/sqlobject.include.tests.rst b/docs/api/sqlobject.include.tests.rst index 0bf1f136..e17b4def 100644 --- a/docs/api/sqlobject.include.tests.rst +++ b/docs/api/sqlobject.include.tests.rst @@ -1,5 +1,5 @@ -sqlobject\.include\.tests package -================================= +sqlobject.include.tests package +=============================== .. automodule:: sqlobject.include.tests :members: diff --git a/docs/api/sqlobject.include.tests.test_hashcol.rst b/docs/api/sqlobject.include.tests.test_hashcol.rst index 2ec99027..5289acd7 100644 --- a/docs/api/sqlobject.include.tests.test_hashcol.rst +++ b/docs/api/sqlobject.include.tests.test_hashcol.rst @@ -1,8 +1,7 @@ -sqlobject\.include\.tests\.test\_hashcol module -=============================================== +sqlobject.include.tests.test\_hashcol module +============================================ .. automodule:: sqlobject.include.tests.test_hashcol :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.index.rst b/docs/api/sqlobject.index.rst index c2facd02..bc96433c 100644 --- a/docs/api/sqlobject.index.rst +++ b/docs/api/sqlobject.index.rst @@ -1,5 +1,5 @@ -sqlobject\.index module -======================= +sqlobject.index module +====================== .. automodule:: sqlobject.index :members: diff --git a/docs/api/sqlobject.inheritance.iteration.rst b/docs/api/sqlobject.inheritance.iteration.rst index b6318065..0e9538f8 100644 --- a/docs/api/sqlobject.inheritance.iteration.rst +++ b/docs/api/sqlobject.inheritance.iteration.rst @@ -1,5 +1,5 @@ -sqlobject\.inheritance\.iteration module -======================================== +sqlobject.inheritance.iteration module +====================================== .. automodule:: sqlobject.inheritance.iteration :members: diff --git a/docs/api/sqlobject.inheritance.rst b/docs/api/sqlobject.inheritance.rst index 5d9dae4c..7ab19a77 100644 --- a/docs/api/sqlobject.inheritance.rst +++ b/docs/api/sqlobject.inheritance.rst @@ -1,5 +1,5 @@ -sqlobject\.inheritance package -============================== +sqlobject.inheritance package +============================= .. automodule:: sqlobject.inheritance :members: diff --git a/docs/api/sqlobject.inheritance.tests.rst b/docs/api/sqlobject.inheritance.tests.rst index c29cbc40..994c85ab 100644 --- a/docs/api/sqlobject.inheritance.tests.rst +++ b/docs/api/sqlobject.inheritance.tests.rst @@ -1,5 +1,5 @@ -sqlobject\.inheritance\.tests package -===================================== +sqlobject.inheritance.tests package +=================================== .. automodule:: sqlobject.inheritance.tests :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_aggregates.rst b/docs/api/sqlobject.inheritance.tests.test_aggregates.rst index 15fa5afd..58a2944d 100644 --- a/docs/api/sqlobject.inheritance.tests.test_aggregates.rst +++ b/docs/api/sqlobject.inheritance.tests.test_aggregates.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_aggregates module -====================================================== +sqlobject.inheritance.tests.test\_aggregates module +=================================================== .. automodule:: sqlobject.inheritance.tests.test_aggregates :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_asdict.rst b/docs/api/sqlobject.inheritance.tests.test_asdict.rst index c24c5114..6a33dce2 100644 --- a/docs/api/sqlobject.inheritance.tests.test_asdict.rst +++ b/docs/api/sqlobject.inheritance.tests.test_asdict.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_asdict module -================================================== +sqlobject.inheritance.tests.test\_asdict module +=============================================== .. automodule:: sqlobject.inheritance.tests.test_asdict :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst b/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst index eb1661dd..5447d0bd 100644 --- a/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst +++ b/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_deep\_inheritance module -============================================================= +sqlobject.inheritance.tests.test\_deep\_inheritance module +========================================================== .. automodule:: sqlobject.inheritance.tests.test_deep_inheritance :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst b/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst index 3504fe65..8b9b7069 100644 --- a/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst +++ b/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_destroy\_cascade module -============================================================ +sqlobject.inheritance.tests.test\_destroy\_cascade module +========================================================= .. automodule:: sqlobject.inheritance.tests.test_destroy_cascade :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst b/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst index b6e28aa5..68059887 100644 --- a/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst +++ b/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_foreignKey module -====================================================== +sqlobject.inheritance.tests.test\_foreignKey module +=================================================== .. automodule:: sqlobject.inheritance.tests.test_foreignKey :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_indexes.rst b/docs/api/sqlobject.inheritance.tests.test_indexes.rst index bb2e3f7b..cda20165 100644 --- a/docs/api/sqlobject.inheritance.tests.test_indexes.rst +++ b/docs/api/sqlobject.inheritance.tests.test_indexes.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_indexes module -=================================================== +sqlobject.inheritance.tests.test\_indexes module +================================================ .. automodule:: sqlobject.inheritance.tests.test_indexes :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,indexDefinitions diff --git a/docs/api/sqlobject.inheritance.tests.test_inheritance.rst b/docs/api/sqlobject.inheritance.tests.test_inheritance.rst index 0c186b0a..7af4fc40 100644 --- a/docs/api/sqlobject.inheritance.tests.test_inheritance.rst +++ b/docs/api/sqlobject.inheritance.tests.test_inheritance.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_inheritance module -======================================================= +sqlobject.inheritance.tests.test\_inheritance module +==================================================== .. automodule:: sqlobject.inheritance.tests.test_inheritance :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst b/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst index b56d0474..31cea2e6 100644 --- a/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst +++ b/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst @@ -1,8 +1,7 @@ -sqlobject\.inheritance\.tests\.test\_inheritance\_tree module -============================================================= +sqlobject.inheritance.tests.test\_inheritance\_tree module +========================================================== .. automodule:: sqlobject.inheritance.tests.test_inheritance_tree :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.joins.rst b/docs/api/sqlobject.joins.rst index c864c429..70e43b04 100644 --- a/docs/api/sqlobject.joins.rst +++ b/docs/api/sqlobject.joins.rst @@ -1,5 +1,5 @@ -sqlobject\.joins module -======================= +sqlobject.joins module +====================== .. automodule:: sqlobject.joins :members: diff --git a/docs/api/sqlobject.main.rst b/docs/api/sqlobject.main.rst index 5f8c4b8e..b4de5566 100644 --- a/docs/api/sqlobject.main.rst +++ b/docs/api/sqlobject.main.rst @@ -1,5 +1,5 @@ -sqlobject\.main module -====================== +sqlobject.main module +===================== .. automodule:: sqlobject.main :members: diff --git a/docs/api/sqlobject.manager.command.rst b/docs/api/sqlobject.manager.command.rst index be4a0073..bae2b682 100644 --- a/docs/api/sqlobject.manager.command.rst +++ b/docs/api/sqlobject.manager.command.rst @@ -1,8 +1,7 @@ -sqlobject\.manager\.command module -================================== +sqlobject.manager.command module +================================ .. automodule:: sqlobject.manager.command :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.manager.rst b/docs/api/sqlobject.manager.rst index 2b3387c6..3a8c8a81 100644 --- a/docs/api/sqlobject.manager.rst +++ b/docs/api/sqlobject.manager.rst @@ -1,5 +1,5 @@ -sqlobject\.manager package -========================== +sqlobject.manager package +========================= .. automodule:: sqlobject.manager :members: diff --git a/docs/api/sqlobject.maxdb.maxdbconnection.rst b/docs/api/sqlobject.maxdb.maxdbconnection.rst index c2cae465..1a6490ea 100644 --- a/docs/api/sqlobject.maxdb.maxdbconnection.rst +++ b/docs/api/sqlobject.maxdb.maxdbconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.maxdb\.maxdbconnection module -======================================== +sqlobject.maxdb.maxdbconnection module +====================================== .. automodule:: sqlobject.maxdb.maxdbconnection :members: diff --git a/docs/api/sqlobject.maxdb.rst b/docs/api/sqlobject.maxdb.rst index 1ca83a35..d591970e 100644 --- a/docs/api/sqlobject.maxdb.rst +++ b/docs/api/sqlobject.maxdb.rst @@ -1,5 +1,5 @@ -sqlobject\.maxdb package -======================== +sqlobject.maxdb package +======================= .. automodule:: sqlobject.maxdb :members: diff --git a/docs/api/sqlobject.mssql.mssqlconnection.rst b/docs/api/sqlobject.mssql.mssqlconnection.rst index d3121122..ffcc0023 100644 --- a/docs/api/sqlobject.mssql.mssqlconnection.rst +++ b/docs/api/sqlobject.mssql.mssqlconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.mssql\.mssqlconnection module -======================================== +sqlobject.mssql.mssqlconnection module +====================================== .. automodule:: sqlobject.mssql.mssqlconnection :members: diff --git a/docs/api/sqlobject.mssql.rst b/docs/api/sqlobject.mssql.rst index 84c45418..7e622693 100644 --- a/docs/api/sqlobject.mssql.rst +++ b/docs/api/sqlobject.mssql.rst @@ -1,5 +1,5 @@ -sqlobject\.mssql package -======================== +sqlobject.mssql package +======================= .. automodule:: sqlobject.mssql :members: diff --git a/docs/api/sqlobject.mysql.mysqlconnection.rst b/docs/api/sqlobject.mysql.mysqlconnection.rst index 224601ef..3e5a9ef8 100644 --- a/docs/api/sqlobject.mysql.mysqlconnection.rst +++ b/docs/api/sqlobject.mysql.mysqlconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.mysql\.mysqlconnection module -======================================== +sqlobject.mysql.mysqlconnection module +====================================== .. automodule:: sqlobject.mysql.mysqlconnection :members: diff --git a/docs/api/sqlobject.mysql.rst b/docs/api/sqlobject.mysql.rst index e244266d..7e69a9e2 100644 --- a/docs/api/sqlobject.mysql.rst +++ b/docs/api/sqlobject.mysql.rst @@ -1,5 +1,5 @@ -sqlobject\.mysql package -======================== +sqlobject.mysql package +======================= .. automodule:: sqlobject.mysql :members: diff --git a/docs/api/sqlobject.postgres.pgconnection.rst b/docs/api/sqlobject.postgres.pgconnection.rst index 3880e511..854104d5 100644 --- a/docs/api/sqlobject.postgres.pgconnection.rst +++ b/docs/api/sqlobject.postgres.pgconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.postgres\.pgconnection module -======================================== +sqlobject.postgres.pgconnection module +====================================== .. automodule:: sqlobject.postgres.pgconnection :members: diff --git a/docs/api/sqlobject.postgres.rst b/docs/api/sqlobject.postgres.rst index 5decb52a..ad93e75d 100644 --- a/docs/api/sqlobject.postgres.rst +++ b/docs/api/sqlobject.postgres.rst @@ -1,5 +1,5 @@ -sqlobject\.postgres package -=========================== +sqlobject.postgres package +========================== .. automodule:: sqlobject.postgres :members: diff --git a/docs/api/sqlobject.rdbhost.rdbhostconnection.rst b/docs/api/sqlobject.rdbhost.rdbhostconnection.rst deleted file mode 100644 index 016d0e66..00000000 --- a/docs/api/sqlobject.rdbhost.rdbhostconnection.rst +++ /dev/null @@ -1,7 +0,0 @@ -sqlobject\.rdbhost\.rdbhostconnection module -============================================ - -.. automodule:: sqlobject.rdbhost.rdbhostconnection - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/sqlobject.rdbhost.rst b/docs/api/sqlobject.rdbhost.rst deleted file mode 100644 index c4f57c87..00000000 --- a/docs/api/sqlobject.rdbhost.rst +++ /dev/null @@ -1,15 +0,0 @@ -sqlobject\.rdbhost package -========================== - -.. automodule:: sqlobject.rdbhost - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - - sqlobject.rdbhost.rdbhostconnection - diff --git a/docs/api/sqlobject.rst b/docs/api/sqlobject.rst index 1695715e..2711347b 100644 --- a/docs/api/sqlobject.rst +++ b/docs/api/sqlobject.rst @@ -19,7 +19,6 @@ Subpackages sqlobject.mssql sqlobject.mysql sqlobject.postgres - sqlobject.rdbhost sqlobject.sqlite sqlobject.sybase sqlobject.tests diff --git a/docs/api/sqlobject.sqlbuilder.rst b/docs/api/sqlobject.sqlbuilder.rst index 4c49ca06..bd21c086 100644 --- a/docs/api/sqlobject.sqlbuilder.rst +++ b/docs/api/sqlobject.sqlbuilder.rst @@ -1,5 +1,5 @@ -sqlobject\.sqlbuilder module -============================ +sqlobject.sqlbuilder module +=========================== .. automodule:: sqlobject.sqlbuilder :members: diff --git a/docs/api/sqlobject.sqlite.rst b/docs/api/sqlobject.sqlite.rst index 8afe099e..f62364ef 100644 --- a/docs/api/sqlobject.sqlite.rst +++ b/docs/api/sqlobject.sqlite.rst @@ -1,5 +1,5 @@ -sqlobject\.sqlite package -========================= +sqlobject.sqlite package +======================== .. automodule:: sqlobject.sqlite :members: diff --git a/docs/api/sqlobject.sqlite.sqliteconnection.rst b/docs/api/sqlobject.sqlite.sqliteconnection.rst index 37e64745..0ce1763a 100644 --- a/docs/api/sqlobject.sqlite.sqliteconnection.rst +++ b/docs/api/sqlobject.sqlite.sqliteconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.sqlite\.sqliteconnection module -========================================== +sqlobject.sqlite.sqliteconnection module +======================================== .. automodule:: sqlobject.sqlite.sqliteconnection :members: diff --git a/docs/api/sqlobject.sresults.rst b/docs/api/sqlobject.sresults.rst index 7b18b85f..690de42e 100644 --- a/docs/api/sqlobject.sresults.rst +++ b/docs/api/sqlobject.sresults.rst @@ -1,5 +1,5 @@ -sqlobject\.sresults module -========================== +sqlobject.sresults module +========================= .. automodule:: sqlobject.sresults :members: diff --git a/docs/api/sqlobject.styles.rst b/docs/api/sqlobject.styles.rst index f5238168..04aeb8c8 100644 --- a/docs/api/sqlobject.styles.rst +++ b/docs/api/sqlobject.styles.rst @@ -1,5 +1,5 @@ -sqlobject\.styles module -======================== +sqlobject.styles module +======================= .. automodule:: sqlobject.styles :members: diff --git a/docs/api/sqlobject.sybase.rst b/docs/api/sqlobject.sybase.rst index 84e1fc2a..b5d5aef8 100644 --- a/docs/api/sqlobject.sybase.rst +++ b/docs/api/sqlobject.sybase.rst @@ -1,5 +1,5 @@ -sqlobject\.sybase package -========================= +sqlobject.sybase package +======================== .. automodule:: sqlobject.sybase :members: diff --git a/docs/api/sqlobject.sybase.sybaseconnection.rst b/docs/api/sqlobject.sybase.sybaseconnection.rst index b684c94a..3cdf4c02 100644 --- a/docs/api/sqlobject.sybase.sybaseconnection.rst +++ b/docs/api/sqlobject.sybase.sybaseconnection.rst @@ -1,5 +1,5 @@ -sqlobject\.sybase\.sybaseconnection module -========================================== +sqlobject.sybase.sybaseconnection module +======================================== .. automodule:: sqlobject.sybase.sybaseconnection :members: diff --git a/docs/api/sqlobject.tests.dbtest.rst b/docs/api/sqlobject.tests.dbtest.rst index 617ef7f7..b6fb5132 100644 --- a/docs/api/sqlobject.tests.dbtest.rst +++ b/docs/api/sqlobject.tests.dbtest.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.dbtest module -=============================== +sqlobject.tests.dbtest module +============================= .. automodule:: sqlobject.tests.dbtest :members: diff --git a/docs/api/sqlobject.tests.rst b/docs/api/sqlobject.tests.rst index e7a409ce..fcab4e81 100644 --- a/docs/api/sqlobject.tests.rst +++ b/docs/api/sqlobject.tests.rst @@ -1,5 +1,5 @@ -sqlobject\.tests package -======================== +sqlobject.tests package +======================= .. automodule:: sqlobject.tests :members: @@ -13,6 +13,7 @@ Submodules sqlobject.tests.dbtest sqlobject.tests.test_ForeignKey + sqlobject.tests.test_ForeignKey_cascade sqlobject.tests.test_NoneValuedResultItem sqlobject.tests.test_SQLMultipleJoin sqlobject.tests.test_SQLRelatedJoin @@ -29,6 +30,7 @@ Submodules sqlobject.tests.test_columns_order sqlobject.tests.test_combining_joins sqlobject.tests.test_comparison + sqlobject.tests.test_compat sqlobject.tests.test_complex_sorting sqlobject.tests.test_constraints sqlobject.tests.test_converters diff --git a/docs/api/sqlobject.tests.test_ForeignKey.rst b/docs/api/sqlobject.tests.test_ForeignKey.rst index 1ca6c9d7..c495f8b9 100644 --- a/docs/api/sqlobject.tests.test_ForeignKey.rst +++ b/docs/api/sqlobject.tests.test_ForeignKey.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_ForeignKey module -========================================= +sqlobject.tests.test\_ForeignKey module +======================================= .. automodule:: sqlobject.tests.test_ForeignKey :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_ForeignKey_cascade.rst b/docs/api/sqlobject.tests.test_ForeignKey_cascade.rst new file mode 100644 index 00000000..cbe1c448 --- /dev/null +++ b/docs/api/sqlobject.tests.test_ForeignKey_cascade.rst @@ -0,0 +1,7 @@ +sqlobject.tests.test\_ForeignKey\_cascade module +================================================ + +.. automodule:: sqlobject.tests.test_ForeignKey_cascade + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst b/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst index bac55967..35ee6435 100644 --- a/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst +++ b/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_NoneValuedResultItem module -=================================================== +sqlobject.tests.test\_NoneValuedResultItem module +================================================= .. automodule:: sqlobject.tests.test_NoneValuedResultItem :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst b/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst index a7473fbd..7ae46d52 100644 --- a/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst +++ b/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_SQLMultipleJoin module -============================================== +sqlobject.tests.test\_SQLMultipleJoin module +============================================ .. automodule:: sqlobject.tests.test_SQLMultipleJoin :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst b/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst index e1813011..e12f1f55 100644 --- a/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst +++ b/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_SQLRelatedJoin module -============================================= +sqlobject.tests.test\_SQLRelatedJoin module +=========================================== .. automodule:: sqlobject.tests.test_SQLRelatedJoin :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_SingleJoin.rst b/docs/api/sqlobject.tests.test_SingleJoin.rst index 7f4179da..80f8907c 100644 --- a/docs/api/sqlobject.tests.test_SingleJoin.rst +++ b/docs/api/sqlobject.tests.test_SingleJoin.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_SingleJoin module -========================================= +sqlobject.tests.test\_SingleJoin module +======================================= .. automodule:: sqlobject.tests.test_SingleJoin :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_aggregates.rst b/docs/api/sqlobject.tests.test_aggregates.rst index 117aadde..283cea6f 100644 --- a/docs/api/sqlobject.tests.test_aggregates.rst +++ b/docs/api/sqlobject.tests.test_aggregates.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_aggregates module -========================================= +sqlobject.tests.test\_aggregates module +======================================= .. automodule:: sqlobject.tests.test_aggregates :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_aliases.rst b/docs/api/sqlobject.tests.test_aliases.rst index d96edd13..fa46a461 100644 --- a/docs/api/sqlobject.tests.test_aliases.rst +++ b/docs/api/sqlobject.tests.test_aliases.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_aliases module -====================================== +sqlobject.tests.test\_aliases module +==================================== .. automodule:: sqlobject.tests.test_aliases :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_asdict.rst b/docs/api/sqlobject.tests.test_asdict.rst index b316c7ef..e1550441 100644 --- a/docs/api/sqlobject.tests.test_asdict.rst +++ b/docs/api/sqlobject.tests.test_asdict.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_asdict module -===================================== +sqlobject.tests.test\_asdict module +=================================== .. automodule:: sqlobject.tests.test_asdict :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_auto.rst b/docs/api/sqlobject.tests.test_auto.rst index 9c23284c..6d578d5e 100644 --- a/docs/api/sqlobject.tests.test_auto.rst +++ b/docs/api/sqlobject.tests.test_auto.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_auto module -=================================== +sqlobject.tests.test\_auto module +================================= .. automodule:: sqlobject.tests.test_auto :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_basic.rst b/docs/api/sqlobject.tests.test_basic.rst index 549436dd..40235ecf 100644 --- a/docs/api/sqlobject.tests.test_basic.rst +++ b/docs/api/sqlobject.tests.test_basic.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_basic module -==================================== +sqlobject.tests.test\_basic module +================================== .. automodule:: sqlobject.tests.test_basic :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_blob.rst b/docs/api/sqlobject.tests.test_blob.rst index 798a66c0..1d01d512 100644 --- a/docs/api/sqlobject.tests.test_blob.rst +++ b/docs/api/sqlobject.tests.test_blob.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_blob module -=================================== +sqlobject.tests.test\_blob module +================================= .. automodule:: sqlobject.tests.test_blob :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_boundattributes.rst b/docs/api/sqlobject.tests.test_boundattributes.rst index ec672dfb..47d9e2b1 100644 --- a/docs/api/sqlobject.tests.test_boundattributes.rst +++ b/docs/api/sqlobject.tests.test_boundattributes.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_boundattributes module -============================================== +sqlobject.tests.test\_boundattributes module +============================================ .. automodule:: sqlobject.tests.test_boundattributes :members: diff --git a/docs/api/sqlobject.tests.test_cache.rst b/docs/api/sqlobject.tests.test_cache.rst index 3e01991e..69f05823 100644 --- a/docs/api/sqlobject.tests.test_cache.rst +++ b/docs/api/sqlobject.tests.test_cache.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_cache module -==================================== +sqlobject.tests.test\_cache module +================================== .. automodule:: sqlobject.tests.test_cache :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_class_hash.rst b/docs/api/sqlobject.tests.test_class_hash.rst index 076937ab..f25b66b6 100644 --- a/docs/api/sqlobject.tests.test_class_hash.rst +++ b/docs/api/sqlobject.tests.test_class_hash.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_class\_hash module -========================================== +sqlobject.tests.test\_class\_hash module +======================================== .. automodule:: sqlobject.tests.test_class_hash :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_columns_order.rst b/docs/api/sqlobject.tests.test_columns_order.rst index 6187c32d..c48cf7c0 100644 --- a/docs/api/sqlobject.tests.test_columns_order.rst +++ b/docs/api/sqlobject.tests.test_columns_order.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_columns\_order module -============================================= +sqlobject.tests.test\_columns\_order module +=========================================== .. automodule:: sqlobject.tests.test_columns_order :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_combining_joins.rst b/docs/api/sqlobject.tests.test_combining_joins.rst index 7b30db09..e1961a80 100644 --- a/docs/api/sqlobject.tests.test_combining_joins.rst +++ b/docs/api/sqlobject.tests.test_combining_joins.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_combining\_joins module -=============================================== +sqlobject.tests.test\_combining\_joins module +============================================= .. automodule:: sqlobject.tests.test_combining_joins :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_comparison.rst b/docs/api/sqlobject.tests.test_comparison.rst index e54aacc3..02127b7b 100644 --- a/docs/api/sqlobject.tests.test_comparison.rst +++ b/docs/api/sqlobject.tests.test_comparison.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_comparison module -========================================= +sqlobject.tests.test\_comparison module +======================================= .. automodule:: sqlobject.tests.test_comparison :members: diff --git a/docs/api/sqlobject.tests.test_compat.rst b/docs/api/sqlobject.tests.test_compat.rst new file mode 100644 index 00000000..e37cb543 --- /dev/null +++ b/docs/api/sqlobject.tests.test_compat.rst @@ -0,0 +1,7 @@ +sqlobject.tests.test\_compat module +=================================== + +.. automodule:: sqlobject.tests.test_compat + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/sqlobject.tests.test_complex_sorting.rst b/docs/api/sqlobject.tests.test_complex_sorting.rst index 570fe23c..2321e963 100644 --- a/docs/api/sqlobject.tests.test_complex_sorting.rst +++ b/docs/api/sqlobject.tests.test_complex_sorting.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_complex\_sorting module -=============================================== +sqlobject.tests.test\_complex\_sorting module +============================================= .. automodule:: sqlobject.tests.test_complex_sorting :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_constraints.rst b/docs/api/sqlobject.tests.test_constraints.rst index d3521779..c967979b 100644 --- a/docs/api/sqlobject.tests.test_constraints.rst +++ b/docs/api/sqlobject.tests.test_constraints.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_constraints module -========================================== +sqlobject.tests.test\_constraints module +======================================== .. automodule:: sqlobject.tests.test_constraints :members: diff --git a/docs/api/sqlobject.tests.test_converters.rst b/docs/api/sqlobject.tests.test_converters.rst index 4548363b..a6893bda 100644 --- a/docs/api/sqlobject.tests.test_converters.rst +++ b/docs/api/sqlobject.tests.test_converters.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_converters module -========================================= +sqlobject.tests.test\_converters module +======================================= .. automodule:: sqlobject.tests.test_converters :members: diff --git a/docs/api/sqlobject.tests.test_create_drop.rst b/docs/api/sqlobject.tests.test_create_drop.rst index d7845b9b..fcd3eea5 100644 --- a/docs/api/sqlobject.tests.test_create_drop.rst +++ b/docs/api/sqlobject.tests.test_create_drop.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_create\_drop module -=========================================== +sqlobject.tests.test\_create\_drop module +========================================= .. automodule:: sqlobject.tests.test_create_drop :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_csvexport.rst b/docs/api/sqlobject.tests.test_csvexport.rst index 2935f61d..0927f4f0 100644 --- a/docs/api/sqlobject.tests.test_csvexport.rst +++ b/docs/api/sqlobject.tests.test_csvexport.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_csvexport module -======================================== +sqlobject.tests.test\_csvexport module +====================================== .. automodule:: sqlobject.tests.test_csvexport :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_csvimport.rst b/docs/api/sqlobject.tests.test_csvimport.rst index 56981a8d..f7db4138 100644 --- a/docs/api/sqlobject.tests.test_csvimport.rst +++ b/docs/api/sqlobject.tests.test_csvimport.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_csvimport module -======================================== +sqlobject.tests.test\_csvimport module +====================================== .. automodule:: sqlobject.tests.test_csvimport :members: diff --git a/docs/api/sqlobject.tests.test_cyclic_reference.rst b/docs/api/sqlobject.tests.test_cyclic_reference.rst index 1cb34572..910421ab 100644 --- a/docs/api/sqlobject.tests.test_cyclic_reference.rst +++ b/docs/api/sqlobject.tests.test_cyclic_reference.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_cyclic\_reference module -================================================ +sqlobject.tests.test\_cyclic\_reference module +============================================== .. automodule:: sqlobject.tests.test_cyclic_reference :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_datetime.rst b/docs/api/sqlobject.tests.test_datetime.rst index b6b42358..87a1b043 100644 --- a/docs/api/sqlobject.tests.test_datetime.rst +++ b/docs/api/sqlobject.tests.test_datetime.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_datetime module -======================================= +sqlobject.tests.test\_datetime module +===================================== .. automodule:: sqlobject.tests.test_datetime :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_decimal.rst b/docs/api/sqlobject.tests.test_decimal.rst index 9e680e3a..86868120 100644 --- a/docs/api/sqlobject.tests.test_decimal.rst +++ b/docs/api/sqlobject.tests.test_decimal.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_decimal module -====================================== +sqlobject.tests.test\_decimal module +==================================== .. automodule:: sqlobject.tests.test_decimal :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_declarative.rst b/docs/api/sqlobject.tests.test_declarative.rst index 097a81fc..a022b0cc 100644 --- a/docs/api/sqlobject.tests.test_declarative.rst +++ b/docs/api/sqlobject.tests.test_declarative.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_declarative module -========================================== +sqlobject.tests.test\_declarative module +======================================== .. automodule:: sqlobject.tests.test_declarative :members: diff --git a/docs/api/sqlobject.tests.test_default_style.rst b/docs/api/sqlobject.tests.test_default_style.rst index 50868333..5cd86ffe 100644 --- a/docs/api/sqlobject.tests.test_default_style.rst +++ b/docs/api/sqlobject.tests.test_default_style.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_default\_style module -============================================= +sqlobject.tests.test\_default\_style module +=========================================== .. automodule:: sqlobject.tests.test_default_style :members: diff --git a/docs/api/sqlobject.tests.test_delete.rst b/docs/api/sqlobject.tests.test_delete.rst index 8ee8eb76..508cc86a 100644 --- a/docs/api/sqlobject.tests.test_delete.rst +++ b/docs/api/sqlobject.tests.test_delete.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_delete module -===================================== +sqlobject.tests.test\_delete module +=================================== .. automodule:: sqlobject.tests.test_delete :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_distinct.rst b/docs/api/sqlobject.tests.test_distinct.rst index 5b2b72c6..68080aa3 100644 --- a/docs/api/sqlobject.tests.test_distinct.rst +++ b/docs/api/sqlobject.tests.test_distinct.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_distinct module -======================================= +sqlobject.tests.test\_distinct module +===================================== .. automodule:: sqlobject.tests.test_distinct :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_empty.rst b/docs/api/sqlobject.tests.test_empty.rst index 111994c7..69964d49 100644 --- a/docs/api/sqlobject.tests.test_empty.rst +++ b/docs/api/sqlobject.tests.test_empty.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_empty module -==================================== +sqlobject.tests.test\_empty module +================================== .. automodule:: sqlobject.tests.test_empty :members: diff --git a/docs/api/sqlobject.tests.test_enum.rst b/docs/api/sqlobject.tests.test_enum.rst index c5c5a0f1..0ae9e483 100644 --- a/docs/api/sqlobject.tests.test_enum.rst +++ b/docs/api/sqlobject.tests.test_enum.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_enum module -=================================== +sqlobject.tests.test\_enum module +================================= .. automodule:: sqlobject.tests.test_enum :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_events.rst b/docs/api/sqlobject.tests.test_events.rst index 78c7b839..7c1b9ef8 100644 --- a/docs/api/sqlobject.tests.test_events.rst +++ b/docs/api/sqlobject.tests.test_events.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_events module -===================================== +sqlobject.tests.test\_events module +=================================== .. automodule:: sqlobject.tests.test_events :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_exceptions.rst b/docs/api/sqlobject.tests.test_exceptions.rst index 45d09fd2..5e440e43 100644 --- a/docs/api/sqlobject.tests.test_exceptions.rst +++ b/docs/api/sqlobject.tests.test_exceptions.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_exceptions module -========================================= +sqlobject.tests.test\_exceptions module +======================================= .. automodule:: sqlobject.tests.test_exceptions :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_expire.rst b/docs/api/sqlobject.tests.test_expire.rst index 26b81f54..614f06ce 100644 --- a/docs/api/sqlobject.tests.test_expire.rst +++ b/docs/api/sqlobject.tests.test_expire.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_expire module -===================================== +sqlobject.tests.test\_expire module +=================================== .. automodule:: sqlobject.tests.test_expire :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_groupBy.rst b/docs/api/sqlobject.tests.test_groupBy.rst index f823fd07..c8f6cc92 100644 --- a/docs/api/sqlobject.tests.test_groupBy.rst +++ b/docs/api/sqlobject.tests.test_groupBy.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_groupBy module -====================================== +sqlobject.tests.test\_groupBy module +==================================== .. automodule:: sqlobject.tests.test_groupBy :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_identity.rst b/docs/api/sqlobject.tests.test_identity.rst index e154f2dc..63716dfb 100644 --- a/docs/api/sqlobject.tests.test_identity.rst +++ b/docs/api/sqlobject.tests.test_identity.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_identity module -======================================= +sqlobject.tests.test\_identity module +===================================== .. automodule:: sqlobject.tests.test_identity :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_indexes.rst b/docs/api/sqlobject.tests.test_indexes.rst index cfe8e4bc..023d8fdb 100644 --- a/docs/api/sqlobject.tests.test_indexes.rst +++ b/docs/api/sqlobject.tests.test_indexes.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_indexes module -====================================== +sqlobject.tests.test\_indexes module +==================================== .. automodule:: sqlobject.tests.test_indexes :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns,indexDefinitions diff --git a/docs/api/sqlobject.tests.test_inheritance.rst b/docs/api/sqlobject.tests.test_inheritance.rst index 369aa4df..a381ee17 100644 --- a/docs/api/sqlobject.tests.test_inheritance.rst +++ b/docs/api/sqlobject.tests.test_inheritance.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_inheritance module -========================================== +sqlobject.tests.test\_inheritance module +======================================== .. automodule:: sqlobject.tests.test_inheritance :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_joins.rst b/docs/api/sqlobject.tests.test_joins.rst index 67ef3475..a0679733 100644 --- a/docs/api/sqlobject.tests.test_joins.rst +++ b/docs/api/sqlobject.tests.test_joins.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_joins module -==================================== +sqlobject.tests.test\_joins module +================================== .. automodule:: sqlobject.tests.test_joins :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_joins_conditional.rst b/docs/api/sqlobject.tests.test_joins_conditional.rst index 8bc28896..ca6fd0b5 100644 --- a/docs/api/sqlobject.tests.test_joins_conditional.rst +++ b/docs/api/sqlobject.tests.test_joins_conditional.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_joins\_conditional module -================================================= +sqlobject.tests.test\_joins\_conditional module +=============================================== .. automodule:: sqlobject.tests.test_joins_conditional :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_jsonbcol.rst b/docs/api/sqlobject.tests.test_jsonbcol.rst index 48eba47e..30f0d54a 100644 --- a/docs/api/sqlobject.tests.test_jsonbcol.rst +++ b/docs/api/sqlobject.tests.test_jsonbcol.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_jsonbcol module -======================================= +sqlobject.tests.test\_jsonbcol module +===================================== .. automodule:: sqlobject.tests.test_jsonbcol :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_jsoncol.rst b/docs/api/sqlobject.tests.test_jsoncol.rst index 18334b2a..bc78a692 100644 --- a/docs/api/sqlobject.tests.test_jsoncol.rst +++ b/docs/api/sqlobject.tests.test_jsoncol.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_jsoncol module -====================================== +sqlobject.tests.test\_jsoncol module +==================================== .. automodule:: sqlobject.tests.test_jsoncol :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_lazy.rst b/docs/api/sqlobject.tests.test_lazy.rst index 34df4cbb..98994362 100644 --- a/docs/api/sqlobject.tests.test_lazy.rst +++ b/docs/api/sqlobject.tests.test_lazy.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_lazy module -=================================== +sqlobject.tests.test\_lazy module +================================= .. automodule:: sqlobject.tests.test_lazy :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_md5.rst b/docs/api/sqlobject.tests.test_md5.rst index 8ea42178..922a8b88 100644 --- a/docs/api/sqlobject.tests.test_md5.rst +++ b/docs/api/sqlobject.tests.test_md5.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_md5 module -================================== +sqlobject.tests.test\_md5 module +================================ .. automodule:: sqlobject.tests.test_md5 :members: diff --git a/docs/api/sqlobject.tests.test_mysql.rst b/docs/api/sqlobject.tests.test_mysql.rst index 1ac3d767..adb997f9 100644 --- a/docs/api/sqlobject.tests.test_mysql.rst +++ b/docs/api/sqlobject.tests.test_mysql.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_mysql module -==================================== +sqlobject.tests.test\_mysql module +================================== .. automodule:: sqlobject.tests.test_mysql :members: diff --git a/docs/api/sqlobject.tests.test_new_joins.rst b/docs/api/sqlobject.tests.test_new_joins.rst index 5f751be7..7edf06aa 100644 --- a/docs/api/sqlobject.tests.test_new_joins.rst +++ b/docs/api/sqlobject.tests.test_new_joins.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_new\_joins module -========================================= +sqlobject.tests.test\_new\_joins module +======================================= .. automodule:: sqlobject.tests.test_new_joins :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_parse_uri.rst b/docs/api/sqlobject.tests.test_parse_uri.rst index d5adf4e9..8a7cf626 100644 --- a/docs/api/sqlobject.tests.test_parse_uri.rst +++ b/docs/api/sqlobject.tests.test_parse_uri.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_parse\_uri module -========================================= +sqlobject.tests.test\_parse\_uri module +======================================= .. automodule:: sqlobject.tests.test_parse_uri :members: diff --git a/docs/api/sqlobject.tests.test_paste.rst b/docs/api/sqlobject.tests.test_paste.rst index 2a284741..dc0fb590 100644 --- a/docs/api/sqlobject.tests.test_paste.rst +++ b/docs/api/sqlobject.tests.test_paste.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_paste module -==================================== +sqlobject.tests.test\_paste module +================================== .. automodule:: sqlobject.tests.test_paste :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_perConnection.rst b/docs/api/sqlobject.tests.test_perConnection.rst index e4d43710..7d4b1781 100644 --- a/docs/api/sqlobject.tests.test_perConnection.rst +++ b/docs/api/sqlobject.tests.test_perConnection.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_perConnection module -============================================ +sqlobject.tests.test\_perConnection module +========================================== .. automodule:: sqlobject.tests.test_perConnection :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_pickle.rst b/docs/api/sqlobject.tests.test_pickle.rst index 6e7cfe33..c44fa27d 100644 --- a/docs/api/sqlobject.tests.test_pickle.rst +++ b/docs/api/sqlobject.tests.test_pickle.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_pickle module -===================================== +sqlobject.tests.test\_pickle module +=================================== .. automodule:: sqlobject.tests.test_pickle :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_picklecol.rst b/docs/api/sqlobject.tests.test_picklecol.rst index 56db3212..7edd7c08 100644 --- a/docs/api/sqlobject.tests.test_picklecol.rst +++ b/docs/api/sqlobject.tests.test_picklecol.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_picklecol module -======================================== +sqlobject.tests.test\_picklecol module +====================================== .. automodule:: sqlobject.tests.test_picklecol :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_postgres.rst b/docs/api/sqlobject.tests.test_postgres.rst index 374a7869..d8082b5c 100644 --- a/docs/api/sqlobject.tests.test_postgres.rst +++ b/docs/api/sqlobject.tests.test_postgres.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_postgres module -======================================= +sqlobject.tests.test\_postgres module +===================================== .. automodule:: sqlobject.tests.test_postgres :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst b/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst index 2af5978d..8c044b55 100644 --- a/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst +++ b/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_reparent\_sqlmeta module -================================================ +sqlobject.tests.test\_reparent\_sqlmeta module +============================================== .. automodule:: sqlobject.tests.test_reparent_sqlmeta :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_schema.rst b/docs/api/sqlobject.tests.test_schema.rst index 42f80883..9cf3457f 100644 --- a/docs/api/sqlobject.tests.test_schema.rst +++ b/docs/api/sqlobject.tests.test_schema.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_schema module -===================================== +sqlobject.tests.test\_schema module +=================================== .. automodule:: sqlobject.tests.test_schema :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_select.rst b/docs/api/sqlobject.tests.test_select.rst index 0ba89a83..9e63811b 100644 --- a/docs/api/sqlobject.tests.test_select.rst +++ b/docs/api/sqlobject.tests.test_select.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_select module -===================================== +sqlobject.tests.test\_select module +=================================== .. automodule:: sqlobject.tests.test_select :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_select_through.rst b/docs/api/sqlobject.tests.test_select_through.rst index 8c5cdaa1..f0833e2f 100644 --- a/docs/api/sqlobject.tests.test_select_through.rst +++ b/docs/api/sqlobject.tests.test_select_through.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_select\_through module -============================================== +sqlobject.tests.test\_select\_through module +============================================ .. automodule:: sqlobject.tests.test_select_through :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_setters.rst b/docs/api/sqlobject.tests.test_setters.rst index 0bb4d136..f35cfb0e 100644 --- a/docs/api/sqlobject.tests.test_setters.rst +++ b/docs/api/sqlobject.tests.test_setters.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_setters module -====================================== +sqlobject.tests.test\_setters module +==================================== .. automodule:: sqlobject.tests.test_setters :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_slice.rst b/docs/api/sqlobject.tests.test_slice.rst index f0559438..f2819a1b 100644 --- a/docs/api/sqlobject.tests.test_slice.rst +++ b/docs/api/sqlobject.tests.test_slice.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_slice module -==================================== +sqlobject.tests.test\_slice module +================================== .. automodule:: sqlobject.tests.test_slice :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sorting.rst b/docs/api/sqlobject.tests.test_sorting.rst index 04e1510a..a461b701 100644 --- a/docs/api/sqlobject.tests.test_sorting.rst +++ b/docs/api/sqlobject.tests.test_sorting.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sorting module -====================================== +sqlobject.tests.test\_sorting module +==================================== .. automodule:: sqlobject.tests.test_sorting :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sqlbuilder.rst b/docs/api/sqlobject.tests.test_sqlbuilder.rst index e70be201..f71230de 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sqlbuilder module -========================================= +sqlobject.tests.test\_sqlbuilder module +======================================= .. automodule:: sqlobject.tests.test_sqlbuilder :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst b/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst index b3291a43..47c965c9 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sqlbuilder\_dbspecific module -===================================================== +sqlobject.tests.test\_sqlbuilder\_dbspecific module +=================================================== .. automodule:: sqlobject.tests.test_sqlbuilder_dbspecific :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst b/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst index d6c874d1..56eb89fa 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_sqlbuilder\_importproxy module -====================================================== +sqlobject.tests.test\_sqlbuilder\_importproxy module +==================================================== .. automodule:: sqlobject.tests.test_sqlbuilder_importproxy :members: diff --git a/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst b/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst index d92359b0..ac24093e 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sqlbuilder\_joins\_instances module -=========================================================== +sqlobject.tests.test\_sqlbuilder\_joins\_instances module +========================================================= .. automodule:: sqlobject.tests.test_sqlbuilder_joins_instances :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sqlite.rst b/docs/api/sqlobject.tests.test_sqlite.rst index 1da10c03..7ef87923 100644 --- a/docs/api/sqlobject.tests.test_sqlite.rst +++ b/docs/api/sqlobject.tests.test_sqlite.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sqlite module -===================================== +sqlobject.tests.test\_sqlite module +=================================== .. automodule:: sqlobject.tests.test_sqlite :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_sqlmeta_idName.rst b/docs/api/sqlobject.tests.test_sqlmeta_idName.rst index e01eb945..b2954f4c 100644 --- a/docs/api/sqlobject.tests.test_sqlmeta_idName.rst +++ b/docs/api/sqlobject.tests.test_sqlmeta_idName.rst @@ -1,5 +1,5 @@ -sqlobject\.tests\.test\_sqlmeta\_idName module -============================================== +sqlobject.tests.test\_sqlmeta\_idName module +============================================ .. automodule:: sqlobject.tests.test_sqlmeta_idName :members: diff --git a/docs/api/sqlobject.tests.test_sqlobject_admin.rst b/docs/api/sqlobject.tests.test_sqlobject_admin.rst index d76ebc52..517d03ed 100644 --- a/docs/api/sqlobject.tests.test_sqlobject_admin.rst +++ b/docs/api/sqlobject.tests.test_sqlobject_admin.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_sqlobject\_admin module -=============================================== +sqlobject.tests.test\_sqlobject\_admin module +============================================= .. automodule:: sqlobject.tests.test_sqlobject_admin :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_string_id.rst b/docs/api/sqlobject.tests.test_string_id.rst index fbadd05c..28be7463 100644 --- a/docs/api/sqlobject.tests.test_string_id.rst +++ b/docs/api/sqlobject.tests.test_string_id.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_string\_id module -========================================= +sqlobject.tests.test\_string\_id module +======================================= .. automodule:: sqlobject.tests.test_string_id :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_style.rst b/docs/api/sqlobject.tests.test_style.rst index 04722741..df0981a2 100644 --- a/docs/api/sqlobject.tests.test_style.rst +++ b/docs/api/sqlobject.tests.test_style.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_style module -==================================== +sqlobject.tests.test\_style module +================================== .. automodule:: sqlobject.tests.test_style :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_subqueries.rst b/docs/api/sqlobject.tests.test_subqueries.rst index 59b034be..c4ef5395 100644 --- a/docs/api/sqlobject.tests.test_subqueries.rst +++ b/docs/api/sqlobject.tests.test_subqueries.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_subqueries module -========================================= +sqlobject.tests.test\_subqueries module +======================================= .. automodule:: sqlobject.tests.test_subqueries :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_transactions.rst b/docs/api/sqlobject.tests.test_transactions.rst index 940636ca..1c218945 100644 --- a/docs/api/sqlobject.tests.test_transactions.rst +++ b/docs/api/sqlobject.tests.test_transactions.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_transactions module -=========================================== +sqlobject.tests.test\_transactions module +========================================= .. automodule:: sqlobject.tests.test_transactions :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_unicode.rst b/docs/api/sqlobject.tests.test_unicode.rst index cb48d1ad..c90397f9 100644 --- a/docs/api/sqlobject.tests.test_unicode.rst +++ b/docs/api/sqlobject.tests.test_unicode.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_unicode module -====================================== +sqlobject.tests.test\_unicode module +==================================== .. automodule:: sqlobject.tests.test_unicode :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_uuidcol.rst b/docs/api/sqlobject.tests.test_uuidcol.rst index 4ca71612..6ea9226e 100644 --- a/docs/api/sqlobject.tests.test_uuidcol.rst +++ b/docs/api/sqlobject.tests.test_uuidcol.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_uuidcol module -====================================== +sqlobject.tests.test\_uuidcol module +==================================== .. automodule:: sqlobject.tests.test_uuidcol :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_validation.rst b/docs/api/sqlobject.tests.test_validation.rst index 98ccedec..30e3fe04 100644 --- a/docs/api/sqlobject.tests.test_validation.rst +++ b/docs/api/sqlobject.tests.test_validation.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_validation module -========================================= +sqlobject.tests.test\_validation module +======================================= .. automodule:: sqlobject.tests.test_validation :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.tests.test_views.rst b/docs/api/sqlobject.tests.test_views.rst index 291d57c7..ed828c36 100644 --- a/docs/api/sqlobject.tests.test_views.rst +++ b/docs/api/sqlobject.tests.test_views.rst @@ -1,8 +1,7 @@ -sqlobject\.tests\.test\_views module -==================================== +sqlobject.tests.test\_views module +================================== .. automodule:: sqlobject.tests.test_views :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.util.csvexport.rst b/docs/api/sqlobject.util.csvexport.rst index 465bed93..25a3cb86 100644 --- a/docs/api/sqlobject.util.csvexport.rst +++ b/docs/api/sqlobject.util.csvexport.rst @@ -1,5 +1,5 @@ -sqlobject\.util\.csvexport module -================================= +sqlobject.util.csvexport module +=============================== .. automodule:: sqlobject.util.csvexport :members: diff --git a/docs/api/sqlobject.util.csvimport.rst b/docs/api/sqlobject.util.csvimport.rst index 6b19ee8d..415b518c 100644 --- a/docs/api/sqlobject.util.csvimport.rst +++ b/docs/api/sqlobject.util.csvimport.rst @@ -1,5 +1,5 @@ -sqlobject\.util\.csvimport module -================================= +sqlobject.util.csvimport module +=============================== .. automodule:: sqlobject.util.csvimport :members: diff --git a/docs/api/sqlobject.util.moduleloader.rst b/docs/api/sqlobject.util.moduleloader.rst index 9bfeb892..f262edad 100644 --- a/docs/api/sqlobject.util.moduleloader.rst +++ b/docs/api/sqlobject.util.moduleloader.rst @@ -1,5 +1,5 @@ -sqlobject\.util\.moduleloader module -==================================== +sqlobject.util.moduleloader module +================================== .. automodule:: sqlobject.util.moduleloader :members: diff --git a/docs/api/sqlobject.util.rst b/docs/api/sqlobject.util.rst index 2e194f23..9f5c1b38 100644 --- a/docs/api/sqlobject.util.rst +++ b/docs/api/sqlobject.util.rst @@ -1,5 +1,5 @@ -sqlobject\.util package -======================= +sqlobject.util package +====================== .. automodule:: sqlobject.util :members: diff --git a/docs/api/sqlobject.util.threadinglocal.rst b/docs/api/sqlobject.util.threadinglocal.rst index 3511fe17..dddba4d2 100644 --- a/docs/api/sqlobject.util.threadinglocal.rst +++ b/docs/api/sqlobject.util.threadinglocal.rst @@ -1,5 +1,5 @@ -sqlobject\.util\.threadinglocal module -====================================== +sqlobject.util.threadinglocal module +==================================== .. automodule:: sqlobject.util.threadinglocal :members: diff --git a/docs/api/sqlobject.versioning.rst b/docs/api/sqlobject.versioning.rst index 5f8c4d28..852d3e95 100644 --- a/docs/api/sqlobject.versioning.rst +++ b/docs/api/sqlobject.versioning.rst @@ -1,5 +1,5 @@ -sqlobject\.versioning package -============================= +sqlobject.versioning package +============================ .. automodule:: sqlobject.versioning :members: diff --git a/docs/api/sqlobject.versioning.test.rst b/docs/api/sqlobject.versioning.test.rst index f87edc23..6f9ea366 100644 --- a/docs/api/sqlobject.versioning.test.rst +++ b/docs/api/sqlobject.versioning.test.rst @@ -1,5 +1,5 @@ -sqlobject\.versioning\.test package -=================================== +sqlobject.versioning.test package +================================= .. automodule:: sqlobject.versioning.test :members: diff --git a/docs/api/sqlobject.versioning.test.test_version.rst b/docs/api/sqlobject.versioning.test.test_version.rst index 6ddf7722..2f03e9e4 100644 --- a/docs/api/sqlobject.versioning.test.test_version.rst +++ b/docs/api/sqlobject.versioning.test.test_version.rst @@ -1,8 +1,7 @@ -sqlobject\.versioning\.test\.test\_version module -================================================= +sqlobject.versioning.test.test\_version module +============================================== .. automodule:: sqlobject.versioning.test.test_version :members: :undoc-members: :show-inheritance: - :exclude-members: columnDefinitions,columnList,columns diff --git a/docs/api/sqlobject.views.rst b/docs/api/sqlobject.views.rst index ce4b406e..d0a30c58 100644 --- a/docs/api/sqlobject.views.rst +++ b/docs/api/sqlobject.views.rst @@ -1,5 +1,5 @@ -sqlobject\.views module -======================= +sqlobject.views module +====================== .. automodule:: sqlobject.views :members: diff --git a/docs/api/sqlobject.wsgi_middleware.rst b/docs/api/sqlobject.wsgi_middleware.rst index 79bc8727..b0aa884c 100644 --- a/docs/api/sqlobject.wsgi_middleware.rst +++ b/docs/api/sqlobject.wsgi_middleware.rst @@ -1,5 +1,5 @@ -sqlobject\.wsgi\_middleware module -================================== +sqlobject.wsgi\_middleware module +================================= .. automodule:: sqlobject.wsgi_middleware :members: diff --git a/docs/conf.py b/docs/conf.py index f8272ab1..b6e460e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,6 +35,11 @@ 'sphinx.ext.viewcode', ] +# Exclude uninformative members from the api docs +autodoc_default_options = { + 'exclude-members': 'columnDefinitions,columnList,columns,indexDefinitions' +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/download.rst b/docs/download.rst index 1d9e5467..d873d0bd 100644 --- a/docs/download.rst +++ b/docs/download.rst @@ -63,7 +63,7 @@ MySQL ^^^^^ mysql (installs MySQL-python for Python 2.7 and mysqlclient for Python 3.4+) -mysql-connector oursql pymysql +mysql-connector pymysql mariadb ODBC ^^^^ @@ -73,13 +73,13 @@ pyodbc pypyodbc odbc (synonym for pyodbc) PostgreSQL ^^^^^^^^^^ -psycopg2 psycopg postgres postgresql (synonyms for psycopg2) -pygresql pypostgresql py-postgresql pg8000 +psycopg psycopg2 postgres postgresql (synonyms for psycopg2) +pygresql pg8000 The rest ^^^^^^^^ -sapdb sqlite (pysqlite) sybase +sapdb sybase Repositories ------------ diff --git a/docs/rebuild b/docs/rebuild index 12703849..4798a23d 100755 --- a/docs/rebuild +++ b/docs/rebuild @@ -2,4 +2,4 @@ PYTHONPATH=.. make html && find . -name \*.tmp -type f -delete && -exec rsync -ahP --del --exclude=.buildinfo --exclude=objects.inv _build/html . +exec rsync -ahPv --del --exclude=.buildinfo --exclude=objects.inv _build/html . diff --git a/run_with_env.cmd b/run_with_env.cmd deleted file mode 100644 index 5da547c4..00000000 --- a/run_with_env.cmd +++ /dev/null @@ -1,88 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific -:: environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ -:: -:: Notes about batch files for Python people: -:: -:: Quotes in values are literally part of the values: -:: SET FOO="bar" -:: FOO is now five characters long: " b a r " -:: If you don't want quotes, don't include them on the right-hand side. -:: -:: The CALL lines at the end of this file look redundant, but if you move them -:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y -:: case, I don't know why. -@ECHO OFF - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf - -:: Extract the major and minor versions, and allow for the minor version to be -:: more than 9. This requires the version number to have two dots in it. -SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% -IF "%PYTHON_VERSION:~3,1%" == "." ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% -) ELSE ( - SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% -) - -:: Based on the Python version, determine what SDK version to use, and whether -:: to set the SDK for 64-bit. -IF %MAJOR_PYTHON_VERSION% == 2 ( - SET WINDOWS_SDK_VERSION="v7.0" - SET SET_SDK_64=Y -) ELSE ( - IF %MAJOR_PYTHON_VERSION% == 3 ( - SET WINDOWS_SDK_VERSION="v7.1" - IF %MINOR_PYTHON_VERSION% LEQ 4 ( - SET SET_SDK_64=Y - ) ELSE ( - SET SET_SDK_64=N - IF EXIST "%WIN_WDK%" ( - :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN "%WIN_WDK%" 0wdf - ) - ) - ) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 - ) -) - -IF %PYTHON_ARCH% == 64 ( - IF %SET_SDK_64% == Y ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) ELSE ( - ECHO Using default MSVC build environment for 64 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 - ) -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) diff --git a/setup.cfg b/setup.cfg index af28274e..461bc50f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ tag_date = 0 tag_svn_revision = 0 [flake8] -exclude = .git,.tox,docs/europython/*.py,validators.py +exclude = .git,.tox,docs/europython/*.py # E305: expected 2 blank lines after class or function definition, found 1 # W503 line break before binary operator # W605 invalid escape sequence diff --git a/setup.py b/setup.py index 9e5a9543..9158ff54 100755 --- a/setup.py +++ b/setup.py @@ -11,19 +11,19 @@ execfile(versionpath, sqlobject_version) # noqa: F821 'execfile' Py3 elif sys.version_info >= (3, 4): - exec(open(versionpath, 'rU').read(), sqlobject_version) + exec(open(versionpath, 'r').read(), sqlobject_version) else: raise ImportError("SQLObject requires Python 2.7 or 3.4+") subpackages = ['firebird', 'include', 'include.tests', 'inheritance', 'inheritance.tests', - 'manager', 'maxdb', 'mysql', 'mssql', 'postgres', 'rdbhost', + 'manager', 'maxdb', 'mysql', 'mssql', 'postgres', 'sqlite', 'sybase', 'tests', 'util', 'versioning', 'versioning.test'] setup( - name="SQLObject", + name="sqlobject", version=sqlobject_version['version'], description="Object-Relational Manager, aka database wrapper", long_description="""\ @@ -42,12 +42,12 @@ `SourceForge `_ and `GitHub `_. -.. image:: https://travis-ci.org/sqlobject/sqlobject.svg?branch=master - :target: https://travis-ci.org/sqlobject/sqlobject -""", +.. image:: https://github.com/sqlobject/sqlobject/actions/workflows/run-tests.yaml/badge.svg?branch=github-actions + :target: https://github.com/sqlobject/sqlobject/actions/workflows/run-tests.yaml +""", # noqa: E501 line too long long_description_content_type="text/x-rst", classifiers=[ - "Development Status :: 5 - Production/Stable", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: " "GNU Library or Lesser General Public License (LGPL)", @@ -59,6 +59,13 @@ "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", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Database :: Front-Ends", "Topic :: Software Development :: Libraries :: Python Modules", @@ -68,12 +75,12 @@ maintainer="Oleg Broytman", maintainer_email="phd@phdru.name", url="http://sqlobject.org/", - download_url="https://pypi.org/project/SQLObject/%s/" % + download_url="https://pypi.org/project/sqlobject/%s/" % sqlobject_version['version'], project_urls={ 'Homepage': 'http://sqlobject.org/', 'Development docs': 'http://sqlobject.org/devel/', - 'Download': 'https://pypi.org/project/SQLObject/%s/' % + 'Download': 'https://pypi.org/project/sqlobject/%s/' % sqlobject_version['version'], 'Github repo': 'https://github.com/sqlobject', 'Issue tracker': 'https://github.com/sqlobject/sqlobject/issues', @@ -99,6 +106,7 @@ install_requires=[ "FormEncode>=1.1.1,!=1.3.0; python_version=='2.7'", "FormEncode>=1.3.1; python_version>='3.4'", + "FormEncode>=2.1.1; python_version >= '3.13'", "PyDispatcher>=2.0.4", ], extras_require={ @@ -113,25 +121,53 @@ 'mysql:python_version=="2.7"': ['MySQL-python'], 'mysql:python_version>="3.4"': ['mysqlclient'], 'mysql-connector': ['mysql-connector'], - 'oursql': ['oursql'], - 'pymysql': ['pymysql'], + 'mysql-connector-python:python_version=="2.7"': + ['mysql-connector-python <= 8.0.23'], + 'mysql-connector-python:python_version=="3.4"': + ['mysql-connector-python <= 8.0.22, > 2.0', 'protobuf < 3.19'], + 'mysql-connector-python:python_version=="3.5"': + ['mysql-connector-python <= 8.0.23, >= 8.0.5'], + 'mysql-connector-python:python_version=="3.6"': + ['mysql-connector-python <= 8.0.28, >= 8.0.6'], + 'mysql-connector-python:python_version=="3.7"': + ['mysql-connector-python <= 8.0.29, >= 8.0.13'], + 'mysql-connector-python:python_version=="3.8"': + ['mysql-connector-python <= 8.0.29, >= 8.0.19'], + 'mysql-connector-python:python_version=="3.9"': + ['mysql-connector-python <= 8.0.29, >= 8.0.24'], + 'mysql-connector-python:python_version=="3.10"': + ['mysql-connector-python <= 8.0.29, >= 8.0.28'], + 'mysql-connector-python:python_version>="3.11"': + ['mysql-connector-python >= 8.0.29'], + 'pymysql:python_version == "2.7" or python_version == "3.5"': + ['pymysql < 1.0'], + 'pymysql:python_version == "3.4"': ['pymysql < 0.10.0'], + 'pymysql:python_version == "3.6"': ['pymysql < 1.0.3'], + 'pymysql:python_version >= "3.7"': ['pymysql'], + 'mariadb': ['mariadb'], # ODBC 'odbc': ['pyodbc'], 'pyodbc': ['pyodbc'], 'pypyodbc': ['pypyodbc'], # PostgreSQL - 'psycopg2': ['psycopg2'], - 'psycopg': ['psycopg2'], - 'postgres': ['psycopg2'], - 'postgresql': ['psycopg2'], - 'pygresql': ['pygresql'], - 'pypostgresql': ['py-postgresql'], - 'py-postgresql': ['py-postgresql'], - 'pg8000': ['pg8000'], + 'psycopg:python_version>="3.6"': ['psycopg[binary]'], + 'psycopg-c:python_version>="3.6"': ['psycopg-c'], + 'psycopg2': ['psycopg2-binary'], + 'postgres': ['psycopg2-binary'], + 'postgresql': ['psycopg2-binary'], + 'psycopg2-binary:python_version=="3.4"': ['psycopg2-binary == 2.8.4'], + 'psycopg2-binary:python_version!="3.4"': ['psycopg2-binary'], + 'pygresql:python_version=="3.4"': ['pygresql < 5.2'], + 'pygresql:python_version!="3.4"': ['pygresql'], + 'pg8000:python_version=="2.7"': ['pg8000 < 1.13'], + 'pg8000:python_version=="3.4"': ['pg8000 < 1.12.4'], + 'pg8000:python_version>="3.5"': ['pg8000'], # 'sapdb': ['sapdb'], - 'sqlite': ['pysqlite'], 'sybase': ['Sybase'], + # Non-DB API drivers + 'zope-dt:python_version=="3.4"': ['zope.datetime < 4.3'], + 'zope-dt:python_version!="3.4"': ['zope.datetime'], }, ) @@ -164,7 +200,7 @@ It currently supports MySQL through the `MySQLdb` package, PostgreSQL through the `psycopg` package, SQLite, Firebird, MaxDB (SAP DB), MS SQL -Sybase and Rdbhost. Python 2.7 or 3.4+ is required. +and Sybase. Python 2.7 or 3.4+ is required. Where is SQLObject @@ -177,7 +213,7 @@ https://lists.sourceforge.net/mailman/listinfo/sqlobject-discuss Download: -https://pypi.org/project/SQLObject/@@/ +https://pypi.org/project/sqlobject/@@/ News and changes: http://sqlobject.org/docs/News.html diff --git a/sqlobject/.coveragerc b/sqlobject/.coveragerc deleted file mode 100644 index c49e277b..00000000 --- a/sqlobject/.coveragerc +++ /dev/null @@ -1,11 +0,0 @@ -[run] -omit = - firebird/*.py - maxdb/*.py - mssql/*.py - rdbhost/*.py - sybase/*.py - tests/test_boundattributes.py - tests/test_paste.py - util/threadinglocal.py - wsgi_middleware.py diff --git a/sqlobject/.gitignore b/sqlobject/.gitignore deleted file mode 100644 index b53725ca..00000000 --- a/sqlobject/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/.coverage -/coverage.xml diff --git a/sqlobject/__version__.py b/sqlobject/__version__.py index 6e6740f7..37af2a9e 100644 --- a/sqlobject/__version__.py +++ b/sqlobject/__version__.py @@ -1,7 +1,7 @@ -version = '3.7.1' +version = '3.13.1' major = 3 -minor = 7 +minor = 13 micro = 1 release_level = 'final' serial = 0 diff --git a/sqlobject/col.py b/sqlobject/col.py index 81195393..49cd709a 100644 --- a/sqlobject/col.py +++ b/sqlobject/col.py @@ -44,29 +44,30 @@ datetime_available = True try: - from mx import DateTime + from mx import DateTime as mxDateTime except ImportError: - try: - # old version of mxDateTime, - # or Zope's Version if we're running with Zope - import DateTime - except ImportError: - mxdatetime_available = False - else: - mxdatetime_available = True + mxdatetime_available = False else: mxdatetime_available = True +try: + # DateTime from Zope + import DateTime as zopeDateTime +except ImportError: + zope_datetime_available = False +else: + zope_datetime_available = True + DATETIME_IMPLEMENTATION = "datetime" MXDATETIME_IMPLEMENTATION = "mxDateTime" +ZOPE_DATETIME_IMPLEMENTATION = "zopeDateTime" -if mxdatetime_available: - if hasattr(DateTime, "Time"): - DateTimeType = type(DateTime.now()) - TimeType = type(DateTime.Time()) - else: # Zope - DateTimeType = type(DateTime.DateTime()) - TimeType = type(DateTime.DateTime.Time(DateTime.DateTime())) +if mxdatetime_available and hasattr(mxDateTime, "Time"): + mxDateTimeType = type(mxDateTime.now()) + mxTimeType = type(mxDateTime.Time()) + +if zope_datetime_available: + zopeDateTimeType = type(zopeDateTime.DateTime()) __all__ = ["datetime_available", "mxdatetime_available", "default_datetime_implementation", "DATETIME_IMPLEMENTATION"] @@ -74,6 +75,9 @@ if mxdatetime_available: __all__.append("MXDATETIME_IMPLEMENTATION") +if zope_datetime_available: + __all__.append("ZOPE_DATETIME_IMPLEMENTATION") + default_datetime_implementation = DATETIME_IMPLEMENTATION if not PY2: @@ -170,8 +174,13 @@ def __init__(self, # True: a CASCADE constraint is generated # False: a RESTRICT constraint is generated # 'null': a SET NULL trigger is generated - if isinstance(cascade, str): - assert cascade == 'null', ( + if not isinstance(cascade, (bool, string_type, type(None))): + raise TypeError( + 'Expected cascade to be True, False, None or "null", ' + "(you gave: %r %r)" % (type(cascade), cascade) + ) + if isinstance(cascade, str) and (cascade != 'null'): + raise ValueError( "The only string value allowed for cascade is 'null' " "(you gave: %r)" % cascade) self.cascade = cascade @@ -733,9 +742,9 @@ def addSQLAttrs(self, str): if self.length and self.length >= 1: _ret = "%s(%d)" % (_ret, self.length) if self.unsigned: - _ret = _ret + " UNSIGNED" + _ret += " UNSIGNED" if self.zerofill: - _ret = _ret + " ZEROFILL" + _ret += " ZEROFILL" return _ret def _sqlType(self): @@ -1092,9 +1101,8 @@ def maxdbCreateSQL(self): sql = ' '.join([fidName, self._maxdbType()]) tName = other.sqlmeta.table idName = self.refColumn or other.sqlmeta.idName - sql = sql + ',' + '\n' - sql = sql + 'FOREIGN KEY (%s) REFERENCES %s(%s)' % (fidName, tName, - idName) + sql += ',\nFOREIGN KEY (%s) REFERENCES %s(%s)' % (fidName, tName, + idName) return sql def maxdbCreateReferenceConstraint(self): @@ -1252,7 +1260,7 @@ def to_python(self, value, state): datetime.time, sqlbuilder.SQLExpression)): return value if mxdatetime_available: - if isinstance(value, DateTimeType): + if isinstance(value, mxDateTimeType): # convert mxDateTime instance to datetime if (self.format.find("%H") >= 0) or \ (self.format.find("%T")) >= 0: @@ -1262,13 +1270,23 @@ def to_python(self, value, state): int(value.second)) else: return datetime.date(value.year, value.month, value.day) - elif isinstance(value, TimeType): + elif isinstance(value, mxTimeType): # convert mxTime instance to time if self.format.find("%d") >= 0: return datetime.timedelta(seconds=value.seconds) else: return datetime.time(value.hour, value.minute, int(value.second)) + if zope_datetime_available: + if isinstance(value, zopeDateTimeType): + # convert zopeDateTime instance to datetime + if (self.format.find("%H") >= 0) or \ + (self.format.find("%T")) >= 0: + return datetime.datetime( + value.year(), value.month(), value.day(), + value.hour(), value.minute(), int(value.second())) + else: + return datetime.date(value.year, value.month, value.day) try: if self.format.find(".%f") >= 0: if '.' in value: @@ -1310,23 +1328,30 @@ def to_python(self, value, state): if value is None: return None if isinstance(value, - (DateTimeType, TimeType, sqlbuilder.SQLExpression)): + (mxDateTimeType, mxTimeType, + sqlbuilder.SQLExpression)): return value if isinstance(value, datetime.datetime): - return DateTime.DateTime(value.year, value.month, value.day, - value.hour, value.minute, - value.second) + return mxDateTime.DateTime(value.year, value.month, value.day, + value.hour, value.minute, + value.second) elif isinstance(value, datetime.date): - return DateTime.Date(value.year, value.month, value.day) + return mxDateTime.Date(value.year, value.month, value.day) elif isinstance(value, datetime.time): - return DateTime.Time(value.hour, value.minute, value.second) + return mxDateTime.Time(value.hour, value.minute, value.second) elif isinstance(value, datetime.timedelta): if value.days: raise validators.Invalid( "the value for the TimeCol '%s' must has days=0, " "it has days=%d" % (self.name, value.days), value, state) - return DateTime.Time(seconds=value.seconds) + return mxDateTime.Time(seconds=value.seconds) + if zope_datetime_available: + if isinstance(value, zopeDateTimeType): + # convert zopeDateTime instance to mxdatetime + return mxDateTime.DateTime( + value.year(), value.month(), value.day(), + value.hour(), value.minute(), int(value.second())) try: if self.format.find(".%f") >= 0: if '.' in value: @@ -1342,9 +1367,9 @@ def to_python(self, value, state): else: value += '.0' value = datetime.datetime.strptime(value, self.format) - return DateTime.DateTime(value.year, value.month, value.day, - value.hour, value.minute, - value.second) + return mxDateTime.DateTime(value.year, value.month, value.day, + value.hour, value.minute, + value.second) except Exception: raise validators.Invalid( "expected a date/time string of the '%s' format " @@ -1356,7 +1381,8 @@ def from_python(self, value, state): if value is None: return None if isinstance(value, - (DateTimeType, TimeType, sqlbuilder.SQLExpression)): + (mxDateTimeType, mxTimeType, + sqlbuilder.SQLExpression)): return value if hasattr(value, "strftime"): return value.strftime(self.format) @@ -1366,6 +1392,76 @@ def from_python(self, value, state): self.name, type(value), value), value, state) +if zope_datetime_available: + class ZopeDateTimeValidator(validators.DateValidator): + def to_python(self, value, state): + if value is None: + return None + if isinstance(value, + (zopeDateTimeType, sqlbuilder.SQLExpression)): + return value + if isinstance(value, datetime.datetime): + return zopeDateTime.DateTime( + value.year, value.month, value.day, + value.hour, value.minute, value.second) + elif isinstance(value, datetime.date): + return zopeDateTime.DateTime( + value.year, value.month, value.day) + elif isinstance(value, datetime.time): + return zopeDateTime.DateTime( + value.hour, value.minute, value.second) + elif isinstance(value, datetime.timedelta): + if value.days: + raise validators.Invalid( + "the value for the TimeCol '%s' must has days=0, " + "it has days=%d" % (self.name, value.days), + value, state) + return zopeDateTime.DateTime(seconds=value.seconds) + if mxdatetime_available: + if isinstance(value, mxDateTimeType): + # convert mxDateTime instance to zopeDateTime + return zopeDateTime.DateTime( + value.year, value.month, value.day, + value.hour, value.minute, value.second) + try: + if self.format.find(".%f") >= 0: + if '.' in value: + _value = value.split('.') + microseconds = _value[-1] + _l = len(microseconds) + if _l < 6: + _value[-1] = microseconds + '0' * (6 - _l) + elif _l > 6: + _value[-1] = microseconds[:6] + if _l != 6: + value = '.'.join(_value) + else: + value += '.0' + value = datetime.datetime.strptime(value, self.format) + return zopeDateTime.DateTime( + value.year, value.month, value.day, + value.hour, value.minute, value.second) + except Exception: + raise validators.Invalid( + "expected a date/time string of the '%s' format " + "in the DateTimeCol '%s', got %s %r instead" % ( + self.format, self.name, type(value), value), + value, state) + + def from_python(self, value, state): + if value is None: + return None + if isinstance(value, + (zopeDateTimeType, sqlbuilder.SQLExpression)): + return value + if hasattr(value, "strftime"): + return value.strftime(self.format) + raise validators.Invalid( + "expected a zopeDateTime in the DateTimeCol '%s', " + "got %s %r instead" % ( + self.name, type(value), value), value, state) + + class SODateTimeCol(SOCol): datetimeFormat = '%Y-%m-%d %H:%M:%S.%f' @@ -1381,6 +1477,8 @@ def createValidators(self): validatorClass = DateTimeValidator elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION: validatorClass = MXDateTimeValidator + elif default_datetime_implementation == ZOPE_DATETIME_IMPLEMENTATION: + validatorClass = ZopeDateTimeValidator if default_datetime_implementation: _validators.insert(0, validatorClass(name=self.name, format=self.datetimeFormat)) @@ -1422,7 +1520,9 @@ def now(): if default_datetime_implementation == DATETIME_IMPLEMENTATION: return datetime.datetime.now() elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION: - return DateTime.now() + return mxDateTime.now() + elif default_datetime_implementation == ZOPE_DATETIME_IMPLEMENTATION: + return zopeDateTime.DateTimr() else: assert 0, ("No datetime implementation available " "(DATETIME_IMPLEMENTATION=%r)" @@ -1463,6 +1563,8 @@ def createValidators(self): validatorClass = DateValidator elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION: validatorClass = MXDateTimeValidator + elif default_datetime_implementation == ZOPE_DATETIME_IMPLEMENTATION: + validatorClass = ZopeDateTimeValidator if default_datetime_implementation: _validators.insert(0, validatorClass(name=self.name, format=self.dateFormat)) @@ -1506,7 +1608,10 @@ def to_python(self, value, state): raise validators.Invalid( "the value for the TimeCol '%s' must has days=0, " "it has days=%d" % (self.name, value.days), value, state) - return datetime.time(*time.gmtime(value.seconds)[3:6]) + return datetime.time( + *time.gmtime(value.seconds)[3:6], + microsecond=value.microseconds + ) value = super(TimeValidator, self).to_python(value, state) if isinstance(value, datetime.datetime): value = value.time() @@ -1530,6 +1635,8 @@ def createValidators(self): validatorClass = TimeValidator elif default_datetime_implementation == MXDATETIME_IMPLEMENTATION: validatorClass = MXDateTimeValidator + elif default_datetime_implementation == ZOPE_DATETIME_IMPLEMENTATION: + validatorClass = ZopeDateTimeValidator if default_datetime_implementation: _validators.insert(0, validatorClass(name=self.name, format=self.timeFormat)) @@ -1915,6 +2022,8 @@ def to_python(self, value, state): def from_python(self, value, state): if value is None: return None + if isinstance(value, str): + return value if isinstance(value, UUID): return str(value) raise validators.Invalid( @@ -1971,11 +2080,13 @@ class JsonbCol(Col): baseClass = SOJsonbCol -class JSONValidator(StringValidator): +class JSONValidator(SOValidator): def to_python(self, value, state): if value is None: return None + if isinstance(value, (bool, int, float, long, dict, list)): + return value if isinstance(value, string_type): return json.loads(value) raise validators.Invalid( @@ -1998,14 +2109,71 @@ def from_python(self, value, state): class SOJSONCol(SOStringCol): def createValidators(self): - return [JSONValidator(name=self.name)] + \ - super(SOJSONCol, self).createValidators() + return [JSONValidator(name=self.name)] + + # Doesn't work, especially with Postgres + # def _sqlType(self): + # return 'JSON' class JSONCol(StringCol): baseClass = SOJSONCol +############################################# +# +# Added by Tippett +# + +# +# Convenience stuff Monkey-Patched onto sqlbuilder.SQLObjectField, +# so you can do: +# +# Table.q.column.json_extract("key") == value +# Table.q.column.json_contains("item") +# Table.q.column.json_length("key") +# +# instead of +# +# func.JSON_EXTRACT(Table.q.column, '$.key') == value +# func.JSON_CONTAINS(Table.q.column, json.dumps("item")) +# func.JSON_LENGTH(Table.q.column, '$.key') +# + +def _json_extract(col, key, path=None): + """ + """ + assert isinstance(col.column, SOJSONCol), \ + "{!r} is not a JSONCol".format(col) + if path is None: + return sqlbuilder.func.JSON_EXTRACT(col, '$.%s' % key) + else: + return sqlbuilder.func.JSON_EXTRACT(col, '$.%s' % key, path) + + +def _json_contains(col, value, path=None): + assert isinstance(col.column, SOJSONCol), \ + "{!r} is not a JSONCol".format(col) + if path is None: + return sqlbuilder.func.JSON_CONTAINS(col, json.dumps(value)) + else: + return sqlbuilder.func.JSON_CONTAINS(col, json.dumps(value), path) + + +def _json_length(col, path=None): + assert isinstance(col.column, SOJSONCol), \ + "{!r} is not a JSONCol".format(col) + if path is None: + return sqlbuilder.func.JSON_LENGTH(col) + else: + return sqlbuilder.func.JSON_LENGTH(col, path) + + +sqlbuilder.SQLObjectField.json_extract = _json_extract +sqlbuilder.SQLObjectField.json_contains = _json_contains +sqlbuilder.SQLObjectField.json_length = _json_length + + def pushKey(kw, name, value): if name not in kw: kw[name] = value diff --git a/sqlobject/compat.py b/sqlobject/compat.py index 72d696f4..fc8edf25 100644 --- a/sqlobject/compat.py +++ b/sqlobject/compat.py @@ -47,4 +47,13 @@ def load_module_from_file(base_name, module_name, filename): def load_module_from_file(base_name, module_name, filename): specs = importlib.util.spec_from_file_location(module_name, filename) - return specs.loader.load_module() + loader = specs.loader + if hasattr(loader, 'create_module'): + module = loader.create_module(specs) + else: + module = None + if module is None: + return specs.loader.load_module() + else: + loader.exec_module(module) + return module diff --git a/sqlobject/conftest.py b/sqlobject/conftest.py index af39c6ff..3673168c 100644 --- a/sqlobject/conftest.py +++ b/sqlobject/conftest.py @@ -16,7 +16,6 @@ 'dbm': 'dbm:///data', 'postgres': 'postgres:///test', 'postgresql': 'postgres:///test', - 'rdbhost': 'rdhbost://role:authcode@www.rdbhost.com/', 'pygresql': 'pygresql://localhost/test', 'sqlite': 'sqlite:/:memory:', 'sybase': 'sybase://test:test123@sybase/test?autoCommit=0', diff --git a/sqlobject/constraints.py b/sqlobject/constraints.py index 9241579f..1b8bd0fe 100644 --- a/sqlobject/constraints.py +++ b/sqlobject/constraints.py @@ -51,8 +51,8 @@ def isBool(obj, col, value): class InList: - def __init__(self, l): - self.list = l + def __init__(self, _l): + self.list = _l def __call__(self, obj, col, value): if value not in self.list: diff --git a/sqlobject/converters.py b/sqlobject/converters.py index 5f4a7c93..667fdded 100644 --- a/sqlobject/converters.py +++ b/sqlobject/converters.py @@ -16,13 +16,25 @@ try: - from mx.DateTime import DateTimeType, DateTimeDeltaType + from mx.DateTime import DateTimeType as mxDateTimeType, \ + DateTimeDeltaType as mxDateTimeDeltaType except ImportError: - try: - from DateTime import DateTimeType, DateTimeDeltaType - except ImportError: - DateTimeType = None - DateTimeDeltaType = None + mxDateTimeType = None + mxDateTimeDeltaType = None + +try: + import pendulum +except ImportError: + pendulumDateTimeType = None +else: + pendulumDateTimeType = pendulum.DateTime + +try: + from DateTime import DateTime as zopeDateTime +except ImportError: + zopeDateTimeType = None +else: + zopeDateTimeType = type(zopeDateTime()) try: import Sybase @@ -84,14 +96,14 @@ def StringLikeConverter(value, db): elif isinstance(value, buffer_type): value = str(value) - if db in ('mysql', 'postgres', 'rdbhost'): + if db in ('mysql', 'postgres'): for orig, repl in sqlStringReplace: value = value.replace(orig, repl) elif db in ('sqlite', 'firebird', 'sybase', 'maxdb', 'mssql'): value = value.replace("'", "''") else: assert 0, "Database %s unknown" % db - if db in ('postgres', 'rdbhost') and ('\\' in value): + if (db == 'postgres') and ('\\' in value): return "E'%s'" % value return "'%s'" % value @@ -125,7 +137,7 @@ def LongConverter(value, db): def BoolConverter(value, db): - if db in ('postgres', 'rdbhost'): + if db == 'postgres': if value: return "'t'" else: @@ -144,17 +156,6 @@ def FloatConverter(value, db): registerConverter(float, FloatConverter) -if DateTimeType: - def mxDateTimeConverter(value, db): - return "'%s'" % value.strftime("%Y-%m-%d %H:%M:%S") - - registerConverter(DateTimeType, mxDateTimeConverter) - - def mxTimeConverter(value, db): - return "'%s'" % value.strftime("%H:%M:%S") - - registerConverter(DateTimeDeltaType, mxTimeConverter) - def NoneConverter(value, db): return "NULL" @@ -209,6 +210,32 @@ def TimeConverterMS(value, db): registerConverter(datetime.time, TimeConverterMS) +if mxDateTimeType: + def mxDateTimeConverter(value, db): + return "'%s'" % value.strftime("%Y-%m-%d %H:%M:%S") + + registerConverter(mxDateTimeType, mxDateTimeConverter) + + def mxTimeConverter(value, db): + return "'%s'" % value.strftime("%H:%M:%S") + + registerConverter(mxDateTimeDeltaType, mxTimeConverter) + + +if pendulumDateTimeType: + def pendulumConverter(value, db): + return "'%s'" % value.to_datetime_string() + + registerConverter(pendulum.DateTime, pendulumConverter) + + +if zopeDateTimeType: + def zopeDateTimeConverter(value, db): + return "'%s'" % value.strftime("%Y-%m-%d %H:%M:%S") + + registerConverter(zopeDateTimeType, zopeDateTimeConverter) + + def DecimalConverter(value, db): return value.to_eng_string() @@ -237,7 +264,7 @@ def sqlrepr(obj, db=None): def quote_str(s, db): - if db in ('postgres', 'rdbhost') and ('\\' in s): + if (db == 'postgres') and ('\\' in s): return "E'%s'" % s return "'%s'" % s diff --git a/sqlobject/dbconnection.py b/sqlobject/dbconnection.py index fbff4576..75176479 100644 --- a/sqlobject/dbconnection.py +++ b/sqlobject/dbconnection.py @@ -108,8 +108,8 @@ def oldUri(self): auth = getattr(self, 'user', '') or '' if auth: if self.password: - auth = auth + ':' + self.password - auth = auth + '@' + auth += ':' + self.password + auth += '@' else: assert not getattr(self, 'password', None), ( 'URIs cannot express passwords without usernames') @@ -129,8 +129,8 @@ def uri(self): if auth: auth = quote(auth) if self.password: - auth = auth + ':' + quote(self.password) - auth = auth + '@' + auth += ':' + quote(self.password) + auth += '@' else: assert not getattr(self, 'password', None), ( 'URIs cannot express passwords without usernames') @@ -799,7 +799,7 @@ def __init__(self, dbConnection): self._obsolete = True self._dbConnection = dbConnection self._connection = dbConnection.getConnection() - self._dbConnection._setAutoCommit(self._connection, 0) + self._dbConnection._setAutoCommit(self._connection, False) self.cache = CacheSet(cache=dbConnection.doCache) self._deletedCache = {} self._obsolete = False @@ -924,7 +924,7 @@ def __getattr__(self, attr): def _makeObsolete(self): self._obsolete = True if self._dbConnection.autoCommit: - self._dbConnection._setAutoCommit(self._connection, 1) + self._dbConnection._setAutoCommit(self._connection, True) self._dbConnection.releaseConnection(self._connection, explicit=True) self._connection = None @@ -938,7 +938,7 @@ def begin(self): "without rolling back this one" self._obsolete = False self._connection = self._dbConnection.getConnection() - self._dbConnection._setAutoCommit(self._connection, 0) + self._dbConnection._setAutoCommit(self._connection, False) def __del__(self): if self._obsolete: @@ -989,14 +989,21 @@ def __set__(self, obj, value): def getConnection(self): try: - return self.threadingLocal.connection + connection = self.threadingLocal.connection + if isinstance(connection, string_type): + connection = connectionForURI(connection) + self.threadingLocal.connection = connection except AttributeError: try: - return self.processConnection + connection = self.processConnection + if isinstance(connection, string_type): + connection = connectionForURI(connection) + self.processConnection = connection except AttributeError: raise AttributeError( "No connection has been defined for this thread " "or process") + return connection def doInTransaction(self, func, *args, **kw): """ @@ -1019,6 +1026,8 @@ def doInTransaction(self, func, *args, **kw): except AttributeError: old_conn = self.processConnection old_conn_is_threading = False + if isinstance(old_conn, string_type): + old_conn = connectionForURI(old_conn) conn = old_conn.transaction() if old_conn_is_threading: self.threadConnection = conn @@ -1071,12 +1080,12 @@ def registerConnection(self, schemes, builder): def registerConnectionInstance(self, inst): if inst.name: assert (inst.name not in self.instanceNames - or self.instanceNames[inst.name] is cls # noqa - ), ("A instance has already been registered " - "with the name %s" % inst.name) + or self.instanceNames[inst.name] is self.__class__ + ), ("An instance has already been registered " + "with the name %s" % inst.name) assert inst.name.find(':') == -1, \ "You cannot include ':' " \ - "in your class names (%r)" % cls.name # noqa + "in your DB connection names (%r)" % inst.name self.instanceNames[inst.name] = inst def connectionForURI(self, uri, oldUri=False, **args): @@ -1124,6 +1133,5 @@ def dbConnectionForScheme(self, scheme): from . import mssql # noqa from . import mysql # noqa from . import postgres # noqa -from . import rdbhost # noqa from . import sqlite # noqa from . import sybase # noqa diff --git a/sqlobject/firebird/firebirdconnection.py b/sqlobject/firebird/firebirdconnection.py index 4624b487..9c36f7af 100644 --- a/sqlobject/firebird/firebirdconnection.py +++ b/sqlobject/firebird/firebirdconnection.py @@ -11,7 +11,7 @@ class FirebirdConnection(DBAPI): dbName = 'firebird' schemes = [dbName] - limit_re = re.compile('^\s*(select )(.*)', re.IGNORECASE) + limit_re = re.compile(r'^\s*(select )(.*)', re.IGNORECASE) def __init__(self, host, db, port='3050', user='sysdba', password='masterkey', autoCommit=1, diff --git a/sqlobject/include/hashcol.py b/sqlobject/include/hashcol.py index e36b980e..7a8bbf4d 100644 --- a/sqlobject/include/hashcol.py +++ b/sqlobject/include/hashcol.py @@ -6,8 +6,10 @@ class DbHash: - """ Presents a comparison object for hashes, allowing plain text to be - automagically compared with the base content. """ + """ + Presents a comparison object for hashes, allowing plain text to be + automagically compared with the base content + """ def __init__(self, hash, hashMethod): self.hash = hash @@ -61,7 +63,7 @@ def __repr__(self): class HashValidator(sqlobject.col.StringValidator): - """ Provides formal SQLObject validation services for the HashCol. """ + """Provides formal SQLObject validation services for the HashCol""" def to_python(self, value, state): """ Passes out a hash object. """ @@ -70,14 +72,14 @@ def to_python(self, value, state): return DbHash(hash=value, hashMethod=self.hashMethod) def from_python(self, value, state): - """ Store the given value as a MD5 hash, or None if specified. """ + """Store the given value as a MD5 hash, or None if specified""" if value is None: return None return self.hashMethod(value) class SOHashCol(sqlobject.col.SOStringCol): - """ The internal HashCol definition. By default, enforces a md5 digest. """ + """The internal HashCol definition. By default, enforces a md5 digest""" def __init__(self, **kw): if 'hashMethod' not in kw: @@ -99,7 +101,9 @@ def createValidators(self): class HashCol(sqlobject.col.StringCol): - """ End-user HashCol class. May be instantiated with 'hashMethod', a function - which returns the string hash of any other string (i.e. basestring). """ + """ + End-user HashCol class. May be instantiated with 'hashMethod', a function + which returns the string hash of any other string (i.e. basestring) + """ baseClass = SOHashCol diff --git a/sqlobject/inheritance/tests/test_deep_inheritance.py b/sqlobject/inheritance/tests/test_deep_inheritance.py index 1ef4d36f..4bdc8e34 100644 --- a/sqlobject/inheritance/tests/test_deep_inheritance.py +++ b/sqlobject/inheritance/tests/test_deep_inheritance.py @@ -72,6 +72,11 @@ def test_creation_fail2(): def test_deep_inheritance(): + conn = getConnection() + if conn.module.__name__ == 'mysql.connector' \ + and conn.connector_type == 'mysql.connector-python': + skip("connector-python falls into an infinite loop here") + setupClass([DIManager, DIEmployee, DIPerson]) manager = DIManager(firstName='Project', lastName='Manager', @@ -81,7 +86,6 @@ def test_deep_inheritance(): so_position='Project leader', manager=manager).id DIPerson(firstName='Oneof', lastName='Authors', manager=manager) - conn = getConnection() cache = conn.cache cache.clear() diff --git a/sqlobject/inheritance/tests/test_inheritance.py b/sqlobject/inheritance/tests/test_inheritance.py index cc26c8f5..2dfd0e3a 100644 --- a/sqlobject/inheritance/tests/test_inheritance.py +++ b/sqlobject/inheritance/tests/test_inheritance.py @@ -1,7 +1,21 @@ +import pytest from pytest import raises from sqlobject import IntCol, StringCol from sqlobject.inheritance import InheritableSQLObject -from sqlobject.tests.dbtest import setupClass +from sqlobject.tests.dbtest import getConnection, setupClass + + +try: + connection = getConnection() +except (AttributeError, NameError): + # The module was imported during documentation building + pass +else: + if connection.module.__name__ == 'mysql.connector' \ + and connection.connector_type == 'mysql.connector-python': + pytestmark = pytest.mark.skip( + "connector-python falls into an infinite loop here") + ######################################## # Inheritance diff --git a/sqlobject/inheritance/tests/test_inheritance_tree.py b/sqlobject/inheritance/tests/test_inheritance_tree.py index 58cdb7ea..34666c38 100644 --- a/sqlobject/inheritance/tests/test_inheritance_tree.py +++ b/sqlobject/inheritance/tests/test_inheritance_tree.py @@ -1,6 +1,7 @@ +from pytest import skip from sqlobject import StringCol from sqlobject.inheritance import InheritableSQLObject -from sqlobject.tests.dbtest import setupClass +from sqlobject.tests.dbtest import getConnection, setupClass ######################################## # Inheritance Tree @@ -28,6 +29,11 @@ class Tree5(Tree2): def test_tree(): + conn = getConnection() + if conn.module.__name__ == 'mysql.connector' \ + and conn.connector_type == 'mysql.connector-python': + skip("connector-python falls into an infinite loop here") + setupClass([Tree1, Tree2, Tree3, Tree4, Tree5]) Tree1(aprop='t1') # t1 diff --git a/sqlobject/joins.py b/sqlobject/joins.py index afb0f881..948d04d3 100644 --- a/sqlobject/joins.py +++ b/sqlobject/joins.py @@ -2,6 +2,7 @@ from . import boundattributes from . import classregistry from . import events +from . import sresults from . import styles from . import sqlbuilder from .styles import capword @@ -170,9 +171,9 @@ def __init__(self, addRemoveName=None, **kw): if not self.joinMethodName: name = self.otherClassName[0].lower() + self.otherClassName[1:] if name.endswith('s'): - name = name + "es" + name += "es" else: - name = name + "s" + name += "s" self.joinMethodName = name if addRemoveName: self.addRemoveName = addRemoveName @@ -291,46 +292,79 @@ class RelatedJoin(MultipleJoin): class OtherTableToJoin(sqlbuilder.SQLExpression): - def __init__(self, otherTable, otherIdName, interTable, joinColumn): + def __init__(self, otherTable, otherIdName, interTable, joinColumn, alias): self.otherTable = otherTable self.otherIdName = otherIdName self.interTable = interTable self.joinColumn = joinColumn + self.alias = alias def tablesUsedImmediate(self): - return [self.otherTable, self.interTable] + return [ + '%s %s' % (self.otherTable, self.alias) + if self.alias else self.otherTable, + self.interTable, + ] def __sqlrepr__(self, db): - return '%s.%s = %s.%s' % (self.otherTable, self.otherIdName, - self.interTable, self.joinColumn) + return '%s.%s = %s.%s' % ( + self.alias if self.alias else self.otherTable, + self.otherIdName, self.interTable, self.joinColumn) class JoinToTable(sqlbuilder.SQLExpression): - def __init__(self, table, idName, interTable, joinColumn): + def __init__(self, table, idName, interTable, joinColumn, alias): self.table = table self.idName = idName self.interTable = interTable self.joinColumn = joinColumn + self.alias = alias def tablesUsedImmediate(self): - return [self.table, self.interTable] + return [ + '%s %s' % (self.table, self.alias) + if self.alias else self.table, + self.interTable, + ] def __sqlrepr__(self, db): - return '%s.%s = %s.%s' % (self.interTable, self.joinColumn, self.table, - self.idName) + return '%s.%s = %s.%s' % ( + self.interTable, self.joinColumn, + self.alias if self.alias else self.table, self.idName) class TableToId(sqlbuilder.SQLExpression): - def __init__(self, table, idName, idValue): + def __init__(self, table, idName, idValue, alias): self.table = table self.idName = idName self.idValue = idValue + self.alias = alias def tablesUsedImmediate(self): - return [self.table] + return [ + '%s %s' % (self.table, self.alias) + if self.alias else self.table, + ] def __sqlrepr__(self, db): - return '%s.%s = %s' % (self.table, self.idName, self.idValue) + return '%s.%s = %s' % ( + self.alias if self.alias else self.table, + self.idName, self.idValue) + + +class SQLJoinSelectResults(sresults.SelectResults): + def filter(self, filter_clause): + clause_tables = filter_clause.tablesUsed(None) + if self._SOSQLRelatedJoin_realSourceClass.sqlmeta.table \ + in clause_tables: + tableClass = self._SOSQLRelatedJoin_realSourceClass.__name__ + raise ValueError( + "Using table '%s' in the filter expression without an alias " + "could produce wrong SQL. Most probably you need " + "Alias(%s, '_SO_SQLRelatedJoin_OtherTable') instead." + % (tableClass, tableClass) + ) + return sresults.SelectResults.filter(self, filter_clause) class SOSQLRelatedJoin(SORelatedJoin): @@ -339,22 +373,47 @@ def performJoin(self, inst): conn = inst._connection else: conn = None - results = self.otherClass.select(sqlbuilder.AND( - OtherTableToJoin( - self.otherClass.sqlmeta.table, self.otherClass.sqlmeta.idName, - self.intermediateTable, self.otherColumn + needAlias = self.soClass is self.otherClass + if needAlias: + source = sqlbuilder.Alias( + self.otherClass, '_SO_SQLRelatedJoin_OtherTable') + sresultsClass = SQLJoinSelectResults + else: + source = self.otherClass + sresultsClass = self.otherClass.SelectResultsClass + results = sresultsClass( + source, + sqlbuilder.AND( + OtherTableToJoin( + self.otherClass.sqlmeta.table, + self.otherClass.sqlmeta.idName, + self.intermediateTable, self.otherColumn, + '_SO_SQLRelatedJoin_OtherTable' if needAlias else '', + ), + JoinToTable( + self.soClass.sqlmeta.table, self.soClass.sqlmeta.idName, + self.intermediateTable, self.joinColumn, + '_SO_SQLRelatedJoin_ThisTable' if needAlias else '', + ), + TableToId( + self.soClass.sqlmeta.table, self.soClass.sqlmeta.idName, + inst.id, + '_SO_SQLRelatedJoin_ThisTable' if needAlias else '', + ), ), - JoinToTable( - self.soClass.sqlmeta.table, self.soClass.sqlmeta.idName, - self.intermediateTable, self.joinColumn + clauseTables=( + '%s _SO_SQLRelatedJoin_ThisTable' % self.soClass.sqlmeta.table + if needAlias else self.soClass.sqlmeta.table, + '%s _SO_SQLRelatedJoin_OtherTable' % + self.otherClass.sqlmeta.table + if needAlias else self.otherClass.sqlmeta.table, + self.intermediateTable, ), - TableToId(self.soClass.sqlmeta.table, self.soClass.sqlmeta.idName, - inst.id), - ), clauseTables=(self.soClass.sqlmeta.table, - self.otherClass.sqlmeta.table, - self.intermediateTable), - connection=conn) - return results.orderBy(self.orderBy) + connection=conn, + orderBy=self.orderBy, + ) + results._SOSQLRelatedJoin_realSourceClass = self.otherClass + return results class SQLRelatedJoin(RelatedJoin): diff --git a/sqlobject/main.py b/sqlobject/main.py index 5d9b537a..7d2fd1c3 100644 --- a/sqlobject/main.py +++ b/sqlobject/main.py @@ -190,6 +190,7 @@ class sqlmeta(with_metaclass(declarative.DeclarativeMeta, object)): table = None idName = None + idSize = None # Allowed values are 'TINY/SMALL/MEDIUM/BIG/None' idSequence = None # This function is used to coerce IDs into the proper format, # so you should replace it with str, or another function, if you @@ -268,6 +269,13 @@ def send(cls, signal, *args, **kw): @classmethod def setClass(cls, soClass): + if cls.idType not in (int, str): + raise TypeError('sqlmeta.idType must be int or str, not %r' + % cls.idType) + if cls.idSize not in ('TINY', 'SMALL', 'MEDIUM', 'BIG', None): + raise ValueError( + "sqlmeta.idType must be 'TINY', 'SMALL', 'MEDIUM', 'BIG' " + "or None, not %r" % cls.idSize) cls.soClass = soClass if not cls.style: cls.style = styles.defaultStyle @@ -1621,7 +1629,6 @@ def destroySelf(self): join.joinColumn, self.id) self._connection.query(q) - depends = [] depends = self._SO_depends() for k in depends: # Free related joins @@ -1640,36 +1647,45 @@ def destroySelf(self): continue query = [] - delete = setnull = restrict = False + restrict = False for _col in cols: + query.append(getattr(k.q, _col.name) == self.id) if _col.cascade is False: # Found a restriction restrict = True - query.append(getattr(k.q, _col.name) == self.id) + query = sqlbuilder.OR(*query) + results = k.select(query, connection=self._connection) + if restrict and results.count(): + # Restrictions only apply if there are + # matching records on the related table + raise SQLObjectIntegrityError( + "Tried to delete %s::%s but " + "table %s has a restriction against it" % + (klass.__name__, self.id, k.__name__)) + + setnull = {} + for _col in cols: if _col.cascade == 'null': - setnull = _col.name - elif _col.cascade: + setnull[_col.name] = None + if setnull: + for row in results: + clear = {} + for name in setnull: + if getattr(row, name) == self.id: + clear[name] = None + row.set(**clear) + + delete = False + for _col in cols: + if _col.cascade is True: delete = True assert delete or setnull or restrict, ( "Class %s depends on %s accoriding to " "findDependantColumns, but this seems inaccurate" % (k, klass)) - query = sqlbuilder.OR(*query) - results = k.select(query, connection=self._connection) - if restrict: - if results.count(): - # Restrictions only apply if there are - # matching records on the related table - raise SQLObjectIntegrityError( - "Tried to delete %s::%s but " - "table %s has a restriction against it" % - (klass.__name__, self.id, k.__name__)) - else: + if delete: for row in results: - if delete: - row.destroySelf() - else: - row.set(**{setnull: None}) + row.destroySelf() self.sqlmeta._obsolete = True self._connection._SO_delete(self) diff --git a/sqlobject/manager/command.py b/sqlobject/manager/command.py index e9891d91..fc8db834 100755 --- a/sqlobject/manager/command.py +++ b/sqlobject/manager/command.py @@ -1095,8 +1095,8 @@ def update_db(self, version, conn): connection=conn) def strip_comments(self, sql): - lines = [l for l in sql.splitlines() - if not l.strip().startswith('--')] + lines = [_l for _l in sql.splitlines() + if not _l.strip().startswith('--')] return '\n'.join(lines) def base_dir(self): diff --git a/sqlobject/mssql/mssqlconnection.py b/sqlobject/mssql/mssqlconnection.py index b135fe44..d2fff311 100644 --- a/sqlobject/mssql/mssqlconnection.py +++ b/sqlobject/mssql/mssqlconnection.py @@ -10,7 +10,7 @@ class MSSQLConnection(DBAPI): dbName = 'mssql' schemes = [dbName] - limit_re = re.compile('^\s*(select )(.*)', re.IGNORECASE) + limit_re = re.compile(r'^\s*(select )(.*)', re.IGNORECASE) odbc_keywords = ('Server', 'Port', 'User Id', 'Password', 'Database') @@ -105,6 +105,8 @@ def _make_conn_str(keys): ('host', keys.host), ('port', keys.port), ('timeout', keys.timeout), + ('charset', keys.charset), + ('tds_version', keys.tds_version), ): if value: keys_dict[attr] = value @@ -118,6 +120,8 @@ def _make_conn_str(keys): self.host = host self.port = port self.db = db + self.charset = kw.pop("charset", None) + self.tds_version = kw.pop("tds_version", None) self._server_version = None self._can_use_max_types = None self._can_use_microseconds = None diff --git a/sqlobject/mysql/mysqlconnection.py b/sqlobject/mysql/mysqlconnection.py index 2d7afa58..04490aa7 100644 --- a/sqlobject/mysql/mysqlconnection.py +++ b/sqlobject/mysql/mysqlconnection.py @@ -1,17 +1,21 @@ -import os - from sqlobject import col, dberrors from sqlobject.compat import PY2 +from sqlobject.converters import registerConverter, StringLikeConverter from sqlobject.dbconnection import DBAPI class ErrorMessage(str): def __new__(cls, e, append_msg=''): - obj = str.__new__(cls, e.args[1] + append_msg) + if len(e.args) > 1: + errmsg = e.args[1] + else: + errmsg = '' try: - obj.code = int(e.args[0]) + errcode = int(e.args[0]) except ValueError: - obj.code = e.args[0] + errcode = e.args[0] + obj = str.__new__(cls, errmsg + append_msg) + obj.code = errcode obj.module = e.__module__ obj.exception = e.__class__.__name__ return obj @@ -28,25 +32,26 @@ class MySQLConnection(DBAPI): odbc_keywords = ('Server', 'Port', 'UID', 'Password', 'Database') def __init__(self, db, user, password='', host='localhost', port=0, **kw): - drivers = kw.pop('driver', None) or 'mysqldb' + drivers = kw.pop('driver', None) or 'mysqldb,mysqlclient,' + \ + 'mysql-connector,mysql-connector-python,pymysql' for driver in drivers.split(','): - driver = driver.strip() + driver = driver.strip().lower() if not driver: continue try: - if driver.lower() in ('mysqldb', 'pymysql'): - if driver.lower() == 'pymysql': + if driver in ('mysqldb', 'pymysql'): + if driver == 'pymysql': import pymysql pymysql.install_as_MySQLdb() import MySQLdb - if driver.lower() == 'mysqldb': + if driver == 'mysqldb': if MySQLdb.version_info[:3] < (1, 2, 2): raise ValueError( 'SQLObject requires MySQLdb 1.2.2 or later') import MySQLdb.constants.CR import MySQLdb.constants.ER self.module = MySQLdb - if driver.lower() == 'mysqldb': + if driver == 'mysqldb': self.CR_SERVER_GONE_ERROR = \ MySQLdb.constants.CR.SERVER_GONE_ERROR self.CR_SERVER_LOST = \ @@ -57,7 +62,7 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): self.CR_SERVER_LOST = \ MySQLdb.constants.CR.CR_SERVER_LOST self.ER_DUP_ENTRY = MySQLdb.constants.ER.DUP_ENTRY - elif driver == 'connector': + elif driver in ('connector', 'connector-python'): import mysql.connector self.module = mysql.connector self.CR_SERVER_GONE_ERROR = \ @@ -65,13 +70,13 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): self.CR_SERVER_LOST = \ mysql.connector.errorcode.CR_SERVER_LOST self.ER_DUP_ENTRY = mysql.connector.errorcode.ER_DUP_ENTRY - elif driver == 'oursql': - import oursql - self.module = oursql - self.CR_SERVER_GONE_ERROR = \ - oursql.errnos['CR_SERVER_GONE_ERROR'] - self.CR_SERVER_LOST = oursql.errnos['CR_SERVER_LOST'] - self.ER_DUP_ENTRY = oursql.errnos['ER_DUP_ENTRY'] + if driver == 'connector-python': + self.connector_type = 'mysql.connector-python' + else: + self.connector_type = 'mysql.connector' + elif driver == 'mariadb': + import mariadb + self.module = mariadb elif driver == 'pyodbc': import pyodbc self.module = pyodbc @@ -87,8 +92,8 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): else: raise ValueError( 'Unknown MySQL driver "%s", ' - 'expected mysqldb, connector, ' - 'oursql, pymysql, ' + 'expected mysqldb, connector, connector-python, ' + 'pymysql, mariadb, ' 'odbc, pyodbc or pypyodbc' % driver) except ImportError: pass @@ -97,12 +102,14 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): else: raise ImportError( 'Cannot find a MySQL driver, tried %s' % drivers) + self.host = host self.port = port or 3306 self.db = db self.user = user self.password = password self.kw = {} + for key in ("unix_socket", "init_command", "read_default_file", "read_default_group", "conv"): if key in kw: @@ -111,7 +118,7 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): "client_flag", "local_infile"): if key in kw: self.kw[key] = int(kw.pop(key)) - if driver == 'connector': + if driver in ('connector', 'connector-python'): for key in ("ssl_key", "ssl_cert", "ssl_ca", "ssl_capath"): if key in kw: self.kw[key] = kw.pop(key) @@ -127,20 +134,22 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): self.dbEncoding = None self.driver = driver + if driver in ('mariadb', 'odbc', 'pyodbc', 'pypyodbc'): + self.CR_SERVER_GONE_ERROR = 2006 + self.CR_SERVER_LOST = 2013 + self.ER_DUP_ENTRY = '23000' + if driver in ('odbc', 'pyodbc', 'pypyodbc'): self.make_odbc_conn_str(kw.pop('odbcdrv', 'MySQL ODBC 5.3 ANSI Driver'), db, host, port, user, password ) - self.CR_SERVER_GONE_ERROR = 2006 - self.CR_SERVER_LOST = 2013 - self.ER_DUP_ENTRY = '23000' - elif self.driver == 'oursql': - if "use_unicode" not in self.kw: - self.kw["use_unicode"] = not PY2 - # oursql doesn't implement ping(True) yet - self.kw["autoreconnect"] = True + elif driver == 'mariadb': + self.kw.pop("charset", None) + + elif driver in ('connector', 'connector-python'): + registerConverter(bytes, ConnectorBytesConverter) global mysql_Bin if not PY2 and mysql_Bin is None: @@ -150,6 +159,8 @@ def __init__(self, db, user, password='', host='localhost', port=0, **kw): self._server_version = None self._can_use_microseconds = None + self._can_use_json_funcs = None + DBAPI.__init__(self, **kw) @classmethod @@ -161,14 +172,14 @@ def _connectionFromParams(cls, user, password, host, port, path, args): def makeConnection(self): dbEncoding = self.dbEncoding if dbEncoding: - if self.driver.lower() in ('mysqldb', 'pymysql'): + if self.driver in ('mysqldb', 'pymysql'): from MySQLdb.connections import Connection if not hasattr(Connection, 'set_character_set'): # monkeypatch pre MySQLdb 1.2.1 def character_set_name(self): return dbEncoding + '_' + dbEncoding Connection.character_set_name = character_set_name - if self.driver == 'connector': + if self.driver in ('connector', 'connector-python'): self.kw['consume_results'] = True try: if self.driver in ('odbc', 'pyodbc', 'pypyodbc'): @@ -179,7 +190,13 @@ def character_set_name(self): conn = self.module.connect( host=self.host, port=self.port, db=self.db, user=self.user, passwd=self.password, **self.kw) - if self.driver != 'oursql': + if self.driver == 'mariadb': + # Attempt to reconnect. + # This setting is persistent due to ``auto_reconnect``. + # mariadb doesn't implement ping(True) + conn.auto_reconnect = True + conn.ping() + else: # Attempt to reconnect. This setting is persistent. conn.ping(True) except self.module.OperationalError as e: @@ -201,8 +218,6 @@ def character_set_name(self): conn.setencoding(encoding=dbEncoding) elif hasattr(conn, 'set_character_set'): conn.set_character_set(dbEncoding) - elif self.driver == 'oursql': - conn.charset = dbEncoding elif hasattr(conn, 'query'): # works along with monkeypatching code above conn.query("SET NAMES %s" % dbEncoding) @@ -214,11 +229,11 @@ def _setAutoCommit(self, conn, auto): try: conn.autocommit(auto) except TypeError: - # mysql-connector has autocommit as a property + # mysql-connector{-python} has autocommit as a property conn.autocommit = auto def _force_reconnect(self, conn): - if self.driver.lower() == 'pymysql': + if self.driver in ('pymysql',): conn.ping(True) self._setAutoCommit(conn, bool(self.autoCommit)) if self.dbEncoding: @@ -229,7 +244,8 @@ def _executeRetry(self, conn, cursor, query): self.printDebug(conn, query, 'QueryR') dbEncoding = self.dbEncoding if dbEncoding and not isinstance(query, bytes) and ( - self.driver in ('mysqldb', 'connector', 'oursql')): + self.driver in ('mysqldb', 'connector', 'connector-python', + 'mariadb')): query = query.encode(dbEncoding, 'surrogateescape') # When a server connection is lost and a query is attempted, most of # the time the query will raise a SERVER_LOST exception, then at the @@ -243,7 +259,7 @@ def _executeRetry(self, conn, cursor, query): # reconnect flag must be set when making the connection to indicate # that autoreconnecting is desired. In MySQLdb 1.2.2 or newer this is # done by calling ping(True) on the connection. - # PyMySQL needs explicit reconnect + # [PC]yMySQL need explicit reconnect # each time we detect connection timeout. for count in range(3): try: @@ -255,7 +271,7 @@ def _executeRetry(self, conn, cursor, query): raise dberrors.OperationalError(ErrorMessage(e)) if self.debug: self.printDebug(conn, str(e), 'ERROR') - if self.driver.lower() == 'pymysql': + if self.driver in ('pymysql',): self._force_reconnect(conn) else: raise dberrors.OperationalError(ErrorMessage(e)) @@ -263,6 +279,9 @@ def _executeRetry(self, conn, cursor, query): msg = ErrorMessage(e) if e.args[0] == self.ER_DUP_ENTRY: raise dberrors.DuplicateEntryError(msg) + elif isinstance(e.args[0], str) \ + and e.args[0].startswith('Duplicate'): + raise dberrors.DuplicateEntryError(msg) else: raise dberrors.IntegrityError(msg) except self.module.InternalError as e: @@ -328,9 +347,21 @@ def createIndexSQL(self, soClass, index): return index.mysqlCreateIndexSQL(soClass) def createIDColumn(self, soClass): - if soClass.sqlmeta.idType == str: + if soClass.sqlmeta.idType is str: return '%s TEXT PRIMARY KEY' % soClass.sqlmeta.idName - return '%s INT PRIMARY KEY AUTO_INCREMENT' % soClass.sqlmeta.idName + if soClass.sqlmeta.idType is not int: + raise TypeError('sqlmeta.idType must be int or str, not %r' + % soClass.sqlmeta.idType) + if soClass.sqlmeta.idSize is None: + mysql_int_type = 'INT' + elif soClass.sqlmeta.idSize in ('TINY', 'SMALL', 'MEDIUM', 'BIG'): + mysql_int_type = '%sINT' % soClass.sqlmeta.idSize + else: + raise ValueError( + "sqlmeta.idSize must be 'TINY', 'SMALL', 'MEDIUM', 'BIG' " + "or None, not %r" % soClass.sqlmeta.idSize) + return '%s %s PRIMARY KEY AUTO_INCREMENT' \ + % (soClass.sqlmeta.idName, mysql_int_type) def joinSQLType(self, join): return 'INT NOT NULL' @@ -345,6 +376,8 @@ def tableExists(self, tableName): except dberrors.ProgrammingError as e: if e.args[0].code in (1146, '42S02'): # ER_NO_SUCH_TABLE return False + if self.driver == 'mariadb': + return False raise def addColumn(self, tableName, column): @@ -375,6 +408,9 @@ def columnsFromSchema(self, tableName, soClass): # (SQLObject expected '') kw['notNone'] = (nullAllowed.upper() != 'YES' and True or False) + if not PY2 and isinstance(t, bytes): + t = t.decode('ascii') + if default and t.startswith('int'): kw['default'] = int(default) elif default and t.startswith('float'): @@ -391,6 +427,8 @@ def columnsFromSchema(self, tableName, soClass): return results def guessClass(self, t): + if not PY2 and isinstance(t, bytes): + t = t.decode('ascii') if t.startswith('int'): return col.IntCol, {} elif t.startswith('enum'): @@ -447,10 +485,10 @@ def guessClass(self, t): return col.Col, {} def listTables(self): - return [v[0] for v in self.queryAll("SHOW TABLES")] + return _decodeBytearrays(self.queryAll("SHOW TABLES")) def listDatabases(self): - return [v[0] for v in self.queryAll("SHOW DATABASES")] + return _decodeBytearrays(self.queryAll("SHOW DATABASES")) def _createOrDropDatabase(self, op="CREATE"): self.query('%s DATABASE %s' % (op, self.db)) @@ -482,9 +520,6 @@ def server_version(self): def can_use_microseconds(self): if self._can_use_microseconds is not None: return self._can_use_microseconds - if os.environ.get('APPVEYOR') or os.environ.get('TRAVIS'): - self._can_use_microseconds = False - return False server_version = self.server_version() if server_version is None: return None @@ -495,3 +530,33 @@ def can_use_microseconds(self): can_use_microseconds = (server_version >= (5, 6, 4)) self._can_use_microseconds = can_use_microseconds return can_use_microseconds + + def can_use_json_funcs(self): + if self._can_use_json_funcs is not None: + return self._can_use_json_funcs + server_version = self.server_version() + if server_version is None: + return None + server_version, db_tag = server_version + if db_tag == "MariaDB": + can_use_json_funcs = (server_version >= (10, 2, 7)) + else: # MySQL + can_use_json_funcs = (server_version >= (5, 7, 0)) + self._can_use_json_funcs = can_use_json_funcs + return can_use_json_funcs + + +def ConnectorBytesConverter(value, db): + if not PY2: + # For PY2 this converter is called also for SQLite + assert db == 'mysql' + value = value.decode('latin1') + return StringLikeConverter(value, db) + + +def _decodeBytearrays(v_list): + if not v_list: + return [] + if not PY2 and isinstance(v_list[0][0], bytearray): + return [v[0].decode('ascii') for v in v_list] + return [v[0] for v in v_list] diff --git a/sqlobject/postgres/pgconnection.py b/sqlobject/postgres/pgconnection.py index 8dd1db58..3ce5ef99 100644 --- a/sqlobject/postgres/pgconnection.py +++ b/sqlobject/postgres/pgconnection.py @@ -10,18 +10,43 @@ class ErrorMessage(str): def __new__(cls, e, append_msg=''): - obj = str.__new__(cls, e.args[0] + append_msg) - if e.__module__ == 'psycopg2': - obj.code = getattr(e, 'pgcode', None) - obj.error = getattr(e, 'pgerror', None) + eargs0 = emessage = e.args[0] + if e.__module__.startswith('pg8000') \ + and isinstance(e.args, tuple) and len(e.args) > 1: + # pg8000 =~ 1.12 for Python 3.4 + ecode = e.args[2] + eerror = emessage = e.args[3] + elif e.__module__.startswith('pg8000') and isinstance(eargs0, dict): + # pg8000 =~ 1.13 for Python 2.7 + # pg8000 for Python 3.5+ + ecode = eargs0['C'] + eerror = emessage = eargs0['M'] + elif e.__module__ in ('psycopg.errors', 'pg'): # psycopg, PyGreSQL + ecode = e.sqlstate + eerror = emessage = e.args[0] + elif hasattr(e, 'pgcode'): # psycopg2 or psycopg2.errors + ecode = getattr(e, 'pgcode', None) + eerror = getattr(e, 'pgerror', None) else: - obj.code = getattr(e, 'code', None) - obj.error = getattr(e, 'error', None) + ecode = getattr(e, 'code', None) + eerror = getattr(e, 'error', None) + obj = str.__new__(cls, emessage + append_msg) + obj.code = ecode + obj.error = eerror obj.module = e.__module__ obj.exception = e.__class__.__name__ return obj +def _getuser(): + # ``getuser()`` on w32 can raise ``ImportError`` + # due to absent of ``pwd`` module. + try: + return getuser() + except ImportError: + return None + + class PostgresConnection(DBAPI): supportTransactions = True @@ -32,24 +57,28 @@ class PostgresConnection(DBAPI): def __init__(self, dsn=None, host=None, port=None, db=None, user=None, password=None, **kw): - drivers = kw.pop('driver', None) or 'psycopg' + drivers = kw.pop('driver', None) or 'psycopg,psycopg2,pygresql,pg8000' for driver in drivers.split(','): driver = driver.strip() if not driver: continue try: - if driver in ('psycopg', 'psycopg2'): - import psycopg2 as psycopg + if driver == 'psycopg': + import psycopg self.module = psycopg + elif driver == 'psycopg2': + import psycopg2 + self.module = psycopg2 elif driver == 'pygresql': import pgdb self.module = pgdb - elif driver in ('py-postgresql', 'pypostgresql'): - from postgresql.driver import dbapi20 - self.module = dbapi20 elif driver == 'pg8000': - import pg8000 - self.module = pg8000 + try: + import pg8000.dbapi + self.module = pg8000.dbapi + except ImportError: + import pg8000 + self.module = pg8000 elif driver == 'pyodbc': import pyodbc self.module = pyodbc @@ -66,7 +95,7 @@ def __init__(self, dsn=None, host=None, port=None, db=None, raise ValueError( 'Unknown PostgreSQL driver "%s", ' 'expected psycopg, psycopg2, ' - 'pygresql, pypostgresql, pg8000, ' + 'pygresql, pg8000, ' 'odbc, pyodbc or pypyodbc' % driver) except ImportError: pass @@ -76,11 +105,15 @@ def __init__(self, dsn=None, host=None, port=None, db=None, raise ImportError( 'Cannot find a PostgreSQL driver, tried %s' % drivers) - if driver.startswith('psycopg'): + if driver.startswith('psycopg2'): + # Register a converter for psycopg2 Binary type. + registerConverter(type(self.module.Binary('')), + Psyco2BinaryConverter) + elif driver.startswith('psycopg'): # Register a converter for psycopg Binary type. registerConverter(type(self.module.Binary('')), PsycoBinaryConverter) - elif driver in ('pygresql', 'py-postgresql', 'pypostgresql', 'pg8000'): + elif driver in ('pygresql', 'pg8000'): registerConverter(type(self.module.Binary(b'')), PostgresBinaryConverter) elif driver in ('odbc', 'pyodbc', 'pypyodbc'): @@ -106,12 +139,15 @@ def __init__(self, dsn=None, host=None, port=None, db=None, if driver == 'pygresql': dsn_dict["host"] = "%s:%d" % (host, port) elif driver.startswith('psycopg') and \ - psycopg.__version__.split('.')[0] == '1': + self.module.__version__.split('.')[0] == '1': dsn_dict["port"] = str(port) else: dsn_dict["port"] = port if db: - dsn_dict["database"] = db + if driver == 'psycopg': + dsn_dict["dbname"] = db + else: + dsn_dict["database"] = db if user: dsn_dict["user"] = user if password: @@ -149,19 +185,12 @@ def __init__(self, dsn=None, host=None, port=None, db=None, if sslmode: dsn.append('sslmode=%s' % sslmode) dsn = ' '.join(dsn) - if driver in ('py-postgresql', 'pypostgresql'): - if host and host.startswith('/'): - dsn_dict["host"] = dsn_dict["port"] = None - dsn_dict["unix"] = host - else: - if "unix" in dsn_dict: - del dsn_dict["unix"] if driver == 'pg8000': if host and host.startswith('/'): dsn_dict["host"] = None dsn_dict["unix_sock"] = host if user is None: - dsn_dict["user"] = getuser() + dsn_dict["user"] = _getuser() self.dsn = dsn self.driver = driver self.unicodeCols = kw.pop('unicodeCols', False) @@ -237,7 +266,8 @@ def _executeRetry(self, conn, cursor, query): raise dberrors.OperationalError(ErrorMessage(e)) except self.module.IntegrityError as e: msg = ErrorMessage(e) - if getattr(e, 'code', -1) == '23505' or \ + if getattr(msg, 'code', -1) == '23505' or \ + getattr(e, 'code', -1) == '23505' or \ getattr(e, 'pgcode', -1) == '23505' or \ getattr(e, 'sqlstate', -1) == '23505' or \ e.args[0] == '23505': @@ -276,11 +306,6 @@ def _queryInsertID(self, conn, soInstance, id, names, values): table = soInstance.sqlmeta.table idName = soInstance.sqlmeta.idName c = conn.cursor() - if id is None and self.driver in ('py-postgresql', 'pypostgresql'): - sequenceName = soInstance.sqlmeta.idSequence or \ - '%s_%s_seq' % (table, idName) - self._executeRetry(conn, c, "SELECT NEXTVAL('%s')" % sequenceName) - id = c.fetchone()[0] if id is not None: names = [idName] + names values = [id] + values @@ -318,7 +343,22 @@ def createIndexSQL(self, soClass, index): return index.postgresCreateIndexSQL(soClass) def createIDColumn(self, soClass): - key_type = {int: "SERIAL", str: "TEXT"}[soClass.sqlmeta.idType] + if soClass.sqlmeta.idType is int: + if soClass.sqlmeta.idSize in ('TINY', 'SMALL'): + key_type = 'SMALLSERIAL' + elif soClass.sqlmeta.idSize in ('MEDIUM', None): + key_type = 'SERIAL' + elif soClass.sqlmeta.idSize == 'BIG': + key_type = 'BIGSERIAL' + else: + raise ValueError( + "sqlmeta.idSize must be 'TINY', 'SMALL', 'MEDIUM', 'BIG' " + "or None, not %r" % soClass.sqlmeta.idSize) + elif soClass.sqlmeta.idType is str: + key_type = "TEXT" + else: + raise TypeError('sqlmeta.idType must be int or str, not %r' + % soClass.sqlmeta.idType) return '%s %s PRIMARY KEY' % (soClass.sqlmeta.idName, key_type) def dropTable(self, tableName, cascade=False): @@ -353,7 +393,8 @@ def columnsFromSchema(self, tableName, soClass): colQuery = """ SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), a.attnotnull, - (SELECT substring(d.adsrc for 128) FROM pg_catalog.pg_attrdef d + (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128) + FROM pg_catalog.pg_attrdef d WHERE d.adrelid=a.attrelid AND d.adnum = a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid =%s::regclass @@ -540,8 +581,8 @@ def dropDatabase(self): self._createOrDropDatabase(op="DROP") -# Converter for Binary types -def PsycoBinaryConverter(value, db): +# Converters for Binary types +def Psyco2BinaryConverter(value, db): assert db == 'postgres' return str(value) @@ -560,6 +601,11 @@ def escape_bytea(value): ) +def PsycoBinaryConverter(value, db): + assert db == 'postgres' + return sqlrepr(escape_bytea(value.obj), db) + + def PostgresBinaryConverter(value, db): assert db == 'postgres' return sqlrepr(escape_bytea(value), db) diff --git a/sqlobject/rdbhost/__init__.py b/sqlobject/rdbhost/__init__.py deleted file mode 100644 index 588c6a84..00000000 --- a/sqlobject/rdbhost/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from sqlobject.dbconnection import registerConnection - - -def builder(): - from . import rdbhostconnection - return rdbhostconnection.RdbhostConnection - -registerConnection(['rdbhost'], builder) diff --git a/sqlobject/rdbhost/rdbhostconnection.py b/sqlobject/rdbhost/rdbhostconnection.py deleted file mode 100644 index 25200e90..00000000 --- a/sqlobject/rdbhost/rdbhostconnection.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -This module written by David Keeney, 2009, 2010 - -Released under the LGPL for use with the SQLObject ORM library. -""" - -from sqlobject.dbconnection import DBAPI -from sqlobject.postgres.pgconnection import PostgresConnection - - -class RdbhostConnection(PostgresConnection): - - supportTransactions = False - dbName = 'rdbhost' - schemes = [dbName] - - def __init__(self, dsn=None, host=None, port=None, db=None, - user=None, password=None, unicodeCols=False, **kw): - from rdbhdb import rdbhdb as rdb - # monkey patch % escaping into Cursor._execute - old_execute = getattr(rdb.Cursor, '_execute') - setattr(rdb.Cursor, '_old_execute', old_execute) - - def _execute(self, query, *args): - assert not any([a for a in args]) - query = query.replace('%', '%%') - self._old_execute(query, (), (), ()) - setattr(rdb.Cursor, '_execute', _execute) - - self.module = rdb - self.user = user - self.host = host - self.port = port - self.db = db - self.password = password - self.dsn_dict = dsn_dict = {} - self.use_dsn = dsn is not None - if host: - dsn_dict["host"] = host - if user: - dsn_dict["role"] = user - if password: - dsn_dict["authcode"] = password - if dsn is None: - dsn = [] - if db: - dsn.append('dbname=%s' % db) - if user: - dsn.append('user=%s' % user) - if password: - dsn.append('password=%s' % password) - if host: - dsn.append('host=%s' % host) - if port: - dsn.append('port=%d' % port) - dsn = ' '.join(dsn) - self.dsn = dsn - self.unicodeCols = unicodeCols - self.schema = kw.pop('schema', None) - self.dbEncoding = 'utf-8' - DBAPI.__init__(self, **kw) diff --git a/sqlobject/sqlbuilder.py b/sqlobject/sqlbuilder.py index 5178c73d..f998c0a9 100644 --- a/sqlobject/sqlbuilder.py +++ b/sqlobject/sqlbuilder.py @@ -300,6 +300,8 @@ def tablesUsedSet(obj, db): class SQLOp(SQLExpression): def __init__(self, op, expr1, expr2): self.op = op.upper() + if isinstance(expr1, Subquery): + expr1, expr2 = expr2, expr1 self.expr1 = expr1 self.expr2 = expr2 @@ -308,7 +310,8 @@ def __sqlrepr__(self, db): s2 = sqlrepr(self.expr2, db) if s1[0] != '(' and s1 != 'NULL': s1 = '(' + s1 + ')' - if s2[0] != '(' and s2 != 'NULL': + if s2[0] != '(' and s2 != 'NULL' and \ + not isinstance(self.expr2, Subquery): s2 = '(' + s2 + ')' return "(%s %s %s)" % (s1, self.op, s2) @@ -477,7 +480,8 @@ def __init__(self, tableName): def __getattr__(self, attr): if attr.startswith('__'): - raise AttributeError + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) return self.FieldClass(self.tableName, attr) def __sqlrepr__(self, db): @@ -499,7 +503,8 @@ def __init__(self, soClass): def __getattr__(self, attr): if attr.startswith('__'): - raise AttributeError + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) if attr == 'id': return self._getattrFromID(attr) elif attr in self.soClass.sqlmeta.columns: @@ -562,14 +567,16 @@ class TableSpace: def __getattr__(self, attr): if attr.startswith('__'): - raise AttributeError + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) return self.TableClass(attr) class ConstantSpace: def __getattr__(self, attr): if attr.startswith('__'): - raise AttributeError + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) return SQLConstant(attr) @@ -625,7 +632,8 @@ def __init__(self, table, alias=None): def __getattr__(self, attr): if attr.startswith('__'): - raise AttributeError + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) if self.table: attr = getattr(self.table.q, attr).fieldName return self.FieldClass(self.tableName, attr, self.alias, self) @@ -635,16 +643,55 @@ def __sqlrepr__(self, db): self.alias) +class AliasSQLMeta(): + def __init__(self, table, alias): + self.__table = table + self.__alias = alias + + def __getattr__(self, attr): + if attr.startswith('__'): + raise AttributeError("Attribute '%s' is forbidden in '%s'" % ( + attr, self.__class__.__name__)) + table = self.__table + if (attr == "table"): + return '%s %s' % (table.sqlmeta.table, self.__alias) + return getattr(table.sqlmeta, attr) + + class Alias(SQLExpression): def __init__(self, table, alias=None): self.q = AliasTable(table, alias) + def __getattr__(self, attr): + table = self.q.table + if (attr == "sqlmeta") and hasattr(table, "sqlmeta"): + alias = self.q.alias + return AliasSQLMeta(table, alias) + return getattr(table, attr) + def __sqlrepr__(self, db): return sqlrepr(self.q, db) def components(self): return [self.q] + def select(self, clause=None, clauseTables=None, + orderBy=NoDefault, limit=None, + lazyColumns=False, reversed=False, + distinct=False, connection=None, + join=None, forUpdate=False): + # Import here to avoid circular import + from .sresults import SelectResults + return SelectResults(self, clause, + clauseTables=clauseTables, + orderBy=orderBy, + limit=limit, + lazyColumns=lazyColumns, + reversed=reversed, + distinct=distinct, + connection=connection, + join=join, forUpdate=forUpdate) + class Union(SQLExpression): def __init__(self, *tables): @@ -1092,7 +1139,7 @@ def __sqlrepr__(self, db): def _quote_like_special(s, db): - if db in ('postgres', 'rdbhost'): + if db == 'postgres': escape = r'\\' else: escape = '\\' @@ -1433,7 +1480,6 @@ class RLIKE(LIKE): 'maxdb': 'RLIKE', 'mysql': 'RLIKE', 'postgres': '~', - 'rdbhost': '~', 'sqlite': 'REGEXP' } @@ -1507,8 +1553,9 @@ def tablesUsedImmediate(self): class ImportProxy(SQLExpression): - '''Class to be used in column definitions that rely on other tables that might - not yet be in a classregistry. + ''' + Class to be used in column definitions that rely on other tables that might + not yet be in a classregistry ''' FieldClass = ImportProxyField diff --git a/sqlobject/sqlite/sqliteconnection.py b/sqlobject/sqlite/sqliteconnection.py index 024b01c6..9d491d94 100644 --- a/sqlobject/sqlite/sqliteconnection.py +++ b/sqlobject/sqlite/sqliteconnection.py @@ -4,6 +4,7 @@ from _thread import get_ident except ImportError: from thread import get_ident +from threading import enumerate as enumerate_threads try: from urllib import quote except ImportError: @@ -32,65 +33,27 @@ class SQLiteConnection(DBAPI): schemes = [dbName] def __init__(self, filename, autoCommit=1, **kw): - drivers = kw.pop('driver', None) or 'pysqlite2,sqlite3,sqlite' - for driver in drivers.split(','): - driver = driver.strip() - if not driver: - continue - try: - if driver in ('sqlite2', 'pysqlite2'): - from pysqlite2 import dbapi2 as sqlite - self.using_sqlite2 = True - elif driver == 'sqlite3': - import sqlite3 as sqlite - self.using_sqlite2 = True - elif driver in ('sqlite', 'sqlite1'): - import sqlite - self.using_sqlite2 = False - else: - raise ValueError( - 'Unknown SQLite driver "%s", ' - 'expected pysqlite2, sqlite3 or sqlite' % driver) - except ImportError: - pass - else: - break - else: - raise ImportError( - 'Cannot find an SQLite driver, tried %s' % drivers) - if self.using_sqlite2: - sqlite.encode = base64.b64encode - sqlite.decode = base64.b64decode + import sqlite3 as sqlite + sqlite.encode = base64.b64encode + sqlite.decode = base64.b64decode self.module = sqlite self.filename = filename # full path to sqlite-db-file self._memory = filename == ':memory:' - if self._memory and not self.using_sqlite2: - raise ValueError("You must use sqlite2 to use in-memory databases") # connection options opts = {} - if self.using_sqlite2: - if autoCommit: - opts["isolation_level"] = None - global sqlite2_Binary - if sqlite2_Binary is None: - sqlite2_Binary = sqlite.Binary - sqlite.Binary = lambda s: sqlite2_Binary(sqlite.encode(s)) - if 'factory' in kw: - factory = kw.pop('factory') - if isinstance(factory, str): - factory = globals()[factory] - opts['factory'] = factory(sqlite) - else: - opts['autocommit'] = Boolean(autoCommit) - if 'encoding' in kw: - opts['encoding'] = kw.pop('encoding') - if 'mode' in kw: - opts['mode'] = int(kw.pop('mode'), 0) + if autoCommit: + opts["isolation_level"] = None + global sqlite2_Binary + if sqlite2_Binary is None: + sqlite2_Binary = sqlite.Binary + sqlite.Binary = lambda s: sqlite2_Binary(sqlite.encode(s)) + if 'factory' in kw: + factory = kw.pop('factory') + if isinstance(factory, str): + factory = globals()[factory] + opts['factory'] = factory(sqlite) if 'timeout' in kw: - if self.using_sqlite2: - opts['timeout'] = float(kw.pop('timeout')) - else: - opts['timeout'] = int(float(kw.pop('timeout')) * 1000) + opts['timeout'] = float(kw.pop('timeout')) if 'check_same_thread' in kw: opts["check_same_thread"] = Boolean(kw.pop('check_same_thread')) # use only one connection for sqlite - supports multiple) @@ -145,6 +108,7 @@ def getConnection(self): self._connectionNumbers[id(conn)] = self._connectionCount self._connectionCount += 1 return conn + self._releaseUnusedConnections() threadid = get_ident() if (self._pool is not None and threadid in self._threadPool): conn = self._threadPool[threadid] @@ -176,20 +140,18 @@ def releaseConnection(self, conn, explicit=False): else: if self._pool and conn in self._pool: self._pool.remove(conn) + if threadid: + del self._threadOrigination[id(conn)] + del self._threadPool[threadid] conn.close() def _setAutoCommit(self, conn, auto): - if self.using_sqlite2: - if auto: - conn.isolation_level = None - else: - conn.isolation_level = "" + if auto: + conn.isolation_level = None else: - conn.autocommit = auto + conn.isolation_level = "" def _setIsolationLevel(self, conn, level): - if not self.using_sqlite2: - return conn.isolation_level = level def makeMemoryConnection(self): @@ -206,9 +168,23 @@ def makeConnection(self): conn.text_factory = str # Convert text data to str, not unicode return conn + def _releaseUnusedConnections(self): + """Release connections from threads that're no longer active""" + thread_ids = set(t.ident for t in enumerate_threads()) + tp_set = set(self._threadPool) + unused_connections = [ + self._threadPool[tid] for tid in tp_set - thread_ids + ] + for unused_connection in unused_connections: + try: + self.releaseConnection(unused_connection, explicit=True) + except self.module.ProgrammingError: + pass # Ignore error in `conn.close()` from a different thread + def close(self): DBAPI.close(self) self._threadPool = {} + self._threadOrigination = {} if self._memory: self._memoryConn.close() self.makeMemoryConnection() @@ -290,8 +266,11 @@ def createIDColumn(self, soClass): return self._createIDColumn(soClass.sqlmeta) def _createIDColumn(self, sqlmeta): - if sqlmeta.idType == str: + if sqlmeta.idType is str: return '%s TEXT PRIMARY KEY' % sqlmeta.idName + if sqlmeta.idType is not int: + raise TypeError('sqlmeta.idType must be int or str, not %r' + % sqlmeta.idType) return '%s INTEGER PRIMARY KEY AUTOINCREMENT' % sqlmeta.idName def joinSQLType(self, join): diff --git a/sqlobject/tests/dbtest.py b/sqlobject/tests/dbtest.py index b1cf6e4b..65fcb273 100644 --- a/sqlobject/tests/dbtest.py +++ b/sqlobject/tests/dbtest.py @@ -33,7 +33,7 @@ def test_featureX(): pytest.skip("Doesn't support featureX") """ supportsMatrix = { - '-blobData': 'mssql rdbhost', + '-blobData': 'mssql', '-decimalColumn': 'mssql', '-dropTableCascade': 'sybase mssql mysql', '-emptyTable': 'mssql', @@ -43,7 +43,7 @@ def test_featureX(): '+memorydb': 'sqlite', '+rlike': 'mysql postgres sqlite', '+schema': 'postgres', - '-transactions': 'mysql rdbhost', + '-transactions': ' ', } @@ -63,7 +63,7 @@ def setupClass(soClasses, force=False): If force is true, then the database will be recreated no matter what. """ - global hub + # global hub if not isinstance(soClasses, (list, tuple)): soClasses = [soClasses] connection = getConnection() @@ -101,6 +101,9 @@ def getConnection(**kw): conn.debug = True if conftest.option.show_sql_output: conn.debugOutput = True + if (conn.dbName == 'postgres') and (conn.driver == 'pg8000') \ + and conn._pool is not None: + conn._pool = None if (conn.dbName == 'sqlite') and not conn._memory: speedupSQLiteConnection(conn) return conn @@ -120,11 +123,7 @@ def getConnectionURI(): if 'sphinx' not in sys.modules: print("Could not open database: %s" % e, file=sys.stderr) else: - if (connection.dbName == 'firebird') \ - or ( - (connection.dbName == 'mysql') - and ((os.environ.get('APPVEYOR')) or (os.environ.get('TRAVIS'))) - ): + if (connection.dbName == 'firebird'): use_microseconds(False) diff --git a/sqlobject/tests/test_ForeignKey_cascade.py b/sqlobject/tests/test_ForeignKey_cascade.py new file mode 100644 index 00000000..e8942b58 --- /dev/null +++ b/sqlobject/tests/test_ForeignKey_cascade.py @@ -0,0 +1,150 @@ +from sqlobject import ForeignKey, SQLObject, StringCol, \ + SQLObjectIntegrityError, SQLObjectNotFound +from sqlobject.tests.dbtest import raises, setupClass + + +class SOTestPerson1(SQLObject): + name = StringCol() + + +class SOTestMessageCascadeTrue(SQLObject): + sender = ForeignKey('SOTestPerson1', cascade=True) + recipient = ForeignKey('SOTestPerson1', cascade=True) + body = StringCol() + + +def test1(): + setupClass([SOTestPerson1, SOTestMessageCascadeTrue]) + + john = SOTestPerson1(name='john') + emily = SOTestPerson1(name='emily') + message = SOTestMessageCascadeTrue( + sender=emily, recipient=john, body='test1' + ) + + SOTestPerson1.delete(emily.id) + john.expire() + message.expire() + + john.sync() + raises(SQLObjectNotFound, emily.sync) + raises(SQLObjectNotFound, message.sync) + + +class SOTestPerson2(SQLObject): + name = StringCol() + + +class SOTestMessageCascadeFalse(SQLObject): + sender = ForeignKey('SOTestPerson2', cascade=False) + recipient = ForeignKey('SOTestPerson2', cascade=False) + body = StringCol() + + +def test2(): + setupClass([SOTestPerson2, SOTestMessageCascadeFalse]) + + john = SOTestPerson2(name='john') + emily = SOTestPerson2(name='emily') + message = SOTestMessageCascadeFalse( + sender=emily, recipient=john, body='test2' + ) + + raises(SQLObjectIntegrityError, SOTestPerson2.delete, emily.id) + john.expire() + emily.expire() + message.expire() + + john.sync() + emily.sync() + message.sync() + + assert message.sender == emily + assert message.recipient == john + + +class SOTestPerson3(SQLObject): + name = StringCol() + + +class SOTestMessageCascadeNull(SQLObject): + sender = ForeignKey('SOTestPerson3', cascade='null') + recipient = ForeignKey('SOTestPerson3', cascade='null') + body = StringCol() + + +def test3(): + setupClass([SOTestPerson3, SOTestMessageCascadeNull]) + + john = SOTestPerson3(name='john') + emily = SOTestPerson3(name='emily') + message = SOTestMessageCascadeNull( + sender=emily, recipient=john, body='test3' + ) + + SOTestPerson3.delete(emily.id) + john.expire() + message.expire() + + john.sync() + message.sync() + raises(SQLObjectNotFound, emily.sync) + + assert message.sender is None + assert message.recipient == john + + SOTestPerson3.delete(john.id) + john.expire() + message.expire() + + message.sync() + raises(SQLObjectNotFound, john.sync) + + assert message.recipient is None + + +class SOTestPerson4(SQLObject): + name = StringCol() + + +class SOTestMessageCascadeMixed(SQLObject): + sender = ForeignKey('SOTestPerson4', cascade=True) + recipient = ForeignKey('SOTestPerson4', cascade='null') + body = StringCol() + + +def test4(): + setupClass([SOTestPerson4, SOTestMessageCascadeMixed]) + + john = SOTestPerson4(name='john') + emily = SOTestPerson4(name='emily') + message = SOTestMessageCascadeMixed( + sender=emily, recipient=john, body='test4' + ) + + SOTestPerson4.delete(emily.id) + john.expire() + message.expire() + + john.sync() + raises(SQLObjectNotFound, message.sync) + + +def test5(): + setupClass([SOTestPerson4, SOTestMessageCascadeMixed]) + + john = SOTestPerson4(name='john') + emily = SOTestPerson4(name='emily') + message = SOTestMessageCascadeMixed( + sender=emily, recipient=john, body='test5' + ) + + john.destroySelf() + emily.expire() + message.expire() + + emily.sync() + message.sync() + + assert message.recipient is None + assert message.sender == emily diff --git a/sqlobject/tests/test_SQLRelatedJoin.py b/sqlobject/tests/test_SQLRelatedJoin.py index 7620258f..babd9f6b 100644 --- a/sqlobject/tests/test_SQLRelatedJoin.py +++ b/sqlobject/tests/test_SQLRelatedJoin.py @@ -1,5 +1,7 @@ import pytest -from sqlobject import RelatedJoin, SQLObject, SQLRelatedJoin, StringCol +from sqlobject import RelatedJoin, SQLObject, SQLRelatedJoin, StringCol, \ + ForeignKey +from sqlobject.sqlbuilder import Alias from sqlobject.tests.dbtest import setupClass, supports @@ -23,7 +25,7 @@ def createAllTables(): setupClass(Tourtment) -def test_1(): +def createData(): createAllTables() # create some tourtments t1 = Tourtment(name='Tourtment #1') @@ -43,7 +45,11 @@ def test_1(): t2.addFighter(trunks) t3.addFighter(gohan) t3.addFighter(trunks) - # do some selects + return t1, t2, t3, gokou, vegeta, gohan, trunks + + +def test_1(): + t1, t2, t3, gokou, vegeta, gohan, trunks = createData() for i, j in zip(t1.fightersAsList, t1.fightersAsSResult): assert i is j assert len(t2.fightersAsList) == t2.fightersAsSResult.count() @@ -62,3 +68,47 @@ def test_related_join_transaction(): finally: trans.commit(True) Tourtment._connection.autoCommit = True + + +def test_related_join_filter(): + t1, t2, t3, gokou, vegeta, gohan, trunks = createData() + filteredFighters = t1.fightersAsSResult.filter( + Fighter.q.name.startswith('go') + ).orderBy('id') + for i, j in zip(filteredFighters, [gokou, gohan]): + assert i is j + + +class RecursiveGroup(SQLObject): + name = StringCol(length=255, unique=True) + subgroups = SQLRelatedJoin( + 'RecursiveGroup', + otherColumn='group_id', + intermediateTable='rec_group_map', + createRelatedTable=False, + ) + + +class RecGroupMap(SQLObject): + recursive_group = ForeignKey('RecursiveGroup') + group = ForeignKey('RecursiveGroup') + + +def test_rec_group(): + setupClass([RecursiveGroup, RecGroupMap]) + a = RecursiveGroup(name='a') + a1 = RecursiveGroup(name='a1') + a.addRecursiveGroup(a1) + a2 = RecursiveGroup(name='a2') + a.addRecursiveGroup(a2) + + assert sorted(a.subgroups, key=lambda x: x.name) == [a1, a2] + + pytest.raises( + ValueError, + a.subgroups.filter, + RecursiveGroup.q.name == 'a1', + ) + + rgroupAlias = Alias(RecursiveGroup, '_SO_SQLRelatedJoin_OtherTable') + assert list(a.subgroups.filter(rgroupAlias.q.name == 'a1')) == [a1] diff --git a/sqlobject/tests/test_auto.py b/sqlobject/tests/test_auto.py index 1b66821e..ee4967ab 100644 --- a/sqlobject/tests/test_auto.py +++ b/sqlobject/tests/test_auto.py @@ -117,19 +117,6 @@ class TestAuto: ) """ - rdbhostCreate = """ - CREATE TABLE auto_test ( - auto_id SERIAL PRIMARY KEY, - first_name VARCHAR(100), - last_name VARCHAR(200) NOT NULL, - age INT DEFAULT 0, - created VARCHAR(40) NOT NULL, - happy char(1) DEFAULT 'Y' NOT NULL, - long_field TEXT, - wannahavefun BOOL DEFAULT FALSE NOT NULL - ) - """ - sqliteCreate = """ CREATE TABLE auto_test ( auto_id INTEGER PRIMARY KEY AUTOINCREMENT , @@ -177,7 +164,7 @@ class TestAuto: DROP TABLE auto_test """ - sqliteDrop = sybaseDrop = mssqlDrop = rdbhostDrop = postgresDrop + sqliteDrop = sybaseDrop = mssqlDrop = postgresDrop def setup_method(self, meth): conn = getConnection() diff --git a/sqlobject/tests/test_basic.py b/sqlobject/tests/test_basic.py index d9d1a2e4..361bf935 100644 --- a/sqlobject/tests/test_basic.py +++ b/sqlobject/tests/test_basic.py @@ -337,3 +337,23 @@ class SOTestSO13(SQLObject): assert SOTestSO13._connection.uri() == 'sqlite:///db2' del sqlhub.processConnection + + +def _test_wrong_sqlmeta_idType(): + class SOTestSO13(SQLObject): + class sqlmeta: + idType = dict + + +def test_wrong_sqlmeta_idType(): + pytest.raises(TypeError, _test_wrong_sqlmeta_idType) + + +def _test_wrong_sqlmeta_idSize(): + class SOTestSO14(SQLObject): + class sqlmeta: + idSize = 'DEFAULT' + + +def test_wrong_sqlmeta_idSize(): + pytest.raises(ValueError, _test_wrong_sqlmeta_idSize) diff --git a/sqlobject/tests/test_boundattributes.py b/sqlobject/tests/test_boundattributes.py index 5ae20152..1453658d 100644 --- a/sqlobject/tests/test_boundattributes.py +++ b/sqlobject/tests/test_boundattributes.py @@ -3,7 +3,9 @@ from sqlobject import boundattributes from sqlobject import declarative -pytestmark = pytest.mark.skipif('True') +pytestmark = pytest.mark.skipif( + True, + reason='The module "boundattributes" and its tests were not finished yet') class SOTestMe(object): diff --git a/sqlobject/tests/test_converters.py b/sqlobject/tests/test_converters.py index 0468a75c..a064c69f 100644 --- a/sqlobject/tests/test_converters.py +++ b/sqlobject/tests/test_converters.py @@ -262,8 +262,8 @@ def test_timedelta(): def test_quote_unquote_str(): assert quote_str('test%', 'postgres') == "'test%'" assert quote_str('test%', 'sqlite') == "'test%'" - assert quote_str('test\%', 'postgres') == "E'test\\%'" - assert quote_str('test\\%', 'sqlite') == "'test\%'" + assert quote_str('test\\%', 'postgres') == "E'test\\%'" + assert quote_str('test\\%', 'sqlite') == "'test\\%'" assert unquote_str("'test%'") == 'test%' assert unquote_str("'test\\%'") == 'test\\%' assert unquote_str("E'test\\%'") == 'test\\%' diff --git a/sqlobject/tests/test_datetime.py b/sqlobject/tests/test_datetime.py index 58c43f99..ae22adce 100644 --- a/sqlobject/tests/test_datetime.py +++ b/sqlobject/tests/test_datetime.py @@ -3,10 +3,11 @@ from sqlobject import SQLObject from sqlobject import col -from sqlobject.col import DATETIME_IMPLEMENTATION, DateCol, DateTimeCol, \ - MXDATETIME_IMPLEMENTATION, TimeCol, mxdatetime_available, use_microseconds +from sqlobject.col import DateCol, DateTimeCol, TimeCol, use_microseconds, \ + DATETIME_IMPLEMENTATION, MXDATETIME_IMPLEMENTATION, mxdatetime_available, \ + ZOPE_DATETIME_IMPLEMENTATION, zope_datetime_available from sqlobject.tests.dbtest import getConnection, setupClass - +from sqlobject.converters import pendulumDateTimeType ######################################## # Date/time columns @@ -24,7 +25,7 @@ class DateTime1(SQLObject): def test_dateTime(): setupClass(DateTime1) - _now = datetime.now() + _now = datetime.now().replace(microsecond=0) dt1 = DateTime1(col1=_now, col2=_now, col3=_now.time()) assert isinstance(dt1.col1, datetime) @@ -82,7 +83,7 @@ def test_microseconds(): if mxdatetime_available: col.default_datetime_implementation = MXDATETIME_IMPLEMENTATION - from mx.DateTime import now, Time + from mx.DateTime import now as mx_now, Time as mxTime dateFormat = None # use default try: @@ -92,10 +93,9 @@ def test_microseconds(): pass else: if connection.dbName == "sqlite": - if connection.using_sqlite2: - # mxDateTime sends and PySQLite2 returns - # full date/time for dates - dateFormat = "%Y-%m-%d %H:%M:%S.%f" + # mxDateTime sends and sqlite3 returns + # full date/time for dates + dateFormat = "%Y-%m-%d %H:%M:%S.%f" class DateTime2(SQLObject): col1 = DateTimeCol() @@ -104,11 +104,11 @@ class DateTime2(SQLObject): def test_mxDateTime(): setupClass(DateTime2) - _now = now() + _now = mx_now() dt2 = DateTime2(col1=_now, col2=_now.pydate(), - col3=Time(_now.hour, _now.minute, _now.second)) + col3=mxTime(_now.hour, _now.minute, _now.second)) - assert isinstance(dt2.col1, col.DateTimeType) + assert isinstance(dt2.col1, col.mxDateTimeType) assert dt2.col1.year == _now.year assert dt2.col1.month == _now.month assert dt2.col1.day == _now.day @@ -116,7 +116,7 @@ def test_mxDateTime(): assert dt2.col1.minute == _now.minute assert dt2.col1.second == int(_now.second) - assert isinstance(dt2.col2, col.DateTimeType) + assert isinstance(dt2.col2, col.mxDateTimeType) assert dt2.col2.year == _now.year assert dt2.col2.month == _now.month assert dt2.col2.day == _now.day @@ -124,7 +124,48 @@ def test_mxDateTime(): assert dt2.col2.minute == 0 assert dt2.col2.second == 0 - assert isinstance(dt2.col3, (col.DateTimeType, col.TimeType)) + assert isinstance(dt2.col3, (col.mxDateTimeType, col.mxTimeType)) assert dt2.col3.hour == _now.hour assert dt2.col3.minute == _now.minute assert dt2.col3.second == int(_now.second) + +if pendulumDateTimeType: + col.default_datetime_implementation = DATETIME_IMPLEMENTATION + import pendulum + + class DateTimePendulum(SQLObject): + col1 = DateTimeCol() + + def test_PendulumDateTime(): + setupClass(DateTimePendulum) + _now = pendulum.now() + dtp = DateTimePendulum(col1=_now) + + assert isinstance(dtp.col1, datetime) + assert dtp.col1.year == _now.year + assert dtp.col1.month == _now.month + assert dtp.col1.day == _now.day + assert dtp.col1.hour == _now.hour + assert dtp.col1.minute == _now.minute + assert int(dtp.col1.second) == int(_now.second) + + +if zope_datetime_available: + col.default_datetime_implementation = ZOPE_DATETIME_IMPLEMENTATION + from DateTime import DateTime as zopeDateTime + + class DateTime3(SQLObject): + col1 = DateTimeCol() + + def test_ZopeDateTime(): + setupClass(DateTime3) + _now = zopeDateTime() + dt3 = DateTime3(col1=_now) + + assert isinstance(dt3.col1, col.zopeDateTimeType) + assert dt3.col1.year() == _now.year() + assert dt3.col1.month() == _now.month() + assert dt3.col1.day() == _now.day() + assert dt3.col1.hour() == _now.hour() + assert dt3.col1.minute() == _now.minute() + assert int(dt3.col1.second()) == int(_now.second()) diff --git a/sqlobject/tests/test_dbconnection.py b/sqlobject/tests/test_dbconnection.py new file mode 100644 index 00000000..6e2dfc87 --- /dev/null +++ b/sqlobject/tests/test_dbconnection.py @@ -0,0 +1,17 @@ +from pytest import raises +from sqlobject.dbconnection import DBConnection + + +def test_name(): + connection = DBConnection(name='test') + connection.close = lambda: True + + with raises(AssertionError) as error: + DBConnection(name='test') + assert str(error.value) == 'An instance has already been registered ' \ + 'with the name test' + + with raises(AssertionError) as error: + DBConnection(name='test:test') + assert str(error.value) == "You cannot include ':' " \ + "in your DB connection names ('test:test')" diff --git a/sqlobject/tests/test_decimal.py b/sqlobject/tests/test_decimal.py index c7388e0d..06cd2fbe 100644 --- a/sqlobject/tests/test_decimal.py +++ b/sqlobject/tests/test_decimal.py @@ -16,7 +16,7 @@ pass else: if not support_decimal_column: - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require Decimal support") class DecimalTable(SQLObject): diff --git a/sqlobject/tests/test_exceptions.py b/sqlobject/tests/test_exceptions.py index 42dd134c..ff05bd0e 100644 --- a/sqlobject/tests/test_exceptions.py +++ b/sqlobject/tests/test_exceptions.py @@ -1,6 +1,7 @@ import pytest from sqlobject import SQLObject, StringCol -from sqlobject.dberrors import DuplicateEntryError, ProgrammingError +from sqlobject.dberrors import DatabaseError, DuplicateEntryError, \ + OperationalError, ProgrammingError from sqlobject.tests.dbtest import getConnection, raises, setupClass, supports @@ -25,12 +26,15 @@ def test_exceptions(): raises(DuplicateEntryError, SOTestException, name="test") connection = getConnection() - if connection.module.__name__ != 'psycopg2': - return SOTestExceptionWithNonexistingTable.setConnection(connection) try: list(SOTestExceptionWithNonexistingTable.select()) except ProgrammingError as e: - assert e.args[0].code == '42P01' + assert e.args[0].code in (1146, '42P01') + except OperationalError: + assert connection.dbName == 'sqlite' + except DatabaseError: + assert connection.dbName == 'postgres' \ + and connection.driver == 'pg8000' else: assert False, "DID NOT RAISE" diff --git a/sqlobject/tests/test_identity.py b/sqlobject/tests/test_identity.py index b3111f25..2c862efe 100644 --- a/sqlobject/tests/test_identity.py +++ b/sqlobject/tests/test_identity.py @@ -20,10 +20,10 @@ def test_identity(): SOTestIdentity(n=100) # i1 # verify result i1get = SOTestIdentity.get(1) - assert(i1get.n == 100) + assert (i1get.n == 100) # insert while giving identity SOTestIdentity(id=2, n=200) # i2 # verify result i2get = SOTestIdentity.get(2) - assert(i2get.n == 200) + assert (i2get.n == 200) diff --git a/sqlobject/tests/test_jsoncol.py b/sqlobject/tests/test_jsoncol.py index 1a120cda..52bb354e 100644 --- a/sqlobject/tests/test_jsoncol.py +++ b/sqlobject/tests/test_jsoncol.py @@ -1,3 +1,4 @@ +import pytest from sqlobject import SQLObject, JSONCol from sqlobject.tests.dbtest import setupClass @@ -18,17 +19,38 @@ class JSONTest(SQLObject): u"unicode", u"unicode'with'apostrophes", u"unicode\"with\"quotes", ], u"unicode", u"unicode'with'apostrophes", u"unicode\"with\"quotes", + {'test': 'Test'}, ) -def test_JSONCol(): +def _setup(): setupClass(JSONTest) for _id, test_data in enumerate(_json_test_data): - json = JSONTest(id=_id + 1, json=test_data) + JSONTest(id=_id + 1, json=test_data) + +def test_JSONCol(): + _setup() JSONTest._connection.cache.clear() for _id, test_data in enumerate(_json_test_data): json = JSONTest.get(_id + 1) assert json.json == test_data + + +def test_JSONCol_funcs(): + connection = JSONTest._connection + if not hasattr(connection, 'can_use_json_funcs') \ + or not connection.can_use_json_funcs(): + pytest.skip( + "The database doesn't support JSON functions; " + "JSON functions are supported by MariaDB since version 10.2.7 " + "and by MySQL since version 5.7.") + _setup() + + rows = list( + JSONTest.select(JSONTest.q.json.json_extract('test') == 'Test') + ) + assert len(rows) == 1 + assert rows[0].json == {'test': 'Test'} diff --git a/sqlobject/tests/test_mysql.py b/sqlobject/tests/test_mysql.py index de4b040b..5a15aca0 100644 --- a/sqlobject/tests/test_mysql.py +++ b/sqlobject/tests/test_mysql.py @@ -1,5 +1,6 @@ import pytest -from sqlobject import SQLObject +from sqlobject import SQLObject, IntCol +from sqlobject.sqlbuilder import Select, ANY from sqlobject.tests.dbtest import getConnection, setupClass @@ -10,7 +11,7 @@ pass else: if connection.dbName != "mysql": - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require MySQL") class SOTestSOListMySQL(SQLObject): @@ -24,3 +25,26 @@ def test_list_databases(): def test_list_tables(): setupClass(SOTestSOListMySQL) assert SOTestSOListMySQL.sqlmeta.table in connection.listTables() + + +class SOTestANY(SQLObject): + value = IntCol() + + +def test_ANY(): + setupClass(SOTestANY) + SOTestANY(value=10) + SOTestANY(value=20) + SOTestANY(value=30) + assert len(list(SOTestANY.select( + SOTestANY.q.value > ANY(Select([SOTestANY.q.value]))))) == 2 + + +class SOTestMySQLidSize(SQLObject): + class sqlmeta: + idSize = 'BIG' + + +def test_idSize(): + assert 'id BIGINT PRIMARY KEY AUTO_INCREMENT' \ + in SOTestMySQLidSize.createTableSQL(connection=connection)[0] diff --git a/sqlobject/tests/test_paste.py b/sqlobject/tests/test_paste.py index 36623093..5cd90298 100644 --- a/sqlobject/tests/test_paste.py +++ b/sqlobject/tests/test_paste.py @@ -5,7 +5,7 @@ try: from sqlobject.wsgi_middleware import make_middleware except ImportError: - pytestmark = pytest.mark.skipif('True') + pytestmark = pytest.mark.skipif(True, reason='These tests require Paste') from .dbtest import getConnection, getConnectionURI, setupClass diff --git a/sqlobject/tests/test_postgres.py b/sqlobject/tests/test_postgres.py index e656ce78..037a78a8 100644 --- a/sqlobject/tests/test_postgres.py +++ b/sqlobject/tests/test_postgres.py @@ -1,6 +1,7 @@ import os import pytest -from sqlobject import SQLObject, StringCol +from sqlobject import SQLObject, StringCol, IntCol +from sqlobject.sqlbuilder import Select, SOME from sqlobject.tests.dbtest import getConnection, setupClass @@ -16,7 +17,7 @@ pass else: if connection.dbName != "postgres": - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require PostgreSQL") class SOTestSSLMode(SQLObject): @@ -56,3 +57,26 @@ def test_list_databases(): def test_list_tables(): setupClass(SOTestSOList) assert SOTestSOList.sqlmeta.table in connection.listTables() + + +class SOTestSOME(SQLObject): + value = IntCol() + + +def test_SOME(): + setupClass(SOTestSOME) + SOTestSOME(value=10) + SOTestSOME(value=20) + SOTestSOME(value=30) + assert len(list(SOTestSOME.select( + SOTestSOME.q.value > SOME(Select([SOTestSOME.q.value]))))) == 2 + + +class SOTestPgidSize(SQLObject): + class sqlmeta: + idSize = 'BIG' + + +def test_idSize(): + assert 'id BIGSERIAL PRIMARY KEY' \ + in SOTestPgidSize.createTableSQL(connection=connection)[0] diff --git a/sqlobject/tests/test_schema.py b/sqlobject/tests/test_schema.py index 4a7887d4..5d4e3ad6 100644 --- a/sqlobject/tests/test_schema.py +++ b/sqlobject/tests/test_schema.py @@ -22,7 +22,9 @@ def test_connection_schema(): conn.query('SET search_path TO test') setupClass(SOTestSchema) assert SOTestSchema._connection is conn - SOTestSchema(foo='bar') - assert conn.queryAll("SELECT * FROM test.so_test_schema") - conn.schema = None - conn.query('SET search_path TO public') + try: + SOTestSchema(foo='bar') + assert conn.queryAll("SELECT * FROM test.so_test_schema") + finally: + conn.schema = None + conn.query('SET search_path TO public') diff --git a/sqlobject/tests/test_select.py b/sqlobject/tests/test_select.py index 3a0055ea..ab132768 100644 --- a/sqlobject/tests/test_select.py +++ b/sqlobject/tests/test_select.py @@ -66,7 +66,7 @@ def test_04_indexed_ended_by_exception(): try: while 1: all[count] - count = count + 1 + count += 1 # Stop the test if it's gone on too long if count > len(names): break @@ -183,10 +183,7 @@ def test_select_RLIKE(): setupClass(IterTest) if IterTest._connection.dbName == "sqlite": - if not IterTest._connection.using_sqlite2: - pytest.skip("These tests require SQLite v2+") - - # Implement regexp() function for SQLite; only works with PySQLite2 + # Implement regexp() function for SQLite import re def regexp(regexp, test): diff --git a/sqlobject/tests/test_sqlbuilder.py b/sqlobject/tests/test_sqlbuilder.py index 1d2c8501..1b198fc2 100644 --- a/sqlobject/tests/test_sqlbuilder.py +++ b/sqlobject/tests/test_sqlbuilder.py @@ -1,7 +1,7 @@ from sqlobject import IntCol, SQLObject, StringCol from sqlobject.compat import PY2 -from sqlobject.sqlbuilder import AND, CONCAT, Delete, Insert, SQLOp, Select, \ - Union, Update, const, func, sqlrepr +from sqlobject.sqlbuilder import AND, ANY, CONCAT, Delete, Insert, \ + SQLOp, Select, Union, Update, const, func, sqlrepr from sqlobject.tests.dbtest import getConnection, raises, setupClass @@ -119,3 +119,16 @@ def test_CONCAT(): if not PY2 and not isinstance(result, str): result = result.decode('ascii') assert result == "test-suffix" + + +def test_ANY(): + setupClass(SOTestSQLBuilder) + + select = Select( + [SOTestSQLBuilder.q.name], + 'value' == ANY(SOTestSQLBuilder.q.so_value), + ) + + assert sqlrepr(select, 'mysql') == \ + "SELECT so_test_sql_builder.name FROM so_test_sql_builder " \ + "WHERE (('value') = ANY (so_test_sql_builder.so_value))" diff --git a/sqlobject/tests/test_sqlite.py b/sqlobject/tests/test_sqlite.py index 4e9611a8..8858bcab 100644 --- a/sqlobject/tests/test_sqlite.py +++ b/sqlobject/tests/test_sqlite.py @@ -16,7 +16,7 @@ pass else: if connection.dbName != "sqlite": - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require SQLite") class SQLiteFactoryTest(SQLObject): @@ -25,10 +25,6 @@ class SQLiteFactoryTest(SQLObject): def test_sqlite_factory(): setupClass(SQLiteFactoryTest) - - if not SQLiteFactoryTest._connection.using_sqlite2: - pytest.skip("These tests require SQLite v2+") - factory = [None] def SQLiteConnectionFactory(sqlite): @@ -46,10 +42,6 @@ class MyConnection(sqlite.Connection): def test_sqlite_factory_str(): setupClass(SQLiteFactoryTest) - - if not SQLiteFactoryTest._connection.using_sqlite2: - pytest.skip("These tests require SQLite v2+") - factory = [None] def SQLiteConnectionFactory(sqlite): @@ -72,9 +64,6 @@ class MyConnection(sqlite.Connection): def test_sqlite_aggregate(): setupClass(SQLiteFactoryTest) - if not SQLiteFactoryTest._connection.using_sqlite2: - pytest.skip("These tests require SQLite v2+") - def SQLiteConnectionFactory(sqlite): class MyConnection(sqlite.Connection): def __init__(self, *args, **kwargs): @@ -140,7 +129,7 @@ def test_memorydb(): def test_list_databases(): - assert connection.listDatabases() == ['main'] + assert 'main' in connection.listDatabases() def test_list_tables(): @@ -156,9 +145,6 @@ def test_truediv(): setupClass(SQLiteTruedivTest) if SQLiteTruedivTest._connection.dbName == "sqlite": - if not SQLiteTruedivTest._connection.using_sqlite2: - pytest.skip("These tests require SQLite v2+") - def SQLiteConnectionFactory(sqlite): class MyConnection(sqlite.Connection): def __init__(self, *args, **kwargs): diff --git a/sqlobject/tests/test_transactions.py b/sqlobject/tests/test_transactions.py index 56ce7f8e..ee66620b 100644 --- a/sqlobject/tests/test_transactions.py +++ b/sqlobject/tests/test_transactions.py @@ -16,7 +16,7 @@ pass else: if not support_transactions: - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require transactions") class SOTestSOTrans(SQLObject): diff --git a/sqlobject/tests/test_uuidcol.py b/sqlobject/tests/test_uuidcol.py index 117d80ec..d1e2c22f 100644 --- a/sqlobject/tests/test_uuidcol.py +++ b/sqlobject/tests/test_uuidcol.py @@ -12,11 +12,11 @@ class UuidContainer(SQLObject): - uuiddata = UuidCol(default=None) + uuiddata = UuidCol(alternateID=True, default=None) def test_uuidCol(): - setupClass([UuidContainer], force=True) + setupClass([UuidContainer]) my_uuid = UuidContainer(uuiddata=testuuid) iid = my_uuid.id @@ -26,3 +26,15 @@ def test_uuidCol(): my_uuid_2 = UuidContainer.get(iid) assert my_uuid_2.uuiddata == testuuid + + +def test_alternate_id(): + setupClass([UuidContainer]) + + UuidContainer(uuiddata=testuuid) + + UuidContainer._connection.cache.clear() + + my_uuid_2 = UuidContainer.byUuiddata(testuuid) + + assert my_uuid_2.uuiddata == testuuid diff --git a/tox.ini b/tox.ini index dc17448c..04bc01f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,37 +1,42 @@ [tox] -minversion = 2.0 -envlist = py{27,34,35,36,37}-sqlite{,-memory},py{27,37}-flake8 +minversion = 3.15 +envlist = py{27,34,35,36,37,38,39,310,311,312,313,314}-sqlite{,-memory},py{27,37,314}-flake8 # Base test environment settings [testenv] # Ensure we cd into sqlobject before running the tests changedir = ./sqlobject/ -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 + py34: zope.datetime < 4.3 + !py34: zope.datetime mysqldb: mysql-python - mysqlclient: mysqlclient - mysql-connector: mysql-connector <= 2.2.2 - mysql-oursql: git+https://github.com/sqlobject/oursql.git@master#egg=oursql - mysql-oursql3: git+https://github.com/sqlobject/oursql.git@py3k#egg=oursql - pymysql: pymysql - postgres-psycopg: psycopg2-binary - postgres-pygresql: pygresql - pypostgresql: git+https://github.com/sqlobject/py-postgresql.git@fix_w32#egg=pypostgresql - postgres-pg8000: git+https://github.com/sqlobject/pg8000.git@getuser#egg=pg8000 + mysqlclient: -rdevscripts/requirements/requirements_mysqlclient.txt + mysql-connector: mysql-connector + mysql-connector_py: -rdevscripts/requirements/requirements_connector_python.txt + pymysql: -rdevscripts/requirements/requirements_pymysql.txt + mariadb: mariadb + psycopg: psycopg + psycopg-binary: psycopg[binary] + psycopg_c: psycopg[c] + psycopg2: psycopg2 + py34-psycopg2-binary: psycopg2-binary==2.8.4 + !py34-psycopg2-binary: psycopg2-binary + pygresql: -rdevscripts/requirements/requirements_pygresql.txt + pg8000: -rdevscripts/requirements/requirements_pg8000.txt pyodbc: pyodbc pypyodbc: pypyodbc firebird-fdb: fdb firebirdsql: firebirdsql -passenv = CI TRAVIS TRAVIS_* APPVEYOR DISTUTILS_USE_SDK MSSdk INCLUDE LIB PGPASSWORD WINDIR +# Upgrade pip/setuptools/wheel +download = true +passenv = CI +setenv = + PGPASSWORD = test # Don't fail or warn on uninstalled commands platform = linux whitelist_externals = @@ -48,12 +53,12 @@ whitelist_externals = [mysqldb] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=mysqldb&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=mysqldb&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py27-mysqldb] +[testenv:py27-mysqldb-noauto] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysqldb]commands} @@ -61,294 +66,235 @@ commands = [mysqlclient] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=mysqldb&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=mysqldb&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py34-mysqlclient] -commands = {[mysqlclient]commands} - -[testenv:py35-mysqlclient] -commands = {[mysqlclient]commands} - -[testenv:py36-mysqlclient] -commands = {[mysqlclient]commands} - -[testenv:py37-mysqlclient] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysqlclient] commands = {[mysqlclient]commands} [mysql-connector] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=connector&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://runner:@localhost/sqlobject_test?driver=connector&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" [testenv:py27-mysql-connector] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-connector]commands} -[testenv:py34-mysql-connector] -commands = {[mysql-connector]commands} - -[testenv:py35-mysql-connector] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-connector] commands = {[mysql-connector]commands} -[testenv:py36-mysql-connector] -commands = {[mysql-connector]commands} - -[testenv:py37-mysql-connector] -commands = {[mysql-connector]commands} - -[oursql] +[mysql-connector_py] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=oursql&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://runner:@localhost/sqlobject_test?driver=connector-python&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py27-mysql-oursql] +[testenv:py27-mysql-connector_py] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base - {[oursql]commands} - -[testenv:py34-mysql-oursql3] -commands = {[oursql]commands} - -[testenv:py35-mysql-oursql3] -commands = {[oursql]commands} - -[testenv:py36-mysql-oursql3] -commands = {[oursql]commands} + {[mysql-connector_py]commands} -[testenv:py37-mysql-oursql3] -commands = {[oursql]commands} +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-connector_py] +commands = {[mysql-connector_py]commands} [pymysql] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=pymysql&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=pymysql&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py27-pymysql] +[testenv:py27-mysql-pymysql] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[pymysql]commands} -[testenv:py34-pymysql] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pymysql] commands = {[pymysql]commands} -[testenv:py35-pymysql] -commands = {[pymysql]commands} - -[testenv:py36-pymysql] -commands = {[pymysql]commands} +[mariadb] +commands = + {[testenv]commands} + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=mariadb&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py37-pymysql] -commands = {[pymysql]commands} +[testenv:py3{6,7,8,9,10,11,12,13,14}-mariadb] +commands = {[mariadb]commands} [mysql-pyodbc] commands = {[testenv]commands} {envpython} -c "import pyodbc; print(pyodbc.drivers())" - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=pyodbc&odbcdrv=MySQL&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=pyodbc&odbcdrv=MySQL&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py27-mysql-pyodbc] +[testenv:py27-mysql-pyodbc-noauto] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-pyodbc]commands} -[testenv:py34-mysql-pyodbc] -commands = {[mysql-pyodbc]commands} - -[testenv:py35-mysql-pyodbc] -commands = {[mysql-pyodbc]commands} - -[testenv:py36-mysql-pyodbc] -commands = {[mysql-pyodbc]commands} - -[testenv:py37-mysql-pyodbc] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pyodbc-noauto] commands = {[mysql-pyodbc]commands} [mysql-pypyodbc] commands = {[testenv]commands} - -mysql -uroot -e 'drop database sqlobject_test;' - mysql -uroot -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=MySQL&charset=utf8&debug=1 - mysql -uroot -e 'drop database sqlobject_test;' + -mysql --execute="drop database sqlobject_test;" + mysql --execute="create database sqlobject_test;" + pytest -D "mysql://localhost/sqlobject_test?driver=pypyodbc&odbcdrv=MySQL&charset=utf8&debug=1" + mysql --execute="drop database sqlobject_test;" -[testenv:py27-mysql-pypyodbc] +[testenv:py27-mysql-pypyodbc-noauto] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-pypyodbc]commands} -[testenv:py34-mysql-pypyodbc] -commands = {[mysql-pypyodbc]commands} - -[testenv:py35-mysql-pypyodbc] -commands = {[mysql-pypyodbc]commands} - -[testenv:py36-mysql-pypyodbc] -commands = {[mysql-pypyodbc]commands} - -[testenv:py37-mysql-pypyodbc] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pypyodbc-noauto] commands = {[mysql-pypyodbc]commands} # PostgreSQL test environments [psycopg] commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test - -[testenv:py27-postgres-psycopg] -commands = - easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base - {[psycopg]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py34-postgres-psycopg] +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg] commands = {[psycopg]commands} -[testenv:py35-postgres-psycopg] -commands = {[psycopg]commands} +[psycopg-binary] +commands = + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py36-postgres-psycopg] -commands = {[psycopg]commands} +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-binary] +commands = {[psycopg-binary]commands} -[testenv:py37-postgres-psycopg] -commands = {[psycopg]commands} +[psycopg_c] +commands = + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[pygresql] +[testenv:py3{8,9,10,11,12,13,14}-postgres-psycopg_c] +commands = {[psycopg_c]commands} + +[psycopg2] commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=pygresql&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg2&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pygresql] +[testenv:py27-postgres-psycopg2] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base - {[pygresql]commands} + {[psycopg2]commands} -[testenv:py34-postgres-pygresql] -commands = {[pygresql]commands} +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-psycopg2] +commands = {[psycopg2]commands} -[testenv:py35-postgres-pygresql] -commands = {[pygresql]commands} +[psycopg2-binary] +commands = + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg2&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py36-postgres-pygresql] -commands = {[pygresql]commands} +[testenv:py27-postgres-psycopg2-binary] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2-binary]commands} -[testenv:py37-postgres-pygresql] -commands = {[pygresql]commands} +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-psycopg2-binary] +commands = {[psycopg2-binary]commands} -[pypostgresql] +[pygresql] commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=pypostgresql&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test - -[testenv:py34-pypostgresql] -commands = {[pypostgresql]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pygresql&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py35-pypostgresql] -commands = {[pypostgresql]commands} - -[testenv:py36-pypostgresql] -commands = {[pypostgresql]commands} +[testenv:py27-postgres-pygresql] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pygresql]commands} -[testenv:py37-pypostgresql] -commands = {[pypostgresql]commands} +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pygresql] +commands = {[pygresql]commands} [pg8000] commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=pg8000&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pg8000&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test [testenv:py27-postgres-pg8000] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[pg8000]commands} -[testenv:py34-postgres-pg8000] -commands = {[pg8000]commands} - -[testenv:py35-postgres-pg8000] -commands = {[pg8000]commands} - -[testenv:py36-postgres-pg8000] -commands = {[pg8000]commands} - -[testenv:py37-postgres-pg8000] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pg8000] commands = {[pg8000]commands} [postgres-pyodbc] commands = {[testenv]commands} {envpython} -c "import pyodbc; print(pyodbc.drivers())" - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=pyodbc&odbcdrv=PostgreSQL%20ANSI&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pyodbc&odbcdrv=PostgreSQL%20ANSI&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pyodbc] +[testenv:py27-postgres-pyodbc-noauto] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[postgres-pyodbc]commands} -[testenv:py34-postgres-pyodbc] -commands = {[postgres-pyodbc]commands} - -[testenv:py35-postgres-pyodbc] -commands = {[postgres-pyodbc]commands} - -[testenv:py36-postgres-pyodbc] -commands = {[postgres-pyodbc]commands} - -[testenv:py37-postgres-pyodbc] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pyodbc-noauto] commands = {[postgres-pyodbc]commands} [postgres-pypyodbc] commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D postgres://postgres:@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=PostgreSQL%20ANSI&charset=utf-8&debug=1 tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=PostgreSQL%20ANSI&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pypyodbc] +[testenv:py27-postgres-pypyodbc-noauto] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[postgres-pypyodbc]commands} -[testenv:py34-postgres-pypyodbc] -commands = {[postgres-pypyodbc]commands} - -[testenv:py35-postgres-pypyodbc] -commands = {[postgres-pypyodbc]commands} - -[testenv:py36-postgres-pypyodbc] -commands = {[postgres-pypyodbc]commands} - -[testenv:py37-postgres-pypyodbc] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pypyodbc-noauto] commands = {[postgres-pypyodbc]commands} @@ -357,7 +303,7 @@ commands = {[postgres-pypyodbc]commands} commands = {[testenv]commands} -rm -f /tmp/sqlobject_test.sqdb - pytest --cov=sqlobject -D sqlite:///tmp/sqlobject_test.sqdb?debug=1 + pytest -D "sqlite:///tmp/sqlobject_test.sqdb?debug=1" rm -f /tmp/sqlobject_test.sqdb [testenv:py27-sqlite] @@ -365,39 +311,22 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[sqlite]commands} -[testenv:py34-sqlite] -commands = {[sqlite]commands} - -[testenv:py35-sqlite] -commands = {[sqlite]commands} - -[testenv:py36-sqlite] -commands = {[sqlite]commands} - -[testenv:py37-sqlite] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite] commands = {[sqlite]commands} [sqlite-memory] commands = {[testenv]commands} - pytest --cov=sqlobject -D sqlite:/:memory:?debug=1 + pytest -D sqlite:/:memory:?debug=1 [testenv:py27-sqlite-memory] commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[sqlite-memory]commands} -[testenv:py34-sqlite-memory] -commands = {[sqlite-memory]commands} - -[testenv:py35-sqlite-memory] -commands = {[sqlite-memory]commands} - -[testenv:py36-sqlite-memory] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite-memory] commands = {[sqlite-memory]commands} -[testenv:py37-sqlite-memory] -commands = {[sqlite-memory]commands} # Firebird database test environments [fdb] @@ -405,7 +334,7 @@ commands = {[testenv]commands} sudo rm -f /tmp/test.fdb isql-fb -u test -p test -i /var/lib/firebird/create_test_db - pytest --cov=sqlobject -D 'firebird://test:test@localhost/tmp/test.fdb?debug=1' + pytest -D "firebird://test:test@localhost/tmp/test.fdb?debug=1" sudo rm -f /tmp/test.fdb [testenv:py27-firebird-fdb] @@ -413,16 +342,7 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[fdb]commands} -[testenv:py34-firebird-fdb] -commands = {[fdb]commands} - -[testenv:py35-firebird-fdb] -commands = {[fdb]commands} - -[testenv:py36-firebird-fdb] -commands = {[fdb]commands} - -[testenv:py37-firebird-fdb] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-firebird-fdb] commands = {[fdb]commands} [firebirdsql] @@ -430,7 +350,7 @@ commands = {[testenv]commands} sudo rm -f /tmp/test.fdb isql-fb -u test -p test -i /var/lib/firebird/create_test_db - pytest --cov=sqlobject -D 'firebird://test:test@localhost:3050/tmp/test.fdb?driver=firebirdsql&charset=utf8&debug=1' + pytest -D "firebird://test:test@localhost:3050/tmp/test.fdb?driver=firebirdsql&charset=utf8&debug=1" sudo rm -f /tmp/test.fdb [testenv:py27-firebirdsql] @@ -438,34 +358,20 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[firebirdsql]commands} -[testenv:py34-firebirdsql] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-firebirdsql] commands = {[firebirdsql]commands} -[testenv:py35-firebirdsql] -commands = {[firebirdsql]commands} - -[testenv:py36-firebirdsql] -commands = {[firebirdsql]commands} - -[testenv:py37-firebirdsql] -commands = {[firebirdsql]commands} # Special test environments -[testenv:py27-flake8] +[testenv:py{27,34,35,36,37,38,39,310,311,312,313,314}-flake8] changedir = ./ deps = flake8 + pytest commands = {[testenv]commands} flake8 . -[testenv:py37-flake8] -changedir = ./ -deps = - flake8 -commands = - {[testenv]commands} - flake8 . # Windows testing [mssql-pyodbc-w32] @@ -474,41 +380,55 @@ commands = {envpython} -c "import pyodbc; print(pyodbc.drivers())" -sqlcmd -U sa -P "Password12!" -S .\SQL2014 -Q "DROP DATABASE sqlobject_test" sqlcmd -U sa -P "Password12!" -S .\SQL2014 -Q "CREATE DATABASE sqlobject_test" - pytest --cov=sqlobject -D "mssql://sa:Password12!@localhost\SQL2014/sqlobject_test?driver=pyodbc&odbcdrv=SQL%20Server&timeout=30&debug=1" + pytest -D "mssql://sa:Password12!@localhost\SQL2014/sqlobject_test?driver=pyodbc&odbcdrv=SQL%20Server&timeout=30&debug=1" sqlcmd -U sa -P "Password12!" -S .\SQL2014 -Q "DROP DATABASE sqlobject_test" -[testenv:py27-mssql-pyodbc-w32] +[testenv:py27-mssql-pyodbc-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mssql-pyodbc-w32]commands} -[testenv:py34-mssql-pyodbc-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mssql-pyodbc-noauto-w32] platform = win32 commands = {[mssql-pyodbc-w32]commands} -[testenv:py35-mssql-pyodbc-w32] +[mysqldb-w32] platform = win32 -commands = {[mssql-pyodbc-w32]commands} +commands = + {[testenv]commands} + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=mysqldb&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py36-mssql-pyodbc-w32] +[testenv:py27-mysqldb-noauto-w32] platform = win32 -commands = {[mssql-pyodbc-w32]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysqldb-w32]commands} -[testenv:py37-mssql-pyodbc-w32] +[mysqlclient-w32] platform = win32 commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[mssql-pyodbc-w32]commands} + {[testenv]commands} + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=mysqldb&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" + +[testenv:py3{6,7,8,9,10,11,12,13}-mysqlclient-w32] +platform = win32 +commands = {[mysqlclient-w32]commands} [mysql-connector-w32] platform = win32 commands = {[testenv]commands} - -mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' - mysql -u root "-pPassword12!" -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D "mysql://root:Password12!@localhost/sqlobject_test?driver=connector&charset=utf8&debug=1" - mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=connector&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" [testenv:py27-mysql-connector-w32] platform = win32 @@ -516,324 +436,262 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-connector-w32]commands} -[testenv:py34-mysql-connector-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-connector-w32] platform = win32 commands = {[mysql-connector-w32]commands} -[testenv:py35-mysql-connector-w32] +[mysql-connector_py-w32] platform = win32 -commands = {[mysql-connector-w32]commands} +commands = + {[testenv]commands} + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=connector-python&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py36-mysql-connector-w32] +[testenv:py27-mysql-connector_py-w32] platform = win32 -commands = {[mysql-connector-w32]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-connector_py-w32]commands} -[testenv:py37-mysql-connector-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-connector_py-w32] platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[mysql-connector-w32]commands} +commands = {[mysql-connector_py-w32]commands} [pymysql-w32] platform = win32 commands = {[testenv]commands} - -mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' - mysql -u root "-pPassword12!" -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D "mysql://root:Password12!@localhost/sqlobject_test?driver=pymysql&charset=utf8&debug=1" - mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=pymysql&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py27-pymysql-w32] +[testenv:py27-mysql-pymysql-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[pymysql-w32]commands} -[testenv:py34-pymysql-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12}-mysql-pymysql-w32] platform = win32 commands = {[pymysql-w32]commands} -[testenv:py35-pymysql-w32] +[mariadb-w32] platform = win32 -commands = {[pymysql-w32]commands} - -[testenv:py36-pymysql-w32] -platform = win32 -commands = {[pymysql-w32]commands} +commands = + {[testenv]commands} + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=mariadb&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py37-pymysql-w32] +[testenv:py3{6,7,8,9,10,11,12,13,14}-mariadb-w32] platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[pymysql-w32]commands} +commands = {[mariadb-w32]commands} [mysql-pyodbc-w32] platform = win32 commands = {[testenv]commands} {envpython} -c "import pyodbc; print(pyodbc.drivers())" - -mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' - mysql -u root "-pPassword12!" -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:Password12!@localhost/sqlobject_test?driver=pyodbc&odbcdrv=MySQL%20ODBC%205.3%20ANSI%20Driver&charset=utf8&debug=1 - mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=pyodbc&odbcdrv=MySQL%20ODBC%205.3%20ANSI%20Driver&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py27-mysql-pyodbc-w32] +[testenv:py27-mysql-pyodbc-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-pyodbc-w32]commands} -[testenv:py34-mysql-pyodbc-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pyodbc-noauto-w32] platform = win32 commands = {[mysql-pyodbc-w32]commands} -[testenv:py35-mysql-pyodbc-w32] -platform = win32 -commands = {[mysql-pyodbc-w32]commands} - -[testenv:py36-mysql-pyodbc-w32] -platform = win32 -commands = {[mysql-pyodbc-w32]commands} - -[testenv:py37-mysql-pyodbc-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[mysql-pyodbc-w32]commands} - [mysql-pypyodbc-w32] platform = win32 commands = {[testenv]commands} {envpython} -c "import pypyodbc; print(pypyodbc.drivers())" - -mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' - mysql -u root "-pPassword12!" -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:Password12!@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=MySQL%20ODBC%205.3%20ANSI%20Driver&charset=utf8&debug=1 - mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' + -mysql --user=ODBC -e "drop database sqlobject_test;" + mysql --user=ODBC -e "create database sqlobject_test;" + pytest -D "mysql://ODBC@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=MySQL%20ODBC%205.3%20ANSI%20Driver&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" -[testenv:py27-mysql-pypyodbc-w32] +[testenv:py27-mysql-pypyodbc-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[mysql-pypyodbc-w32]commands} -[testenv:py34-mysql-pypyodbc-w32] -platform = win32 -commands = {[mysql-pypyodbc-w32]commands} - -[testenv:py35-mysql-pypyodbc-w32] -platform = win32 -commands = {[mysql-pypyodbc-w32]commands} - -[testenv:py36-mysql-pypyodbc-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pypyodbc-noauto-w32] platform = win32 commands = {[mysql-pypyodbc-w32]commands} -[testenv:py37-mysql-pypyodbc-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[mysql-pypyodbc-w32]commands} - [psycopg-w32] platform = win32 commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=psycopg2&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-psycopg-w32] -platform = win32 -commands = - easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base - {[psycopg-w32]commands} - -[testenv:py34-postgres-psycopg-w32] +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-w32] platform = win32 commands = {[psycopg-w32]commands} -[testenv:py35-postgres-psycopg-w32] +[psycopg-binary-w32] platform = win32 -commands = {[psycopg-w32]commands} +commands = + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py36-postgres-psycopg-w32] +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-binary-w32] platform = win32 -commands = {[psycopg-w32]commands} +commands = {[psycopg-binary-w32]commands} -[testenv:py37-postgres-psycopg-w32] +[psycopg_c-w32] platform = win32 commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[psycopg-w32]commands} + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[pygresql-w32] +[testenv:py3{8,9,10,11,12,13,14}-postgres-psycopg_c-w32] +platform = win32 +commands = {[psycopg_c-w32]commands} + +[psycopg2-w32] platform = win32 commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=pygresql&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg2&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pygresql-w32] +[testenv:py27-postgres-psycopg2-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base - {[pygresql-w32]commands} + {[psycopg2-w32]commands} -[testenv:py34-postgres-pygresql-w32] +[testenv:py3{4,5,6,7,9,10,11,12,13,14}-postgres-psycopg2-w32] platform = win32 -commands = {[pygresql-w32]commands} - -[testenv:py35-postgres-pygresql-w32] -platform = win32 -commands = {[pygresql-w32]commands} +commands = {[psycopg2-w32]commands} -[testenv:py36-postgres-pygresql-w32] -platform = win32 -commands = {[pygresql-w32]commands} - -[testenv:py37-postgres-pygresql-w32] +[psycopg2-binary-w32] platform = win32 commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[pygresql-w32]commands} + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=psycopg2&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[pypostgresql-w32] +[testenv:py27-postgres-psycopg2-binary-w32] platform = win32 commands = - {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=pypostgresql&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2-binary-w32]commands} -[testenv:py34-pypostgresql-w32] +[testenv:py3{4,5,6,7,9,10,11,12,13,14}-postgres-psycopg2-binary-w32] platform = win32 -commands = {[pypostgresql-w32]commands} +commands = {[psycopg2-binary-w32]commands} -[testenv:py35-pypostgresql-w32] +[pygresql-w32] platform = win32 -commands = {[pypostgresql-w32]commands} +commands = + {[testenv]commands} + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pygresql&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py36-pypostgresql-w32] +[testenv:py27-postgres-pygresql-w32] platform = win32 -commands = {[pypostgresql-w32]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pygresql-w32]commands} -[testenv:py37-pypostgresql-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pygresql-w32] platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[pypostgresql-w32]commands} +commands = {[pygresql-w32]commands} [pg8000-w32] platform = win32 commands = {[testenv]commands} - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=pg8000&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pg8000&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pg8000-w32] +[testenv:py27-postgres-pg8000-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[pg8000-w32]commands} -[testenv:py34-postgres-pg8000-w32] -platform = win32 -commands = {[pg8000-w32]commands} - -[testenv:py35-postgres-pg8000-w32] -platform = win32 -commands = {[pg8000-w32]commands} - -[testenv:py36-postgres-pg8000-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pg8000-w32] platform = win32 commands = {[pg8000-w32]commands} -[testenv:py37-postgres-pg8000-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[pg8000-w32]commands} - [postgres-pyodbc-w32] platform = win32 commands = {[testenv]commands} {envpython} -c "import pyodbc; print(pyodbc.drivers())" - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=pyodbc&odbcdrv=PostgreSQL%20ANSI%28x64%29&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pyodbc&odbcdrv=PostgreSQL%20ANSI%28x64%29&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pyodbc-w32] +[testenv:py27-postgres-pyodbc-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[postgres-pyodbc-w32]commands} -[testenv:py34-postgres-pyodbc-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pyodbc-noauto-w32] platform = win32 commands = {[postgres-pyodbc-w32]commands} -[testenv:py35-postgres-pyodbc-w32] -platform = win32 -commands = {[postgres-pyodbc-w32]commands} - -[testenv:py36-postgres-pyodbc-w32] -platform = win32 -commands = {[postgres-pyodbc-w32]commands} - -[testenv:py37-postgres-pyodbc-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[postgres-pyodbc-w32]commands} - [postgres-pypyodbc-w32] platform = win32 commands = {[testenv]commands} {envpython} -c "import pypyodbc; print(pypyodbc.drivers())" - -dropdb -U postgres -w sqlobject_test - createdb -U postgres -w sqlobject_test - pytest --cov=sqlobject -D "postgres://postgres:Password12!@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=PostgreSQL%20ANSI%28x64%29&charset=utf-8&debug=1" tests include/tests inheritance/tests versioning/test - dropdb -U postgres -w sqlobject_test + -dropdb --username=runner --no-password sqlobject_test + createdb --username=runner --no-password sqlobject_test + pytest -D "postgres://runner:test@localhost/sqlobject_test?driver=pypyodbc&odbcdrv=PostgreSQL%20ANSI%28x64%29&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test -[testenv:py27-postgres-pypyodbc-w32] +[testenv:py27-postgres-pypyodbc-noauto-w32] platform = win32 commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[postgres-pypyodbc-w32]commands} -[testenv:py34-postgres-pypyodbc-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pypyodbc-noauto-w32] platform = win32 commands = {[postgres-pypyodbc-w32]commands} -[testenv:py35-postgres-pypyodbc-w32] -platform = win32 -commands = {[postgres-pypyodbc-w32]commands} - -[testenv:py36-postgres-pypyodbc-w32] -platform = win32 -commands = {[postgres-pypyodbc-w32]commands} - -[testenv:py37-postgres-pypyodbc-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[postgres-pypyodbc-w32]commands} - [sqlite-w32] platform = win32 commands = {[testenv]commands} - pytest --cov=sqlobject -D sqlite:/C:/projects/sqlobject/sqlobject_test.sqdb?debug=1 + pytest -D sqlite:/{env:TEMP}/sqlobject_test.sqdb?debug=1 + cmd /c "del {env:TEMP}\sqlobject_test.sqdb" [testenv:py27-sqlite-w32] platform = win32 @@ -841,29 +699,15 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[sqlite-w32]commands} -[testenv:py34-sqlite-w32] -platform = win32 -commands = {[sqlite-w32]commands} - -[testenv:py35-sqlite-w32] -platform = win32 -commands = {[sqlite-w32]commands} - -[testenv:py36-sqlite-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-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} - [sqlite-memory-w32] platform = win32 commands = {[testenv]commands} - pytest --cov=sqlobject -D sqlite:/:memory:?debug=1 + pytest -D sqlite:/:memory:?debug=1 [testenv:py27-sqlite-memory-w32] platform = win32 @@ -871,20 +715,6 @@ commands = easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base {[sqlite-memory-w32]commands} -[testenv:py34-sqlite-memory-w32] -platform = win32 -commands = {[sqlite-memory-w32]commands} - -[testenv:py35-sqlite-memory-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite-memory-w32] platform = win32 commands = {[sqlite-memory-w32]commands} - -[testenv:py36-sqlite-memory-w32] -platform = win32 -commands = {[sqlite-memory-w32]commands} - -[testenv:py37-sqlite-memory-w32] -platform = win32 -commands = - cmd /c "copy ..\\validators.py {envsitepackagesdir}\\formencode\\validators.py" - {[sqlite-memory-w32]commands} diff --git a/validators.py b/validators.py deleted file mode 100644 index 233d5dd5..00000000 --- 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() -