Merge remote-tracking branch 'origin/master' into repl

This commit is contained in:
Oleksandr Shulgin 2015-10-01 19:29:17 +02:00
commit 862eda10c2
10 changed files with 223 additions and 14 deletions

9
NEWS
View File

@ -6,6 +6,7 @@ What's new in psycopg 2.7
New features: New features:
- Added `~psycopg2.extensions.parse_dsn()` function (:ticket:`#321`).
- 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
@ -15,6 +16,14 @@ New features:
(:ticket:`#326`). (:ticket:`#326`).
What's new in psycopg 2.6.2
^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Report the server response status on errors (such as :ticket:`#281`).
- Raise NotSupportedError on unhandled server response status
(:ticket:`#352`).
What's new in psycopg 2.6.1 What's new in psycopg 2.6.1
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -12,6 +12,17 @@
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)
Parse connection string into a dictionary of keywords and values.
Uses libpq's ``PQconninfoParse`` to parse the string according to
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'}
.. class:: connection(dsn, async=False) .. class:: connection(dsn, async=False)

View File

@ -78,6 +78,7 @@ The module interface respects the standard defined in the |DBAPI|_.
.. seealso:: .. seealso::
- `~psycopg2.extensions.parse_dsn`
- libpq `connection string syntax`__ - libpq `connection string syntax`__
- libpq supported `connection parameters`__ - libpq supported `connection parameters`__
- libpq supported `environment variables`__ - libpq supported `environment variables`__
@ -91,7 +92,6 @@ The module interface respects the standard defined in the |DBAPI|_.
The parameters *connection_factory* and *async* are Psycopg extensions The parameters *connection_factory* and *async* are Psycopg extensions
to the |DBAPI|. to the |DBAPI|.
.. data:: apilevel .. data:: apilevel
String constant stating the supported DB API level. For `psycopg2` is String constant stating the supported DB API level. For `psycopg2` is

View File

@ -56,11 +56,12 @@ try:
except ImportError: except ImportError:
pass pass
from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, replicationMessage, lobject, Xid, libpq_version from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid, libpq_version, parse_dsn
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
from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError
from psycopg2._psycopg import replicationMessage
try: try:
from psycopg2._psycopg import set_wait_callback, get_wait_callback from psycopg2._psycopg import set_wait_callback, get_wait_callback

View File

@ -129,28 +129,33 @@ static int pthread_mutex_init(pthread_mutex_t *mutex, void* fake)
/* remove the inline keyword, since it doesn't work unless C++ file */ /* remove the inline keyword, since it doesn't work unless C++ file */
#define inline #define inline
/* Hmmm, MSVC doesn't have a isnan/isinf function, but has _isnan function */ /* Hmmm, MSVC <2015 doesn't have a isnan/isinf function, but has _isnan function */
#if defined (_MSC_VER) #if defined (_MSC_VER)
#if !defined(isnan)
#define isnan(x) (_isnan(x)) #define isnan(x) (_isnan(x))
/* The following line was hacked together from simliar code by Bjorn Reese /* The following line was hacked together from simliar code by Bjorn Reese
* in libxml2 code */ * in libxml2 code */
#define isinf(x) ((_fpclass(x) == _FPCLASS_PINF) ? 1 \ #define isinf(x) ((_fpclass(x) == _FPCLASS_PINF) ? 1 \
: ((_fpclass(x) == _FPCLASS_NINF) ? -1 : 0)) : ((_fpclass(x) == _FPCLASS_NINF) ? -1 : 0))
#endif
#define strcasecmp(x, y) lstrcmpi(x, y) #define strcasecmp(x, y) lstrcmpi(x, y)
#endif #endif
#endif #endif
/* what's this, we have no round function either? */
#if (defined(__FreeBSD__) && __FreeBSD_version < 503000) \ #if (defined(__FreeBSD__) && __FreeBSD_version < 503000) \
|| (defined(_WIN32) && !defined(__GNUC__)) \ || (defined(_WIN32) && !defined(__GNUC__)) \
|| (defined(sun) || defined(__sun__)) \ || (defined(sun) || defined(__sun__)) \
&& (defined(__SunOS_5_8) || defined(__SunOS_5_9)) && (defined(__SunOS_5_8) || defined(__SunOS_5_9))
/* what's this, we have no round function either? */
/* round has been added in the standard library with MSVC 2015 */
#if _MSC_VER < 1900
static double round(double num) static double round(double num)
{ {
return (num >= 0) ? floor(num + 0.5) : ceil(num - 0.5); return (num >= 0) ? floor(num + 0.5) : ceil(num - 0.5);
} }
#endif #endif
#endif
/* resolve missing isinf() function for Solaris */ /* resolve missing isinf() function for Solaris */
#if defined (__SVR4) && defined (__sun) #if defined (__SVR4) && defined (__sun)

