Merge branch 'timezone-seconds'

This commit is contained in:
Daniele Varrazzo 2021-06-15 00:25:55 +01:00
commit af05c3a1ec
23 changed files with 248 additions and 1089 deletions

7
NEWS
View File

@ -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.

View File

@ -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)`
"""

View File

@ -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()

View File

@ -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`

View File

@ -230,7 +230,6 @@ If you have less standard requirements such as:
- creating a :ref:`debug build <debug-build>`,
- using :program:`pg_config` not in the :envvar:`PATH`,
- supporting ``mx.DateTime``,
then take a look at the ``setup.cfg`` file.

View File

@ -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

View File

@ -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

View File

@ -61,8 +61,6 @@ from psycopg2._psycopg import ( # noqa
__version__, __libpq_version__,
)
from psycopg2 import tz # noqa
# Register default adapters.

View File

@ -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,

View File

@ -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

View File

@ -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;
}

View File

@ -1,302 +0,0 @@
/* adapter_mxdatetime.c - mx date/time objects
*
* Copyright (C) 2003-2019 Federico Di Gregorio <fog@debian.org>
* 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 <mxDateTime.h>
#include <string.h>
/* 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);
}

View File

@ -1,70 +0,0 @@
/* adapter_mxdatetime.h - definition for the mx date/time types
*
* Copyright (C) 2003-2019 Federico Di Gregorio <fog@debian.org>
* 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) */

View File

@ -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) {

View File

@ -53,11 +53,6 @@
#include "psycopg/adapter_list.h"
#include "psycopg/typecast_binary.h"
#ifdef HAVE_MXDATETIME
#include <mxDateTime.h>
#include "psycopg/adapter_mxdatetime.h"
#endif
/* some module-level variables, like the datetime module */
#include <datetime.h>
#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; }

View File

@ -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);

View File

@ -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;
}

View File

@ -1,241 +0,0 @@
/* typecast_mxdatetime.c - date and time typecasting functions to mx types
*
* Copyright (C) 2001-2019 Federico Di Gregorio <fog@debian.org>
* 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);
}

View File

@ -58,7 +58,6 @@
<None Include="psycopg\adapter_binary.h" />
<None Include="psycopg\adapter_datetime.h" />
<None Include="psycopg\adapter_list.h" />
<None Include="psycopg\adapter_mxdatetime.h" />
<None Include="psycopg\adapter_pboolean.h" />
<None Include="psycopg\adapter_qstring.h" />
<None Include="psycopg\config.h" />
@ -166,7 +165,6 @@
<Compile Include="psycopg\adapter_binary.c" />
<Compile Include="psycopg\adapter_datetime.c" />
<Compile Include="psycopg\adapter_list.c" />
<Compile Include="psycopg\adapter_mxdatetime.c" />
<Compile Include="psycopg\adapter_pboolean.c" />
<Compile Include="psycopg\adapter_qstring.c" />
<Compile Include="psycopg\connection_int.c" />
@ -187,7 +185,6 @@
<Compile Include="psycopg\typecast_binary.c" />
<Compile Include="psycopg\typecast_builtins.c" />
<Compile Include="psycopg\typecast_datetime.c" />
<Compile Include="psycopg\typecast_mxdatetime.c" />
<Compile Include="psycopg\utils.c" />
<Compile Include="psycopg\win32_support.c" />
<Compile Include="psycopg\lobject_int.c" />

View File

@ -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.

View File

@ -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

View File

@ -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")

View File

@ -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]