Merge branch 'diagnostics' into devel

This commit is contained in:
Daniele Varrazzo 2013-03-18 02:21:09 +00:00
commit 97311967e8
10 changed files with 429 additions and 6 deletions

2
NEWS
View File

@ -6,6 +6,8 @@ What's new in psycopg 2.5
- Added support for Python 3.3.
- 'connection' and 'cursor' objects can be used in 'with' statements
as context managers as specified by recent DBAPI extension.
- Added Diagnostics object to get extended info from a database error.
Many thanks to Matthew Woodcraft for the implementation (ticket #149).
- Added support for backward scrollable cursors. Thanks to Jon Nelson
for the initial patch (ticket #108).
- Added a simple way to customize casting of composite types into Python

View File

@ -139,6 +139,37 @@ functionalities defined by the |DBAPI|_.
.. automethod:: from_string(s)
.. autoclass:: Diagnostics(exception)
.. versionadded:: 2.5
The attributes currently available are:
.. attribute::
column_name
constraint_name
context
datatype_name
internal_position
internal_query
message_detail
message_hint
message_primary
schema_name
severity
source_file
source_function
source_line
sqlstate
statement_position
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.
.. autofunction:: set_wait_callback(f)
.. versionadded:: 2.2.0

View File

@ -157,10 +157,27 @@ available through the following exceptions:
The cursor the exception was raised from; `None` if not applicable.
.. attribute:: diag
A `~psycopg2.extensions.Diagnostics` object containing further
information about the error. ::
>>> try:
... cur.execute("SELECT * FROM barf")
... except Exception, e:
... pass
>>> e.diag.severity
'ERROR'
>>> e.diag.message_primary
'relation "barf" does not exist'
.. versionadded:: 2.5
.. extension::
The `~Error.pgerror`, `~Error.pgcode`, and `~Error.cursor` attributes
are Psycopg extensions.
The `~Error.pgerror`, `~Error.pgcode`, `~Error.cursor`, and
`~Error.diag` attributes are Psycopg extensions.
.. exception:: InterfaceError

View File

@ -58,7 +58,7 @@ except ImportError:
from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid
from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type
from psycopg2._psycopg import ISQLQuote, Notify
from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics
from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError

38
psycopg/diagnostics.h Normal file
View File

@ -0,0 +1,38 @@
/* diagnostics.c - definition for the psycopg Diagnostics type
*
* Copyright (C) 2013 Matthew Woodcraft <matthew@woodcraft.me.uk>
*
* 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_DIAGNOSTICS_H
#define PSYCOPG_DIAGNOSTICS_H 1
extern HIDDEN PyTypeObject diagnosticsType;
typedef struct {
PyObject_HEAD
PyObject *err; /* exception to retrieve the diagnostics from */
} diagnosticsObject;
#endif /* PSYCOPG_DIAGNOSTICS_H */

235
psycopg/diagnostics_type.c Normal file
View File

@ -0,0 +1,235 @@
/* diagnostics.c - present information from libpq error responses
*
* Copyright (C) 2013 Matthew Woodcraft <matthew@woodcraft.me.uk>
*
* 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/diagnostics.h"
#include "psycopg/cursor.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. */
#ifndef PG_DIAG_SCHEMA_NAME
#define PG_DIAG_SCHEMA_NAME 's'
#endif
#ifndef PG_DIAG_TABLE_NAME
#define PG_DIAG_TABLE_NAME 't'
#endif
#ifndef PG_DIAG_COLUMN_NAME
#define PG_DIAG_COLUMN_NAME 'c'
#endif
#ifndef PG_DIAG_DATATYPE_NAME
#define PG_DIAG_DATATYPE_NAME 'd'
#endif
#ifndef PG_DIAG_CONSTRAINT_NAME
#define PG_DIAG_CONSTRAINT_NAME 'n'
#endif
/* Retrieve an error string from the exception's cursor.
*
* If the cursor or its result isn't available, return None.
*/
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;
}
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;
}
/* object calculated member list */
static struct PyGetSetDef diagnosticsObject_getsets[] = {
{ "severity", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SEVERITY },
{ "sqlstate", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SQLSTATE },
{ "message_primary", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_MESSAGE_PRIMARY },
{ "message_detail", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_MESSAGE_DETAIL },
{ "message_hint", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_MESSAGE_HINT },
{ "statement_position", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_STATEMENT_POSITION },
{ "internal_position", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_INTERNAL_POSITION },
{ "internal_query", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_INTERNAL_QUERY },
{ "context", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_CONTEXT },
{ "schema_name", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SCHEMA_NAME },
{ "table_name", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_TABLE_NAME },
{ "column_name", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_COLUMN_NAME },
{ "datatype_name", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_DATATYPE_NAME },
{ "constraint_name", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_CONSTRAINT_NAME },
{ "source_file", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SOURCE_FILE },
{ "source_line", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SOURCE_LINE },
{ "source_function", (getter)psyco_diagnostics_get_field, NULL,
NULL, (void*) PG_DIAG_SOURCE_FUNCTION },
{NULL}
};
/* initialization and finalization methods */
static PyObject *
diagnostics_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
return type->tp_alloc(type, 0);
}
static int
diagnostics_init(diagnosticsObject *self, PyObject *args, PyObject *kwds)
{
PyObject *err = NULL;
if (!PyArg_ParseTuple(args, "O", &err))
return -1;
Py_INCREF(err);
self->err = err;
return 0;
}
static int
diagnostics_traverse(diagnosticsObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->err);
return 0;
}
static void
diagnostics_dealloc(diagnosticsObject* self)
{
Py_CLEAR(self->err);
Py_TYPE(self)->tp_free((PyObject *)self);
}
static void
diagnostics_del(PyObject* self)
{
PyObject_GC_Del(self);
}
/* object type */
static const char diagnosticsType_doc[] =
"Details from a database error report.\n\n"
"The object is returned by the `~psycopg2.Error.diag` attribute of the\n"
"`!Error` object.\n"
"All the information available from the |PQresultErrorField|_ function\n"
"are exposed as attributes by the object, e.g. the `!severity` attribute\n"
"returns the `!PG_DIAG_SEVERITY` code. "
"Please refer to the `PostgreSQL documentation`__ for the meaning of all"
" the attributes.\n\n"
".. |PQresultErrorField| replace:: `!PQresultErrorField()`\n"
".. _PQresultErrorField: http://www.postgresql.org/docs/current/static/"
"libpq-exec.html#LIBPQ-PQRESULTERRORFIELD\n"
".. __: PQresultErrorField_\n";
PyTypeObject diagnosticsType = {
PyVarObject_HEAD_INIT(NULL, 0)
"psycopg2._psycopg.Diagnostics",
sizeof(diagnosticsObject),
0,
(destructor)diagnostics_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*/
diagnosticsType_doc, /*tp_doc*/
(traverseproc)diagnostics_traverse, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
/* Attribute descriptor and subclassing stuff */
0, /*tp_methods*/
0, /*tp_members*/
diagnosticsObject_getsets, /*tp_getset*/
0, /*tp_base*/
0, /*tp_dict*/
0, /*tp_descr_get*/
0, /*tp_descr_set*/
0, /*tp_dictoffset*/
(initproc)diagnostics_init, /*tp_init*/
0, /*tp_alloc will be set to PyType_GenericAlloc in module init*/
diagnostics_new, /*tp_new*/
(freefunc)diagnostics_del, /*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*/
};