View File

@ -198,8 +198,10 @@ pq_raise(connectionObject *conn, cursorObject *curs, PGresult **pgres)
raise and a meaningful message is better than an empty one. raise and a meaningful message is better than an empty one.
Note: it can happen without it being our error: see ticket #82 */ Note: it can happen without it being our error: see ticket #82 */
if (err == NULL || err[0] == '\0') { if (err == NULL || err[0] == '\0') {
PyErr_SetString(DatabaseError, PyErr_Format(DatabaseError,
"error with no message from the libpq"); "error with status %s and no message from the libpq",
PQresStatus(pgres == NULL ?
PQstatus(conn->pgconn) : PQresultStatus(*pgres)));
return; return;
} }
@ -1897,11 +1899,26 @@ pq_fetch(cursorObject *curs, int no_result)
ex = -1; ex = -1;
break; break;
default: case PGRES_BAD_RESPONSE:
Dprintf("pq_fetch: uh-oh, something FAILED: pgconn = %p", curs->conn); case PGRES_NONFATAL_ERROR:
case PGRES_FATAL_ERROR:
Dprintf("pq_fetch: uh-oh, something FAILED: status = %d pgconn = %p",
status, curs->conn);
pq_raise(curs->conn, curs, NULL); pq_raise(curs->conn, curs, NULL);
ex = -1; ex = -1;
break; break;
default:
/* PGRES_COPY_BOTH, PGRES_SINGLE_TUPLE, future statuses */
Dprintf("pq_fetch: got unsupported result: status = %d pgconn = %p",
status, curs->conn);
PyErr_Format(NotSupportedError,
"got server response with unsupported status %s",
PQresStatus(curs->pgres == NULL ?
PQstatus(curs->conn->pgconn) : PQresultStatus(curs->pgres)));
CLEARPGRES(curs->pgres);
ex = -1;
break;
} }
/* error checking, close the connection if necessary (some critical errors /* error checking, close the connection if necessary (some critical errors

View File

@ -113,6 +113,59 @@ psyco_connect(PyObject *self, PyObject *args, PyObject *keywds)
return conn; return conn;
} }
#define psyco_parse_dsn_doc "parse_dsn(dsn) -> dict"
static PyObject *
psyco_parse_dsn(PyObject *self, PyObject *args, PyObject *kwargs)
{
char *err = NULL;
PQconninfoOption *options = NULL, *o;
PyObject *dict = NULL, *res = NULL, *dsn;
static char *kwlist[] = {"dsn", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &dsn)) {
return NULL;
}
Py_INCREF(dsn); /* for ensure_bytes */
if (!(dsn = psycopg_ensure_bytes(dsn))) { goto exit; }
options = PQconninfoParse(Bytes_AS_STRING(dsn), &err);
if (options == NULL) {
if (err != NULL) {
PyErr_Format(ProgrammingError, "error parsing the dsn: %s", err);
PQfreemem(err);
} else {
PyErr_SetString(OperationalError, "PQconninfoParse() failed");
}
goto exit;
}
if (!(dict = PyDict_New())) { goto exit; }
for (o = options; o->keyword != NULL; o++) {
if (o->val != NULL) {
PyObject *value;
if (!(value = Text_FromUTF8(o->val))) { goto exit; }
if (PyDict_SetItemString(dict, o->keyword, value) != 0) {
Py_DECREF(value);
goto exit;
}
Py_DECREF(value);
}
}
/* success */
res = dict;
dict = NULL;
exit:
PQconninfoFree(options); /* safe on null */
Py_XDECREF(dict);
Py_XDECREF(dsn);
return res;
}
/** type registration **/ /** type registration **/
#define psyco_register_type_doc \ #define psyco_register_type_doc \
"register_type(obj, conn_or_curs) -> None -- register obj with psycopg type system\n\n" \ "register_type(obj, conn_or_curs) -> None -- register obj with psycopg type system\n\n" \
@ -709,6 +762,8 @@ error:
static PyMethodDef psycopgMethods[] = { static PyMethodDef psycopgMethods[] = {
{"_connect", (PyCFunction)psyco_connect, {"_connect", (PyCFunction)psyco_connect,
METH_VARARGS|METH_KEYWORDS, psyco_connect_doc}, METH_VARARGS|METH_KEYWORDS, psyco_connect_doc},
{"parse_dsn", (PyCFunction)psyco_parse_dsn,
METH_VARARGS|METH_KEYWORDS, psyco_parse_dsn_doc},
{"adapt", (PyCFunction)psyco_microprotocols_adapt, {"adapt", (PyCFunction)psyco_microprotocols_adapt,
METH_VARARGS, psyco_microprotocols_adapt_doc}, METH_VARARGS, psyco_microprotocols_adapt_doc},

View File

@ -23,16 +23,18 @@
# License for more details. # License for more details.
import os import os
import sys
import time import time
import threading import threading
from operator import attrgetter from operator import attrgetter
from StringIO import StringIO
import psycopg2 import psycopg2
import psycopg2.errorcodes import psycopg2.errorcodes
import psycopg2.extensions import 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 from testutils import skip_before_postgres, skip_after_postgres, skip_before_libpq
from testutils import ConnectingTestCase, skip_if_tpc_disabled from testutils import ConnectingTestCase, skip_if_tpc_disabled
from testutils import skip_if_windows from testutils import skip_if_windows
from testconfig import dsn, dbname from testconfig import dsn, dbname
@ -152,7 +154,8 @@ class ConnectionTests(ConnectingTestCase):
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(100, len(conn.notices)) self.assertEqual(len([n for n in conn.notices if 'CREATE TABLE' in n]),
100)
def test_notices_noappend(self): def test_notices_noappend(self):
conn = self.conn conn = self.conn
@ -307,6 +310,78 @@ class ConnectionTests(ConnectingTestCase):
self.assert_('foobar' not in c.dsn, "password was not obscured") self.assert_('foobar' not in c.dsn, "password was not obscured")
class ParseDsnTestCase(ConnectingTestCase):
def test_parse_dsn(self):
from psycopg2 import ProgrammingError
from psycopg2.extensions import parse_dsn
self.assertEqual(parse_dsn('dbname=test user=tester password=secret'),
dict(user='tester', password='secret', dbname='test'),
"simple DSN parsed")
self.assertRaises(ProgrammingError, parse_dsn,
"dbname=test 2 user=tester password=secret")
self.assertEqual(parse_dsn("dbname='test 2' user=tester password=secret"),
dict(user='tester', password='secret', dbname='test 2'),
"DSN with quoting parsed")
# Can't really use assertRaisesRegexp() here since we need to
# make sure that secret is *not* exposed in the error messgage
# (and it also requires python >= 2.7).
raised = False
try:
# unterminated quote after dbname:
parse_dsn("dbname='test 2 user=tester password=secret")
except ProgrammingError, e:
raised = True
self.assertTrue(str(e).find('secret') < 0,
"DSN was not exposed in error message")
except e:
self.fail("unexpected error condition: " + repr(e))
self.assertTrue(raised, "ProgrammingError raised due to invalid DSN")
@skip_before_libpq(9, 2)
def test_parse_dsn_uri(self):
from psycopg2.extensions import parse_dsn
self.assertEqual(parse_dsn('postgresql://tester:secret@/test'),
dict(user='tester', password='secret', dbname='test'),
"valid URI dsn parsed")
raised = False
try:
# extra '=' after port value
parse_dsn(dsn='postgresql://tester:secret@/test?port=1111=x')
except psycopg2.ProgrammingError, e:
raised = True
self.assertTrue(str(e).find('secret') < 0,
"URI was not exposed in error message")
except e:
self.fail("unexpected error condition: " + repr(e))
self.assertTrue(raised, "ProgrammingError raised due to invalid URI")
def test_unicode_value(self):
from psycopg2.extensions import parse_dsn
snowman = u"\u2603"
d = parse_dsn('dbname=' + snowman)
if sys.version_info[0] < 3:
self.assertEqual(d['dbname'], snowman.encode('utf8'))
else:
self.assertEqual(d['dbname'], snowman)
def test_unicode_key(self):
from psycopg2.extensions import parse_dsn
snowman = u"\u2603"
self.assertRaises(psycopg2.ProgrammingError, parse_dsn,
snowman + '=' + snowman)
def test_bad_param(self):
from psycopg2.extensions import parse_dsn
self.assertRaises(TypeError, parse_dsn, None)
self.assertRaises(TypeError, parse_dsn, 42)
class IsolationLevelsTestCase(ConnectingTestCase): class IsolationLevelsTestCase(ConnectingTestCase):
def setUp(self): def setUp(self):
@ -1103,6 +1178,17 @@ class AutocommitTests(ConnectingTestCase):
self.assertEqual(cur.fetchone()[0], 'on') self.assertEqual(cur.fetchone()[0], 'on')
class ReplicationTest(ConnectingTestCase):
@skip_before_postgres(9, 0)
def test_replication_not_supported(self):
conn = self.repl_connect()
if conn is None: return
cur = conn.cursor()
f = StringIO()
self.assertRaises(psycopg2.NotSupportedError,
cur.copy_expert, "START_REPLICATION 0/0", f)
def test_suite(): def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__) return unittest.TestLoader().loadTestsFromName(__name__)

View File

@ -7,6 +7,8 @@ dbhost = os.environ.get('PSYCOPG2_TESTDB_HOST', None)
dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None) dbport = os.environ.get('PSYCOPG2_TESTDB_PORT', None)
dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None) dbuser = os.environ.get('PSYCOPG2_TESTDB_USER', None)
dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None) dbpass = os.environ.get('PSYCOPG2_TESTDB_PASSWORD', None)
repl_dsn = os.environ.get('PSYCOPG2_TEST_REPL_DSN',
"dbname=psycopg2_test replication=1")
# Check if we want to test psycopg's green path. # Check if we want to test psycopg's green path.
green = os.environ.get('PSYCOPG2_TEST_GREEN', None) green = os.environ.get('PSYCOPG2_TEST_GREEN', None)

