diff --git a/NEWS b/NEWS index 59679271..ebb1288e 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ What's new in psycopg 2.9 - ``with connection`` starts a transaction on autocommit transactions too (:ticket:`#941`). +- Timezones with fractional minutes are supported on Python 3.7 and following + (:ticket:`#1272`). - Escape table and column names in `~cursor.copy_from()` and `~cursor.copy_to()`. - Connection exceptions with sqlstate ``08XXX`` reclassified as @@ -17,6 +19,11 @@ What's new in psycopg 2.9 Other changes: - Dropped support for Python 2.7, 3.4, 3.5 (:tickets:`#1198, #1000, #1197`). +- Dropped support for mx.DateTime. +- Use `datetime.timezone` objects by default in datetime objects instead of + `~psycopg2.tz.FixedOffsetTimezone`. +- The `psycopg2.tz` module is deprecated and scheduled to be dropped in the + next major release. - Build system for Linux/MacOS binary packages moved to GitHub action, now providing :pep:`600`\-style wheels packages. diff --git a/doc/src/conf.py b/doc/src/conf.py index 94f154e9..d565c97d 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -128,8 +128,6 @@ rst_epilog = """ .. _transaction isolation level: https://www.postgresql.org/docs/current/static/transaction-iso.html -.. _mx.DateTime: https://www.egenix.com/products/python/mxBase/mxDateTime/ - .. |MVCC| replace:: :abbr:`MVCC (Multiversion concurrency control)` """ diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst index 8398c984..2123b178 100644 --- a/doc/src/cursor.rst +++ b/doc/src/cursor.rst @@ -498,8 +498,10 @@ The ``cursor`` class The time zone factory used to handle data types such as :sql:`TIMESTAMP WITH TIME ZONE`. It should be a `~datetime.tzinfo` - object. A few implementations are available in the `psycopg2.tz` - module. + object. Default is `datetime.timezone`. + + .. versionchanged:: 2.9 + previosly the default factory was `psycopg2.tz.FixedOffsetTimezone`. .. method:: nextset() diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index d8c07a42..763910dc 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -453,13 +453,6 @@ deal with Python objects adaptation: Specialized adapters for Python datetime objects. -.. class:: DateFromMx - TimeFromMx - TimestampFromMx - IntervalFromMx - - Specialized adapters for `mx.DateTime`_ objects. - .. data:: adapters Dictionary of the currently registered object adapters. Use @@ -1004,20 +997,6 @@ from the database. See :ref:`unicode-handling` for details. Typecasters to convert time-related data types to Python `!datetime` objects. -.. data:: MXDATE - MXDATETIME - MXDATETIMETZ - MXINTERVAL - MXTIME - MXDATEARRAY - MXDATETIMEARRAY - MXDATETIMETZARRAY - MXINTERVALARRAY - MXTIMEARRAY - - Typecasters to convert time-related data types to `mx.DateTime`_ objects. - Only available if Psycopg was compiled with `!mx` support. - .. versionchanged:: 2.2 previously the `DECIMAL` typecaster and the specific time-related typecasters (`!PY*` and `!MX*`) were not exposed by the `extensions` diff --git a/doc/src/install.rst b/doc/src/install.rst index c771c9b7..2eb66c87 100644 --- a/doc/src/install.rst +++ b/doc/src/install.rst @@ -230,7 +230,6 @@ If you have less standard requirements such as: - creating a :ref:`debug build `, - using :program:`pg_config` not in the :envvar:`PATH`, -- supporting ``mx.DateTime``, then take a look at the ``setup.cfg`` file. diff --git a/doc/src/tz.rst b/doc/src/tz.rst index 06333f86..c1bd5ff7 100644 --- a/doc/src/tz.rst +++ b/doc/src/tz.rst @@ -5,6 +5,10 @@ .. module:: psycopg2.tz +.. deprecated:: 2.9 + The module will be dropped in psycopg 2.10. Use `datetime.timezone` + instead. + This module holds two different tzinfo implementations that can be used as the `tzinfo` argument to `~datetime.datetime` constructors, directly passed to Psycopg functions or used to set the `cursor.tzinfo_factory` attribute in diff --git a/doc/src/usage.rst b/doc/src/usage.rst index 3aafa903..5bb69e94 100644 --- a/doc/src/usage.rst +++ b/doc/src/usage.rst @@ -540,7 +540,6 @@ or `!memoryview` (in Python 3). single: Date objects; Adaptation single: Time objects; Adaptation single: Interval objects; Adaptation - single: mx.DateTime; Adaptation .. _adapt-date: @@ -550,8 +549,7 @@ Date/Time objects adaptation Python builtin `~datetime.datetime`, `~datetime.date`, `~datetime.time`, `~datetime.timedelta` are converted into PostgreSQL's :sql:`timestamp[tz]`, :sql:`date`, :sql:`time[tz]`, :sql:`interval` data types. -Time zones are supported too. The Egenix `mx.DateTime`_ objects are adapted -the same way:: +Time zones are supported too. >>> dt = datetime.datetime.now() >>> dt @@ -576,29 +574,39 @@ Time zones handling ''''''''''''''''''' The PostgreSQL type :sql:`timestamp with time zone` (a.k.a. -:sql:`timestamptz`) is converted into Python `~datetime.datetime` objects with -a `~datetime.datetime.tzinfo` attribute set to a -`~psycopg2.tz.FixedOffsetTimezone` instance. +:sql:`timestamptz`) is converted into Python `~datetime.datetime` objects. - >>> cur.execute("SET TIME ZONE 'Europe/Rome';") # UTC + 1 hour - >>> cur.execute("SELECT '2010-01-01 10:30:45'::timestamptz;") - >>> cur.fetchone()[0].tzinfo - psycopg2.tz.FixedOffsetTimezone(offset=60, name=None) + >>> cur.execute("SET TIME ZONE 'Europe/Rome'") # UTC + 1 hour + >>> cur.execute("SELECT '2010-01-01 10:30:45'::timestamptz") + >>> cur.fetchone()[0] + datetime.datetime(2010, 1, 1, 10, 30, 45, + tzinfo=datetime.timezone(datetime.timedelta(seconds=3600))) -Note that only time zones with an integer number of minutes are supported: -this is a limitation of the Python `datetime` module. A few historical time -zones had seconds in the UTC offset: these time zones will have the offset -rounded to the nearest minute, with an error of up to 30 seconds. +.. note:: - >>> cur.execute("SET TIME ZONE 'Asia/Calcutta';") # offset was +5:53:20 - >>> cur.execute("SELECT '1930-01-01 10:30:45'::timestamptz;") - >>> cur.fetchone()[0].tzinfo - psycopg2.tz.FixedOffsetTimezone(offset=353, name=None) + Before Python 3.7, the `datetime` module only supported timezones with an + integer number of minutes. A few historical time zones had seconds in the + UTC offset: these time zones will have the offset rounded to the nearest + minute, with an error of up to 30 seconds, on Python versions before 3.7. + + >>> cur.execute("SET TIME ZONE 'Asia/Calcutta'") # offset was +5:21:10 + >>> cur.execute("SELECT '1900-01-01 10:30:45'::timestamptz") + >>> cur.fetchone()[0].tzinfo + # On Python 3.6: 5h, 21m + datetime.timezone(datetime.timedelta(0, 19260)) + # On Python 3.7 and following: 5h, 21m, 10s + datetime.timezone(datetime.timedelta(seconds=19270)) .. versionchanged:: 2.2.2 timezones with seconds are supported (with rounding). Previously such timezones raised an error. +.. versionchanged:: 2.9 + timezones with seconds are supported without rounding. + +.. versionchanged:: 2.9 + use `datetime.timezone` as default tzinfo object instead of + `~psycopg2.tz.FixedOffsetTimezone`. .. index:: double: Date objects; Infinite diff --git a/lib/__init__.py b/lib/__init__.py index 52c82bc0..8d61892a 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -61,8 +61,6 @@ from psycopg2._psycopg import ( # noqa __version__, __libpq_version__, ) -from psycopg2 import tz # noqa - # Register default adapters. diff --git a/lib/extensions.py b/lib/extensions.py index 1de66076..f941e02a 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -42,14 +42,6 @@ from psycopg2._psycopg import ( # noqa ROWIDARRAY, STRINGARRAY, TIME, TIMEARRAY, UNICODE, UNICODEARRAY, AsIs, Binary, Boolean, Float, Int, QuotedString, ) -try: - from psycopg2._psycopg import ( # noqa - MXDATE, MXDATETIME, MXDATETIMETZ, MXINTERVAL, MXTIME, MXDATEARRAY, - MXDATETIMEARRAY, MXDATETIMETZARRAY, MXINTERVALARRAY, MXTIMEARRAY, - DateFromMx, TimeFromMx, TimestampFromMx, IntervalFromMx, ) -except ImportError: - pass - from psycopg2._psycopg import ( # noqa PYDATE, PYDATETIME, PYDATETIMETZ, PYINTERVAL, PYTIME, PYDATEARRAY, PYDATETIMEARRAY, PYDATETIMETZARRAY, PYINTERVALARRAY, PYTIMEARRAY, diff --git a/lib/tz.py b/lib/tz.py index 81cd8f8c..357aac0f 100644 --- a/lib/tz.py +++ b/lib/tz.py @@ -45,6 +45,11 @@ class FixedOffsetTimezone(datetime.tzinfo): offset and name that instance will be returned. This saves memory and improves comparability. + .. versionchanged:: 2.9 + + The constructor can take either a timedelta or a number of minutes of + offset. Previously only minutes were supported. + .. __: https://docs.python.org/library/datetime.html """ _name = None @@ -54,7 +59,9 @@ class FixedOffsetTimezone(datetime.tzinfo): def __init__(self, offset=None, name=None): if offset is not None: - self._offset = datetime.timedelta(minutes=offset) + if not isinstance(offset, datetime.timedelta): + offset = datetime.timedelta(minutes=offset) + self._offset = offset if name is not None: self._name = name @@ -70,13 +77,23 @@ class FixedOffsetTimezone(datetime.tzinfo): return tz def __repr__(self): - offset_mins = self._offset.seconds // 60 + self._offset.days * 24 * 60 return "psycopg2.tz.FixedOffsetTimezone(offset=%r, name=%r)" \ - % (offset_mins, self._name) + % (self._offset, self._name) + + def __eq__(self, other): + if isinstance(other, FixedOffsetTimezone): + return self._offset == other._offset + else: + return NotImplemented + + def __ne__(self, other): + if isinstance(other, FixedOffsetTimezone): + return self._offset != other._offset + else: + return NotImplemented def __getinitargs__(self): - offset_mins = self._offset.seconds // 60 + self._offset.days * 24 * 60 - return offset_mins, self._name + return self._offset, self._name def utcoffset(self, dt): return self._offset @@ -84,14 +101,16 @@ class FixedOffsetTimezone(datetime.tzinfo): def tzname(self, dt): if self._name is not None: return self._name - else: - seconds = self._offset.seconds + self._offset.days * 86400 - hours, seconds = divmod(seconds, 3600) - minutes = seconds / 60 - if minutes: - return "%+03d:%d" % (hours, minutes) - else: - return "%+03d" % hours + + minutes, seconds = divmod(self._offset.total_seconds(), 60) + hours, minutes = divmod(minutes, 60) + rv = "%+03d" % hours + if minutes or seconds: + rv += ":%02d" % minutes + if seconds: + rv += ":%02d" % seconds + + return rv def dst(self, dt): return ZERO diff --git a/psycopg/adapter_datetime.c b/psycopg/adapter_datetime.c index 74b55582..28a7d092 100644 --- a/psycopg/adapter_datetime.c +++ b/psycopg/adapter_datetime.c @@ -423,8 +423,8 @@ psyco_TimeFromTicks(PyObject *self, PyObject *args) PyObject * psyco_TimestampFromTicks(PyObject *self, PyObject *args) { - PyObject *m = NULL; - PyObject *tz = NULL; + pydatetimeObject *wrapper = NULL; + PyObject *dt_aware = NULL; PyObject *res = NULL; struct tm tm; time_t t; @@ -433,10 +433,6 @@ psyco_TimestampFromTicks(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "d", &ticks)) return NULL; - /* get psycopg2.tz.LOCAL from pythonland */ - if (!(m = PyImport_ImportModule("psycopg2.tz"))) { goto exit; } - if (!(tz = PyObject_GetAttrString(m, "LOCAL"))) { goto exit; } - t = (time_t)floor(ticks); ticks -= (double)t; if (!localtime_r(&t, &tm)) { @@ -444,14 +440,29 @@ psyco_TimestampFromTicks(PyObject *self, PyObject *args) goto exit; } - res = _psyco_Timestamp( - tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, - tm.tm_hour, tm.tm_min, (double)tm.tm_sec + ticks, - tz); + /* Convert the tm to a wrapper containing a naive datetime.datetime */ + if (!(wrapper = (pydatetimeObject *)_psyco_Timestamp( + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, (double)tm.tm_sec + ticks, NULL))) { + goto exit; + } + + /* Localize the datetime and assign it back to the wrapper */ + if (!(dt_aware = PyObject_CallMethod( + wrapper->wrapped, "astimezone", NULL))) { + goto exit; + } + Py_CLEAR(wrapper->wrapped); + wrapper->wrapped = dt_aware; + dt_aware = NULL; + + /* the wrapper is ready to be returned */ + res = (PyObject *)wrapper; + wrapper = NULL; exit: - Py_XDECREF(tz); - Py_XDECREF(m); + Py_XDECREF(dt_aware); + Py_XDECREF(wrapper); return res; } diff --git a/psycopg/adapter_mxdatetime.c b/psycopg/adapter_mxdatetime.c deleted file mode 100644 index 125bd14f..00000000 --- a/psycopg/adapter_mxdatetime.c +++ /dev/null @@ -1,302 +0,0 @@ -/* adapter_mxdatetime.c - mx date/time objects - * - * Copyright (C) 2003-2019 Federico Di Gregorio - * Copyright (C) 2020 The Psycopg Team - * - * 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/adapter_mxdatetime.h" -#include "psycopg/microprotocols_proto.h" - -#include -#include - - -/* Return 0 on success, -1 on failure, but don't set an exception */ - -int -psyco_adapter_mxdatetime_init(void) -{ - if (mxDateTime_ImportModuleAndAPI()) { - Dprintf("psyco_adapter_mxdatetime_init: mx.DateTime initialization failed"); - PyErr_Clear(); - return -1; - } - return 0; -} - - -/* mxdatetime_str, mxdatetime_getquoted - return result of quoting */ - -static PyObject * -mxdatetime_str(mxdatetimeObject *self) -{ - mxDateTimeObject *dt; - mxDateTimeDeltaObject *dtd; - char buf[128] = { 0, }; - - switch (self->type) { - - case PSYCO_MXDATETIME_DATE: - dt = (mxDateTimeObject *)self->wrapped; - if (dt->year >= 1) - PyOS_snprintf(buf, sizeof(buf) - 1, "'%04ld-%02d-%02d'::date", - dt->year, (int)dt->month, (int)dt->day); - else - PyOS_snprintf(buf, sizeof(buf) - 1, "'%04ld-%02d-%02d BC'::date", - 1 - dt->year, (int)dt->month, (int)dt->day); - break; - - case PSYCO_MXDATETIME_TIMESTAMP: - dt = (mxDateTimeObject *)self->wrapped; - if (dt->year >= 1) - PyOS_snprintf(buf, sizeof(buf) - 1, - "'%04ld-%02d-%02dT%02d:%02d:%09.6f'::timestamp", - dt->year, (int)dt->month, (int)dt->day, - (int)dt->hour, (int)dt->minute, dt->second); - else - PyOS_snprintf(buf, sizeof(buf) - 1, - "'%04ld-%02d-%02dT%02d:%02d:%09.6f BC'::timestamp", - 1 - dt->year, (int)dt->month, (int)dt->day, - (int)dt->hour, (int)dt->minute, dt->second); - break; - - case PSYCO_MXDATETIME_TIME: - case PSYCO_MXDATETIME_INTERVAL: - /* given the limitation of the mx.DateTime module that uses the same - type for both time and delta values we need to do some black magic - and make sure we're not using an adapt()ed interval as a simple - time */ - dtd = (mxDateTimeDeltaObject *)self->wrapped; - if (0 <= dtd->seconds && dtd->seconds < 24*3600) { - PyOS_snprintf(buf, sizeof(buf) - 1, "'%02d:%02d:%09.6f'::time", - (int)dtd->hour, (int)dtd->minute, dtd->second); - } else { - double ss = dtd->hour*3600.0 + dtd->minute*60.0 + dtd->second; - - if (dtd->seconds >= 0) - PyOS_snprintf(buf, sizeof(buf) - 1, "'%ld days %.6f seconds'::interval", - dtd->day, ss); - else - PyOS_snprintf(buf, sizeof(buf) - 1, "'-%ld days -%.6f seconds'::interval", - dtd->day, ss); - } - break; - } - - return PyString_FromString(buf); -} - -static PyObject * -mxdatetime_getquoted(mxdatetimeObject *self, PyObject *args) -{ - return mxdatetime_str(self); -} - -static PyObject * -mxdatetime_conform(mxdatetimeObject *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 MxDateTime object **/ - -/* object member list */ - -static struct PyMemberDef mxdatetimeObject_members[] = { - {"adapted", T_OBJECT, offsetof(mxdatetimeObject, wrapped), READONLY}, - {"type", T_INT, offsetof(mxdatetimeObject, type), READONLY}, - {NULL} -}; - -/* object method table */ - -static PyMethodDef mxdatetimeObject_methods[] = { - {"getquoted", (PyCFunction)mxdatetime_getquoted, METH_NOARGS, - "getquoted() -> wrapped object value as SQL date/time"}, - {"__conform__", (PyCFunction)mxdatetime_conform, METH_VARARGS, NULL}, - {NULL} /* Sentinel */ -}; - -/* initialization and finalization methods */ - -static int -mxdatetime_setup(mxdatetimeObject *self, PyObject *obj, int type) -{ - Dprintf("mxdatetime_setup: init mxdatetime object at %p, refcnt = " - FORMAT_CODE_PY_SSIZE_T, - self, Py_REFCNT(self) - ); - - self->type = type; - Py_INCREF(obj); - self->wrapped = obj; - - Dprintf("mxdatetime_setup: good mxdatetime object at %p, refcnt = " - FORMAT_CODE_PY_SSIZE_T, - self, Py_REFCNT(self) - ); - return 0; -} - -static void -mxdatetime_dealloc(PyObject* obj) -{ - mxdatetimeObject *self = (mxdatetimeObject *)obj; - - Py_CLEAR(self->wrapped); - - Dprintf("mxdatetime_dealloc: deleted mxdatetime object at %p, refcnt = " - FORMAT_CODE_PY_SSIZE_T, - obj, Py_REFCNT(obj) - ); - - Py_TYPE(obj)->tp_free(obj); -} - -static int -mxdatetime_init(PyObject *obj, PyObject *args, PyObject *kwds) -{ - PyObject *mx; - int type = -1; /* raise an error if type was not passed! */ - - if (!PyArg_ParseTuple(args, "O|i", &mx, &type)) - return -1; - - return mxdatetime_setup((mxdatetimeObject *)obj, mx, type); -} - -static PyObject * -mxdatetime_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - return type->tp_alloc(type, 0); -} - - -/* object type */ - -#define mxdatetimeType_doc \ -"MxDateTime(mx, type) -> new mx.DateTime wrapper object" - -PyTypeObject mxdatetimeType = { - PyVarObject_HEAD_INIT(NULL, 0) - "psycopg2._psycopg.MxDateTime", - sizeof(mxdatetimeObject), 0, - mxdatetime_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*/ - (reprfunc)mxdatetime_str, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/ - mxdatetimeType_doc, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - mxdatetimeObject_methods, /*tp_methods*/ - mxdatetimeObject_members, /*tp_members*/ - 0, /*tp_getset*/ - 0, /*tp_base*/ - 0, /*tp_dict*/ - 0, /*tp_descr_get*/ - 0, /*tp_descr_set*/ - 0, /*tp_dictoffset*/ - mxdatetime_init, /*tp_init*/ - 0, /*tp_alloc*/ - mxdatetime_new, /*tp_new*/ -}; - - -/** module-level functions **/ - -PyObject * -psyco_DateFromMx(PyObject *self, PyObject *args) -{ - PyObject *mx; - - if (!PyArg_ParseTuple(args, "O!", mxDateTime.DateTime_Type, &mx)) - return NULL; - - return PyObject_CallFunction((PyObject *)&mxdatetimeType, "Oi", mx, - PSYCO_MXDATETIME_DATE); -} - -PyObject * -psyco_TimeFromMx(PyObject *self, PyObject *args) -{ - PyObject *mx; - - if (!PyArg_ParseTuple(args, "O!", mxDateTime.DateTimeDelta_Type, &mx)) - return NULL; - - return PyObject_CallFunction((PyObject *)&mxdatetimeType, "Oi", mx, - PSYCO_MXDATETIME_TIME); -} - -PyObject * -psyco_TimestampFromMx(PyObject *self, PyObject *args) -{ - PyObject *mx; - - if (!PyArg_ParseTuple(args, "O!", mxDateTime.DateTime_Type, &mx)) - return NULL; - - return PyObject_CallFunction((PyObject *)&mxdatetimeType, "Oi", mx, - PSYCO_MXDATETIME_TIMESTAMP); -} - -PyObject * -psyco_IntervalFromMx(PyObject *self, PyObject *args) -{ - PyObject *mx; - - if (!PyArg_ParseTuple(args, "O!", mxDateTime.DateTime_Type, &mx)) - return NULL; - - return PyObject_CallFunction((PyObject *)&mxdatetimeType, "Oi", mx, - PSYCO_MXDATETIME_INTERVAL); -} diff --git a/psycopg/adapter_mxdatetime.h b/psycopg/adapter_mxdatetime.h deleted file mode 100644 index 93d59322..00000000 --- a/psycopg/adapter_mxdatetime.h +++ /dev/null @@ -1,70 +0,0 @@ -/* adapter_mxdatetime.h - definition for the mx date/time types - * - * Copyright (C) 2003-2019 Federico Di Gregorio - * Copyright (C) 2020 The Psycopg Team - * - * 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_MXDATETIME_H -#define PSYCOPG_MXDATETIME_H 1 - -#ifdef __cplusplus -extern "C" { -#endif - -extern HIDDEN PyTypeObject mxdatetimeType; - -typedef struct { - PyObject_HEAD - - PyObject *wrapped; - int type; -#define PSYCO_MXDATETIME_TIME 0 -#define PSYCO_MXDATETIME_DATE 1 -#define PSYCO_MXDATETIME_TIMESTAMP 2 -#define PSYCO_MXDATETIME_INTERVAL 3 - -} mxdatetimeObject; - -HIDDEN int psyco_adapter_mxdatetime_init(void); - -HIDDEN PyObject *psyco_DateFromMx(PyObject *module, PyObject *args); -#define psyco_DateFromMx_doc \ - "DateFromMx(mx) -> new date" - -HIDDEN PyObject *psyco_TimeFromMx(PyObject *module, PyObject *args); -#define psyco_TimeFromMx_doc \ - "TimeFromMx(mx) -> new time" - -HIDDEN PyObject *psyco_TimestampFromMx(PyObject *module, PyObject *args); -#define psyco_TimestampFromMx_doc \ - "TimestampFromMx(mx) -> new timestamp" - -HIDDEN PyObject *psyco_IntervalFromMx(PyObject *module, PyObject *args); -#define psyco_IntervalFromMx_doc \ - "IntervalFromMx(mx) -> new interval" - -#ifdef __cplusplus -} -#endif - -#endif /* !defined(PSYCOPG_MXDATETIME_H) */ diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index 31ce7924..abab40c0 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -1951,10 +1951,11 @@ cursor_setup(cursorObject *self, connectionObject *conn, const char *name) /* default tzinfo factory */ { + /* The datetime api doesn't seem to have a constructor to make a + * datetime.timezone, so use the Python interface. */ PyObject *m = NULL; - if ((m = PyImport_ImportModule("psycopg2.tz"))) { - self->tzinfo_factory = PyObject_GetAttrString( - m, "FixedOffsetTimezone"); + if ((m = PyImport_ImportModule("datetime"))) { + self->tzinfo_factory = PyObject_GetAttrString(m, "timezone"); Py_DECREF(m); } if (!self->tzinfo_factory) { diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index acb67810..57c56ee1 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -53,11 +53,6 @@ #include "psycopg/adapter_list.h" #include "psycopg/typecast_binary.h" -#ifdef HAVE_MXDATETIME -#include -#include "psycopg/adapter_mxdatetime.h" -#endif - /* some module-level variables, like the datetime module */ #include #include "psycopg/adapter_datetime.h" @@ -358,30 +353,6 @@ adapters_init(PyObject *module) if (0 > microprotocols_add(PyDateTimeAPI->DeltaType, NULL, obj)) { goto exit; } Py_CLEAR(obj); -#ifdef HAVE_MXDATETIME - /* As above, we use the callable objects from the psycopg module. - These object are not be available at runtime if mx.DateTime import - failed (e.g. it was available at build time but not at runtime). */ - if (PyMapping_HasKeyString(dict, "TimestampFromMx")) { - if (!(obj = PyMapping_GetItemString(dict, "TimestampFromMx"))) { - goto exit; - } - if (0 > microprotocols_add(mxDateTime.DateTime_Type, NULL, obj)) { - goto exit; - } - Py_CLEAR(obj); - - /* if we found the above, we have this too. */ - if (!(obj = PyMapping_GetItemString(dict, "TimeFromMx"))) { - goto exit; - } - if (0 > microprotocols_add(mxDateTime.DateTimeDelta_Type, NULL, obj)) { - goto exit; - } - Py_CLEAR(obj); - } -#endif - /* Success! */ rv = 0; @@ -943,34 +914,6 @@ datetime_init(void) return 0; } -RAISES_NEG static int -mxdatetime_init(PyObject *module) -{ - Dprintf("psycopgmodule: initializing mx.DateTime module"); - -#ifdef HAVE_MXDATETIME - Py_SET_TYPE(&mxdatetimeType, &PyType_Type); - if (0 > PyType_Ready(&mxdatetimeType)) { return -1; } - - if (mxDateTime_ImportModuleAndAPI()) { - Dprintf("psycopgmodule: mx.DateTime module import failed"); - PyErr_Clear(); - } - - /* If we can't find mx.DateTime objects at runtime, - * remove them from the module (and, as consequence, from the adapters). */ - if (0 != psyco_adapter_mxdatetime_init()) { - PyObject *dict; - if (!(dict = PyModule_GetDict(module))) { return -1; } - if (0 > PyDict_DelItemString(dict, "DateFromMx")) { return -1; } - if (0 > PyDict_DelItemString(dict, "TimeFromMx")) { return -1; } - if (0 > PyDict_DelItemString(dict, "TimestampFromMx")) { return -1; } - if (0 > PyDict_DelItemString(dict, "IntervalFromMx")) { return -1; } - } -#endif - return 0; -} - /** method table and module initialization **/ static PyMethodDef psycopgMethods[] = { @@ -1014,18 +957,6 @@ static PyMethodDef psycopgMethods[] = { {"IntervalFromPy", (PyCFunction)psyco_IntervalFromPy, METH_VARARGS, psyco_IntervalFromPy_doc}, -#ifdef HAVE_MXDATETIME - /* to be deleted if not found at import time */ - {"DateFromMx", (PyCFunction)psyco_DateFromMx, - METH_VARARGS, psyco_DateFromMx_doc}, - {"TimeFromMx", (PyCFunction)psyco_TimeFromMx, - METH_VARARGS, psyco_TimeFromMx_doc}, - {"TimestampFromMx", (PyCFunction)psyco_TimestampFromMx, - METH_VARARGS, psyco_TimestampFromMx_doc}, - {"IntervalFromMx", (PyCFunction)psyco_IntervalFromMx, - METH_VARARGS, psyco_IntervalFromMx_doc}, -#endif - {"set_wait_callback", (PyCFunction)psyco_set_wait_callback, METH_O, psyco_set_wait_callback_doc}, {"get_wait_callback", (PyCFunction)psyco_get_wait_callback, @@ -1086,7 +1017,6 @@ INIT_MODULE(_psycopg)(void) if (0 > add_module_constants(module)) { goto exit; } if (0 > add_module_types(module)) { goto exit; } if (0 > datetime_init()) { goto exit; } - if (0 > mxdatetime_init(module)) { goto exit; } if (0 > encodings_init(module)) { goto exit; } if (0 > typecast_init(module)) { goto exit; } if (0 > adapters_init(module)) { goto exit; } diff --git a/psycopg/typecast.c b/psycopg/typecast.c index 4f713b16..3ac51e79 100644 --- a/psycopg/typecast.c +++ b/psycopg/typecast.c @@ -32,15 +32,6 @@ /* useful function used by some typecasters */ -#ifdef HAVE_MXDATETIME -static const char * -skip_until_space(const char *s) -{ - while (*s && *s != ' ') s++; - return s; -} -#endif - static const char * skip_until_space2(const char *s, Py_ssize_t *len) { @@ -82,11 +73,11 @@ typecast_parse_date(const char* s, const char** t, Py_ssize_t* len, cz += 1; } - /* Is this a BC date? If so, adjust the year value. Note that - * mx.DateTime numbers BC dates from zero rather than one. The - * Python datetime module does not support BC dates at all. */ + /* Is this a BC date? If so, adjust the year value. However + * Python datetime module does not support BC dates, so this will raise + * an exception downstream. */ if (*len >= 2 && s[*len-2] == 'B' && s[*len-1] == 'C') - *year = 1 - (*year); + *year = -(*year); if (t != NULL) *t = s; @@ -175,11 +166,6 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len, #include "psycopg/typecast_basic.c" #include "psycopg/typecast_binary.c" #include "psycopg/typecast_datetime.c" - -#ifdef HAVE_MXDATETIME -#include "psycopg/typecast_mxdatetime.c" -#endif - #include "psycopg/typecast_array.c" static long int typecast_default_DEFAULT[] = {0}; @@ -218,29 +204,6 @@ static typecastObject_initlist typecast_pydatetime[] = { {NULL, NULL, NULL} }; -#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 - -/* 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"}, - {NULL, NULL, NULL} -}; -#endif - /** the type dictionary and associated functions **/ @@ -291,18 +254,6 @@ typecast_init(PyObject *module) psyco_default_cast = typecast_from_c(&typecast_default, dict); /* register the date/time typecasters with their original names */ -#ifdef HAVE_MXDATETIME - if (0 == typecast_mxdatetime_init()) { - for (i = 0; typecast_mxdatetime[i].name != NULL; i++) { - t = (typecastObject *)typecast_from_c(&(typecast_mxdatetime[i]), dict); - if (t == NULL) { goto exit; } - PyDict_SetItem(dict, t->name, (PyObject *)t); - Py_DECREF((PyObject *)t); - t = NULL; - } - } -#endif - if (0 > typecast_datetime_init()) { goto exit; } for (i = 0; typecast_pydatetime[i].name != NULL; i++) { t = (typecastObject *)typecast_from_c(&(typecast_pydatetime[i]), dict); diff --git a/psycopg/typecast_datetime.c b/psycopg/typecast_datetime.c index 095fce17..121a2282 100644 --- a/psycopg/typecast_datetime.c +++ b/psycopg/typecast_datetime.c @@ -104,9 +104,18 @@ _parse_inftz(const char *str, PyObject *curs) goto exit; } - if (!(tzinfo = PyObject_CallFunction(tzinfo_factory, "i", 0))) { - goto exit; +#if PY_VERSION_HEX < 0x03070000 + { + PyObject *tzoff; + if (!(tzoff = PyDelta_FromDSU(0, 0, 0))) { goto exit; } + tzinfo = PyObject_CallFunctionObjArgs(tzinfo_factory, tzoff, NULL); + Py_DECREF(tzoff); + if (!tzinfo) { goto exit; } } +#else + tzinfo = PyDateTime_TimeZone_UTC; + Py_INCREF(tzinfo); +#endif /* m.replace(tzinfo=tzinfo) */ if (!(args = PyTuple_New(0))) { goto exit; } @@ -129,10 +138,11 @@ static PyObject * _parse_noninftz(const char *str, Py_ssize_t len, PyObject *curs) { PyObject* rv = NULL; + PyObject *tzoff = 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; + int hh=0, mm=0, ss=0, us=0, tzsec=0; const char *tp = NULL; Dprintf("typecast_PYDATETIMETZ_cast: s = %s", str); @@ -147,11 +157,11 @@ _parse_noninftz(const char *str, Py_ssize_t len, PyObject *curs) } if (len > 0) { - n = typecast_parse_time(tp, NULL, &len, &hh, &mm, &ss, &us, &tz); + n = typecast_parse_time(tp, NULL, &len, &hh, &mm, &ss, &us, &tzsec); 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); + " hh = %d, mm = %d, ss = %d, us = %d, tzsec = %d", + n, len, hh, mm, ss, us, tzsec); if (n < 3 || n > 6) { PyErr_SetString(DataError, "unable to parse time"); goto exit; @@ -169,17 +179,20 @@ _parse_noninftz(const char *str, Py_ssize_t len, PyObject *curs) 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); + Dprintf("typecast_PYDATETIMETZ_cast: UTC offset = %ds", tzsec); - /* 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)))) { +#if PY_VERSION_HEX < 0x03070000 + /* Before Python 3.7 the timezone offset had to be a whole number + * of minutes, so round the seconds to the closest minute */ + tzsec = 60 * (int)round(tzsec / 60.0); +#endif + if (!(tzoff = PyDelta_FromDSU(0, tzsec, 0))) { goto exit; } + if (!(tzinfo = PyObject_CallFunctionObjArgs( + tzinfo_factory, tzoff, NULL))) { goto exit; } - } else { + } + else { Py_INCREF(Py_None); tzinfo = Py_None; } @@ -192,6 +205,7 @@ _parse_noninftz(const char *str, Py_ssize_t len, PyObject *curs) y, m, d, hh, mm, ss, us, tzinfo); exit: + Py_XDECREF(tzoff); Py_XDECREF(tzinfo); return rv; } @@ -232,17 +246,18 @@ typecast_PYDATETIMETZ_cast(const char *str, Py_ssize_t len, PyObject *curs) static PyObject * typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs) { - PyObject* obj = NULL; + PyObject* rv = NULL; + PyObject *tzoff = NULL; PyObject *tzinfo = NULL; PyObject *tzinfo_factory; - int n, hh=0, mm=0, ss=0, us=0, tz=0; + int n, hh=0, mm=0, ss=0, us=0, tzsec=0; if (str == NULL) { Py_RETURN_NONE; } - n = typecast_parse_time(str, NULL, &len, &hh, &mm, &ss, &us, &tz); + n = typecast_parse_time(str, NULL, &len, &hh, &mm, &ss, &us, &tzsec); Dprintf("typecast_PYTIME_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); + "hh = %d, mm = %d, ss = %d, us = %d, tzsec = %d", + n, len, hh, mm, ss, us, tzsec); if (n < 3 || n > 6) { PyErr_SetString(DataError, "unable to parse time"); @@ -254,25 +269,32 @@ typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs) } tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory; if (n >= 5 && tzinfo_factory != Py_None) { - /* we have a time zone, calculate minutes and create + /* we have a time zone, calculate seconds and create appropriate tzinfo object calling the factory */ - Dprintf("typecast_PYTIME_cast: UTC offset = %ds", tz); + Dprintf("typecast_PYTIME_cast: UTC offset = %ds", tzsec); - /* The datetime module requires that time zone offsets be - a whole number of minutes, so truncate the seconds to the - closest minute. */ - tzinfo = PyObject_CallFunction(tzinfo_factory, "i", - (int)round(tz / 60.0)); - } else { +#if PY_VERSION_HEX < 0x03070000 + /* Before Python 3.7 the timezone offset had to be a whole number + * of minutes, so round the seconds to the closest minute */ + tzsec = 60 * (int)round(tzsec / 60.0); +#endif + if (!(tzoff = PyDelta_FromDSU(0, tzsec, 0))) { goto exit; } + if (!(tzinfo = PyObject_CallFunctionObjArgs(tzinfo_factory, tzoff, NULL))) { + goto exit; + } + } + else { Py_INCREF(Py_None); tzinfo = Py_None; } - if (tzinfo != NULL) { - obj = PyObject_CallFunction((PyObject*)PyDateTimeAPI->TimeType, "iiiiO", - hh, mm, ss, us, tzinfo); - Py_DECREF(tzinfo); - } - return obj; + + rv = PyObject_CallFunction((PyObject*)PyDateTimeAPI->TimeType, "iiiiO", + hh, mm, ss, us, tzinfo); + +exit: + Py_XDECREF(tzoff); + Py_XDECREF(tzinfo); + return rv; } diff --git a/psycopg/typecast_mxdatetime.c b/psycopg/typecast_mxdatetime.c deleted file mode 100644 index d308f136..00000000 --- a/psycopg/typecast_mxdatetime.c +++ /dev/null @@ -1,241 +0,0 @@ -/* typecast_mxdatetime.c - date and time typecasting functions to mx types - * - * Copyright (C) 2001-2019 Federico Di Gregorio - * Copyright (C) 2020 The Psycopg Team - * - * 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. - */ - -#include "mxDateTime.h" - - -/* Return 0 on success, -1 on failure, but don't set an exception */ - -static int -typecast_mxdatetime_init(void) -{ - if (mxDateTime_ImportModuleAndAPI()) { - Dprintf("typecast_mxdatetime_init: mx.DateTime initialization failed"); - PyErr_Clear(); - return -1; - } - return 0; -} - - -/** DATE - cast a date into mx.DateTime python object **/ - -static PyObject * -typecast_MXDATE_cast(const char *str, Py_ssize_t len, PyObject *curs) -{ - int n, y=0, m=0, d=0; - int hh=0, mm=0, ss=0, us=0, tz=0; - const char *tp = NULL; - - if (str == NULL) { Py_RETURN_NONE; } - - Dprintf("typecast_MXDATE_cast: s = %s", str); - - /* check for infinity */ - if (!strcmp(str, "infinity") || !strcmp(str, "-infinity")) { - if (str[0] == '-') { - return mxDateTime.DateTime_FromDateAndTime(-999998,1,1, 0,0,0); - } - else { - return mxDateTime.DateTime_FromDateAndTime(999999,12,31, 0,0,0); - } - } - - n = typecast_parse_date(str, &tp, &len, &y, &m, &d); - Dprintf("typecast_MXDATE_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; - } - - if (len > 0) { - n = typecast_parse_time(tp, NULL, &len, &hh, &mm, &ss, &us, &tz); - Dprintf("typecast_MXDATE_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 != 0 && (n < 3 || n > 6)) { - PyErr_SetString(DataError, "unable to parse time"); - return NULL; - } - } - - Dprintf("typecast_MXDATE_cast: fractionary seconds: %lf", - (double)ss + (double)us/(double)1000000.0); - return mxDateTime.DateTime_FromDateAndTime(y, m, d, hh, mm, - (double)ss + (double)us/(double)1000000.0); -} - -/** TIME - parse time into an mx.DateTime object **/ - -static PyObject * -typecast_MXTIME_cast(const char *str, Py_ssize_t len, PyObject *curs) -{ - int n, hh=0, mm=0, ss=0, us=0, tz=0; - - if (str == NULL) { Py_RETURN_NONE; } - - Dprintf("typecast_MXTIME_cast: s = %s", str); - - n = typecast_parse_time(str, NULL, &len, &hh, &mm, &ss, &us, &tz); - Dprintf("typecast_MXTIME_cast: time parsed, %d components", n); - Dprintf("typecast_MXTIME_cast: hh = %d, mm = %d, ss = %d, us = %d", - hh, mm, ss, us); - - if (n < 3 || n > 6) { - PyErr_SetString(DataError, "unable to parse time"); - return NULL; - } - - Dprintf("typecast_MXTIME_cast: fractionary seconds: %lf", - (double)ss + (double)us/(double)1000000.0); - return mxDateTime.DateTimeDelta_FromTime(hh, mm, - (double)ss + (double)us/(double)1000000.0); -} - -/** INTERVAL - parse an interval into an mx.DateTimeDelta **/ - -static PyObject * -typecast_MXINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) -{ - long years = 0, months = 0, days = 0, denominator = 1; - double hours = 0.0, minutes = 0.0, seconds = 0.0, hundredths = 0.0; - double v = 0.0, sign = 1.0; - int part = 0; - - if (str == NULL) { Py_RETURN_NONE; } - - Dprintf("typecast_MXINTERVAL_cast: s = %s", str); - - while (*str) { - switch (*str) { - - case '-': - sign = -1.0; - break; - - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - v = v * 10.0 + (double)(*str - '0'); - Dprintf("typecast_MXINTERVAL_cast: v = %f", v); - if (part == 6){ - denominator *= 10; - Dprintf("typecast_MXINTERVAL_cast: denominator = %ld", - denominator); - } - break; - - case 'y': - if (part == 0) { - years = (long)(v*sign); - str = skip_until_space(str); - Dprintf("typecast_MXINTERVAL_cast: years = %ld, rest = %s", - years, str); - v = 0.0; sign = 1.0; part = 1; - } - break; - - case 'm': - if (part <= 1) { - months = (long)(v*sign); - str = skip_until_space(str); - Dprintf("typecast_MXINTERVAL_cast: months = %ld, rest = %s", - months, str); - v = 0.0; sign = 1.0; part = 2; - } - break; - - case 'd': - if (part <= 2) { - days = (long)(v*sign); - str = skip_until_space(str); - Dprintf("typecast_MXINTERVAL_cast: days = %ld, rest = %s", - days, str); - v = 0.0; sign = 1.0; part = 3; - } - break; - - case ':': - if (part <= 3) { - hours = v; - Dprintf("typecast_MXINTERVAL_cast: hours = %f", hours); - v = 0.0; part = 4; - } - else if (part == 4) { - minutes = v; - Dprintf("typecast_MXINTERVAL_cast: minutes = %f", minutes); - v = 0.0; part = 5; - } - break; - - case '.': - if (part == 5) { - seconds = v; - Dprintf("typecast_MXINTERVAL_cast: seconds = %f", seconds); - v = 0.0; part = 6; - } - break; - - default: - break; - } - - str++; - } - - /* manage last value, be it minutes or seconds or hundredths of a second */ - if (part == 4) { - minutes = v; - Dprintf("typecast_MXINTERVAL_cast: minutes = %f", minutes); - } - else if (part == 5) { - seconds = v; - Dprintf("typecast_MXINTERVAL_cast: seconds = %f", seconds); - } - else if (part == 6) { - hundredths = v; - Dprintf("typecast_MXINTERVAL_cast: hundredths = %f", hundredths); - hundredths = hundredths/denominator; - Dprintf("typecast_MXINTERVAL_cast: fractions = %.20f", hundredths); - } - - /* calculates seconds */ - if (sign < 0.0) { - seconds = - (hundredths + seconds + minutes*60 + hours*3600); - } - else { - seconds += hundredths + minutes*60 + hours*3600; - } - - /* calculates days */ - days += years*365 + months*30; - - Dprintf("typecast_MXINTERVAL_cast: days = %ld, seconds = %f", - days, seconds); - return mxDateTime.DateTimeDelta_FromDaysAndSeconds(days, seconds); -} diff --git a/psycopg2.cproj b/psycopg2.cproj index af752d22..4c232785 100644 --- a/psycopg2.cproj +++ b/psycopg2.cproj @@ -58,7 +58,6 @@ - @@ -166,7 +165,6 @@ - @@ -187,7 +185,6 @@ - diff --git a/setup.cfg b/setup.cfg index 43c61d16..04328554 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,16 +1,12 @@ [build_ext] # PSYCOPG_DEBUG can be added to enable verbose debug information -define= +define=PSYCOPG_DEBUG # "pg_config" is required to locate PostgreSQL headers and libraries needed to # build psycopg2. If pg_config is not in the path or is installed under a # different name set the following option to the pg_config full path. pg_config= -# If the build system does not find the mx.DateTime headers, try -# setting its value to the right path. -mx_include_dir= - # For Windows only: # Set to 1 if the PostgreSQL library was built with OpenSSL. # Required to link in OpenSSL libraries and dependencies. diff --git a/setup.py b/setup.py index 2cdb7c2c..a844db14 100644 --- a/setup.py +++ b/setup.py @@ -230,7 +230,6 @@ class psycopg_build_ext(build_ext): def initialize_options(self): build_ext.initialize_options(self) self.pgdir = None - self.mx_include_dir = None self.have_ssl = have_ssl self.static_libpq = static_libpq self.pg_config = None @@ -497,24 +496,6 @@ depends = [ parser = configparser.ConfigParser() parser.read('setup.cfg') -# check for mx package -mxincludedir = '' -if parser.has_option('build_ext', 'mx_include_dir'): - mxincludedir = parser.get('build_ext', 'mx_include_dir') -if not mxincludedir: - # look for mxDateTime.h; prefer one located in venv - candidate_dirs = [os.path.join(d, 'mx', 'DateTime', 'mxDateTime') for d in sys.path] \ - + [os.path.join(get_python_inc(plat_specific=1), "mx")] - candidate_dirs = [d for d in candidate_dirs if os.path.exists(os.path.join(d, 'mxDateTime.h'))] or [''] - mxincludedir = candidate_dirs[0] -if mxincludedir.strip() and os.path.exists(mxincludedir): - # Build the support for mx: we will check at runtime if it can be imported - include_dirs.append(mxincludedir) - define_macros.append(('HAVE_MXDATETIME', '1')) - sources.append('adapter_mxdatetime.c') - depends.extend(['adapter_mxdatetime.h', 'typecast_mxdatetime.c']) - version_flags.append('mx') - # generate a nice version string to avoid confusion when users report bugs version_flags.append('pq3') # no more a choice version_flags.append('ext') # no more a choice diff --git a/tests/test_dates.py b/tests/test_dates.py index 29c37b09..d4d2e7fe 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -23,21 +23,16 @@ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. +import sys import math import pickle -from datetime import date, datetime, time, timedelta +from datetime import date, datetime, time, timedelta, timezone import psycopg2 from psycopg2.tz import FixedOffsetTimezone, ZERO import unittest from .testutils import ConnectingTestCase, skip_before_postgres, skip_if_crdb -try: - from mx.DateTime import Date, Time, DateTime, DateTimeDeltaFrom -except ImportError: - # Tests will be skipped - pass - def total_seconds(d): """Return total number of seconds of a timedelta as a float.""" @@ -157,17 +152,27 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.check_time_tz("-01", -3600) self.check_time_tz("+01:15", 4500) self.check_time_tz("-01:15", -4500) - # The Python datetime module does not support time zone - # offsets that are not a whole number of minutes. - # We round the offset to the nearest minute. - self.check_time_tz("+01:15:00", 60 * (60 + 15)) - self.check_time_tz("+01:15:29", 60 * (60 + 15)) - self.check_time_tz("+01:15:30", 60 * (60 + 16)) - self.check_time_tz("+01:15:59", 60 * (60 + 16)) - self.check_time_tz("-01:15:00", -60 * (60 + 15)) - self.check_time_tz("-01:15:29", -60 * (60 + 15)) - self.check_time_tz("-01:15:30", -60 * (60 + 16)) - self.check_time_tz("-01:15:59", -60 * (60 + 16)) + if sys.version_info < (3, 7): + # The Python < 3.7 datetime module does not support time zone + # offsets that are not a whole number of minutes. + # We round the offset to the nearest minute. + self.check_time_tz("+01:15:00", 60 * (60 + 15)) + self.check_time_tz("+01:15:29", 60 * (60 + 15)) + self.check_time_tz("+01:15:30", 60 * (60 + 16)) + self.check_time_tz("+01:15:59", 60 * (60 + 16)) + self.check_time_tz("-01:15:00", -60 * (60 + 15)) + self.check_time_tz("-01:15:29", -60 * (60 + 15)) + self.check_time_tz("-01:15:30", -60 * (60 + 16)) + self.check_time_tz("-01:15:59", -60 * (60 + 16)) + else: + self.check_time_tz("+01:15:00", 60 * (60 + 15)) + self.check_time_tz("+01:15:29", 60 * (60 + 15) + 29) + self.check_time_tz("+01:15:30", 60 * (60 + 15) + 30) + self.check_time_tz("+01:15:59", 60 * (60 + 15) + 59) + self.check_time_tz("-01:15:00", -(60 * (60 + 15))) + self.check_time_tz("-01:15:29", -(60 * (60 + 15) + 29)) + self.check_time_tz("-01:15:30", -(60 * (60 + 15) + 30)) + self.check_time_tz("-01:15:59", -(60 * (60 + 15) + 59)) def check_datetime_tz(self, str_offset, offset): base = datetime(2007, 1, 1, 13, 30, 29) @@ -183,26 +188,52 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertEqual(value.replace(tzinfo=None), base) # Conversion to UTC produces the expected offset. - UTC = FixedOffsetTimezone(0, "UTC") + UTC = timezone(timedelta(0)) value_utc = value.astimezone(UTC).replace(tzinfo=None) self.assertEqual(base - value_utc, timedelta(seconds=offset)) + def test_default_tzinfo(self): + self.curs.execute("select '2000-01-01 00:00+02:00'::timestamptz") + dt = self.curs.fetchone()[0] + self.assert_(isinstance(dt.tzinfo, timezone)) + self.assertEqual(dt, + datetime(2000, 1, 1, tzinfo=timezone(timedelta(minutes=120)))) + + def test_fotz_tzinfo(self): + self.curs.tzinfo_factory = FixedOffsetTimezone + self.curs.execute("select '2000-01-01 00:00+02:00'::timestamptz") + dt = self.curs.fetchone()[0] + self.assert_(not isinstance(dt.tzinfo, timezone)) + self.assert_(isinstance(dt.tzinfo, FixedOffsetTimezone)) + self.assertEqual(dt, + datetime(2000, 1, 1, tzinfo=timezone(timedelta(minutes=120)))) + def test_parse_datetime_timezone(self): self.check_datetime_tz("+01", 3600) self.check_datetime_tz("-01", -3600) self.check_datetime_tz("+01:15", 4500) self.check_datetime_tz("-01:15", -4500) - # The Python datetime module does not support time zone - # offsets that are not a whole number of minutes. - # We round the offset to the nearest minute. - self.check_datetime_tz("+01:15:00", 60 * (60 + 15)) - self.check_datetime_tz("+01:15:29", 60 * (60 + 15)) - self.check_datetime_tz("+01:15:30", 60 * (60 + 16)) - self.check_datetime_tz("+01:15:59", 60 * (60 + 16)) - self.check_datetime_tz("-01:15:00", -60 * (60 + 15)) - self.check_datetime_tz("-01:15:29", -60 * (60 + 15)) - self.check_datetime_tz("-01:15:30", -60 * (60 + 16)) - self.check_datetime_tz("-01:15:59", -60 * (60 + 16)) + if sys.version_info < (3, 7): + # The Python < 3.7 datetime module does not support time zone + # offsets that are not a whole number of minutes. + # We round the offset to the nearest minute. + self.check_datetime_tz("+01:15:00", 60 * (60 + 15)) + self.check_datetime_tz("+01:15:29", 60 * (60 + 15)) + self.check_datetime_tz("+01:15:30", 60 * (60 + 16)) + self.check_datetime_tz("+01:15:59", 60 * (60 + 16)) + self.check_datetime_tz("-01:15:00", -60 * (60 + 15)) + self.check_datetime_tz("-01:15:29", -60 * (60 + 15)) + self.check_datetime_tz("-01:15:30", -60 * (60 + 16)) + self.check_datetime_tz("-01:15:59", -60 * (60 + 16)) + else: + self.check_datetime_tz("+01:15:00", 60 * (60 + 15)) + self.check_datetime_tz("+01:15:29", 60 * (60 + 15) + 29) + self.check_datetime_tz("+01:15:30", 60 * (60 + 15) + 30) + self.check_datetime_tz("+01:15:59", 60 * (60 + 15) + 59) + self.check_datetime_tz("-01:15:00", -(60 * (60 + 15))) + self.check_datetime_tz("-01:15:29", -(60 * (60 + 15) + 29)) + self.check_datetime_tz("-01:15:30", -(60 * (60 + 15) + 30)) + self.check_datetime_tz("-01:15:59", -(60 * (60 + 15) + 59)) def test_parse_time_no_timezone(self): self.assertEqual(self.TIME("13:30:29", self.curs).tzinfo, None) @@ -286,7 +317,7 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertEqual(None, dt.tzinfo) def test_type_roundtrip_datetimetz(self): - tz = FixedOffsetTimezone(8 * 60) + tz = timezone(timedelta(minutes=8 * 60)) dt1 = datetime(2010, 5, 3, 10, 20, 30, tzinfo=tz) dt2 = self._test_type_roundtrip(dt1) self.assertNotEqual(None, dt2.tzinfo) @@ -297,7 +328,7 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertEqual(None, tm.tzinfo) def test_type_roundtrip_timetz(self): - tz = FixedOffsetTimezone(8 * 60) + tz = timezone(timedelta(minutes=8 * 60)) tm1 = time(10, 20, 30, tzinfo=tz) tm2 = self._test_type_roundtrip(tm1) self.assertNotEqual(None, tm2.tzinfo) @@ -314,7 +345,7 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): def test_type_roundtrip_datetimetz_array(self): self._test_type_roundtrip_array( - datetime(2010, 5, 3, 10, 20, 30, tzinfo=FixedOffsetTimezone(0))) + datetime(2010, 5, 3, 10, 20, 30, tzinfo=timezone(timedelta(0)))) def test_type_roundtrip_time_array(self): self._test_type_roundtrip_array(time(10, 20, 30)) @@ -328,10 +359,10 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertEqual(t, time(0, 0)) t = self.execute("select '24:00+05'::timetz;") - self.assertEqual(t, time(0, 0, tzinfo=FixedOffsetTimezone(300))) + self.assertEqual(t, time(0, 0, tzinfo=timezone(timedelta(minutes=300)))) t = self.execute("select '24:00+05:30'::timetz;") - self.assertEqual(t, time(0, 0, tzinfo=FixedOffsetTimezone(330))) + self.assertEqual(t, time(0, 0, tzinfo=timezone(timedelta(minutes=330)))) @skip_before_postgres(8, 1) def test_large_interval(self): @@ -399,11 +430,11 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): t = self.execute("select 'infinity'::timestamptz") self.assert_(t.tzinfo is not None) - self.assert_(t > datetime(4000, 1, 1, tzinfo=FixedOffsetTimezone())) + self.assert_(t > datetime(4000, 1, 1, tzinfo=timezone(timedelta(0)))) t = self.execute("select '-infinity'::timestamptz") self.assert_(t.tzinfo is not None) - self.assert_(t < datetime(1000, 1, 1, tzinfo=FixedOffsetTimezone())) + self.assert_(t < datetime(1000, 1, 1, tzinfo=timezone(timedelta(0)))) def test_redshift_day(self): # Redshift is reported returning 1 day interval as microsec (bug #558) @@ -435,169 +466,6 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assertRaises(psycopg2.NotSupportedError, cur.fetchone) -@unittest.skipUnless( - hasattr(psycopg2._psycopg, 'MXDATETIME'), - 'Requires mx.DateTime support' -) -class mxDateTimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): - """Tests for the mx.DateTime based date handling in psycopg2.""" - - def setUp(self): - ConnectingTestCase.setUp(self) - self.curs = self.conn.cursor() - self.DATE = psycopg2._psycopg.MXDATE - self.TIME = psycopg2._psycopg.MXTIME - self.DATETIME = psycopg2._psycopg.MXDATETIME - self.INTERVAL = psycopg2._psycopg.MXINTERVAL - - psycopg2.extensions.register_type(self.DATE, self.conn) - psycopg2.extensions.register_type(self.TIME, self.conn) - psycopg2.extensions.register_type(self.DATETIME, self.conn) - psycopg2.extensions.register_type(self.INTERVAL, self.conn) - psycopg2.extensions.register_type(psycopg2.extensions.MXDATEARRAY, self.conn) - psycopg2.extensions.register_type(psycopg2.extensions.MXTIMEARRAY, self.conn) - psycopg2.extensions.register_type( - psycopg2.extensions.MXDATETIMEARRAY, self.conn) - psycopg2.extensions.register_type( - psycopg2.extensions.MXINTERVALARRAY, self.conn) - - def tearDown(self): - self.conn.close() - - def test_parse_bc_date(self): - value = self.DATE('00042-01-01 BC', self.curs) - self.assert_(value is not None) - # mx.DateTime numbers BC dates from 0 rather than 1. - self.assertEqual(value.year, -41) - self.assertEqual(value.month, 1) - self.assertEqual(value.day, 1) - - def test_parse_bc_datetime(self): - value = self.DATETIME('00042-01-01 13:30:29 BC', self.curs) - self.assert_(value is not None) - # mx.DateTime numbers BC dates from 0 rather than 1. - self.assertEqual(value.year, -41) - self.assertEqual(value.month, 1) - self.assertEqual(value.day, 1) - self.assertEqual(value.hour, 13) - self.assertEqual(value.minute, 30) - self.assertEqual(value.second, 29) - - def test_parse_time_microseconds(self): - value = self.TIME('13:30:29.123456', self.curs) - self.assertEqual(math.floor(value.second), 29) - self.assertEqual( - int((value.second - math.floor(value.second)) * 1000000), 123456) - - def test_parse_datetime_microseconds(self): - value = self.DATETIME('2007-01-01 13:30:29.123456', self.curs) - self.assertEqual(math.floor(value.second), 29) - self.assertEqual( - int((value.second - math.floor(value.second)) * 1000000), 123456) - - def test_parse_time_timezone(self): - # Time zone information is ignored. - expected = Time(13, 30, 29) - self.assertEqual(expected, self.TIME("13:30:29+01", self.curs)) - self.assertEqual(expected, self.TIME("13:30:29-01", self.curs)) - self.assertEqual(expected, self.TIME("13:30:29+01:15", self.curs)) - self.assertEqual(expected, self.TIME("13:30:29-01:15", self.curs)) - self.assertEqual(expected, self.TIME("13:30:29+01:15:42", self.curs)) - self.assertEqual(expected, self.TIME("13:30:29-01:15:42", self.curs)) - - def test_parse_datetime_timezone(self): - # Time zone information is ignored. - expected = DateTime(2007, 1, 1, 13, 30, 29) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29+01", self.curs)) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29-01", self.curs)) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29+01:15", self.curs)) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29-01:15", self.curs)) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29+01:15:42", self.curs)) - self.assertEqual( - expected, self.DATETIME("2007-01-01 13:30:29-01:15:42", self.curs)) - - def test_parse_interval(self): - value = self.INTERVAL('42 days 05:50:05', self.curs) - self.assert_(value is not None) - self.assertEqual(value.day, 42) - self.assertEqual(value.hour, 5) - self.assertEqual(value.minute, 50) - self.assertEqual(value.second, 5) - - def test_adapt_time(self): - value = self.execute('select (%s)::time::text', - [Time(13, 30, 29)]) - self.assertEqual(value, '13:30:29') - - def test_adapt_datetime(self): - value = self.execute('select (%s)::timestamp::text', - [DateTime(2007, 1, 1, 13, 30, 29.123456)]) - self.assertEqual(value, '2007-01-01 13:30:29.123456') - - def test_adapt_bc_datetime(self): - value = self.execute('select (%s)::timestamp::text', - [DateTime(-41, 1, 1, 13, 30, 29.123456)]) - # microsecs for BC timestamps look not available in PG < 8.4 - # but more likely it's determined at compile time. - self.assert_(value in ( - '0042-01-01 13:30:29.123456 BC', - '0042-01-01 13:30:29 BC'), value) - - def test_adapt_timedelta(self): - value = self.execute('select extract(epoch from (%s)::interval)', - [DateTimeDeltaFrom(days=42, - seconds=45296.123456)]) - seconds = math.floor(value) - self.assertEqual(seconds, 3674096) - self.assertEqual(int(round((value - seconds) * 1000000)), 123456) - - def test_adapt_negative_timedelta(self): - value = self.execute('select extract(epoch from (%s)::interval)', - [DateTimeDeltaFrom(days=-42, - seconds=45296.123456)]) - seconds = math.floor(value) - self.assertEqual(seconds, -3583504) - self.assertEqual(int(round((value - seconds) * 1000000)), 123456) - - def _test_type_roundtrip(self, o1): - o2 = self.execute("select %s;", (o1,)) - self.assertEqual(type(o1), type(o2)) - - def _test_type_roundtrip_array(self, o1): - o1 = [o1] - o2 = self.execute("select %s;", (o1,)) - self.assertEqual(type(o1[0]), type(o2[0])) - - def test_type_roundtrip_date(self): - self._test_type_roundtrip(Date(2010, 5, 3)) - - def test_type_roundtrip_datetime(self): - self._test_type_roundtrip(DateTime(2010, 5, 3, 10, 20, 30)) - - def test_type_roundtrip_time(self): - self._test_type_roundtrip(Time(10, 20, 30)) - - def test_type_roundtrip_interval(self): - self._test_type_roundtrip(DateTimeDeltaFrom(seconds=30)) - - def test_type_roundtrip_date_array(self): - self._test_type_roundtrip_array(Date(2010, 5, 3)) - - def test_type_roundtrip_datetime_array(self): - self._test_type_roundtrip_array(DateTime(2010, 5, 3, 10, 20, 30)) - - def test_type_roundtrip_time_array(self): - self._test_type_roundtrip_array(Time(10, 20, 30)) - - def test_type_roundtrip_interval_array(self): - self._test_type_roundtrip_array(DateTimeDeltaFrom(seconds=30)) - - class FromTicksTestCase(unittest.TestCase): # bug "TimestampFromTicks() throws ValueError (2-2.0.14)" # reported by Jozsef Szalay on 2010-05-06 @@ -605,7 +473,7 @@ class FromTicksTestCase(unittest.TestCase): s = psycopg2.TimestampFromTicks(1273173119.99992) self.assertEqual(s.adapted, datetime(2010, 5, 6, 14, 11, 59, 999920, - tzinfo=FixedOffsetTimezone(-5 * 60))) + tzinfo=timezone(timedelta(minutes=-5 * 60)))) def test_date_value_error_sec_59_99(self): s = psycopg2.DateFromTicks(1273173119.99992) @@ -628,17 +496,27 @@ class FixedOffsetTimezoneTests(unittest.TestCase): def test_repr_with_positive_offset(self): tzinfo = FixedOffsetTimezone(5 * 60) self.assertEqual(repr(tzinfo), - "psycopg2.tz.FixedOffsetTimezone(offset=300, name=None)") + "psycopg2.tz.FixedOffsetTimezone(offset=%r, name=None)" + % timedelta(minutes=5 * 60)) def test_repr_with_negative_offset(self): tzinfo = FixedOffsetTimezone(-5 * 60) self.assertEqual(repr(tzinfo), - "psycopg2.tz.FixedOffsetTimezone(offset=-300, name=None)") + "psycopg2.tz.FixedOffsetTimezone(offset=%r, name=None)" + % timedelta(minutes=-5 * 60)) + + def test_init_with_timedelta(self): + td = timedelta(minutes=5 * 60) + tzinfo = FixedOffsetTimezone(td) + self.assertEqual(tzinfo, FixedOffsetTimezone(5 * 60)) + self.assertEqual(repr(tzinfo), + "psycopg2.tz.FixedOffsetTimezone(offset=%r, name=None)" % td) def test_repr_with_name(self): tzinfo = FixedOffsetTimezone(name="FOO") self.assertEqual(repr(tzinfo), - "psycopg2.tz.FixedOffsetTimezone(offset=0, name='FOO')") + "psycopg2.tz.FixedOffsetTimezone(offset=%r, name='FOO')" + % timedelta(0)) def test_instance_caching(self): self.assert_(FixedOffsetTimezone(name="FOO") diff --git a/tests/test_types_extras.py b/tests/test_types_extras.py index c6530200..26500eb9 100755 --- a/tests/test_types_extras.py +++ b/tests/test_types_extras.py @@ -20,7 +20,7 @@ import json import uuid import warnings from decimal import Decimal -from datetime import date, datetime +from datetime import date, datetime, timedelta, timezone from functools import wraps from pickle import dumps, loads @@ -38,7 +38,6 @@ from psycopg2.extras import ( Inet, Json, NumericRange, Range, RealDictConnection, register_composite, register_hstore, register_range, ) -from psycopg2.tz import FixedOffsetTimezone class TypesExtrasTests(ConnectingTestCase): @@ -1282,7 +1281,7 @@ class RangeTestCase(unittest.TestCase): Date-Time ranges should return a human-readable string as well on string conversion. ''' - tz = FixedOffsetTimezone(-5 * 60, "EST") + tz = timezone(timedelta(minutes=-5 * 60), "EST") r = DateTimeTZRange(datetime(2010, 1, 1, tzinfo=tz), datetime(2011, 1, 1, tzinfo=tz)) expected = '[2010-01-01 00:00:00-05:00, 2011-01-01 00:00:00-05:00)' @@ -1377,9 +1376,9 @@ class RangeCasterTestCase(ConnectingTestCase): def test_cast_timestamptz(self): cur = self.conn.cursor() - ts1 = datetime(2000, 1, 1, tzinfo=FixedOffsetTimezone(600)) + ts1 = datetime(2000, 1, 1, tzinfo=timezone(timedelta(minutes=600))) ts2 = datetime(2000, 12, 31, 23, 59, 59, 999, - tzinfo=FixedOffsetTimezone(600)) + tzinfo=timezone(timedelta(minutes=600))) cur.execute("select tstzrange(%s, %s, '[]')", (ts1, ts2)) r = cur.fetchone()[0] self.assert_(isinstance(r, DateTimeTZRange)) @@ -1465,9 +1464,9 @@ class RangeCasterTestCase(ConnectingTestCase): self.assert_(isinstance(r1, DateTimeRange)) self.assert_(r1.isempty) - ts1 = datetime(2000, 1, 1, tzinfo=FixedOffsetTimezone(600)) + ts1 = datetime(2000, 1, 1, tzinfo=timezone(timedelta(minutes=600))) ts2 = datetime(2000, 12, 31, 23, 59, 59, 999, - tzinfo=FixedOffsetTimezone(600)) + tzinfo=timezone(timedelta(minutes=600))) r = DateTimeTZRange(ts1, ts2, '(]') cur.execute("select %s", (r,)) r1 = cur.fetchone()[0]