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