mirror of
https://github.com/psycopg/psycopg2.git
synced 2024-11-22 17:06:33 +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.
|
into Python tuples/namedtuples.
|
||||||
- More efficient iteration on named cursors, fetching 'itersize' records at
|
- More efficient iteration on named cursors, fetching 'itersize' records at
|
||||||
time from the backend.
|
time from the backend.
|
||||||
|
- 'cursor.description' is provided in named tuples if available.
|
||||||
- Connections and cursors are weakly referenceable.
|
- Connections and cursors are weakly referenceable.
|
||||||
- Added 'b' and 't' mode to large objects: write can deal with both bytes
|
- 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
|
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.
|
This read-only attribute is a sequence of 7-item sequences.
|
||||||
|
|
||||||
Each of these sequences contains information describing one result
|
Each of these sequences is a named tuple (a regular tuple if
|
||||||
column:
|
`!collections.namedtuple()` is not available) containing information
|
||||||
|
describing one result column:
|
||||||
|
|
||||||
0. `!name`: the name of the column returned.
|
0. `!name`: the name of the column returned.
|
||||||
1. `!type_code`: the PostgreSQL OID of the column. You can use the
|
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
|
always `!None` unless the :envvar:`PSYCOPG_DISPLAY_SIZE` parameter
|
||||||
is set at compile time. See also PQgetlength_.
|
is set at compile time. See also PQgetlength_.
|
||||||
3. `!internal_size`: the size in bytes of the column associated to
|
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_.
|
variable-size types See also PQfsize_.
|
||||||
4. `!precision`: total number of significant digits in columns of
|
4. `!precision`: total number of significant digits in columns of
|
||||||
type |NUMERIC|_. `!None` for other types.
|
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.
|
columns of type |NUMERIC|. `!None` for other types.
|
||||||
6. `!null_ok`: always `!None` as not easy to retrieve from the libpq.
|
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: http://www.postgresql.org/docs/9.0/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
|
||||||
.. |NUMERIC| replace:: :sql:`NUMERIC`
|
.. |NUMERIC| replace:: :sql:`NUMERIC`
|
||||||
|
|
||||||
|
.. versionchanged:: 2.4
|
||||||
|
if possible, columns descriptions are named tuple instead of
|
||||||
|
regular tuples.
|
||||||
|
|
||||||
.. method:: close()
|
.. method:: close()
|
||||||
|
|
||||||
Close the cursor now (rather than whenever `!__del__()` is
|
Close the cursor now (rather than whenever `!__del__()` is
|
||||||
|
|
|
@ -42,6 +42,9 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
extern HIDDEN PyObject *psyco_DescriptionType;
|
||||||
|
|
||||||
|
|
||||||
/* Strip off the severity from a Postgres error message. */
|
/* Strip off the severity from a Postgres error message. */
|
||||||
static const char *
|
static const char *
|
||||||
strip_severity(const char *msg)
|
strip_severity(const char *msg)
|
||||||
|
@ -948,7 +951,6 @@ _pq_fetch_tuples(cursorObject *curs)
|
||||||
Py_BLOCK_THREADS;
|
Py_BLOCK_THREADS;
|
||||||
|
|
||||||
dtitem = PyTuple_New(7);
|
dtitem = PyTuple_New(7);
|
||||||
PyTuple_SET_ITEM(curs->description, i, dtitem);
|
|
||||||
|
|
||||||
/* fill the right cast function by accessing three different dictionaries:
|
/* fill the right cast function by accessing three different dictionaries:
|
||||||
- the per-cursor dictionary, if available (can be NULL or None)
|
- the per-cursor dictionary, if available (can be NULL or None)
|
||||||
|
@ -1021,8 +1023,24 @@ _pq_fetch_tuples(cursorObject *curs)
|
||||||
/* 6/ FIXME: null_ok??? */
|
/* 6/ FIXME: null_ok??? */
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
PyTuple_SET_ITEM(dtitem, 6, 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) {
|
if (dsize) {
|
||||||
|
|
|
@ -69,6 +69,9 @@ HIDDEN int psycopg_debug_enabled = 0;
|
||||||
/* Python representation of SQL NULL */
|
/* Python representation of SQL NULL */
|
||||||
HIDDEN PyObject *psyco_null = NULL;
|
HIDDEN PyObject *psyco_null = NULL;
|
||||||
|
|
||||||
|
/* The type of the cursor.description items */
|
||||||
|
HIDDEN PyObject *psyco_DescriptionType = NULL;
|
||||||
|
|
||||||
/** connect module-level function **/
|
/** connect module-level function **/
|
||||||
#define psyco_connect_doc \
|
#define psyco_connect_doc \
|
||||||
"connect(dsn, ...) -- Create a new database connection.\n\n" \
|
"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 **/
|
/** method table and module initialization **/
|
||||||
|
|
||||||
static PyMethodDef psycopgMethods[] = {
|
static PyMethodDef psycopgMethods[] = {
|
||||||
|
@ -886,6 +927,7 @@ INIT_MODULE(_psycopg)(void)
|
||||||
psycoEncodings = PyDict_New();
|
psycoEncodings = PyDict_New();
|
||||||
psyco_encodings_fill(psycoEncodings);
|
psyco_encodings_fill(psycoEncodings);
|
||||||
psyco_null = Bytes_FromString("NULL");
|
psyco_null = Bytes_FromString("NULL");
|
||||||
|
psyco_DescriptionType = psyco_make_description_type();
|
||||||
|
|
||||||
/* set some module's parameters */
|
/* set some module's parameters */
|
||||||
PyModule_AddStringConstant(module, "__version__", PSYCOPG_VERSION);
|
PyModule_AddStringConstant(module, "__version__", PSYCOPG_VERSION);
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
from testutils import unittest
|
from testutils import unittest, skip_if_no_namedtuple
|
||||||
from testconfig import dsn
|
from testconfig import dsn
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,18 +112,6 @@ class ExtrasDictCursorTests(unittest.TestCase):
|
||||||
self.failUnless(row[0] == 'qux')
|
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):
|
class NamedTupleCursorTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from psycopg2.extras import NamedTupleConnection
|
from psycopg2.extras import NamedTupleConnection
|
||||||
|
@ -147,7 +135,7 @@ class NamedTupleCursorTest(unittest.TestCase):
|
||||||
if self.conn is not None:
|
if self.conn is not None:
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_fetchone(self):
|
def test_fetchone(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("select * from nttest where i = 1")
|
curs.execute("select * from nttest where i = 1")
|
||||||
|
@ -157,7 +145,7 @@ class NamedTupleCursorTest(unittest.TestCase):
|
||||||
self.assertEqual(t[1], 'foo')
|
self.assertEqual(t[1], 'foo')
|
||||||
self.assertEqual(t.s, 'foo')
|
self.assertEqual(t.s, 'foo')
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_fetchmany(self):
|
def test_fetchmany(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("select * from nttest order by 1")
|
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].i, 2)
|
||||||
self.assertEqual(res[1].s, 'bar')
|
self.assertEqual(res[1].s, 'bar')
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_fetchall(self):
|
def test_fetchall(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("select * from nttest order by 1")
|
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].i, 3)
|
||||||
self.assertEqual(res[2].s, 'baz')
|
self.assertEqual(res[2].s, 'baz')
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_iter(self):
|
def test_iter(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("select * from nttest order by 1")
|
curs.execute("select * from nttest order by 1")
|
||||||
|
@ -219,7 +207,7 @@ class NamedTupleCursorTest(unittest.TestCase):
|
||||||
# skip the test
|
# skip the test
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_record_updated(self):
|
def test_record_updated(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("select 1 as foo;")
|
curs.execute("select 1 as foo;")
|
||||||
|
@ -231,7 +219,7 @@ class NamedTupleCursorTest(unittest.TestCase):
|
||||||
self.assertEqual(r.bar, 2)
|
self.assertEqual(r.bar, 2)
|
||||||
self.assertRaises(AttributeError, getattr, r, 'foo')
|
self.assertRaises(AttributeError, getattr, r, 'foo')
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_no_result_no_surprise(self):
|
def test_no_result_no_surprise(self):
|
||||||
curs = self.conn.cursor()
|
curs = self.conn.cursor()
|
||||||
curs.execute("update nttest set s = s")
|
curs.execute("update nttest set s = s")
|
||||||
|
@ -240,7 +228,7 @@ class NamedTupleCursorTest(unittest.TestCase):
|
||||||
curs.execute("update nttest set s = s")
|
curs.execute("update nttest set s = s")
|
||||||
self.assertRaises(psycopg2.ProgrammingError, curs.fetchall)
|
self.assertRaises(psycopg2.ProgrammingError, curs.fetchall)
|
||||||
|
|
||||||
@if_has_namedtuple
|
@skip_if_no_namedtuple
|
||||||
def test_minimal_generation(self):
|
def test_minimal_generation(self):
|
||||||
# Instrument the class to verify it gets called the minimum number of times.
|
# Instrument the class to verify it gets called the minimum number of times.
|
||||||
from psycopg2.extras import NamedTupleCursor
|
from psycopg2.extras import NamedTupleCursor
|
||||||
|
|
|
@ -27,7 +27,7 @@ import psycopg2
|
||||||
import psycopg2.extensions
|
import psycopg2.extensions
|
||||||
from psycopg2.extensions import b
|
from psycopg2.extensions import b
|
||||||
from testconfig import dsn
|
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):
|
class CursorTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -172,6 +172,42 @@ class CursorTests(unittest.TestCase):
|
||||||
# everything swallowed in two gulps
|
# everything swallowed in two gulps
|
||||||
self.assertEqual(rv, [(i,((i - 1) % 30) + 1) for i in range(1,51)])
|
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():
|
def test_suite():
|
||||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||||
|
|
|
@ -127,6 +127,19 @@ def skip_if_tpc_disabled(f):
|
||||||
return skip_if_tpc_disabled_
|
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):
|
def skip_if_no_iobase(f):
|
||||||
"""Skip a test if io.TextIOBase is not available."""
|
"""Skip a test if io.TextIOBase is not available."""
|
||||||
def skip_if_no_iobase_(self):
|
def skip_if_no_iobase_(self):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user