Merge branch 'master' into feature/replication-protocol-c-connection-object

This commit is contained in:
Oleksandr Shulgin 2016-03-04 10:52:10 +01:00
commit cb7032554e
13 changed files with 394 additions and 142 deletions

6
NEWS
View File

@ -6,7 +6,10 @@ What's new in psycopg 2.7
New features: New features:
- Added `~psycopg2.extensions.parse_dsn()` function (:ticket:`#321`). - Added `~psycopg2.extensions.parse_dsn()` and
`~psycopg2.extensions.make_dsn()` functions (:tickets:`#321, #363`).
`~psycopg2.connect()` now can take both *dsn* and keyword arguments, merging
them together.
- Added `~psycopg2.__libpq_version__` and - Added `~psycopg2.__libpq_version__` and
`~psycopg2.extensions.libpq_version()` to inspect the version of the `~psycopg2.extensions.libpq_version()` to inspect the version of the
``libpq`` library the module was compiled/loaded with ``libpq`` library the module was compiled/loaded with
@ -27,6 +30,7 @@ What's new in psycopg 2.6.2
- Raise `!NotSupportedError` on unhandled server response status - Raise `!NotSupportedError` on unhandled server response status
(:ticket:`#352`). (:ticket:`#352`).
- Fixed `!PersistentConnectionPool` on Python 3 (:ticket:`#348`). - Fixed `!PersistentConnectionPool` on Python 3 (:ticket:`#348`).
- Fixed `!errorcodes.lookup` initialization thread-safety (:ticket:`#382`).
What's new in psycopg 2.6.1 What's new in psycopg 2.6.1

View File

@ -270,7 +270,7 @@ wasting resources.
A simple application could poll the connection from time to time to check if A simple application could poll the connection from time to time to check if
something new has arrived. A better strategy is to use some I/O completion something new has arrived. A better strategy is to use some I/O completion
function such as :py:func:`~select.select` to sleep until awaken from the kernel when there is function such as :py:func:`~select.select` to sleep until awakened by the kernel when there is
some data to read on the connection, thereby using no CPU unless there is some data to read on the connection, thereby using no CPU unless there is
something to read:: something to read::

View File