View File

@ -1509,7 +1509,8 @@ 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);
IFCLEARPGRES(curs->pgres);
/* don't clear curs->pgres, because it contains detailed error
information */
ex = -1;
break;
}

View File

@ -35,6 +35,7 @@
#include "psycopg/typecast.h"
#include "psycopg/microprotocols.h"
#include "psycopg/microprotocols_proto.h"
#include "psycopg/diagnostics.h"
#include "psycopg/adapter_qstring.h"
#include "psycopg/adapter_binary.h"
@ -500,6 +501,7 @@ psyco_errors_init(void)
PyObject *base;
PyObject *str = NULL;
PyObject *descr = NULL;
PyObject *diag_property = NULL;
int rv = -1;
#if PY_VERSION_HEX >= 0x02050000
@ -541,6 +543,12 @@ psyco_errors_init(void)
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
@ -560,6 +568,7 @@ psyco_errors_init(void)
rv = 0;
exit:
Py_XDECREF(diag_property);
Py_XDECREF(descr);
Py_XDECREF(str);
Py_XDECREF(dict);
@ -882,6 +891,7 @@ INIT_MODULE(_psycopg)(void)
Py_TYPE(&chunkType) = &PyType_Type;
Py_TYPE(&NotifyType) = &PyType_Type;
Py_TYPE(&XidType) = &PyType_Type;
Py_TYPE(&diagnosticsType) = &PyType_Type;
if (PyType_Ready(&connectionType) == -1) goto exit;
if (PyType_Ready(&cursorType) == -1) goto exit;
@ -898,6 +908,7 @@ 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;
if (PyType_Ready(&diagnosticsType) == -1) goto exit;
#ifdef PSYCOPG_EXTENSIONS
Py_TYPE(&lobjectType) = &PyType_Type;
@ -987,6 +998,7 @@ INIT_MODULE(_psycopg)(void)
PyModule_AddObject(module, "ISQLQuote", (PyObject*)&isqlquoteType);
PyModule_AddObject(module, "Notify", (PyObject*)&NotifyType);
PyModule_AddObject(module, "Xid", (PyObject*)&XidType);
PyModule_AddObject(module, "Diagnostics", (PyObject*)&diagnosticsType);
#ifdef PSYCOPG_EXTENSIONS
PyModule_AddObject(module, "lobject", (PyObject*)&lobjectType);
#endif
@ -1031,6 +1043,7 @@ INIT_MODULE(_psycopg)(void)
pydatetimeType.tp_alloc = PyType_GenericAlloc;
NotifyType.tp_alloc = PyType_GenericAlloc;
XidType.tp_alloc = PyType_GenericAlloc;
diagnosticsType.tp_alloc = PyType_GenericAlloc;
#ifdef PSYCOPG_EXTENSIONS
lobjectType.tp_alloc = PyType_GenericAlloc;

