diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index c810ae81..6df90fd2 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -165,9 +165,9 @@ functionalities defined by the |DBAPI|_. table_name A string with the error field if available; `!None` if not available. - The attribute value is available only if the error sent by the server - includes the specified field and should remain available until the - cursor that generated the exception executes another query. + The attribute value is available only if the error sent by the server: + not all the fields are available for all the errors and for all the + server versions. .. autofunction:: set_wait_callback(f) diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index 9570f914..a6b8c80b 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -118,7 +118,7 @@ _mogrify(PyObject *var, PyObject *fmt, cursorObject *curs, PyObject **new) if (kind == 2) { Py_XDECREF(n); psyco_set_error(ProgrammingError, curs, - "argument formats can't be mixed", NULL, NULL); + "argument formats can't be mixed"); return -1; } kind = 1; @@ -190,7 +190,7 @@ _mogrify(PyObject *var, PyObject *fmt, cursorObject *curs, PyObject **new) /* we found %( but not a ) */ Py_XDECREF(n); psyco_set_error(ProgrammingError, curs, - "incomplete placeholder: '%(' without ')'", NULL, NULL); + "incomplete placeholder: '%(' without ')'"); return -1; } c = d + 1; /* after the ) */ @@ -205,7 +205,7 @@ _mogrify(PyObject *var, PyObject *fmt, cursorObject *curs, PyObject **new) if (kind == 1) { Py_XDECREF(n); psyco_set_error(ProgrammingError, curs, - "argument formats can't be mixed", NULL, NULL); + "argument formats can't be mixed"); return -1; } kind = 2; @@ -267,7 +267,7 @@ static PyObject *_psyco_curs_validate_sql_basic( if (!sql || !PyObject_IsTrue(sql)) { psyco_set_error(ProgrammingError, self, - "can't execute an empty query", NULL, NULL); + "can't execute an empty query"); goto fail; } @@ -338,8 +338,7 @@ _psyco_curs_merge_query_args(cursorObject *self, if (!strcmp(s, "not enough arguments for format string") || !strcmp(s, "not all arguments converted")) { Dprintf("psyco_curs_execute: -> got a match"); - psyco_set_error(ProgrammingError, self, - s, NULL, NULL); + psyco_set_error(ProgrammingError, self, s); pe = 1; } @@ -482,13 +481,12 @@ psyco_curs_execute(cursorObject *self, PyObject *args, PyObject *kwargs) if (self->name != NULL) { if (self->query != Py_None) { psyco_set_error(ProgrammingError, self, - "can't call .execute() on named cursors more than once", - NULL, NULL); + "can't call .execute() on named cursors more than once"); return NULL; } if (self->conn->autocommit) { psyco_set_error(ProgrammingError, self, - "can't use a named cursor outside of transactions", NULL, NULL); + "can't use a named cursor outside of transactions"); return NULL; } EXC_IF_NO_MARK(self); @@ -533,7 +531,7 @@ psyco_curs_executemany(cursorObject *self, PyObject *args, PyObject *kwargs) if (self->name != NULL) { psyco_set_error(ProgrammingError, self, - "can't call .executemany() on named cursors", NULL, NULL); + "can't call .executemany() on named cursors"); return NULL; } @@ -1038,7 +1036,7 @@ psyco_curs_callproc(cursorObject *self, PyObject *args) if (self->name != NULL) { psyco_set_error(ProgrammingError, self, - "can't call .callproc() on named cursors", NULL, NULL); + "can't call .callproc() on named cursors"); goto exit; } @@ -1165,13 +1163,13 @@ psyco_curs_scroll(cursorObject *self, PyObject *args, PyObject *kwargs) newpos = value; } else { psyco_set_error(ProgrammingError, self, - "scroll mode must be 'relative' or 'absolute'", NULL, NULL); + "scroll mode must be 'relative' or 'absolute'"); return NULL; } if (newpos < 0 || newpos >= self->rowcount ) { psyco_set_error(ProgrammingError, self, - "scroll destination out of bounds", NULL, NULL); + "scroll destination out of bounds"); return NULL; } diff --git a/psycopg/diagnostics.h b/psycopg/diagnostics.h index adaf7580..9187c500 100644 --- a/psycopg/diagnostics.h +++ b/psycopg/diagnostics.h @@ -26,12 +26,14 @@ #ifndef PSYCOPG_DIAGNOSTICS_H #define PSYCOPG_DIAGNOSTICS_H 1 +#include "psycopg/error.h" + extern HIDDEN PyTypeObject diagnosticsType; typedef struct { PyObject_HEAD - PyObject *err; /* exception to retrieve the diagnostics from */ + errorObject *err; /* exception to retrieve the diagnostics from */ } diagnosticsObject; diff --git a/psycopg/diagnostics_type.c b/psycopg/diagnostics_type.c index 30ef2d46..5e302f07 100644 --- a/psycopg/diagnostics_type.c +++ b/psycopg/diagnostics_type.c @@ -27,7 +27,7 @@ #include "psycopg/psycopg.h" #include "psycopg/diagnostics.h" -#include "psycopg/cursor.h" +#include "psycopg/error.h" /* These are new in PostgreSQL 9.3. Defining them here so that psycopg2 can * use them with a 9.3+ server even if compiled against pre-9.3 headers. */ @@ -55,27 +55,15 @@ static PyObject * psyco_diagnostics_get_field(diagnosticsObject *self, void *closure) { - // closure contains the field code. - PyObject *rv = NULL; - PyObject *curs = NULL; - const char* errortext; - if (!(curs = PyObject_GetAttrString(self->err, "cursor")) || - !PyObject_TypeCheck(curs, &cursorType) || - ((cursorObject *)curs)->pgres == NULL) { - goto exit; + const char *errortext; + + if (!self->err->pgres) { + Py_INCREF(Py_None); + return Py_None; } - errortext = PQresultErrorField( - ((cursorObject *)curs)->pgres, (Py_intptr_t) closure); - if (errortext) { - rv = conn_text_from_chars(((cursorObject *)curs)->conn, errortext); - } -exit: - if (!rv) { - rv = Py_None; - Py_INCREF(rv); - } - Py_XDECREF(curs); - return rv; + + errortext = PQresultErrorField(self->err->pgres, (Py_intptr_t) closure); + return error_text_from_chars(self->err, errortext); } @@ -134,8 +122,14 @@ diagnostics_init(diagnosticsObject *self, PyObject *args, PyObject *kwds) if (!PyArg_ParseTuple(args, "O", &err)) return -1; + if (!PyObject_TypeCheck(err, &errorType)) { + PyErr_SetString(PyExc_TypeError, + "The argument must be a psycopg2.Error"); + return -1; + } + Py_INCREF(err); - self->err = err; + self->err = (errorObject *)err; return 0; } diff --git a/psycopg/error.h b/psycopg/error.h new file mode 100644 index 00000000..9ae6dbd3 --- /dev/null +++ b/psycopg/error.h @@ -0,0 +1,43 @@ +/* error.h - definition for the psycopg base Error type + * + * Copyright (C) 2013 Daniele Varrazzo + * + * 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_ERROR_H +#define PSYCOPG_ERROR_H 1 + +extern HIDDEN PyTypeObject errorType; + +typedef struct { + PyBaseExceptionObject exc; + + PyObject *pgerror; + PyObject *pgcode; + cursorObject *cursor; + char *codec; + PGresult *pgres; +} errorObject; + +HIDDEN PyObject *error_text_from_chars(errorObject *self, const char *str); + +#endif /* PSYCOPG_ERROR_H */ diff --git a/psycopg/error_type.c b/psycopg/error_type.c new file mode 100644 index 00000000..cb632e0d --- /dev/null +++ b/psycopg/error_type.c @@ -0,0 +1,291 @@ +/* error_type.c - python interface to the Error objects + * + * Copyright (C) 2013 Daniele Varrazzo + * + * 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/error.h" +#include "psycopg/diagnostics.h" +#include "psycopg/pqpath.h" + + +PyObject * +error_text_from_chars(errorObject *self, const char *str) +{ + if (str == NULL) { + Py_INCREF(Py_None); + return (Py_None); + } + +#if PY_MAJOR_VERSION < 3 + return PyString_FromString(str); +#else + return PyUnicode_Decode(str, strlen(str), + self->codec ? self->codec : "ascii", "replace"); +#endif +} + + +static const char pgerror_doc[] = + "The error message returned by the backend, if available, else None"; + +static const char pgcode_doc[] = + "The error code returned by the backend, if available, else None"; + +static const char cursor_doc[] = + "The cursor that raised the exception, if available, else None"; + +static const char diag_doc[] = + "A Diagnostics object to get further information about the error"; + +static PyMemberDef error_members[] = { + { "pgerror", T_OBJECT, offsetof(errorObject, pgerror), + READONLY, (char *)pgerror_doc }, + { "pgcode", T_OBJECT, offsetof(errorObject, pgcode), + READONLY, (char *)pgcode_doc }, + { "cursor", T_OBJECT, offsetof(errorObject, cursor), + READONLY, (char *)cursor_doc }, + { NULL } +}; + +static PyObject * +error_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return ((PyTypeObject *)PyExc_StandardError)->tp_new( + type, args, kwargs); +} + +static int +error_init(errorObject *self, PyObject *args, PyObject *kwargs) +{ + if (((PyTypeObject *)PyExc_StandardError)->tp_init( + (PyObject *)self, args, kwargs) < 0) { + return -1; + } + return 0; +} + +static int +error_traverse(errorObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->cursor); + return ((PyTypeObject *)PyExc_StandardError)->tp_traverse( + (PyObject *)self, visit, arg); +} + +static int +error_clear(errorObject *self) +{ + Py_CLEAR(self->pgerror); + Py_CLEAR(self->pgcode); + Py_CLEAR(self->cursor); + PyMem_Free(self->codec); + IFCLEARPGRES(self->pgres); + return ((PyTypeObject *)PyExc_StandardError)->tp_clear((PyObject *)self); +} + +static void +error_dealloc(errorObject *self) +{ + error_clear(self); + return Py_TYPE(self)->tp_free((PyObject *)self); +} + + +static PyObject * +error_get_diag(errorObject *self, void *closure) +{ + return PyObject_CallFunctionObjArgs( + (PyObject *)&diagnosticsType, (PyObject *)self, NULL); +} + +static struct PyGetSetDef error_getsets[] = { + { "diag", (getter)error_get_diag, NULL, (char *)diag_doc }, + { NULL } +}; + + +#if PY_VERSION_HEX >= 0x02050000 + +/* Error.__reduce__ + * + * The method is required to make exceptions picklable: set the cursor + * attribute to None. Only working from Py 2.5: previous versions + * would require implementing __getstate__, and as of 2012 it's a little + * bit too late to care. */ +static PyObject * +psyco_error_reduce(errorObject *self) +{ + PyObject *meth = NULL; + PyObject *tuple = NULL; + PyObject *dict = NULL; + PyObject *rv = NULL; + + if (!(meth = PyObject_GetAttrString(PyExc_StandardError, "__reduce__"))) { + goto error; + } + if (!(tuple = PyObject_CallFunctionObjArgs(meth, self, NULL))) { + goto error; + } + + /* tuple is (type, args): convert it to (type, args, dict) + * with our extra items in the dict. + * + * If these checks fail, we can still return a valid object. Pickle + * will likely fail downstream, but there's nothing else we can do here */ + if (!PyTuple_Check(tuple)) { goto exit; } + if (2 != PyTuple_GET_SIZE(tuple)) { goto exit; } + + if (!(dict = PyDict_New())) { goto error; } + if (0 != PyDict_SetItemString(dict, "pgerror", self->pgerror)) { goto error; } + if (0 != PyDict_SetItemString(dict, "pgcode", self->pgcode)) { goto error; } + + { + PyObject *newtuple; + if (!(newtuple = PyTuple_Pack(3, + PyTuple_GET_ITEM(tuple, 0), + PyTuple_GET_ITEM(tuple, 1), + dict))) { + goto error; + } + Py_DECREF(tuple); + tuple = newtuple; + } + +exit: + rv = tuple; + tuple = NULL; + +error: + Py_XDECREF(dict); + Py_XDECREF(tuple); + Py_XDECREF(meth); + + return rv; +} + +PyObject * +psyco_error_setstate(errorObject *self, PyObject *state) +{ + PyObject *rv = NULL; + + /* we don't call the StandartError's setstate as it would try to load the + * dict content as attributes */ + + if (state == Py_None) { + goto exit; + } + if (!PyDict_Check(state)) { + PyErr_SetString(PyExc_TypeError, "state is not a dictionary"); + goto error; + } + + /* load the dict content in the structure */ + Py_CLEAR(self->pgerror); + self->pgerror = PyDict_GetItemString(state, "pgerror"); + Py_XINCREF(self->pgerror); + + Py_CLEAR(self->pgcode); + self->pgcode = PyDict_GetItemString(state, "pgcode"); + Py_XINCREF(self->pgcode); + + Py_CLEAR(self->cursor); + /* We never expect a cursor in the state as it's not picklable. + * at most there could be a None here, coming from Psycopg < 2.5 */ + +exit: + rv = Py_None; + Py_INCREF(rv); + +error: + return rv; +} + +#endif /* PY_VERSION_HEX >= 0x02050000 */ + +static PyMethodDef error_methods[] = { +#if PY_VERSION_HEX >= 0x02050000 + /* Make Error and all its subclasses picklable. + * + * TODO: this comment applied to the __reduce_ex__ implementation: now + * pickling may work on Py 2.4 too... but it's 2013 now. + * + * Don't do it it on Py 2.4: [__reduce_ex__] it is not used by the pickle + * protocol, and if called manually fails in an unsettling way, + * probably because the exceptions were old-style classes. */ + {"__reduce__", (PyCFunction)psyco_error_reduce, METH_NOARGS }, + {"__setstate__", (PyCFunction)psyco_error_setstate, METH_O }, +#endif + {NULL} +}; + + +PyTypeObject errorType = { + PyVarObject_HEAD_INIT(NULL, 0) + "psycopg2.Error", + sizeof(errorObject), + 0, + (destructor)error_dealloc, /* tp_dealloc */ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*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|Py_TPFLAGS_HAVE_GC, /*tp_flags*/ + Error_doc, /*tp_doc*/ + (traverseproc)error_traverse, /*tp_traverse*/ + (inquiry)error_clear, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + error_methods, /*tp_methods*/ + error_members, /*tp_members*/ + error_getsets, /*tp_getset*/ + 0, /*tp_base Will be set to StandardError in module init */ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + (initproc)error_init, /*tp_init*/ + 0, /*tp_alloc will be set to PyType_GenericAlloc in module init*/ + error_new, /*tp_new*/ + 0, /*tp_free Low-level free-memory routine */ + 0, /*tp_is_gc For PyObject_IS_GC */ + 0, /*tp_bases*/ + 0, /*tp_mro method resolution order */ + 0, /*tp_cache*/ + 0, /*tp_subclasses*/ + 0 /*tp_weaklist*/ +}; diff --git a/psycopg/lobject.h b/psycopg/lobject.h index 6587198c..56f9ead4 100644 --- a/psycopg/lobject.h +++ b/psycopg/lobject.h @@ -78,13 +78,13 @@ RAISES_NEG HIDDEN int lobject_close(lobjectObject *self); #define EXC_IF_LOBJ_LEVEL0(self) \ if (self->conn->autocommit) { \ psyco_set_error(ProgrammingError, NULL, \ - "can't use a lobject outside of transactions", NULL, NULL); \ + "can't use a lobject outside of transactions"); \ return NULL; \ } #define EXC_IF_LOBJ_UNMARKED(self) \ if (self->conn->mark != self->mark) { \ psyco_set_error(ProgrammingError, NULL, \ - "lobject isn't valid anymore", NULL, NULL); \ + "lobject isn't valid anymore"); \ return NULL; \ } diff --git a/psycopg/lobject_type.c b/psycopg/lobject_type.c index 625a2939..fb477522 100644 --- a/psycopg/lobject_type.c +++ b/psycopg/lobject_type.c @@ -333,7 +333,7 @@ lobject_setup(lobjectObject *self, connectionObject *conn, if (conn->autocommit) { psyco_set_error(ProgrammingError, NULL, - "can't use a lobject outside of transactions", NULL, NULL); + "can't use a lobject outside of transactions"); return -1; } diff --git a/psycopg/microprotocols.c b/psycopg/microprotocols.c index 23f12794..1687bc26 100644 --- a/psycopg/microprotocols.c +++ b/psycopg/microprotocols.c @@ -203,7 +203,7 @@ microprotocols_adapt(PyObject *obj, PyObject *proto, PyObject *alt) /* else set the right exception and return NULL */ PyOS_snprintf(buffer, 255, "can't adapt type '%s'", Py_TYPE(obj)->tp_name); - psyco_set_error(ProgrammingError, NULL, buffer, NULL, NULL); + psyco_set_error(ProgrammingError, NULL, buffer); return NULL; } diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 62b8c157..13f64af1 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -38,6 +38,7 @@ #include "psycopg/green.h" #include "psycopg/typecast.h" #include "psycopg/pgtypes.h" +#include "psycopg/error.h" #include @@ -149,15 +150,20 @@ exception_from_sqlstate(const char *sqlstate) /* pq_raise - raise a python exception of the right kind - This function should be called while holding the GIL. */ + This function should be called while holding the GIL. + + The function passes the ownership of the pgres to the returned exception, + wherer the pgres was the explicit argument or taken from the cursor. + So, after calling it curs->pgres will be set to null */ RAISES static void -pq_raise(connectionObject *conn, cursorObject *curs, PGresult *pgres) +pq_raise(connectionObject *conn, cursorObject *curs, PGresult **pgres) { PyObject *exc = NULL; const char *err = NULL; const char *err2 = NULL; const char *code = NULL; + PyObject *pyerr = NULL; if (conn == NULL) { PyErr_SetString(DatabaseError, @@ -171,13 +177,13 @@ pq_raise(connectionObject *conn, cursorObject *curs, PGresult *pgres) conn->closed = 2; if (pgres == NULL && curs != NULL) - pgres = curs->pgres; + pgres = &curs->pgres; - if (pgres) { - err = PQresultErrorMessage(pgres); + if (pgres && *pgres) { + err = PQresultErrorMessage(*pgres); if (err != NULL) { Dprintf("pq_raise: PQresultErrorMessage: err=%s", err); - code = PQresultErrorField(pgres, PG_DIAG_SQLSTATE); + code = PQresultErrorField(*pgres, PG_DIAG_SQLSTATE); } } if (err == NULL) { @@ -210,7 +216,26 @@ pq_raise(connectionObject *conn, cursorObject *curs, PGresult *pgres) err2 = strip_severity(err); Dprintf("pq_raise: err2=%s", err2); - psyco_set_error(exc, curs, err2, err, code); + pyerr = psyco_set_error(exc, curs, err2); + + if (pyerr && PyObject_TypeCheck(pyerr, &errorType)) { + errorObject *perr = (errorObject *)pyerr; + + PyMem_Free(perr->codec); + psycopg_strdup(&perr->codec, conn->codec, 0); + + Py_CLEAR(perr->pgerror); + perr->pgerror = error_text_from_chars(perr, err); + + Py_CLEAR(perr->pgcode); + perr->pgcode = error_text_from_chars(perr, code); + + IFCLEARPGRES(perr->pgres); + if (pgres && *pgres) { + perr->pgres = *pgres; + *pgres = NULL; + } + } } /* pq_set_critical, pq_resolve_critical - manage critical errors @@ -388,14 +413,16 @@ pq_complete_error(connectionObject *conn, PGresult **pgres, char **error) { Dprintf("pq_complete_error: pgconn = %p, pgres = %p, error = %s", conn->pgconn, *pgres, *error ? *error : "(null)"); - if (*pgres != NULL) - pq_raise(conn, NULL, *pgres); + if (*pgres != NULL) { + pq_raise(conn, NULL, pgres); + /* now *pgres is null */ + } else if (*error != NULL) { PyErr_SetString(OperationalError, *error); } else { PyErr_SetString(OperationalError, "unknown error"); } - IFCLEARPGRES(*pgres); + if (*error) { free(*error); *error = NULL; @@ -1509,8 +1536,6 @@ pq_fetch(cursorObject *curs, int no_result) default: Dprintf("pq_fetch: uh-oh, something FAILED: pgconn = %p", curs->conn); pq_raise(curs->conn, curs, NULL); - /* don't clear curs->pgres, because it contains detailed error - information */ ex = -1; break; } diff --git a/psycopg/psycopg.h b/psycopg/psycopg.h index 2e86dca8..cc8281ed 100644 --- a/psycopg/psycopg.h +++ b/psycopg/psycopg.h @@ -120,8 +120,7 @@ HIDDEN PyObject *psyco_GetDecimalType(void); typedef struct cursorObject cursorObject; /* some utility functions */ -RAISES HIDDEN void psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg, - const char *pgerror, const char *pgcode); +RAISES HIDDEN PyObject *psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg); HIDDEN char *psycopg_escape_string(PyObject *conn, const char *from, Py_ssize_t len, char *to, Py_ssize_t *tolen); diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 34985254..8a7539b2 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -35,6 +35,7 @@ #include "psycopg/typecast.h" #include "psycopg/microprotocols.h" #include "psycopg/microprotocols_proto.h" +#include "psycopg/error.h" #include "psycopg/diagnostics.h" #include "psycopg/adapter_qstring.h" @@ -409,8 +410,8 @@ static struct { PyObject **base; const char *docstr; } exctable[] = { - { "psycopg2.Error", &Error, 0, Error_doc }, - { "psycopg2.Warning", &Warning, 0, Warning_doc }, + { "psycopg2.Error", &Error, &PyExc_StandardError, Error_doc }, + { "psycopg2.Warning", &Warning, &PyExc_StandardError, Warning_doc }, { "psycopg2.InterfaceError", &InterfaceError, &Error, InterfaceError_doc }, { "psycopg2.DatabaseError", &DatabaseError, &Error, DatabaseError_doc }, { "psycopg2.InternalError", &InternalError, &DatabaseError, InternalError_doc }, @@ -434,61 +435,6 @@ static struct { }; -#if PY_VERSION_HEX >= 0x02050000 - -/* Error.__reduce_ex__ - * - * The method is required to make exceptions picklable: set the cursor - * attribute to None. Only working from Py 2.5: previous versions - * would require implementing __getstate__, and as of 2012 it's a little - * bit too late to care. */ -static PyObject * -psyco_error_reduce_ex(PyObject *self, PyObject *args) -{ - PyObject *proto = NULL; - PyObject *super = NULL; - PyObject *tuple = NULL; - PyObject *dict = NULL; - PyObject *rv = NULL; - - /* tuple = Exception.__reduce_ex__(self, proto) */ - if (!PyArg_ParseTuple(args, "O", &proto)) { - goto error; - } - if (!(super = PyObject_GetAttrString(PyExc_Exception, "__reduce_ex__"))) { - goto error; - } - if (!(tuple = PyObject_CallFunctionObjArgs(super, self, proto, NULL))) { - goto error; - } - - /* tuple[2]['cursor'] = None - * - * If these checks fail, we can still return a valid object. Pickle - * will likely fail downstream, but there's nothing else we can do here */ - if (!PyTuple_Check(tuple)) { goto exit; } - if (3 > PyTuple_GET_SIZE(tuple)) { goto exit; } - dict = PyTuple_GET_ITEM(tuple, 2); /* borrowed */ - if (!PyDict_Check(dict)) { goto exit; } - - /* Modify the tuple inplace and return it */ - if (0 != PyDict_SetItemString(dict, "cursor", Py_None)) { - goto error; - } - -exit: - rv = tuple; - tuple = NULL; - -error: - Py_XDECREF(tuple); - Py_XDECREF(super); - - return rv; -} - -#endif /* PY_VERSION_HEX >= 0x02050000 */ - static int psyco_errors_init(void) { @@ -498,18 +444,13 @@ psyco_errors_init(void) int i; PyObject *dict = NULL; - PyObject *base; PyObject *str = NULL; - PyObject *descr = NULL; - PyObject *diag_property = NULL; int rv = -1; -#if PY_VERSION_HEX >= 0x02050000 - static PyMethodDef psyco_error_reduce_ex_def = - {"__reduce_ex__", psyco_error_reduce_ex, METH_VARARGS, "pickle helper"}; -#endif + /* 'Error' has been defined elsewhere: only init the other classes */ + Error = (PyObject *)&errorType; - for (i=0; exctable[i].name; i++) { + for (i = 1; exctable[i].name; i++) { if (!(dict = PyDict_New())) { goto exit; } if (exctable[i].docstr) { @@ -518,58 +459,16 @@ psyco_errors_init(void) Py_CLEAR(str); } - if (exctable[i].base == 0) { - #if PY_MAJOR_VERSION < 3 - base = PyExc_StandardError; - #else - /* StandardError is gone in 3.0 */ - base = NULL; - #endif - } - else - base = *exctable[i].base; - if (!(*exctable[i].exc = PyErr_NewException( - exctable[i].name, base, dict))) { + exctable[i].name, *exctable[i].base, dict))) { goto exit; } Py_CLEAR(dict); } - /* Make pgerror, pgcode and cursor default to None on psycopg - error objects. This simplifies error handling code that checks - these attributes. */ - PyObject_SetAttrString(Error, "pgerror", Py_None); - PyObject_SetAttrString(Error, "pgcode", Py_None); - PyObject_SetAttrString(Error, "cursor", Py_None); - - if (!(diag_property = PyObject_CallFunctionObjArgs( - (PyObject *) &PyProperty_Type, &diagnosticsType, NULL))) { - goto exit; - } - PyObject_SetAttrString(Error, "diag", diag_property); - - /* install __reduce_ex__ on Error to make all the subclasses picklable. - * - * Don't install it on Py 2.4: it is not used by the pickle - * protocol, and if called manually fails in an unsettling way, - * probably because the exceptions were old-style classes. */ -#if PY_VERSION_HEX >= 0x02050000 - if (!(descr = PyDescr_NewMethod((PyTypeObject *)Error, - &psyco_error_reduce_ex_def))) { - goto exit; - } - if (0 != PyObject_SetAttrString(Error, - psyco_error_reduce_ex_def.ml_name, descr)) { - goto exit; - } -#endif - rv = 0; exit: - Py_XDECREF(diag_property); - Py_XDECREF(descr); Py_XDECREF(str); Py_XDECREF(dict); return rv; @@ -613,11 +512,10 @@ psyco_errors_set(PyObject *type) Create a new error of the given type with extra attributes. */ -RAISES void -psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg, - const char *pgerror, const char *pgcode) +/* TODO: may have been changed to BORROWED */ +RAISES PyObject * +psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg) { - PyObject *t; PyObject *pymsg; PyObject *err = NULL; connectionObject *conn = NULL; @@ -633,31 +531,24 @@ psyco_set_error(PyObject *exc, cursorObject *curs, const char *msg, else { /* what's better than an error in an error handler in the morning? * Anyway, some error was set, refcount is ok... get outta here. */ - return; + return NULL; + } + + if (err && PyObject_TypeCheck(err, &errorType)) { + errorObject *perr = (errorObject *)err; + if (curs) { + Py_CLEAR(perr->cursor); + Py_INCREF(curs); + perr->cursor = curs; + } } if (err) { - if (curs) { - PyObject_SetAttrString(err, "cursor", (PyObject *)curs); - } - - if (pgerror) { - if ((t = conn_text_from_chars(conn, pgerror))) { - PyObject_SetAttrString(err, "pgerror", t); - Py_DECREF(t); - } - } - - if (pgcode) { - if ((t = conn_text_from_chars(conn, pgcode))) { - PyObject_SetAttrString(err, "pgcode", t); - Py_DECREF(t); - } - } - PyErr_SetObject(exc, err); Py_DECREF(err); } + + return err; } @@ -891,6 +782,7 @@ INIT_MODULE(_psycopg)(void) Py_TYPE(&chunkType) = &PyType_Type; Py_TYPE(&NotifyType) = &PyType_Type; Py_TYPE(&XidType) = &PyType_Type; + Py_TYPE(&errorType) = &PyType_Type; Py_TYPE(&diagnosticsType) = &PyType_Type; if (PyType_Ready(&connectionType) == -1) goto exit; @@ -908,6 +800,8 @@ INIT_MODULE(_psycopg)(void) if (PyType_Ready(&chunkType) == -1) goto exit; if (PyType_Ready(&NotifyType) == -1) goto exit; if (PyType_Ready(&XidType) == -1) goto exit; + errorType.tp_base = (PyTypeObject *)PyExc_StandardError; + if (PyType_Ready(&errorType) == -1) goto exit; if (PyType_Ready(&diagnosticsType) == -1) goto exit; #ifdef PSYCOPG_EXTENSIONS @@ -1043,6 +937,7 @@ INIT_MODULE(_psycopg)(void) pydatetimeType.tp_alloc = PyType_GenericAlloc; NotifyType.tp_alloc = PyType_GenericAlloc; XidType.tp_alloc = PyType_GenericAlloc; + errorType.tp_alloc = PyType_GenericAlloc; diagnosticsType.tp_alloc = PyType_GenericAlloc; #ifdef PSYCOPG_EXTENSIONS diff --git a/psycopg/python.h b/psycopg/python.h index f6d6be0a..9fce6e13 100644 --- a/psycopg/python.h +++ b/psycopg/python.h @@ -114,6 +114,7 @@ typedef unsigned long Py_uhash_t; #define PyInt_AsLong PyLong_AsLong #define PyInt_FromLong PyLong_FromLong #define PyInt_FromSsize_t PyLong_FromSsize_t +#define PyExc_StandardError PyExc_Exception #define PyString_FromFormat PyUnicode_FromFormat #define Py_TPFLAGS_HAVE_ITER 0L #define Py_TPFLAGS_HAVE_RICHCOMPARE 0L diff --git a/psycopg/utils.c b/psycopg/utils.c index 57586c57..698dcb49 100644 --- a/psycopg/utils.c +++ b/psycopg/utils.c @@ -115,10 +115,16 @@ psycopg_escape_identifier_easy(const char *from, Py_ssize_t len) * * Store the return in 'to' and return 0 in case of success, else return -1 * and raise an exception. + * + * If from is null, store null into to. */ RAISES_NEG int psycopg_strdup(char **to, const char *from, Py_ssize_t len) { + if (!from) { + *to = NULL; + return 0; + } if (!len) { len = strlen(from); } if (!(*to = PyMem_Malloc(len + 1))) { PyErr_NoMemory(); diff --git a/setup.py b/setup.py index 41eac4ca..f747aed2 100644 --- a/setup.py +++ b/setup.py @@ -428,7 +428,7 @@ sources = [ 'connection_int.c', 'connection_type.c', 'cursor_int.c', 'cursor_type.c', - 'diagnostics_type.c', + 'diagnostics_type.c', 'error_type.c', 'lobject_int.c', 'lobject_type.c', 'notify_type.c', 'xid_type.c', @@ -441,8 +441,8 @@ sources = [ depends = [ # headers - 'config.h', 'pgtypes.h', 'psycopg.h', 'python.h', - 'connection.h', 'cursor.h', 'diagnostics.h', 'green.h', 'lobject.h', + 'config.h', 'pgtypes.h', 'psycopg.h', 'python.h', 'connection.h', + 'cursor.h', 'diagnostics.h', 'error.h', 'green.h', 'lobject.h', 'notify.h', 'pqpath.h', 'xid.h', 'adapter_asis.h', 'adapter_binary.h', 'adapter_datetime.h', diff --git a/tests/test_module.py b/tests/test_module.py index 1462f292..c65c019e 100755 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -222,6 +222,38 @@ class ExceptionsTestCase(unittest.TestCase): self.assertEqual(diag.sqlstate, '42P01') + def test_diagnostics_independent(self): + cur = self.conn.cursor() + try: + cur.execute("l'acqua e' poca e 'a papera nun galleggia") + except Exception, exc: + diag1 = exc.diag + + self.conn.rollback() + + try: + cur.execute("select level from water where ducks > 1") + except psycopg2.Error, exc: + diag2 = exc.diag + + self.assertEqual(diag1.sqlstate, '42601') + self.assertEqual(diag2.sqlstate, '42P01') + + def test_diagnostics_from_commit(self): + cur = self.conn.cursor() + cur.execute(""" + create temp table test_deferred ( + data int primary key, + ref int references test_deferred (data) + deferrable initially deferred) + """) + cur.execute("insert into test_deferred values (1,2)") + try: + self.conn.commit() + except psycopg2.Error, exc: + e = exc + self.assertEqual(e.diag.sqlstate, '23503') + @skip_before_postgres(9, 3) def test_9_3_diagnostics(self): cur = self.conn.cursor()