@ -12,17 +12,12 @@
The module contains a few objects and function extending the minimum set of The module contains a few objects and function extending the minimum set of
functionalities defined by the |DBAPI|_. functionalities defined by the |DBAPI|_.
.. function:: parse_dsn(dsn) Classes definitions
-------------------
Parse connection string into a dictionary of keywords and values. Instances of these classes are usually returned by factory functions or
attributes. Their definitions are exposed here to allow subclassing,
Uses libpq's ``PQconninfoParse`` to parse the string according to introspection etc.
accepted format(s) and check for supported keywords.
Example::
>>> psycopg2.extensions.parse_dsn('dbname=test user=postgres password=secret')
{'password': 'secret', 'user': 'postgres', 'dbname': 'test'}
.. function:: make_dsn(**kwargs) .. function:: make_dsn(**kwargs)
@ -56,6 +51,7 @@ functionalities defined by the |DBAPI|_.
For a complete description of the class, see `connection`. For a complete description of the class, see `connection`.
.. class:: cursor(conn, name=None) .. class:: cursor(conn, name=None)
It is the class usually returned by the `connection.cursor()` It is the class usually returned by the `connection.cursor()`
@ -66,6 +62,7 @@ functionalities defined by the |DBAPI|_.
For a complete description of the class, see `cursor`. For a complete description of the class, see `cursor`.
.. class:: lobject(conn [, oid [, mode [, new_oid [, new_file ]]]]) .. class:: lobject(conn [, oid [, mode [, new_oid [, new_file ]]]])
Wrapper for a PostgreSQL large object. See :ref:`large-objects` for an Wrapper for a PostgreSQL large object. See :ref:`large-objects` for an
@ -222,39 +219,6 @@ functionalities defined by the |DBAPI|_.
server versions. server versions.
.. autofunction:: set_wait_callback(f)
.. versionadded:: 2.2.0
.. autofunction:: get_wait_callback()
.. versionadded:: 2.2.0
.. function:: libpq_version()
Return the version number of the ``libpq`` dynamic library loaded as an
integer, in the same format of `~connection.server_version`.
Raise `~psycopg2.NotSupportedError` if the ``psycopg2`` module was
compiled with a ``libpq`` version lesser than 9.1 (which can be detected
by the `~psycopg2.__libpq_version__` constant).
.. seealso:: libpq docs for `PQlibVersion()`__.
.. __: http://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQLIBVERSION
.. function:: quote_ident(str, scope)
Return quoted identifier according to PostgreSQL quoting rules.
The *scope* must be a `connection` or a `cursor`, the underlying
connection encoding is used for any necessary character conversion.
Requires libpq >= 9.0.
.. seealso:: libpq docs for `PQescapeIdentifier()`__
.. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER
.. _sql-adaptation-objects: .. _sql-adaptation-objects:
@ -514,6 +478,106 @@ The module exports a few exceptions in addition to the :ref:`standard ones
.. _coroutines-functions:
Coroutines support functions
----------------------------
These functions are used to set and retrieve the callback function for
:ref:`cooperation with coroutine libraries <green-support>`.
.. versionadded:: 2.2.0
.. autofunction:: set_wait_callback(f)
.. autofunction:: get_wait_callback()
Other functions
---------------
.. function:: libpq_version()
Return the version number of the ``libpq`` dynamic library loaded as an
integer, in the same format of `~connection.server_version`.
Raise `~psycopg2.NotSupportedError` if the ``psycopg2`` module was
compiled with a ``libpq`` version lesser than 9.1 (which can be detected
by the `~psycopg2.__libpq_version__` constant).
.. versionadded:: 2.7
.. seealso:: libpq docs for `PQlibVersion()`__.
.. __: http://www.postgresql.org/docs/current/static/libpq-misc.html#LIBPQ-PQLIBVERSION
.. function:: make_dsn(dsn=None, \*\*kwargs)
Create a valid connection string from arguments.
Put together the arguments in *kwargs* into a connection string. If *dsn*
is specified too, merge the arguments coming from both the sources. If the
same argument name is specified in both the sources, the *kwargs* value
overrides the *dsn* value.
The input arguments are validated: the output should always be a valid
connection string (as far as `parse_dsn()` is concerned). If not raise
`~psycopg2.ProgrammingError`.
Example::
>>> from psycopg2.extensions import make_dsn
>>> make_dsn('dbname=foo host=example.com', password="s3cr3t")
'host=example.com password=s3cr3t dbname=foo'
.. versionadded:: 2.7
.. function:: parse_dsn(dsn)
Parse connection string into a dictionary of keywords and values.
Parsing is delegated to the libpq: different versions of the client
library may support different formats or parameters (for example,
`connection URIs`__ are only supported from libpq 9.2). Raise
`~psycopg2.ProgrammingError` if the *dsn* is not valid.
.. __: http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
Example::
>>> from psycopg2.extensions import parse_dsn
>>> parse_dsn('dbname=test user=postgres password=secret')
{'password': 'secret', 'user': 'postgres', 'dbname': 'test'}
>>> parse_dsn("postgresql://someone@example.com/somedb?connect_timeout=10")
{'host': 'example.com', 'user': 'someone', 'dbname': 'somedb', 'connect_timeout': '10'}
.. versionadded:: 2.7
.. seealso:: libpq docs for `PQconninfoParse()`__.
.. __: http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PQCONNINFOPARSE
.. function:: quote_ident(str, scope)
Return quoted identifier according to PostgreSQL quoting rules.
The *scope* must be a `connection` or a `cursor`, the underlying
connection encoding is used for any necessary character conversion.
Requires libpq >= 9.0.
.. versionadded:: 2.7
.. seealso:: libpq docs for `PQescapeIdentifier()`__
.. __: http://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQESCAPEIDENTIFIER
.. index:: .. index::
pair: Isolation level; Constants pair: Isolation level; Constants

View File

@ -17,37 +17,34 @@ The module interface respects the standard defined in the |DBAPI|_.
single: DSN (Database Source Name) single: DSN (Database Source Name)
.. function:: .. function::
connect(dsn, connection_factory=None, cursor_factory=None, async=False) connect(dsn=None, connection_factory=None, cursor_factory=None, async=False, \*\*kwargs)
connect(\*\*kwargs, connection_factory=None, cursor_factory=None, async=False)
Create a new database session and return a new `connection` object. Create a new database session and return a new `connection` object.
The connection parameters can be specified either as a `libpq connection The connection parameters can be specified as a `libpq connection
string`__ using the *dsn* parameter:: string`__ using the *dsn* parameter::
conn = psycopg2.connect("dbname=test user=postgres password=secret") conn = psycopg2.connect("dbname=test user=postgres password=secret")
or using a set of keyword arguments:: or using a set of keyword arguments::
conn = psycopg2.connect(database="test", user="postgres", password="secret") conn = psycopg2.connect(dbname"test", user="postgres", password="secret")
The two call styles are mutually exclusive: you cannot specify connection or using a mix of both: if the same parameter name is specified in both
parameters as keyword arguments together with a connection string; only sources, the *kwargs* value will have precedence over the *dsn* value.
the parameters not needed for the database connection (*i.e.* Note that either the *dsn* or at least one connection-related keyword
*connection_factory*, *cursor_factory*, and *async*) are supported argument is required.
together with the *dsn* argument.
The basic connection parameters are: The basic connection parameters are:
- `!dbname` -- the database name (only in the *dsn* string) - `!dbname` -- the database name (`!database` is a deprecated alias)
- `!database` -- the database name (only as keyword argument)
- `!user` -- user name used to authenticate - `!user` -- user name used to authenticate
- `!password` -- password used to authenticate - `!password` -- password used to authenticate
- `!host` -- database host address (defaults to UNIX socket if not provided) - `!host` -- database host address (defaults to UNIX socket if not provided)
- `!port` -- connection port number (defaults to 5432 if not provided) - `!port` -- connection port number (defaults to 5432 if not provided)
Any other connection parameter supported by the client library/server can Any other connection parameter supported by the client library/server can
be passed either in the connection string or as keywords. The PostgreSQL be passed either in the connection string or as a keyword. The PostgreSQL
documentation contains the complete list of the `supported parameters`__. documentation contains the complete list of the `supported parameters`__.
Also note that the same parameters can be passed to the client library Also note that the same parameters can be passed to the client library
using `environment variables`__. using `environment variables`__.
@ -76,6 +73,9 @@ The module interface respects the standard defined in the |DBAPI|_.
.. versionchanged:: 2.5 .. versionchanged:: 2.5
added the *cursor_factory* parameter. added the *cursor_factory* parameter.
.. versionchanged:: 2.7
both *dsn* and keyword arguments can be specified.
.. seealso:: .. seealso::
- `~psycopg2.extensions.parse_dsn` - `~psycopg2.extensions.parse_dsn`
@ -89,8 +89,8 @@ The module interface respects the standard defined in the |DBAPI|_.
.. extension:: .. extension::
The parameters *connection_factory* and *async* are Psycopg extensions The non-connection-related keyword parameters are Psycopg extensions
to the |DBAPI|. to the |DBAPI|_.
.. data:: apilevel .. data:: apilevel

View File

@ -81,12 +81,12 @@ else:
del Decimal, Adapter del Decimal, Adapter
def connect(dsn=None, def connect(dsn=None, connection_factory=None, cursor_factory=None,
connection_factory=None, cursor_factory=None, async=False, **kwargs): async=False, **kwargs):
""" """
Create a new database connection. Create a new database connection.
The connection parameters can be specified either as a string: The connection parameters can be specified as a string:
conn = psycopg2.connect("dbname=test user=postgres password=secret") conn = psycopg2.connect("dbname=test user=postgres password=secret")
@ -94,9 +94,9 @@ def connect(dsn=None,
conn = psycopg2.connect(database="test", user="postgres", password="secret") conn = psycopg2.connect(database="test", user="postgres", password="secret")
The basic connection parameters are: Or as a mix of both. The basic connection parameters are:
- *dbname*: the database name (only in dsn string) - *dbname*: the database name
- *database*: the database name (only as keyword argument) - *database*: the database name (only as keyword argument)
- *user*: user name used to authenticate - *user*: user name used to authenticate
- *password*: password used to authenticate - *password*: password used to authenticate
@ -116,7 +116,11 @@ def connect(dsn=None,
library: the list of supported parameters depends on the library version. library: the list of supported parameters depends on the library version.
""" """
conn = _connect(dsn, connection_factory, async, **kwargs) if dsn is None and not kwargs:
raise TypeError('missing dsn and no parameters')
dsn = _ext.make_dsn(dsn, **kwargs)
conn = _connect(dsn, connection_factory=connection_factory, async=async)
if cursor_factory is not None: if cursor_factory is not None:
conn.cursor_factory = cursor_factory conn.cursor_factory = cursor_factory

View File

@ -38,11 +38,17 @@ def lookup(code, _cache={}):
return _cache[code] return _cache[code]
# Generate the lookup map at first usage. # Generate the lookup map at first usage.
tmp = {}
for k, v in globals().iteritems(): for k, v in globals().iteritems():
if isinstance(v, str) and len(v) in (2, 5): if isinstance(v, str) and len(v) in (2, 5):
_cache[v] = k tmp[v] = k
return lookup(code) assert tmp
# Atomic update, to avoid race condition on import (bug #382)
_cache.update(tmp)
return _cache[code]
# autogenerated data: do not edit below this point. # autogenerated data: do not edit below this point.

View File

@ -7,7 +7,7 @@ This module holds all the extensions to the DBAPI-2.0 provided by psycopg.
- `lobject` -- the new-type inheritable large object class - `lobject` -- the new-type inheritable large object class
- `adapt()` -- exposes the PEP-246_ compatible adapting mechanism used - `adapt()` -- exposes the PEP-246_ compatible adapting mechanism used
by psycopg to adapt Python types to PostgreSQL ones by psycopg to adapt Python types to PostgreSQL ones
.. _PEP-246: http://www.python.org/peps/pep-0246.html .. _PEP-246: http://www.python.org/peps/pep-0246.html
""" """
# psycopg/extensions.py - DBAPI-2.0 extensions specific to psycopg # psycopg/extensions.py - DBAPI-2.0 extensions specific to psycopg
@ -32,6 +32,9 @@ This module holds all the extensions to the DBAPI-2.0 provided by psycopg.
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details. # License for more details.
import re as _re
import sys as _sys
from psycopg2._psycopg import UNICODE, INTEGER, LONGINTEGER, BOOLEAN, FLOAT from psycopg2._psycopg import UNICODE, INTEGER, LONGINTEGER, BOOLEAN, FLOAT
from psycopg2._psycopg import TIME, DATE, INTERVAL, DECIMAL from psycopg2._psycopg import TIME, DATE, INTERVAL, DECIMAL
from psycopg2._psycopg import BINARYARRAY, BOOLEANARRAY, DATEARRAY, DATETIMEARRAY from psycopg2._psycopg import BINARYARRAY, BOOLEANARRAY, DATEARRAY, DATETIMEARRAY
@ -56,8 +59,8 @@ try:
except ImportError: except ImportError:
pass pass
from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid, libpq_version from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor
from psycopg2._psycopg import parse_dsn, make_dsn, quote_ident from psycopg2._psycopg import lobject, Xid, libpq_version, parse_dsn, quote_ident
from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type
from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics, Column from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics, Column
@ -101,7 +104,6 @@ TRANSACTION_STATUS_INTRANS = 2
TRANSACTION_STATUS_INERROR = 3 TRANSACTION_STATUS_INERROR = 3
TRANSACTION_STATUS_UNKNOWN = 4 TRANSACTION_STATUS_UNKNOWN = 4
import sys as _sys
# Return bytes from a string # Return bytes from a string
if _sys.version_info[0] < 3: if _sys.version_info[0] < 3:
@ -111,6 +113,7 @@ else:
def b(s): def b(s):
return s.encode('utf8') return s.encode('utf8')
def register_adapter(typ, callable): def register_adapter(typ, callable):
"""Register 'callable' as an ISQLQuote adapter for type 'typ'.""" """Register 'callable' as an ISQLQuote adapter for type 'typ'."""
adapters[(typ, ISQLQuote)] = callable adapters[(typ, ISQLQuote)] = callable
@ -154,6 +157,53 @@ class NoneAdapter(object):
return _null return _null
def make_dsn(dsn=None, **kwargs):
"""Convert a set of keywords into a connection strings."""
if dsn is None and not kwargs:
return ''
# If no kwarg is specified don't mung the dsn, but verify it
if not kwargs:
parse_dsn(dsn)
return dsn
# Override the dsn with the parameters
if 'database' in kwargs:
if 'dbname' in kwargs:
raise TypeError(
"you can't specify both 'database' and 'dbname' arguments")
kwargs['dbname'] = kwargs.pop('database')
if dsn is not None:
tmp = parse_dsn(dsn)
tmp.update(kwargs)
kwargs = tmp
dsn = " ".join(["%s=%s" % (k, _param_escape(str(v)))
for (k, v) in kwargs.iteritems()])
# verify that the returned dsn is valid
parse_dsn(dsn)
return dsn
def _param_escape(s,
re_escape=_re.compile(r"([\\'])"),
re_space=_re.compile(r'\s')):
"""
Apply the escaping rule required by PQconnectdb
"""
if not s:
return "''"
s = re_escape.sub(r'\\\1', s)
if re_space.search(s):
s = "'" + s + "'"
return s
# Create default json typecasters for PostgreSQL 9.2 oids # Create default json typecasters for PostgreSQL 9.2 oids
from psycopg2._json import register_default_json, register_default_jsonb from psycopg2._json import register_default_json, register_default_jsonb

View File

@ -28,7 +28,7 @@ old code while porting to psycopg 2. Import it as follows::
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details. # License for more details.
import _psycopg as _2psycopg import psycopg2._psycopg as _2psycopg
from psycopg2.extensions import cursor as _2cursor from psycopg2.extensions import cursor as _2cursor
from psycopg2.extensions import connection as _2connection from psycopg2.extensions import connection as _2connection

View File

@ -199,7 +199,8 @@ exit:
} }
#define psyco_parse_dsn_doc "parse_dsn(dsn) -> dict" #define psyco_parse_dsn_doc \
"parse_dsn(dsn) -> dict -- parse a connection string into parameters"
PyObject * PyObject *
psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs) psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs)
@ -219,7 +220,7 @@ psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs)
options = PQconninfoParse(Bytes_AS_STRING(dsn), &err); options = PQconninfoParse(Bytes_AS_STRING(dsn), &err);
if (options == NULL) { if (options == NULL) {
if (err != NULL) { if (err != NULL) {
PyErr_Format(ProgrammingError, "error parsing the dsn: %s", err); PyErr_Format(ProgrammingError, "invalid dsn: %s", err);
PQfreemem(err); PQfreemem(err);
} else { } else {
PyErr_SetString(OperationalError, "PQconninfoParse() failed"); PyErr_SetString(OperationalError, "PQconninfoParse() failed");

View File

@ -35,6 +35,7 @@ import test_replication
import test_copy import test_copy
import test_cursor import test_cursor
import test_dates import test_dates
import test_errcodes
import test_extras_dictcursor import test_extras_dictcursor
import test_green import test_green
import test_lobject import test_lobject
@ -73,6 +74,7 @@ def test_suite():
suite.addTest(test_copy.test_suite()) suite.addTest(test_copy.test_suite())
suite.addTest(test_cursor.test_suite()) suite.addTest(test_cursor.test_suite())
suite.addTest(test_dates.test_suite()) suite.addTest(test_dates.test_suite())
suite.addTest(test_errcodes.test_suite())
suite.addTest(test_extras_dictcursor.test_suite()) suite.addTest(test_extras_dictcursor.test_suite())
suite.addTest(test_green.test_suite()) suite.addTest(test_green.test_suite())
suite.addTest(test_lobject.test_suite()) suite.addTest(test_lobject.test_suite())

View File

@ -32,6 +32,7 @@ from StringIO import StringIO
import psycopg2 import psycopg2
import psycopg2.errorcodes import psycopg2.errorcodes
import psycopg2.extensions import psycopg2.extensions
ext = psycopg2.extensions
from testutils import unittest, decorate_all_tests, skip_if_no_superuser from testutils import unittest, decorate_all_tests, skip_if_no_superuser
from testutils import skip_before_postgres, skip_after_postgres, skip_before_libpq from testutils import skip_before_postgres, skip_after_postgres, skip_before_libpq
@ -125,7 +126,7 @@ class ConnectionTests(ConnectingTestCase):
if self.conn.server_version >= 90300: if self.conn.server_version >= 90300:
cur.execute("set client_min_messages=debug1") cur.execute("set client_min_messages=debug1")
for i in range(0, 100, 10): for i in range(0, 100, 10):
sql = " ".join(["create temp table table%d (id serial);" % j for j in range(i, i+10)]) sql = " ".join(["create temp table table%d (id serial);" % j for j in range(i, i + 10)])
cur.execute(sql) cur.execute(sql)
self.assertEqual(50, len(conn.notices)) self.assertEqual(50, len(conn.notices))
@ -151,7 +152,7 @@ class ConnectionTests(ConnectingTestCase):
# not limited, but no error # not limited, but no error
for i in range(0, 100, 10): for i in range(0, 100, 10):
sql = " ".join(["create temp table table2_%d (id serial);" % j for j in range(i, i+10)]) sql = " ".join(["create temp table table2_%d (id serial);" % j for j in range(i, i + 10)])
cur.execute(sql) cur.execute(sql)
self.assertEqual(len([n for n in conn.notices if 'CREATE TABLE' in n]), self.assertEqual(len([n for n in conn.notices if 'CREATE TABLE' in n]),
@ -172,7 +173,7 @@ class ConnectionTests(ConnectingTestCase):
self.assert_(self.conn.server_version) self.assert_(self.conn.server_version)
def test_protocol_version(self): def test_protocol_version(self):
self.assert_(self.conn.protocol_version in (2,3), self.assert_(self.conn.protocol_version in (2, 3),
self.conn.protocol_version) self.conn.protocol_version)
def test_tpc_unsupported(self): def test_tpc_unsupported(self):
@ -252,7 +253,7 @@ class ConnectionTests(ConnectingTestCase):
t1.start() t1.start()
i = 1 i = 1
for i in range(1000): for i in range(1000):
cur.execute("select %s;",(i,)) cur.execute("select %s;", (i,))
conn.commit() conn.commit()
while conn.notices: while conn.notices:
notices.append((1, conn.notices.pop())) notices.append((1, conn.notices.pop()))
@ -313,16 +314,15 @@ class ConnectionTests(ConnectingTestCase):
class ParseDsnTestCase(ConnectingTestCase): class ParseDsnTestCase(ConnectingTestCase):
def test_parse_dsn(self): def test_parse_dsn(self):
from psycopg2 import ProgrammingError from psycopg2 import ProgrammingError
from psycopg2.extensions import parse_dsn
self.assertEqual(parse_dsn('dbname=test user=tester password=secret'), self.assertEqual(ext.parse_dsn('dbname=test user=tester password=secret'),
dict(user='tester', password='secret', dbname='test'), dict(user='tester', password='secret', dbname='test'),
"simple DSN parsed") "simple DSN parsed")
self.assertRaises(ProgrammingError, parse_dsn, self.assertRaises(ProgrammingError, ext.parse_dsn,
"dbname=test 2 user=tester password=secret") "dbname=test 2 user=tester password=secret")
self.assertEqual(parse_dsn("dbname='test 2' user=tester password=secret"), self.assertEqual(ext.parse_dsn("dbname='test 2' user=tester password=secret"),
dict(user='tester', password='secret', dbname='test 2'), dict(user='tester', password='secret', dbname='test 2'),
"DSN with quoting parsed") "DSN with quoting parsed")
@ -332,7 +332,7 @@ class ParseDsnTestCase(ConnectingTestCase):
raised = False raised = False
try: try:
# unterminated quote after dbname: # unterminated quote after dbname:
parse_dsn("dbname='test 2 user=tester password=secret") ext.parse_dsn("dbname='test 2 user=tester password=secret")
except ProgrammingError, e: except ProgrammingError, e:
raised = True raised = True
self.assertTrue(str(e).find('secret') < 0, self.assertTrue(str(e).find('secret') < 0,
@ -343,16 +343,14 @@ class ParseDsnTestCase(ConnectingTestCase):
@skip_before_libpq(9, 2) @skip_before_libpq(9, 2)
def test_parse_dsn_uri(self): def test_parse_dsn_uri(self):
from psycopg2.extensions import parse_dsn self.assertEqual(ext.parse_dsn('postgresql://tester:secret@/test'),
self.assertEqual(parse_dsn('postgresql://tester:secret@/test'),
dict(user='tester', password='secret', dbname='test'), dict(user='tester', password='secret', dbname='test'),
"valid URI dsn parsed") "valid URI dsn parsed")
raised = False raised = False
try: try:
# extra '=' after port value # extra '=' after port value
parse_dsn(dsn='postgresql://tester:secret@/test?port=1111=x') ext.parse_dsn(dsn='postgresql://tester:secret@/test?port=1111=x')
except psycopg2.ProgrammingError, e: except psycopg2.ProgrammingError, e:
raised = True raised = True
self.assertTrue(str(e).find('secret') < 0, self.assertTrue(str(e).find('secret') < 0,
@ -362,24 +360,91 @@ class ParseDsnTestCase(ConnectingTestCase):
self.assertTrue(raised, "ProgrammingError raised due to invalid URI") self.assertTrue(raised, "ProgrammingError raised due to invalid URI")
def test_unicode_value(self): def test_unicode_value(self):
from psycopg2.extensions import parse_dsn
snowman = u"\u2603" snowman = u"\u2603"
d = parse_dsn('dbname=' + snowman) d = ext.parse_dsn('dbname=' + snowman)
if sys.version_info[0] < 3: if sys.version_info[0] < 3:
self.assertEqual(d['dbname'], snowman.encode('utf8')) self.assertEqual(d['dbname'], snowman.encode('utf8'))
else: else:
self.assertEqual(d['dbname'], snowman) self.assertEqual(d['dbname'], snowman)
def test_unicode_key(self): def test_unicode_key(self):
from psycopg2.extensions import parse_dsn
snowman = u"\u2603" snowman = u"\u2603"
self.assertRaises(psycopg2.ProgrammingError, parse_dsn, self.assertRaises(psycopg2.ProgrammingError, ext.parse_dsn,
snowman + '=' + snowman) snowman + '=' + snowman)
def test_bad_param(self): def test_bad_param(self):
from psycopg2.extensions import parse_dsn self.assertRaises(TypeError, ext.parse_dsn, None)
self.assertRaises(TypeError, parse_dsn, None) self.assertRaises(TypeError, ext.parse_dsn, 42)
self.assertRaises(TypeError, parse_dsn, 42)
class MakeDsnTestCase(ConnectingTestCase):
def assertDsnEqual(self, dsn1, dsn2):
self.assertEqual(set(dsn1.split()), set(dsn2.split()))
def test_empty_arguments(self):
self.assertEqual(ext.make_dsn(), '')
def test_empty_string(self):
dsn = ext.make_dsn('')
self.assertEqual(dsn, '')
def test_params_validation(self):
self.assertRaises(psycopg2.ProgrammingError,
ext.make_dsn, 'dbnamo=a')
self.assertRaises(psycopg2.ProgrammingError,
ext.make_dsn, dbnamo='a')
self.assertRaises(psycopg2.ProgrammingError,
ext.make_dsn, 'dbname=a', nosuchparam='b')
def test_empty_param(self):
dsn = ext.make_dsn(dbname='sony', password='')
self.assertDsnEqual(dsn, "dbname=sony password=''")
def test_escape(self):
dsn = ext.make_dsn(dbname='hello world')
self.assertEqual(dsn, "dbname='hello world'")
dsn = ext.make_dsn(dbname=r'back\slash')
self.assertEqual(dsn, r"dbname=back\\slash")
dsn = ext.make_dsn(dbname="quo'te")
self.assertEqual(dsn, r"dbname=quo\'te")
dsn = ext.make_dsn(dbname="with\ttab")
self.assertEqual(dsn, "dbname='with\ttab'")
dsn = ext.make_dsn(dbname=r"\every thing'")
self.assertEqual(dsn, r"dbname='\\every thing\''")
def test_database_is_a_keyword(self):
self.assertEqual(ext.make_dsn(database='sigh'), "dbname=sigh")
def test_params_merging(self):
dsn = ext.make_dsn('dbname=foo host=bar', host='baz')
self.assertDsnEqual(dsn, 'dbname=foo host=baz')
dsn = ext.make_dsn('dbname=foo', user='postgres')
self.assertDsnEqual(dsn, 'dbname=foo user=postgres')
def test_no_dsn_munging(self):
dsnin = 'dbname=a host=b user=c password=d'
dsn = ext.make_dsn(dsnin)
self.assertEqual(dsn, dsnin)
@skip_before_libpq(9, 2)
def test_url_is_cool(self):
url = 'postgresql://tester:secret@/test?application_name=wat'
dsn = ext.make_dsn(url)
self.assertEqual(dsn, url)
dsn = ext.make_dsn(url, application_name='woot')
self.assertDsnEqual(dsn,
'dbname=test user=tester password=secret application_name=woot')
self.assertRaises(psycopg2.ProgrammingError,
ext.make_dsn, 'postgresql://tester:secret@/test?nosuch=param')
self.assertRaises(psycopg2.ProgrammingError,
ext.make_dsn, url, nosuch="param")
class IsolationLevelsTestCase(ConnectingTestCase): class IsolationLevelsTestCase(ConnectingTestCase):
@ -587,7 +652,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.close() cnn.close()
return return
gids = [ r[0] for r in cur ] gids = [r[0] for r in cur]
for gid in gids: for gid in gids:
cur.execute("rollback prepared %s;", (gid,)) cur.execute("rollback prepared %s;", (gid,))
cnn.close() cnn.close()
@ -761,13 +826,13 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
def test_status_after_recover(self): def test_status_after_recover(self):
cnn = self.connect() cnn = self.connect()
self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status) self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status)
xns = cnn.tpc_recover() cnn.tpc_recover()
self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status) self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status)
cur = cnn.cursor() cur = cnn.cursor()
cur.execute("select 1") cur.execute("select 1")
self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status) self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status)
xns = cnn.tpc_recover() cnn.tpc_recover()
self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status) self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status)
def test_recovered_xids(self): def test_recovered_xids(self):
@ -789,12 +854,12 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn = self.connect() cnn = self.connect()
xids = cnn.tpc_recover() xids = cnn.tpc_recover()
xids = [ xid for xid in xids if xid.database == dbname ] xids = [xid for xid in xids if xid.database == dbname]
xids.sort(key=attrgetter('gtrid')) xids.sort(key=attrgetter('gtrid'))
# check the values returned # check the values returned
self.assertEqual(len(okvals), len(xids)) self.assertEqual(len(okvals), len(xids))
for (xid, (gid, prepared, owner, database)) in zip (xids, okvals): for (xid, (gid, prepared, owner, database)) in zip(xids, okvals):
self.assertEqual(xid.gtrid, gid) self.assertEqual(xid.gtrid, gid)
self.assertEqual(xid.prepared, prepared) self.assertEqual(xid.prepared, prepared)
self.assertEqual(xid.owner, owner) self.assertEqual(xid.owner, owner)
@ -825,8 +890,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.close() cnn.close()
cnn = self.connect() cnn = self.connect()
xids = [ xid for xid in cnn.tpc_recover() xids = [x for x in cnn.tpc_recover() if x.database == dbname]
if xid.database == dbname ]
self.assertEqual(1, len(xids)) self.assertEqual(1, len(xids))
xid = xids[0] xid = xids[0]
self.assertEqual(xid.format_id, fid) self.assertEqual(xid.format_id, fid)
@ -847,8 +911,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.close() cnn.close()
cnn = self.connect() cnn = self.connect()
xids = [ xid for xid in cnn.tpc_recover() xids = [x for x in cnn.tpc_recover() if x.database == dbname]
if xid.database == dbname ]
self.assertEqual(1, len(xids)) self.assertEqual(1, len(xids))
xid = xids[0] xid = xids[0]
self.assertEqual(xid.format_id, None) self.assertEqual(xid.format_id, None)
@ -893,8 +956,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.tpc_begin(x1) cnn.tpc_begin(x1)
cnn.tpc_prepare() cnn.tpc_prepare()
cnn.reset() cnn.reset()
xid = [ xid for xid in cnn.tpc_recover() xid = [x for x in cnn.tpc_recover() if x.database == dbname][0]
if xid.database == dbname ][0]
self.assertEqual(10, xid.format_id) self.assertEqual(10, xid.format_id)
self.assertEqual('uni', xid.gtrid) self.assertEqual('uni', xid.gtrid)
self.assertEqual('code', xid.bqual) self.assertEqual('code', xid.bqual)
@ -909,8 +971,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.tpc_prepare() cnn.tpc_prepare()
cnn.reset() cnn.reset()
xid = [ xid for xid in cnn.tpc_recover() xid = [x for x in cnn.tpc_recover() if x.database == dbname][0]
if xid.database == dbname ][0]
self.assertEqual(None, xid.format_id) self.assertEqual(None, xid.format_id)
self.assertEqual('transaction-id', xid.gtrid) self.assertEqual('transaction-id', xid.gtrid)
self.assertEqual(None, xid.bqual) self.assertEqual(None, xid.bqual)
@ -929,7 +990,7 @@ class ConnectionTwoPhaseTests(ConnectingTestCase):
cnn.reset() cnn.reset()
xids = cnn.tpc_recover() xids = cnn.tpc_recover()
xid = [ xid for xid in xids if xid.database == dbname ][0] xid = [x for x in xids if x.database == dbname][0]
self.assertEqual(None, xid.format_id) self.assertEqual(None, xid.format_id)
self.assertEqual('dict-connection', xid.gtrid) self.assertEqual('dict-connection', xid.gtrid)
self.assertEqual(None, xid.bqual) self.assertEqual(None, xid.bqual)

65
tests/test_errcodes.py Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python
# test_errcodes.py - unit test for psycopg2.errcodes module
#
# Copyright (C) 2015 Daniele Varrazzo <daniele.varrazzo@gmail.com>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# In addition, as a special exception, the copyright holders give
# permission to link this program with the OpenSSL library (or with
# modified versions of OpenSSL that use the same license as OpenSSL),
# and distribute linked combinations including the two.
#
# You must obey the GNU Lesser General Public License in all respects for
# all of the code used other than OpenSSL.
#
# psycopg2 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
from testutils import unittest, ConnectingTestCase
try:
reload
except NameError:
from imp import reload
from threading import Thread
from psycopg2 import errorcodes
class ErrocodeTests(ConnectingTestCase):
def test_lookup_threadsafe(self):
# Increase if it does not fail with KeyError
MAX_CYCLES = 2000
errs = []
def f(pg_code='40001'):
try:
errorcodes.lookup(pg_code)
except Exception, e:
errs.append(e)
for __ in xrange(MAX_CYCLES):
reload(errorcodes)
(t1, t2) = (Thread(target=f), Thread(target=f))
(t1.start(), t2.start())
(t1.join(), t2.join())
if errs:
self.fail(
"raised %s errors in %s cycles (first is %s %s)" % (
len(errs), MAX_CYCLES,
errs[0].__class__.__name__, errs[0]))
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == "__main__":
unittest.main()

View File

@ -31,11 +31,13 @@ from testutils import ConnectingTestCase, skip_copy_if_green, script_to_py3
import psycopg2 import psycopg2
class ConnectTestCase(unittest.TestCase): class ConnectTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.args = None self.args = None
def connect_stub(*args, **kwargs):
self.args = psycopg2.parse_args(*args, **kwargs) def conect_stub(dsn, connection_factory=None, async=False):
self.args = (dsn, connection_factory, async)
self._connect_orig = psycopg2._connect self._connect_orig = psycopg2._connect
psycopg2._connect = connect_stub psycopg2._connect = connect_stub
@ -43,6 +45,9 @@ class ConnectTestCase(unittest.TestCase):
def tearDown(self): def tearDown(self):
psycopg2._connect = self._connect_orig psycopg2._connect = self._connect_orig
def assertDsnEqual(self, dsn1, dsn2):
self.assertEqual(set(dsn1.split()), set(dsn2.split()))
def test_there_has_to_be_something(self): def test_there_has_to_be_something(self):
self.assertRaises(TypeError, psycopg2.connect) self.assertRaises(TypeError, psycopg2.connect)
self.assertRaises(TypeError, psycopg2.connect, self.assertRaises(TypeError, psycopg2.connect,
@ -57,8 +62,8 @@ class ConnectTestCase(unittest.TestCase):
self.assertEqual(self.args[2], False) self.assertEqual(self.args[2], False)
def test_dsn(self): def test_dsn(self):
psycopg2.connect('dbname=blah x=y') psycopg2.connect('dbname=blah host=y')
self.assertEqual(self.args[0], 'dbname=blah x=y') self.assertEqual(self.args[0], 'dbname=blah host=y')
self.assertEqual(self.args[1], None) self.assertEqual(self.args[1], None)
self.assertEqual(self.args[2], False) self.assertEqual(self.args[2], False)
@ -83,39 +88,31 @@ class ConnectTestCase(unittest.TestCase):
self.assertEqual(len(self.args[0].split()), 4) self.assertEqual(len(self.args[0].split()), 4)
def test_generic_keywords(self): def test_generic_keywords(self):
psycopg2.connect(foo='bar') psycopg2.connect(options='stuff')
self.assertEqual(self.args[0], 'foo=bar') self.assertEqual(self.args[0], 'options=stuff')
def test_factory(self): def test_factory(self):
def f(dsn, async=False): def f(dsn, async=False):
pass pass
psycopg2.connect(database='foo', bar='baz', connection_factory=f) psycopg2.connect(database='foo', host='baz', connection_factory=f)
dsn = " %s " % self.args[0] self.assertDsnEqual(self.args[0], 'dbname=foo host=baz')
self.assertIn(" dbname=foo ", dsn)
self.assertIn(" bar=baz ", dsn)
self.assertEqual(self.args[1], f) self.assertEqual(self.args[1], f)
self.assertEqual(self.args[2], False) self.assertEqual(self.args[2], False)
psycopg2.connect("dbname=foo bar=baz", connection_factory=f) psycopg2.connect("dbname=foo host=baz", connection_factory=f)
dsn = " %s " % self.args[0] self.assertDsnEqual(self.args[0], 'dbname=foo host=baz')
self.assertIn(" dbname=foo ", dsn)
self.assertIn(" bar=baz ", dsn)
self.assertEqual(self.args[1], f) self.assertEqual(self.args[1], f)
self.assertEqual(self.args[2], False) self.assertEqual(self.args[2], False)
def test_async(self): def test_async(self):
psycopg2.connect(database='foo', bar='baz', async=1) psycopg2.connect(database='foo', host='baz', async=1)
dsn = " %s " % self.args[0] self.assertDsnEqual(self.args[0], 'dbname=foo host=baz')
self.assertIn(" dbname=foo ", dsn)
self.assertIn(" bar=baz ", dsn)
self.assertEqual(self.args[1], None) self.assertEqual(self.args[1], None)
self.assert_(self.args[2]) self.assert_(self.args[2])
psycopg2.connect("dbname=foo bar=baz", async=True) psycopg2.connect("dbname=foo host=baz", async=True)
dsn = " %s " % self.args[0] self.assertDsnEqual(self.args[0], 'dbname=foo host=baz')
self.assertIn(" dbname=foo ", dsn)
self.assertIn(" bar=baz ", dsn)
self.assertEqual(self.args[1], None) self.assertEqual(self.args[1], None)
self.assert_(self.args[2]) self.assert_(self.args[2])
@ -127,9 +124,7 @@ class ConnectTestCase(unittest.TestCase):
def test_empty_param(self): def test_empty_param(self):
psycopg2.connect(database='sony', password='') psycopg2.connect(database='sony', password='')
dsn = " %s " % self.args[0] self.assertDsnEqual(self.args[0], "dbname=sony password=''")
self.assertIn(" dbname=sony ", dsn)
self.assertIn(" password='' ", dsn)
def test_escape(self): def test_escape(self):
psycopg2.connect(database='hello world') psycopg2.connect(database='hello world')
@ -147,13 +142,12 @@ class ConnectTestCase(unittest.TestCase):
psycopg2.connect(database=r"\every thing'") psycopg2.connect(database=r"\every thing'")
self.assertEqual(self.args[0], r"dbname='\\every thing\''") self.assertEqual(self.args[0], r"dbname='\\every thing\''")
def test_no_kwargs_swallow(self): def test_params_merging(self):
self.assertRaises(TypeError, psycopg2.connect('dbname=foo', database='bar')
psycopg2.connect, 'dbname=foo', database='foo') self.assertEqual(self.args[0], 'dbname=bar')
self.assertRaises(TypeError,
psycopg2.connect, 'dbname=foo', user='postgres') psycopg2.connect('dbname=foo', user='postgres')
self.assertRaises(TypeError, self.assertDsnEqual(self.args[0], 'dbname=foo user=postgres')
psycopg2.connect, 'dbname=foo', no_such_param='meh')
class ExceptionsTestCase(ConnectingTestCase): class ExceptionsTestCase(ConnectingTestCase):
@ -219,7 +213,8 @@ class ExceptionsTestCase(ConnectingTestCase):
self.assertEqual(diag.sqlstate, '42P01') self.assertEqual(diag.sqlstate, '42P01')
del diag del diag
gc.collect(); gc.collect() gc.collect()
gc.collect()
assert(w() is None) assert(w() is None)
@skip_copy_if_green @skip_copy_if_green
@ -341,7 +336,7 @@ class TestVersionDiscovery(unittest.TestCase):
self.assertTrue(type(psycopg2.__libpq_version__) is int) self.assertTrue(type(psycopg2.__libpq_version__) is int)
try: try:
self.assertTrue(type(psycopg2.extensions.libpq_version()) is int) self.assertTrue(type(psycopg2.extensions.libpq_version()) is int)
except NotSupportedError: except psycopg2.NotSupportedError:
self.assertTrue(psycopg2.__libpq_version__ < 90100) self.assertTrue(psycopg2.__libpq_version__ < 90100)