View File

@ -428,6 +428,7 @@ sources = [
'connection_int.c', 'connection_type.c',
'cursor_int.c', 'cursor_type.c',
'diagnostics_type.c',
'lobject_int.c', 'lobject_type.c',
'notify_type.c', 'xid_type.c',
@ -441,7 +442,7 @@ sources = [
depends = [
# headers
'config.h', 'pgtypes.h', 'psycopg.h', 'python.h',
'connection.h', 'cursor.h', 'green.h', 'lobject.h',
'connection.h', 'cursor.h', 'diagnostics.h', 'green.h', 'lobject.h',
'notify.h', 'pqpath.h', 'xid.h',
'adapter_asis.h', 'adapter_binary.h', 'adapter_datetime.h',

View File

@ -22,7 +22,8 @@
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
from testutils import unittest, skip_before_python
from testutils import unittest, skip_before_python, skip_before_postgres
from testconfig import dsn
import psycopg2
@ -154,6 +155,90 @@ class ExceptionsTestCase(unittest.TestCase):
self.assert_(e.pgerror)
self.assert_(e.cursor is cur)
def test_diagnostics_attributes(self):
cur = self.conn.cursor()
try:
cur.execute("select * from nonexist")
except psycopg2.Error, exc:
e = exc
diag = e.diag
self.assert_(isinstance(diag, psycopg2.extensions.Diagnostics))
for attr in [
'column_name', 'constraint_name', 'context', 'datatype_name',
'internal_position', 'internal_query', 'message_detail',
'message_hint', 'message_primary', 'schema_name', 'severity',
'source_file', 'source_function', 'source_line', 'sqlstate',
'statement_position', 'table_name', ]:
v = getattr(diag, attr)
if v is not None:
self.assert_(isinstance(v, str))
def test_diagnostics_values(self):
cur = self.conn.cursor()
try:
cur.execute("select * from nonexist")
except psycopg2.Error, exc:
e = exc
self.assertEqual(e.diag.sqlstate, '42P01')
self.assertEqual(e.diag.severity, 'ERROR')
self.assertEqual(e.diag.statement_position, '15')
def test_diagnostics_life(self):
import gc
from weakref import ref
def tmp():
cur = self.conn.cursor()
try:
cur.execute("select * from nonexist")
except psycopg2.Error, exc:
return cur, exc
cur, e = tmp()
diag = e.diag
w = ref(cur)
del e, cur
gc.collect()
assert(w() is not None)
self.assertEqual(diag.sqlstate, '42P01')
del diag
gc.collect()
assert(w() is None)
def test_diagnostics_copy(self):
from StringIO import StringIO
f = StringIO()
cur = self.conn.cursor()
try:
cur.copy_to(f, 'nonexist')
except psycopg2.Error, exc:
diag = exc.diag
self.assertEqual(diag.sqlstate, '42P01')
@skip_before_postgres(9, 3)
def test_9_3_diagnostics(self):
cur = self.conn.cursor()
cur.execute("""
create temp table test_exc (
data int constraint chk_eq1 check (data = 1)
)""")
try:
cur.execute("insert into test_exc values(2)")
except psycopg2.Error, exc:
e = exc
self.assertEqual(e.pgcode, '23514')
self.assertEqual(e.diag.schema_name[:7], "pg_temp")
self.assertEqual(e.diag.table_name, "test_exc")
self.assertEqual(e.diag.column_name, None)
self.assertEqual(e.diag.constraint_name, "chk_eq1")
self.assertEqual(e.diag.datatype_name, None)
@skip_before_python(2, 5)
def test_pickle(self):
import pickle