diff options
author | Daniele Varrazzo <daniele.varrazzo@gmail.com> | 2018-10-11 04:39:14 +0100 |
---|---|---|
committer | Daniele Varrazzo <daniele.varrazzo@gmail.com> | 2018-10-11 04:39:14 +0100 |
commit | 7619c91d62793c4424603cb8bd7116fb8cdbef20 (patch) | |
tree | 96b08aa485ce8ce47a6b8d1f7adecc051bb0735b | |
parent | e00c4e2a7fdc98ea016ebfcf2c9012b04a75a5fa (diff) | |
parent | 6b3d3604bfdf7986f4bf4bfc6ba061f5cd4451bf (diff) | |
download | psycopg2-7619c91d62793c4424603cb8bd7116fb8cdbef20.tar.gz |
Merge branch 'description-extra-attrs'
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | doc/src/cursor.rst | 73 | ||||
-rw-r--r-- | doc/src/extensions.rst | 74 | ||||
-rw-r--r-- | psycopg/column.h | 48 | ||||
-rw-r--r-- | psycopg/column_type.c | 375 | ||||
-rw-r--r-- | psycopg/pqpath.c | 63 | ||||
-rw-r--r-- | psycopg/psycopgmodule.c | 67 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-rwxr-xr-x | tests/test_cursor.py | 23 |
9 files changed, 597 insertions, 133 deletions
@@ -7,6 +7,9 @@ What's new in psycopg 2.8 New features: - Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`). +- Added `~psycopg2.extensions.Column.table_oid` and + `~psycopg2.extensions.Column.table_column` attributes on `cursor.description` + items (:ticket:`#661`). - Added `connection.host` property (:ticket:`#726`). - `~psycopg2.sql.Identifier` can represent qualified names in SQL composition (:ticket:`#732`). diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst index 057ccbf..1d7098f 100644 --- a/doc/src/cursor.rst +++ b/doc/src/cursor.rst @@ -37,46 +37,49 @@ The ``cursor`` class .. attribute:: description - This read-only attribute is a sequence of 7-item sequences. - - Each of these sequences is a named tuple (a regular tuple if - :func:`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 - |pg_type|_ system table to get more informations about the type. - This is the value used by Psycopg to decide what Python type use - to represent the value. See also - :ref:`type-casting-from-sql-to-python`. - 2. `!display_size`: the actual length of the column in bytes. - Obtaining this value is computationally intensive, so it is - 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 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 fractional part in - columns of type |NUMERIC|. `!None` for other types. - 6. `!null_ok`: always `!None` as not easy to retrieve from the libpq. - - This attribute will be `!None` for operations that do not return rows - or if the cursor has not had an operation invoked via the - |execute*|_ methods yet. - - .. |pg_type| replace:: :sql:`pg_type` - .. _pg_type: https://www.postgresql.org/docs/current/static/catalog-pg-type.html - .. _PQgetlength: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQGETLENGTH - .. _PQfsize: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFSIZE - .. _NUMERIC: https://www.postgresql.org/docs/current/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL - .. |NUMERIC| replace:: :sql:`NUMERIC` + Read-only attribute describing the result of a query. It is a + sequence of `~psycopg2.extensions.Column` instances, each one + describing one result column in order. The attribute is `!None` for + operations that do not return rows or if the cursor has not had an + operation invoked via the |execute*|_ methods yet. + + For compatibility with the DB-API, every object can be unpacked as a + 7-items sequence: the attributes retuned this way are the following. + For further details and other attributes available check the + `~psycopg2.extensions.Column` documentation. + + 0. `~psycopg2.extensions.Column.name`: the name of the column returned. + + 1. `~psycopg2.extensions.Column.type_code`: the PostgreSQL OID of the + column. + + 2. `~psycopg2.extensions.Column.display_size`: the actual length of + the column in bytes. + + 3. `~psycopg2.extensions.Column.internal_size`: the size in bytes of + the column associated to this column on the server. + + 4. `~psycopg2.extensions.Column.precision`: total number of + significant digits in columns of type |NUMERIC|. `!None` + for other types. + + 5. `~psycopg2.extensions.Column.scale`: count of decimal digits in + the fractional part in columns of type |NUMERIC|. `!None` + for other types. + + 6. `~psycopg2.extensions.Column.null_ok`: always `!None` as not easy + to retrieve from the libpq. .. versionchanged:: 2.4 if possible, columns descriptions are named tuple instead of regular tuples. + .. versionchanged:: 2.8 + columns descriptions are instances of `!Column`, exposing extra + attributes. + + .. |NUMERIC| replace:: :sql:`NUMERIC` + .. method:: close() Close the cursor now (rather than whenever `del` is executed). diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 5bcc26f..159774b 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -154,6 +154,80 @@ introspection etc. Close the object and remove it from the database. +.. class:: Column + + Description of one result column, exposed as items of the + `cursor.description` sequence. + + .. versionadded:: 2.8 + + in previous version the `!description` attribute was a sequence of + simple tuples or namedtuples. + + .. attribute:: name + + The name of the column returned. + + .. attribute:: type_code + + The PostgreSQL OID of the column. You can use the |pg_type|_ system + table to get more informations about the type. This is the value used + by Psycopg to decide what Python type use to represent the value. See + also :ref:`type-casting-from-sql-to-python`. + + .. attribute:: display_size + + The actual length of the column in bytes. Obtaining this value is + computationally intensive, so it is always `!None` unless the + :envvar:`PSYCOPG_DISPLAY_SIZE` parameter is set at compile time. See + also PQgetlength_. + + .. attribute:: internal_size + + The size in bytes of the column associated to this column on the + server. Set to a negative value for variable-size types See also + PQfsize_. + + .. attribute:: precision + + Total number of significant digits in columns of type |NUMERIC|_. + `!None` for other types. + + .. attribute:: scale + + Count of decimal digits in the fractional part in columns of type + |NUMERIC|. `!None` for other types. + + .. attribute:: null_ok + + Always `!None` as not easy to retrieve from the libpq. + + .. attribute:: table_oid + + The oid of the table from which the column was fetched (matching + :sql:`pg_class.oid`). `!None` if the column is not a simple reference + to a table column. See also PQftable_. + + .. versionadded:: 2.8 + + .. attribute:: table_column + + The number of the column (within its table) making up the result + (matching :sql:`pg_attribute.attnum`, so it will start from 1). + `!None` if the column is not a simple reference to a table column. See + also PQftablecol_. + + .. versionadded:: 2.8 + + .. |pg_type| replace:: :sql:`pg_type` + .. _pg_type: https://www.postgresql.org/docs/current/static/catalog-pg-type.html + .. _PQgetlength: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQGETLENGTH + .. _PQfsize: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFSIZE + .. _PQftable: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFTABLE + .. _PQftablecol: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFTABLECOL + .. _NUMERIC: https://www.postgresql.org/docs/current/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL + .. |NUMERIC| replace:: :sql:`NUMERIC` + .. autoclass:: Notify(pid, channel, payload='') :members: pid, channel, payload diff --git a/psycopg/column.h b/psycopg/column.h new file mode 100644 index 0000000..59e9d9c --- /dev/null +++ b/psycopg/column.h @@ -0,0 +1,48 @@ +/* column.h - definition for a column in cursor.description type + * + * Copyright (C) 2018 Daniele Varrazzo <daniele.varrazzo@gmail.com> + * + * This file is part of psycopg. + * + * psycopg2 is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link this program with the OpenSSL library (or with + * modified versions of OpenSSL that use the same license as OpenSSL), + * and distribute linked combinations including the two. + * + * You must obey the GNU Lesser General Public License in all respects for + * all of the code used other than OpenSSL. + * + * psycopg2 is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + */ + +#ifndef PSYCOPG_COLUMN_H +#define PSYCOPG_COLUMN_H 1 + +extern HIDDEN PyTypeObject columnType; + +typedef struct { + PyObject_HEAD + + PyObject *name; + PyObject *type_code; + PyObject *display_size; + PyObject *internal_size; + PyObject *precision; + PyObject *scale; + PyObject *null_ok; + + /* Extensions to the DBAPI */ + PyObject *table_oid; + PyObject *table_column; + +} columnObject; + +#endif /* PSYCOPG_COLUMN_H */ diff --git a/psycopg/column_type.c b/psycopg/column_type.c new file mode 100644 index 0000000..8ee7d4c --- /dev/null +++ b/psycopg/column_type.c @@ -0,0 +1,375 @@ +/* column_type.c - python interface to cursor.description objects + * + * Copyright (C) 2018 Daniele Varrazzo <daniele.varrazzo@gmail.com> + * + * This file is part of psycopg. + * + * psycopg2 is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * In addition, as a special exception, the copyright holders give + * permission to link this program with the OpenSSL library (or with + * modified versions of OpenSSL that use the same license as OpenSSL), + * and distribute linked combinations including the two. + * + * You must obey the GNU Lesser General Public License in all respects for + * all of the code used other than OpenSSL. + * + * psycopg2 is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + */ + +#define PSYCOPG_MODULE +#include "psycopg/psycopg.h" + +#include "psycopg/column.h" + + +static const char column_doc[] = + "Description of a column returned by a query.\n\n" + "The DBAPI demands this object to be a 7-items sequence. This object\n" + "respects this interface, but adds names for the exposed attributes\n" + "and adds attribute not requested by the DBAPI."; + +static const char name_doc[] = + "The name of the column returned."; + +static const char type_code_doc[] = + "The PostgreSQL OID of the column.\n\n" + "You can use the pg_type system table to get more informations about the\n" + "type. This is the value used by Psycopg to decide what Python type use\n" + "to represent the value"; + +static const char display_size_doc[] = + "The actual length of the column in bytes.\n\n" + "Obtaining this value is computationally intensive, so it is always None\n" + "unless the PSYCOPG_DISPLAY_SIZE parameter is set at compile time."; + +static const char internal_size_doc[] = + "The size in bytes of the column associated to this column on the server.\n\n" + "Set to a negative value for variable-size types."; + +static const char precision_doc[] = + "Total number of significant digits in columns of type NUMERIC.\n\n" + "None for other types."; + +static const char scale_doc[] = + "Count of decimal digits in the fractional part in columns of type NUMERIC.\n\n" + "None for other types."; + +static const char null_ok_doc[] = + "Always none."; + +static const char table_oid_doc[] = + "The OID of the table from which the column was fetched.\n\n" + "None if not available"; + +static const char table_column_doc[] = + "The number (within its table) of the column making up the result\n\n" + "None if not available. Note that PostgreSQL column numbers start at 1"; + + +static PyMemberDef column_members[] = { + { "name", T_OBJECT, offsetof(columnObject, name), READONLY, (char *)name_doc }, + { "type_code", T_OBJECT, offsetof(columnObject, type_code), READONLY, (char *)type_code_doc }, + { "display_size", T_OBJECT, offsetof(columnObject, display_size), READONLY, (char *)display_size_doc }, + { "internal_size", T_OBJECT, offsetof(columnObject, internal_size), READONLY, (char *)internal_size_doc }, + { "precision", T_OBJECT, offsetof(columnObject, precision), READONLY, (char *)precision_doc }, + { "scale", T_OBJECT, offsetof(columnObject, scale), READONLY, (char *)scale_doc }, + { "null_ok", T_OBJECT, offsetof(columnObject, null_ok), READONLY, (char *)null_ok_doc }, + { "table_oid", T_OBJECT, offsetof(columnObject, table_oid), READONLY, (char *)table_oid_doc }, + { "table_column", T_OBJECT, offsetof(columnObject, table_column), READONLY, (char *)table_column_doc }, + { NULL } +}; + + +static PyObject * +column_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return type->tp_alloc(type, 0); +} + + +static int +column_init(columnObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = { + "name", "type_code", "display_size", "internal_size", + "precision", "scale", "null_ok", "table_oid", "table_column", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOOOOOOO", kwlist, + &self->name, &self->type_code, &self->display_size, + &self->internal_size, &self->precision, &self->scale, + &self->null_ok, &self->table_oid, &self->table_column)) { + return -1; + } + + return 0; +} + + +static void +column_dealloc(columnObject *self) +{ + Py_CLEAR(self->name); + Py_CLEAR(self->type_code); + Py_CLEAR(self->display_size); + Py_CLEAR(self->internal_size); + Py_CLEAR(self->precision); + Py_CLEAR(self->scale); + Py_CLEAR(self->null_ok); + Py_CLEAR(self->table_oid); + Py_CLEAR(self->table_column); + + Py_TYPE(self)->tp_free((PyObject *)self); +} + + +static PyObject* +column_repr(columnObject *self) +{ + PyObject *rv = NULL; + PyObject *format = NULL; + PyObject *args = NULL; + PyObject *tmp; + + if (!(format = Text_FromUTF8("Column(name=%r, type_code=%r)"))) { + goto exit; + } + + if (!(args = PyTuple_New(2))) { goto exit; } + + tmp = self->name ? self->name : Py_None; + Py_INCREF(tmp); + PyTuple_SET_ITEM(args, 0, tmp); + + tmp = self->type_code ? self->type_code : Py_None; + Py_INCREF(tmp); + PyTuple_SET_ITEM(args, 1, tmp); + + rv = Text_Format(format, args); + +exit: + Py_XDECREF(args); + Py_XDECREF(format); + + return rv; +} + + +static PyObject * +column_richcompare(columnObject *self, PyObject *other, int op) +{ + PyObject *rv = NULL; + PyObject *tself = NULL; + + if (!(tself = PyObject_CallFunctionObjArgs( + (PyObject *)&PyTuple_Type, (PyObject *)self, NULL))) { + goto exit; + } + + rv = PyObject_RichCompare(tself, other, op); + +exit: + Py_XDECREF(tself); + return rv; +} + + +/* column description can be accessed as a 7 items tuple for DBAPI compatibility */ + +static Py_ssize_t +column_len(columnObject *self) +{ + return 7; +} + + +static PyObject * +column_getitem(columnObject *self, Py_ssize_t item) +{ + PyObject *rv = NULL; + + if (item < 0) + item += 7; + + switch (item) { + case 0: + rv = self->name; + break; + case 1: + rv = self->type_code; + break; + case 2: + rv = self->display_size; + break; + case 3: + rv = self->internal_size; + break; + case 4: + rv = self->precision; + break; + case 5: + rv = self->scale; + break; + case 6: + rv = self->null_ok; + break; + default: + PyErr_SetString(PyExc_IndexError, "index out of range"); + return NULL; + } + + if (!rv) { + rv = Py_None; + } + + Py_INCREF(rv); + return rv; +} + + +static PySequenceMethods column_sequence = { + (lenfunc)column_len, /* sq_length */ + 0, /* sq_concat */ + 0, /* sq_repeat */ + (ssizeargfunc)column_getitem, /* sq_item */ + 0, /* sq_slice */ + 0, /* sq_ass_item */ + 0, /* sq_ass_slice */ + 0, /* sq_contains */ + 0, /* sq_inplace_concat */ + 0, /* sq_inplace_repeat */ +}; + + +static PyObject * +column_getstate(columnObject *self) +{ + return PyObject_CallFunctionObjArgs( + (PyObject *)&PyTuple_Type, (PyObject *)self, NULL); +} + + +PyObject * +column_setstate(columnObject *self, PyObject *state) +{ + Py_ssize_t size; + PyObject *rv = NULL; + + if (state == Py_None) { + goto exit; + } + if (!PyTuple_Check(state)) { + PyErr_SetString(PyExc_TypeError, "state is not a tuple"); + goto error; + } + + size = PyTuple_GET_SIZE(state); + + if (size > 0) { + Py_CLEAR(self->name); + self->name = PyTuple_GET_ITEM(state, 0); + Py_INCREF(self->name); + } + if (size > 1) { + Py_CLEAR(self->type_code); + self->type_code = PyTuple_GET_ITEM(state, 1); + Py_INCREF(self->type_code); + } + if (size > 2) { + Py_CLEAR(self->display_size); + self->display_size = PyTuple_GET_ITEM(state, 2); + Py_INCREF(self->display_size); + } + if (size > 3) { + Py_CLEAR(self->internal_size); + self->internal_size = PyTuple_GET_ITEM(state, 3); + Py_INCREF(self->internal_size); + } + if (size > 4) { + Py_CLEAR(self->precision); + self->precision = PyTuple_GET_ITEM(state, 4); + Py_INCREF(self->precision); + } + if (size > 5) { + Py_CLEAR(self->scale); + self->scale = PyTuple_GET_ITEM(state, 5); + Py_INCREF(self->scale); + } + if (size > 6) { + Py_CLEAR(self->null_ok); + self->null_ok = PyTuple_GET_ITEM(state, 6); + Py_INCREF(self->null_ok); + } + if (size > 7) { + Py_CLEAR(self->table_oid); + self->table_oid = PyTuple_GET_ITEM(state, 7); + Py_INCREF(self->table_oid); + } + if (size > 8) { + Py_CLEAR(self->table_column); + self->table_column = PyTuple_GET_ITEM(state, 8); + Py_INCREF(self->table_column); + } + +exit: + rv = Py_None; + Py_INCREF(rv); + +error: + return rv; +} + + +static PyMethodDef column_methods[] = { + /* Make Column picklable. */ + {"__getstate__", (PyCFunction)column_getstate, METH_NOARGS }, + {"__setstate__", (PyCFunction)column_setstate, METH_O }, + {NULL} +}; + + +PyTypeObject columnType = { + PyVarObject_HEAD_INIT(NULL, 0) + "psycopg2.extensions.Column", + sizeof(columnObject), 0, + (destructor)column_dealloc, /* tp_dealloc */ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + (reprfunc)column_repr, /*tp_repr*/ + 0, /*tp_as_number*/ + &column_sequence, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ + column_doc, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + (richcmpfunc)column_richcompare, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + column_methods, /*tp_methods*/ + column_members, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + (initproc)column_init, /*tp_init*/ + 0, /*tp_alloc*/ + column_new, /*tp_new*/ +}; diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 287262c..2893739 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -41,6 +41,7 @@ #include "psycopg/typecast.h" #include "psycopg/pgtypes.h" #include "psycopg/error.h" +#include "psycopg/column.h" #include "psycopg/libpq_support.h" #include "libpq-fe.h" @@ -1206,12 +1207,17 @@ _pq_fetch_tuples(cursorObject *curs) Oid ftype = PQftype(curs->pgres, i); int fsize = PQfsize(curs->pgres, i); int fmod = PQfmod(curs->pgres, i); + Oid ftable = PQftable(curs->pgres, i); + int ftablecol = PQftablecol(curs->pgres, i); - PyObject *dtitem = NULL; + columnObject *column = NULL; PyObject *type = NULL; PyObject *cast = NULL; - if (!(dtitem = PyTuple_New(7))) { goto exit; } + if (!(column = (columnObject *)PyObject_CallObject( + (PyObject *)&columnType, NULL))) { + goto exit; + } /* fill the right cast function by accessing three different dictionaries: - the per-cursor dictionary, if available (can be NULL or None) @@ -1248,20 +1254,16 @@ _pq_fetch_tuples(cursorObject *curs) curs->conn, PQfname(curs->pgres, i)))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 0, tmp); + column->name = tmp; } - PyTuple_SET_ITEM(dtitem, 1, type); + column->type_code = type; type = NULL; /* 2/ display size is the maximum size of this field result tuples. */ if (dsize && dsize[i] >= 0) { PyObject *tmp; if (!(tmp = PyInt_FromLong(dsize[i]))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 2, tmp); - } - else { - Py_INCREF(Py_None); - PyTuple_SET_ITEM(dtitem, 2, Py_None); + column->display_size = tmp; } /* 3/ size on the backend */ @@ -1270,18 +1272,18 @@ _pq_fetch_tuples(cursorObject *curs) if (ftype == NUMERICOID) { PyObject *tmp; if (!(tmp = PyInt_FromLong((fmod >> 16)))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 3, tmp); + column->internal_size = tmp; } else { /* If variable length record, return maximum size */ PyObject *tmp; if (!(tmp = PyInt_FromLong(fmod))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 3, tmp); + column->internal_size = tmp; } } else { PyObject *tmp; if (!(tmp = PyInt_FromLong(fsize))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 3, tmp); + column->internal_size = tmp; } /* 4,5/ scale and precision */ @@ -1291,40 +1293,35 @@ _pq_fetch_tuples(cursorObject *curs) if (!(tmp = PyInt_FromLong((fmod >> 16) & 0xFFFF))) { goto err_for; } - PyTuple_SET_ITEM(dtitem, 4, tmp); + column->precision = tmp; if (!(tmp = PyInt_FromLong(fmod & 0xFFFF))) { - PyTuple_SET_ITEM(dtitem, 5, tmp); + goto err_for; } - PyTuple_SET_ITEM(dtitem, 5, tmp); - } - else { - Py_INCREF(Py_None); - PyTuple_SET_ITEM(dtitem, 4, Py_None); - Py_INCREF(Py_None); - PyTuple_SET_ITEM(dtitem, 5, Py_None); + column->scale = tmp; } - /* 6/ FIXME: null_ok??? */ - Py_INCREF(Py_None); - PyTuple_SET_ITEM(dtitem, 6, Py_None); + /* table_oid, table_column */ + if (ftable != InvalidOid) { + PyObject *tmp; + if (!(tmp = PyInt_FromLong((long)ftable))) { goto err_for; } + column->table_oid = tmp; + } - /* Convert into a namedtuple if available */ - if (Py_None != psyco_DescriptionType) { - PyObject *tmp = dtitem; - dtitem = PyObject_CallObject(psyco_DescriptionType, tmp); - Py_DECREF(tmp); - if (NULL == dtitem) { goto err_for; } + if (ftablecol > 0) { + PyObject *tmp; + if (!(tmp = PyInt_FromLong((long)ftablecol))) { goto err_for; } + column->table_column = tmp; } - PyTuple_SET_ITEM(description, i, dtitem); - dtitem = NULL; + PyTuple_SET_ITEM(description, i, (PyObject *)column); + column = NULL; continue; err_for: Py_XDECREF(type); - Py_XDECREF(dtitem); + Py_XDECREF(column); goto exit; } diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 23e648d..ff04b25 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -32,6 +32,7 @@ #include "psycopg/replication_cursor.h" #include "psycopg/replication_message.h" #include "psycopg/green.h" +#include "psycopg/column.h" #include "psycopg/lobject.h" #include "psycopg/notify.h" #include "psycopg/xid.h" @@ -69,9 +70,6 @@ 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; - /* macro trick to stringify a macro expansion */ #define xstr(s) str(s) #define str(s) #s @@ -839,63 +837,6 @@ 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 *coll = NULL; - PyObject *nt = NULL; - PyTypeObject *t = NULL; - PyObject *s = NULL; - PyObject *rv = NULL; - - /* Try to import collections.namedtuple */ - if (!(coll = PyImport_ImportModule("collections"))) { - Dprintf("psyco_make_description_type: collections import failed"); - goto error; - } - if (!(nt = PyObject_GetAttrString(coll, "namedtuple"))) { - Dprintf("psyco_make_description_type: no collections.namedtuple"); - goto error; - } - - /* Build the namedtuple */ - if(!(t = (PyTypeObject *)PyObject_CallFunction(nt, "ss", "Column", - "name type_code display_size internal_size precision scale null_ok"))) { - goto exit; - } - - /* Export the tuple on the extensions module - * Required to guarantee picklability on Py > 3.3 (see Python issue 21374) - * for previous Py version the module is psycopg2 anyway but for consistency - * we'd rather expose it from the extensions module. */ - if (!(s = Text_FromUTF8("psycopg2.extensions"))) { goto exit; } - if (0 > PyDict_SetItemString(t->tp_dict, "__module__", s)) { goto exit; } - - rv = (PyObject *)t; - t = NULL; - -exit: - Py_XDECREF(coll); - Py_XDECREF(nt); - Py_XDECREF((PyObject *)t); - Py_XDECREF(s); - - return rv; - -error: - /* controlled error: we will fall back to regular tuples. Return None. */ - PyErr_Clear(); - rv = Py_None; - Py_INCREF(rv); - goto exit; -} - - /** method table and module initialization **/ static PyMethodDef psycopgMethods[] = { @@ -1041,6 +982,9 @@ INIT_MODULE(_psycopg)(void) Py_TYPE(&chunkType) = &PyType_Type; if (PyType_Ready(&chunkType) == -1) goto exit; + Py_TYPE(&columnType) = &PyType_Type; + if (PyType_Ready(&columnType) == -1) goto exit; + Py_TYPE(¬ifyType) = &PyType_Type; if (PyType_Ready(¬ifyType) == -1) goto exit; @@ -1119,7 +1063,6 @@ INIT_MODULE(_psycopg)(void) if (!(psycoEncodings = PyDict_New())) { goto exit; } if (0 != psyco_encodings_fill(psycoEncodings)) { goto exit; } psyco_null = Bytes_FromString("NULL"); - if (!(psyco_DescriptionType = psyco_make_description_type())) { goto exit; } /* set some module's parameters */ PyModule_AddStringConstant(module, "__version__", xstr(PSYCOPG_VERSION)); @@ -1138,6 +1081,7 @@ INIT_MODULE(_psycopg)(void) PyModule_AddObject(module, "ReplicationCursor", (PyObject*)&replicationCursorType); PyModule_AddObject(module, "ReplicationMessage", (PyObject*)&replicationMessageType); PyModule_AddObject(module, "ISQLQuote", (PyObject*)&isqlquoteType); + PyModule_AddObject(module, "Column", (PyObject*)&columnType); PyModule_AddObject(module, "Notify", (PyObject*)¬ifyType); PyModule_AddObject(module, "Xid", (PyObject*)&xidType); PyModule_AddObject(module, "Diagnostics", (PyObject*)&diagnosticsType); @@ -1150,7 +1094,6 @@ INIT_MODULE(_psycopg)(void) PyModule_AddObject(module, "List", (PyObject*)&listType); PyModule_AddObject(module, "QuotedString", (PyObject*)&qstringType); PyModule_AddObject(module, "lobject", (PyObject*)&lobjectType); - PyModule_AddObject(module, "Column", psyco_DescriptionType); /* encodings dictionary in module dictionary */ PyModule_AddObject(module, "encodings", psycoEncodings); @@ -486,7 +486,7 @@ sources = [ 'libpq_support.c', 'win32_support.c', 'solaris_support.c', 'connection_int.c', 'connection_type.c', - 'cursor_int.c', 'cursor_type.c', + 'cursor_int.c', 'cursor_type.c', 'column_type.c', 'replication_connection_type.c', 'replication_cursor_type.c', 'replication_message_type.c', @@ -508,7 +508,7 @@ depends = [ 'replication_connection.h', 'replication_cursor.h', 'replication_message.h', - 'notify.h', 'pqpath.h', 'xid.h', + 'notify.h', 'pqpath.h', 'xid.h', 'column.h', 'libpq_support.h', 'win32_support.h', 'adapter_asis.h', 'adapter_binary.h', 'adapter_datetime.h', diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 37110db..d048f3e 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -377,7 +377,7 @@ class CursorTests(ConnectingTestCase): for i, rec in enumerate(curs): self.assertEqual(i + 1, curs.rownumber) - def test_namedtuple_description(self): + def test_description_attribs(self): curs = self.conn.cursor() curs.execute("""select 3.14::decimal(10,2) as pi, @@ -412,6 +412,27 @@ class CursorTests(ConnectingTestCase): self.assertEqual(c.precision, None) self.assertEqual(c.scale, None) + def test_description_extra_attribs(self): + curs = self.conn.cursor() + curs.execute(""" + create table testcol ( + pi decimal(10,2), + hi text) + """) + curs.execute("select oid from pg_class where relname = %s", ('testcol',)) + oid = curs.fetchone()[0] + + curs.execute("insert into testcol values (3.14, 'hello')") + curs.execute("select hi, pi, 42 from testcol") + self.assertEqual(curs.description[0].table_oid, oid) + self.assertEqual(curs.description[0].table_column, 2) + + self.assertEqual(curs.description[1].table_oid, oid) + self.assertEqual(curs.description[1].table_column, 1) + + self.assertEqual(curs.description[2].table_oid, None) + self.assertEqual(curs.description[2].table_column, None) + def test_pickle_description(self): curs = self.conn.cursor() curs.execute('SELECT 1 AS foo') |