diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index dea10417..3cdd4c4f 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -12,6 +12,17 @@ The module contains a few objects and function extending the minimum set of 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) diff --git a/doc/src/module.rst b/doc/src/module.rst index 36073a23..ad19aa7b 100644 --- a/doc/src/module.rst +++ b/doc/src/module.rst @@ -78,7 +78,7 @@ The module interface respects the standard defined in the |DBAPI|_. .. seealso:: - - `parse_dsn` + - `~psycopg2.extensions.parse_dsn` - libpq `connection string syntax`__ - libpq supported `connection parameters`__ - libpq supported `environment variables`__ @@ -92,18 +92,6 @@ The module interface respects the standard defined in the |DBAPI|_. The parameters *connection_factory* and *async* are Psycopg extensions to 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.parse_dsn('dbname=test user=postgres password=secret') - {'password': 'secret', 'user': 'postgres', 'dbname': 'test'} - .. data:: apilevel String constant stating the supported DB API level. For `psycopg2` is diff --git a/lib/__init__.py b/lib/__init__.py index 27b9d172..cf8c06ae 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -56,7 +56,7 @@ from psycopg2._psycopg import Error, Warning, DataError, DatabaseError, Programm from psycopg2._psycopg import IntegrityError, InterfaceError, InternalError from psycopg2._psycopg import NotSupportedError, OperationalError -from psycopg2._psycopg import _connect, parse_dsn, apilevel, threadsafety, paramstyle +from psycopg2._psycopg import _connect, apilevel, threadsafety, paramstyle from psycopg2._psycopg import __version__ from psycopg2 import tz diff --git a/lib/extensions.py b/lib/extensions.py index 216d8ad2..f951c519 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -56,7 +56,7 @@ try: except ImportError: pass -from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid +from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid, parse_dsn from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics, Column diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index d8f893cc..f36fbf42 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -112,13 +112,12 @@ psyco_connect(PyObject *self, PyObject *args, PyObject *keywds) return conn; } -#define psyco_parse_dsn_doc \ -"parse_dsn(dsn) -- Parse database connection string.\n\n" +#define psyco_parse_dsn_doc "parse_dsn(dsn) -> dict" static PyObject * psyco_parse_dsn(PyObject *self, PyObject *args) { - char *dsn, *err; + char *dsn, *err = NULL; PQconninfoOption *options = NULL, *o; PyObject *res = NULL, *value; @@ -127,20 +126,33 @@ psyco_parse_dsn(PyObject *self, PyObject *args) } options = PQconninfoParse(dsn, &err); - if (!options) { - PyErr_Format(PyExc_RuntimeError, "PQconninfoParse: %s: %s", dsn, err); - PQfreemem(err); + if (options == NULL) { + if (err != NULL) { + PyErr_Format(ProgrammingError, "error parsing the dsn: %s", err); + PQfreemem(err); + } else { + PyErr_SetString(OperationalError, "PQconninfoParse() failed"); + } return NULL; } res = PyDict_New(); - for (o = options; o->keyword != NULL; o++) { - if (o->val != NULL) { - value = PyString_FromString(o->val); - if (value == NULL || PyDict_SetItemString(res, o->keyword, value) != 0) { - Py_DECREF(res); - res = NULL; - break; + if (res != NULL) { + for (o = options; o->keyword != NULL; o++) { + if (o->val != NULL) { + value = Text_FromUTF8(o->val); + if (value == NULL) { + Py_DECREF(res); + res = NULL; + break; + } + if (PyDict_SetItemString(res, o->keyword, value) != 0) { + Py_DECREF(value); + Py_DECREF(res); + res = NULL; + break; + } + Py_DECREF(value); } } } diff --git a/tests/test_connection.py b/tests/test_connection.py index 340693e2..eb80d4c4 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -270,6 +270,37 @@ class ConnectionTests(ConnectingTestCase): self.assert_(c.closed, "connection failed so it must be closed") self.assert_('foobar' not in c.dsn, "password was not obscured") + 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.assertEqual(parse_dsn("dbname='test 2' user=tester password=secret"), + dict(user='tester', password='secret', dbname='test 2'), + "DSN with quoting parsed") + + self.assertEqual(parse_dsn('postgresql://tester:secret@/test'), + dict(user='tester', password='secret', dbname='test'), + "simple URI dsn 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(e.message.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") + class IsolationLevelsTestCase(ConnectingTestCase):