mirror of
https://github.com/psycopg/psycopg2.git
synced 2024-11-26 10:53:44 +03:00
Provide cursor.description as named tuple if possible
If namedtuple() is not available, use regular tuples.
This commit is contained in:
parent
e2cbc3411d
commit
c620f18be1
1
NEWS
1
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user