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/.gitignore b/.gitignore index 8647366f..9825d011 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.cache/ +/.pytest_cache/ /.tox/ /SQLObject.egg-info/ /build/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 848791ae..00000000 --- a/.travis.yml +++ /dev/null @@ -1,100 +0,0 @@ -# Prefer docker container with setuid/sudo -sudo: required - -language: python - -cache: pip - -addons: - apt: - packages: - - python-egenix-mxdatetime - - python-mysqldb - - python-psycopg2 - - python3-psycopg2 - - firebird2.5-super - postgresql: "9.4" - -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. - - 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 touch /var/lib/firebird/create_test_db && - sudo chmod 666 /var/lib/firebird/create_test_db && - echo "CREATE DATABASE 'localhost:/tmp/test.fdb';" > /var/lib/firebird/create_test_db && - sudo chmod 644 /var/lib/firebird/create_test_db && - sudo gsec -user sysdba -pass masterkey -add test -pw test; - fi - -env: - - TOXENV=py26-mysqldb - - TOXENV=py27-mysqldb - - TOXENV=py34-mysqlclient - - TOXENV=py35-mysqlclient - - TOXENV=py26-mysql-connector - - TOXENV=py27-mysql-connector - - TOXENV=py34-mysql-connector - - TOXENV=py35-mysql-connector - - TOXENV=py26-mysql-oursql - - TOXENV=py27-mysql-oursql - - TOXENV=py26-pymysql - - TOXENV=py27-pymysql - - TOXENV=py34-pymysql - - TOXENV=py35-pymysql - - TOXENV=py26-postgres-psycopg - - TOXENV=py27-postgres-psycopg - - TOXENV=py34-postgres-psycopg - - TOXENV=py35-postgres-psycopg - - TOXENV=py26-postgres-pygresql - - TOXENV=py27-postgres-pygresql - - TOXENV=py34-postgres-pygresql - - TOXENV=py35-postgres-pygresql - - TOXENV=py34-pypostgresql - - TOXENV=py35-pypostgresql - - TOXENV=py26-sqlite - - TOXENV=py27-sqlite - - TOXENV=py34-sqlite - - TOXENV=py35-sqlite - - TOXENV=py26-sqlite-memory - - TOXENV=py27-sqlite-memory - - TOXENV=py34-sqlite-memory - - TOXENV=py35-sqlite-memory - - TOXENV=py27-flake8 - - TOXENV=py34-flake8 - - TOXENV=py27-firebird-fdb - - TOXENV=py34-firebird-fdb - - TOXENV=py35-firebird-fdb - - TOXENV=py27-firebirdsql - - TOXENV=py34-firebirdsql - - TOXENV=py35-firebirdsql - -install: pip install tox coveralls codecov - -matrix: - allow_failures: - - env: TOXENV=py26-postgres-pygresql - - env: TOXENV=py27-postgres-pygresql - - env: TOXENV=py34-postgres-pygresql - - env: TOXENV=py35-postgres-pygresql - - env: TOXENV=py34-pypostgresql - - env: TOXENV=py35-pypostgresql - - env: TOXENV=py27-firebird-fdb - - env: TOXENV=py34-firebird-fdb - - env: TOXENV=py35-firebird-fdb - - env: TOXENV=py27-firebirdsql - - env: TOXENV=py34-firebirdsql - - env: TOXENV=py35-firebirdsql - fast_finish: true - -script: tox -e ${TOXENV} - -after_success: - - cd sqlobject - - coveralls - - codecov diff --git a/ANNOUNCE.rst b/ANNOUNCE.rst new file mode 100644 index 00000000..b1d6645f --- /dev/null +++ b/ANNOUNCE.rst @@ -0,0 +1,101 @@ +Hello! + +I'm pleased to announce version 3.13.2a0, the 2nd bugfix of the +branch 3.13 of SQLObject. + + +What's new in SQLObject +======================= + +The contributors for this release are: + + +For a more complete list, please see the news: +http://sqlobject.org/News.html + + +What is SQLObject +================= + +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/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. + + +Where is SQLObject +================== + +Site: +http://sqlobject.org + +Download: +https://pypi.org/project/SQLObject/3.13.2a0.dev20251208/ + +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/MANIFEST.in b/MANIFEST.in index f486d465..c3330428 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ global-include *.py *.rst *.txt -include LICENSE MANIFEST.in .travis.yml circle.yml tox.ini -include debian/* sqlobject/.coveragerc -include docs/Makefile docs/genapidocs docs/rebuild recursive-include docs *.css *.html *.js *.gif *.png -prune docs/_build +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 173a5def..cf8a985e 100644 --- a/README.rst +++ b/README.rst @@ -1,13 +1,85 @@ -SQLObject 3.3.0a0 -================= +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.6, 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 c7ccf442..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,102 +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: 3.3.{build} - -# Match travis -clone_depth: 50 - -services: - - mysql - - postgresql - -environment: - MYSQL_PWD: "Password12!" - PGUSER: "postgres" - PGPASSWORD: "Password12!" - - matrix: - # from https://www.appveyor.com/docs/installed-software/#python - - PYTHON: "C:\\Python27" - db: mssql2014 - TOX_ENV: "py27-mssql-w32" - - PYTHON: "C:\\Python34" - db: mssql2014 - TOX_ENV: "py34-mssql-w32" - - PYTHON: "C:\\Python27" - db: mysql - TOX_ENV: "py27-mysql-connector-w32" - - PYTHON: "C:\\Python34" - db: mysql - TOX_ENV: "py34-mysql-connector-w32" - - PYTHON: "C:\\Python27" - db: postgresql - TOX_ENV: "py27-postgres-psycopg-w32" - - PYTHON: "C:\\Python27-x64" - db: postgresql - TOX_ENV: "py27-postgres-psycopg-w32" - - PYTHON: "C:\\Python34" - db: postgresql - TOX_ENV: "py34-postgres-psycopg-w32" - - PYTHON: "C:\\Python34-x64" - db: postgresql - TOX_ENV: "py34-postgres-psycopg-w32" - - PYTHON: "C:\\Python35" - db: postgresql - TOX_ENV: "py35-postgres-psycopg-w32" - - PYTHON: "C:\\Python35-x64" - db: postgresql - TOX_ENV: "py35-postgres-psycopg-w32" - - PYTHON: "C:\\Python27" - TOX_ENV: "py27-sqlite-w32" - - PYTHON: "C:\\Python27-x64" - TOX_ENV: "py27-sqlite-w32" - - PYTHON: "C:\\Python34" - TOX_ENV: "py34-sqlite-w32" - - PYTHON: "C:\\Python34-x64" - TOX_ENV: "py34-sqlite-w32" - - PYTHON: "C:\\Python35" - TOX_ENV: "py35-sqlite-w32" - - PYTHON: "C:\\Python35-x64" - TOX_ENV: "py35-sqlite-w32" - - PYTHON: "C:\\Python27" - TOX_ENV: "py27-sqlite-memory-w32" - - PYTHON: "C:\\Python27-x64" - TOX_ENV: "py27-sqlite-memory-w32" - - PYTHON: "C:\\Python34" - TOX_ENV: "py34-sqlite-memory-w32" - - PYTHON: "C:\\Python34-x64" - TOX_ENV: "py34-sqlite-memory-w32" - - PYTHON: "C:\\Python35" - TOX_ENV: "py35-sqlite-memory-w32" - - PYTHON: "C:\\Python35-x64" - TOX_ENV: "py35-sqlite-memory-w32" - -install: - # Ensure we use the right python version - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin;C:\\Program Files\\PostgreSQL\\9.5\\bin;%PATH%" - - "python --version" - - "pip install -r requirements.txt" - - "pip install tox" - # Enable TCP for mssql - # (from appveyor documentation) - - ps: | - [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null - [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null - $serverName = $env:COMPUTERNAME - $instanceName = 'SQL2014' - $smo = 'Microsoft.SqlServer.Management.Smo.' - $wmi = new-object ($smo + 'Wmi.ManagedComputer') - $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']" - $Tcp = $wmi.GetSmoObject($uri) - $Tcp.IsEnabled = $true - $TCP.alter() - Set-Service SQLBrowser -StartupType Manual - Start-Service SQLBrowser - Start-Service "MSSQL`$$instanceName" - -# Not a C project, so no build step -build: false - -test_script: - - "tox -e %TOX_ENV%" diff --git a/devscripts/BRANCH-CHECKLIST b/devscripts/BRANCH-CHECKLIST new file mode 100644 index 00000000..29899043 --- /dev/null +++ b/devscripts/BRANCH-CHECKLIST @@ -0,0 +1,30 @@ +0. Run full test suite in master. Continue if all tests passed. + +1. If the branching point is master run devscripts/branch $NEW_BRANCH. If + it's not master run devscripts/branch $NEW_BRANCH $TREEISH, where + $TREEISH is a branch, a commit id or a tag. + +1a. The script creates a new branch and calls editor; edit README.rst, + __version__.py and News.rst in the branch - set version. In + 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 + 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 + master - set version for the next release. In setup.py edit + "Development Status" in trove classifiers. Commit. + +1c. The script updates versions in ANNOUNCE.rst. + +2. To deprecate a version of Python edit files ANNOUNCE.rst, README.rst, + devscripts/release, devscripts/setup, docs/News.rst, docs/SQLObject.rst, + docs/TODO.rst, requirements.txt, setup.py, sqlobject/main.py, tox.ini in + master. Edit metadata at SourceForge. + +3. Do a null-merge from the new branch to the higher branch or the + master. + +4. Run devscripts/push-all to push all branches and tags to the public + repositories. diff --git a/devscripts/RELEASE-CHECKLIST b/devscripts/RELEASE-CHECKLIST new file mode 100644 index 00000000..a7909b4f --- /dev/null +++ b/devscripts/RELEASE-CHECKLIST @@ -0,0 +1,49 @@ +0. Run full test suite in all branches and in master. Continue if all + tests passed. + +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; 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. + +2. If it's not master - null-merge to the next higher branch. + +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. + +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. Move old releases at + SourceForge to subdirectory OldFiles. + +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. + +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. + +8. Send announcement to the SQLObject mailing list. For a stable + release send announcements to python, python-announce and python-db + mailing lists. + +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/add-remotes b/devscripts/add-remotes new file mode 100755 index 00000000..3b664bd9 --- /dev/null +++ b/devscripts/add-remotes @@ -0,0 +1,29 @@ +#! /bin/sh + +if [ -z "$2" ]; then + echo "Usage: $0 sf-url gl-url gh-url" >&2 + exit 1 +fi + +sf_url="$1" +gl_url="$2" +gh_url="$3" + +if ! echo "$sf_url" | grep -q ^$USER@git\\.code\\.sf\\.net:/p/sqlobject/; then + echo "Usage: $0 SF-URL gl-url gh-url" >&2 + exit 1 +fi + +if ! echo "$gl_url" | grep -q ^git@gitlab.com:sqlobject/; then + echo "Usage: $0 sf-url GL-URL gh-url" >&2 + exit 1 +fi + +if ! echo "$gh_url" | grep -q ^git@github.com:sqlobject/; then + echo "Usage: $0 sf-url gl-url GH-URL" >&2 + exit 1 +fi + + git remote add sf "$sf_url" && + git remote add gl "$gl_url" && +exec git remote add gh "$gh_url" diff --git a/devscripts/branch b/devscripts/branch new file mode 100755 index 00000000..dfcd6df1 --- /dev/null +++ b/devscripts/branch @@ -0,0 +1,52 @@ +#! /bin/sh + +if [ -z "$1" -o -n "$3" ]; then + echo "Usage: $0 branch [treeish]" >&2 + exit 1 +fi + +branch="$1" +treeish="$2" + +. `dirname $0`/split_tag.sh && +branch="$1" + +if [ -z "$treeish" ]; then + treeish="master" +fi + +split_tag "`git describe --abbrev=0 \"$treeish\"`" +prev_branch="$major.$minor" + +split_tag $branch +next_minor="`expr $minor + 1`" + +git checkout -b "$branch" "$treeish" && +echo " +version = '$major.$minor' +major = $major +minor = $minor +micro = 0 +release_level = 'branch' +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 setup.cfg setup.py docs/DeveloperGuide.rst && +git commit --message="Branch $branch" README.rst sqlobject/__version__.py docs/News.rst setup.cfg setup.py docs/DeveloperGuide.rst && + +if [ "$treeish" = master ]; then + git checkout master && echo " +version = '$major.$next_minor' +major = $major +minor = $next_minor +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 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 + +else + exec sed -i /"$prev_branch"/"$major.$next_minor"/ ANNOUNCE.rst +fi diff --git a/devscripts/build-all-docs b/devscripts/build-all-docs new file mode 100755 index 00000000..62020c9d --- /dev/null +++ b/devscripts/build-all-docs @@ -0,0 +1,18 @@ +#! /bin/sh + +build_docs() { + git checkout "$1" && + devscripts/build-docs && + rsync -ahPv --del --exclude=/robots.txt docs/html/ ../SQLObject-docs/"$2"/ +} + +cd "`dirname \"$0\"`" && +PROG_DIR="`pwd`" && + +cd .. && +build_docs 3.13.1 && +build_docs master devel && +rm -rf docs/html && + +cd ../SQLObject-docs && +exec sitemap_gen.py --config="$PROG_DIR"/sqlobject.org-sitemapconfig.xml diff --git a/devscripts/build-docs b/devscripts/build-docs new file mode 100755 index 00000000..dcd94d87 --- /dev/null +++ b/devscripts/build-docs @@ -0,0 +1,4 @@ +#! /bin/sh + +cd "`dirname \"$0\"`"/../docs && +exec ./rebuild diff --git a/devscripts/cleanup b/devscripts/cleanup new file mode 100755 index 00000000..77e348b3 --- /dev/null +++ b/devscripts/cleanup @@ -0,0 +1,5 @@ +#! /bin/sh + +cd "`dirname $0`"/../.. && +find . \( -name \*.orig -o -name \*.rej -o -name \*.tmp -o -name \*.log \) -type f -delete && +exec rm -f /tmp/test-sqlite.sqdb* diff --git a/devscripts/flake8/.gitignore b/devscripts/flake8/.gitignore new file mode 100644 index 00000000..d2eae243 --- /dev/null +++ b/devscripts/flake8/.gitignore @@ -0,0 +1,5 @@ +E* +F* +W* +all-results +sort-by-lines diff --git a/devscripts/flake8/run b/devscripts/flake8/run new file mode 100755 index 00000000..ee82ca24 --- /dev/null +++ b/devscripts/flake8/run @@ -0,0 +1,5 @@ +#! /bin/sh + +flake8 ../.. | sort >all-results && +awk '{print $2}' all-results | sort | uniq -c | + sort -k 1,1nr -k 2,2 >sort-by-lines diff --git a/devscripts/flake8/split b/devscripts/flake8/split new file mode 100755 index 00000000..2cdbfaca --- /dev/null +++ b/devscripts/flake8/split @@ -0,0 +1,5 @@ +#! /bin/sh + +while read _count code; do + grep -F " $code " all-results | sort -t : -k 1,1 -k 2,2nr -k 3,3nr >"$code" +done &2 + exit 1 +} + +get_current_branch() { + current_branch="`git branch | grep '^\*' | awk '{print $2}'`" +} + +if [ -n "$new_branch" ]; then + if [ -n "$old_branch" ]; then + if [ -n "$1" ]; then + usage + fi + else + get_current_branch + old_branch="$current_branch" + fi +elif [ -n "$old_branch" ]; then + if [ -n "$1" ]; then + usage + else + get_current_branch + new_branch="$current_branch" + fi +elif [ -n "$1" ]; then + if [ -n "$2" ]; then + if [ -n "$3" ]; then + usage + fi + old_branch="$1" + new_branch="$2" + else + usage + fi +else + usage +fi + +git checkout "$new_branch" && +exec git merge --strategy=ours "$old_branch" diff --git a/devscripts/postrelease b/devscripts/postrelease new file mode 100755 index 00000000..a799b553 --- /dev/null +++ b/devscripts/postrelease @@ -0,0 +1,9 @@ +#! /bin/sh + +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="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 new file mode 100755 index 00000000..7fe520db --- /dev/null +++ b/devscripts/prerelease @@ -0,0 +1,48 @@ +#! /bin/sh + +if [ -z "$1" -o -n "$3" ]; then + echo "Usage: $0 new_tag [branch]" >&2 + exit 1 +elif [ -z "$2" ]; then + tag="$1" +else + tag="$1" + branch="$2" +fi + +. `dirname $0`/split_tag.sh && +split_tag $tag $branch + +git checkout "$branch" && +echo " +version = '$tag' +major = $major +minor = $minor +micro = $micro +release_level = '$state' +serial = $serial +version_info = (major, minor, micro, release_level, serial)" > sqlobject/__version__.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 new file mode 100755 index 00000000..46ac39e7 --- /dev/null +++ b/devscripts/prerelease-tag @@ -0,0 +1,17 @@ +#! /bin/sh + +if [ -z "$1" -o -n "$3" ]; then + echo "Usage: $0 new_tag [branch]" >&2 + exit 1 +elif [ -z "$2" ]; then + tag="$1" +else + tag="$1" + branch="$2" +fi + +. `dirname $0`/split_tag.sh && +split_tag $tag $branch + +git checkout "$branch" && +exec git tag --message="Release $tag" --sign $tag diff --git a/devscripts/publish-docs b/devscripts/publish-docs new file mode 100755 index 00000000..f69df594 --- /dev/null +++ b/devscripts/publish-docs @@ -0,0 +1,5 @@ +#! /bin/sh + +cd "`dirname \"$0\"`"/../../SQLObject-docs && chmod -R a+rX . && +exec rsync -hlrtP4 --del . \ + web.sourceforge.net:/home/project-web/sqlobject/htdocs/ diff --git a/devscripts/push-all b/devscripts/push-all new file mode 100755 index 00000000..7d4020ca --- /dev/null +++ b/devscripts/push-all @@ -0,0 +1,5 @@ +#! /bin/sh + +git push --all sf && git push --tags sf && +git push --all gl && git push --tags gl && +git push --all gh && exec git push --tags gh diff --git a/devscripts/release b/devscripts/release new file mode 100755 index 00000000..44ad33a1 --- /dev/null +++ b/devscripts/release @@ -0,0 +1,29 @@ +#! /bin/sh + +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 && + +python setup.py build_py && +python setup.py build --executable '/usr/bin/env python' && +python setup.py sdist && + +find build -name '*.py[co]' -delete && +python setup.py bdist_wheel --universal && + +version=`python setup.py --version` +. devscripts/split_tag.sh && +split_tag $version + +if [ "$state" = final ]; then + 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 --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 new file mode 100644 index 00000000..02833990 --- /dev/null +++ b/devscripts/requirements/requirements.txt @@ -0,0 +1,8 @@ +# DateTime from Zope +DateTime + +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/requirements_docs.txt b/devscripts/requirements/requirements_dev.txt similarity index 63% rename from requirements_docs.txt rename to devscripts/requirements/requirements_dev.txt index 94d38eb8..c7dc3125 100644 --- a/requirements_docs.txt +++ b/devscripts/requirements/requirements_dev.txt @@ -1,3 +1,4 @@ -r requirements.txt -Sphinx +wheel +twine diff --git a/devscripts/requirements/requirements_docs.txt b/devscripts/requirements/requirements_docs.txt new file mode 100644 index 00000000..e36ba634 --- /dev/null +++ b/devscripts/requirements/requirements_docs.txt @@ -0,0 +1,4 @@ +-r requirements.txt + +Sphinx < 2.0; python_version == '2.7' or python_version == '3.4' +Sphinx; python_version >= '3.5' 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 new file mode 100644 index 00000000..c328e74a --- /dev/null +++ b/devscripts/requirements/requirements_tests.txt @@ -0,0 +1,9 @@ +-r requirements.txt + +setuptools +pytest < 5.0; python_version == '2.7' or python_version == '3.4' +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 new file mode 100644 index 00000000..b72a322f --- /dev/null +++ b/devscripts/requirements/requirements_tox.txt @@ -0,0 +1 @@ +tox >= 3.15, < 4 diff --git a/devscripts/sftp-frs b/devscripts/sftp-frs new file mode 100755 index 00000000..f7d664b5 --- /dev/null +++ b/devscripts/sftp-frs @@ -0,0 +1,2 @@ +#! /bin/sh +exec sftp frs.sourceforge.net:/home/frs/project/sqlobject diff --git a/devscripts/sftp-web b/devscripts/sftp-web new file mode 100755 index 00000000..e39ad5b2 --- /dev/null +++ b/devscripts/sftp-web @@ -0,0 +1,2 @@ +#! /bin/sh +exec sftp web.sourceforge.net:/home/project-web/sqlobject diff --git a/devscripts/split_tag.sh b/devscripts/split_tag.sh new file mode 100644 index 00000000..d0024f04 --- /dev/null +++ b/devscripts/split_tag.sh @@ -0,0 +1,22 @@ +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 /' -e 's/post\([0-9]\+\)/ post \1/'` + major=$1 + minor=$2 + micro=$3 + if [ -n "$4" ]; then + if [ "$4" = rc ]; then + state="release candidate" + else + state=$4 + fi + serial=$5 + else + state=final + serial=0 + fi + + if [ -z "$branch" ]; then + branch=$major.$minor + fi +} diff --git a/devscripts/sqlobject.org-sitemapconfig.xml b/devscripts/sqlobject.org-sitemapconfig.xml new file mode 100644 index 00000000..dc370559 --- /dev/null +++ b/devscripts/sqlobject.org-sitemapconfig.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/devscripts/test-split_tag.sh b/devscripts/test-split_tag.sh new file mode 100755 index 00000000..40b9d3d4 --- /dev/null +++ b/devscripts/test-split_tag.sh @@ -0,0 +1,33 @@ +#! /bin/sh + +. `dirname $0`/split_tag.sh && + +test_eq() { + if [ "$1" != "$2" ]; then + echo "$1" != "$2" >&2 + fi +} + +split_tag 21.12.42c4 +test_eq "$branch" 21.12 +test_eq "$major" 21 +test_eq "$minor" 12 +test_eq "$micro" 42 +test_eq "$state" "release candidate" +test_eq "$serial" 4 + +split_tag 21.12.42rc4 +test_eq "$branch" 21.12 +test_eq "$major" 21 +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 new file mode 100755 index 00000000..f1e9a817 --- /dev/null +++ b/devscripts/tox-select-envs @@ -0,0 +1,12 @@ +#! /bin/sh + +pattern="$1" +shift +envs="`tox --listenvs-all | grep -F $pattern | grep -v 'noauto\|w32' | sed 's/$/,/'`" + +if [ -n "$envs" ]; then + exec tox -e "$envs" "$@" +else + echo "No environments match $pattern" >&2 + exit 1 +fi 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 cf35dfc1..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 @@ -34,6 +33,15 @@ Contributions have been made by: * Gregor Horvath * Nathan Edwards * Lutz Steinborn +* 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 69699782..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 @@ -92,12 +92,12 @@ have any code. ``SOBoolCol`` has a method to create ``BoolValidator`` and methods to create backend-specific column type. ``BoolValidator`` has identical methods ``from_python`` and ``to_python``; the method passes ``None``, ``SQLExpression`` and bool values unchanged; int and -objects that have method ``__nonzero__`` are converted to bool; other -objects trigger validation error. Bool values that are returned by call -to ``from_python`` will be converted to SQL strings by -``BoolConverter``; bool values from ``to_python`` (is is supposed they -are originated from the backend via DB API driver) are passed to the -application. +objects that have method ``__nonzero__`` (``__bool__`` in Python 3) are +converted to bool; other objects trigger validation error. Bool values +that are returned by call to ``from_python`` will be converted to SQL +strings by ``BoolConverter``; bool values from ``to_python`` (is is +supposed they are originated from the backend via DB API driver) are +passed to the application. Objects that are returned from ``from_python`` must be registered with converters. Another approach for ``from_python`` is to return an object @@ -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 @@ -234,7 +240,7 @@ Tests are important. Tests keep everything from falling apart. All new additions should have tests. Testing uses pytest, an alternative to ``unittest``. It is available -at http://pytest.org/ and https://pypi.python.org/pypi/pytest. Read its +at http://pytest.org/ and https://pypi.org/project/pytest/. Read its `getting started`_ document for more. .. _getting started: http://docs.pytest.org/en/latest/getting-started.html @@ -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 c2669926..e01b1376 100644 --- a/docs/News.rst +++ b/docs/News.rst @@ -5,183 +5,293 @@ News .. contents:: Contents: :backlinks: none -.. _start: +SQLObject development (master) +============================== -SQLObject 3.3.0 (master) -======================== +SQLObject 3.13.1 +================ -SQLObject 3.2.0 -=============== +Released 2025 Dec 08. -Released 11 Mar 2017. +Bug fixes +--------- -Minor features --------------- +* ``UuidValidator.from_python()`` now accepts strings as a valid input. + This fixes #199. -* 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. +* 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. -* Remove ``driver`` keyword from RdbhostConnection as it allows one driver - ``rdbhdb``. +* Fixed #195: Minor ``NameError`` in ``pgconnection.py`` + when using ``psycopg`` version 1 with a non-default port. -* 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. +Tests +----- -* Add ``driver`` keyword for MySQLConnection. Allowed values are 'mysqldb', - 'connector', 'oursql' and 'pymysql'. Default is to test for mysqldb only. +* Tested with Python 3.14. -* Add support for `MySQL Connector - `_ (pure python; `binary - packages `_ are not at - PyPI and hence are hard to install and test). +* Run tests with source-only (non-binary) ``psycopg`` and ``psycopg2``. -* Add support for `oursql `_ MySQL - driver (only Python 2.6 and 2.7 until oursql author fixes Python 3 - compatibility). +SQLObject 3.13.0 +================ -* Add support for `PyMySQL `_ - pure - python mysql interface). +Released 2025 Mar 07. -* Add parameter ``timeout`` for MSSQLConnection (usable only with pymssql - driver); timeouts are in seconds. +Drivers +------- -* Remove deprecated ez_setup.py. +* Extended default list of MySQL drivers to ``mysqldb``, ``mysqlclient``, + ``mysql-connector``, ``mysql-connector-python``, ``pymysql``. -Drivers (work in progress) --------------------------- +* Extended default list of PostgreSQL drivers to ``psycopg``, ``psycopg2``, + ``pygresql``, ``pg8000``. -* Extend support for PyGreSQL driver. There are still some problems. +* Fixed outstanding problems with ``psycopg``. It's now the first class driver. -* Add support for `py-postgresql - `_ PostgreSQL driver. There - are still problems with the driver. +* Fixed all problems with ``pg8000``. It's now the first class driver. -* Add support for `pyfirebirdsql - `_.There are still problems with - the driver. +* Dropped support for ``CyMySQL``; + its author refused to fix unicode-related problems. -Bug fixes ---------- +* Dropped support for ``py-postgresql``; it's completely broken + with debianized ``Postgres`` and the authors reject fixes. -* Fix MSSQLConnection.columnsFromSchema: remove `(` and `)` from default - value. +Tests +----- -* Fix MSSQLConnection and SybaseConnection: insert default values into a table - with just one IDENTITY column. +* Added tests for ``mysqldb`` (aka ``mysql-python``) + and ``mysqlclient`` on w32. -* Remove excessive NULLs from ``CREATE TABLE`` for MSSQL/Sybase. +* Improved tests of ``mysql-connector`` and ``mysql-connector-python``. -* Fix concatenation operator for MSSQL/Sybase (it's ``+``, not ``||``). +CI +-- -* Fix MSSQLConnection.server_version() under Py3 (decode version to str). +* Tests(GHActions): Fixed old bugs in the workflow on w32. -Documentation -------------- +* Run tests with ``psycopg[c]``. + +SQLObject 3.12.0.post2 +====================== + +Released 2025 Feb 01. + +Installation/dependencies +------------------------- + +* Use ``FormEncode`` 2.1.1 for Python 3.13. -* The docs are now generated with Sphinx. +SQLObject 3.12.0 +================ -* Move ``docs/LICENSE`` to the top-level directory so that Github - recognizes it. +Released 2024 Dec 20. + +Drivers +------- + +* Add support for CyMySQL; there're some problems with unicode yet. + +* Separate ``psycopg`` and ``psycopg2``; + ``psycopg`` is actually ``psycopg3`` now; not all tests pass. + +* Minor fix in getting error code from PyGreSQL. + +* Dropped ``oursql``. It wasn't updated in years. + +* Dropped ``PySQLite2``. Only builtin ``sqlite3`` is supported. Tests ----- -* Rename ``py.test`` -> ``pytest`` in tests and docs. +* Run tests with Python 3.13. + +* Run tests with ``psycopg-c``; not all tests pass. + +* Fix ``test_exceptions.py`` under MariaDB, PostgreSQL and SQLite. -* Great Renaming: fix ``pytest`` warnings by renaming ``TestXXX`` classes - to ``SOTestXXX`` to prevent ``pytest`` to recognize them as test classes. +* ``py-postgres``: Set ``sslmode`` to ``allow``; + upstream changed default to ``prefer``. -* Fix ``pytest`` warnings by converting yield tests to plain calls: yield - tests were deprecated in ``pytest``. +CI +-- -* Tests are now run at CIs with Python 3.5. +* Run tests with ``PyGreSQL`` on w32, do not ignore errors. -* Drop ``Circle CI``. +* Skip tests with ``pg8000`` on w32. -* Run at Travis CI tests with Firebird backend (server version 2.5; - drivers fdb and firebirdsql). There are problems with tests. +* GHActions: Switch to ``setup-miniconda``. -* 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. +* GHActions: Python 3.13. -SQLObject 3.1.0 -=============== +SQLObject 3.11.0 +================ -Released 16 Aug 2016. +Released 2023 Nov 11. Features -------- -* Add UuidCol. +* 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. -* Add JsonbCol. Only for PostgreSQL. - Requires psycopg2 >= 2.5.4 and PostgreSQL >= 9.2. +* Test that ``idType`` is either ``int`` or ``str``. -* Add JSONCol, a universal json column. +* 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 -* For Python >= 3.4 minimal FormEncode version is now 1.3.1. +SQLObject 3.10.3 +================ -* If mxDateTime is in use, convert timedelta (returned by MySQL) to - mxDateTime.Time. +Released 2023 Oct 25. -Documentation -------------- +Bug fixes +--------- + +* 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 +------- + +* 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 +----- -* Developer's Guide is extended to explain SQLObject architecture - and how to create a new column type. +* Run tests with Python 3.12. -* Fix URLs that can be found; remove missing links. +CI +-- -* Rename reStructuredText files from \*.txt to \*.rst. +* GHActions: Ensure ``pip`` only if needed -Source code ------------ + 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. -* Fix all `import *` using https://github.com/zestyping/star-destroyer. +SQLObject 3.10.2 +================ + +Released 2023 Aug 09. + +Minor features +-------------- + +* Class ``Alias`` grows a method ``.select()`` to match ``SQLObject.select()``. + +Bug fixes +--------- + +* 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. + +CI +-- + +* Install all Python and PyPy versions from ``conda-forge``. + +SQLObject 3.10.1 +================ + +Released 2022 Dec 22. + +Minor features +-------------- + +* Use ``module_loader.exec_module(module_loader.create_module())`` + instead of ``module_loader.load_module()`` when available. + +Drivers +------- + +* Added ``mysql-connector-python``. Tests ----- -* Tests are now run at Circle CI. +* Run tests with Python 3.11. -* 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. +* 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.0.0 -=============== +SQLObject 3.10.0 +================ -Released 1 Jun 2016. +Released 2022 Sep 20. Features -------- -* Support for Python 2 and Python 3 with one codebase! - (Python version >= 3.4 currently required.) +* Allow connections in ``ConnectionHub`` to be strings. + This allows to open a new connection in every thread. -Minor features --------------- +* Add compatibility with ``Pendulum``. + +Tests +----- -* PyDispatcher (>=2.0.4) was made an external dependency. +* Run tests with Python 3.10. -Development ------------ +CI +-- -* Source code was made flake8-clean. +* GitHub Actions. + +* Stop testing at Travis CI. + +* Stop testing at AppVeyor. Documentation ------------- -* Documentation is published at http://sqlobject.readthedocs.org/ in - Sphinx format. +* DevGuide: source code must be pure ASCII. + +* DevGuide: ``reStructuredText`` format for docstrings is recommended. + +* DevGuide: de-facto good commit message format is required: + subject/body/trailers. + +* DevGuide: ``conventional commit`` format for commit message subject lines + is recommended. + +* DevGuide: ``Markdown`` format for commit message bodies is recommended. + +* DevGuide: commit messages must be pure ASCII. + `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/News1.rst b/docs/News1.rst index 77296c21..e3838c22 100644 --- a/docs/News1.rst +++ b/docs/News1.rst @@ -5,8 +5,6 @@ News .. contents:: Contents: :backlinks: none -.. _start: - SQLObject 0.6.1 =============== diff --git a/docs/News2.rst b/docs/News2.rst index 9a37d77b..2e23ed40 100644 --- a/docs/News2.rst +++ b/docs/News2.rst @@ -5,8 +5,6 @@ News .. contents:: Contents: :backlinks: none -.. _start: - SQLObject 0.8.7 =============== diff --git a/docs/News3.rst b/docs/News3.rst index a6805270..2b6f0672 100644 --- a/docs/News3.rst +++ b/docs/News3.rst @@ -5,8 +5,6 @@ News .. contents:: Contents: :backlinks: none -.. _start: - SQLObject 0.10.9 ================ diff --git a/docs/News4.rst b/docs/News4.rst index 11f76d42..546a7fd4 100644 --- a/docs/News4.rst +++ b/docs/News4.rst @@ -5,8 +5,6 @@ News .. contents:: Contents: :backlinks: none -.. _start: - SQLObject 0.15.1 ================ diff --git a/docs/News5.rst b/docs/News5.rst index 6f1118ce..32f6659c 100644 --- a/docs/News5.rst +++ b/docs/News5.rst @@ -5,8 +5,6 @@ News .. contents:: Contents: :backlinks: none -.. _start: - SQLObject 2.2.1 =============== 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/Python3.rst b/docs/Python3.rst index f14b106a..ad4fb56a 100644 --- a/docs/Python3.rst +++ b/docs/Python3.rst @@ -40,7 +40,7 @@ Note that the default encoding of MySQL databases is *latin1*, which can cause problems with general Unicode strings. We recommend specifying the character set as *utf8* when using MySQL to protect against these issues. -.. _mysqlclient: https://pypi.python.org/pypi/mysqlclient +.. _mysqlclient: https://pypi.org/project/mysqlclient/ Using databases created with SQLObject and Python 2 in Python 3 diff --git a/docs/SQLObject.rst b/docs/SQLObject.rst index 003172ed..1dd4dcd7 100644 --- a/docs/SQLObject.rst +++ b/docs/SQLObject.rst @@ -46,31 +46,39 @@ used with the same query syntax. Requirements ============ -Currently SQLObject supports MySQL_ via MySQLdb_ aka MySQL-python (called -mysqlclient_ for Python 3), `MySQL Connector`_, oursql_ and PyMySQL_. For -PostgreSQL_ psycopg2_ or psycopg1 are recommended; PyGreSQL_ and py-postgresql_ -are supported but have problems (not all tests passed). 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). +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.python.org/pypi/mysqlclient -.. _`MySQL Connector`: https://pypi.python.org/pypi/mysql-connector -.. _oursql: https://github.com/python-oursql/oursql -.. _PyMySQL: https://github.com/PyMySQL/PyMySQL/ +.. _mysqlclient: https://pypi.org/project/mysqlclient/ +.. _`MySQL Connector`: https://pypi.org/project/mysql-connector/ +.. _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.python.org/pypi/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/ -.. _pyfirebirdsql: https://pypi.python.org/pypi/firebirdsql +.. _pyfirebirdsql: https://pypi.org/project/firebirdsql/ .. _`MAX DB`: http://maxdb.sap.com/ .. _sapdb: http://maxdb.sap.com/doc/7_8/50/01923f25b842438a408805774f6989/frameset.htm .. _Sybase: http://www.object-craft.com.au/projects/sybase/ @@ -78,8 +86,10 @@ via pymssql_ (+ FreeTDS_) or adodbapi_ (Win32). .. _pymssql: http://www.pymssql.org/en/latest/index.html .. _FreeTDS: http://www.freetds.org/ .. _adodbapi: http://adodbapi.sourceforge.net/ +.. _PyODBC: https://pypi.org/project/pyodbc/ +.. _PyPyODBC: https://pypi.org/project/pypyodbc/ -Python 2.6, 2.7 or 3.4+ is required. +Python 2.7 or 3.4+ is required. Compared To Other Database Wrappers =================================== @@ -256,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 @@ -296,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 @@ -453,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. @@ -538,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:: @@ -576,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 @@ -757,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 @@ -1238,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; @@ -1348,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 + @@ -1781,8 +1800,11 @@ 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``; 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. Example:: @@ -1825,9 +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``, psycopg1, -``psycopg`` (tries psycopg2 and psycopg1), ``pygresql``, ``pygresql`` -or ``pypostgresql``. 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``. @@ -1846,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 b2d3f9de..0d2c9423 100644 --- a/docs/TODO.rst +++ b/docs/TODO.rst @@ -1,6 +1,23 @@ TODO ---- +* Fix unicode problems with pyodbc. + +* Resolve timeout problems with MSSQL. + +* PyPy. + +* Use https://pypi.org/project/psycopg2cffi/ to run SQLObject + under PyPy. + +* https://pypi.org/project/turbodbc/ + +* PyODBC and PyPyODBC for linux and w32: SQLite (libsqliteodbc). + +* https://pypi.org/project/JayDeBeApi/ + +* Jython. + * Quote table/column names that are reserved keywords (order => "order", values => `values` for MySQL). @@ -73,7 +90,7 @@ TODO * Support PyODBC driver for all backends. -* `dbms `_ is a DB API wrapper for DB +* `dbms `_ is a DB API wrapper for DB API drivers for IBM DB2, Firebird, MSSQL Server, MySQL, Oracle, PostgreSQL, SQLite and ODBC. @@ -90,7 +107,7 @@ TODO * Use DBUtils_, especially SolidConnection. -.. _DBUtils: https://pypi.python.org/pypi/DBUtils +.. _DBUtils: https://pypi.org/project/DBUtils/ * ``_fromDatabase`` currently doesn't support IDs that don't fit into the normal naming scheme. It should do so. You can still use ``_idName`` diff --git a/docs/api/sqlobject.include.tests.test_hashcol.rst b/docs/api/sqlobject.include.tests.test_hashcol.rst index 99c80d9e..5289acd7 100644 --- a/docs/api/sqlobject.include.tests.test_hashcol.rst +++ b/docs/api/sqlobject.include.tests.test_hashcol.rst @@ -1,5 +1,5 @@ -sqlobject.include.tests.test_hashcol module -=========================================== +sqlobject.include.tests.test\_hashcol module +============================================ .. automodule:: sqlobject.include.tests.test_hashcol :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_aggregates.rst b/docs/api/sqlobject.inheritance.tests.test_aggregates.rst index ee01f6b6..58a2944d 100644 --- a/docs/api/sqlobject.inheritance.tests.test_aggregates.rst +++ b/docs/api/sqlobject.inheritance.tests.test_aggregates.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_aggregates module -================================================== +sqlobject.inheritance.tests.test\_aggregates module +=================================================== .. automodule:: sqlobject.inheritance.tests.test_aggregates :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_asdict.rst b/docs/api/sqlobject.inheritance.tests.test_asdict.rst index ff95591f..6a33dce2 100644 --- a/docs/api/sqlobject.inheritance.tests.test_asdict.rst +++ b/docs/api/sqlobject.inheritance.tests.test_asdict.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_asdict module -============================================== +sqlobject.inheritance.tests.test\_asdict module +=============================================== .. automodule:: sqlobject.inheritance.tests.test_asdict :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst b/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst index ed4bf269..5447d0bd 100644 --- a/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst +++ b/docs/api/sqlobject.inheritance.tests.test_deep_inheritance.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_deep_inheritance module -======================================================== +sqlobject.inheritance.tests.test\_deep\_inheritance module +========================================================== .. automodule:: sqlobject.inheritance.tests.test_deep_inheritance :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst b/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst index 3c5e96dc..8b9b7069 100644 --- a/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst +++ b/docs/api/sqlobject.inheritance.tests.test_destroy_cascade.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_destroy_cascade module -======================================================= +sqlobject.inheritance.tests.test\_destroy\_cascade module +========================================================= .. automodule:: sqlobject.inheritance.tests.test_destroy_cascade :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst b/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst index e3f09426..68059887 100644 --- a/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst +++ b/docs/api/sqlobject.inheritance.tests.test_foreignKey.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_foreignKey module -================================================== +sqlobject.inheritance.tests.test\_foreignKey module +=================================================== .. automodule:: sqlobject.inheritance.tests.test_foreignKey :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_indexes.rst b/docs/api/sqlobject.inheritance.tests.test_indexes.rst index b2e8d5e6..cda20165 100644 --- a/docs/api/sqlobject.inheritance.tests.test_indexes.rst +++ b/docs/api/sqlobject.inheritance.tests.test_indexes.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_indexes module -=============================================== +sqlobject.inheritance.tests.test\_indexes module +================================================ .. automodule:: sqlobject.inheritance.tests.test_indexes :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_inheritance.rst b/docs/api/sqlobject.inheritance.tests.test_inheritance.rst index a0c8e080..7af4fc40 100644 --- a/docs/api/sqlobject.inheritance.tests.test_inheritance.rst +++ b/docs/api/sqlobject.inheritance.tests.test_inheritance.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_inheritance module -=================================================== +sqlobject.inheritance.tests.test\_inheritance module +==================================================== .. automodule:: sqlobject.inheritance.tests.test_inheritance :members: diff --git a/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst b/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst index b9224cca..31cea2e6 100644 --- a/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst +++ b/docs/api/sqlobject.inheritance.tests.test_inheritance_tree.rst @@ -1,5 +1,5 @@ -sqlobject.inheritance.tests.test_inheritance_tree module -======================================================== +sqlobject.inheritance.tests.test\_inheritance\_tree module +========================================================== .. automodule:: sqlobject.inheritance.tests.test_inheritance_tree :members: diff --git a/docs/api/sqlobject.rdbhost.rdbhostconnection.rst b/docs/api/sqlobject.rdbhost.rdbhostconnection.rst deleted file mode 100644 index 3f8e7379..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 19f0020d..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.tests.rst b/docs/api/sqlobject.tests.rst index 98f38cd6..fcab4e81 100644 --- a/docs/api/sqlobject.tests.rst +++ b/docs/api/sqlobject.tests.rst @@ -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,11 +30,13 @@ 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 sqlobject.tests.test_create_drop sqlobject.tests.test_csvexport + sqlobject.tests.test_csvimport sqlobject.tests.test_cyclic_reference sqlobject.tests.test_datetime sqlobject.tests.test_decimal diff --git a/docs/api/sqlobject.tests.test_ForeignKey.rst b/docs/api/sqlobject.tests.test_ForeignKey.rst index a4a9dc33..c495f8b9 100644 --- a/docs/api/sqlobject.tests.test_ForeignKey.rst +++ b/docs/api/sqlobject.tests.test_ForeignKey.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_ForeignKey module -====================================== +sqlobject.tests.test\_ForeignKey module +======================================= .. automodule:: sqlobject.tests.test_ForeignKey :members: 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 9e0a01e2..35ee6435 100644 --- a/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst +++ b/docs/api/sqlobject.tests.test_NoneValuedResultItem.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_NoneValuedResultItem module -================================================ +sqlobject.tests.test\_NoneValuedResultItem module +================================================= .. automodule:: sqlobject.tests.test_NoneValuedResultItem :members: diff --git a/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst b/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst index c7e1ee1b..7ae46d52 100644 --- a/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst +++ b/docs/api/sqlobject.tests.test_SQLMultipleJoin.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_SQLMultipleJoin module -=========================================== +sqlobject.tests.test\_SQLMultipleJoin module +============================================ .. automodule:: sqlobject.tests.test_SQLMultipleJoin :members: diff --git a/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst b/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst index ffd4f028..e12f1f55 100644 --- a/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst +++ b/docs/api/sqlobject.tests.test_SQLRelatedJoin.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_SQLRelatedJoin module -========================================== +sqlobject.tests.test\_SQLRelatedJoin module +=========================================== .. automodule:: sqlobject.tests.test_SQLRelatedJoin :members: diff --git a/docs/api/sqlobject.tests.test_SingleJoin.rst b/docs/api/sqlobject.tests.test_SingleJoin.rst index 2f0f52a3..80f8907c 100644 --- a/docs/api/sqlobject.tests.test_SingleJoin.rst +++ b/docs/api/sqlobject.tests.test_SingleJoin.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_SingleJoin module -====================================== +sqlobject.tests.test\_SingleJoin module +======================================= .. automodule:: sqlobject.tests.test_SingleJoin :members: diff --git a/docs/api/sqlobject.tests.test_aggregates.rst b/docs/api/sqlobject.tests.test_aggregates.rst index c64faf15..283cea6f 100644 --- a/docs/api/sqlobject.tests.test_aggregates.rst +++ b/docs/api/sqlobject.tests.test_aggregates.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_aggregates module -====================================== +sqlobject.tests.test\_aggregates module +======================================= .. automodule:: sqlobject.tests.test_aggregates :members: diff --git a/docs/api/sqlobject.tests.test_aliases.rst b/docs/api/sqlobject.tests.test_aliases.rst index c883b8c1..fa46a461 100644 --- a/docs/api/sqlobject.tests.test_aliases.rst +++ b/docs/api/sqlobject.tests.test_aliases.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_aliases module -=================================== +sqlobject.tests.test\_aliases module +==================================== .. automodule:: sqlobject.tests.test_aliases :members: diff --git a/docs/api/sqlobject.tests.test_asdict.rst b/docs/api/sqlobject.tests.test_asdict.rst index 40e4c364..e1550441 100644 --- a/docs/api/sqlobject.tests.test_asdict.rst +++ b/docs/api/sqlobject.tests.test_asdict.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_asdict module -================================== +sqlobject.tests.test\_asdict module +=================================== .. automodule:: sqlobject.tests.test_asdict :members: diff --git a/docs/api/sqlobject.tests.test_auto.rst b/docs/api/sqlobject.tests.test_auto.rst index 0ea2f524..6d578d5e 100644 --- a/docs/api/sqlobject.tests.test_auto.rst +++ b/docs/api/sqlobject.tests.test_auto.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_auto module -================================ +sqlobject.tests.test\_auto module +================================= .. automodule:: sqlobject.tests.test_auto :members: diff --git a/docs/api/sqlobject.tests.test_basic.rst b/docs/api/sqlobject.tests.test_basic.rst index fabffe60..40235ecf 100644 --- a/docs/api/sqlobject.tests.test_basic.rst +++ b/docs/api/sqlobject.tests.test_basic.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_basic module -================================= +sqlobject.tests.test\_basic module +================================== .. automodule:: sqlobject.tests.test_basic :members: diff --git a/docs/api/sqlobject.tests.test_blob.rst b/docs/api/sqlobject.tests.test_blob.rst index e3c67850..1d01d512 100644 --- a/docs/api/sqlobject.tests.test_blob.rst +++ b/docs/api/sqlobject.tests.test_blob.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_blob module -================================ +sqlobject.tests.test\_blob module +================================= .. automodule:: sqlobject.tests.test_blob :members: diff --git a/docs/api/sqlobject.tests.test_boundattributes.rst b/docs/api/sqlobject.tests.test_boundattributes.rst index a648d71c..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 dd344907..69f05823 100644 --- a/docs/api/sqlobject.tests.test_cache.rst +++ b/docs/api/sqlobject.tests.test_cache.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_cache module -================================= +sqlobject.tests.test\_cache module +================================== .. automodule:: sqlobject.tests.test_cache :members: diff --git a/docs/api/sqlobject.tests.test_class_hash.rst b/docs/api/sqlobject.tests.test_class_hash.rst index 17fc1371..f25b66b6 100644 --- a/docs/api/sqlobject.tests.test_class_hash.rst +++ b/docs/api/sqlobject.tests.test_class_hash.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_class_hash module -====================================== +sqlobject.tests.test\_class\_hash module +======================================== .. automodule:: sqlobject.tests.test_class_hash :members: diff --git a/docs/api/sqlobject.tests.test_columns_order.rst b/docs/api/sqlobject.tests.test_columns_order.rst index 12c5b619..c48cf7c0 100644 --- a/docs/api/sqlobject.tests.test_columns_order.rst +++ b/docs/api/sqlobject.tests.test_columns_order.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_columns_order module -========================================= +sqlobject.tests.test\_columns\_order module +=========================================== .. automodule:: sqlobject.tests.test_columns_order :members: diff --git a/docs/api/sqlobject.tests.test_combining_joins.rst b/docs/api/sqlobject.tests.test_combining_joins.rst index 7ab4faa3..e1961a80 100644 --- a/docs/api/sqlobject.tests.test_combining_joins.rst +++ b/docs/api/sqlobject.tests.test_combining_joins.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_combining_joins module -=========================================== +sqlobject.tests.test\_combining\_joins module +============================================= .. automodule:: sqlobject.tests.test_combining_joins :members: diff --git a/docs/api/sqlobject.tests.test_comparison.rst b/docs/api/sqlobject.tests.test_comparison.rst index f44f5ffc..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 a2f9eb8b..2321e963 100644 --- a/docs/api/sqlobject.tests.test_complex_sorting.rst +++ b/docs/api/sqlobject.tests.test_complex_sorting.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_complex_sorting module -=========================================== +sqlobject.tests.test\_complex\_sorting module +============================================= .. automodule:: sqlobject.tests.test_complex_sorting :members: diff --git a/docs/api/sqlobject.tests.test_constraints.rst b/docs/api/sqlobject.tests.test_constraints.rst index 6ddd2f69..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 fd05137e..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 173f9608..fcd3eea5 100644 --- a/docs/api/sqlobject.tests.test_create_drop.rst +++ b/docs/api/sqlobject.tests.test_create_drop.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_create_drop module -======================================= +sqlobject.tests.test\_create\_drop module +========================================= .. automodule:: sqlobject.tests.test_create_drop :members: diff --git a/docs/api/sqlobject.tests.test_csvexport.rst b/docs/api/sqlobject.tests.test_csvexport.rst index 26b28675..0927f4f0 100644 --- a/docs/api/sqlobject.tests.test_csvexport.rst +++ b/docs/api/sqlobject.tests.test_csvexport.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_csvexport module -===================================== +sqlobject.tests.test\_csvexport module +====================================== .. automodule:: sqlobject.tests.test_csvexport :members: diff --git a/docs/api/sqlobject.tests.test_csvimport.rst b/docs/api/sqlobject.tests.test_csvimport.rst new file mode 100644 index 00000000..f7db4138 --- /dev/null +++ b/docs/api/sqlobject.tests.test_csvimport.rst @@ -0,0 +1,7 @@ +sqlobject.tests.test\_csvimport module +====================================== + +.. automodule:: sqlobject.tests.test_csvimport + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/sqlobject.tests.test_cyclic_reference.rst b/docs/api/sqlobject.tests.test_cyclic_reference.rst index 21ecd08d..910421ab 100644 --- a/docs/api/sqlobject.tests.test_cyclic_reference.rst +++ b/docs/api/sqlobject.tests.test_cyclic_reference.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_cyclic_reference module -============================================ +sqlobject.tests.test\_cyclic\_reference module +============================================== .. automodule:: sqlobject.tests.test_cyclic_reference :members: diff --git a/docs/api/sqlobject.tests.test_datetime.rst b/docs/api/sqlobject.tests.test_datetime.rst index e2198966..87a1b043 100644 --- a/docs/api/sqlobject.tests.test_datetime.rst +++ b/docs/api/sqlobject.tests.test_datetime.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_datetime module -==================================== +sqlobject.tests.test\_datetime module +===================================== .. automodule:: sqlobject.tests.test_datetime :members: diff --git a/docs/api/sqlobject.tests.test_decimal.rst b/docs/api/sqlobject.tests.test_decimal.rst index 7752caaf..86868120 100644 --- a/docs/api/sqlobject.tests.test_decimal.rst +++ b/docs/api/sqlobject.tests.test_decimal.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_decimal module -=================================== +sqlobject.tests.test\_decimal module +==================================== .. automodule:: sqlobject.tests.test_decimal :members: diff --git a/docs/api/sqlobject.tests.test_declarative.rst b/docs/api/sqlobject.tests.test_declarative.rst index 2c5c2c23..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 8245134b..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 65f95c8b..508cc86a 100644 --- a/docs/api/sqlobject.tests.test_delete.rst +++ b/docs/api/sqlobject.tests.test_delete.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_delete module -================================== +sqlobject.tests.test\_delete module +=================================== .. automodule:: sqlobject.tests.test_delete :members: diff --git a/docs/api/sqlobject.tests.test_distinct.rst b/docs/api/sqlobject.tests.test_distinct.rst index c7ab42ca..68080aa3 100644 --- a/docs/api/sqlobject.tests.test_distinct.rst +++ b/docs/api/sqlobject.tests.test_distinct.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_distinct module -==================================== +sqlobject.tests.test\_distinct module +===================================== .. automodule:: sqlobject.tests.test_distinct :members: diff --git a/docs/api/sqlobject.tests.test_empty.rst b/docs/api/sqlobject.tests.test_empty.rst index d10bc015..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 11f97770..0ae9e483 100644 --- a/docs/api/sqlobject.tests.test_enum.rst +++ b/docs/api/sqlobject.tests.test_enum.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_enum module -================================ +sqlobject.tests.test\_enum module +================================= .. automodule:: sqlobject.tests.test_enum :members: diff --git a/docs/api/sqlobject.tests.test_events.rst b/docs/api/sqlobject.tests.test_events.rst index d6c08997..7c1b9ef8 100644 --- a/docs/api/sqlobject.tests.test_events.rst +++ b/docs/api/sqlobject.tests.test_events.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_events module -================================== +sqlobject.tests.test\_events module +=================================== .. automodule:: sqlobject.tests.test_events :members: diff --git a/docs/api/sqlobject.tests.test_exceptions.rst b/docs/api/sqlobject.tests.test_exceptions.rst index 6c92b729..5e440e43 100644 --- a/docs/api/sqlobject.tests.test_exceptions.rst +++ b/docs/api/sqlobject.tests.test_exceptions.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_exceptions module -====================================== +sqlobject.tests.test\_exceptions module +======================================= .. automodule:: sqlobject.tests.test_exceptions :members: diff --git a/docs/api/sqlobject.tests.test_expire.rst b/docs/api/sqlobject.tests.test_expire.rst index e2d56ea9..614f06ce 100644 --- a/docs/api/sqlobject.tests.test_expire.rst +++ b/docs/api/sqlobject.tests.test_expire.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_expire module -================================== +sqlobject.tests.test\_expire module +=================================== .. automodule:: sqlobject.tests.test_expire :members: diff --git a/docs/api/sqlobject.tests.test_groupBy.rst b/docs/api/sqlobject.tests.test_groupBy.rst index fd8cc9ff..c8f6cc92 100644 --- a/docs/api/sqlobject.tests.test_groupBy.rst +++ b/docs/api/sqlobject.tests.test_groupBy.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_groupBy module -=================================== +sqlobject.tests.test\_groupBy module +==================================== .. automodule:: sqlobject.tests.test_groupBy :members: diff --git a/docs/api/sqlobject.tests.test_identity.rst b/docs/api/sqlobject.tests.test_identity.rst index 8e8537c4..63716dfb 100644 --- a/docs/api/sqlobject.tests.test_identity.rst +++ b/docs/api/sqlobject.tests.test_identity.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_identity module -==================================== +sqlobject.tests.test\_identity module +===================================== .. automodule:: sqlobject.tests.test_identity :members: diff --git a/docs/api/sqlobject.tests.test_indexes.rst b/docs/api/sqlobject.tests.test_indexes.rst index f862ac8d..023d8fdb 100644 --- a/docs/api/sqlobject.tests.test_indexes.rst +++ b/docs/api/sqlobject.tests.test_indexes.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_indexes module -=================================== +sqlobject.tests.test\_indexes module +==================================== .. automodule:: sqlobject.tests.test_indexes :members: diff --git a/docs/api/sqlobject.tests.test_inheritance.rst b/docs/api/sqlobject.tests.test_inheritance.rst index 2913d3a6..a381ee17 100644 --- a/docs/api/sqlobject.tests.test_inheritance.rst +++ b/docs/api/sqlobject.tests.test_inheritance.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_inheritance module -======================================= +sqlobject.tests.test\_inheritance module +======================================== .. automodule:: sqlobject.tests.test_inheritance :members: diff --git a/docs/api/sqlobject.tests.test_joins.rst b/docs/api/sqlobject.tests.test_joins.rst index 08f486eb..a0679733 100644 --- a/docs/api/sqlobject.tests.test_joins.rst +++ b/docs/api/sqlobject.tests.test_joins.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_joins module -================================= +sqlobject.tests.test\_joins module +================================== .. automodule:: sqlobject.tests.test_joins :members: diff --git a/docs/api/sqlobject.tests.test_joins_conditional.rst b/docs/api/sqlobject.tests.test_joins_conditional.rst index a347d30c..ca6fd0b5 100644 --- a/docs/api/sqlobject.tests.test_joins_conditional.rst +++ b/docs/api/sqlobject.tests.test_joins_conditional.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_joins_conditional module -============================================= +sqlobject.tests.test\_joins\_conditional module +=============================================== .. automodule:: sqlobject.tests.test_joins_conditional :members: diff --git a/docs/api/sqlobject.tests.test_jsonbcol.rst b/docs/api/sqlobject.tests.test_jsonbcol.rst index ef8a6b86..30f0d54a 100644 --- a/docs/api/sqlobject.tests.test_jsonbcol.rst +++ b/docs/api/sqlobject.tests.test_jsonbcol.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_jsonbcol module -==================================== +sqlobject.tests.test\_jsonbcol module +===================================== .. automodule:: sqlobject.tests.test_jsonbcol :members: diff --git a/docs/api/sqlobject.tests.test_jsoncol.rst b/docs/api/sqlobject.tests.test_jsoncol.rst index 93ba6fb4..bc78a692 100644 --- a/docs/api/sqlobject.tests.test_jsoncol.rst +++ b/docs/api/sqlobject.tests.test_jsoncol.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_jsoncol module -=================================== +sqlobject.tests.test\_jsoncol module +==================================== .. automodule:: sqlobject.tests.test_jsoncol :members: diff --git a/docs/api/sqlobject.tests.test_lazy.rst b/docs/api/sqlobject.tests.test_lazy.rst index 317b3650..98994362 100644 --- a/docs/api/sqlobject.tests.test_lazy.rst +++ b/docs/api/sqlobject.tests.test_lazy.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_lazy module -================================ +sqlobject.tests.test\_lazy module +================================= .. automodule:: sqlobject.tests.test_lazy :members: diff --git a/docs/api/sqlobject.tests.test_md5.rst b/docs/api/sqlobject.tests.test_md5.rst index d71f26b0..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 881f36db..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 abff0b3f..7edf06aa 100644 --- a/docs/api/sqlobject.tests.test_new_joins.rst +++ b/docs/api/sqlobject.tests.test_new_joins.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_new_joins module -===================================== +sqlobject.tests.test\_new\_joins module +======================================= .. automodule:: sqlobject.tests.test_new_joins :members: diff --git a/docs/api/sqlobject.tests.test_parse_uri.rst b/docs/api/sqlobject.tests.test_parse_uri.rst index 8d33ae5b..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 64607769..dc0fb590 100644 --- a/docs/api/sqlobject.tests.test_paste.rst +++ b/docs/api/sqlobject.tests.test_paste.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_paste module -================================= +sqlobject.tests.test\_paste module +================================== .. automodule:: sqlobject.tests.test_paste :members: diff --git a/docs/api/sqlobject.tests.test_perConnection.rst b/docs/api/sqlobject.tests.test_perConnection.rst index d36711a0..7d4b1781 100644 --- a/docs/api/sqlobject.tests.test_perConnection.rst +++ b/docs/api/sqlobject.tests.test_perConnection.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_perConnection module -========================================= +sqlobject.tests.test\_perConnection module +========================================== .. automodule:: sqlobject.tests.test_perConnection :members: diff --git a/docs/api/sqlobject.tests.test_pickle.rst b/docs/api/sqlobject.tests.test_pickle.rst index a1540275..c44fa27d 100644 --- a/docs/api/sqlobject.tests.test_pickle.rst +++ b/docs/api/sqlobject.tests.test_pickle.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_pickle module -================================== +sqlobject.tests.test\_pickle module +=================================== .. automodule:: sqlobject.tests.test_pickle :members: diff --git a/docs/api/sqlobject.tests.test_picklecol.rst b/docs/api/sqlobject.tests.test_picklecol.rst index be2c174f..7edd7c08 100644 --- a/docs/api/sqlobject.tests.test_picklecol.rst +++ b/docs/api/sqlobject.tests.test_picklecol.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_picklecol module -===================================== +sqlobject.tests.test\_picklecol module +====================================== .. automodule:: sqlobject.tests.test_picklecol :members: diff --git a/docs/api/sqlobject.tests.test_postgres.rst b/docs/api/sqlobject.tests.test_postgres.rst index 393803db..d8082b5c 100644 --- a/docs/api/sqlobject.tests.test_postgres.rst +++ b/docs/api/sqlobject.tests.test_postgres.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_postgres module -==================================== +sqlobject.tests.test\_postgres module +===================================== .. automodule:: sqlobject.tests.test_postgres :members: diff --git a/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst b/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst index 60a316e9..8c044b55 100644 --- a/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst +++ b/docs/api/sqlobject.tests.test_reparent_sqlmeta.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_reparent_sqlmeta module -============================================ +sqlobject.tests.test\_reparent\_sqlmeta module +============================================== .. automodule:: sqlobject.tests.test_reparent_sqlmeta :members: diff --git a/docs/api/sqlobject.tests.test_schema.rst b/docs/api/sqlobject.tests.test_schema.rst index b5a5c6bc..9cf3457f 100644 --- a/docs/api/sqlobject.tests.test_schema.rst +++ b/docs/api/sqlobject.tests.test_schema.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_schema module -================================== +sqlobject.tests.test\_schema module +=================================== .. automodule:: sqlobject.tests.test_schema :members: diff --git a/docs/api/sqlobject.tests.test_select.rst b/docs/api/sqlobject.tests.test_select.rst index f3bda2ac..9e63811b 100644 --- a/docs/api/sqlobject.tests.test_select.rst +++ b/docs/api/sqlobject.tests.test_select.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_select module -================================== +sqlobject.tests.test\_select module +=================================== .. automodule:: sqlobject.tests.test_select :members: diff --git a/docs/api/sqlobject.tests.test_select_through.rst b/docs/api/sqlobject.tests.test_select_through.rst index 8a1c7a92..f0833e2f 100644 --- a/docs/api/sqlobject.tests.test_select_through.rst +++ b/docs/api/sqlobject.tests.test_select_through.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_select_through module -========================================== +sqlobject.tests.test\_select\_through module +============================================ .. automodule:: sqlobject.tests.test_select_through :members: diff --git a/docs/api/sqlobject.tests.test_setters.rst b/docs/api/sqlobject.tests.test_setters.rst index 110c04d3..f35cfb0e 100644 --- a/docs/api/sqlobject.tests.test_setters.rst +++ b/docs/api/sqlobject.tests.test_setters.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_setters module -=================================== +sqlobject.tests.test\_setters module +==================================== .. automodule:: sqlobject.tests.test_setters :members: diff --git a/docs/api/sqlobject.tests.test_slice.rst b/docs/api/sqlobject.tests.test_slice.rst index 7486e203..f2819a1b 100644 --- a/docs/api/sqlobject.tests.test_slice.rst +++ b/docs/api/sqlobject.tests.test_slice.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_slice module -================================= +sqlobject.tests.test\_slice module +================================== .. automodule:: sqlobject.tests.test_slice :members: diff --git a/docs/api/sqlobject.tests.test_sorting.rst b/docs/api/sqlobject.tests.test_sorting.rst index f15e9fea..a461b701 100644 --- a/docs/api/sqlobject.tests.test_sorting.rst +++ b/docs/api/sqlobject.tests.test_sorting.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sorting module -=================================== +sqlobject.tests.test\_sorting module +==================================== .. automodule:: sqlobject.tests.test_sorting :members: diff --git a/docs/api/sqlobject.tests.test_sqlbuilder.rst b/docs/api/sqlobject.tests.test_sqlbuilder.rst index 18f50c13..f71230de 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sqlbuilder module -====================================== +sqlobject.tests.test\_sqlbuilder module +======================================= .. automodule:: sqlobject.tests.test_sqlbuilder :members: diff --git a/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst b/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst index 27b8b45a..47c965c9 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder_dbspecific.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sqlbuilder_dbspecific module -================================================= +sqlobject.tests.test\_sqlbuilder\_dbspecific module +=================================================== .. automodule:: sqlobject.tests.test_sqlbuilder_dbspecific :members: diff --git a/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst b/docs/api/sqlobject.tests.test_sqlbuilder_importproxy.rst index 45d83c04..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 0789abd1..ac24093e 100644 --- a/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst +++ b/docs/api/sqlobject.tests.test_sqlbuilder_joins_instances.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sqlbuilder_joins_instances module -====================================================== +sqlobject.tests.test\_sqlbuilder\_joins\_instances module +========================================================= .. automodule:: sqlobject.tests.test_sqlbuilder_joins_instances :members: diff --git a/docs/api/sqlobject.tests.test_sqlite.rst b/docs/api/sqlobject.tests.test_sqlite.rst index d6004569..7ef87923 100644 --- a/docs/api/sqlobject.tests.test_sqlite.rst +++ b/docs/api/sqlobject.tests.test_sqlite.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sqlite module -================================== +sqlobject.tests.test\_sqlite module +=================================== .. automodule:: sqlobject.tests.test_sqlite :members: diff --git a/docs/api/sqlobject.tests.test_sqlmeta_idName.rst b/docs/api/sqlobject.tests.test_sqlmeta_idName.rst index c051696c..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 71d3114a..517d03ed 100644 --- a/docs/api/sqlobject.tests.test_sqlobject_admin.rst +++ b/docs/api/sqlobject.tests.test_sqlobject_admin.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_sqlobject_admin module -=========================================== +sqlobject.tests.test\_sqlobject\_admin module +============================================= .. automodule:: sqlobject.tests.test_sqlobject_admin :members: diff --git a/docs/api/sqlobject.tests.test_string_id.rst b/docs/api/sqlobject.tests.test_string_id.rst index 70116477..28be7463 100644 --- a/docs/api/sqlobject.tests.test_string_id.rst +++ b/docs/api/sqlobject.tests.test_string_id.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_string_id module -===================================== +sqlobject.tests.test\_string\_id module +======================================= .. automodule:: sqlobject.tests.test_string_id :members: diff --git a/docs/api/sqlobject.tests.test_style.rst b/docs/api/sqlobject.tests.test_style.rst index 202721a4..df0981a2 100644 --- a/docs/api/sqlobject.tests.test_style.rst +++ b/docs/api/sqlobject.tests.test_style.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_style module -================================= +sqlobject.tests.test\_style module +================================== .. automodule:: sqlobject.tests.test_style :members: diff --git a/docs/api/sqlobject.tests.test_subqueries.rst b/docs/api/sqlobject.tests.test_subqueries.rst index 97ee2874..c4ef5395 100644 --- a/docs/api/sqlobject.tests.test_subqueries.rst +++ b/docs/api/sqlobject.tests.test_subqueries.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_subqueries module -====================================== +sqlobject.tests.test\_subqueries module +======================================= .. automodule:: sqlobject.tests.test_subqueries :members: diff --git a/docs/api/sqlobject.tests.test_transactions.rst b/docs/api/sqlobject.tests.test_transactions.rst index 1e2cd7e5..1c218945 100644 --- a/docs/api/sqlobject.tests.test_transactions.rst +++ b/docs/api/sqlobject.tests.test_transactions.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_transactions module -======================================== +sqlobject.tests.test\_transactions module +========================================= .. automodule:: sqlobject.tests.test_transactions :members: diff --git a/docs/api/sqlobject.tests.test_unicode.rst b/docs/api/sqlobject.tests.test_unicode.rst index 7ac2e758..c90397f9 100644 --- a/docs/api/sqlobject.tests.test_unicode.rst +++ b/docs/api/sqlobject.tests.test_unicode.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_unicode module -=================================== +sqlobject.tests.test\_unicode module +==================================== .. automodule:: sqlobject.tests.test_unicode :members: diff --git a/docs/api/sqlobject.tests.test_uuidcol.rst b/docs/api/sqlobject.tests.test_uuidcol.rst index f6e30079..6ea9226e 100644 --- a/docs/api/sqlobject.tests.test_uuidcol.rst +++ b/docs/api/sqlobject.tests.test_uuidcol.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_uuidcol module -=================================== +sqlobject.tests.test\_uuidcol module +==================================== .. automodule:: sqlobject.tests.test_uuidcol :members: diff --git a/docs/api/sqlobject.tests.test_validation.rst b/docs/api/sqlobject.tests.test_validation.rst index c0240304..30e3fe04 100644 --- a/docs/api/sqlobject.tests.test_validation.rst +++ b/docs/api/sqlobject.tests.test_validation.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_validation module -====================================== +sqlobject.tests.test\_validation module +======================================= .. automodule:: sqlobject.tests.test_validation :members: diff --git a/docs/api/sqlobject.tests.test_views.rst b/docs/api/sqlobject.tests.test_views.rst index be986715..ed828c36 100644 --- a/docs/api/sqlobject.tests.test_views.rst +++ b/docs/api/sqlobject.tests.test_views.rst @@ -1,5 +1,5 @@ -sqlobject.tests.test_views module -================================= +sqlobject.tests.test\_views module +================================== .. automodule:: sqlobject.tests.test_views :members: diff --git a/docs/api/sqlobject.versioning.test.test_version.rst b/docs/api/sqlobject.versioning.test.test_version.rst index e2b21e1b..2f03e9e4 100644 --- a/docs/api/sqlobject.versioning.test.test_version.rst +++ b/docs/api/sqlobject.versioning.test.test_version.rst @@ -1,5 +1,5 @@ -sqlobject.versioning.test.test_version module -============================================= +sqlobject.versioning.test.test\_version module +============================================== .. automodule:: sqlobject.versioning.test.test_version :members: diff --git a/docs/api/sqlobject.wsgi_middleware.rst b/docs/api/sqlobject.wsgi_middleware.rst index 635e3e76..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/community.rst b/docs/community.rst index 91c0ae0c..c3123857 100644 --- a/docs/community.rst +++ b/docs/community.rst @@ -22,6 +22,9 @@ contributing you should read the `Developer Guide `_. The `Author List `_ tries to list all the major contributors. +Questions can also be asked and answered on `StackOverflow +`_. + One can also contribute to `community-editable recipe/documentation site `_. 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 5aea6d08..d873d0bd 100644 --- a/docs/download.rst +++ b/docs/download.rst @@ -2,7 +2,7 @@ Download SQLObject ++++++++++++++++++ The latest releases are always available on the `Python Package Index -`_, and is installable +`_, and is installable with `pip `_ or `easy_install `_. @@ -38,6 +38,49 @@ when you install ``SQLObject==bugfix`` you will be installing a specific version, and "bugfix" is just a kind of label for a way of acquiring the version (it points to a branch in the repository). +Drivers +------- + +SQLObject can be used with a number of drivers_. They can be installed +separately but it's also possible to install them with ``pip install``, +for example ``pip install SQLObject[mysql]`` or +``pip install SQLObject[postgres]``. The following drivers are +available: + +.. _drivers: SQLObject.html#requirements + +Firebird/Interbase +^^^^^^^^^^^^^^^^^^ + +fdb firebirdsql kinterbasdb + +MS SQL +^^^^^^ + +adodbapi pymssql + +MySQL +^^^^^ + +mysql (installs MySQL-python for Python 2.7 and mysqlclient for Python 3.4+) +mysql-connector pymysql mariadb + +ODBC +^^^^ + +pyodbc pypyodbc odbc (synonym for pyodbc) + +PostgreSQL +^^^^^^^^^^ + +psycopg psycopg2 postgres postgresql (synonyms for psycopg2) +pygresql pg8000 + +The rest +^^^^^^^^ + +sapdb sybase + Repositories ------------ diff --git a/docs/index.rst b/docs/index.rst index 25c07dd2..f1f1ecee 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,6 +68,14 @@ Here's how you'd use the object:: >>> p is p2 True +Queries:: + + >>> p3 = Person.selectBy(lname="Doe")[0] + >>> p3 + + >>> pc = Person.select(Person.q.lname=="Doe").count() + >>> pc + 1 Indices and tables ================== 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/requirements.txt b/requirements.txt deleted file mode 100644 index 89214067..00000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ ---install-option="--compile --optimize" - -FormEncode >= 1.1.1, != 1.3.0; python_version >= '2.6' and python_version < '3.0' -FormEncode >= 1.3.1; python_version >= '3.4' -PyDispatcher >= 2.0.4 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index ca2ff5d6..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,5 +0,0 @@ --r requirements.txt - -pytest -pytest-cov -tox >= 1.8 diff --git a/setup.cfg b/setup.cfg index 4c5fd41a..461bc50f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,18 @@ +[bdist_wheel] +universal = 1 + +[easy_install] +optimize = 2 + [egg_info] -tag_build = dev -tag_date = 1 +tag_build = +tag_date = 0 tag_svn_revision = 0 [flake8] -exclude = .git,.tox,docs/europython/*.py,ez_setup.py +exclude = .git,.tox,docs/europython/*.py # E305: expected 2 blank lines after class or function definition, found 1 -ignore = E305 - -[bdist_wheel] -universal = 1 +# W503 line break before binary operator +# W605 invalid escape sequence +ignore = E305,W503,W605 diff --git a/setup.py b/setup.py index 031c43d9..9158ff54 100755 --- a/setup.py +++ b/setup.py @@ -1,55 +1,32 @@ #!/usr/bin/env python -import sys -from imp import load_source from os.path import abspath, dirname, join +from setuptools import setup +import sys + +versionpath = join(abspath(dirname(__file__)), 'sqlobject', '__version__.py') +sqlobject_version = {} + +if sys.version_info[:2] == (2, 7): + execfile(versionpath, sqlobject_version) # noqa: F821 'execfile' Py3 -try: - from setuptools import setup - is_setuptools = True -except ImportError: - from distutils.core import setup - is_setuptools = False +elif sys.version_info >= (3, 4): + exec(open(versionpath, 'r').read(), sqlobject_version) -versionpath = join(abspath(dirname(__file__)), "sqlobject", "__version__.py") -load_source("sqlobject_version", versionpath) -from sqlobject_version import version # noqa: ignore flake8 E402 +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'] -kw = {} -if is_setuptools: - kw['entry_points'] = """ - [paste.filter_app_factory] - main = sqlobject.wsgi_middleware:make_middleware - """ - install_requires = [] - if (sys.version_info[0] == 2) and (sys.version_info[:2] >= (2, 6)): - install_requires.append("FormEncode>=1.1.1,!=1.3.0") - elif (sys.version_info[0] == 3) and (sys.version_info[:2] >= (3, 4)): - install_requires.append("FormEncode>=1.3.1") - else: - raise ImportError("SQLObject requires Python 2.6, 2.7 or 3.4+") - install_requires.append("PyDispatcher>=2.0.4") - kw['install_requires'] = install_requires - kw['extras_require'] = { - 'mysql': ['MySQLdb'], - 'postgresql': ['psycopg'], # or pgdb from PyGreSQL - 'sqlite': ['pysqlite'], - 'firebird': ['fdb'], # or kinterbasdb - 'sybase': ['Sybase'], - 'mssql': ['adodbapi'], # or pymssql - 'sapdb': ['sapdb'], - } - -setup(name="SQLObject", - version=version, - description="Object-Relational Manager, aka database wrapper", - long_description="""\ +setup( + name="sqlobject", + version=sqlobject_version['version'], + description="Object-Relational Manager, aka database wrapper", + long_description="""\ SQLObject is a popular *Object Relational Manager* for providing an object interface to your database, with tables as classes, rows as instances, and columns as attributes. @@ -59,77 +36,140 @@ applications. Supports MySQL, PostgreSQL, SQLite, Firebird, Sybase, MSSQL and MaxDB (SAPDB). -Python 2.6, 2.7 or 3.4+ is required. +Python 2.7 or 3.4+ is required. For development see the projects at `SourceForge `_ and `GitHub `_. -.. image:: https://travis-ci.org/sqlobject/sqlobject.svg?branch=master - :target: https://travis-ci.org/sqlobject/sqlobject -""", - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: " - "GNU Library or Lesser General Public License (LGPL)", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Topic :: Database", - "Topic :: Database :: Front-Ends", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - author="Ian Bicking", - author_email="ianb@colorstudy.com", - maintainer="Oleg Broytman", - maintainer_email="phd@phdru.name", - url="http://sqlobject.org/devel/", - download_url="https://pypi.python.org/pypi/SQLObject/%s" % version, - license="LGPL", - packages=["sqlobject"] + - ['sqlobject.%s' % package for package in subpackages], - scripts=["scripts/sqlobject-admin", "scripts/sqlobject-convertOldURI"], - package_data={"sqlobject": - [ - "../LICENSE", - "../docs/*.rst", - "../docs/html/*", - "../docs/html/_sources/*", - "../docs/html/_sources/api/*", - "../docs/html/_modules/*", - "../docs/html/_modules/sqlobject/*", - "../docs/html/_modules/sqlobject/mysql/*", - "../docs/html/_modules/sqlobject/postgres/*", - "../docs/html/_modules/sqlobject/manager/*", - "../docs/html/_modules/sqlobject/inheritance/*", - "../docs/html/_modules/sqlobject/inheritance/tests/*", - "../docs/html/_modules/sqlobject/mssql/*", - "../docs/html/_modules/sqlobject/tests/*", - "../docs/html/_modules/sqlobject/rdbhost/*", - "../docs/html/_modules/sqlobject/versioning/*", - "../docs/html/_modules/sqlobject/versioning/test/*", - "../docs/html/_modules/sqlobject/util/*", - "../docs/html/_modules/sqlobject/maxdb/*", - "../docs/html/_modules/sqlobject/firebird/*", - "../docs/html/_modules/sqlobject/sybase/*", - "../docs/html/_modules/sqlobject/sqlite/*", - "../docs/html/_modules/sqlobject/include/*", - "../docs/html/_modules/sqlobject/include/tests/*", - "../docs/html/_modules/pydispatch/*", - "../docs/html/_modules/_pytest/*", - "../docs/html/api/*", - "../docs/html/_static/*", - ], - "sqlobject.maxdb": ["readme.txt"], - }, - requires=['FormEncode', 'PyDispatcher'], - **kw - ) +.. 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 :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: " + "GNU Library or Lesser General Public License (LGPL)", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "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", + ], + author="Ian Bicking", + author_email="ianb@colorstudy.com", + maintainer="Oleg Broytman", + maintainer_email="phd@phdru.name", + url="http://sqlobject.org/", + 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/' % + sqlobject_version['version'], + 'Github repo': 'https://github.com/sqlobject', + 'Issue tracker': 'https://github.com/sqlobject/sqlobject/issues', + 'SourceForge project': 'https://sourceforge.net/projects/sqlobject/', + 'Twitter': 'https://twitter.com/SQLObject', + 'Wikipedia': 'https://en.wikipedia.org/wiki/SQLObject', + }, + keywords=["sql", "orm", "object-relational mapper"], + license="LGPL", + platforms="Any", + packages=["sqlobject"] + + ['sqlobject.%s' % package for package in subpackages], + scripts=["scripts/sqlobject-admin", "scripts/sqlobject-convertOldURI"], + package_data={ + "sqlobject.maxdb": ["readme.txt"], + }, + entry_points=""" + [paste.filter_app_factory] + main = sqlobject.wsgi_middleware:make_middleware + """, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + requires=['FormEncode', 'PyDispatcher'], + 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={ + # Firebird/Interbase + 'fdb': ['fdb'], + 'firebirdsql': ['firebirdsql'], + 'kinterbasdb': ['kinterbasdb'], + # MS SQL + 'adodbapi': ['adodbapi'], + 'pymssql': ['pymssql'], + # MySQL + 'mysql:python_version=="2.7"': ['MySQL-python'], + 'mysql:python_version>="3.4"': ['mysqlclient'], + 'mysql-connector': ['mysql-connector'], + '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 + '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'], + 'sybase': ['Sybase'], + # Non-DB API drivers + 'zope-dt:python_version=="3.4"': ['zope.datetime < 4.3'], + 'zope-dt:python_version!="3.4"': ['zope.datetime'], + }, +) # Send announce to: # sqlobject-discuss@lists.sourceforge.net @@ -160,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.6, 2.7 or 3.4+ is required. +and Sybase. Python 2.7 or 3.4+ is required. Where is SQLObject @@ -172,11 +212,8 @@ Mailing list: https://lists.sourceforge.net/mailman/listinfo/sqlobject-discuss -Archives: -http://news.gmane.org/gmane.comp.python.sqlobject - Download: -https://pypi.python.org/pypi/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 cc4f6fb7..37af2a9e 100644 --- a/sqlobject/__version__.py +++ b/sqlobject/__version__.py @@ -1,8 +1,8 @@ -version = '3.3.0' +version = '3.13.1' major = 3 -minor = 3 -micro = 0 -release_level = 'alpha' +minor = 13 +micro = 1 +release_level = 'final' serial = 0 version_info = (major, minor, micro, release_level, serial) diff --git a/sqlobject/col.py b/sqlobject/col.py index 26c9a513..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 @@ -387,8 +396,8 @@ def firebirdCreateSQL(self): [self.dbName, self._firebirdType()] + self._extraSQL()) else: return ' '.join( - [self.dbName] + [self._firebirdType()[0]] + - self._extraSQL() + [self._firebirdType()[1]]) + [self.dbName] + [self._firebirdType()[0]] + + self._extraSQL() + [self._firebirdType()[1]]) def maxdbCreateSQL(self): return ' '.join([self.dbName, self._maxdbType()] + self._extraSQL()) @@ -701,7 +710,7 @@ def to_python(self, value, state): if hasattr(value, attr_name): try: return converter(value) - except: + except Exception: break raise validators.Invalid( "expected an int in the IntCol '%s', got %s %r instead" % ( @@ -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): @@ -789,8 +798,18 @@ def to_python(self, value, state): return None if isinstance(value, (bool, sqlbuilder.SQLExpression)): return value - if isinstance(value, (int, long)) or hasattr(value, '__nonzero__'): + if PY2 and hasattr(value, '__nonzero__') \ + or not PY2 and hasattr(value, '__bool__'): return bool(value) + try: + connection = state.connection or state.soObject._connection + except AttributeError: + pass + else: + if connection.dbName == 'postgres' and \ + connection.driver in ('odbc', 'pyodbc', 'pypyodbc') and \ + isinstance(value, string_type): + return bool(int(value)) raise validators.Invalid( "expected a bool or an int in the BoolCol '%s', " "got %s %r instead" % ( @@ -845,7 +864,7 @@ def to_python(self, value, state): if hasattr(value, attr_name): try: return converter(value) - except: + except Exception: break raise validators.Invalid( "expected a float in the FloatCol '%s', got %s %r instead" % ( @@ -1082,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): @@ -1203,7 +1221,7 @@ def from_python(self, value, state): value = (value,) try: return ",".join(value) - except: + except Exception: raise validators.Invalid( "expected a string or a sequence of strings " "in the SetCol '%s', got %s %r instead" % ( @@ -1242,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: @@ -1252,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: @@ -1274,7 +1302,7 @@ def to_python(self, value, state): else: value += '.0' return datetime.datetime.strptime(value, self.format) - except: + except Exception: raise validators.Invalid( "expected a date/time string of the '%s' format " "in the DateTimeCol '%s', got %s %r instead" % ( @@ -1300,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: @@ -1332,10 +1367,10 @@ 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) - except: + 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 " "in the DateTimeCol '%s', got %s %r instead" % ( @@ -1346,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) @@ -1356,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' @@ -1371,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)) @@ -1412,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)" @@ -1453,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)) @@ -1496,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() @@ -1520,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)) @@ -1615,7 +1732,7 @@ def to_python(self, value, state): value = value.replace(connection.decimalSeparator, ".") try: return Decimal(value) - except: + except Exception: raise validators.Invalid( "expected a Decimal in the DecimalCol '%s', " "got %s %r instead" % ( @@ -1636,7 +1753,7 @@ def from_python(self, value, state): value = value.replace(connection.decimalSeparator, ".") try: return Decimal(value) - except: + except Exception: raise validators.Invalid( "can not parse Decimal value '%s' " "in the DecimalCol from '%s'" % ( @@ -1758,12 +1875,12 @@ def to_python(self, value, state): dbName = connection.dbName binaryType = connection._binaryType if isinstance(value, str): + if not PY2 and dbName == "mysql": + value = value.encode('ascii', errors='surrogateescape') if dbName == "sqlite": if not PY2: value = bytes(value, 'ascii') value = connection.module.decode(value) - if dbName == "mysql" and not PY2: - value = value.encode('ascii', errors='surrogateescape') return value if isinstance(value, bytes): return value @@ -1905,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( @@ -1932,6 +2051,8 @@ class UuidCol(Col): class JsonbValidator(SOValidator): def to_python(self, value, state): + if isinstance(value, string_type): + return json.loads(value) return value def from_python(self, value, state): @@ -1959,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( @@ -1986,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 7e8d0e1c..fc8edf25 100644 --- a/sqlobject/compat.py +++ b/sqlobject/compat.py @@ -1,7 +1,8 @@ +import os import sys import types -# Credit to six authors: https://pypi.python.org/pypi/six +# Credit to six authors: https://pypi.org/project/six/ # License: MIT @@ -29,3 +30,30 @@ def __new__(cls, name, this_bases, d): unicode_type = str class_types = (type,) buffer_type = memoryview + +if PY2: + import imp + + def load_module_from_file(base_name, module_name, filename): + fp, pathname, description = imp.find_module( + base_name, [os.path.dirname(filename)]) + try: + module = imp.load_module(module_name, fp, pathname, description) + finally: + fp.close() + return module +else: + import importlib.util + + def load_module_from_file(base_name, module_name, filename): + specs = importlib.util.spec_from_file_location(module_name, filename) + 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 f5baaf50..75176479 100644 --- a/sqlobject/dbconnection.py +++ b/sqlobject/dbconnection.py @@ -13,17 +13,17 @@ import warnings import weakref -from .cache import CacheSet from . import classregistry from . import col -from .converters import sqlrepr from . import sqlbuilder -from .util.threadinglocal import local as threading_local +from .cache import CacheSet from .compat import PY2, string_type, unicode_type +from .converters import sqlrepr +from .events import send, CommitSignal, RollbackSignal +from .util.threadinglocal import local as threading_local -warnings.filterwarnings("ignore", "DB-API extension cursor.lastrowid used") -_connections = {} +warnings.filterwarnings("ignore", "DB-API extension cursor.lastrowid used") def _closeConnection(ref): @@ -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') @@ -210,12 +210,6 @@ def _parseOldURI(uri): @staticmethod def _parseURI(uri): parsed = urlparse(uri) - if sys.version_info[0:2] == (2, 6): - # In python 2.6, urlparse only parses the uri completely - # for certain schemes, so we force the scheme to - # something that will be parsed correctly - scheme = parsed.scheme - parsed = urlparse(uri.replace(scheme, 'http', 1)) host, path = parsed.hostname, parsed.path user, password, port = None, None, None if parsed.username: @@ -433,7 +427,9 @@ def _executeRetry(self, conn, cursor, query): def _query(self, conn, s): if self.debug: self.printDebug(conn, s, 'Query') - self._executeRetry(conn, conn.cursor(), s) + c = conn.cursor() + self._executeRetry(conn, c, s) + c.close() def query(self, s): return self._runWithConnection(self._query, s) @@ -444,6 +440,7 @@ def _queryAll(self, conn, s): c = conn.cursor() self._executeRetry(conn, c, s) value = c.fetchall() + c.close() if self.debugOutput: self.printDebug(conn, value, 'QueryAll', 'result') return value @@ -461,6 +458,7 @@ def _queryAllDescription(self, conn, s): c = conn.cursor() self._executeRetry(conn, c, s) value = c.fetchall() + c.close() if self.debugOutput: self.printDebug(conn, value, 'QueryAll', 'result') return c.description, value @@ -474,6 +472,7 @@ def _queryOne(self, conn, s): c = conn.cursor() self._executeRetry(conn, c, s) value = c.fetchone() + c.close() if self.debugOutput: self.printDebug(conn, value, 'QueryOne', 'result') return value @@ -619,9 +618,9 @@ def _SO_selectOne(self, so, columnNames): def _SO_selectOneAlt(self, so, columnNames, condition): if columnNames: - columns = [isinstance(x, string_type) and - sqlbuilder.SQLConstant(x) or - x for x in columnNames] + columns = [isinstance(x, string_type) + and sqlbuilder.SQLConstant(x) + or x for x in columnNames] else: columns = None return self.queryOne(self.sqlrepr(sqlbuilder.Select( @@ -667,7 +666,6 @@ def _SO_intermediateInsert(self, table, firstColumn, firstValue, def _SO_columnClause(self, soClass, kw): from . import main - ops = {None: "IS"} data = [] if 'id' in kw: data.append((soClass.sqlmeta.idName, kw.pop('id'))) @@ -695,7 +693,7 @@ def _SO_columnClause(self, soClass, kw): return None return ' AND '.join( ['%s %s %s' % - (dbName, ops.get(value, "="), self.sqlrepr(value)) + (dbName, "IS" if value is None else "=", self.sqlrepr(value)) for dbName, value in data]) @@ -734,6 +732,15 @@ def createEmptyDatabase(self): """ raise NotImplementedError + def make_odbc_conn_str(self, odb_source, db, host=None, port=None, + user=None, password=None): + odbc_conn_parts = ['Driver={%s}' % odb_source] + for odbc_keyword, value in \ + zip(self.odbc_keywords, (host, port, user, password, db)): + if value is not None: + odbc_conn_parts.append('%s=%s' % (odbc_keyword, value)) + self.odbc_conn_str = ';'.join(odbc_conn_parts) + class Iteration(object): @@ -775,10 +782,11 @@ def _cleanup(self): if getattr(self, 'query', None) is None: # already cleaned up return - self.query = None + self.cursor.close() if not self.keepConnection: self.dbconn.releaseConnection(self.rawconn) - self.dbconn = self.rawconn = self.select = self.cursor = None + self.query = self.dbconn = self.rawconn = \ + self.select = self.cursor = None def __del__(self): self._cleanup() @@ -791,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 @@ -847,6 +855,7 @@ def commit(self, close=False): return if self._dbConnection.debug: self._dbConnection.printDebug(self._connection, '', 'COMMIT') + self._send_event(CommitSignal) self._connection.commit() subCaches = [(sub[0], sub[1].allIDs()) for sub in self.cache.allSubCachesByClassNames().items()] @@ -866,6 +875,7 @@ def rollback(self): if self._dbConnection.debug: self._dbConnection.printDebug(self._connection, '', 'ROLLBACK') subCaches = [(sub, sub.allIDs()) for sub in self.cache.allSubCaches()] + self._send_event(RollbackSignal) self._connection.rollback() for subCache, ids in subCaches: @@ -875,6 +885,20 @@ def rollback(self): inst.expire() self._makeObsolete() + def _send_event(self, signal): + """ + Pushes a list of class_names and related ids in cache. + :param signal: Type of event signal to use + """ + cached_classes_and_ids = [ + (class_name, cache.allIDs()) for class_name, cache in + self.cache.allSubCachesByClassNames().items() + ] + + if cached_classes_and_ids: + from .main import sqlmeta # Import here to avoid circular import + send(signal, sqlmeta, cached_classes_and_ids) + def __getattr__(self, attr): """ If nothing else works, let the parent connection handle it. @@ -900,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 @@ -914,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: @@ -965,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): """ @@ -995,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 @@ -1003,7 +1036,7 @@ def doInTransaction(self, func, *args, **kw): try: try: value = func(*args, **kw) - except: + except Exception: conn.rollback() raise else: @@ -1046,13 +1079,13 @@ 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) + assert (inst.name not in self.instanceNames + 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): @@ -1100,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/declarative.py b/sqlobject/declarative.py index d03a46ed..8fe2cbd4 100644 --- a/sqlobject/declarative.py +++ b/sqlobject/declarative.py @@ -199,8 +199,8 @@ def __repr__(self, cls): @staticmethod def _repr_vars(dictNames): names = [n for n in dictNames - if not n.startswith('_') and - n != 'declarative_count'] + if not n.startswith('_') + and n != 'declarative_count'] names.sort() return names diff --git a/sqlobject/events.py b/sqlobject/events.py index bf329df9..d38a2276 100644 --- a/sqlobject/events.py +++ b/sqlobject/events.py @@ -211,6 +211,19 @@ class DropTableSignal(Signal): after the table has been dropped. """ + +class CommitSignal(Signal): + """ + Called on transaction commit + """ + + +class RollbackSignal(Signal): + """ + Called on transaction rollback + """ + + ############################################################ # Event Debugging ############################################################ diff --git a/sqlobject/firebird/firebirdconnection.py b/sqlobject/firebird/firebirdconnection.py index 3bf3fb57..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, @@ -135,6 +135,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): if self.debug: self.printDebug(conn, q, 'QueryIns') c.execute(q) + c.close() if self.debugOutput: self.printDebug(conn, id, 'QueryIns', 'result') return id 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/__init__.py b/sqlobject/inheritance/__init__.py index a1340d4a..e6c57b44 100644 --- a/sqlobject/inheritance/__init__.py +++ b/sqlobject/inheritance/__init__.py @@ -31,8 +31,8 @@ def __init__(self, sourceClass, clause, clauseTables=None, if clause is None or isinstance(clause, str) and clause == 'all': clause = sqlbuilder.SQLTrueClause - dbName = (ops.get('connection', None) or - sourceClass._connection).dbName + dbName = (ops.get('connection', None) + or sourceClass._connection).dbName tablesSet = tablesUsedSet(clause, dbName) tablesSet.add(str(sourceClass.sqlmeta.table)) @@ -411,12 +411,12 @@ def _create(self, id, **kw): # TC: the parent if the child can not be created. try: super(InheritableSQLObject, self)._create(id, **kw) - except: + except Exception: # If we are outside a transaction and this is a child, # destroy the parent connection = self._connection - if (not isinstance(connection, dbconnection.Transaction) and - connection.autoCommit) and self.sqlmeta.parentClass: + if (not isinstance(connection, dbconnection.Transaction) + and connection.autoCommit) and self.sqlmeta.parentClass: self._parent.destroySelf() # TC: Do we need to do this?? self._parent = None @@ -457,8 +457,8 @@ def select(cls, clause=None, *args, **kwargs): # if the clause was one of TRUE varians, replace it if (clause is None) or (clause is sqlbuilder.SQLTrueClause) \ or ( - isinstance(clause, string_type) and - (clause == 'all')): + isinstance(clause, string_type) + and (clause == 'all')): clause = addClause else: # patch WHERE condition: diff --git a/sqlobject/inheritance/iteration.py b/sqlobject/inheritance/iteration.py index fd7b78f5..558700ea 100644 --- a/sqlobject/inheritance/iteration.py +++ b/sqlobject/inheritance/iteration.py @@ -11,7 +11,11 @@ def __init__(self, dbconn, rawconn, select, keepConnection=False): super(InheritableIteration, self).__init__(dbconn, rawconn, select, keepConnection) self.lazyColumns = select.ops.get('lazyColumns', False) - self.cursor.arraysize = self.defaultArraySize + try: + self.cursor.arraysize = self.defaultArraySize + self.use_arraysize = True + except AttributeError: # pymssql doesn't have arraysize + self.use_arraysize = False self._results = [] # Find the index of the childName column childNameIdx = None @@ -24,7 +28,11 @@ def __init__(self, dbconn, rawconn, select, keepConnection=False): def next(self): if not self._results: - self._results = list(self.cursor.fetchmany()) + if self.use_arraysize: + _results = self.cursor.fetchmany() + else: + _results = self.cursor.fetchmany(size=self.defaultArraySize) + self._results = list(_results) if not self.lazyColumns: self.fetchChildren() if not self._results: diff --git a/sqlobject/inheritance/tests/test_asdict.py b/sqlobject/inheritance/tests/test_asdict.py index 5129ac27..95d6b818 100644 --- a/sqlobject/inheritance/tests/test_asdict.py +++ b/sqlobject/inheritance/tests/test_asdict.py @@ -9,7 +9,7 @@ class InheritablePersonAD(InheritableSQLObject): firstName = StringCol() - lastName = StringCol(alternateID=True, length=255) + lastName = StringCol(alternateID=True, length=100) class ManagerAD(InheritablePersonAD): diff --git a/sqlobject/inheritance/tests/test_deep_inheritance.py b/sqlobject/inheritance/tests/test_deep_inheritance.py index 411beb22..4bdc8e34 100644 --- a/sqlobject/inheritance/tests/test_deep_inheritance.py +++ b/sqlobject/inheritance/tests/test_deep_inheritance.py @@ -10,7 +10,7 @@ class DIPerson(InheritableSQLObject): firstName = StringCol(length=100) - lastName = StringCol(alternateID=True, length=255) + lastName = StringCol(alternateID=True, length=100) manager = ForeignKey("DIManager", default=None) @@ -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 e1d51fa9..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 @@ -9,13 +23,13 @@ class InheritablePerson(InheritableSQLObject): - firstName = StringCol() - lastName = StringCol(alternateID=True, length=255) + firstName = StringCol(length=100) + lastName = StringCol(alternateID=True, length=100) class Employee(InheritablePerson): _inheritable = False - so_position = StringCol() + so_position = StringCol(length=100) def setup(): @@ -79,7 +93,7 @@ def test_inheritance_select(): try: person = InheritablePerson.byLastName("Oneof") - except: + except Exception: pass else: raise RuntimeError("unknown person %s" % person) 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 675beb70..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 @@ -102,8 +103,7 @@ def _applyOrderBy(self, results, defaultSortClass): class MinType(object): """Sort less than everything, for handling None's in the results""" - # functools.total_ordering would simplify this, but isn't available - # for python 2.6 + # functools.total_ordering would simplify this def __lt__(self, other): if self is other: @@ -171,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 @@ -292,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): @@ -340,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): @@ -441,19 +499,19 @@ def _finishSet(self): events.listen(self.event_CreateTableSignal, self.otherClass, events.CreateTableSignal) self.clause = ( - (self.otherClass.q.id == - sqlbuilder.Field(self.intermediateTable, self.otherColumn)) & - (sqlbuilder.Field(self.intermediateTable, self.joinColumn) == - self.soClass.q.id)) + (self.otherClass.q.id + == sqlbuilder.Field(self.intermediateTable, self.otherColumn)) + & (sqlbuilder.Field(self.intermediateTable, self.joinColumn) + == self.soClass.q.id)) def __get__(self, obj, type): if obj is None: return self query = ( - (self.otherClass.q.id == - sqlbuilder.Field(self.intermediateTable, self.otherColumn)) & - (sqlbuilder.Field(self.intermediateTable, self.joinColumn) == - obj.id)) + (self.otherClass.q.id + == sqlbuilder.Field(self.intermediateTable, self.otherColumn)) + & (sqlbuilder.Field(self.intermediateTable, self.joinColumn) + == obj.id)) select = self.otherClass.select(query) return _ManyToManySelectWrapper(obj, self, select) @@ -546,15 +604,15 @@ def _setOtherClass(self, otherClass): self.joinColumn = styles.getStyle( self.soClass).tableReference(self.soClass.sqlmeta.table) self.clause = ( - sqlbuilder.Field(self.otherClass.sqlmeta.table, self.joinColumn) == - self.soClass.q.id) + sqlbuilder.Field(self.otherClass.sqlmeta.table, self.joinColumn) + == self.soClass.q.id) def __get__(self, obj, type): if obj is None: return self query = ( - sqlbuilder.Field(self.otherClass.sqlmeta.table, self.joinColumn) == - obj.id) + sqlbuilder.Field(self.otherClass.sqlmeta.table, self.joinColumn) + == obj.id) select = self.otherClass.select(query) return _OneToManySelectWrapper(obj, self, select) diff --git a/sqlobject/main.py b/sqlobject/main.py index 0ac510c7..7d2fd1c3 100644 --- a/sqlobject/main.py +++ b/sqlobject/main.py @@ -43,9 +43,9 @@ from .util.threadinglocal import local from sqlobject.compat import PY2, with_metaclass, string_type, unicode_type -if ((sys.version_info[0] == 2) and (sys.version_info[:2] < (2, 6))) or \ +if ((sys.version_info[0] == 2) and (sys.version_info[:2] < (2, 7))) or \ ((sys.version_info[0] == 3) and (sys.version_info[:2] < (3, 4))): - raise ImportError("SQLObject requires Python 2.6, 2.7 or 3.4+") + raise ImportError("SQLObject requires Python 2.7 or 3.4+") if not PY2: # alias for python 3 compatability @@ -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 @@ -693,9 +701,9 @@ def deprecated(message, level=1, stacklevel=2): if warnings_level is not None and warnings_level <= level: warnings.warn(message, DeprecationWarning, stacklevel=stacklevel) -# if sys.version_info[:2] < (2, 6): -# deprecated("Support for Python 2.5 has been declared obsolete " -# "and will be removed in the next release of SQLObject") +# if sys.version_info[:2] < (2, 7): +# deprecated("Support for Python 2.6 has been declared obsolete " +# "and will be removed in the next release of SQLObject") def setDeprecationLevel(warning=1, exception=None): @@ -1410,8 +1418,8 @@ def _findAlternateID(cls, name, dbName, value, connection=None): *[getattr(cls.q, _n) == _v for _n, _v in zip(name, new_value)]) return (connection or cls._connection)._SO_selectOneAlt( cls, - [cls.sqlmeta.idName] + - [column.dbName for column in cls.sqlmeta.columnList], + [cls.sqlmeta.idName] + + [column.dbName for column in cls.sqlmeta.columnList], condition), None @classmethod @@ -1536,8 +1544,8 @@ def createTableSQL(cls, createJoinTables=True, createIndexes=True, def createJoinTables(cls, ifNotExists=False, connection=None): conn = connection or cls._connection for join in cls._getJoinsToCreate(): - if (ifNotExists and - conn.tableExists(join.intermediateTable)): + if (ifNotExists + and conn.tableExists(join.intermediateTable)): continue conn._SO_createJoinTable(join) @@ -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 2b7bc21d..fc8db834 100755 --- a/sqlobject/manager/command.py +++ b/sqlobject/manager/command.py @@ -297,8 +297,8 @@ def run(self): if self.description: self.parser.description = self.description self.options, self.args = self.parser.parse_args(self.raw_args) - if (getattr(self.options, 'simulate', False) and - not self.options.verbose): + if (getattr(self.options, 'simulate', False) + and not self.options.verbose): self.options.verbose = 1 if self.min_args is not None and len(self.args) < self.min_args: self.runner.invalid( @@ -391,9 +391,9 @@ def classes_from_module(self, module): else: for name in dir(module): value = getattr(module, name) - if (isinstance(value, type) and - issubclass(value, sqlobject.SQLObject) and - value.__module__ == module.__name__): + if (isinstance(value, type) + and issubclass(value, sqlobject.SQLObject) + and value.__module__ == module.__name__): all.append(value) return all @@ -414,8 +414,8 @@ def config(self): return None config_file = self.options.config_file if appconfig: - if (not config_file.startswith('egg:') and - not config_file.startswith('config:')): + if (not config_file.startswith('egg:') + and not config_file.startswith('config:')): config_file = 'config:' + config_file return appconfig(config_file, relative_to=os.getcwd()) @@ -446,9 +446,9 @@ def ini_config(self, conf_fn): possible_sections = [] for section in p.sections(): name = section.strip().lower() - if (conf_section == name or - (conf_section == name.split(':')[-1] and - name.split(':')[0] in ('app', 'application'))): + if (conf_section == name + or (conf_section == name.split(':')[-1] + and name.split(':')[0] in ('app', 'application'))): possible_sections.append(section) if not possible_sections: @@ -514,8 +514,8 @@ def classes_from_egg(self, egg_spec): def load_options_from_egg(self, egg_spec): dist, conf = self.config_from_egg(egg_spec) - if (hasattr(self.options, 'output_dir') and - not self.options.output_dir and conf.get('history_dir')): + if (hasattr(self.options, 'output_dir') + and not self.options.output_dir and conf.get('history_dir')): dir = conf['history_dir'] dir = dir.replace('$base', dist.location) self.options.output_dir = dir @@ -653,8 +653,8 @@ def command(self): dbs_created = [] constraints = {} for soClass in self.classes(require_some=True): - if (self.options.create_db and - soClass._connection not in dbs_created): + if (self.options.create_db + and soClass._connection not in dbs_created): if not self.options.simulate: try: soClass._connection.createEmptyDatabase() @@ -1007,8 +1007,8 @@ def command(self): f = open(os.path.join(last_version_dir, fn), 'r') content = f.read() f.close() - if (self.strip_comments(files_copy[fn]) != - self.strip_comments(content)): + if (self.strip_comments(files_copy[fn]) + != self.strip_comments(content)): if v > 1: print("Content does not match: %s" % fn) break @@ -1063,8 +1063,8 @@ def command(self): print("Cannot edit upgrader because there is no " "previous version") else: - breaker = ('-' * 20 + ' lines below this will be ignored ' + - '-' * 20) + breaker = ('-' * 20 + ' lines below this will be ignored ' + + '-' * 20) pre_text = breaker + '\n' + '\n'.join(all_diffs) text = self.open_editor('\n\n' + pre_text, breaker=breaker, extension='.sql') @@ -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): @@ -1117,8 +1117,8 @@ def base_dir(self): def find_output_dir(self): today = time.strftime('%Y-%m-%d', time.localtime()) if self.options.version_name: - dir = os.path.join(self.base_dir(), today + '-' + - self.options.version_name) + dir = os.path.join(self.base_dir(), today + '-' + + self.options.version_name) if os.path.exists(dir): print("Error, directory already exists: %s" % dir) @@ -1227,7 +1227,7 @@ def command(self): if not sim: try: conn.query(sql) - except: + except Exception: print("Error in script: %s" % upgrader) raise self.update_db(next_version, conn) diff --git a/sqlobject/maxdb/maxdbconnection.py b/sqlobject/maxdb/maxdbconnection.py index c46c3eab..4fd53b1b 100644 --- a/sqlobject/maxdb/maxdbconnection.py +++ b/sqlobject/maxdb/maxdbconnection.py @@ -138,6 +138,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): if self.debug: self.printDebug(conn, q, 'QueryIns') c.execute(q) + c.close() if self.debugOutput: self.printDebug(conn, id, 'QueryIns', 'result') return id diff --git a/sqlobject/maxdb/readme.txt b/sqlobject/maxdb/readme.txt index 3984fbd8..e1129834 100644 --- a/sqlobject/maxdb/readme.txt +++ b/sqlobject/maxdb/readme.txt @@ -1,30 +1,27 @@ -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -Author: -Edigram SA - Paris France -Tel:0144779400 - -SAPDBAPI installation ---------------------- -The sapdb module can be downloaded from: - -Win32 -------- - -ftp://ftp.sap.com/pub/sapdb/bin/win/sapdb-python-win32-7.4.03.31a.zip - - -Linux ------- - -ftp://ftp.sap.com/pub/sapdb/bin/linux/sapdb-python-linux-i386-7.4.03.31a.tgz - - -uncompress the archive and add the sapdb directory path to your PYTHONPATH. - - - - +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +Author: +Edigram SA - Paris France +Tel:0144779400 + +SAPDBAPI installation +--------------------- + +The sapdb module can be downloaded from: + +Win32 +----- + +ftp://ftp.sap.com/pub/sapdb/bin/win/sapdb-python-win32-7.4.03.33a.zip + + +Linux +----- + +ftp://ftp.sap.com/pub/sapdb/bin/linux/sapdb-python-linux-i386-7.4.03.33a.tgz + + +Uncompress the archive and add the sapdb directory path to your PYTHONPATH. diff --git a/sqlobject/mssql/mssqlconnection.py b/sqlobject/mssql/mssqlconnection.py index 1350514f..d2fff311 100644 --- a/sqlobject/mssql/mssqlconnection.py +++ b/sqlobject/mssql/mssqlconnection.py @@ -10,7 +10,9 @@ 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') def __init__(self, db, user, password='', host='localhost', port=None, autoCommit=0, **kw): @@ -24,10 +26,23 @@ def __init__(self, db, user, password='', host='localhost', port=None, import adodbapi as sqlmodule elif driver == 'pymssql': import pymssql as sqlmodule + elif driver == 'pyodbc': + import pyodbc + self.module = pyodbc + elif driver == 'pypyodbc': + import pypyodbc + self.module = pypyodbc + elif driver == 'odbc': + try: + import pyodbc + except ImportError: + import pypyodbc as pyodbc + self.module = pyodbc else: raise ValueError( 'Unknown MSSQL driver "%s", ' - 'expected adodb or pymssql' % driver) + 'expected adodb, pymssql, ' + 'odbc, pyodbc or pypyodbc' % driver) except ImportError: pass else: @@ -35,9 +50,19 @@ def __init__(self, db, user, password='', host='localhost', port=None, else: raise ImportError( 'Cannot find an MSSQL driver, tried %s' % drivers) - self.module = sqlmodule - if sqlmodule.__name__ == 'adodbapi': + timeout = kw.pop('timeout', None) + if timeout: + timeout = int(timeout) + self.timeout = timeout + + if driver in ('odbc', 'pyodbc', 'pypyodbc'): + self.make_odbc_conn_str(kw.pop('odbcdrv', 'SQL Server'), + db, host, port, user, password + ) + + elif driver in ('adodb', 'adodbapi'): + self.module = sqlmodule self.dbconnection = sqlmodule.connect # ADO uses unicode only (AFAIK) self.usingUnicodeStrings = True @@ -64,17 +89,13 @@ def __init__(self, db, user, password='', host='localhost', port=None, kw.pop("ncli", None) kw.pop("sspi", None) - else: # pymssql + elif driver == 'pymssql': + self.module = sqlmodule self.dbconnection = sqlmodule.connect sqlmodule.Binary = lambda st: str(st) # don't know whether pymssql uses unicode self.usingUnicodeStrings = False - timeout = kw.pop('timeout', None) - if timeout: - timeout = int(timeout) - self.timeout = timeout - def _make_conn_str(keys): keys_dict = {} for attr, value in ( @@ -84,11 +105,14 @@ 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 return keys_dict self.make_conn_str = _make_conn_str + self.driver = driver self.autoCommit = int(autoCommit) self.user = user @@ -96,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 @@ -115,20 +141,48 @@ def insert_id(self, conn): # converting the identity to an int is ugly, but it gets returned # as a decimal otherwise :S c.execute('SELECT CONVERT(INT, @@IDENTITY)') - return c.fetchone()[0] + result = c.fetchone()[0] + c.close() + return result def makeConnection(self): - conn_descr = self.make_conn_str(self) - if isinstance(conn_descr, dict): - con = self.dbconnection(**conn_descr) + if self.driver in ('odbc', 'pyodbc', 'pypyodbc'): + self.debugWriter.write( + "ODBC connect string: " + self.odbc_conn_str) + timeout = self.timeout + if timeout: + kw = dict(timeout=timeout) + else: + kw = dict() + conn = self.module.connect(self.odbc_conn_str, **kw) + if timeout: + conn.timeout = timeout else: - con = self.dbconnection(conn_descr) - cur = con.cursor() + conn_descr = self.make_conn_str(self) + if isinstance(conn_descr, dict): + conn = self.dbconnection(**conn_descr) + else: + conn = self.dbconnection(conn_descr) + cur = conn.cursor() cur.execute('SET ANSI_NULLS ON') cur.execute("SELECT CAST('12345.21' AS DECIMAL(10, 2))") self.decimalSeparator = str(cur.fetchone()[0])[-3] cur.close() - return con + return conn + + def _setAutoCommit(self, conn, auto): + auto = bool(auto) + if self.driver in ('adodb', 'adodbapi'): + if auto: + option = "ON" + else: + option = "OFF" + c = conn.cursor() + c.execute("SET AUTOCOMMIT " + option) + elif self.driver == 'pymssql': + conn.autocommit(auto) + elif self.driver in ('odbc', 'pyodbc', 'pypyodbc'): + conn.autocommit = auto HAS_IDENTITY = """ select 1 @@ -142,6 +196,7 @@ def _hasIdentity(self, conn, table): c = conn.cursor() c.execute(query) r = c.fetchone() + c.close() return r is not None def _queryInsertID(self, conn, soInstance, id, names, values): @@ -179,6 +234,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): c.execute(q) if has_identity: c.execute('SET IDENTITY_INSERT %s OFF' % table) + c.close() if id is None: id = self.insert_id(conn) @@ -293,19 +349,6 @@ def columnsFromSchema(self, tableName, soClass): results.append(colClass(**kw)) return results - def _setAutoCommit(self, conn, auto): - # raise Exception(repr(auto)) - return - # conn.auto_commit = auto - option = "ON" - if auto == 0: - option = "OFF" - c = conn.cursor() - c.execute("SET AUTOCOMMIT " + option) - # from mx.ODBC.Windows import SQL - # connection.setconnectoption( - # SQL.AUTOCOMMIT, SQL.AUTOCOMMIT_ON if auto else SQL.AUTOCOMMIT_OFF) - # precision and scale is needed for decimal columns def guessClass(self, t, size, precision, scale): """ @@ -352,7 +395,7 @@ def server_version(self): server_version = server_version.decode('ascii') server_version = server_version.split('.')[0] server_version = int(server_version) - except: + except Exception: server_version = None # unknown self._server_version = server_version return server_version diff --git a/sqlobject/mysql/mysqlconnection.py b/sqlobject/mysql/mysqlconnection.py index cffb7d5d..04490aa7 100644 --- a/sqlobject/mysql/mysqlconnection.py +++ b/sqlobject/mysql/mysqlconnection.py @@ -1,12 +1,21 @@ 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) - obj.code = int(e.args[0]) + if len(e.args) > 1: + errmsg = e.args[1] + else: + errmsg = '' + try: + errcode = int(e.args[0]) + except ValueError: + 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 @@ -20,26 +29,29 @@ class MySQLConnection(DBAPI): dbName = 'mysql' schemes = [dbName] + 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 = \ @@ -50,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 = \ @@ -58,18 +70,31 @@ 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 + elif driver == 'pypyodbc': + import pypyodbc + self.module = pypyodbc + elif driver == 'odbc': + try: + import pyodbc + except ImportError: + import pypyodbc as pyodbc + self.module = pyodbc else: raise ValueError( 'Unknown MySQL driver "%s", ' - 'expected mysqldb, connector, ' - 'oursql or pymysql' % driver) + 'expected mysqldb, connector, connector-python, ' + 'pymysql, mariadb, ' + 'odbc, pyodbc or pypyodbc' % driver) except ImportError: pass else: @@ -77,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: @@ -91,17 +118,39 @@ 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)) - for key in ("ssl_key", "ssl_cert", "ssl_ca", "ssl_capath"): - if key in kw: - if "ssl" not in self.kw: - self.kw["ssl"] = {} - self.kw["ssl"][key[4:]] = kw.pop(key) + 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) + else: + for key in ("ssl_key", "ssl_cert", "ssl_ca", "ssl_capath"): + if key in kw: + if "ssl" not in self.kw: + self.kw["ssl"] = {} + self.kw["ssl"][key[4:]] = kw.pop(key) if "charset" in kw: self.dbEncoding = self.kw["charset"] = kw.pop("charset") else: 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 + ) + + 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: mysql_Bin = self.module.Binary @@ -110,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 @@ -121,22 +172,33 @@ 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: - 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': - # Attempt to reconnect. This setting is persistent. - conn.ping(True) + if self.driver in ('odbc', 'pyodbc', 'pypyodbc'): + self.debugWriter.write( + "ODBC connect string: " + self.odbc_conn_str) + conn = self.module.connect(self.odbc_conn_str) + else: + conn = self.module.connect( + host=self.host, port=self.port, db=self.db, + user=self.user, passwd=self.password, **self.kw) + 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: conninfo = ("; used connection string: " "host=%(host)s, port=%(port)s, " @@ -146,10 +208,16 @@ def character_set_name(self): self._setAutoCommit(conn, bool(self.autoCommit)) if dbEncoding: - if hasattr(conn, 'set_character_set'): + if self.driver in ('odbc', 'pyodbc'): + conn.setdecoding(self.module.SQL_CHAR, encoding=dbEncoding) + conn.setdecoding(self.module.SQL_WCHAR, encoding=dbEncoding) + if PY2: + conn.setencoding(str, encoding=dbEncoding) + conn.setencoding(unicode, encoding=dbEncoding) # noqa + else: + 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) @@ -161,15 +229,23 @@ 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 in ('pymysql',): + conn.ping(True) + self._setAutoCommit(conn, bool(self.autoCommit)) + if self.dbEncoding: + conn.query("SET NAMES %s" % self.dbEncoding) + def _executeRetry(self, conn, cursor, query): if self.debug: self.printDebug(conn, query, 'QueryR') dbEncoding = self.dbEncoding if dbEncoding and not isinstance(query, bytes) and ( - self.driver == 'connector'): + 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 @@ -183,6 +259,8 @@ 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. + # [PC]yMySQL need explicit reconnect + # each time we detect connection timeout. for count in range(3): try: return cursor.execute(query) @@ -193,12 +271,17 @@ def _executeRetry(self, conn, cursor, query): raise dberrors.OperationalError(ErrorMessage(e)) if self.debug: self.printDebug(conn, str(e), 'ERROR') + if self.driver in ('pymysql',): + self._force_reconnect(conn) else: raise dberrors.OperationalError(ErrorMessage(e)) except self.module.IntegrityError as e: 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: @@ -234,7 +317,14 @@ def _queryInsertID(self, conn, soInstance, id, names, values): try: id = c.lastrowid except AttributeError: - id = c.insert_id() + try: + id = c.insert_id + except AttributeError: + self._executeRetry(conn, c, "SELECT LAST_INSERT_ID();") + id = c.fetchone()[0] + else: + id = c.insert_id() + c.close() if self.debugOutput: self.printDebug(conn, id, 'QueryIns', 'result') return id @@ -257,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' @@ -272,7 +374,9 @@ def tableExists(self, tableName): self.query('DESCRIBE %s' % (tableName)) return True except dberrors.ProgrammingError as e: - if e.args[0].code == 1146: # ER_NO_SUCH_TABLE + if e.args[0].code in (1146, '42S02'): # ER_NO_SUCH_TABLE + return False + if self.driver == 'mariadb': return False raise @@ -304,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'): @@ -320,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'): @@ -376,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)) @@ -403,7 +512,7 @@ def server_version(self): server_version = server_version[0] server_version = tuple(int(v) for v in server_version.split('.')) server_version = (server_version, db_tag) - except: + except Exception: server_version = None # unknown self._server_version = server_version return server_version @@ -421,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 5920cd58..3ce5ef99 100644 --- a/sqlobject/postgres/pgconnection.py +++ b/sqlobject/postgres/pgconnection.py @@ -1,3 +1,4 @@ +from getpass import getuser import re from sqlobject import col from sqlobject import dberrors @@ -9,55 +10,93 @@ 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 dbName = 'postgres' schemes = [dbName, 'postgresql'] + odbc_keywords = ('Server', 'Port', 'UID', 'Password', 'Database') + 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 == 'psycopg2': - import psycopg2 as psycopg - self.module = psycopg - elif driver == 'psycopg1': + if driver == 'psycopg': import psycopg self.module = psycopg - elif driver == 'psycopg': - try: - import psycopg2 as psycopg - except ImportError: - 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': + try: + import pg8000.dbapi + self.module = pg8000.dbapi + except ImportError: + import pg8000 + self.module = pg8000 + elif driver == 'pyodbc': + import pyodbc + self.module = pyodbc + elif driver == 'pypyodbc': + import pypyodbc + self.module = pypyodbc + elif driver == 'odbc': + try: + import pyodbc + except ImportError: + import pypyodbc as pyodbc + self.module = pyodbc else: raise ValueError( 'Unknown PostgreSQL driver "%s", ' - 'expected psycopg2, psycopg1, ' - 'pygresql or pypostgresql' % driver) + 'expected psycopg, psycopg2, ' + 'pygresql, pg8000, ' + 'odbc, pyodbc or pypyodbc' % driver) except ImportError: pass else: @@ -66,79 +105,94 @@ 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 type(self.module.Binary) in (type, type(PostgresBinaryConverter)): - # Register a converter for Binary type. + elif driver in ('pygresql', 'pg8000'): registerConverter(type(self.module.Binary(b'')), PostgresBinaryConverter) + elif driver in ('odbc', 'pyodbc', 'pypyodbc'): + registerConverter(bytearray, OdbcBinaryConverter) + self.db = db self.user = user + self.password = password self.host = host self.port = port - self.db = db - self.password = password - self.dsn_dict = dsn_dict = {} - if host: - dsn_dict["host"] = host - if port: - if driver == 'pygresql': - dsn_dict["host"] = "%s:%d" % (host, port) - elif driver.startswith('psycopg') and \ - psycopg.__version__.split('.')[0] == '1': - dsn_dict["port"] = str(port) - else: - dsn_dict["port"] = port - if db: - dsn_dict["database"] = db - if user: - dsn_dict["user"] = user - if password: - dsn_dict["password"] = password - sslmode = kw.pop("sslmode", None) - if sslmode: - dsn_dict["sslmode"] = sslmode - self.use_dsn = dsn is not None - if dsn is None: - if driver == 'pygresql': - dsn = '' - if host: - dsn += host - dsn += ':' - if db: - dsn += db - dsn += ':' - if user: - dsn += user - dsn += ':' - if password: - dsn += password - else: - 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) - 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 in ('odbc', 'pyodbc', 'pypyodbc'): + self.make_odbc_conn_str(kw.pop('odbcdrv', 'PostgreSQL ANSI'), + db, host, port, user, password + ) + sslmode = kw.pop("sslmode", None) + if sslmode: + self.odbc_conn_str += ';sslmode=require' + else: + self.dsn_dict = dsn_dict = {} + if host: + dsn_dict["host"] = host + if port: + if driver == 'pygresql': + dsn_dict["host"] = "%s:%d" % (host, port) + elif driver.startswith('psycopg') and \ + self.module.__version__.split('.')[0] == '1': + dsn_dict["port"] = str(port) + else: + dsn_dict["port"] = port + if db: + if driver == 'psycopg': + dsn_dict["dbname"] = db + else: + dsn_dict["database"] = db + if user: + dsn_dict["user"] = user + if password: + dsn_dict["password"] = password + sslmode = kw.pop("sslmode", None) + if sslmode: + dsn_dict["sslmode"] = sslmode + self.use_dsn = dsn is not None + if dsn is None: + if driver == 'pygresql': + dsn = '' + if host: + dsn += host + dsn += ':' + if db: + dsn += db + dsn += ':' + if user: + dsn += user + dsn += ':' + if password: + dsn += password + else: + 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) + if sslmode: + dsn.append('sslmode=%s' % sslmode) + dsn = ' '.join(dsn) + 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() + self.dsn = dsn self.driver = driver - self.dsn = dsn self.unicodeCols = kw.pop('unicodeCols', False) self.schema = kw.pop('schema', None) self.dbEncoding = kw.pop("charset", None) @@ -164,7 +218,11 @@ def _setAutoCommit(self, conn, auto): def makeConnection(self): try: - if self.use_dsn: + if self.driver in ('odbc', 'pyodbc', 'pypyodbc'): + self.debugWriter.write( + "ODBC connect string: " + self.odbc_conn_str) + conn = self.module.connect(self.odbc_conn_str) + elif self.use_dsn: conn = self.module.connect(self.dsn) else: conn = self.module.connect(**self.dsn_dict) @@ -182,22 +240,37 @@ def makeConnection(self): self._executeRetry(conn, c, "SET search_path TO " + self.schema) dbEncoding = self.dbEncoding if dbEncoding: + if self.driver in ('odbc', 'pyodbc'): + conn.setdecoding(self.module.SQL_CHAR, encoding=dbEncoding) + conn.setdecoding(self.module.SQL_WCHAR, encoding=dbEncoding) + if PY2: + conn.setencoding(str, encoding=dbEncoding) + conn.setencoding(unicode, encoding=dbEncoding) # noqa + else: + conn.setencoding(encoding=dbEncoding) self._executeRetry(conn, c, "SET client_encoding TO '%s'" % dbEncoding) + c.close() return conn def _executeRetry(self, conn, cursor, query): if self.debug: self.printDebug(conn, query, 'QueryR') + dbEncoding = self.dbEncoding + if dbEncoding and isinstance(query, bytes) and ( + self.driver == 'pg8000'): + query = query.decode(dbEncoding) try: return cursor.execute(query) except self.module.OperationalError as e: 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': + getattr(e, 'sqlstate', -1) == '23505' or \ + e.args[0] == '23505': raise dberrors.DuplicateEntryError(msg) else: raise dberrors.IntegrityError(msg) @@ -205,7 +278,10 @@ def _executeRetry(self, conn, cursor, query): raise dberrors.InternalError(ErrorMessage(e)) except self.module.ProgrammingError as e: msg = ErrorMessage(e) - if (len(e.args) >= 2) and e.args[1] == '23505': + if ( + (len(e.args) > 2) and (e.args[1] == 'ERROR') + and (e.args[2] == '23505')) \ + or ((len(e.args) >= 2) and (e.args[1] == '23505')): raise dberrors.DuplicateEntryError(msg) else: raise dberrors.ProgrammingError(msg) @@ -230,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 @@ -249,6 +320,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): self._executeRetry(conn, c, q) if id is None: id = c.fetchone()[0] + c.close() if self.debugOutput: self.printDebug(conn, id, 'QueryIns', 'result') return id @@ -271,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): @@ -306,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 @@ -493,17 +581,39 @@ 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) if PY2: - def PostgresBinaryConverter(value, db): - assert db == 'postgres' - return sqlrepr(bytes(value), db) + def escape_bytea(value): + return ''.join( + ['\\' + (x[1:].rjust(3, '0')) + for x in (oct(ord(c)) for c in value)] + ) else: - def PostgresBinaryConverter(value, db): - assert db == 'postgres' - return sqlrepr(value.decode('latin1'), db) + def escape_bytea(value): + return ''.join( + ['\\' + (x[2:].rjust(3, '0')) + for x in (oct(ord(c)) for c in value.decode('latin1'))] + ) + + +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) + + +def OdbcBinaryConverter(value, db): + assert db == 'postgres' + value = bytes(value) + if not PY2: + value = value.decode('latin1') + return value 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 2b4cdc79..f998c0a9 100644 --- a/sqlobject/sqlbuilder.py +++ b/sqlobject/sqlbuilder.py @@ -137,6 +137,18 @@ def __div__(self, other): def __rdiv__(self, other): return SQLOp("/", other, self) + def __truediv__(self, other): + return SQLOp("/", self, other) + + def __rtruediv__(self, other): + return SQLOp("/", other, self) + + def __floordiv__(self, other): + return SQLConstant("FLOOR")(SQLOp("/", self, other)) + + def __rfloordiv__(self, other): + return SQLConstant("FLOOR")(SQLOp("/", other, self)) + def __pos__(self): return SQLPrefix("+", self) @@ -288,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 @@ -296,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) @@ -465,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): @@ -487,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: @@ -550,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) @@ -613,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) @@ -623,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): @@ -1080,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 = '\\' @@ -1421,7 +1480,6 @@ class RLIKE(LIKE): 'maxdb': 'RLIKE', 'mysql': 'RLIKE', 'postgres': '~', - 'rdbhost': '~', 'sqlite': 'REGEXP' } @@ -1495,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 @@ -1510,6 +1569,7 @@ def __init__(self, clsName, registry=None): def __nonzero__(self): return True + __bool__ = __nonzero__ def __getattr__(self, attr): if self.soClass is None: diff --git a/sqlobject/sqlite/sqliteconnection.py b/sqlobject/sqlite/sqliteconnection.py index 70fae634..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.encodestring - sqlite.decode = base64.decodestring + 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] @@ -170,26 +134,24 @@ def releaseConnection(self, conn, explicit=False): return threadid = self._threadOrigination.get(id(conn)) DBAPI.releaseConnection(self, conn, explicit=explicit) - if (self._pool is not None and threadid and - threadid not in self._threadPool): + if (self._pool is not None and threadid + and threadid not in self._threadPool): self._threadPool[threadid] = conn 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() @@ -258,6 +234,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): # lastrowid is a DB-API extension from "PEP 0249": if id is None: id = int(c.lastrowid) + c.close() if self.debugOutput: self.printDebug(conn, id, 'QueryIns', 'result') return id @@ -289,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/sresults.py b/sqlobject/sresults.py index 9abfa899..4573cd10 100644 --- a/sqlobject/sresults.py +++ b/sqlobject/sresults.py @@ -214,8 +214,8 @@ def count(self): """ Counting elements of current select results """ assert not self.ops.get('start') and not self.ops.get('end'), \ "start/end/limit have no meaning with 'count'" - assert not (self.ops.get('distinct') and - (self.ops.get('start') or self.ops.get('end'))), \ + assert not (self.ops.get('distinct') + and (self.ops.get('start') or self.ops.get('end'))), \ "distinct-counting of sliced objects is not supported" if self.ops.get('distinct'): # Column must be specified, so we are using unique ID column. diff --git a/sqlobject/sybase/sybaseconnection.py b/sqlobject/sybase/sybaseconnection.py index f79441a2..11f44344 100644 --- a/sqlobject/sybase/sybaseconnection.py +++ b/sqlobject/sybase/sybaseconnection.py @@ -46,7 +46,9 @@ def insert_id(self, conn): """ c = conn.cursor() c.execute('SELECT @@IDENTITY') - return c.fetchone()[0] + result = c.fetchone()[0] + c.close() + return result def makeConnection(self): return self.module.connect(self.host, self.user, self.password, @@ -68,6 +70,7 @@ def _hasIdentity(self, conn, table): c = conn.cursor() c.execute(query) r = c.fetchone() + c.close() return r is not None def _queryInsertID(self, conn, soInstance, id, names, values): @@ -93,6 +96,7 @@ def _queryInsertID(self, conn, soInstance, id, names, values): c.execute(q) if has_identity and identity_insert_on: c.execute('SET IDENTITY_INSERT %s OFF' % table) + c.close() if id is None: id = self.insert_id(conn) if self.debugOutput: diff --git a/sqlobject/tests/dbtest.py b/sqlobject/tests/dbtest.py index 9102d99f..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,7 +101,10 @@ def getConnection(**kw): conn.debug = True if conftest.option.show_sql_output: conn.debugOutput = True - if conn.dbName == 'sqlite': + 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,7 +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': + if (connection.dbName == 'firebird'): use_microseconds(False) diff --git a/sqlobject/tests/test_ForeignKey.py b/sqlobject/tests/test_ForeignKey.py index 0e802750..50715eff 100644 --- a/sqlobject/tests/test_ForeignKey.py +++ b/sqlobject/tests/test_ForeignKey.py @@ -71,8 +71,8 @@ def test1(): assert s.count() == 1 assert s[0] == w1 s = SOTestWorkKey.select( - (SOTestWorkKey.q.composer == c) & - (SOTestWorkKey.q.title == 'Symphony No. 9')) + (SOTestWorkKey.q.composer == c) + & (SOTestWorkKey.q.title == 'Symphony No. 9')) assert s.count() == 1 assert s[0] == w1 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 e58ab1ae..ee4967ab 100644 --- a/sqlobject/tests/test_auto.py +++ b/sqlobject/tests/test_auto.py @@ -48,8 +48,8 @@ def test_dynamicColumn(self): nickname = StringCol('nickname', length=10) Person.sqlmeta.addColumn(nickname, changeSchema=True) Person(name='robert', nickname='bob') - assert ([p.name for p in Person.select('all')] == - ['bob', 'jake', 'jane', 'robert', 'tim']) + assert ([p.name for p in Person.select('all')] + == ['bob', 'jake', 'jane', 'robert', 'tim']) Person.sqlmeta.delColumn(nickname, changeSchema=True) def test_dynamicJoin(self): @@ -62,9 +62,9 @@ def test_dynamicJoin(self): phone.person = Person.selectBy(name='tim')[0] else: phone.person = Person.selectBy(name='bob')[0] - l = [p.phone for p in Person.selectBy(name='tim')[0].phones] - l.sort() - assert l == ['555-394-2930', '555-555-5555'] + _l = [p.phone for p in Person.selectBy(name='tim')[0].phones] + _l.sort() + assert _l == ['555-394-2930', '555-555-5555'] Phone.sqlmeta.delColumn(col, changeSchema=True) Person.sqlmeta.delJoin(join) @@ -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() @@ -193,7 +180,7 @@ def teardown_method(self, meth): if dropper: try: conn.query(dropper) - except: # Perhaps we don't have DROP permission + except Exception: # Perhaps we don't have DROP permission pass def test_classCreate(self): 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_blob.py b/sqlobject/tests/test_blob.py index 8c43d8a5..2c9b20ff 100644 --- a/sqlobject/tests/test_blob.py +++ b/sqlobject/tests/test_blob.py @@ -22,11 +22,13 @@ def test_BLOBCol(): else: data = bytes(range(256)) - prof = ImageData() - prof.image = data + prof = ImageData(image=data) iid = prof.id ImageData._connection.cache.clear() prof2 = ImageData.get(iid) assert prof2.image == data + + ImageData(image=b'string') + assert ImageData.selectBy(image=b'string').count() == 1 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_compat.py b/sqlobject/tests/test_compat.py new file mode 100644 index 00000000..3b5fc653 --- /dev/null +++ b/sqlobject/tests/test_compat.py @@ -0,0 +1,10 @@ +from sqlobject.compat import load_module_from_file + + +def test_load_module_from_path(): + module = load_module_from_file( + 'test_compat', 'sqlobject.tests.test_compat', __file__ + ) + assert module.__file__ == __file__ + assert module.__name__ == 'sqlobject.tests.test_compat' + assert module.__package__ == 'sqlobject.tests' 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_csvimport.py b/sqlobject/tests/test_csvimport.py new file mode 100644 index 00000000..990f4509 --- /dev/null +++ b/sqlobject/tests/test_csvimport.py @@ -0,0 +1,21 @@ +import csv +from datetime import datetime +from sqlobject.util.csvimport import load_csv + +csv_data = """\ +name:str,age:datetime,value:int +Test,2000-01-01 21:44:33,42""" + + +def test_load_csv(): + loaded = load_csv(csv.reader(csv_data.split('\n')), + default_class='SQLObject') + assert loaded == { + 'SQLObject': [ + { + 'age': datetime(2000, 1, 1, 21, 44, 33), + 'name': 'Test', + 'value': 42 + } + ] + } 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_declarative.py b/sqlobject/tests/test_declarative.py index 3bd452b8..4c75be6c 100644 --- a/sqlobject/tests/test_declarative.py +++ b/sqlobject/tests/test_declarative.py @@ -41,8 +41,8 @@ def __instanceinit__(self, new_attrs): def add_attrs(old_attrs, new_attrs): old_attrs = old_attrs[:] for name in new_attrs.keys(): - if (name in old_attrs or name.startswith('_') or - name in ('add_attrs', 'declarative_count', 'attrs')): + if (name in old_attrs or name.startswith('_') + or name in ('add_attrs', 'declarative_count', 'attrs')): continue old_attrs.append(name) old_attrs.sort() diff --git a/sqlobject/tests/test_enum.py b/sqlobject/tests/test_enum.py index d5cba13c..bf2078e4 100644 --- a/sqlobject/tests/test_enum.py +++ b/sqlobject/tests/test_enum.py @@ -10,66 +10,66 @@ class Enum1(SQLObject): - l = EnumCol(enumValues=['a', 'bcd', 'e']) + cl = EnumCol(enumValues=['a', 'bcd', 'e']) def testBad(): setupClass(Enum1) - for l in ['a', 'bcd', 'a', 'e']: - Enum1(l=l) + for _l in ['a', 'bcd', 'a', 'e']: + Enum1(cl=_l) raises( (Enum1._connection.module.IntegrityError, Enum1._connection.module.ProgrammingError, validators.Invalid), - Enum1, l='b') + Enum1, cl='b') class EnumWithNone(SQLObject): - l = EnumCol(enumValues=['a', 'bcd', 'e', None]) + cl = EnumCol(enumValues=['a', 'bcd', 'e', None]) def testNone(): setupClass(EnumWithNone) - for l in [None, 'a', 'bcd', 'a', 'e', None]: - e = EnumWithNone(l=l) - assert e.l == l + for _l in [None, 'a', 'bcd', 'a', 'e', None]: + e = EnumWithNone(cl=_l) + assert e.cl == _l class EnumWithDefaultNone(SQLObject): - l = EnumCol(enumValues=['a', 'bcd', 'e', None], default=None) + cl = EnumCol(enumValues=['a', 'bcd', 'e', None], default=None) def testDefaultNone(): setupClass(EnumWithDefaultNone) e = EnumWithDefaultNone() - assert e.l is None + assert e.cl is None class EnumWithDefaultOther(SQLObject): - l = EnumCol(enumValues=['a', 'bcd', 'e', None], default='a') + cl = EnumCol(enumValues=['a', 'bcd', 'e', None], default='a') def testDefaultOther(): setupClass(EnumWithDefaultOther) e = EnumWithDefaultOther() - assert e.l == 'a' + assert e.cl == 'a' class EnumUnicode(SQLObject): n = UnicodeCol() - l = EnumCol(enumValues=['a', 'b']) + cl = EnumCol(enumValues=['a', 'b']) def testUnicode(): setupClass(EnumUnicode) - EnumUnicode(n=u'a', l='a') - EnumUnicode(n=u'b', l=u'b') - EnumUnicode(n=u'\u201c', l='a') - EnumUnicode(n=u'\u201c', l=u'b') + EnumUnicode(n=u'a', cl='a') + EnumUnicode(n=u'b', cl=u'b') + EnumUnicode(n=u'\u201c', cl='a') + EnumUnicode(n=u'\u201c', cl=u'b') 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_joins.py b/sqlobject/tests/test_joins.py index 684d2a93..9cc7c19e 100644 --- a/sqlobject/tests/test_joins.py +++ b/sqlobject/tests/test_joins.py @@ -111,8 +111,8 @@ def test_basic(self): def test_defaultOrder(self): p1 = PersonJoiner2.byName('bob') - assert ([i.zip for i in p1.addressJoiner2s] == - ['33333', '22222', '11111']) + assert ([i.zip for i in p1.addressJoiner2s] + == ['33333', '22222', '11111']) _personJoiner3_getters = [] 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_new_joins.py b/sqlobject/tests/test_new_joins.py index 3a97e98b..82d27d34 100644 --- a/sqlobject/tests/test_new_joins.py +++ b/sqlobject/tests/test_new_joins.py @@ -116,8 +116,8 @@ def test_basic(self): def test_defaultOrder(self): p1 = PersonJNew2.byName('bob') - assert ([i.zip for i in p1.addressJ2s] == - ['33333', '22222', '11111']) + assert ([i.zip for i in p1.addressJ2s] + == ['33333', '22222', '11111']) _personJ3_getters = [] 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 41ef0efd..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 @@ -108,10 +108,10 @@ def test_select_getOne(): assert IterTest.selectBy(name='a').getOne() == a assert IterTest.select(IterTest.q.name == 'b').getOne() == b assert IterTest.selectBy(name='c').getOne(None) is None - raises(SQLObjectNotFound, 'IterTest.selectBy(name="c").getOne()') + raises(SQLObjectNotFound, IterTest.selectBy(name="c").getOne) IterTest(name='b') - raises(SQLObjectIntegrityError, 'IterTest.selectBy(name="b").getOne()') - raises(SQLObjectIntegrityError, 'IterTest.selectBy(name="b").getOne(None)') + raises(SQLObjectIntegrityError, IterTest.selectBy(name="b").getOne) + raises(SQLObjectIntegrityError, IterTest.selectBy(name="b").getOne, None) def test_selectBy(): @@ -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_select_through.py b/sqlobject/tests/test_select_through.py index b043a031..4852a349 100644 --- a/sqlobject/tests/test_select_through.py +++ b/sqlobject/tests/test_select_through.py @@ -40,7 +40,8 @@ def setup_module(mod): def testBadRef(): - pytest.raises(AttributeError, 'threes[0].throughTo.four') + with pytest.raises(AttributeError): + threes[0].throughTo.four def testThroughFK(): diff --git a/sqlobject/tests/test_sqlbuilder.py b/sqlobject/tests/test_sqlbuilder.py index 9a3b6e30..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 @@ -57,6 +57,14 @@ def test_modulo(): "(((so_test_sql_builder.so_value) % (2)) = (0))" +def test_division(): + SOTestSQLBuilder(name='test', so_value=-11) + assert sqlrepr(SOTestSQLBuilder.q.so_value / 4 == -2.75, 'mysql') == \ + "(((so_test_sql_builder.so_value) / (4)) = (-2.75))" + assert sqlrepr(SOTestSQLBuilder.q.so_value // 4 == -3, 'mysql') == \ + "((FLOOR(((so_test_sql_builder.so_value) / (4)))) = (-3))" + + def test_str_or_sqlrepr(): select = Select(['id', 'name'], staticTables=['employees'], where='value>0', orderBy='id') @@ -111,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 442ecf3c..8858bcab 100644 --- a/sqlobject/tests/test_sqlite.py +++ b/sqlobject/tests/test_sqlite.py @@ -1,7 +1,9 @@ +import math import threading import pytest -from sqlobject import SQLObject, StringCol +from sqlobject import SQLObject, StringCol, FloatCol from sqlobject.compat import string_type +from sqlobject.sqlbuilder import Select from sqlobject.tests.dbtest import getConnection, setupClass, supports from sqlobject.tests.dbtest import setSQLiteConnectionFactory from .test_basic import SOTestSO1 @@ -14,7 +16,7 @@ pass else: if connection.dbName != "sqlite": - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require SQLite") class SQLiteFactoryTest(SQLObject): @@ -23,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): @@ -44,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): @@ -70,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): @@ -138,9 +129,32 @@ def test_memorydb(): def test_list_databases(): - assert connection.listDatabases() == ['main'] + assert 'main' in connection.listDatabases() def test_list_tables(): setupClass(SOTestSO1) assert SOTestSO1.sqlmeta.table in connection.listTables() + + +class SQLiteTruedivTest(SQLObject): + value = FloatCol() + + +def test_truediv(): + setupClass(SQLiteTruedivTest) + + if SQLiteTruedivTest._connection.dbName == "sqlite": + def SQLiteConnectionFactory(sqlite): + class MyConnection(sqlite.Connection): + def __init__(self, *args, **kwargs): + super(MyConnection, self).__init__(*args, **kwargs) + self.create_function("floor", 1, math.floor) + return MyConnection + + setSQLiteConnectionFactory(SQLiteTruedivTest, SQLiteConnectionFactory) + + SQLiteTruedivTest(value=-5.0) + assert SQLiteTruedivTest._connection.queryAll( + SQLiteTruedivTest._connection.sqlrepr( + Select(SQLiteTruedivTest.q.value // 4)))[0][0] == -2 diff --git a/sqlobject/tests/test_transactions.py b/sqlobject/tests/test_transactions.py index 0fbac423..ee66620b 100644 --- a/sqlobject/tests/test_transactions.py +++ b/sqlobject/tests/test_transactions.py @@ -1,7 +1,8 @@ import pytest from sqlobject import SQLObject, SQLObjectNotFound, StringCol from sqlobject.tests.dbtest import raises, setupClass, supports - +from sqlobject import events +from sqlobject.main import sqlmeta ######################################## # Transaction test @@ -15,7 +16,7 @@ pass else: if not support_transactions: - pytestmark = pytest.mark.skip('') + pytestmark = pytest.mark.skip("These tests require transactions") class SOTestSOTrans(SQLObject): @@ -24,7 +25,25 @@ class sqlmeta: name = StringCol(length=10, alternateID=True, dbName='name_col') +def make_watcher(): + log = [] + + def watch(*args): + log.append(args) + + watch.log = log + return watch + + +def make_listen(signal): + watcher = make_watcher() + events.listen(watcher, sqlmeta, signal) + return watcher + + def test_transaction(): + commit_watcher = make_listen(events.CommitSignal) + rollback_watcher = make_listen(events.RollbackSignal) setupClass(SOTestSOTrans) SOTestSOTrans(name='bob') SOTestSOTrans(name='tim') @@ -34,8 +53,8 @@ def test_transaction(): SOTestSOTrans(name='joe', connection=trans) trans.rollback() trans.begin() - assert ([n.name for n in SOTestSOTrans.select(connection=trans)] == - ['bob', 'tim']) + assert ([n.name for n in SOTestSOTrans.select(connection=trans)] + == ['bob', 'tim']) b = SOTestSOTrans.byName('bob', connection=trans) b.name = 'robert' trans.commit() @@ -44,13 +63,22 @@ def test_transaction(): trans.rollback() trans.begin() assert b.name == 'robert' + assert len(commit_watcher.log) == 1 + assert commit_watcher.log[0][0][0][0] == 'SOTestSOTrans' + assert commit_watcher.log[0][0][0][1] == [1, 2] + assert len(rollback_watcher.log) == 2 + assert rollback_watcher.log[0][0][0][0] == 'SOTestSOTrans' + assert rollback_watcher.log[0][0][0][1] == [3] + assert rollback_watcher.log[1][0][0][0] == 'SOTestSOTrans' + assert rollback_watcher.log[1][0][0][1] == [1, 2] finally: SOTestSOTrans._connection.autoCommit = True def test_transaction_commit_sync(): setupClass(SOTestSOTrans) - trans = SOTestSOTrans._connection.transaction() + connection = SOTestSOTrans._connection + trans = connection.transaction() try: SOTestSOTrans(name='bob') bOut = SOTestSOTrans.byName('bob') @@ -60,14 +88,16 @@ def test_transaction_commit_sync(): trans.commit() assert bOut.name == 'robert' finally: - SOTestSOTrans._connection.autoCommit = True + trans.rollback() + connection.autoCommit = True + connection.close() def test_transaction_delete(close=False): setupClass(SOTestSOTrans) connection = SOTestSOTrans._connection if (connection.dbName == 'sqlite') and connection._memory: - pytest.skip("The following test requires a different connection") + pytest.skip("The test doesn't work with sqlite memory connection") trans = connection.transaction() try: SOTestSOTrans(name='bob') @@ -79,8 +109,9 @@ def test_transaction_delete(close=False): bOutID = bOutInst.id # noqa: bOutID is used in the string code below trans.commit(close=close) assert bOut.count() == 0 - raises(SQLObjectNotFound, "SOTestSOTrans.get(bOutID)") - raises(SQLObjectNotFound, "bOutInst.name") + raises(SQLObjectNotFound, SOTestSOTrans.get, bOutID) + with raises(SQLObjectNotFound): + bOutInst.name finally: trans.rollback() connection.autoCommit = True 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/sqlobject/tests/test_validation.py b/sqlobject/tests/test_validation.py index 86f1a10a..ae62f3c5 100644 --- a/sqlobject/tests/test_validation.py +++ b/sqlobject/tests/test_validation.py @@ -63,6 +63,7 @@ def __int__(self): class SOValidationTestBool(SOValidationTest): def __nonzero__(self): return self.value + __bool__ = __nonzero__ class SOValidationTestFloat(SOValidationTest): @@ -86,7 +87,9 @@ def test_confirmType(self): raises(validators.Invalid, setattr, t, 'name2', 1) raises(validators.Invalid, setattr, t, 'name3', '1') raises(validators.Invalid, setattr, t, 'name4', '1') - raises(validators.Invalid, setattr, t, 'name6', '1') + if t._connection.dbName != 'postgres' or \ + t._connection.driver not in ('odbc', 'pyodbc', 'pypyodbc'): + raises(validators.Invalid, setattr, t, 'name6', '1') raises(validators.Invalid, setattr, t, 'name7', 1) t.name2 = 'you' assert t.name2 == 'you' diff --git a/sqlobject/tests/test_views.py b/sqlobject/tests/test_views.py index 08f44aba..44ed0457 100644 --- a/sqlobject/tests/test_views.py +++ b/sqlobject/tests/test_views.py @@ -97,7 +97,7 @@ def testAliasOverride(): def checkAttr(cls, id, attr, value): - assert getattr(cls.get(id), attr) == value + assert getattr(cls.get(id), attr) == value def testGetVPC(): diff --git a/sqlobject/util/csvimport.py b/sqlobject/util/csvimport.py index ba0fa0d2..e2ec686a 100644 --- a/sqlobject/util/csvimport.py +++ b/sqlobject/util/csvimport.py @@ -88,8 +88,8 @@ class names will be attributes of that. """ objects = {} classnames = data.keys() - if (not keyorder and isinstance(class_getter, types.ModuleType) and - hasattr(class_getter, 'soClasses')): + if (not keyorder and isinstance(class_getter, types.ModuleType) + and hasattr(class_getter, 'soClasses')): keyorder = [c.__name__ for c in class_getter.soClasses] if not keyorder: classnames.sort() diff --git a/sqlobject/util/moduleloader.py b/sqlobject/util/moduleloader.py index bbb8d0c6..ee7fb2ef 100644 --- a/sqlobject/util/moduleloader.py +++ b/sqlobject/util/moduleloader.py @@ -1,6 +1,6 @@ -import imp import os import sys +from sqlobject.compat import load_module_from_file def load_module(module_name): @@ -24,7 +24,6 @@ def load_module_from_name(filename, module_name): % (os.path.dirname(filename), e)) f.write('#\n') f.close() - fp = None if module_name in sys.modules: return sys.modules[module_name] if '.' in module_name: @@ -33,12 +32,4 @@ def load_module_from_name(filename, module_name): load_module_from_name(os.path.dirname(filename), parent_name) else: base_name = module_name - fp = None - try: - fp, pathname, stuff = imp.find_module( - base_name, [os.path.dirname(filename)]) - module = imp.load_module(module_name, fp, pathname, stuff) - finally: - if fp is not None: - fp.close() - return module + return load_module_from_file(base_name, module_name, filename) diff --git a/tox.ini b/tox.ini index 3cd16776..04bc01f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,46 @@ [tox] -minversion = 1.8 -envlist = py{26,27}-{mysqldb,mysql-oursql},py{34,35}-{mysqlclient,pypostgresql},py{26,27,34,35}-{mysql-connector,pymysql,postgres-psycopg,postgres-pygresql,sqlite,sqlite-memory},py{27,34,35}-{firebird-fdb,firebirdsql},py{27,34}-flake8,py{27,34,35}-{mssql,mysql-connector,postgres-psycopg,sqlite,sqlite-memory}-w32 +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/ +commands = + {envpython} --version + {envpython} -c "import struct; print(struct.calcsize('P') * 8)" + {envpython} -m pytest --version deps = - pytest - pytest-cov - py{26,27}: FormEncode >= 1.1.1, != 1.3.0 - py{34,35}: FormEncode >= 1.3.1 - PyDispatcher>=2.0.4 - py{26,27}: egenix-mx-base + -rdevscripts/requirements/requirements_tests.txt + py34: zope.datetime < 4.3 + !py34: zope.datetime mysqldb: mysql-python - mysqlclient: mysqlclient + mysqlclient: -rdevscripts/requirements/requirements_mysqlclient.txt mysql-connector: mysql-connector - mysql-oursql: oursql - pymysql: pymysql - postgres-psycopg: psycopg2 - postgres-pygresql: pygresql - pypostgresql: py-postgresql + 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 - mssql: pymssql -passenv = CI TRAVIS TRAVIS_* PGPASSWORD +# Upgrade pip/setuptools/wheel +download = true +passenv = CI +setenv = + PGPASSWORD = test # Don't fail or warn on uninstalled commands +platform = linux whitelist_externals = + cmd mysql createdb dropdb @@ -38,285 +52,669 @@ whitelist_externals = # MySQL test environments [mysqldb] commands = - -mysql -e 'drop database sqlobject_test;' - mysql -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=mysqldb&debug=1 - mysql -e 'drop database sqlobject_test;' - -[testenv:py26-mysqldb] -commands = {[mysqldb]commands} + {[testenv]commands} + -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] -commands = {[mysqldb]commands} +[testenv:py27-mysqldb-noauto] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysqldb]commands} [mysqlclient] commands = - -mysql -e 'drop database sqlobject_test;' - mysql -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=mysqldb&charset=utf8&debug=1 - mysql -e 'drop database sqlobject_test;' + {[testenv]commands} + -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] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysqlclient] commands = {[mysqlclient]commands} [mysql-connector] commands = - -mysql -e 'drop database sqlobject_test;' - mysql -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=connector&charset=utf8&debug=1 - mysql -e 'drop database sqlobject_test;' - -[testenv:py26-mysql-connector] -commands = {[mysql-connector]commands} + {[testenv]commands} + -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 = {[mysql-connector]commands} - -[testenv:py34-mysql-connector] -commands = {[mysql-connector]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[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} -[oursql] +[mysql-connector_py] commands = - -mysql -e 'drop database sqlobject_test;' - mysql -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=oursql&charset=utf8&debug=1 - mysql -e 'drop database sqlobject_test;' + {[testenv]commands} + -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:py26-mysql-oursql] -commands = {[oursql]commands} +[testenv:py27-mysql-connector_py] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-connector_py]commands} -[testenv:py27-mysql-oursql] -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 = - -mysql -e 'drop database sqlobject_test;' - mysql -e 'create database sqlobject_test;' - pytest --cov=sqlobject -D mysql://root:@localhost/sqlobject_test?driver=pymysql&charset=utf8&debug=1 - mysql -e 'drop database sqlobject_test;' + {[testenv]commands} + -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:py26-pymysql] -commands = {[pymysql]commands} +[testenv:py27-mysql-pymysql] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pymysql]commands} -[testenv:py27-pymysql] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pymysql] commands = {[pymysql]commands} -[testenv:py34-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:py35-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 --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-noauto] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-pyodbc]commands} + +[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 --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-noauto] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-pypyodbc]commands} + +[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 = - -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]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:py26-postgres-psycopg] +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg] commands = {[psycopg]commands} -[testenv:py27-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:py34-postgres-psycopg] -commands = {[psycopg]commands} +[testenv:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-binary] +commands = {[psycopg-binary]commands} -[testenv:py35-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 = - -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 + {[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:py26-postgres-pygresql] -commands = {[pygresql]commands} +[testenv:py27-postgres-psycopg2] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-psycopg2] +commands = {[psycopg2]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:py27-postgres-psycopg2-binary] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2-binary]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-psycopg2-binary] +commands = {[psycopg2-binary]commands} + +[pygresql] +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:py27-postgres-pygresql] -commands = {[pygresql]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pygresql]commands} -[testenv:py34-postgres-pygresql] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pygresql] commands = {[pygresql]commands} -[testenv:py35-postgres-pygresql] -commands = {[pygresql]commands} +[pg8000] +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=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: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 --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-noauto] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[postgres-pyodbc]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pyodbc-noauto] +commands = {[postgres-pyodbc]commands} -[pypostgresql] +[postgres-pypyodbc] 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]commands} + -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:py34-pypostgresql] -commands = {[pypostgresql]commands} +[testenv:py27-postgres-pypyodbc-noauto] +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[postgres-pypyodbc]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pypyodbc-noauto] +commands = {[postgres-pypyodbc]commands} -[testenv:py35-pypostgresql] -commands = {[pypostgresql]commands} # SQLite test environments [sqlite] commands = - -rm /tmp/sqlobject_test.sqdb - pytest --cov=sqlobject -D sqlite:///tmp/sqlobject_test.sqdb?debug=1 - rm /tmp/sqlobject_test.sqdb - -[testenv:py26-sqlite] -commands = {[sqlite]commands} + {[testenv]commands} + -rm -f /tmp/sqlobject_test.sqdb + pytest -D "sqlite:///tmp/sqlobject_test.sqdb?debug=1" + rm -f /tmp/sqlobject_test.sqdb [testenv:py27-sqlite] -commands = {[sqlite]commands} - -[testenv:py34-sqlite] -commands = {[sqlite]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[sqlite]commands} -[testenv:py35-sqlite] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite] commands = {[sqlite]commands} [sqlite-memory] commands = - pytest --cov=sqlobject -D sqlite:/:memory:?debug=1 - -[testenv:py26-sqlite-memory] -commands = {[sqlite-memory]commands} + {[testenv]commands} + pytest -D sqlite:/:memory:?debug=1 [testenv:py27-sqlite-memory] -commands = {[sqlite-memory]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[sqlite-memory]commands} -[testenv:py34-sqlite-memory] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite-memory] commands = {[sqlite-memory]commands} -[testenv:py35-sqlite-memory] -commands = {[sqlite-memory]commands} # Firebird database test environments [fdb] 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' - sudo rm /tmp/test.fdb + pytest -D "firebird://test:test@localhost/tmp/test.fdb?debug=1" + sudo rm -f /tmp/test.fdb [testenv:py27-firebird-fdb] -commands = {[fdb]commands} - -[testenv:py34-firebird-fdb] -commands = {[fdb]commands} +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[fdb]commands} -[testenv:py35-firebird-fdb] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-firebird-fdb] commands = {[fdb]commands} [firebirdsql] 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' - sudo rm /tmp/test.fdb + 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] -commands = {[firebirdsql]commands} +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} # Special test environments -[testenv:py27-flake8] +[testenv:py{27,34,35,36,37,38,39,310,311,312,313,314}-flake8] changedir = ./ deps = flake8 -commands = flake8 . + pytest +commands = + {[testenv]commands} + flake8 . -[testenv:py34-flake8] -changedir = ./ -deps = - flake8 -commands = flake8 . # Windows testing -[mssql-w32] +[mssql-pyodbc-w32] +platform = win32 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=pymssql&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-w32] -commands = {[mssql-w32]commands} +[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-w32] -commands = {[mssql-w32]commands} +[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-w32] -commands = {[mssql-w32]commands} +[mysqldb-w32] +platform = win32 +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:py27-mysqldb-noauto-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysqldb-w32]commands} + +[mysqlclient-w32] +platform = win32 +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 = - -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&debug=1" - mysql -u root "-pPassword12!" -e 'drop database sqlobject_test;' + {[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&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" [testenv:py27-mysql-connector-w32] -commands = {[mysql-connector-w32]commands} +platform = win32 +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] -commands = {[mysql-connector-w32]commands} +[mysql-connector_py-w32] +platform = win32 +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:py27-mysql-connector_py-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-connector_py-w32]commands} -[psycopg-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-connector_py-w32] +platform = win32 +commands = {[mysql-connector_py-w32]commands} + +[pymysql-w32] +platform = win32 +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=pymysql&charset=utf8&debug=1" + mysql --user=ODBC -e "drop database sqlobject_test;" + +[testenv:py27-mysql-pymysql-w32] +platform = win32 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 + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pymysql-w32]commands} -[testenv:py27-postgres-psycopg-w32] -commands = {[psycopg-w32]commands} +[testenv:py3{4,5,6,7,8,9,10,11,12}-mysql-pymysql-w32] +platform = win32 +commands = {[pymysql-w32]commands} -[testenv:py34-postgres-psycopg-w32] -commands = {[psycopg-w32]commands} +[mariadb-w32] +platform = win32 +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:py3{6,7,8,9,10,11,12,13,14}-mariadb-w32] +platform = win32 +commands = {[mariadb-w32]commands} + +[mysql-pyodbc-w32] +platform = win32 +commands = + {[testenv]commands} + {envpython} -c "import pyodbc; print(pyodbc.drivers())" + -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-noauto-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-pyodbc-w32]commands} -[testenv:py35-postgres-psycopg-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} + +[mysql-pypyodbc-w32] +platform = win32 +commands = + {[testenv]commands} + {envpython} -c "import pypyodbc; print(pypyodbc.drivers())" + -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-noauto-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[mysql-pypyodbc-w32]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-mysql-pypyodbc-noauto-w32] +platform = win32 +commands = {[mysql-pypyodbc-w32]commands} + +[psycopg-w32] +platform = win32 +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:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-w32] +platform = win32 commands = {[psycopg-w32]commands} +[psycopg-binary-w32] +platform = win32 +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:py3{6,7,8,9,10,11,12,13,14}-postgres-psycopg-binary-w32] +platform = win32 +commands = {[psycopg-binary-w32]commands} + +[psycopg_c-w32] +platform = win32 +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: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 --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-psycopg2-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2-w32]commands} + +[testenv:py3{4,5,6,7,9,10,11,12,13,14}-postgres-psycopg2-w32] +platform = win32 +commands = {[psycopg2-w32]commands} + +[psycopg2-binary-w32] +platform = win32 +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:py27-postgres-psycopg2-binary-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[psycopg2-binary-w32]commands} + +[testenv:py3{4,5,6,7,9,10,11,12,13,14}-postgres-psycopg2-binary-w32] +platform = win32 +commands = {[psycopg2-binary-w32]commands} + +[pygresql-w32] +platform = win32 +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:py27-postgres-pygresql-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[pygresql-w32]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pygresql-w32] +platform = win32 +commands = {[pygresql-w32]commands} + +[pg8000-w32] +platform = win32 +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=pg8000&charset=utf-8&debug=1" + dropdb --username=runner --no-password sqlobject_test + +[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:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pg8000-w32] +platform = win32 +commands = {[pg8000-w32]commands} + +[postgres-pyodbc-w32] +platform = win32 +commands = + {[testenv]commands} + {envpython} -c "import pyodbc; print(pyodbc.drivers())" + -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-noauto-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[postgres-pyodbc-w32]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pyodbc-noauto-w32] +platform = win32 +commands = {[postgres-pyodbc-w32]commands} + +[postgres-pypyodbc-w32] +platform = win32 +commands = + {[testenv]commands} + {envpython} -c "import pypyodbc; print(pypyodbc.drivers())" + -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-noauto-w32] +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[postgres-pypyodbc-w32]commands} + +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-postgres-pypyodbc-noauto-w32] +platform = win32 +commands = {[postgres-pypyodbc-w32]commands} + [sqlite-w32] +platform = win32 commands = - pytest --cov=sqlobject -D sqlite:/C:/projects/sqlobject/sqlobject_test.sqdb?debug=1 + {[testenv]commands} + pytest -D sqlite:/{env:TEMP}/sqlobject_test.sqdb?debug=1 + cmd /c "del {env:TEMP}\sqlobject_test.sqdb" [testenv:py27-sqlite-w32] -commands = {[sqlite-w32]commands} - -[testenv:py34-sqlite-w32] -commands = {[sqlite-w32]commands} +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[sqlite-w32]commands} -[testenv:py35-sqlite-w32] +[testenv:py3{4,5,6,7,8,9,10,11,12,13,14}-sqlite-w32] +platform = win32 commands = {[sqlite-w32]commands} [sqlite-memory-w32] +platform = win32 commands = - pytest --cov=sqlobject -D sqlite:/:memory:?debug=1 + {[testenv]commands} + pytest -D sqlite:/:memory:?debug=1 [testenv:py27-sqlite-memory-w32] -commands = {[sqlite-memory-w32]commands} - -[testenv:py34-sqlite-memory-w32] -commands = {[sqlite-memory-w32]commands} +platform = win32 +commands = + easy_install -i https://downloads.egenix.com/python/index/ucs2/ egenix-mx-base + {[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}