summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2011-02-19 00:05:43 +0000
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2011-02-19 00:05:43 +0000
commitc620f18be14b994337a3bb939974e1fb769823fd (patch)
treefd450abdf6103a04ead99f9a7512ab45af8eb14d
parente2cbc3411dbfa901422241f75cd5906aaa8181f4 (diff)
downloadpsycopg2-c620f18be14b994337a3bb939974e1fb769823fd.tar.gz
Provide cursor.description as named tuple if possible
If namedtuple() is not available, use regular tuples.
-rw-r--r--NEWS1
-rw-r--r--doc/src/cursor.rst13
-rw-r--r--psycopg/pqpath.c24
-rw-r--r--psycopg/psycopgmodule.c42
-rwxr-xr-xtests/extras_dictcursor.py28
-rwxr-xr-xtests/test_cursor.py38
-rw-r--r--tests/testutils.py13
7 files changed, 131 insertions, 28 deletions
diff --git a/NEWS b/NEWS
index abf4aab..133c3e6 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,7 @@ What's new in psycopg 2.4
into Python tuples/namedtuples.
- More efficient iteration on named cursors, fetching 'itersize' records at
time from the backend.
+ - 'cursor.description' is provided in named tuples if available.
- Connections and cursors are weakly referenceable.
- Added 'b' and 't' mode to large objects: write can deal with both bytes
strings and unicode; read can return either bytes strings or decoded
diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst
index 467ff79..0d1fdc6 100644
--- a/doc/src/cursor.rst
+++ b/doc/src/cursor.rst
@@ -39,8 +39,9 @@ The ``cursor`` class
This read-only attribute is a sequence of 7-item sequences.
- Each of these sequences contains information describing one result
- column:
+ Each of these sequences is a named tuple (a regular tuple if
+ `!collections.namedtuple()` is not available) containing information
+ describing one result column:
0. `!name`: the name of the column returned.
1. `!type_code`: the PostgreSQL OID of the column. You can use the
@@ -53,11 +54,11 @@ The ``cursor`` class
always `!None` unless the :envvar:`PSYCOPG_DISPLAY_SIZE` parameter
is set at compile time. See also PQgetlength_.
3. `!internal_size`: the size in bytes of the column associated to
- this column on the server. Set to a egative value for
+ this column on the server. Set to a negative value for
variable-size types See also PQfsize_.
4. `!precision`: total number of significant digits in columns of
type |NUMERIC|_. `!None` for other types.
- 5. `!scale`: count of decimal digits in the freactional part in
+ 5. `!scale`: count of decimal digits in the fractional part in
columns of type |NUMERIC|. `!None` for other types.
6. `!null_ok`: always `!None` as not easy to retrieve from the libpq.
@@ -72,6 +73,10 @@ The ``cursor`` class
.. _NUMERIC: http://www.postgresql.org/docs/9.0/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
.. |NUMERIC| replace:: :sql:`NUMERIC`
+ .. versionchanged:: 2.4
+ if possible, columns descriptions are named tuple instead of
+ regular tuples.
+
.. method:: close()
Close the cursor now (rather than whenever `!__del__()` is
diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c
index 7f9a101..f61549f 100644
--- a/psycopg/pqpath.c
+++ b/psycopg/pqpath.c
@@ -42,6 +42,9 @@
#include <string.h>
+extern HIDDEN PyObject *psyco_DescriptionType;
+
+
/* Strip off the severity from a Postgres error message. */
static const char *
strip_severity(const char *msg)
@@ -948,7 +951,6 @@ _pq_fetch_tuples(cursorObject *curs)
Py_BLOCK_THREADS;
dtitem = PyTuple_New(7);
- PyTuple_SET_ITEM(curs->description, i, dtitem);
/* fill the right cast function by accessing three different dictionaries:
- the per-cursor dictionary, if available (can be NULL or None)
@@ -1021,8 +1023,24 @@ _pq_fetch_tuples(cursorObject *curs)
/* 6/ FIXME: null_ok??? */
Py_INCREF(Py_None);
PyTuple_SET_ITEM(dtitem, 6, Py_None);
-
- Py_UNBLOCK_THREADS;
+
+ /* Convert into a namedtuple if available */
+ if (Py_None != psyco_DescriptionType) {
+ PyObject *tmp = dtitem;
+ if ((dtitem = PyObject_CallObject(psyco_DescriptionType, tmp))) {
+ Py_DECREF(tmp);
+ }
+ else {
+ /* FIXME: this function is painfully missing any error check.
+ * The caller doesn't expect them, so swallow it. */
+ PyErr_Clear();
+ dtitem = tmp;
+ }
+ }
+
+ PyTuple_SET_ITEM(curs->description, i, dtitem);
+
+ Py_UNBLOCK_THREADS;
}
if (dsize) {
diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c
index 59562e2..82a6a2d 100644
--- a/psycopg/psycopgmodule.c
+++ b/psycopg/psycopgmodule.c
@@ -69,6 +69,9 @@ HIDDEN int psycopg_debug_enabled = 0;
/* Python representation of SQL NULL */
HIDDEN PyObject *psyco_null = NULL;
+/* The type of the cursor.description items */
+HIDDEN PyObject *psyco_DescriptionType = NULL;
+
/** connect module-level function **/
#define psyco_connect_doc \
"connect(dsn, ...) -- Create a new database connection.\n\n" \
@@ -685,6 +688,44 @@ psyco_GetDecimalType(void)
}
+/* Create a namedtuple for cursor.description items
+ *
+ * Return None in case of expected errors (e.g. namedtuples not available)
+ * NULL in case of errors to propagate.
+ */
+static PyObject *
+psyco_make_description_type(void)
+{
+ PyObject *nt = NULL;
+ PyObject *coll = NULL;
+ PyObject *rv = NULL;
+
+ /* Try to import collections.namedtuple */
+ if (!(coll = PyImport_ImportModule("collections"))) {
+ Dprintf("psyco_make_description_type: collections import failed");
+ PyErr_Clear();
+ rv = Py_None;
+ goto exit;
+ }
+ if (!(nt = PyObject_GetAttrString(coll, "namedtuple"))) {
+ Dprintf("psyco_make_description_type: no collections.namedtuple");
+ PyErr_Clear();
+ rv = Py_None;
+ goto exit;
+ }
+
+ /* Build the namedtuple */
+ rv = PyObject_CallFunction(nt, "ss", "Column",
+ "name type_code display_size internal_size precision scale null_ok");
+
+exit:
+ Py_XDECREF(coll);
+ Py_XDECREF(nt);
+
+ return rv;
+}
+
+
/** method table and module initialization **/
static PyMethodDef psycopgMethods[] = {
@@ -886,6 +927,7 @@ INIT_MODULE(_psycopg)(void)
psycoEncodings = PyDict_New();
psyco_encodings_fill(psycoEncodings);
psyco_null = Bytes_FromString("NULL");
+ psyco_DescriptionType = psyco_make_description_type();
/* set some module's parameters */
PyModule_AddStringConstant(module, "__version__", PSYCOPG_VERSION);
diff --git a/tests/extras_dictcursor.py b/tests/extras_dictcursor.py
index 1bb44ad..70f51d2 100755
--- a/tests/extras_dictcursor.py
+++ b/tests/extras_dictcursor.py
@@ -16,7 +16,7 @@
import psycopg2
import psycopg2.extras
-from testutils import unittest
+from testutils import unittest, skip_if_no_namedtuple
from testconfig import dsn
@@ -112,18 +112,6 @@ class ExtrasDictCursorTests(unittest.TestCase):
self.failUnless(row[0] == 'qux')
-def if_has_namedtuple(f):
- def if_has_namedtuple_(self):
- try:
- from collections import namedtuple
- except ImportError:
- return self.skipTest("collections.namedtuple not available")
- else:
- return f(self)
-
- if_has_namedtuple_.__name__ = f.__name__
- return if_has_namedtuple_
-
class NamedTupleCursorTest(unittest.TestCase):
def setUp(self):
from psycopg2.extras import NamedTupleConnection
@@ -147,7 +135,7 @@ class NamedTupleCursorTest(unittest.TestCase):
if self.conn is not None:
self.conn.close()
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_fetchone(self):
curs = self.conn.cursor()
curs.execute("select * from nttest where i = 1")
@@ -157,7 +145,7 @@ class NamedTupleCursorTest(unittest.TestCase):
self.assertEqual(t[1], 'foo')
self.assertEqual(t.s, 'foo')
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_fetchmany(self):
curs = self.conn.cursor()
curs.execute("select * from nttest order by 1")
@@ -168,7 +156,7 @@ class NamedTupleCursorTest(unittest.TestCase):
self.assertEqual(res[1].i, 2)
self.assertEqual(res[1].s, 'bar')
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_fetchall(self):
curs = self.conn.cursor()
curs.execute("select * from nttest order by 1")
@@ -181,7 +169,7 @@ class NamedTupleCursorTest(unittest.TestCase):
self.assertEqual(res[2].i, 3)
self.assertEqual(res[2].s, 'baz')
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_iter(self):
curs = self.conn.cursor()
curs.execute("select * from nttest order by 1")
@@ -219,7 +207,7 @@ class NamedTupleCursorTest(unittest.TestCase):
# skip the test
pass
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_record_updated(self):
curs = self.conn.cursor()
curs.execute("select 1 as foo;")
@@ -231,7 +219,7 @@ class NamedTupleCursorTest(unittest.TestCase):
self.assertEqual(r.bar, 2)
self.assertRaises(AttributeError, getattr, r, 'foo')
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_no_result_no_surprise(self):
curs = self.conn.cursor()
curs.execute("update nttest set s = s")
@@ -240,7 +228,7 @@ class NamedTupleCursorTest(unittest.TestCase):
curs.execute("update nttest set s = s")
self.assertRaises(psycopg2.ProgrammingError, curs.fetchall)
- @if_has_namedtuple
+ @skip_if_no_namedtuple
def test_minimal_generation(self):
# Instrument the class to verify it gets called the minimum number of times.
from psycopg2.extras import NamedTupleCursor
diff --git a/tests/test_cursor.py b/tests/test_cursor.py
index b8a8b66..74f86a4 100755
--- a/tests/test_cursor.py
+++ b/tests/test_cursor.py
@@ -27,7 +27,7 @@ import psycopg2
import psycopg2.extensions
from psycopg2.extensions import b
from testconfig import dsn
-from testutils import unittest, skip_before_postgres
+from testutils import unittest, skip_before_postgres, skip_if_no_namedtuple
class CursorTests(unittest.TestCase):
@@ -172,6 +172,42 @@ class CursorTests(unittest.TestCase):
# everything swallowed in two gulps
self.assertEqual(rv, [(i,((i - 1) % 30) + 1) for i in range(1,51)])
+ @skip_if_no_namedtuple
+ def test_namedtuple_description(self):
+ curs = self.conn.cursor()
+ curs.execute("""select
+ 3.14::decimal(10,2) as pi,
+ 'hello'::text as hi,
+ '2010-02-18'::date as now;
+ """)
+ self.assertEqual(len(curs.description), 3)
+ for c in curs.description:
+ self.assertEqual(len(c), 7) # DBAPI happy
+ for a in ('name', 'type_code', 'display_size', 'internal_size',
+ 'precision', 'scale', 'null_ok'):
+ self.assert_(hasattr(c, a), a)
+
+ c = curs.description[0]
+ self.assertEqual(c.name, 'pi')
+ self.assert_(c.type_code in psycopg2.extensions.DECIMAL.values)
+ self.assert_(c.internal_size > 0)
+ self.assertEqual(c.precision, 10)
+ self.assertEqual(c.scale, 2)
+
+ c = curs.description[1]
+ self.assertEqual(c.name, 'hi')
+ self.assert_(c.type_code in psycopg2.STRING.values)
+ self.assert_(c.internal_size < 0)
+ self.assertEqual(c.precision, None)
+ self.assertEqual(c.scale, None)
+
+ c = curs.description[2]
+ self.assertEqual(c.name, 'now')
+ self.assert_(c.type_code in psycopg2.extensions.DATE.values)
+ self.assert_(c.internal_size > 0)
+ self.assertEqual(c.precision, None)
+ self.assertEqual(c.scale, None)
+
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
diff --git a/tests/testutils.py b/tests/testutils.py
index 8e99f04..26551d4 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -127,6 +127,19 @@ def skip_if_tpc_disabled(f):
return skip_if_tpc_disabled_
+def skip_if_no_namedtuple(f):
+ def skip_if_no_namedtuple_(self):
+ try:
+ from collections import namedtuple
+ except ImportError:
+ return self.skipTest("collections.namedtuple not available")
+ else:
+ return f(self)
+
+ skip_if_no_namedtuple_.__name__ = f.__name__
+ return skip_if_no_namedtuple_
+
+
def skip_if_no_iobase(f):
"""Skip a test if io.TextIOBase is not available."""
def skip_if_no_iobase_(self):