Merge branch 'master' into drop-2to3

This commit is contained in:
Daniele Varrazzo 2018-05-20 23:56:29 +01:00
commit e8a831dda2
41 changed files with 924 additions and 369 deletions

View File

@ -13,43 +13,17 @@ environment:
matrix: matrix:
# For Python versions available on Appveyor, see # For Python versions available on Appveyor, see
# http://www.appveyor.com/docs/installed-software#python # 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) OPENSSL_VERSION: "1_0_2n"
# Py 3.4 = VS Ver. 10.0 (VS 2010) POSTGRES_VERSION: "10_1"
# 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
PSYCOPG2_TESTDB: psycopg2_test PSYCOPG2_TESTDB: psycopg2_test
PSYCOPG2_TESTDB_USER: postgres PSYCOPG2_TESTDB_USER: postgres
@ -77,7 +51,22 @@ cache:
init: init:
# Uncomment next line to get RDP access during the build. # 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')) #- 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 Python to the path
- SET PATH=%PYTHON%;%PYTHON%\Scripts;C:\Program Files\Git\mingw64\bin;%PATH% - SET PATH=%PYTHON%;%PYTHON%\Scripts;C:\Program Files\Git\mingw64\bin;%PATH%
@ -153,8 +142,8 @@ install:
} }
# Download OpenSSL source # Download OpenSSL source
- CD C:\Others - CD C:\Others
- IF NOT EXIST OpenSSL_1_0_2m.zip ( - IF NOT EXIST OpenSSL_%OPENSSL_VERSION%.zip (
curl -fsSL -o OpenSSL_1_0_2m.zip https://github.com/openssl/openssl/archive/OpenSSL_1_0_2m.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 # To use OpenSSL >= 1.1.0, both libpq and psycopg build environments have
@ -166,15 +155,15 @@ install:
# - nmake build_libs install_dev # - nmake build_libs install_dev
- IF NOT EXIST %OPENSSLTOP%\lib\ssleay32.lib ( - IF NOT EXIST %OPENSSLTOP%\lib\ssleay32.lib (
CD %BUILD_DIR% && CD %BUILD_DIR% &&
7z x C:\Others\OpenSSL_1_0_2m.zip && 7z x C:\Others\OpenSSL_%OPENSSL_VERSION%.zip &&
CD openssl-OpenSSL_1_0_2m && CD openssl-OpenSSL_%OPENSSL_VERSION% &&
perl Configure %TARGET% no-asm no-shared no-zlib --prefix=%OPENSSLTOP% --openssldir=%OPENSSLTOP% && perl Configure %TARGET% no-asm no-shared no-zlib --prefix=%OPENSSLTOP% --openssldir=%OPENSSLTOP% &&
CALL ms\%DO% && CALL ms\%DO% &&
nmake -f ms\nt.mak init headers lib && nmake -f ms\nt.mak init headers lib &&
COPY inc32\openssl\*.h %OPENSSLTOP%\include\openssl && COPY inc32\openssl\*.h %OPENSSLTOP%\include\openssl &&
COPY out32\*.lib %OPENSSLTOP%\lib && COPY out32\*.lib %OPENSSLTOP%\lib &&
CD %BASE_DIR% && 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 # Setup directories for building PostgreSQL librarires
@ -188,8 +177,8 @@ install:
# Download PostgreSQL source # Download PostgreSQL source
- CD C:\Others - CD C:\Others
- IF NOT EXIST postgres-REL_10_1.zip ( - IF NOT EXIST postgres-REL_%POSTGRES_VERSION%.zip (
curl -fsSL -o postgres-REL_10_1.zip https://github.com/postgres/postgres/archive/REL_10_1.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) # Setup build config file (config.pl)
@ -200,11 +189,11 @@ install:
# Prepare local include directory for building from # Prepare local include directory for building from
# Build pg_config in place # Build pg_config in place
# NOTE: Cannot set and use the same variable inside an IF # 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 ( - IF NOT EXIST %PGTOP%\lib\libpq.lib (
CD %BUILD_DIR% && CD %BUILD_DIR% &&
7z x C:\Others\postgres-REL_10_1.zip && 7z x C:\Others\postgres-REL_%POSTGRES_VERSION%.zip &&
CD postgres-REL_10_1\src\tools\msvc && CD postgres-REL_%POSTGRES_VERSION%\src\tools\msvc &&
ECHO $config-^>{ldap} = 0; > config.pl && ECHO $config-^>{ldap} = 0; > config.pl &&
ECHO $config-^>{openssl} = "%OPENSSLTOP:\=\\%"; >> config.pl && ECHO $config-^>{openssl} = "%OPENSSLTOP:\=\\%"; >> config.pl &&
ECHO.>> config.pl && ECHO.>> config.pl &&

View File

@ -6,12 +6,14 @@ language: python
python: python:
- 2.7 - 2.7
- 3.7-dev
- 3.6 - 3.6
- 3.5 - 3.5
- 3.4 - 3.4
install: install:
- python setup.py install - pip install -U pip setuptools wheel
- pip install .
- rm -rf psycopg2.egg-info - rm -rf psycopg2.egg-info
- sudo scripts/travis_prepare.sh - sudo scripts/travis_prepare.sh

10
LICENSE
View File

@ -1,5 +1,5 @@
psycopg2 and the LGPL psycopg2 and the LGPL
===================== ---------------------
psycopg2 is free software: you can redistribute it and/or modify it psycopg2 is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published under the terms of the GNU Lesser General Public License as published
@ -29,15 +29,15 @@ If not, see <http://www.gnu.org/licenses/>.
Alternative licenses Alternative licenses
==================== --------------------
If you prefer you can use the Zope Database Adapter ZPsycopgDA (i.e., 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. published on the Zope web site, http://www.zope.org/Resources/ZPL.
Also, the following BSD-like license applies (at your option) to the Also, the following BSD-like license applies (at your option) to the
files following the pattern psycopg/adapter*.{h,c} and files following the pattern ``psycopg/adapter*.{h,c}`` and
psycopg/microprotocol*.{h,c}: ``psycopg/microprotocol*.{h,c}``:
Permission is granted to anyone to use this software for any purpose, Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it including commercial applications, and to alter it and redistribute it

34
NEWS
View File

@ -4,6 +4,10 @@ Current release
What's new in psycopg 2.8 What's new in psycopg 2.8
------------------------- -------------------------
New features:
- Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`).
Other changes: Other changes:
- Dropped support for Python 2.6, 3.2, 3.3. - Dropped support for Python 2.6, 3.2, 3.3.
@ -15,17 +19,41 @@ Other changes:
install``. 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 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`). - 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 `~psycopg2.extras.MinTimeLoggingCursor` on Python 3 (:ticket:`#609`).
- Fixed parsing of array of points as floats (:ticket:`#613`). - Fixed parsing of array of points as floats (:ticket:`#613`).
- Fixed `~psycopg2.__libpq_version__` building with libpq >= 10.1 - Fixed `~psycopg2.__libpq_version__` building with libpq >= 10.1
(:ticket:`632`). (:ticket:`632`).
- Fixed `~cursor.rowcount` after `~cursor.executemany()` with :sql:`RETURNING` statements - Fixed `~cursor.rowcount` after `~cursor.executemany()` with :sql:`RETURNING`
(:ticket:`633`). statements (:ticket:`633`).
- Wheel packages compiled against PostgreSQL 10.1 libpq and OpenSSL 1.0.2m. - 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 What's new in psycopg 2.7.3.2

View File

@ -25,29 +25,40 @@ Documentation is included in the ``doc`` directory and is `available online`__.
.. __: http://initd.org/psycopg/docs/ .. __: http://initd.org/psycopg/docs/
For any other resource (source code repository, bug tracker, mailing list)
please check the `project homepage`__.
Installation Installation
------------ ------------
If your ``pip`` version supports wheel_ packages it should be possible to Building Psycopg requires a few prerequisites (a C compiler, some development
install a binary version of Psycopg including all the dependencies from PyPI_. packages): please check the install_ and the faq_ documents in the ``doc`` dir
Just run:: 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 $ pip install psycopg2
If you want to build Psycopg from source you will need some prerequisites (a C or using ``setup.py`` if you have downloaded the source package locally::
compiler, development packages): please check the install_ and the faq_
documents in the ``doc`` dir for the details.
.. _wheel: http://pythonwheels.com/ $ python setup.py build
.. _PyPI: https://pypi.python.org/pypi/psycopg2 $ 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 .. _install: http://initd.org/psycopg/docs/install.html#install-from-source
.. _faq: http://initd.org/psycopg/docs/faq.html#faq-compile .. _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/ .. __: http://initd.org/psycopg/

View File

@ -29,8 +29,6 @@ doctest:
upload: upload:
# this command requires ssh configured to the proper target # this command requires ssh configured to the proper target
tar czf - -C html . | ssh psycoweb tar xzvf - -C docs/current 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: clean:
$(MAKE) $(SPHOPTS) -C src $@ $(MAKE) $(SPHOPTS) -C src $@

View File

@ -100,5 +100,5 @@ Test packages may be uploaded on the `PyPI testing site`__ using::
assuming `proper configuration`__ of ``~/.pypirc``. assuming `proper configuration`__ of ``~/.pypirc``.
.. __: https://testpypi.python.org/pypi/psycopg2 .. __: https://test.pypi.org/project/psycopg2/
.. __: https://wiki.python.org/moin/TestPyPI .. __: https://wiki.python.org/moin/TestPyPI

View File

@ -485,7 +485,7 @@ details. You can check the `psycogreen`_ project for further informations and
resources about the topic. resources about the topic.
.. _coroutine: http://en.wikipedia.org/wiki/Coroutine .. _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 .. _green threads: http://en.wikipedia.org/wiki/Green_threads
.. _Eventlet: http://eventlet.net/ .. _Eventlet: http://eventlet.net/
.. _gevent: http://www.gevent.org/ .. _gevent: http://www.gevent.org/

View File

@ -61,8 +61,8 @@ except ImportError:
release = version release = version
intersphinx_mapping = { intersphinx_mapping = {
'py': ('http://docs.python.org/2', None), 'py': ('https://docs.python.org/2', None),
'py3': ('http://docs.python.org/3', None), 'py3': ('https://docs.python.org/3', None),
} }
# Pattern to generate links to the bug tracker # Pattern to generate links to the bug tracker

View File

@ -41,11 +41,6 @@ The ``connection`` class
previously only valid PostgreSQL identifiers were accepted as previously only valid PostgreSQL identifiers were accepted as
cursor name. 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 The *cursor_factory* argument can be used to create non-standard
cursors. The class returned must be a subclass of cursors. The class returned must be a subclass of
`psycopg2.extensions.cursor`. See :ref:`subclassing-cursor` for `psycopg2.extensions.cursor`. See :ref:`subclassing-cursor` for

View File

@ -555,6 +555,38 @@ Other functions
.. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER .. __: 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:: .. index::
pair: Isolation level; Constants pair: Isolation level; Constants

View File

@ -48,6 +48,7 @@ Psycopg 2 is both Unicode and Python 3 friendly.
errorcodes errorcodes
faq faq
news news
license
.. ifconfig:: builder != 'text' .. ifconfig:: builder != 'text'

View File

@ -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 there is an experimental `porting of Psycopg for Ctypes`__, but it is not as
mature as the C implementation yet. 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/ .. _PostgreSQL: http://www.postgresql.org/
.. _Python: http://www.python.org/ .. _Python: http://www.python.org/
.. _libpq: http://www.postgresql.org/docs/current/static/libpq.html .. _libpq: http://www.postgresql.org/docs/current/static/libpq.html
@ -32,94 +22,20 @@ The current `!psycopg2` implementation supports:
.. index:: .. index::
single: Install; from PyPI single: Prerequisites
single: Install; wheel
single: Wheel
Binary install from PyPI Prerequisites
------------------------ -------------
`!psycopg2` is `available on PyPI`__ in the form of wheel_ packages for the The current `!psycopg2` implementation supports:
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:
.. code-block:: console ..
NOTE: keep consistent with setup.py and the /features/ page.
$ pip install psycopg2 - Python version 2.7
- Python 3 versions from 3.4 to 3.6
Make sure to use an up-to-date version of :program:`pip` (you can upgrade it - PostgreSQL server versions from 7.4 to 10
using something like ``pip install -U pip``) - PostgreSQL client library version from 9.1
.. __: 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
<disable-wheel>`.
.. 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 <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/
@ -128,8 +44,10 @@ page`__ or from PyPI_.
Build prerequisites Build prerequisites
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
These notes illustrate how to compile Psycopg on Linux. If you want to compile The build prerequisites are to be met in order to install Psycopg from source
Psycopg on other platforms you may have to adjust some details accordingly. 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 Psycopg is a C wrapper around the libpq_ PostgreSQL client library. To install
it from sources you will need: 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: 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 .. code-block:: console
$ python setup.py build $ 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 <build-prerequisites>`.
.. index:: .. index::
single: setup.py single: setup.py
single: setup.cfg single: setup.cfg
Non-standard builds Non-standard builds
^^^^^^^^^^^^^^^^^^^ -------------------
If you have less standard requirements such as: 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 - Edit the ``setup.cfg`` file adding the ``PSYCOPG_DEBUG`` flag to the
``define`` option. ``define`` option.
- :ref:`Compile and install <source-package>` the package. - :ref:`Compile and install <build-prerequisites>` the package.
- Set the :envvar:`PSYCOPG_DEBUG` environment variable: - 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 - Google for `!psycopg2` *your error message*. Especially useful the week
after the release of a new OS X version. 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 - 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 ever and about the quality time you have wasted figuring out the correct
:envvar:`ARCHFLAGS`. Especially useful from the Starbucks near you. :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

7
doc/src/license.rst Normal file
View File

@ -0,0 +1,7 @@
.. index::
single: License
License
=======
.. include:: ../../LICENSE

View File

@ -1,3 +1,7 @@
.. index::
single: Release notes
single: News
Release notes Release notes
============= =============

View File

@ -24,13 +24,18 @@ directly in the client application.
.. method:: getconn(key=None) .. 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) .. method:: putconn(conn, key=None, close=False)
Put away a connection. Put away a connection.
If *close* is `!True`, discard the connection from the pool. If *close* is `!True`, discard the connection from the pool.
*key* should be used consistently with `getconn()`.
.. method:: closeall .. method:: closeall

View File

@ -5,6 +5,7 @@
import os import os
import sys import sys
def main(): def main():
if len(sys.argv) != 3: if len(sys.argv) != 3:
sys.stderr.write("usage: %s index.rst text-dir\n") sys.stderr.write("usage: %s index.rst text-dir\n")
@ -17,20 +18,20 @@ def main():
return 0 return 0
def iter_file_base(fn): def iter_file_base(fn):
f = open(fn) f = open(fn)
have_line = iter(f).__next__
while not have_line().startswith('.. toctree'): while not next(f).startswith('.. toctree'):
pass pass
while have_line().strip().startswith(':'): while next(f).strip().startswith(':'):
pass pass
yield os.path.splitext(os.path.basename(fn))[0] yield os.path.splitext(os.path.basename(fn))[0]
n = 0 n = 0
while True: while True:
line = have_line() line = next(f)
if line.isspace(): if line.isspace():
continue continue
if line.startswith(".."): if line.startswith(".."):
@ -44,6 +45,7 @@ def iter_file_base(fn):
# maybe format changed? # maybe format changed?
raise Exception("Not enough files found. Format change in index.rst?") raise Exception("Not enough files found. Format change in index.rst?")
def emit(basename, txt_dir): def emit(basename, txt_dir):
f = open(os.path.join(txt_dir, basename + ".txt")) f = open(os.path.join(txt_dir, basename + ".txt"))
for line in f: for line in f:

View File

@ -63,7 +63,7 @@ from psycopg2._psycopg import ( # noqa
string_types, binary_types, new_type, new_array_type, register_type, string_types, binary_types, new_type, new_array_type, register_type,
ISQLQuote, Notify, Diagnostics, Column, ISQLQuote, Notify, Diagnostics, Column,
QueryCanceledError, TransactionRollbackError, QueryCanceledError, TransactionRollbackError,
set_wait_callback, get_wait_callback, ) set_wait_callback, get_wait_callback, encrypt_password, )
"""Isolation level values.""" """Isolation level values."""

View File

@ -344,7 +344,20 @@ class NamedTupleCursor(_cursor):
return return
def _make_nt(self): 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): class LoggingConnection(_connection):

View File

@ -100,7 +100,7 @@ _pydatetime_string_delta(pydatetimeObject *self)
char buffer[8]; char buffer[8];
int i; int i;
int a = obj->microseconds; int a = PyDateTime_DELTA_GET_MICROSECONDS(obj);
for (i=0; i < 6 ; i++) { for (i=0; i < 6 ; i++) {
buffer[5-i] = '0' + (a % 10); buffer[5-i] = '0' + (a % 10);
@ -109,7 +109,9 @@ _pydatetime_string_delta(pydatetimeObject *self)
buffer[6] = '\0'; buffer[6] = '\0';
return Bytes_FromFormat("'%d days %d.%s seconds'::interval", 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 * static PyObject *

View File

@ -38,13 +38,14 @@ list_quote(listObject *self)
{ {
/* adapt the list by calling adapt() recursively and then wrapping /* adapt the list by calling adapt() recursively and then wrapping
everything into "ARRAY[]" */ 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 /* 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 * so we use the {NULL,...} syntax. The same syntax is also necessary
* some element is a list of only null still fails: for that we should use * to convert array of arrays containing only nulls. */
* the '{...}' syntax uniformly but we cannot do it in the current
* infrastructure. TODO in psycopg3 */
int all_nulls = 1; int all_nulls = 1;
Py_ssize_t i, len; 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 /* empty arrays are converted to NULLs (still searching for a way to
insert an empty array in postgresql */ 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; i<len; i++) { for (i = 0; i < len; i++) {
PyObject *quoted;
PyObject *wrapped = PyList_GET_ITEM(self->wrapped, i); PyObject *wrapped = PyList_GET_ITEM(self->wrapped, i);
if (wrapped == Py_None) { if (wrapped == Py_None) {
Py_INCREF(psyco_null); Py_INCREF(psyco_null);
quoted = psyco_null; qs[i] = psyco_null;
} }
else { else {
quoted = microprotocol_getquoted(wrapped, if (!(qs[i] = microprotocol_getquoted(
(connectionObject*)self->connection); wrapped, (connectionObject*)self->connection))) {
if (quoted == NULL) goto error; 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; all_nulls = 0;
} }
}
/* here we don't loose a refcnt: SET_ITEM does not change the bufsize += Bytes_GET_SIZE(qs[i]) + 1; /* this, and a comma */
reference count and we are just transferring ownership of the tmp
object to the tuple */
PyTuple_SET_ITEM(tmp, i, quoted);
} }
/* now that we have a tuple of adapted objects we just need to join them /* Create an array literal, usually ARRAY[...] but if the contents are
and put "ARRAY[] around the result */ * all NULL or array of NULL we must use the '{...}' syntax
str = Bytes_FromString(", "); */
joined = PyObject_CallMethod(str, "join", "(O)", tmp); if (!(ptr = buf = PyMem_Malloc(bufsize + 8))) {
if (joined == NULL) goto error; PyErr_NoMemory();
goto exit;
}
/* PG doesn't like ARRAY[NULL..] */
if (!all_nulls) { if (!all_nulls) {
res = Bytes_FromFormat("ARRAY[%s]", Bytes_AsString(joined)); strcpy(ptr, "ARRAY[");
} else { ptr += 6;
res = Bytes_FromFormat("'{%s}'", Bytes_AsString(joined)); 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: res = Bytes_FromStringAndSize(buf, ptr - buf);
Py_XDECREF(tmp);
Py_XDECREF(str); exit:
Py_XDECREF(joined); if (qs) {
for (i = 0; i < len; i++) {
PyObject *q = qs[i];
Py_XDECREF(q);
}
PyMem_Free(qs);
}
PyMem_Free(buf);
return res; return res;
} }

View File

@ -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. /* Return a new "string" from a char* from the database.
* *
* On Py2 just get a string, on Py3 decode it in the connection codec. * 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; int rv = -1;
PGresult *pgres = NULL; PGresult *pgres = NULL;
char *error = 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, PyErr_SetString(ProgrammingError,
"the 'deferrable' setting is only available" "the 'deferrable' setting is only available"
" from PostgreSQL 9.1"); " from PostgreSQL 9.1");
@ -1209,24 +1214,24 @@ conn_set_session(connectionObject *self, int autocommit,
Py_BEGIN_ALLOW_THREADS; Py_BEGIN_ALLOW_THREADS;
pthread_mutex_lock(&self->lock); pthread_mutex_lock(&self->lock);
if (autocommit) { if (want_autocommit) {
/* we are in autocommit state, so no BEGIN will be issued: /* we are or are going in autocommit state, so no BEGIN will be issued:
* configure the session with the characteristics requested */ * configure the session with the characteristics requested */
if (isolevel != self->isolevel) { if (isolevel != SRV_STATE_UNCHANGED) {
if (0 > pq_set_guc_locked(self, if (0 > pq_set_guc_locked(self,
"default_transaction_isolation", srv_isolevels[isolevel], "default_transaction_isolation", srv_isolevels[isolevel],
&pgres, &error, &_save)) { &pgres, &error, &_save)) {
goto endlock; goto endlock;
} }
} }
if (readonly != self->readonly) { if (readonly != SRV_STATE_UNCHANGED) {
if (0 > pq_set_guc_locked(self, if (0 > pq_set_guc_locked(self,
"default_transaction_read_only", srv_state_guc[readonly], "default_transaction_read_only", srv_state_guc[readonly],
&pgres, &error, &_save)) { &pgres, &error, &_save)) {
goto endlock; goto endlock;
} }
} }
if (deferrable != self->deferrable) { if (deferrable != SRV_STATE_UNCHANGED) {
if (0 > pq_set_guc_locked(self, if (0 > pq_set_guc_locked(self,
"default_transaction_deferrable", srv_state_guc[deferrable], "default_transaction_deferrable", srv_state_guc[deferrable],
&pgres, &error, &_save)) { &pgres, &error, &_save)) {
@ -1251,7 +1256,7 @@ conn_set_session(connectionObject *self, int autocommit,
goto endlock; goto endlock;
} }
} }
if (self->deferrable != STATE_DEFAULT) { if (self->server_version >= 90100 && self->deferrable != STATE_DEFAULT) {
if (0 > pq_set_guc_locked(self, if (0 > pq_set_guc_locked(self,
"default_transaction_deferrable", "default", "default_transaction_deferrable", "default",
&pgres, &error, &_save)) { &pgres, &error, &_save)) {
@ -1260,10 +1265,18 @@ conn_set_session(connectionObject *self, int autocommit,
} }
} }
if (autocommit != SRV_STATE_UNCHANGED) {
self->autocommit = autocommit; self->autocommit = autocommit;
}
if (isolevel != SRV_STATE_UNCHANGED) {
self->isolevel = isolevel; self->isolevel = isolevel;
}
if (readonly != SRV_STATE_UNCHANGED) {
self->readonly = readonly; self->readonly = readonly;
}
if (deferrable != SRV_STATE_UNCHANGED) {
self->deferrable = deferrable; self->deferrable = deferrable;
}
rv = 0; rv = 0;
endlock: endlock:

View File

@ -39,6 +39,7 @@
extern HIDDEN const char *srv_isolevels[]; extern HIDDEN const char *srv_isolevels[];
extern HIDDEN const char *srv_readonly[]; extern HIDDEN const char *srv_readonly[];
extern HIDDEN const char *srv_deferrable[]; extern HIDDEN const char *srv_deferrable[];
extern HIDDEN const int SRV_STATE_UNCHANGED;
/** DBAPI methods **/ /** DBAPI methods **/
@ -561,10 +562,10 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs)
PyObject *deferrable = Py_None; PyObject *deferrable = Py_None;
PyObject *autocommit = Py_None; PyObject *autocommit = Py_None;
int c_isolevel = self->isolevel; int c_isolevel = SRV_STATE_UNCHANGED;
int c_readonly = self->readonly; int c_readonly = SRV_STATE_UNCHANGED;
int c_deferrable = self->deferrable; int c_deferrable = SRV_STATE_UNCHANGED;
int c_autocommit = self->autocommit; int c_autocommit = SRV_STATE_UNCHANGED;
static char *kwlist[] = static char *kwlist[] =
{"isolation_level", "readonly", "deferrable", "autocommit", NULL}; {"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 (!_psyco_set_session_check_setter_wrapper(self)) { return -1; }
if (-1 == (value = PyObject_IsTrue(pyvalue))) { return -1; } if (-1 == (value = PyObject_IsTrue(pyvalue))) { return -1; }
if (0 > conn_set_session(self, value, if (0 > conn_set_session(self, value,
self->isolevel, self->readonly, self->deferrable)) { SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) {
return -1; 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 (!_psyco_set_session_check_setter_wrapper(self)) { return -1; }
if (0 > (value = _psyco_conn_parse_isolevel(pyvalue))) { return -1; } if (0 > (value = _psyco_conn_parse_isolevel(pyvalue))) { return -1; }
if (0 > conn_set_session(self, self->autocommit, if (0 > conn_set_session(self, SRV_STATE_UNCHANGED,
value, self->readonly, self->deferrable)) { value, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) {
return -1; return -1;
} }
@ -715,13 +716,13 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args)
if (level == 0) { if (level == 0) {
if (0 > conn_set_session(self, 1, if (0 > conn_set_session(self, 1,
self->isolevel, self->readonly, self->deferrable)) { SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) {
return NULL; return NULL;
} }
} }
else { else {
if (0 > conn_set_session(self, 0, if (0 > conn_set_session(self, 0,
level, self->readonly, self->deferrable)) { level, SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED)) {
return NULL; 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 (!_psyco_set_session_check_setter_wrapper(self)) { return -1; }
if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; }
if (0 > conn_set_session(self, self->autocommit, if (0 > conn_set_session(self, SRV_STATE_UNCHANGED,
self->isolevel, value, self->deferrable)) { SRV_STATE_UNCHANGED, value, SRV_STATE_UNCHANGED)) {
return -1; 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 (!_psyco_set_session_check_setter_wrapper(self)) { return -1; }
if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; }
if (0 > conn_set_session(self, self->autocommit, if (0 > conn_set_session(self, SRV_STATE_UNCHANGED,
self->isolevel, self->readonly, value)) { SRV_STATE_UNCHANGED, SRV_STATE_UNCHANGED, value)) {
return -1; return -1;
} }

View File

@ -59,6 +59,11 @@ psyco_curs_close(cursorObject *self)
char buffer[128]; char buffer[128];
PGTransactionStatusType status; PGTransactionStatusType status;
if (!self->query) {
Dprintf("skipping named cursor close because unused");
goto close;
}
if (self->conn) { if (self->conn) {
status = PQtransactionStatus(self->conn->pgconn); status = PQtransactionStatus(self->conn->pgconn);
} }
@ -66,17 +71,18 @@ psyco_curs_close(cursorObject *self)
status = PQTRANS_UNKNOWN; status = PQTRANS_UNKNOWN;
} }
if (!(status == PQTRANS_UNKNOWN || status == PQTRANS_INERROR)) { 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); EXC_IF_NO_MARK(self);
PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname); PyOS_snprintf(buffer, 127, "CLOSE %s", self->qname);
if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL; if (pq_execute(self, buffer, 0, 0, 1) == -1) return NULL;
} }
else {
Dprintf("skipping named curs close because tx status %d",
(int)status);
}
}
close:
self->closed = 1; self->closed = 1;
Dprintf("psyco_curs_close: cursor at %p closed", self); Dprintf("psyco_curs_close: cursor at %p closed", self);
@ -592,8 +598,6 @@ psyco_curs_mogrify(cursorObject *self, PyObject *args, PyObject *kwargs)
return NULL; return NULL;
} }
EXC_IF_CURS_CLOSED(self);
return _psyco_curs_mogrify(self, operation, vars); return _psyco_curs_mogrify(self, operation, vars);
} }

View File

@ -72,6 +72,10 @@ HIDDEN PyObject *psyco_null = NULL;
/* The type of the cursor.description items */ /* The type of the cursor.description items */
HIDDEN PyObject *psyco_DescriptionType = NULL; 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 **/ /** connect module-level function **/
#define psyco_connect_doc \ #define psyco_connect_doc \
"_connect(dsn, [connection_factory], [async]) -- New database connection.\n\n" "_connect(dsn, [connection_factory], [async]) -- New database connection.\n\n"
@ -403,6 +407,105 @@ psyco_libpq_version(PyObject *self)
#endif #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 /* psyco_encodings_fill
Fill the module's postgresql<->python encoding table */ Fill the module's postgresql<->python encoding table */
@ -852,6 +955,8 @@ static PyMethodDef psycopgMethods[] = {
METH_O, psyco_set_wait_callback_doc}, METH_O, psyco_set_wait_callback_doc},
{"get_wait_callback", (PyCFunction)psyco_get_wait_callback, {"get_wait_callback", (PyCFunction)psyco_get_wait_callback,
METH_NOARGS, psyco_get_wait_callback_doc}, 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 */ {NULL, NULL, 0, NULL} /* Sentinel */
}; };
@ -885,7 +990,7 @@ INIT_MODULE(_psycopg)(void)
psycopg_debug_enabled = 1; psycopg_debug_enabled = 1;
#endif #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 */ /* initialize all the new types and then the module */
Py_TYPE(&connectionType) = &PyType_Type; Py_TYPE(&connectionType) = &PyType_Type;
@ -1017,7 +1122,7 @@ INIT_MODULE(_psycopg)(void)
if (!(psyco_DescriptionType = psyco_make_description_type())) { goto exit; } if (!(psyco_DescriptionType = psyco_make_description_type())) { goto exit; }
/* set some module's parameters */ /* 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_AddStringConstant(module, "__doc__", "psycopg PostgreSQL driver");
PyModule_AddIntConstant(module, "__libpq_version__", PG_VERSION_NUM); PyModule_AddIntConstant(module, "__libpq_version__", PG_VERSION_NUM);
PyModule_AddIntMacro(module, REPLICATION_PHYSICAL); PyModule_AddIntMacro(module, REPLICATION_PHYSICAL);

View File

@ -87,6 +87,7 @@ typedef unsigned long Py_uhash_t;
#ifndef PyNumber_Int #ifndef PyNumber_Int
#define PyNumber_Int PyNumber_Long #define PyNumber_Int PyNumber_Long
#endif #endif
#endif /* PY_MAJOR_VERSION > 2 */ #endif /* PY_MAJOR_VERSION > 2 */
#if PY_MAJOR_VERSION < 3 #if PY_MAJOR_VERSION < 3
@ -104,6 +105,10 @@ typedef unsigned long Py_uhash_t;
#define Bytes_ConcatAndDel PyString_ConcatAndDel #define Bytes_ConcatAndDel PyString_ConcatAndDel
#define _Bytes_Resize _PyString_Resize #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 #else
#define Bytes_Type PyBytes_Type #define Bytes_Type PyBytes_Type

View File

@ -1,6 +1,7 @@
/* solaris_support.c - emulate functions missing on Solaris /* solaris_support.c - emulate functions missing on Solaris
* *
* Copyright (C) 2017 My Karlsson <mk@acc.umu.se> * Copyright (C) 2017 My Karlsson <mk@acc.umu.se>
* Copyright (c) 2018, Joyent, Inc.
* *
* This file is part of psycopg. * This file is part of psycopg.
* *
@ -28,7 +29,8 @@
#include "psycopg/solaris_support.h" #include "psycopg/solaris_support.h"
#if defined(__sun) && defined(__SVR4) #if defined(__sun) && defined(__SVR4)
/* timeradd is missing on Solaris */ /* timeradd is missing on Solaris 10 */
#ifndef timeradd
void void
timeradd(struct timeval *a, struct timeval *b, struct timeval *c) 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; c->tv_sec -= 1;
} }
} }
#endif /* timeradd */
#endif /* defined(__sun) && defined(__SVR4) */ #endif /* defined(__sun) && defined(__SVR4) */

View File

@ -1,6 +1,7 @@
/* solaris_support.h - definitions for solaris_support.c /* solaris_support.h - definitions for solaris_support.c
* *
* Copyright (C) 2017 My Karlsson <mk@acc.umu.se> * Copyright (C) 2017 My Karlsson <mk@acc.umu.se>
* Copyright (c) 2018, Joyent, Inc.
* *
* This file is part of psycopg. * This file is part of psycopg.
* *
@ -30,8 +31,10 @@
#if defined(__sun) && defined(__SVR4) #if defined(__sun) && defined(__SVR4)
#include <sys/time.h> #include <sys/time.h>
#ifndef timeradd
extern HIDDEN void timeradd(struct timeval *a, struct timeval *b, struct timeval *c); 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); extern HIDDEN void timersub(struct timeval *a, struct timeval *b, struct timeval *c);
#endif #endif
#endif
#endif /* !defined(PSYCOPG_SOLARIS_SUPPORT_H) */ #endif /* !defined(PSYCOPG_SOLARIS_SUPPORT_H) */

View File

@ -406,6 +406,11 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs)
} }
break; break;
case 'P':
PyErr_SetString(NotSupportedError,
"iso_8601 intervalstyle currently not supported");
return NULL;
default: default:
break; break;
} }

View File

@ -168,11 +168,11 @@ psycopg_ensure_bytes(PyObject *obj)
PyObject *rv = NULL; PyObject *rv = NULL;
if (!obj) { return NULL; } if (!obj) { return NULL; }
if (PyUnicode_CheckExact(obj)) { if (PyUnicode_Check(obj)) {
rv = PyUnicode_AsUTF8String(obj); rv = PyUnicode_AsUTF8String(obj);
Py_DECREF(obj); Py_DECREF(obj);
} }
else if (Bytes_CheckExact(obj)) { else if (Bytes_Check(obj)) {
rv = obj; rv = obj;
} }
else { else {
@ -282,7 +282,7 @@ exit:
/* Make a connection string out of a string and a dictionary of arguments. /* 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 * PyObject *
psycopg_make_dsn(PyObject *dsn, PyObject *kwargs) psycopg_make_dsn(PyObject *dsn, PyObject *kwargs)

View File

@ -9,7 +9,7 @@ To invalidate the cache, update this file and check it into git.
Currently used modules built in the cache: Currently used modules built in the cache:
OpenSSL OpenSSL
Version: 1.0.2m Version: 1.0.2n
PostgreSQL PostgreSQL
Version: 10.1 Version: 10.1

View File

@ -56,15 +56,15 @@ fi
# Unsupported postgres versions that we still support # Unsupported postgres versions that we still support
# Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs # Images built by https://github.com/psycopg/psycopg2-wheels/tree/build-dinosaurs
if [[ -n "$TEST_PAST" ]]; then 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.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 fi
# Postgres built from master # Postgres built from master

View File

@ -39,6 +39,7 @@ except ImportError:
from distutils.command.build_ext import build_ext from distutils.command.build_ext import build_ext
from distutils.sysconfig import get_python_inc from distutils.sysconfig import get_python_inc
from distutils.ccompiler import get_default_compiler from distutils.ccompiler import get_default_compiler
from distutils.errors import CompileError
from distutils.util import get_platform from distutils.util import get_platform
try: try:
@ -89,15 +90,23 @@ class PostgresConfig:
if not self.pg_config_exe: if not self.pg_config_exe:
self.pg_config_exe = self.autodetect_pg_config_path() self.pg_config_exe = self.autodetect_pg_config_path()
if self.pg_config_exe is None: if self.pg_config_exe is None:
sys.stderr.write("""\ sys.stderr.write("""
Error: pg_config executable not found. Error: pg_config executable not found.
Please add the directory containing pg_config to the PATH pg_config is required to build psycopg2 from source. Please add the directory
or specify the full executable path with the option: 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 ... python setup.py build_ext --pg-config /path/to/pg_config build ...
or with the pg_config option in 'setup.cfg'. 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
<http://initd.org/psycopg/docs/install.html>).
""") """)
sys.exit(1) sys.exit(1)
@ -271,8 +280,37 @@ class psycopg_build_ext(build_ext):
else: else:
return build_ext.get_export_symbols(self, extension) return build_ext.get_export_symbols(self, extension)
built_files = 0
def build_extension(self, extension): def 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) 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
<http://initd.org/psycopg/docs/install.html>).
""")
raise
sysVer = sys.version_info[:2] sysVer = sys.version_info[:2]
# For Python versions that use MSVC compiler 2008, re-insert the # For Python versions that use MSVC compiler 2008, re-insert the
@ -543,10 +581,7 @@ if version_flags:
else: else:
PSYCOPG_VERSION_EX = PSYCOPG_VERSION PSYCOPG_VERSION_EX = PSYCOPG_VERSION
if not PLATFORM_IS_WINDOWS: define_macros.append(('PSYCOPG_VERSION', PSYCOPG_VERSION_EX))
define_macros.append(('PSYCOPG_VERSION', '"' + PSYCOPG_VERSION_EX + '"'))
else:
define_macros.append(('PSYCOPG_VERSION', '\\"' + PSYCOPG_VERSION_EX + '\\"'))
if parser.has_option('build_ext', 'have_ssl'): if parser.has_option('build_ext', 'have_ssl'):
have_ssl = int(parser.get('build_ext', 'have_ssl')) have_ssl = int(parser.get('build_ext', 'have_ssl'))

View File

@ -37,7 +37,9 @@ from psycopg2 import extensions as ext
from .testutils import ( from .testutils import (
unittest, decorate_all_tests, skip_if_no_superuser, unittest, decorate_all_tests, skip_if_no_superuser,
skip_before_postgres, skip_after_postgres, skip_before_libpq, 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 from .testconfig import dsn, dbname
@ -246,6 +248,13 @@ class ConnectionTests(ConnectingTestCase):
else: else:
del os.environ['PGCLIENTENCODING'] 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): def test_weakref(self):
from weakref import ref from weakref import ref
import gc import gc
@ -400,6 +409,13 @@ class ParseDsnTestCase(ConnectingTestCase):
self.assertRaises(TypeError, ext.parse_dsn, None) self.assertRaises(TypeError, ext.parse_dsn, None)
self.assertRaises(TypeError, ext.parse_dsn, 42) 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): class MakeDsnTestCase(ConnectingTestCase):
def test_empty_arguments(self): def test_empty_arguments(self):
@ -1381,6 +1397,102 @@ class TransactionControlTests(ConnectingTestCase):
cur.execute("SHOW default_transaction_read_only;") cur.execute("SHOW default_transaction_read_only;")
self.assertEqual(cur.fetchone()[0], 'off') 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): class AutocommitTests(ConnectingTestCase):
def test_closed(self): def test_closed(self):
@ -1539,9 +1651,13 @@ import os
import sys import sys
import time import time
import signal import signal
import warnings
import threading import threading
import psycopg2 # ignore wheel deprecation warning
with warnings.catch_warnings():
warnings.simplefilter('ignore')
import psycopg2
def handle_sigabort(sig, frame): def handle_sigabort(sig, frame):
sys.exit(1) sys.exit(1)

View File

@ -119,6 +119,12 @@ class CursorTests(ConnectingTestCase):
nref2 = sys.getrefcount(foo) nref2 = sys.getrefcount(foo)
self.assertEqual(nref1, nref2) 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): def test_bad_placeholder(self):
cur = self.conn.cursor() cur = self.conn.cursor()
self.assertRaises(psycopg2.ProgrammingError, self.assertRaises(psycopg2.ProgrammingError,
@ -430,6 +436,11 @@ class CursorTests(ConnectingTestCase):
self.assertEqual([(2,), (3,), (4,)], cur2.fetchmany(3)) self.assertEqual([(2,), (3,), (4,)], cur2.fetchmany(3))
self.assertEqual([(5,), (6,), (7,)], cur2.fetchall()) 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) @skip_before_postgres(8, 0)
def test_scroll(self): def test_scroll(self):
cur = self.conn.cursor() cur = self.conn.cursor()

View File

@ -438,6 +438,14 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin):
r = cur.fetchone()[0] r = cur.fetchone()[0]
self.assertEqual(r, v, "%s -> %s != %s" % (s, r, v)) 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. # Only run the datetime tests if psycopg was compiled with support.
if not hasattr(psycopg2.extensions, 'PYDATETIME'): if not hasattr(psycopg2.extensions, 'PYDATETIME'):
@ -639,7 +647,8 @@ class FromTicksTestCase(unittest.TestCase):
def test_date_value_error_sec_59_99(self): def test_date_value_error_sec_59_99(self):
from datetime import date from datetime import date
s = psycopg2.DateFromTicks(1273173119.99992) 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): def test_time_value_error_sec_59_99(self):
from datetime import time from datetime import time

View File

@ -19,12 +19,11 @@ from datetime import timedelta
import psycopg2 import psycopg2
import psycopg2.extras import psycopg2.extras
import unittest 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): class _DictCursorBase(ConnectingTestCase):
"""Test if DictCursor extension class works."""
def setUp(self): def setUp(self):
ConnectingTestCase.setUp(self) ConnectingTestCase.setUp(self)
curs = self.conn.cursor() curs = self.conn.cursor()
@ -32,6 +31,30 @@ class ExtrasDictCursorTests(ConnectingTestCase):
curs.execute("INSERT INTO ExtrasDictCursorTests VALUES ('bar')") curs.execute("INSERT INTO ExtrasDictCursorTests VALUES ('bar')")
self.conn.commit() 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): def testDictConnCursorArgs(self):
self.conn.close() self.conn.close()
self.conn = self.connect(connection_factory=psycopg2.extras.DictConnection) self.conn = self.connect(connection_factory=psycopg2.extras.DictConnection)
@ -81,35 +104,6 @@ class ExtrasDictCursorTests(ConnectingTestCase):
self.failUnless(row[0] == 'bar') self.failUnless(row[0] == 'bar')
return row 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): def testDictCursorWithNamedCursorFetchOne(self):
self._testWithNamedCursor(lambda curs: curs.fetchone()) self._testWithNamedCursor(lambda curs: curs.fetchone())
@ -145,6 +139,94 @@ class ExtrasDictCursorTests(ConnectingTestCase):
self.failUnless(row['foo'] == 'bar') self.failUnless(row['foo'] == 'bar')
self.failUnless(row[0] == '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): def testDictCursorRealWithNamedCursorFetchOne(self):
self._testWithNamedCursorReal(lambda curs: curs.fetchone()) self._testWithNamedCursorReal(lambda curs: curs.fetchone())
@ -180,51 +262,36 @@ class ExtrasDictCursorTests(ConnectingTestCase):
row = getter(curs) row = getter(curs)
self.failUnless(row['foo'] == 'bar') self.failUnless(row['foo'] == 'bar')
def _testNamedCursorNotGreedy(self, curs): @skip_from_python(3)
curs.itersize = 2 def test_iter_methods_2(self):
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
curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) curs = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
curs.execute("select 10 as a, 20 as b") curs.execute("select 10 as a, 20 as b")
r = curs.fetchone() r = curs.fetchone()
d = pickle.dumps(r) self.assert_(isinstance(r.keys(), list))
r1 = pickle.loads(d) self.assertEqual(len(r.keys()), 2)
self.assertEqual(r, r1) self.assert_(isinstance(r.values(), list))
self.assertEqual(r['a'], r1['a']) self.assertEqual(len(r.values()), 2)
self.assertEqual(r['b'], r1['b']) self.assert_(isinstance(r.items(), list))
self.assertEqual(r._column_mapping, r1._column_mapping) 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): class NamedTupleCursorTest(ConnectingTestCase):
@ -349,6 +416,22 @@ class NamedTupleCursorTest(ConnectingTestCase):
curs.execute("update nttest set s = s") curs.execute("update nttest set s = s")
self.assertRaises(psycopg2.ProgrammingError, curs.fetchall) 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): def test_minimal_generation(self):
# Instrument the class to verify it gets called the minimum number of times. # Instrument the class to verify it gets called the minimum number of times.
from psycopg2.extras import NamedTupleCursor from psycopg2.extras import NamedTupleCursor

View File

@ -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)", ([True, None],))
curs.execute("insert into na (boola) values (%s)", ([None, 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)", ([['a', None]],))
# curs.execute("insert into na (textaa) values (%s)", ([[None, 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)", ([[None]],))
curs.execute("insert into na (intaa) values (%s)", ([[42, 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 (intaa) values (%s)", ([[None, None]],))
# curs.execute("insert into na (boolaa) values (%s)", ([[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)", ([[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) @testutils.skip_from_python(3)
def testTypeRoundtripBuffer(self): def testTypeRoundtripBuffer(self):

View File

@ -179,8 +179,8 @@ class HstoreTestCase(ConnectingTestCase):
m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q) m = re.match(br'hstore\(ARRAY\[([^\]]+)\], ARRAY\[([^\]]+)\]\)', q)
self.assert_(m, repr(q)) self.assert_(m, repr(q))
kk = m.group(1).split(b", ") kk = m.group(1).split(b",")
vv = m.group(2).split(b", ") vv = m.group(2).split(b",")
ii = list(zip(kk, vv)) ii = list(zip(kk, vv))
ii.sort() ii.sort()

View File

@ -26,7 +26,7 @@ import psycopg2
import psycopg2.extensions as ext import psycopg2.extensions as ext
import unittest import unittest
from .testutils import ConnectingTestCase from .testutils import ConnectingTestCase, skip_before_postgres
class WithTestCase(ConnectingTestCase): class WithTestCase(ConnectingTestCase):
@ -215,6 +215,11 @@ class WithCursorTestCase(WithTestCase):
else: else:
self.fail("where is my exception?") 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(): def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__) return unittest.TestLoader().loadTestsFromName(__name__)

View File

@ -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] [tox]
envlist = py27 envlist = py{27,34,35,36}
[testenv] [testenv]
commands = make check commands = make check
whitelist_externals = make
[flake8] [flake8]
max-line-length = 85 max-line-length = 85