View File

@ -28,7 +28,7 @@ import os
import platform import platform
import sys import sys
from functools import wraps from functools import wraps
from testconfig import dsn from testconfig import dsn, repl_dsn
try: try:
import unittest2 import unittest2
@ -103,11 +103,35 @@ class ConnectingTestCase(unittest.TestCase):
"%s (did you remember calling ConnectingTestCase.setUp()?)" "%s (did you remember calling ConnectingTestCase.setUp()?)"
% e) % e)
if 'dsn' in kwargs:
conninfo = kwargs.pop('dsn')
else:
conninfo = dsn
import psycopg2 import psycopg2
conn = psycopg2.connect(dsn, **kwargs) conn = psycopg2.connect(conninfo, **kwargs)
self._conns.append(conn) self._conns.append(conn)
return conn return conn
def repl_connect(self, **kwargs):
"""Return a connection set up for replication
The connection is on "PSYCOPG2_TEST_REPL_DSN" unless overridden by
a *dsn* kwarg.
Should raise a skip test if not available, but guard for None on
old Python versions.
"""
if 'dsn' not in kwargs:
kwargs['dsn'] = repl_dsn
import psycopg2
try:
conn = self.connect(**kwargs)
except psycopg2.OperationalError, e:
return self.skipTest("replication db not configured: %s" % e)
conn.autocommit = True
return conn
def _get_conn(self): def _get_conn(self):
if not hasattr(self, '_the_conn'): if not hasattr(self, '_the_conn'):
self._the_conn = self.connect() self._the_conn = self.connect()
@ -388,4 +412,3 @@ class py3_raises_typeerror(object):
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
assert type is TypeError assert type is TypeError
return True return True