Provide cursor.description as named tuple if possible

If namedtuple() is not available, use regular tuples.
This commit is contained in:
Daniele Varrazzo 2011-02-19 00:05:43 +00:00
parent e2cbc3411d
commit c620f18be1
7 changed files with 131 additions and 28 deletions

1
NEWS
View File

@ -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

View File

@ -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

View File

@ -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)
@ -1022,6 +1024,22 @@ _pq_fetch_tuples(cursorObject *curs)
Py_INCREF(Py_None);
PyTuple_SET_ITEM(dtitem, 6, Py_None);
/* 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;
}

View File

@ -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);

View File

@ -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

View File

@ -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__)

View File

@ -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):