diff --git a/.appveyor.yml b/.appveyor.yml index a49c802f..1761e99f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,43 +13,17 @@ environment: matrix: # For Python versions available on Appveyor, see # http://www.appveyor.com/docs/installed-software#python + - {PYVER: "27", PYTHON_ARCH: "32"} + - {PYVER: "27", PYTHON_ARCH: "64"} + - {PYVER: "34", PYTHON_ARCH: "32"} + - {PYVER: "34", PYTHON_ARCH: "64"} + - {PYVER: "35", PYTHON_ARCH: "32"} + - {PYVER: "35", PYTHON_ARCH: "64"} + - {PYVER: "36", PYTHON_ARCH: "32"} + - {PYVER: "36", PYTHON_ARCH: "64"} - # Py 2.7 = VS Ver. 9.0 (VS 2008) - # Py 3.4 = VS Ver. 10.0 (VS 2010) - # Py 3.5, 3.6 = VS Ver. 14.0 (VS 2015) - - - PYTHON: C:\Python27-x64 - PYTHON_ARCH: 64 - VS_VER: 9.0 - - - PYTHON: C:\Python27 - PYTHON_ARCH: 32 - VS_VER: 9.0 - - - PYTHON: C:\Python36-x64 - PYTHON_ARCH: 64 - VS_VER: 14.0 - - - PYTHON: C:\Python36 - PYTHON_ARCH: 32 - VS_VER: 14.0 - - - PYTHON: C:\Python35-x64 - PYTHON_ARCH: 64 - VS_VER: 14.0 - - - PYTHON: C:\Python35 - PYTHON_ARCH: 32 - VS_VER: 14.0 - - - PYTHON: C:\Python34-x64 - DISTUTILS_USE_SDK: '1' - PYTHON_ARCH: 64 - VS_VER: 10.0 - - - PYTHON: C:\Python34 - PYTHON_ARCH: 32 - VS_VER: 10.0 + OPENSSL_VERSION: "1_0_2n" + POSTGRES_VERSION: "10_1" PSYCOPG2_TESTDB: psycopg2_test PSYCOPG2_TESTDB_USER: postgres @@ -77,7 +51,22 @@ cache: init: # Uncomment next line to get RDP access during the build. #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - # + + # Set env variable according to the build environment + - SET PYTHON=C:\Python%PYVER% + - IF "%PYTHON_ARCH%"=="64" SET PYTHON=%PYTHON%-x64 + + # Py 2.7 = VS Ver. 9.0 (VS 2008) + # Py 3.3, 3.4 = VS Ver. 10.0 (VS 2010) + # Py 3.5, 3.6 = VS Ver. 14.0 (VS 2015) + - IF "%PYVER%"=="27" SET VS_VER=9.0 + - IF "%PYVER%"=="33" SET VS_VER=10.0 + - IF "%PYVER%"=="34" SET VS_VER=10.0 + - IF "%PYVER%"=="35" SET VS_VER=14.0 + - IF "%PYVER%"=="36" SET VS_VER=14.0 + + - IF "%VS_VER%"=="10.0" IF "%PYTHON_ARCH%"=="64" SET DISTUTILS_USE_SDK=1 + # Set Python to the path - SET PATH=%PYTHON%;%PYTHON%\Scripts;C:\Program Files\Git\mingw64\bin;%PATH% @@ -153,8 +142,8 @@ install: } # Download OpenSSL source - CD C:\Others - - IF NOT EXIST OpenSSL_1_0_2m.zip ( - curl -fsSL -o OpenSSL_1_0_2m.zip https://github.com/openssl/openssl/archive/OpenSSL_1_0_2m.zip + - IF NOT EXIST OpenSSL_%OPENSSL_VERSION%.zip ( + curl -fsSL -o OpenSSL_%OPENSSL_VERSION%.zip https://github.com/openssl/openssl/archive/OpenSSL_%OPENSSL_VERSION%.zip ) # To use OpenSSL >= 1.1.0, both libpq and psycopg build environments have @@ -166,15 +155,15 @@ install: # - nmake build_libs install_dev - IF NOT EXIST %OPENSSLTOP%\lib\ssleay32.lib ( CD %BUILD_DIR% && - 7z x C:\Others\OpenSSL_1_0_2m.zip && - CD openssl-OpenSSL_1_0_2m && + 7z x C:\Others\OpenSSL_%OPENSSL_VERSION%.zip && + CD openssl-OpenSSL_%OPENSSL_VERSION% && perl Configure %TARGET% no-asm no-shared no-zlib --prefix=%OPENSSLTOP% --openssldir=%OPENSSLTOP% && CALL ms\%DO% && nmake -f ms\nt.mak init headers lib && COPY inc32\openssl\*.h %OPENSSLTOP%\include\openssl && COPY out32\*.lib %OPENSSLTOP%\lib && CD %BASE_DIR% && - RMDIR /S /Q %BUILD_DIR%\openssl-OpenSSL_1_0_2m + RMDIR /S /Q %BUILD_DIR%\openssl-OpenSSL_%OPENSSL_VERSION% ) # Setup directories for building PostgreSQL librarires @@ -188,8 +177,8 @@ install: # Download PostgreSQL source - CD C:\Others - - IF NOT EXIST postgres-REL_10_1.zip ( - curl -fsSL -o postgres-REL_10_1.zip https://github.com/postgres/postgres/archive/REL_10_1.zip + - IF NOT EXIST postgres-REL_%POSTGRES_VERSION%.zip ( + curl -fsSL -o postgres-REL_%POSTGRES_VERSION%.zip https://github.com/postgres/postgres/archive/REL_%POSTGRES_VERSION%.zip ) # Setup build config file (config.pl) @@ -200,11 +189,11 @@ install: # Prepare local include directory for building from # Build pg_config in place # NOTE: Cannot set and use the same variable inside an IF - - SET PGBUILD=%BUILD_DIR%\postgres-REL_10_1 + - SET PGBUILD=%BUILD_DIR%\postgres-REL_%POSTGRES_VERSION% - IF NOT EXIST %PGTOP%\lib\libpq.lib ( CD %BUILD_DIR% && - 7z x C:\Others\postgres-REL_10_1.zip && - CD postgres-REL_10_1\src\tools\msvc && + 7z x C:\Others\postgres-REL_%POSTGRES_VERSION%.zip && + CD postgres-REL_%POSTGRES_VERSION%\src\tools\msvc && ECHO $config-^>{ldap} = 0; > config.pl && ECHO $config-^>{openssl} = "%OPENSSLTOP:\=\\%"; >> config.pl && ECHO.>> config.pl && diff --git a/.travis.yml b/.travis.yml index 2fbf7010..51cba9a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,14 @@ language: python python: - 2.7 + - 3.7-dev - 3.6 - 3.5 - 3.4 install: - - python setup.py install + - pip install -U pip setuptools wheel + - pip install . - rm -rf psycopg2.egg-info - sudo scripts/travis_prepare.sh diff --git a/LICENSE b/LICENSE index 360a44f6..bdeaf9c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ psycopg2 and the LGPL -===================== +--------------------- psycopg2 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published @@ -29,15 +29,15 @@ If not, see . Alternative licenses -==================== +-------------------- If you prefer you can use the Zope Database Adapter ZPsycopgDA (i.e., -every file inside the ZPsycopgDA directory) user the ZPL license as +every file inside the ZPsycopgDA directory) using the ZPL license as published on the Zope web site, http://www.zope.org/Resources/ZPL. Also, the following BSD-like license applies (at your option) to the -files following the pattern psycopg/adapter*.{h,c} and -psycopg/microprotocol*.{h,c}: +files following the pattern ``psycopg/adapter*.{h,c}`` and +``psycopg/microprotocol*.{h,c}``: Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it diff --git a/NEWS b/NEWS index aca5cd0b..c3268d4e 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,10 @@ Current release What's new in psycopg 2.8 ------------------------- +New features: + +- Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`). + Other changes: - Dropped support for Python 2.6, 3.2, 3.3. @@ -15,17 +19,41 @@ Other changes: install``. +What's new in psycopg 2.7.5 +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Allow non-ascii chars in namedtuple fields (regression introduced fixing + :ticket:`#211`). +- Fixed adaptation of arrays of arrays of nulls (:ticket:`#325`). +- Fixed building on Solaris 11 and derivatives such as SmartOS and illumos + (:ticket:`#677`). +- Maybe fixed building on MSYS2 (as reported in :ticket:`#658`). +- Allow string subclasses in connection and other places (:ticket:`#679`). +- Don't raise an exception closing an unused named cursor (:ticket:`#716`). + + What's new in psycopg 2.7.4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- Moving away from installing the wheel package by default. + Packages installed from wheel raise a warning on import. Added package + ``psycopg2-binary`` to install from wheel instead (:ticket:`#543`). +- Convert fields names into valid Python identifiers in + `~psycopg2.extras.NamedTupleCursor` (:ticket:`#211`). - Fixed Solaris 10 support (:ticket:`#532`). +- `cursor.mogrify()` can be called on closed cursors (:ticket:`#579`). +- Fixed setting session characteristics in corner cases on autocommit + connections (:ticket:`#580`). - Fixed `~psycopg2.extras.MinTimeLoggingCursor` on Python 3 (:ticket:`#609`). - Fixed parsing of array of points as floats (:ticket:`#613`). - Fixed `~psycopg2.__libpq_version__` building with libpq >= 10.1 (:ticket:`632`). -- Fixed `~cursor.rowcount` after `~cursor.executemany()` with :sql:`RETURNING` statements - (:ticket:`633`). -- Wheel packages compiled against PostgreSQL 10.1 libpq and OpenSSL 1.0.2m. +- Fixed `~cursor.rowcount` after `~cursor.executemany()` with :sql:`RETURNING` + statements (:ticket:`633`). +- Fixed compatibility problem with pypy3 (:ticket:`#649`). +- Wheel packages compiled against PostgreSQL 10.1 libpq and OpenSSL 1.0.2n. +- Wheel packages for Python 2.6 no more available (support dropped from + wheel building infrastructure). What's new in psycopg 2.7.3.2 diff --git a/README.rst b/README.rst index 69b1d83d..a9785e3a 100644 --- a/README.rst +++ b/README.rst @@ -25,29 +25,40 @@ Documentation is included in the ``doc`` directory and is `available online`__. .. __: http://initd.org/psycopg/docs/ +For any other resource (source code repository, bug tracker, mailing list) +please check the `project homepage`__. + Installation ------------ -If your ``pip`` version supports wheel_ packages it should be possible to -install a binary version of Psycopg including all the dependencies from PyPI_. -Just run:: +Building Psycopg requires a few prerequisites (a C compiler, some development +packages): please check the install_ and the faq_ documents in the ``doc`` dir +or online for the details. + +If prerequisites are met, you can install psycopg like any other Python +package, using ``pip`` to download it from PyPI_:: - $ pip install -U pip # make sure your pip is up-to-date $ pip install psycopg2 -If you want to build Psycopg from source you will need some prerequisites (a C -compiler, development packages): please check the install_ and the faq_ -documents in the ``doc`` dir for the details. +or using ``setup.py`` if you have downloaded the source package locally:: -.. _wheel: http://pythonwheels.com/ -.. _PyPI: https://pypi.python.org/pypi/psycopg2 + $ python setup.py build + $ sudo python setup.py install + +You can also obtain a stand-alone package, not requiring a compiler or +external libraries, by installing the `psycopg2-binary`_ package from PyPI:: + + $ pip install psycopg2-binary + +The binary package is a practical choice for development and testing but in +production it is advised to use the package built from sources. + +.. _PyPI: https://pypi.org/project/psycopg2/ +.. _psycopg2-binary: https://pypi.org/project/psycopg2-binary/ .. _install: http://initd.org/psycopg/docs/install.html#install-from-source .. _faq: http://initd.org/psycopg/docs/faq.html#faq-compile -For any other resource (source code repository, bug tracker, mailing list) -please check the `project homepage`__. - .. __: http://initd.org/psycopg/ diff --git a/doc/Makefile b/doc/Makefile index 2903b9d0..558d0a75 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -29,8 +29,6 @@ doctest: upload: # this command requires ssh configured to the proper target tar czf - -C html . | ssh psycoweb tar xzvf - -C docs/current - # this command requires a .pypirc with the right privileges - # python src/tools/pypi_docs_upload.py psycopg2 $$(pwd)/html clean: $(MAKE) $(SPHOPTS) -C src $@ diff --git a/doc/release.rst b/doc/release.rst index 3576cdc9..3ea4a9fa 100644 --- a/doc/release.rst +++ b/doc/release.rst @@ -100,5 +100,5 @@ Test packages may be uploaded on the `PyPI testing site`__ using:: assuming `proper configuration`__ of ``~/.pypirc``. -.. __: https://testpypi.python.org/pypi/psycopg2 +.. __: https://test.pypi.org/project/psycopg2/ .. __: https://wiki.python.org/moin/TestPyPI diff --git a/doc/src/advanced.rst b/doc/src/advanced.rst index d1683b8b..724cb281 100644 --- a/doc/src/advanced.rst +++ b/doc/src/advanced.rst @@ -485,7 +485,7 @@ details. You can check the `psycogreen`_ project for further informations and resources about the topic. .. _coroutine: http://en.wikipedia.org/wiki/Coroutine -.. _greenlet: http://pypi.python.org/pypi/greenlet +.. _greenlet: https://pypi.org/project/greenlet/ .. _green threads: http://en.wikipedia.org/wiki/Green_threads .. _Eventlet: http://eventlet.net/ .. _gevent: http://www.gevent.org/ diff --git a/doc/src/conf.py b/doc/src/conf.py index a27d6cf4..2c52a568 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -61,8 +61,8 @@ except ImportError: release = version intersphinx_mapping = { - 'py': ('http://docs.python.org/2', None), - 'py3': ('http://docs.python.org/3', None), + 'py': ('https://docs.python.org/2', None), + 'py3': ('https://docs.python.org/3', None), } # Pattern to generate links to the bug tracker diff --git a/doc/src/connection.rst b/doc/src/connection.rst index 6f13ecd3..2910f301 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -41,11 +41,6 @@ The ``connection`` class previously only valid PostgreSQL identifiers were accepted as cursor name. - .. warning:: - It is unsafe to expose the *name* to an untrusted source, for - instance you shouldn't allow *name* to be read from a HTML form. - Consider it as part of the query, not as a query parameter. - The *cursor_factory* argument can be used to create non-standard cursors. The class returned must be a subclass of `psycopg2.extensions.cursor`. See :ref:`subclassing-cursor` for diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 8545fcfa..34d53a7e 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -555,6 +555,38 @@ Other functions .. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER +.. method:: encrypt_password(password, user, scope=None, algorithm=None) + + Return the encrypted form of a PostgreSQL password. + + :param password: the cleartext password to encrypt + :param user: the name of the user to use the password for + :param scope: the scope to encrypt the password into; if *algorithm* is + ``md5`` it can be `!None` + :type scope: `connection` or `cursor` + :param algorithm: the password encryption algorithm to use + + The *algorithm* ``md5`` is always supported. Other algorithms are only + supported if the client libpq version is at least 10 and may require a + compatible server version: check the `PostgreSQL encryption + documentation`__ to know the algorithms supported by your server. + + .. __: https://www.postgresql.org/docs/current/static/encryption-options.html + + Using `!None` as *algorithm* will result in querying the server to know the + current server password encryption setting, which is a blocking operation: + query the server separately and specify a value for *algorithm* if you + want to maintain a non-blocking behaviour. + + .. versionadded:: 2.8 + + .. seealso:: PostgreSQL docs for the `password_encryption`__ setting, libpq `PQencryptPasswordConn()`__, `PQencryptPassword()`__ functions. + + .. __: https://www.postgresql.org/docs/current/static/runtime-config-connection.html#GUC-PASSWORD-ENCRYPTION + .. __: https://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN + .. __: https://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORD + + .. index:: pair: Isolation level; Constants diff --git a/doc/src/index.rst b/doc/src/index.rst index 852bbc2c..7ae073d7 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -48,6 +48,7 @@ Psycopg 2 is both Unicode and Python 3 friendly. errorcodes faq news + license .. ifconfig:: builder != 'text' diff --git a/doc/src/install.rst b/doc/src/install.rst index 6d30e343..f5524a56 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -12,16 +12,6 @@ to use Psycopg on a different Python implementation (PyPy, Jython, IronPython) there is an experimental `porting of Psycopg for Ctypes`__, but it is not as mature as the C implementation yet. -The current `!psycopg2` implementation supports: - -.. - NOTE: keep consistent with setup.py and the /features/ page. - -- Python version 2.7 -- Python 3 versions from 3.4 to 3.6 -- PostgreSQL server versions from 7.4 to 10 -- PostgreSQL client library version from 9.1 - .. _PostgreSQL: http://www.postgresql.org/ .. _Python: http://www.python.org/ .. _libpq: http://www.postgresql.org/docs/current/static/libpq.html @@ -32,94 +22,20 @@ The current `!psycopg2` implementation supports: .. index:: - single: Install; from PyPI - single: Install; wheel - single: Wheel + single: Prerequisites -Binary install from PyPI ------------------------- +Prerequisites +------------- -`!psycopg2` is `available on PyPI`__ in the form of wheel_ packages for the -most common platform (Linux, OSX, Windows): this should make you able to -install a binary version of the module including all the dependencies simply -using: +The current `!psycopg2` implementation supports: -.. code-block:: console +.. + NOTE: keep consistent with setup.py and the /features/ page. - $ pip install psycopg2 - -Make sure to use an up-to-date version of :program:`pip` (you can upgrade it -using something like ``pip install -U pip``) - -.. __: PyPI_ -.. _PyPI: https://pypi.python.org/pypi/psycopg2/ -.. _wheel: http://pythonwheels.com/ - -.. note:: - - The binary packages come with their own versions of a few C libraries, - among which ``libpq`` and ``libssl``, which will be used regardless of other - libraries available on the client: upgrading the system libraries will not - upgrade the libraries used by `!psycopg2`. Please build `!psycopg2` from - source if you want to maintain binary upgradeability. - -.. warning:: - - Because the `!psycopg` wheel package uses its own ``libssl`` binary, it is - incompatible with other extension modules binding with ``libssl`` as well, - for instance the Python `ssl` module: the result will likely be a - segfault. If you need using both `!psycopg2` and other libraries using - ``libssl`` please :ref:`disable the use of wheel packages for Psycopg - `. - - - -.. index:: - single: Install; disable wheel - single: Wheel; disable - -.. _disable-wheel: - -Disabling wheel packages -^^^^^^^^^^^^^^^^^^^^^^^^ - -If you want to disable the use of wheel binary packages and use the system -system libraries available on your client you can use the :command:`pip` -|--no-binary option|__: - -.. code-block:: console - - $ pip install --no-binary psycopg2 - -.. |--no-binary option| replace:: ``--no-binary`` option -.. __: https://pip.pypa.io/en/stable/reference/pip_install/#install-no-binary - -which can be specified in your :file:`requirements.txt` files too, e.g. use: - -.. code-block:: none - - psycopg2>=2.7,<2.8 --no-binary psycopg2 - -to use the last bugfix release of the `!psycopg2` 2.7 package, specifying to -always compile it from source. Of course in this case you will have to meet -the :ref:`build prerequisites `. - - - -.. index:: - single: Install; from source - -.. _install-from-source: - -Install from source -------------------- - -.. _source-package: - -You can download a copy of Psycopg source files from the `Psycopg download -page`__ or from PyPI_. - -.. __: http://initd.org/psycopg/download/ +- Python version 2.7 +- Python 3 versions from 3.4 to 3.6 +- PostgreSQL server versions from 7.4 to 10 +- PostgreSQL client library version from 9.1 @@ -128,8 +44,10 @@ page`__ or from PyPI_. Build prerequisites ^^^^^^^^^^^^^^^^^^^ -These notes illustrate how to compile Psycopg on Linux. If you want to compile -Psycopg on other platforms you may have to adjust some details accordingly. +The build prerequisites are to be met in order to install Psycopg from source +code, from a source distribution package, GitHub_ or from PyPI. + +.. _GitHub: https://github.com/psycopg/psycopg2 Psycopg is a C wrapper around the libpq_ PostgreSQL client library. To install it from sources you will need: @@ -161,6 +79,12 @@ it from sources you will need: Once everything is in place it's just a matter of running the standard: +.. code-block:: console + + $ pip install psycopg2 + +or, from the directory containing the source code: + .. code-block:: console $ python setup.py build @@ -197,12 +121,92 @@ which is OS-dependent (for instance setting a suitable +.. index:: + single: Install; from PyPI + single: Install; wheel + single: Wheel + +Binary install from PyPI +------------------------ + +`!psycopg2` is also `available on PyPI`__ in the form of wheel_ packages for +the most common platform (Linux, OSX, Windows): this should make you able to +install a binary version of the module, not requiring the above build or +runtime prerequisites, simply using: + +.. code-block:: console + + $ pip install psycopg2-binary + +Make sure to use an up-to-date version of :program:`pip` (you can upgrade it +using something like ``pip install -U pip``) + +.. __: PyPI-binary_ +.. _PyPI-binary: https://pypi.org/project/psycopg2-binary/ +.. _wheel: http://pythonwheels.com/ + +.. note:: + + The binary packages come with their own versions of a few C libraries, + among which ``libpq`` and ``libssl``, which will be used regardless of other + libraries available on the client: upgrading the system libraries will not + upgrade the libraries used by `!psycopg2`. Please build `!psycopg2` from + source if you want to maintain binary upgradeability. + +.. warning:: + + The `!psycopg2` wheel package comes packaged, among the others, with its + own ``libssl`` binary. This may create conflicts with other extension + modules binding with ``libssl`` as well, for instance with the Python + `ssl` module: in some cases, under concurrency, the interaction between + the two libraries may result in a segfault. In case of doubts you are + advised to use a package built from source. + + + +.. index:: + single: Install; disable wheel + single: Wheel; disable + +.. _disable-wheel: + +Disabling wheel packages for Psycopg 2.7 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In version 2.7.x, `pip install psycopg2` would have tried to install the wheel +binary package of Psycopg. Because of the problems the wheel package have +displayed, `psycopg2-binary` has become a separate package, and from 2.8 it +has become the only way to install the binary package. + +If you are using psycopg 2.7 and you want to disable the use of wheel binary +packages, relying on the system system libraries available on your client, you +can use the :command:`pip` |--no-binary option|__, e.g.: + +.. code-block:: console + + $ pip install --no-binary :all: psycopg2 + +.. |--no-binary option| replace:: ``--no-binary`` option +.. __: https://pip.pypa.io/en/stable/reference/pip_install/#install-no-binary + +which can be specified in your :file:`requirements.txt` files too, e.g. use: + +.. code-block:: none + + psycopg2>=2.7,<2.8 --no-binary psycopg2 + +to use the last bugfix release of the `!psycopg2` 2.7 package, specifying to +always compile it from source. Of course in this case you will have to meet +the :ref:`build prerequisites `. + + + .. index:: single: setup.py single: setup.cfg Non-standard builds -^^^^^^^^^^^^^^^^^^^ +------------------- If you have less standard requirements such as: @@ -242,7 +246,7 @@ order to create a debug package: - Edit the ``setup.cfg`` file adding the ``PSYCOPG_DEBUG`` flag to the ``define`` option. -- :ref:`Compile and install ` the package. +- :ref:`Compile and install ` the package. - Set the :envvar:`PSYCOPG_DEBUG` environment variable: @@ -300,10 +304,14 @@ Try the following. *In order:* - Google for `!psycopg2` *your error message*. Especially useful the week after the release of a new OS X version. -- Write to the `Mailing List`__. +- Write to the `Mailing List`_. + +- If you think that you have discovered a bug, test failure or missing feature + please raise a ticket in the `bug tracker`_. - Complain on your blog or on Twitter that `!psycopg2` is the worst package ever and about the quality time you have wasted figuring out the correct :envvar:`ARCHFLAGS`. Especially useful from the Starbucks near you. -.. __: https://lists.postgresql.org/mj/mj_wwwusr?func=lists-long-full&extra=psycopg +.. _mailing list: https://lists.postgresql.org/mj/mj_wwwusr?func=lists-long-full&extra=psycopg +.. _bug tracker: https://github.com/psycopg/psycopg2/issues diff --git a/doc/src/license.rst b/doc/src/license.rst new file mode 100644 index 00000000..53a4e724 --- /dev/null +++ b/doc/src/license.rst @@ -0,0 +1,7 @@ +.. index:: + single: License + +License +======= + +.. include:: ../../LICENSE diff --git a/doc/src/news.rst b/doc/src/news.rst index d5b11a69..053d6464 100644 --- a/doc/src/news.rst +++ b/doc/src/news.rst @@ -1,3 +1,7 @@ +.. index:: + single: Release notes + single: News + Release notes ============= diff --git a/doc/src/pool.rst b/doc/src/pool.rst index 7624dc88..38cd08fa 100644 --- a/doc/src/pool.rst +++ b/doc/src/pool.rst @@ -24,13 +24,18 @@ directly in the client application. .. method:: getconn(key=None) - Get a free connection and assign it to *key* if not `!None`. + Get a free connection from the pool. + + The *key* parameter is optional: if used, the connection will be + associated to the key and calling `!getconn()` with the same key again + will return the same connection. .. method:: putconn(conn, key=None, close=False) Put away a connection. If *close* is `!True`, discard the connection from the pool. + *key* should be used consistently with `getconn()`. .. method:: closeall diff --git a/doc/src/tools/stitch_text.py b/doc/src/tools/stitch_text.py index dca745bd..e026622f 100755 --- a/doc/src/tools/stitch_text.py +++ b/doc/src/tools/stitch_text.py @@ -5,6 +5,7 @@ import os import sys + def main(): if len(sys.argv) != 3: sys.stderr.write("usage: %s index.rst text-dir\n") @@ -17,20 +18,20 @@ def main(): return 0 + def iter_file_base(fn): f = open(fn) - have_line = iter(f).__next__ - while not have_line().startswith('.. toctree'): + while not next(f).startswith('.. toctree'): pass - while have_line().strip().startswith(':'): + while next(f).strip().startswith(':'): pass yield os.path.splitext(os.path.basename(fn))[0] n = 0 while True: - line = have_line() + line = next(f) if line.isspace(): continue if line.startswith(".."): @@ -44,6 +45,7 @@ def iter_file_base(fn): # maybe format changed? raise Exception("Not enough files found. Format change in index.rst?") + def emit(basename, txt_dir): f = open(os.path.join(txt_dir, basename + ".txt")) for line in f: diff --git a/lib/extensions.py b/lib/extensions.py index 8644e411..3661e6cd 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -63,7 +63,7 @@ from psycopg2._psycopg import ( # noqa string_types, binary_types, new_type, new_array_type, register_type, ISQLQuote, Notify, Diagnostics, Column, QueryCanceledError, TransactionRollbackError, - set_wait_callback, get_wait_callback, ) + set_wait_callback, get_wait_callback, encrypt_password, ) """Isolation level values.""" diff --git a/lib/extras.py b/lib/extras.py index 1b0b2b6d..3ef223ee 100644 --- a/lib/extras.py +++ b/lib/extras.py @@ -344,7 +344,20 @@ class NamedTupleCursor(_cursor): return def _make_nt(self): - return namedtuple("Record", [d[0] for d in self.description or ()]) + # ascii except alnum and underscore + nochars = ' !"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~' + re_clean = _re.compile('[' + _re.escape(nochars) + ']') + + def f(s): + s = re_clean.sub('_', s) + # Python identifier cannot start with numbers, namedtuple fields + # cannot start with underscore. So... + if s[0] == '_' or '0' <= s[0] <= '9': + s = 'f' + s + + return s + + return namedtuple("Record", [f(d[0]) for d in self.description or ()]) class LoggingConnection(_connection): diff --git a/psycopg/adapter_datetime.c b/psycopg/adapter_datetime.c index 9d04df40..a78311ee 100644 --- a/psycopg/adapter_datetime.c +++ b/psycopg/adapter_datetime.c @@ -100,7 +100,7 @@ _pydatetime_string_delta(pydatetimeObject *self) char buffer[8]; int i; - int a = obj->microseconds; + int a = PyDateTime_DELTA_GET_MICROSECONDS(obj); for (i=0; i < 6 ; i++) { buffer[5-i] = '0' + (a % 10); @@ -109,7 +109,9 @@ _pydatetime_string_delta(pydatetimeObject *self) buffer[6] = '\0'; return Bytes_FromFormat("'%d days %d.%s seconds'::interval", - obj->days, obj->seconds, buffer); + PyDateTime_DELTA_GET_DAYS(obj), + PyDateTime_DELTA_GET_SECONDS(obj), + buffer); } static PyObject * diff --git a/psycopg/adapter_list.c b/psycopg/adapter_list.c index dec17b4c..3fdff76a 100644 --- a/psycopg/adapter_list.c +++ b/psycopg/adapter_list.c @@ -38,13 +38,14 @@ list_quote(listObject *self) { /* adapt the list by calling adapt() recursively and then wrapping everything into "ARRAY[]" */ - PyObject *tmp = NULL, *str = NULL, *joined = NULL, *res = NULL; + PyObject *res = NULL; + PyObject **qs = NULL; + Py_ssize_t bufsize = 0; + char *buf = NULL, *ptr; /* list consisting of only NULL don't work with the ARRAY[] construct - * so we use the {NULL,...} syntax. Note however that list of lists where - * some element is a list of only null still fails: for that we should use - * the '{...}' syntax uniformly but we cannot do it in the current - * infrastructure. TODO in psycopg3 */ + * so we use the {NULL,...} syntax. The same syntax is also necessary + * to convert array of arrays containing only nulls. */ int all_nulls = 1; Py_ssize_t i, len; @@ -53,47 +54,95 @@ list_quote(listObject *self) /* empty arrays are converted to NULLs (still searching for a way to insert an empty array in postgresql */ - if (len == 0) return Bytes_FromString("'{}'"); + if (len == 0) { + res = Bytes_FromString("'{}'"); + goto exit; + } - tmp = PyTuple_New(len); + if (!(qs = PyMem_New(PyObject *, len))) { + PyErr_NoMemory(); + goto exit; + } + memset(qs, 0, len * sizeof(PyObject *)); - for (i=0; iwrapped, i); if (wrapped == Py_None) { Py_INCREF(psyco_null); - quoted = psyco_null; + qs[i] = psyco_null; } else { - quoted = microprotocol_getquoted(wrapped, - (connectionObject*)self->connection); - if (quoted == NULL) goto error; - all_nulls = 0; + if (!(qs[i] = microprotocol_getquoted( + wrapped, (connectionObject*)self->connection))) { + goto exit; + } + + /* Lists of arrays containing only nulls are also not supported + * by the ARRAY construct so we should do some special casing */ + if (!PyList_Check(wrapped) || Bytes_AS_STRING(qs[i])[0] == 'A') { + all_nulls = 0; + } } - - /* here we don't loose a refcnt: SET_ITEM does not change the - reference count and we are just transferring ownership of the tmp - object to the tuple */ - PyTuple_SET_ITEM(tmp, i, quoted); + bufsize += Bytes_GET_SIZE(qs[i]) + 1; /* this, and a comma */ } - /* now that we have a tuple of adapted objects we just need to join them - and put "ARRAY[] around the result */ - str = Bytes_FromString(", "); - joined = PyObject_CallMethod(str, "join", "(O)", tmp); - if (joined == NULL) goto error; + /* Create an array literal, usually ARRAY[...] but if the contents are + * all NULL or array of NULL we must use the '{...}' syntax + */ + if (!(ptr = buf = PyMem_Malloc(bufsize + 8))) { + PyErr_NoMemory(); + goto exit; + } - /* PG doesn't like ARRAY[NULL..] */ if (!all_nulls) { - res = Bytes_FromFormat("ARRAY[%s]", Bytes_AsString(joined)); - } else { - res = Bytes_FromFormat("'{%s}'", Bytes_AsString(joined)); + strcpy(ptr, "ARRAY["); + ptr += 6; + for (i = 0; i < len; i++) { + Py_ssize_t sl; + sl = Bytes_GET_SIZE(qs[i]); + memcpy(ptr, Bytes_AS_STRING(qs[i]), sl); + ptr += sl; + *ptr++ = ','; + } + *(ptr - 1) = ']'; + } + else { + *ptr++ = '\''; + *ptr++ = '{'; + for (i = 0; i < len; i++) { + /* in case all the adapted things are nulls (or array of nulls), + * the quoted string is either NULL or an array of the form + * '{NULL,...}', in which case we have to strip the extra quotes */ + char *s; + Py_ssize_t sl; + s = Bytes_AS_STRING(qs[i]); + sl = Bytes_GET_SIZE(qs[i]); + if (s[0] != '\'') { + memcpy(ptr, s, sl); + ptr += sl; + } + else { + memcpy(ptr, s + 1, sl - 2); + ptr += sl - 2; + } + *ptr++ = ','; + } + *(ptr - 1) = '}'; + *ptr++ = '\''; } - error: - Py_XDECREF(tmp); - Py_XDECREF(str); - Py_XDECREF(joined); + res = Bytes_FromStringAndSize(buf, ptr - buf); + +exit: + if (qs) { + for (i = 0; i < len; i++) { + PyObject *q = qs[i]; + Py_XDECREF(q); + } + PyMem_Free(qs); + } + PyMem_Free(buf); + return res; } diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index e8081b9e..a60c4a9b 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -67,6 +67,9 @@ const char *srv_state_guc[] = { }; +const int SRV_STATE_UNCHANGED = -1; + + /* Return a new "string" from a char* from the database. * * On Py2 just get a string, on Py3 decode it in the connection codec. @@ -1188,8 +1191,10 @@ conn_set_session(connectionObject *self, int autocommit, int rv = -1; PGresult *pgres = NULL; char *error = NULL; + int want_autocommit = autocommit == SRV_STATE_UNCHANGED ? + self->autocommit : autocommit; - if (deferrable != self->deferrable && self->server_version < 90100) { + if (deferrable != SRV_STATE_UNCHANGED && self->server_version < 90100) { PyErr_SetString(ProgrammingError, "the 'deferrable' setting is only available" " from PostgreSQL 9.1"); @@ -1209,24 +1214,24 @@ conn_set_session(connectionObject *self, int autocommit, Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&self->lock); - if (autocommit) { - /* we are in autocommit state, so no BEGIN will be issued: + if (want_autocommit) { + /* we are or are going in autocommit state, so no BEGIN will be issued: * configure the session with the characteristics requested */ - if (isolevel != self->isolevel) { + if (isolevel != SRV_STATE_UNCHANGED) { if (0 > pq_set_guc_locked(self, "default_transaction_isolation", srv_isolevels[isolevel], &pgres, &error, &_save)) { goto endlock; } } - if (readonly != self->readonly) { + if (readonly != SRV_STATE_UNCHANGED) { if (0 > pq_set_guc_locked(self, "default_transaction_read_only", srv_state_guc[readonly], &pgres, &error, &_save)) { goto endlock; } } - if (deferrable != self->deferrable) { + if (deferrable != SRV_STATE_UNCHANGED) { if (0 > pq_set_guc_locked(self, "default_transaction_deferrable", srv_state_guc[deferrable], &pgres, &error, &_save)) { @@ -1251,7 +1256,7 @@ conn_set_session(connectionObject *self, int autocommit, goto endlock; } } - if (self->deferrable != STATE_DEFAULT) { + if (self->server_version >= 90100 && self->deferrable != STATE_DEFAULT) { if (0 > pq_set_guc_locked(self, "default_transaction_deferrable", "default", &pgres, &error, &_save)) { @@ -1260,10 +1265,18 @@ conn_set_session(connectionObject *self, int autocommit, } } - self->autocommit = autocommit; - self->isolevel = isolevel; - self->readonly = readonly; - self->deferrable = deferrable; + if (autocommit != SRV_STATE_UNCHANGED) { + self->autocommit = autocommit; + } + if (isolevel != SRV_STATE_UNCHANGED) { + self->isolevel = isolevel; + } + if (readonly != SRV_STATE_UNCHANGED) { + self->readonly = readonly; + } + if (deferrable != SRV_STATE_UNCHANGED) { + self->deferrable = deferrable; + } rv = 0; endlock: diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 8c5085b5..6a66d48d 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -39,6 +39,7 @@ extern HIDDEN const char *srv_isolevels[]; extern HIDDEN const char *srv_readonly[]; extern HIDDEN const char *srv_deferrable[]; +extern HIDDEN const int SRV_STATE_UNCHANGED; /** DBAPI methods **/ @@ -561,10 +562,10 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) PyObject *deferrable = Py_None; PyObject *autocommit = Py_None; - int c_isolevel = self->isolevel; - int c_readonly = self->readonly; - int c_deferrable = self->deferrable; - int c_autocommit = self->autocommit; + int c_isolevel = SRV_STATE_UNCHANGED; + int c_readonly = SRV_STATE_UNCHANGED; + int c_deferrable = SRV_STATE_UNCHANGED; + int c_autocommit = SRV_STATE_UNCHANGED; static char *kwlist[] = {"isolation_level", "readonly", "deferrable", "autocommit", NULL}; @@ -637,7 +638,7 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } if (-1 == (value = PyObject_IsTrue(pyvalue))) { return -1; } if (0 > conn_set_session(self, value, - self->isolevel, self->readonly, self->deferrable)) { + SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) { return -1; } @@ -668,8 +669,8 @@ psyco_conn_isolation_level_set(connectionObject *self, PyObject *pyvalue) if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } if (0 > (value = _psyco_conn_parse_isolevel(pyvalue))) { return -1; } - if (0 > conn_set_session(self, self->autocommit, - value, self->readonly, self->deferrable)) { + if (0 > conn_set_session(self, SRV_STATE_UNCHANGED, + value, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) { return -1; } @@ -715,13 +716,13 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) if (level == 0) { if (0 > conn_set_session(self, 1, - self->isolevel, self->readonly, self->deferrable)) { + SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) { return NULL; } } else { if (0 > conn_set_session(self, 0, - level, self->readonly, self->deferrable)) { + level, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) { return NULL; } } @@ -767,8 +768,8 @@ psyco_conn_readonly_set(connectionObject *self, PyObject *pyvalue) if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } - if (0 > conn_set_session(self, self->autocommit, - self->isolevel, value, self->deferrable)) { + if (0 > conn_set_session(self, SRV_STATE_UNCHANGED, + SRV_STATE_UNCHANGED, value, SRV_STATE_UNCHANGED)) { return -1; } @@ -813,8 +814,8 @@ psyco_conn_deferrable_set(connectionObject *self, PyObject *pyvalue) if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } - if (0 > conn_set_session(self, self->autocommit, - self->isolevel, self->readonly, value)) { + if (0 > conn_set_session(self, SRV_STATE_UNCHANGED, + SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, value)) { return -1; } diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index a70e9d34..d73bc3a4 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -59,6 +59,11 @@ psyco_curs_close(cursorObject *self) char buffer[128]; PGTransactionStatusType status; + if (!self->query) { + Dprintf("skipping named cursor close because unused"); + goto close; + } + if (self->conn) { status = PQtransactionStatus(self->conn->pgconn); } @@ -66,17 +71,18 @@ psyco_curs_close(cursorObject *self) status = PQTRANS_UNKNOWN; } - if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) { - EXC_IF_NO_MARK(self); - PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname); - if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; - } - else { + if (status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR) { Dprintf("skipping named curs close because tx status %d", (int)status); + goto close; } + + EXC_IF_NO_MARK(self); + PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname); + if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; } +close: self->closed = 1; Dprintf("psyco_curs_close: cursor at %p closed", self); @@ -592,8 +598,6 @@ psyco_curs_mogrify(cursorObject *self, PyObject *args, PyObject *kwargs) return NULL; } - EXC_IF_CURS_CLOSED(self); - return _psyco_curs_mogrify(self, operation, vars); } diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 6c95bd69..23e648d2 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -72,6 +72,10 @@ HIDDEN PyObject *psyco_null = NULL; /* The type of the cursor.description items */ HIDDEN PyObject *psyco_DescriptionType = NULL; +/* macro trick to stringify a macro expansion */ +#define xstr(s) str(s) +#define str(s) #s + /** connect module-level function **/ #define psyco_connect_doc \ "_connect(dsn, [connection_factory], [async]) -- New database connection.\n\n" @@ -403,6 +407,105 @@ psyco_libpq_version(PyObject *self) #endif } +/* encrypt_password - Prepare the encrypted password form */ +#define psyco_encrypt_password_doc \ +"encrypt_password(password, user, [scope], [algorithm]) -- Prepares the encrypted form of a PostgreSQL password.\n\n" + +static PyObject * +psyco_encrypt_password(PyObject *self, PyObject *args, PyObject *kwargs) +{ + char *encrypted = NULL; + PyObject *password = NULL, *user = NULL; + PyObject *scope = Py_None, *algorithm = Py_None; + PyObject *res = NULL; + connectionObject *conn = NULL; + + static char *kwlist[] = {"password", "user", "scope", "algorithm", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|OO", kwlist, + &password, &user, &scope, &algorithm)) { + return NULL; + } + + /* for ensure_bytes */ + Py_INCREF(user); + Py_INCREF(password); + Py_INCREF(algorithm); + + if (scope != Py_None) { + if (PyObject_TypeCheck(scope, &cursorType)) { + conn = ((cursorObject*)scope)->conn; + } + else if (PyObject_TypeCheck(scope, &connectionType)) { + conn = (connectionObject*)scope; + } + else { + PyErr_SetString(PyExc_TypeError, + "the scope must be a connection or a cursor"); + goto exit; + } + } + + if (!(user = psycopg_ensure_bytes(user))) { goto exit; } + if (!(password = psycopg_ensure_bytes(password))) { goto exit; } + if (algorithm != Py_None) { + if (!(algorithm = psycopg_ensure_bytes(algorithm))) { + goto exit; + } + } + + /* If we have to encrypt md5 we can use the libpq < 10 API */ + if (algorithm != Py_None && + strcmp(Bytes_AS_STRING(algorithm), "md5") == 0) { + encrypted = PQencryptPassword( + Bytes_AS_STRING(password), Bytes_AS_STRING(user)); + } + + /* If the algorithm is not md5 we have to use the API available from + * libpq 10. */ + else { +#if PG_VERSION_NUM >= 100000 + if (!conn) { + PyErr_SetString(ProgrammingError, + "password encryption (other than 'md5' algorithm)" + " requires a connection or cursor"); + goto exit; + } + + /* TODO: algo = None will block: forbid on async/green conn? */ + encrypted = PQencryptPasswordConn(conn->pgconn, + Bytes_AS_STRING(password), Bytes_AS_STRING(user), + algorithm != Py_None ? Bytes_AS_STRING(algorithm) : NULL); +#else + PyErr_SetString(NotSupportedError, + "password encryption (other than 'md5' algorithm)" + " requires libpq 10"); + goto exit; +#endif + } + + if (encrypted) { + res = Text_FromUTF8(encrypted); + } + else { + const char *msg = PQerrorMessage(conn->pgconn); + PyErr_Format(ProgrammingError, + "password encryption failed: %s", msg ? msg : "no reason given"); + goto exit; + } + +exit: + if (encrypted) { + PQfreemem(encrypted); + } + Py_XDECREF(user); + Py_XDECREF(password); + Py_XDECREF(algorithm); + + return res; +} + + /* psyco_encodings_fill Fill the module's postgresql<->python encoding table */ @@ -852,6 +955,8 @@ static PyMethodDef psycopgMethods[] = { METH_O, psyco_set_wait_callback_doc}, {"get_wait_callback", (PyCFunction)psyco_get_wait_callback, METH_NOARGS, psyco_get_wait_callback_doc}, + {"encrypt_password", (PyCFunction)psyco_encrypt_password, + METH_VARARGS|METH_KEYWORDS, psyco_encrypt_password_doc}, {NULL, NULL, 0, NULL} /* Sentinel */ }; @@ -885,7 +990,7 @@ INIT_MODULE(_psycopg)(void) psycopg_debug_enabled = 1; #endif - Dprintf("initpsycopg: initializing psycopg %s", PSYCOPG_VERSION); + Dprintf("initpsycopg: initializing psycopg %s", xstr(PSYCOPG_VERSION)); /* initialize all the new types and then the module */ Py_TYPE(&connectionType) = &PyType_Type; @@ -1017,7 +1122,7 @@ INIT_MODULE(_psycopg)(void) if (!(psyco_DescriptionType = psyco_make_description_type())) { goto exit; } /* set some module's parameters */ - PyModule_AddStringConstant(module, "__version__", PSYCOPG_VERSION); + PyModule_AddStringConstant(module, "__version__", xstr(PSYCOPG_VERSION)); PyModule_AddStringConstant(module, "__doc__", "psycopg PostgreSQL driver"); PyModule_AddIntConstant(module, "__libpq_version__", PG_VERSION_NUM); PyModule_AddIntMacro(module, REPLICATION_PHYSICAL); diff --git a/psycopg/python.h b/psycopg/python.h index fc8c2fed..fa894bf3 100644 --- a/psycopg/python.h +++ b/psycopg/python.h @@ -87,6 +87,7 @@ typedef unsigned long Py_uhash_t; #ifndef PyNumber_Int #define PyNumber_Int PyNumber_Long #endif + #endif /* PY_MAJOR_VERSION > 2 */ #if PY_MAJOR_VERSION < 3 @@ -104,6 +105,10 @@ typedef unsigned long Py_uhash_t; #define Bytes_ConcatAndDel PyString_ConcatAndDel #define _Bytes_Resize _PyString_Resize +#define PyDateTime_DELTA_GET_DAYS(o) (((PyDateTime_Delta*)o)->days) +#define PyDateTime_DELTA_GET_SECONDS(o) (((PyDateTime_Delta*)o)->seconds) +#define PyDateTime_DELTA_GET_MICROSECONDS(o) (((PyDateTime_Delta*)o)->microseconds) + #else #define Bytes_Type PyBytes_Type diff --git a/psycopg/solaris_support.c b/psycopg/solaris_support.c index cf82e2ed..e5f8edf6 100644 --- a/psycopg/solaris_support.c +++ b/psycopg/solaris_support.c @@ -1,6 +1,7 @@ /* solaris_support.c - emulate functions missing on Solaris * * Copyright (C) 2017 My Karlsson + * Copyright (c) 2018, Joyent, Inc. * * This file is part of psycopg. * @@ -28,7 +29,8 @@ #include "psycopg/solaris_support.h" #if defined(__sun) && defined(__SVR4) -/* timeradd is missing on Solaris */ +/* timeradd is missing on Solaris 10 */ +#ifndef timeradd void timeradd(struct timeval *a, struct timeval *b, struct timeval *c) { @@ -51,4 +53,5 @@ timersub(struct timeval *a, struct timeval *b, struct timeval *c) c->tv_sec -= 1; } } +#endif /* timeradd */ #endif /* defined(__sun) && defined(__SVR4) */ diff --git a/psycopg/solaris_support.h b/psycopg/solaris_support.h index 33c2f2b8..880e9f18 100644 --- a/psycopg/solaris_support.h +++ b/psycopg/solaris_support.h @@ -1,6 +1,7 @@ /* solaris_support.h - definitions for solaris_support.c * * Copyright (C) 2017 My Karlsson + * Copyright (c) 2018, Joyent, Inc. * * This file is part of psycopg. * @@ -30,8 +31,10 @@ #if defined(__sun) && defined(__SVR4) #include +#ifndef timeradd extern HIDDEN void timeradd(struct timeval *a, struct timeval *b, struct timeval *c); extern HIDDEN void timersub(struct timeval *a, struct timeval *b, struct timeval *c); #endif +#endif #endif /* !defined(PSYCOPG_SOLARIS_SUPPORT_H) */ diff --git a/psycopg/typecast_datetime.c b/psycopg/typecast_datetime.c index f24223cb..e34117dd 100644 --- a/psycopg/typecast_datetime.c +++ b/psycopg/typecast_datetime.c @@ -406,6 +406,11 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) } break; + case 'P': + PyErr_SetString(NotSupportedError, + "iso_8601 intervalstyle currently not supported"); + return NULL; + default: break; } diff --git a/psycopg/utils.c b/psycopg/utils.c index 7073504f..261810fd 100644 --- a/psycopg/utils.c +++ b/psycopg/utils.c @@ -168,11 +168,11 @@ psycopg_ensure_bytes(PyObject *obj) PyObject *rv = NULL; if (!obj) { return NULL; } - if (PyUnicode_CheckExact(obj)) { + if (PyUnicode_Check(obj)) { rv = PyUnicode_AsUTF8String(obj); Py_DECREF(obj); } - else if (Bytes_CheckExact(obj)) { + else if (Bytes_Check(obj)) { rv = obj; } else { @@ -282,7 +282,7 @@ exit: /* Make a connection string out of a string and a dictionary of arguments. * - * Helper to call psycopg2.extensions.make_dns() + * Helper to call psycopg2.extensions.make_dsn() */ PyObject * psycopg_make_dsn(PyObject *dsn, PyObject *kwargs) diff --git a/scripts/appveyor.cache_rebuild b/scripts/appveyor.cache_rebuild index da1b2be5..04825149 100644 --- a/scripts/appveyor.cache_rebuild +++ b/scripts/appveyor.cache_rebuild @@ -9,7 +9,7 @@ To invalidate the cache, update this file and check it into git. Currently used modules built in the cache: OpenSSL - Version: 1.0.2m + Version: 1.0.2n PostgreSQL Version: 10.1 diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh index 0320654a..342e24cc 100755 --- a/scripts/travis_test.sh +++ b/scripts/travis_test.sh @@ -56,15 +56,15 @@ fi # Unsupported postgres versions that we still support # Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs if [[ -n "$TEST_PAST" ]]; then - run_test 7.4 - run_test 8.0 - run_test 8.1 - run_test 8.2 - run_test 8.3 - run_test 8.4 - run_test 9.0 - run_test 9.1 run_test 9.2 + run_test 9.1 + run_test 9.0 + run_test 8.4 + run_test 8.3 + run_test 8.2 + run_test 8.1 + run_test 8.0 + run_test 7.4 fi # Postgres built from master diff --git a/setup.py b/setup.py index 16819cd3..f2e82602 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ except ImportError: from distutils.command.build_ext import build_ext from distutils.sysconfig import get_python_inc from distutils.ccompiler import get_default_compiler +from distutils.errors import CompileError from distutils.util import get_platform try: @@ -89,15 +90,23 @@ class PostgresConfig: if not self.pg_config_exe: self.pg_config_exe = self.autodetect_pg_config_path() if self.pg_config_exe is None: - sys.stderr.write("""\ + sys.stderr.write(""" Error: pg_config executable not found. -Please add the directory containing pg_config to the PATH -or specify the full executable path with the option: +pg_config is required to build psycopg2 from source. Please add the directory +containing pg_config to the $PATH or specify the full executable path with the +option: python setup.py build_ext --pg-config /path/to/pg_config build ... or with the pg_config option in 'setup.cfg'. + +If you prefer to avoid building psycopg2 from source, please install the PyPI +'psycopg2-binary' package instead. + +For further information please check the 'doc/src/install.rst' file (also at +). + """) sys.exit(1) @@ -271,8 +280,37 @@ class psycopg_build_ext(build_ext): else: return build_ext.get_export_symbols(self, extension) + built_files = 0 + def build_extension(self, extension): - build_ext.build_extension(self, extension) + # Count files compiled to print the binary blurb only if the first fails + compile_orig = getattr(self.compiler, '_compile', None) + if compile_orig is not None: + def _compile(*args, **kwargs): + rv = compile_orig(*args, **kwargs) + psycopg_build_ext.built_files += 1 + return rv + + self.compiler._compile = _compile + + try: + build_ext.build_extension(self, extension) + psycopg_build_ext.built_files += 1 + except CompileError: + if self.built_files == 0: + sys.stderr.write(""" +It appears you are missing some prerequisite to build the package from source. + +You may install a binary package by installing 'psycopg2-binary' from PyPI. +If you want to install psycopg2 from source, please install the packages +required for the build and try again. + +For further information please check the 'doc/src/install.rst' file (also at +). + +""") + raise + sysVer = sys.version_info[:2] # For Python versions that use MSVC compiler 2008, re-insert the @@ -543,10 +581,7 @@ if version_flags: else: PSYCOPG_VERSION_EX = PSYCOPG_VERSION -if not PLATFORM_IS_WINDOWS: - define_macros.append(('PSYCOPG_VERSION', '"' + PSYCOPG_VERSION_EX + '"')) -else: - define_macros.append(('PSYCOPG_VERSION', '\\"' + PSYCOPG_VERSION_EX + '\\"')) +define_macros.append(('PSYCOPG_VERSION', PSYCOPG_VERSION_EX)) if parser.has_option('build_ext', 'have_ssl'): have_ssl = int(parser.get('build_ext', 'have_ssl')) diff --git a/tests/test_connection.py b/tests/test_connection.py index 02db2645..13635f1f 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -37,7 +37,9 @@ from psycopg2 import extensions as ext from .testutils import ( unittest, decorate_all_tests, skip_if_no_superuser, skip_before_postgres, skip_after_postgres, skip_before_libpq, - ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow) + ConnectingTestCase, skip_if_tpc_disabled, skip_if_windows, slow, + libpq_version +) from .testconfig import dsn, dbname @@ -246,6 +248,13 @@ class ConnectionTests(ConnectingTestCase): else: del os.environ['PGCLIENTENCODING'] + def test_connect_no_string(self): + class MyString(str): + pass + + conn = psycopg2.connect(MyString(dsn)) + conn.close() + def test_weakref(self): from weakref import ref import gc @@ -400,6 +409,13 @@ class ParseDsnTestCase(ConnectingTestCase): self.assertRaises(TypeError, ext.parse_dsn, None) self.assertRaises(TypeError, ext.parse_dsn, 42) + def test_str_subclass(self): + class MyString(str): + pass + + res = ext.parse_dsn(MyString("dbname=test")) + self.assertEqual(res, {'dbname': 'test'}) + class MakeDsnTestCase(ConnectingTestCase): def test_empty_arguments(self): @@ -1381,6 +1397,102 @@ class TransactionControlTests(ConnectingTestCase): cur.execute("SHOW default_transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'off') + def test_idempotence_check(self): + self.conn.autocommit = False + self.conn.readonly = True + self.conn.autocommit = True + self.conn.readonly = True + + cur = self.conn.cursor() + cur.execute("SHOW transaction_read_only") + self.assertEqual(cur.fetchone()[0], 'on') + + +class TestEncryptPassword(ConnectingTestCase): + @skip_before_postgres(10) + def test_encrypt_password_post_9_6(self): + cur = self.conn.cursor() + cur.execute("SHOW password_encryption;") + server_encryption_algorithm = cur.fetchone()[0] + + # MD5 algorithm + self.assertEqual( + ext.encrypt_password('psycopg2', 'ashesh', self.conn, 'md5'), + 'md594839d658c28a357126f105b9cb14cfc' + ) + + # keywords + self.assertEqual( + ext.encrypt_password( + password='psycopg2', user='ashesh', + scope=self.conn, algorithm='md5'), + 'md594839d658c28a357126f105b9cb14cfc' + ) + if libpq_version() < 100000: + self.assertRaises( + psycopg2.NotSupportedError, + ext.encrypt_password, 'psycopg2', 'ashesh', self.conn, + 'scram-sha-256' + ) + else: + enc_password = ext.encrypt_password( + 'psycopg2', 'ashesh', self.conn + ) + if server_encryption_algorithm == 'md5': + self.assertEqual( + enc_password, 'md594839d658c28a357126f105b9cb14cfc' + ) + elif server_encryption_algorithm == 'scram-sha-256': + self.assertEqual(enc_password[:14], 'SCRAM-SHA-256$') + + self.assertEqual( + ext.encrypt_password( + 'psycopg2', 'ashesh', self.conn, 'scram-sha-256' + )[:14], 'SCRAM-SHA-256$' + ) + + self.assertRaises(psycopg2.ProgrammingError, + ext.encrypt_password, 'psycopg2', 'ashesh', self.conn, 'abc') + + @skip_after_postgres(10) + def test_encrypt_password_pre_10(self): + self.assertEqual( + ext.encrypt_password('psycopg2', 'ashesh', self.conn), + 'md594839d658c28a357126f105b9cb14cfc' + ) + + self.assertRaises(psycopg2.ProgrammingError, + ext.encrypt_password, 'psycopg2', 'ashesh', self.conn, 'abc') + + def test_encrypt_md5(self): + self.assertEqual( + ext.encrypt_password('psycopg2', 'ashesh', algorithm='md5'), + 'md594839d658c28a357126f105b9cb14cfc' + ) + + def test_encrypt_scram(self): + if libpq_version() >= 100000: + self.assert_( + ext.encrypt_password( + 'psycopg2', 'ashesh', self.conn, 'scram-sha-256') + .startswith('SCRAM-SHA-256$')) + else: + self.assertRaises(psycopg2.NotSupportedError, + ext.encrypt_password, + password='psycopg2', user='ashesh', + scope=self.conn, algorithm='scram-sha-256') + + def test_bad_types(self): + self.assertRaises(TypeError, ext.encrypt_password) + self.assertRaises(TypeError, ext.encrypt_password, + 'password', 42, self.conn, 'md5') + self.assertRaises(TypeError, ext.encrypt_password, + 42, 'user', self.conn, 'md5') + self.assertRaises(TypeError, ext.encrypt_password, + 42, 'user', 'wat', 'abc') + self.assertRaises(TypeError, ext.encrypt_password, + 'password', 'user', 'wat', 42) + class AutocommitTests(ConnectingTestCase): def test_closed(self): @@ -1539,9 +1651,13 @@ import os import sys import time import signal +import warnings import threading -import psycopg2 +# ignore wheel deprecation warning +with warnings.catch_warnings(): + warnings.simplefilter('ignore') + import psycopg2 def handle_sigabort(sig, frame): sys.exit(1) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index d5223860..b48fe7fc 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -119,6 +119,12 @@ class CursorTests(ConnectingTestCase): nref2 = sys.getrefcount(foo) self.assertEqual(nref1, nref2) + def test_modify_closed(self): + cur = self.conn.cursor() + cur.close() + sql = cur.mogrify("select %s", (10,)) + self.assertEqual(sql, b"select 10") + def test_bad_placeholder(self): cur = self.conn.cursor() self.assertRaises(psycopg2.ProgrammingError, @@ -430,6 +436,11 @@ class CursorTests(ConnectingTestCase): self.assertEqual([(2,), (3,), (4,)], cur2.fetchmany(3)) self.assertEqual([(5,), (6,), (7,)], cur2.fetchall()) + @skip_before_postgres(8, 0) + def test_named_noop_close(self): + cur = self.conn.cursor('test') + cur.close() + @skip_before_postgres(8, 0) def test_scroll(self): cur = self.conn.cursor() diff --git a/tests/test_dates.py b/tests/test_dates.py index 47ef41cb..bb5aee30 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -438,6 +438,14 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): r = cur.fetchone()[0] self.assertEqual(r, v, "%s -> %s != %s" % (s, r, v)) + @skip_before_postgres(8, 4) + def test_interval_iso_8601_not_supported(self): + # We may end up supporting, but no pressure for it + cur = self.conn.cursor() + cur.execute("set local intervalstyle to iso_8601") + cur.execute("select '1 day 2 hours'::interval") + self.assertRaises(psycopg2.NotSupportedError, cur.fetchone) + # Only run the datetime tests if psycopg was compiled with support. if not hasattr(psycopg2.extensions, 'PYDATETIME'): @@ -639,7 +647,8 @@ class FromTicksTestCase(unittest.TestCase): def test_date_value_error_sec_59_99(self): from datetime import date s = psycopg2.DateFromTicks(1273173119.99992) - self.assertEqual(s.adapted, date(2010, 5, 6)) + # The returned date is local + self.assert_(s.adapted in [date(2010, 5, 6), date(2010, 5, 7)]) def test_time_value_error_sec_59_99(self): from datetime import time diff --git a/tests/test_extras_dictcursor.py b/tests/test_extras_dictcursor.py index 75c22773..a9201f12 100755 --- a/tests/test_extras_dictcursor.py +++ b/tests/test_extras_dictcursor.py @@ -19,12 +19,11 @@ from datetime import timedelta import psycopg2 import psycopg2.extras import unittest -from .testutils import ConnectingTestCase, skip_before_postgres +from .testutils import ConnectingTestCase, skip_before_postgres, \ + skip_before_python, skip_from_python -class ExtrasDictCursorTests(ConnectingTestCase): - """Test if DictCursor extension class works.""" - +class _DictCursorBase(ConnectingTestCase): def setUp(self): ConnectingTestCase.setUp(self) curs = self.conn.cursor() @@ -32,6 +31,30 @@ class ExtrasDictCursorTests(ConnectingTestCase): curs.execute("INSERT INTO ExtrasDictCursorTests VALUES ('bar')") self.conn.commit() + def _testIterRowNumber(self, curs): + # Only checking for dataset < itersize: + # see CursorTests.test_iter_named_cursor_rownumber + curs.itersize = 20 + curs.execute("""select * from generate_series(1,10)""") + for i, r in enumerate(curs): + self.assertEqual(i + 1, curs.rownumber) + + def _testNamedCursorNotGreedy(self, curs): + curs.itersize = 2 + curs.execute("""select clock_timestamp() as ts from generate_series(1,3)""") + recs = [] + for t in curs: + time.sleep(0.01) + recs.append(t) + + # check that the dataset was not fetched in a single gulp + self.assert_(recs[1]['ts'] - recs[0]['ts'] < timedelta(seconds=0.005)) + self.assert_(recs[2]['ts'] - recs[1]['ts'] > timedelta(seconds=0.0099)) + + +class ExtrasDictCursorTests(_DictCursorBase): + """Test if DictCursor extension class works.""" + def testDictConnCursorArgs(self): self.conn.close() self.conn = self.connect(connection_factory=psycopg2.extras.DictConnection) @@ -81,35 +104,6 @@ class ExtrasDictCursorTests(ConnectingTestCase): self.failUnless(row[0] == 'bar') return row - def testDictCursorWithPlainCursorRealFetchOne(self): - self._testWithPlainCursorReal(lambda curs: curs.fetchone()) - - def testDictCursorWithPlainCursorRealFetchMany(self): - self._testWithPlainCursorReal(lambda curs: curs.fetchmany(100)[0]) - - def testDictCursorWithPlainCursorRealFetchManyNoarg(self): - self._testWithPlainCursorReal(lambda curs: curs.fetchmany()[0]) - - def testDictCursorWithPlainCursorRealFetchAll(self): - self._testWithPlainCursorReal(lambda curs: curs.fetchall()[0]) - - def testDictCursorWithPlainCursorRealIter(self): - def getter(curs): - for row in curs: - return row - self._testWithPlainCursorReal(getter) - - @skip_before_postgres(8, 0) - def testDictCursorWithPlainCursorRealIterRowNumber(self): - curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - self._testIterRowNumber(curs) - - def _testWithPlainCursorReal(self, getter): - curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) - curs.execute("SELECT * FROM ExtrasDictCursorTests") - row = getter(curs) - self.failUnless(row['foo'] == 'bar') - def testDictCursorWithNamedCursorFetchOne(self): self._testWithNamedCursor(lambda curs: curs.fetchone()) @@ -145,6 +139,94 @@ class ExtrasDictCursorTests(ConnectingTestCase): self.failUnless(row['foo'] == 'bar') self.failUnless(row[0] == 'bar') + def testPickleDictRow(self): + import pickle + curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + curs.execute("select 10 as a, 20 as b") + r = curs.fetchone() + d = pickle.dumps(r) + r1 = pickle.loads(d) + self.assertEqual(r, r1) + self.assertEqual(r[0], r1[0]) + self.assertEqual(r[1], r1[1]) + self.assertEqual(r['a'], r1['a']) + self.assertEqual(r['b'], r1['b']) + self.assertEqual(r._index, r1._index) + + @skip_from_python(3) + def test_iter_methods_2(self): + curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + curs.execute("select 10 as a, 20 as b") + r = curs.fetchone() + self.assert_(isinstance(r.keys(), list)) + self.assertEqual(len(r.keys()), 2) + self.assert_(isinstance(r.values(), tuple)) # sic? + self.assertEqual(len(r.values()), 2) + self.assert_(isinstance(r.items(), list)) + self.assertEqual(len(r.items()), 2) + + self.assert_(not isinstance(r.iterkeys(), list)) + self.assertEqual(len(list(r.iterkeys())), 2) + self.assert_(not isinstance(r.itervalues(), list)) + self.assertEqual(len(list(r.itervalues())), 2) + self.assert_(not isinstance(r.iteritems(), list)) + self.assertEqual(len(list(r.iteritems())), 2) + + @skip_before_python(3) + def test_iter_methods_3(self): + curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + curs.execute("select 10 as a, 20 as b") + r = curs.fetchone() + self.assert_(not isinstance(r.keys(), list)) + self.assertEqual(len(list(r.keys())), 2) + self.assert_(not isinstance(r.values(), list)) + self.assertEqual(len(list(r.values())), 2) + self.assert_(not isinstance(r.items(), list)) + self.assertEqual(len(list(r.items())), 2) + + +class ExtrasDictCursorRealTests(_DictCursorBase): + def testDictCursorWithPlainCursorRealFetchOne(self): + self._testWithPlainCursorReal(lambda curs: curs.fetchone()) + + def testDictCursorWithPlainCursorRealFetchMany(self): + self._testWithPlainCursorReal(lambda curs: curs.fetchmany(100)[0]) + + def testDictCursorWithPlainCursorRealFetchManyNoarg(self): + self._testWithPlainCursorReal(lambda curs: curs.fetchmany()[0]) + + def testDictCursorWithPlainCursorRealFetchAll(self): + self._testWithPlainCursorReal(lambda curs: curs.fetchall()[0]) + + def testDictCursorWithPlainCursorRealIter(self): + def getter(curs): + for row in curs: + return row + self._testWithPlainCursorReal(getter) + + @skip_before_postgres(8, 0) + def testDictCursorWithPlainCursorRealIterRowNumber(self): + curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + self._testIterRowNumber(curs) + + def _testWithPlainCursorReal(self, getter): + curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + curs.execute("SELECT * FROM ExtrasDictCursorTests") + row = getter(curs) + self.failUnless(row['foo'] == 'bar') + + def testPickleRealDictRow(self): + import pickle + curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + curs.execute("select 10 as a, 20 as b") + r = curs.fetchone() + d = pickle.dumps(r) + r1 = pickle.loads(d) + self.assertEqual(r, r1) + self.assertEqual(r['a'], r1['a']) + self.assertEqual(r['b'], r1['b']) + self.assertEqual(r._column_mapping, r1._column_mapping) + def testDictCursorRealWithNamedCursorFetchOne(self): self._testWithNamedCursorReal(lambda curs: curs.fetchone()) @@ -180,51 +262,36 @@ class ExtrasDictCursorTests(ConnectingTestCase): row = getter(curs) self.failUnless(row['foo'] == 'bar') - def _testNamedCursorNotGreedy(self, curs): - curs.itersize = 2 - curs.execute("""select clock_timestamp() as ts from generate_series(1,3)""") - recs = [] - for t in curs: - time.sleep(0.01) - recs.append(t) - - # check that the dataset was not fetched in a single gulp - self.assert_(recs[1]['ts'] - recs[0]['ts'] < timedelta(seconds=0.005)) - self.assert_(recs[2]['ts'] - recs[1]['ts'] > timedelta(seconds=0.0099)) - - def _testIterRowNumber(self, curs): - # Only checking for dataset < itersize: - # see CursorTests.test_iter_named_cursor_rownumber - curs.itersize = 20 - curs.execute("""select * from generate_series(1,10)""") - for i, r in enumerate(curs): - self.assertEqual(i + 1, curs.rownumber) - - def testPickleDictRow(self): - import pickle - curs = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) - curs.execute("select 10 as a, 20 as b") - r = curs.fetchone() - d = pickle.dumps(r) - r1 = pickle.loads(d) - self.assertEqual(r, r1) - self.assertEqual(r[0], r1[0]) - self.assertEqual(r[1], r1[1]) - self.assertEqual(r['a'], r1['a']) - self.assertEqual(r['b'], r1['b']) - self.assertEqual(r._index, r1._index) - - def testPickleRealDictRow(self): - import pickle + @skip_from_python(3) + def test_iter_methods_2(self): curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) curs.execute("select 10 as a, 20 as b") r = curs.fetchone() - d = pickle.dumps(r) - r1 = pickle.loads(d) - self.assertEqual(r, r1) - self.assertEqual(r['a'], r1['a']) - self.assertEqual(r['b'], r1['b']) - self.assertEqual(r._column_mapping, r1._column_mapping) + self.assert_(isinstance(r.keys(), list)) + self.assertEqual(len(r.keys()), 2) + self.assert_(isinstance(r.values(), list)) + self.assertEqual(len(r.values()), 2) + self.assert_(isinstance(r.items(), list)) + self.assertEqual(len(r.items()), 2) + + self.assert_(not isinstance(r.iterkeys(), list)) + self.assertEqual(len(list(r.iterkeys())), 2) + self.assert_(not isinstance(r.itervalues(), list)) + self.assertEqual(len(list(r.itervalues())), 2) + self.assert_(not isinstance(r.iteritems(), list)) + self.assertEqual(len(list(r.iteritems())), 2) + + @skip_before_python(3) + def test_iter_methods_3(self): + curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + curs.execute("select 10 as a, 20 as b") + r = curs.fetchone() + self.assert_(not isinstance(r.keys(), list)) + self.assertEqual(len(list(r.keys())), 2) + self.assert_(not isinstance(r.values(), list)) + self.assertEqual(len(list(r.values())), 2) + self.assert_(not isinstance(r.items(), list)) + self.assertEqual(len(list(r.items())), 2) class NamedTupleCursorTest(ConnectingTestCase): @@ -349,6 +416,22 @@ class NamedTupleCursorTest(ConnectingTestCase): curs.execute("update nttest set s = s") self.assertRaises(psycopg2.ProgrammingError, curs.fetchall) + def test_bad_col_names(self): + curs = self.conn.cursor() + curs.execute('select 1 as "foo.bar_baz", 2 as "?column?", 3 as "3"') + rv = curs.fetchone() + self.assertEqual(rv.foo_bar_baz, 1) + self.assertEqual(rv.f_column_, 2) + self.assertEqual(rv.f3, 3) + + @skip_before_python(3) + @skip_before_postgres(8) + def test_nonascii_name(self): + curs = self.conn.cursor() + curs.execute('select 1 as \xe5h\xe9') + rv = curs.fetchone() + self.assertEqual(getattr(rv, '\xe5h\xe9'), 1) + def test_minimal_generation(self): # Instrument the class to verify it gets called the minimum number of times. from psycopg2.extras import NamedTupleCursor diff --git a/tests/test_types_basic.py b/tests/test_types_basic.py index a93265d9..76b9aa3f 100755 --- a/tests/test_types_basic.py +++ b/tests/test_types_basic.py @@ -224,16 +224,31 @@ class TypesBasicTests(ConnectingTestCase): curs.execute("insert into na (boola) values (%s)", ([True, None],)) curs.execute("insert into na (boola) values (%s)", ([None, None],)) - # TODO: array of array of nulls are not supported yet - # curs.execute("insert into na (textaa) values (%s)", ([[None]],)) + curs.execute("insert into na (textaa) values (%s)", ([[None]],)) curs.execute("insert into na (textaa) values (%s)", ([['a', None]],)) - # curs.execute("insert into na (textaa) values (%s)", ([[None, None]],)) - # curs.execute("insert into na (intaa) values (%s)", ([[None]],)) + curs.execute("insert into na (textaa) values (%s)", ([[None, None]],)) + + curs.execute("insert into na (intaa) values (%s)", ([[None]],)) curs.execute("insert into na (intaa) values (%s)", ([[42, None]],)) - # curs.execute("insert into na (intaa) values (%s)", ([[None, None]],)) - # curs.execute("insert into na (boolaa) values (%s)", ([[None]],)) + curs.execute("insert into na (intaa) values (%s)", ([[None, None]],)) + + curs.execute("insert into na (boolaa) values (%s)", ([[None]],)) curs.execute("insert into na (boolaa) values (%s)", ([[True, None]],)) - # curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) + curs.execute("insert into na (boolaa) values (%s)", ([[None, None]],)) + + @testutils.skip_before_postgres(8, 2) + def testNestedArrays(self): + curs = self.conn.cursor() + for a in [ + [[1]], + [[None]], + [[None, None, None]], + [[None, None], [1, None]], + [[None, None], [None, None]], + [[[None, None], [None, None]]], + ]: + curs.execute("select %s::int[]", (a,)) + self.assertEqual(curs.fetchone()[0], a) @testutils.skip_from_python(3) def testTypeRoundtripBuffer(self): diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py index 5cb13534..cda163b6 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -179,8 +179,8 @@ class HstoreTestCase(ConnectingTestCase): m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q) self.assert_(m, repr(q)) - kk = m.group(1).split(b", ") - vv = m.group(2).split(b", ") + kk = m.group(1).split(b",") + vv = m.group(2).split(b",") ii = list(zip(kk, vv)) ii.sort() diff --git a/tests/test_with.py b/tests/test_with.py index 1392d85f..f26f8f9c 100755 --- a/tests/test_with.py +++ b/tests/test_with.py @@ -26,7 +26,7 @@ import psycopg2 import psycopg2.extensions as ext import unittest -from .testutils import ConnectingTestCase +from .testutils import ConnectingTestCase, skip_before_postgres class WithTestCase(ConnectingTestCase): @@ -215,6 +215,11 @@ class WithCursorTestCase(WithTestCase): else: self.fail("where is my exception?") + @skip_before_postgres(8, 0) + def test_named_with_noop(self): + with self.conn.cursor('named') as cur: + pass + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/tox.ini b/tox.ini index 17612e25..a0eafa4d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,9 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - [tox] -envlist = py27 +envlist = py{27,34,35,36} [testenv] commands = make check +whitelist_externals = make [flake8] max-line-length = 85