diff --git a/NEWS b/NEWS index b2d21535..84be2015 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,8 @@ What's new in psycopg 2.7.2 2.7 by mistake. - Don't display the password in `connection.dsn` when the connection string is specified as an URI (:ticket:`#528`). +- Return objects with timezone parsing "infinity" :sql:`timestamptz` + (:ticket:`#536`). What's new in psycopg 2.7.1 diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 24324086..8545fcfa 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -823,10 +823,12 @@ from the database. See :ref:`unicode-handling` for details. .. data:: PYDATE PYDATETIME + PYDATETIMETZ PYINTERVAL PYTIME PYDATEARRAY PYDATETIMEARRAY + PYDATETIMETZARRAY PYINTERVALARRAY PYTIMEARRAY @@ -835,10 +837,12 @@ from the database. See :ref:`unicode-handling` for details. .. data:: MXDATE MXDATETIME + MXDATETIMETZ MXINTERVAL MXTIME MXDATEARRAY MXDATETIMEARRAY + MXDATETIMETZARRAY MXINTERVALARRAY MXTIMEARRAY @@ -851,3 +855,5 @@ from the database. See :ref:`unicode-handling` for details. module. In older versions they can be imported from the implementation module `!psycopg2._psycopg`. +.. versionchanged:: 2.7.2 + added `!*DATETIMETZ*` objects. diff --git a/lib/extensions.py b/lib/extensions.py index 00d71f0a..91b81331 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -43,16 +43,16 @@ from psycopg2._psycopg import ( # noqa try: from psycopg2._psycopg import ( # noqa - MXDATE, MXDATETIME, MXINTERVAL, MXTIME, - MXDATEARRAY, MXDATETIMEARRAY, MXINTERVALARRAY, MXTIMEARRAY, + MXDATE, MXDATETIME, MXDATETIMETZ, MXINTERVAL, MXTIME, MXDATEARRAY, + MXDATETIMEARRAY, MXDATETIMETZARRAY, MXINTERVALARRAY, MXTIMEARRAY, DateFromMx, TimeFromMx, TimestampFromMx, IntervalFromMx, ) except ImportError: pass try: from psycopg2._psycopg import ( # noqa - PYDATE, PYDATETIME, PYINTERVAL, PYTIME, - PYDATEARRAY, PYDATETIMEARRAY, PYINTERVALARRAY, PYTIMEARRAY, + PYDATE, PYDATETIME, PYDATETIMETZ, PYINTERVAL, PYTIME, PYDATEARRAY, + PYDATETIMEARRAY, PYDATETIMETZARRAY, PYINTERVALARRAY, PYTIMEARRAY, DateFromPy, TimeFromPy, TimestampFromPy, IntervalFromPy, ) except ImportError: pass diff --git a/psycopg/typecast.c b/psycopg/typecast.c index 4fd92be0..cf19fcdc 100644 --- a/psycopg/typecast.c +++ b/psycopg/typecast.c @@ -197,6 +197,7 @@ typecast_UNKNOWN_cast(const char *str, Py_ssize_t len, PyObject *curs) #include "psycopg/typecast_builtins.c" #define typecast_PYDATETIMEARRAY_cast typecast_GENERIC_ARRAY_cast +#define typecast_PYDATETIMETZARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_PYDATEARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_PYTIMEARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_PYINTERVALARRAY_cast typecast_GENERIC_ARRAY_cast @@ -204,10 +205,12 @@ typecast_UNKNOWN_cast(const char *str, Py_ssize_t len, PyObject *curs) /* a list of initializers, used to make the typecasters accessible anyway */ static typecastObject_initlist typecast_pydatetime[] = { {"PYDATETIME", typecast_DATETIME_types, typecast_PYDATETIME_cast}, + {"PYDATETIMETZ", typecast_DATETIMETZ_types, typecast_PYDATETIMETZ_cast}, {"PYTIME", typecast_TIME_types, typecast_PYTIME_cast}, {"PYDATE", typecast_DATE_types, typecast_PYDATE_cast}, {"PYINTERVAL", typecast_INTERVAL_types, typecast_PYINTERVAL_cast}, {"PYDATETIMEARRAY", typecast_DATETIMEARRAY_types, typecast_PYDATETIMEARRAY_cast, "PYDATETIME"}, + {"PYDATETIMETZARRAY", typecast_DATETIMETZARRAY_types, typecast_PYDATETIMETZARRAY_cast, "PYDATETIMETZ"}, {"PYTIMEARRAY", typecast_TIMEARRAY_types, typecast_PYTIMEARRAY_cast, "PYTIME"}, {"PYDATEARRAY", typecast_DATEARRAY_types, typecast_PYDATEARRAY_cast, "PYDATE"}, {"PYINTERVALARRAY", typecast_INTERVALARRAY_types, typecast_PYINTERVALARRAY_cast, "PYINTERVAL"}, @@ -216,6 +219,7 @@ static typecastObject_initlist typecast_pydatetime[] = { #ifdef HAVE_MXDATETIME #define typecast_MXDATETIMEARRAY_cast typecast_GENERIC_ARRAY_cast +#define typecast_MXDATETIMETZARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_MXDATEARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_MXTIMEARRAY_cast typecast_GENERIC_ARRAY_cast #define typecast_MXINTERVALARRAY_cast typecast_GENERIC_ARRAY_cast @@ -223,10 +227,12 @@ static typecastObject_initlist typecast_pydatetime[] = { /* a list of initializers, used to make the typecasters accessible anyway */ static typecastObject_initlist typecast_mxdatetime[] = { {"MXDATETIME", typecast_DATETIME_types, typecast_MXDATE_cast}, + {"MXDATETIMETZ", typecast_DATETIMETZ_types, typecast_MXDATE_cast}, {"MXTIME", typecast_TIME_types, typecast_MXTIME_cast}, {"MXDATE", typecast_DATE_types, typecast_MXDATE_cast}, {"MXINTERVAL", typecast_INTERVAL_types, typecast_MXINTERVAL_cast}, {"MXDATETIMEARRAY", typecast_DATETIMEARRAY_types, typecast_MXDATETIMEARRAY_cast, "MXDATETIME"}, + {"MXDATETIMETZARRAY", typecast_DATETIMETZARRAY_types, typecast_MXDATETIMETZARRAY_cast, "MXDATETIMETZ"}, {"MXTIMEARRAY", typecast_TIMEARRAY_types, typecast_MXTIMEARRAY_cast, "MXTIME"}, {"MXDATEARRAY", typecast_DATEARRAY_types, typecast_MXDATEARRAY_cast, "MXDATE"}, {"MXINTERVALARRAY", typecast_INTERVALARRAY_types, typecast_MXINTERVALARRAY_cast, "MXINTERVAL"}, diff --git a/psycopg/typecast_builtins.c b/psycopg/typecast_builtins.c index fa548a73..75bcaeca 100644 --- a/psycopg/typecast_builtins.c +++ b/psycopg/typecast_builtins.c @@ -7,6 +7,7 @@ static long int typecast_UNICODE_types[] = {19, 18, 25, 1042, 1043, 0}; static long int typecast_STRING_types[] = {19, 18, 25, 1042, 1043, 0}; static long int typecast_BOOLEAN_types[] = {16, 0}; static long int typecast_DATETIME_types[] = {1114, 1184, 704, 1186, 0}; +static long int typecast_DATETIMETZ_types[] = {1184, 0}; static long int typecast_TIME_types[] = {1083, 1266, 0}; static long int typecast_DATE_types[] = {1082, 0}; static long int typecast_INTERVAL_types[] = {704, 1186, 0}; @@ -20,6 +21,7 @@ static long int typecast_UNICODEARRAY_types[] = {1002, 1003, 1009, 1014, 1015, 0 static long int typecast_STRINGARRAY_types[] = {1002, 1003, 1009, 1014, 1015, 0}; static long int typecast_BOOLEANARRAY_types[] = {1000, 0}; static long int typecast_DATETIMEARRAY_types[] = {1115, 1185, 0}; +static long int typecast_DATETIMETZARRAY_types[] = {1185, 0}; static long int typecast_TIMEARRAY_types[] = {1183, 1270, 0}; static long int typecast_DATEARRAY_types[] = {1182, 0}; static long int typecast_INTERVALARRAY_types[] = {1187, 0}; @@ -41,6 +43,7 @@ static typecastObject_initlist typecast_builtins[] = { {"STRING", typecast_STRING_types, typecast_STRING_cast, NULL}, {"BOOLEAN", typecast_BOOLEAN_types, typecast_BOOLEAN_cast, NULL}, {"DATETIME", typecast_DATETIME_types, typecast_DATETIME_cast, NULL}, + {"DATETIMETZ", typecast_DATETIMETZ_types, typecast_DATETIMETZ_cast, NULL}, {"TIME", typecast_TIME_types, typecast_TIME_cast, NULL}, {"DATE", typecast_DATE_types, typecast_DATE_cast, NULL}, {"INTERVAL", typecast_INTERVAL_types, typecast_INTERVAL_cast, NULL}, diff --git a/psycopg/typecast_datetime.c b/psycopg/typecast_datetime.c index a833d86c..fec57de6 100644 --- a/psycopg/typecast_datetime.c +++ b/psycopg/typecast_datetime.c @@ -80,91 +80,152 @@ typecast_PYDATE_cast(const char *str, Py_ssize_t len, PyObject *curs) return obj; } -/** DATETIME - cast a timestamp into a datetime python object **/ +/* convert the strings -infinity and infinity into a datetime with timezone */ +static PyObject * +_parse_inftz(const char *str, PyObject *curs) +{ + PyObject *rv = NULL; + PyObject *m = NULL; + PyObject *tzinfo_factory = NULL; + PyObject *tzinfo = NULL; + PyObject *args = NULL; + PyObject *kwargs = NULL; + PyObject *replace = NULL; + + if (!(m = PyObject_GetAttrString( + (PyObject*)PyDateTimeAPI->DateTimeType, + (str[0] == '-' ? "min" : "max")))) { + goto exit; + } + + tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory; + if (tzinfo_factory == Py_None) { + rv = m; + m = NULL; + goto exit; + } + + if (!(tzinfo = PyObject_CallFunction(tzinfo_factory, "i", 0))) { + goto exit; + } + + /* m.replace(tzinfo=tzinfo) */ + if (!(args = PyTuple_New(0))) { goto exit; } + if (!(kwargs = PyDict_New())) { goto exit; } + if (0 != PyDict_SetItemString(kwargs, "tzinfo", tzinfo)) { goto exit; } + if (!(replace = PyObject_GetAttrString(m, "replace"))) { goto exit; } + rv = PyObject_Call(replace, args, kwargs); + +exit: + Py_XDECREF(replace); + Py_XDECREF(args); + Py_XDECREF(kwargs); + Py_XDECREF(tzinfo); + Py_XDECREF(m); + + return rv; +} static PyObject * -typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs) +_parse_noninftz(const char *str, Py_ssize_t len, PyObject *curs) { - PyObject* obj = NULL; + PyObject* rv = NULL; PyObject *tzinfo = NULL; PyObject *tzinfo_factory; int n, y=0, m=0, d=0; int hh=0, mm=0, ss=0, us=0, tz=0; const char *tp = NULL; + Dprintf("typecast_PYDATETIMETZ_cast: s = %s", str); + n = typecast_parse_date(str, &tp, &len, &y, &m, &d); + Dprintf("typecast_PYDATE_cast: tp = %p " + "n = %d, len = " FORMAT_CODE_PY_SSIZE_T "," + " y = %d, m = %d, d = %d", + tp, n, len, y, m, d); + if (n != 3) { + PyErr_SetString(DataError, "unable to parse date"); + goto exit; + } + + if (len > 0) { + n = typecast_parse_time(tp, NULL, &len, &hh, &mm, &ss, &us, &tz); + Dprintf("typecast_PYDATETIMETZ_cast: n = %d," + " len = " FORMAT_CODE_PY_SSIZE_T "," + " hh = %d, mm = %d, ss = %d, us = %d, tz = %d", + n, len, hh, mm, ss, us, tz); + if (n < 3 || n > 6) { + PyErr_SetString(DataError, "unable to parse time"); + goto exit; + } + } + + if (ss > 59) { + mm += 1; + ss -= 60; + } + if (y > 9999) + y = 9999; + + tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory; + if (n >= 5 && tzinfo_factory != Py_None) { + /* we have a time zone, calculate minutes and create + appropriate tzinfo object calling the factory */ + Dprintf("typecast_PYDATETIMETZ_cast: UTC offset = %ds", tz); + + /* The datetime module requires that time zone offsets be + a whole number of minutes, so truncate the seconds to the + closest minute. */ + // printf("%d %d %d\n", tz, tzmin, round(tz / 60.0)); + if (!(tzinfo = PyObject_CallFunction(tzinfo_factory, "i", + (int)round(tz / 60.0)))) { + goto exit; + } + } else { + Py_INCREF(Py_None); + tzinfo = Py_None; + } + + Dprintf("typecast_PYDATETIMETZ_cast: tzinfo: %p, refcnt = " + FORMAT_CODE_PY_SSIZE_T, + tzinfo, Py_REFCNT(tzinfo)); + rv = PyObject_CallFunction( + (PyObject*)PyDateTimeAPI->DateTimeType, "iiiiiiiO", + y, m, d, hh, mm, ss, us, tzinfo); + +exit: + Py_XDECREF(tzinfo); + return rv; +} + +/** DATETIME - cast a timestamp into a datetime python object **/ + +static PyObject * +typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs) +{ if (str == NULL) { Py_RETURN_NONE; } /* check for infinity */ if (!strcmp(str, "infinity") || !strcmp(str, "-infinity")) { - if (str[0] == '-') { - obj = PyObject_GetAttrString( - (PyObject*)PyDateTimeAPI->DateTimeType, "min"); - } - else { - obj = PyObject_GetAttrString( - (PyObject*)PyDateTimeAPI->DateTimeType, "max"); - } + return PyObject_GetAttrString( + (PyObject*)PyDateTimeAPI->DateTimeType, + (str[0] == '-' ? "min" : "max")); } - else { - Dprintf("typecast_PYDATETIME_cast: s = %s", str); - n = typecast_parse_date(str, &tp, &len, &y, &m, &d); - Dprintf("typecast_PYDATE_cast: tp = %p " - "n = %d, len = " FORMAT_CODE_PY_SSIZE_T "," - " y = %d, m = %d, d = %d", - tp, n, len, y, m, d); - if (n != 3) { - PyErr_SetString(DataError, "unable to parse date"); - return NULL; - } + return _parse_noninftz(str, len, curs); +} - if (len > 0) { - n = typecast_parse_time(tp, NULL, &len, &hh, &mm, &ss, &us, &tz); - Dprintf("typecast_PYDATETIME_cast: n = %d," - " len = " FORMAT_CODE_PY_SSIZE_T "," - " hh = %d, mm = %d, ss = %d, us = %d, tz = %d", - n, len, hh, mm, ss, us, tz); - if (n < 3 || n > 6) { - PyErr_SetString(DataError, "unable to parse time"); - return NULL; - } - } +/** DATETIMETZ - cast a timestamptz into a datetime python object **/ - if (ss > 59) { - mm += 1; - ss -= 60; - } - if (y > 9999) - y = 9999; +static PyObject * +typecast_PYDATETIMETZ_cast(const char *str, Py_ssize_t len, PyObject *curs) +{ + if (str == NULL) { Py_RETURN_NONE; } - tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory; - if (n >= 5 && tzinfo_factory != Py_None) { - /* we have a time zone, calculate minutes and create - appropriate tzinfo object calling the factory */ - Dprintf("typecast_PYDATETIME_cast: UTC offset = %ds", tz); - - /* The datetime module requires that time zone offsets be - a whole number of minutes, so truncate the seconds to the - closest minute. */ - // printf("%d %d %d\n", tz, tzmin, round(tz / 60.0)); - tzinfo = PyObject_CallFunction(tzinfo_factory, "i", - (int)round(tz / 60.0)); - } else { - Py_INCREF(Py_None); - tzinfo = Py_None; - } - if (tzinfo != NULL) { - obj = PyObject_CallFunction( - (PyObject*)PyDateTimeAPI->DateTimeType, "iiiiiiiO", - y, m, d, hh, mm, ss, us, tzinfo); - Dprintf("typecast_PYDATETIME_cast: tzinfo: %p, refcnt = " - FORMAT_CODE_PY_SSIZE_T, - tzinfo, Py_REFCNT(tzinfo) - ); - Py_DECREF(tzinfo); - } + if (!strcmp(str, "infinity") || !strcmp(str, "-infinity")) { + return _parse_inftz(str, curs); } - return obj; + + return _parse_noninftz(str, len, curs); } /** TIME - parse time into a time object **/ @@ -345,4 +406,5 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) #define typecast_TIME_cast typecast_PYTIME_cast #define typecast_INTERVAL_cast typecast_PYINTERVAL_cast #define typecast_DATETIME_cast typecast_PYDATETIME_cast +#define typecast_DATETIMETZ_cast typecast_PYDATETIMETZ_cast #endif diff --git a/psycopg/typecast_mxdatetime.c b/psycopg/typecast_mxdatetime.c index 4b03d158..12c734a6 100644 --- a/psycopg/typecast_mxdatetime.c +++ b/psycopg/typecast_mxdatetime.c @@ -248,5 +248,6 @@ typecast_MXINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) #define typecast_TIME_cast typecast_MXTIME_cast #define typecast_INTERVAL_cast typecast_MXINTERVAL_cast #define typecast_DATETIME_cast typecast_MXDATE_cast +#define typecast_DATETIMETZ_cast typecast_MXDATE_cast #endif diff --git a/setup.py b/setup.py index 4912ee75..09c6f847 100644 --- a/setup.py +++ b/setup.py @@ -528,9 +528,10 @@ have_mxdatetime = False use_pydatetime = int(parser.get('build_ext', 'use_pydatetime')) # check for mx package +mxincludedir = '' if parser.has_option('build_ext', 'mx_include_dir'): mxincludedir = parser.get('build_ext', 'mx_include_dir') -else: +if not mxincludedir: mxincludedir = os.path.join(get_python_inc(plat_specific=1), "mx") if mxincludedir.strip() and os.path.exists(mxincludedir): # Build the support for mx: we will check at runtime if it can be imported diff --git a/tests/test_dates.py b/tests/test_dates.py index 98b1f555..83eea327 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -392,6 +392,25 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertRaises(OverflowError, f, '00:00:100000000000000000:00') self.assertRaises(OverflowError, f, '00:00:00.100000000000000000') + def test_adapt_infinity_tz(self): + from datetime import datetime + + t = self.execute("select 'infinity'::timestamp") + self.assert_(t.tzinfo is None) + self.assert_(t > datetime(4000, 1, 1)) + + t = self.execute("select '-infinity'::timestamp") + self.assert_(t.tzinfo is None) + self.assert_(t < datetime(1000, 1, 1)) + + t = self.execute("select 'infinity'::timestamptz") + self.assert_(t.tzinfo is not None) + self.assert_(t > datetime(4000, 1, 1, tzinfo=FixedOffsetTimezone())) + + t = self.execute("select '-infinity'::timestamptz") + self.assert_(t.tzinfo is not None) + self.assert_(t < datetime(1000, 1, 1, tzinfo=FixedOffsetTimezone())) + # Only run the datetime tests if psycopg was compiled with support. if not hasattr(psycopg2.extensions, 'PYDATETIME'):