diff --git a/ChangeLog b/ChangeLog index 4a196ac9..8ef45992 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2010-02-15 Federico Di Gregorio + + * Added new Decimal adapter that correctly converts NaN and infinity + to PostgreSQL NaN numeric values. Also added tests. + 2010-02-15 Daniele Varrazzo * lib/errorcodes.py: Updated to PostgreSQL 8.4; added lookup() function. diff --git a/psycopg/adapter_pdecimal.c b/psycopg/adapter_pdecimal.c new file mode 100644 index 00000000..11d25950 --- /dev/null +++ b/psycopg/adapter_pdecimal.c @@ -0,0 +1,253 @@ +/* adapter_pdecimal.c - psycopg Decimal type wrapper implementation + * + * Copyright (C) 2003-2010 Federico Di Gregorio + * + * 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 PY_SSIZE_T_CLEAN +#include +#include +#include +#include + +#define PSYCOPG_MODULE +#include "psycopg/config.h" +#include "psycopg/python.h" +#include "psycopg/psycopg.h" +#include "psycopg/adapter_pdecimal.h" +#include "psycopg/microprotocols_proto.h" + + +/** the Decimal object **/ + +static PyObject * +pdecimal_str(pdecimalObject *self) +{ + PyObject *res = NULL; + PyObject *check = PyObject_CallMethod(self->wrapped, "is_finite", NULL); + + if (check == Py_True) + res = PyObject_Str(self->wrapped); + else + res = PyString_FromString("'NaN'::numeric"); + + Py_DECREF(check); + return res; +} + +static PyObject * +pdecimal_getquoted(pdecimalObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) return NULL; + return pdecimal_str(self); +} + +static PyObject * +pdecimal_conform(pdecimalObject *self, PyObject *args) +{ + PyObject *res, *proto; + + if (!PyArg_ParseTuple(args, "O", &proto)) return NULL; + + if (proto == (PyObject*)&isqlquoteType) + res = (PyObject*)self; + else + res = Py_None; + + Py_INCREF(res); + return res; +} + +/** the Decimal object */ + +/* object member list */ + +static struct PyMemberDef pdecimalObject_members[] = { + {"adapted", T_OBJECT, offsetof(pdecimalObject, wrapped), RO}, + {NULL} +}; + +/* object method table */ + +static PyMethodDef pdecimalObject_methods[] = { + {"getquoted", (PyCFunction)pdecimal_getquoted, METH_VARARGS, + "getquoted() -> wrapped object value as SQL-quoted string"}, + {"__conform__", (PyCFunction)pdecimal_conform, METH_VARARGS, NULL}, + {NULL} /* Sentinel */ +}; + +/* initialization and finalization methods */ + +static int +pdecimal_setup(pdecimalObject *self, PyObject *obj) +{ + Dprintf("pdecimal_setup: init pdecimal object at %p, refcnt = " + FORMAT_CODE_PY_SSIZE_T, + self, ((PyObject *)self)->ob_refcnt + ); + + Py_INCREF(obj); + self->wrapped = obj; + + Dprintf("pdecimal_setup: good pdecimal object at %p, refcnt = " + FORMAT_CODE_PY_SSIZE_T, + self, ((PyObject *)self)->ob_refcnt + ); + return 0; +} + +static int +pdecimal_traverse(PyObject *obj, visitproc visit, void *arg) +{ + pdecimalObject *self = (pdecimalObject *)obj; + + Py_VISIT(self->wrapped); + return 0; +} + +static void +pdecimal_dealloc(PyObject* obj) +{ + pdecimalObject *self = (pdecimalObject *)obj; + + Py_CLEAR(self->wrapped); + + Dprintf("pdecimal_dealloc: deleted pdecimal object at %p, refcnt = " + FORMAT_CODE_PY_SSIZE_T, + obj, obj->ob_refcnt + ); + + obj->ob_type->tp_free(obj); +} + +static int +pdecimal_init(PyObject *obj, PyObject *args, PyObject *kwds) +{ + PyObject *o; + + if (!PyArg_ParseTuple(args, "O", &o)) + return -1; + + return pdecimal_setup((pdecimalObject *)obj, o); +} + +static PyObject * +pdecimal_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + return type->tp_alloc(type, 0); +} + +static void +pdecimal_del(PyObject* self) +{ + PyObject_GC_Del(self); +} + +static PyObject * +pdecimal_repr(pdecimalObject *self) +{ + return PyString_FromFormat("", + self); +} + + +/* object type */ + +#define pdecimalType_doc \ +"Decimal(str) -> new Decimal adapter object" + +PyTypeObject pdecimalType = { + PyObject_HEAD_INIT(NULL) + 0, + "psycopg2._psycopg.Decimal", + sizeof(pdecimalObject), + 0, + pdecimal_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + + 0, /*tp_compare*/ + + (reprfunc)pdecimal_repr, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + + 0, /*tp_call*/ + (reprfunc)pdecimal_str, /*tp_str*/ + + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + + Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE|Py_TPFLAGS_HAVE_GC, /*tp_flags*/ + pdecimalType_doc, /*tp_doc*/ + + pdecimal_traverse, /*tp_traverse*/ + 0, /*tp_clear*/ + + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + + /* Attribute descriptor and subclassing stuff */ + + pdecimalObject_methods, /*tp_methods*/ + pdecimalObject_members, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + + pdecimal_init, /*tp_init*/ + 0, /*tp_alloc will be set to PyType_GenericAlloc in module init*/ + pdecimal_new, /*tp_new*/ + (freefunc)pdecimal_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*/ +}; + + +/** module-level functions **/ + +PyObject * +psyco_Decimal(PyObject *module, PyObject *args) +{ + PyObject *obj; + + if (!PyArg_ParseTuple(args, "O", &obj)) + return NULL; + + return PyObject_CallFunction((PyObject *)&pdecimalType, "O", obj); +} diff --git a/psycopg/adapter_pdecimal.h b/psycopg/adapter_pdecimal.h new file mode 100644 index 00000000..82825a23 --- /dev/null +++ b/psycopg/adapter_pdecimal.h @@ -0,0 +1,58 @@ +/* adapter_pdecimal.h - definition for the psycopg Decimal type wrapper + * + * Copyright (C) 2003-2010 Federico Di Gregorio + * + * 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_PDECIMAL_H +#define PSYCOPG_PDECIMAL_H 1 + +#define PY_SSIZE_T_CLEAN +#include + +#include "psycopg/config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +extern HIDDEN PyTypeObject pdecimalType; + +typedef struct { + PyObject_HEAD + + /* this is the real object we wrap */ + PyObject *wrapped; + +} pdecimalObject; + +/* functions exported to psycopgmodule.c */ + +HIDDEN PyObject *psyco_Decimal(PyObject *module, PyObject *args); +#define psyco_Decimal_doc \ + "Decimal(obj) -> new decimal.Decimal value" + +#ifdef __cplusplus +} +#endif + +#endif /* !defined(PSYCOPG_PDECIMAL_H) */ diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index 6c25b523..7726319b 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -41,6 +41,7 @@ #include "psycopg/adapter_binary.h" #include "psycopg/adapter_pboolean.h" #include "psycopg/adapter_pfloat.h" +#include "psycopg/adapter_pdecimal.h" #include "psycopg/adapter_asis.h" #include "psycopg/adapter_list.h" #include "psycopg/typecast_binary.h" @@ -276,6 +277,7 @@ static void psyco_adapters_init(PyObject *mod) { PyObject *call; + PyTypeObject *type; microprotocols_add(&PyFloat_Type, NULL, (PyObject*)&pfloatType); microprotocols_add(&PyInt_Type, NULL, (PyObject*)&asisType); @@ -286,8 +288,9 @@ psyco_adapters_init(PyObject *mod) microprotocols_add(&PyUnicode_Type, NULL, (PyObject*)&qstringType); microprotocols_add(&PyBuffer_Type, NULL, (PyObject*)&binaryType); microprotocols_add(&PyList_Type, NULL, (PyObject*)&listType); - microprotocols_add((PyTypeObject*)psyco_GetDecimalType(), - NULL, (PyObject*)&asisType); + + if ((type = (PyTypeObject*)psyco_GetDecimalType()) != NULL) + microprotocols_add(type, NULL, (PyObject*)&pdecimalType); /* the module has already been initialized, so we can obtain the callable objects directly from its dictionary :) */ @@ -579,13 +582,13 @@ psyco_is_main_interp(void) the float type. If decimals are not to be used, return NULL. - */ +*/ PyObject * psyco_GetDecimalType(void) { - PyObject *decimalType = NULL; static PyObject *cachedType = NULL; + PyObject *decimalType = NULL; PyObject *decimal; /* Use the cached object if running from the main interpreter. */ @@ -603,8 +606,7 @@ psyco_GetDecimalType(void) } else { PyErr_Clear(); - decimalType = (PyObject *)&PyFloat_Type; - Py_INCREF(decimalType); + decimalType = NULL; } /* Store the object from future uses. */ @@ -637,6 +639,8 @@ static PyMethodDef psycopgMethods[] = { {"Boolean", (PyCFunction)psyco_Boolean, METH_VARARGS, psyco_Float_doc}, {"Float", (PyCFunction)psyco_Float, + METH_VARARGS, psyco_Decimal_doc}, + {"Decimal", (PyCFunction)psyco_Decimal, METH_VARARGS, psyco_Boolean_doc}, {"Binary", (PyCFunction)psyco_Binary, METH_VARARGS, psyco_Binary_doc}, @@ -702,6 +706,7 @@ init_psycopg(void) isqlquoteType.ob_type = &PyType_Type; pbooleanType.ob_type = &PyType_Type; pfloatType.ob_type = &PyType_Type; + pdecimalType.ob_type = &PyType_Type; asisType.ob_type = &PyType_Type; listType.ob_type = &PyType_Type; chunkType.ob_type = &PyType_Type; @@ -714,6 +719,7 @@ init_psycopg(void) if (PyType_Ready(&isqlquoteType) == -1) return; if (PyType_Ready(&pbooleanType) == -1) return; if (PyType_Ready(&pfloatType) == -1) return; + if (PyType_Ready(&pdecimalType) == -1) return; if (PyType_Ready(&asisType) == -1) return; if (PyType_Ready(&listType) == -1) return; if (PyType_Ready(&chunkType) == -1) return; @@ -815,6 +821,7 @@ init_psycopg(void) isqlquoteType.tp_alloc = PyType_GenericAlloc; pbooleanType.tp_alloc = PyType_GenericAlloc; pfloatType.tp_alloc = PyType_GenericAlloc; + pdecimalType.tp_alloc = PyType_GenericAlloc; connectionType.tp_alloc = PyType_GenericAlloc; asisType.tp_alloc = PyType_GenericAlloc; qstringType.tp_alloc = PyType_GenericAlloc; diff --git a/psycopg/typecast_basic.c b/psycopg/typecast_basic.c index a1c33944..e9ad527a 100644 --- a/psycopg/typecast_basic.c +++ b/psycopg/typecast_basic.c @@ -132,8 +132,14 @@ typecast_DECIMAL_cast(const char *s, Py_ssize_t len, PyObject *curs) return PyErr_NoMemory(); strncpy(buffer, s, (size_t) len); buffer[len] = '\0'; decimalType = psyco_GetDecimalType(); - res = PyObject_CallFunction(decimalType, "s", buffer); - Py_DECREF(decimalType); + /* Fall back on float if decimal is not available */ + if (decimalType != NULL) { + res = PyObject_CallFunction(decimalType, "s", buffer); + Py_DECREF(decimalType); + } + else { + res = PyObject_CallFunction((PyObject*)&PyFloat_Type, "s", buffer); + } PyMem_Free(buffer); return res; diff --git a/psycopg2.cproj b/psycopg2.cproj index 8445c45f..94429ab9 100644 --- a/psycopg2.cproj +++ b/psycopg2.cproj @@ -148,6 +148,7 @@ + diff --git a/psycopg2.sln b/psycopg2.sln index 6fd68b15..aed4f214 100644 --- a/psycopg2.sln +++ b/psycopg2.sln @@ -19,11 +19,19 @@ Global Policies = $0 $0.TextStylePolicy = $1 $1.FileWidth = 120 - $1.TabWidth = 4 - $1.TabsToSpaces = True $1.NoTabsAfterNonTabs = False - $1.RemoveTrailingWhitespace = True - $1.EolMarker = Native + $0.DotNetNamingPolicy = $2 + $2.DirectoryNamespaceAssociation = None + $2.ResourceNamePolicy = FileName + $0.TextStylePolicy = $3 + $3.NoTabsAfterNonTabs = False + $3.inheritsSet = Mono + $3.inheritsScope = text/x-python + $3.scope = text/plain + $0.StandardHeader = $4 + $4.Text = + $4.IncludeInNewFiles = False + $4.inheritsSet = MITX11License name = psycopg2 EndGlobalSection EndGlobal diff --git a/setup.py b/setup.py index 57d71db8..784e08ed 100644 --- a/setup.py +++ b/setup.py @@ -341,7 +341,8 @@ sources = [ 'connection_type.c', 'connection_int.c', 'cursor_type.c', 'cursor_int.c', 'lobject_type.c', 'lobject_int.c', 'adapter_qstring.c', 'adapter_pboolean.c', 'adapter_binary.c', - 'adapter_asis.c', 'adapter_list.c', 'adapter_datetime.c', 'adapter_pfloat.c', + 'adapter_asis.c', 'adapter_list.c', 'adapter_datetime.c', + 'adapter_pfloat.c', 'adapter_pdecimal.c', 'utils.c'] parser = ConfigParser.ConfigParser() diff --git a/tests/types_basic.py b/tests/types_basic.py index 063264ef..aaca2374 100755 --- a/tests/types_basic.py +++ b/tests/types_basic.py @@ -59,15 +59,25 @@ class TypesBasicTests(unittest.TestCase): self.failUnless(s == 1971, "wrong integer quoting: " + str(s)) s = self.execute("SELECT %s AS foo", (1971L,)) self.failUnless(s == 1971L, "wrong integer quoting: " + str(s)) - # Python 2.4 defaults to Decimal? (Apparently it does not.) + if sys.version_info[0] < 2 or sys.version_info[1] < 4: + s = self.execute("SELECT %s AS foo", (19.10,)) + self.failUnless(abs(s - 19.10) < 0.001, + "wrong float quoting: " + str(s)) + + def testDecimal(self): if sys.version_info[0] >= 2 and sys.version_info[1] >= 4: s = self.execute("SELECT %s AS foo", (decimal.Decimal("19.10"),)) self.failUnless(s - decimal.Decimal("19.10") == 0, "wrong decimal quoting: " + str(s)) - else: - s = self.execute("SELECT %s AS foo", (19.10,)) - self.failUnless(abs(s - 19.10) < 0.001, - "wrong float quoting: " + str(s)) + s = self.execute("SELECT %s AS foo", (decimal.Decimal("NaN"),)) + self.failUnless(str(s) == "NaN", "wrong decimal quoting: " + str(s)) + self.failUnless(type(s) == decimal.Decimal, "wrong decimal conversion: " + repr(s)) + s = self.execute("SELECT %s AS foo", (decimal.Decimal("infinity"),)) + self.failUnless(str(s) == "NaN", "wrong decimal quoting: " + str(s)) + self.failUnless(type(s) == decimal.Decimal, "wrong decimal conversion: " + repr(s)) + s = self.execute("SELECT %s AS foo", (decimal.Decimal("-infinity"),)) + self.failUnless(str(s) == "NaN", "wrong decimal quoting: " + str(s)) + self.failUnless(type(s) == decimal.Decimal, "wrong decimal conversion: " + repr(s)) def testFloat(self): s = self.execute("SELECT %s AS foo", (float("nan"),))