Merge from trunk

This commit is contained in:
Federico Di Gregorio 2009-02-22 23:39:13 +01:00
commit 49bdaf92f6
8 changed files with 263 additions and 67 deletions

View File

@ -1,3 +1,38 @@
2009-02-17 James Henstridge <james@jamesh.id.au>
* psycopg/utils.c (psycopg_escape_string): same here.
* psycopg/adapter_binary.c (binary_escape): simplify PostgreSQL
version check.
* setup.py (psycopg_build_ext.finalize_options): use a single
define of the PostgreSQL version in a form that can easily be used
by #ifdefs.
* tests/test_dates.py (DatetimeTests, mxDateTimeTests): full test
coverage for datetime and time strings with and without time zone
information.
* psycopg/typecast_datetime.c (typecast_PYDATETIME_cast): adjust
to handle the changes in typecast_parse_time.
(typecast_PYTIME_cast): add support for time zone aware time
values.
* psycopg/typecast_mxdatetime.c (typecast_MXDATE_cast): make sure
that values with time zones are correctly processed (even though
that means ignoring the time zone value).
(typecast_MXTIME_cast): same here.
* psycopg/typecast.c (typecast_parse_time): Update method to parse
second resolution timezone offsets.
* psycopg/typecast.c (typecast_parse_time): Fix up handling of
negative timezone offsets with a non-zero minutes field.
* tests/test_dates.py (DatetimeTests): Add tests for time zone
parsing. The test for HH:MM:SS time zones is disabled because we
don't currently support it.
2009-02-16 Federico Di Gregorio <fog@initd.org>
* FreeBSD now has round(). Modified config.h as suggested by

View File

@ -42,9 +42,7 @@ static unsigned char *
binary_escape(unsigned char *from, size_t from_length,
size_t *to_length, PGconn *conn)
{
#if PG_MAJOR_VERSION > 8 || \
(PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION > 1) || \
(PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION == 1 && PG_PATCH_VERSION >= 4)
#if PG_VERSION_HEX >= 0x080104
if (conn)
return PQescapeByteaConn(conn, from, from_length, to_length);
else

View File

@ -96,7 +96,7 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
int* hh, int* mm, int* ss, int* us, int* tz)
{
int acc = -1, cz = 0;
int tzs = 1, tzhh = 0, tzmm = 0;
int tzsign = 1, tzhh = 0, tzmm = 0, tzss = 0;
int usd = 0;
/* sets microseconds and timezone to 0 because they may be missing */
@ -105,7 +105,7 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
Dprintf("typecast_parse_time: len = " FORMAT_CODE_PY_SSIZE_T ", s = %s",
*len, s);
while (cz < 6 && *len > 0 && *s) {
while (cz < 7 && *len > 0 && *s) {
switch (*s) {
case ':':
if (cz == 0) *hh = acc;
@ -113,6 +113,7 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
else if (cz == 2) *ss = acc;
else if (cz == 3) *us = acc;
else if (cz == 4) tzhh = acc;
else if (cz == 5) tzmm = acc;
acc = -1; cz++;
break;
case '.':
@ -125,7 +126,7 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
case '-':
/* seconds or microseconds here, anything else is an error */
if (cz < 2 || cz > 3) return -1;
if (*s == '-') tzs = -1;
if (*s == '-') tzsign = -1;
if (cz == 2) *ss = acc;
else if (cz == 3) *us = acc;
acc = -1; cz = 4;
@ -151,11 +152,12 @@ typecast_parse_time(const char* s, const char** t, Py_ssize_t* len,
else if (cz == 2) { *ss = acc; cz += 1; }
else if (cz == 3) { *us = acc; cz += 1; }
else if (cz == 4) { tzhh = acc; cz += 1; }
else if (cz == 5) tzmm = acc;
else if (cz == 5) { tzmm = acc; cz += 1; }
else if (cz == 6) tzss = acc;
}
if (t != NULL) *t = s;
*tz = tzs * tzhh*60 + tzmm;
*tz = tzsign * (3600 * tzhh + 60 * tzmm + tzss);
if (*us != 0) {
while (usd++ < 6) *us *= 10;

View File

@ -74,6 +74,8 @@ static PyObject *
typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
{
PyObject* obj = NULL;
PyObject *tzinfo = NULL;
PyObject *tzinfo_factory;
int n, y=0, m=0, d=0;
int hh=0, mm=0, ss=0, us=0, tz=0;
const char *tp = NULL;
@ -108,7 +110,7 @@ typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
" len = " FORMAT_CODE_PY_SSIZE_T ","
" hh = %d, mm = %d, ss = %d, us = %d, tz = %d",
n, len, hh, mm, ss, us, tz);
if (n < 3 || n > 5) {
if (n < 3 || n > 6) {
PyErr_SetString(DataError, "unable to parse time");
return NULL;
}
@ -121,24 +123,34 @@ typecast_PYDATETIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
if (y > 9999)
y = 9999;
if (n == 5 && ((cursorObject*)curs)->tzinfo_factory != Py_None) {
tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory;
if (n >= 5 && tzinfo_factory != Py_None) {
/* we have a time zone, calculate minutes and create
appropriate tzinfo object calling the factory */
PyObject *tzinfo;
Dprintf("typecast_PYDATETIME_cast: UTC offset = %dm", tz);
tzinfo = PyObject_CallFunction(
((cursorObject*)curs)->tzinfo_factory, "i", tz);
Dprintf("typecast_PYDATETIME_cast: UTC offset = %ds", tz);
/* The datetime module requires that time zone offsets be
a whole number of minutes, so fail if we have a time
zone with a seconds offset.
*/
if (tz % 60 != 0) {
PyErr_Format(PyExc_ValueError, "time zone offset %d is not "
"a whole number of minutes", tz);
return NULL;
}
tzinfo = PyObject_CallFunction(tzinfo_factory, "i", tz / 60);
} else {
Py_INCREF(Py_None);
tzinfo = Py_None;
}
if (tzinfo != NULL) {
obj = PyObject_CallFunction(pyDateTimeTypeP, "iiiiiiiO",
y, m, d, hh, mm, ss, us, tzinfo);
Dprintf("typecast_PYDATETIME_cast: tzinfo: %p, refcnt = "
FORMAT_CODE_PY_SSIZE_T,
tzinfo, tzinfo->ob_refcnt
);
Py_XDECREF(tzinfo);
}
else {
obj = PyObject_CallFunction(pyDateTimeTypeP, "iiiiiii",
y, m, d, hh, mm, ss, us);
Py_DECREF(tzinfo);
}
}
return obj;
@ -150,6 +162,8 @@ static PyObject *
typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
{
PyObject* obj = NULL;
PyObject *tzinfo = NULL;
PyObject *tzinfo_factory;
int n, hh=0, mm=0, ss=0, us=0, tz=0;
if (str == NULL) {Py_INCREF(Py_None); return Py_None;}
@ -159,16 +173,38 @@ typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
"hh = %d, mm = %d, ss = %d, us = %d, tz = %d",
n, len, hh, mm, ss, us, tz);
if (n < 3 || n > 5) {
if (n < 3 || n > 6) {
PyErr_SetString(DataError, "unable to parse time");
return NULL;
}
else {
if (ss > 59) {
mm += 1;
ss -= 60;
if (ss > 59) {
mm += 1;
ss -= 60;
}
tzinfo_factory = ((cursorObject *)curs)->tzinfo_factory;
if (n >= 5 && tzinfo_factory != Py_None) {
/* we have a time zone, calculate minutes and create
appropriate tzinfo object calling the factory */
Dprintf("typecast_PYTIME_cast: UTC offset = %ds", tz);
/* The datetime module requires that time zone offsets be
a whole number of minutes, so fail if we have a time
zone with a seconds offset.
*/
if (tz % 60 != 0) {
PyErr_Format(PyExc_ValueError, "time zone offset %d is not "
"a whole number of minutes", tz);
return NULL;
}
obj = PyObject_CallFunction(pyTimeTypeP, "iiii", hh, mm, ss, us);
tzinfo = PyObject_CallFunction(tzinfo_factory, "i", tz / 60);
} else {
Py_INCREF(Py_None);
tzinfo = Py_None;
}
if (tzinfo != NULL) {
obj = PyObject_CallFunction(pyTimeTypeP, "iiiiO",
hh, mm, ss, us, tzinfo);
Py_DECREF(tzinfo);
}
return obj;
}

View File

@ -63,7 +63,7 @@ typecast_MXDATE_cast(const char *str, Py_ssize_t len, PyObject *curs)
" 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 > 5)) {
if (n != 0 && (n < 3 || n > 6)) {
PyErr_SetString(DataError, "unable to parse time");
return NULL;
}
@ -91,7 +91,7 @@ typecast_MXTIME_cast(const char *str, Py_ssize_t len, PyObject *curs)
Dprintf("typecast_MXTIME_cast: hh = %d, mm = %d, ss = %d, us = %d",
hh, mm, ss, us);
if (n < 3 || n > 5) {
if (n < 3 || n > 6) {
PyErr_SetString(DataError, "unable to parse time");
return NULL;
}

View File

@ -31,9 +31,7 @@ psycopg_escape_string(PyObject *obj, const char *from, Py_ssize_t len,
#ifndef PSYCOPG_OWN_QUOTING
{
#if PG_MAJOR_VERSION > 8 || \
(PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION > 1) || \
(PG_MAJOR_VERSION == 8 && PG_MINOR_VERSION == 1 && PG_PATCH_VERSION >= 4)
#if PG_VERSION_HEX >= 0x080104
int err;
if (conn && conn->pgconn)
ql = PQescapeStringConn(conn->pgconn, to+eq+1, from, len, &err);

View File

@ -204,12 +204,11 @@ class psycopg_build_ext(build_ext):
# *at least* PostgreSQL 7.4 is available (this is the only
# 7.x series supported by psycopg 2)
pgversion = self.get_pg_config("version").split()[1]
pgmajor, pgminor, pgpatch = pgversion.split('.')
except:
pgmajor, pgminor, pgpatch = 7, 4, 0
define_macros.append(("PG_MAJOR_VERSION", pgmajor))
define_macros.append(("PG_MINOR_VERSION", pgminor))
define_macros.append(("PG_PATCH_VERSION", pgpatch))
pgversion = "7.4.0"
pgmajor, pgminor, pgpatch = pgversion.split('.')
define_macros.append(("PG_VERSION_HEX", "0x%02X%02X%02X" %
(int(pgmajor), int(pgminor), int(pgpatch))))
except Warning, w:
if self.pg_config == self.DEFAULT_PG_CONFIG:
sys.stderr.write("Warning: %s" % str(w))

View File

@ -3,49 +3,48 @@ import math
import unittest
import psycopg2
from psycopg2.tz import FixedOffsetTimezone
import tests
class CommonDatetimeTestsMixin:
def execute(self, *args):
conn = psycopg2.connect(tests.dsn)
curs = conn.cursor()
curs.execute(*args)
return curs.fetchone()[0]
self.curs.execute(*args)
return self.curs.fetchone()[0]
def test_parse_date(self):
value = self.DATE('2007-01-01', None)
value = self.DATE('2007-01-01', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.year, 2007)
self.assertEqual(value.month, 1)
self.assertEqual(value.day, 1)
def test_parse_null_date(self):
value = self.DATE(None, None)
value = self.DATE(None, self.curs)
self.assertEqual(value, None)
def test_parse_incomplete_date(self):
self.assertRaises(psycopg2.DataError, self.DATE, '2007', None)
self.assertRaises(psycopg2.DataError, self.DATE, '2007-01', None)
self.assertRaises(psycopg2.DataError, self.DATE, '2007', self.curs)
self.assertRaises(psycopg2.DataError, self.DATE, '2007-01', self.curs)
def test_parse_time(self):
value = self.TIME('13:30:29', None)
value = self.TIME('13:30:29', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.hour, 13)
self.assertEqual(value.minute, 30)
self.assertEqual(value.second, 29)
def test_parse_null_time(self):
value = self.TIME(None, None)
value = self.TIME(None, self.curs)
self.assertEqual(value, None)
def test_parse_incomplete_time(self):
self.assertRaises(psycopg2.DataError, self.TIME, '13', None)
self.assertRaises(psycopg2.DataError, self.TIME, '13:30', None)
self.assertRaises(psycopg2.DataError, self.TIME, '13', self.curs)
self.assertRaises(psycopg2.DataError, self.TIME, '13:30', self.curs)
def test_parse_datetime(self):
value = self.DATETIME('2007-01-01 13:30:29', None)
value = self.DATETIME('2007-01-01 13:30:29', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.year, 2007)
self.assertEqual(value.month, 1)
@ -55,23 +54,21 @@ class CommonDatetimeTestsMixin:
self.assertEqual(value.second, 29)
def test_parse_null_datetime(self):
value = self.DATETIME(None, None)
value = self.DATETIME(None, self.curs)
self.assertEqual(value, None)
def test_parse_incomplete_time(self):
self.assertRaises(psycopg2.DataError,
self.DATETIME, '2007', None)
self.DATETIME, '2007', self.curs)
self.assertRaises(psycopg2.DataError,
self.DATETIME, '2007-01', None)
self.DATETIME, '2007-01', self.curs)
self.assertRaises(psycopg2.DataError,
self.DATETIME, '2007-01-01 13', None)
self.DATETIME, '2007-01-01 13', self.curs)
self.assertRaises(psycopg2.DataError,
self.DATETIME, '2007-01-01 13:30', None)
self.assertRaises(psycopg2.DataError,
self.DATETIME, '2007-01-01 13:30:29+00:10:50', None)
self.DATETIME, '2007-01-01 13:30', self.curs)
def test_parse_null_interval(self):
value = self.INTERVAL(None, None)
value = self.INTERVAL(None, self.curs)
self.assertEqual(value, None)
@ -79,39 +76,137 @@ class DatetimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
"""Tests for the datetime based date handling in psycopg2."""
def setUp(self):
self.conn = psycopg2.connect(tests.dsn)
self.curs = self.conn.cursor()
self.DATE = psycopg2._psycopg.PYDATE
self.TIME = psycopg2._psycopg.PYTIME
self.DATETIME = psycopg2._psycopg.PYDATETIME
self.INTERVAL = psycopg2._psycopg.PYINTERVAL
def tearDown(self):
self.conn.close()
def test_parse_bc_date(self):
# datetime does not support BC dates
self.assertRaises(ValueError, self.DATE, '00042-01-01 BC', None)
self.assertRaises(ValueError, self.DATE, '00042-01-01 BC', self.curs)
def test_parse_bc_datetime(self):
# datetime does not support BC dates
self.assertRaises(ValueError, self.DATETIME,
'00042-01-01 13:30:29 BC', None)
'00042-01-01 13:30:29 BC', self.curs)
def test_parse_time_microseconds(self):
value = self.TIME('13:30:29.123456', None)
value = self.TIME('13:30:29.123456', self.curs)
self.assertEqual(value.second, 29)
self.assertEqual(value.microsecond, 123456)
def test_parse_datetime_microseconds(self):
value = self.DATETIME('2007-01-01 13:30:29.123456', None)
value = self.DATETIME('2007-01-01 13:30:29.123456', self.curs)
self.assertEqual(value.second, 29)
self.assertEqual(value.microsecond, 123456)
def check_time_tz(self, str_offset, offset):
from datetime import time, timedelta
base = time(13, 30, 29)
base_str = '13:30:29'
value = self.TIME(base_str + str_offset, self.curs)
# Value has time zone info and correct UTC offset.
self.assertNotEqual(value.tzinfo, None),
self.assertEqual(value.utcoffset(), timedelta(seconds=offset))
# Time portion is correct.
self.assertEqual(value.replace(tzinfo=None), base)
def test_parse_time_timezone(self):
self.check_time_tz("+01", 3600)
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, so we get an
# error here. Check that we are generating an understandable
# error message.
try:
self.check_time_tz("+01:15:42", 4542)
except ValueError, exc:
self.assertEqual(exc.message, "time zone offset 4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
try:
self.check_time_tz("-01:15:42", -4542)
except ValueError, exc:
self.assertEqual(exc.message, "time zone offset -4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
def check_datetime_tz(self, str_offset, offset):
from datetime import datetime, timedelta
base = datetime(2007, 1, 1, 13, 30, 29)
base_str = '2007-01-01 13:30:29'
value = self.DATETIME(base_str + str_offset, self.curs)
# Value has time zone info and correct UTC offset.
self.assertNotEqual(value.tzinfo, None),
self.assertEqual(value.utcoffset(), timedelta(seconds=offset))
# Datetime is correct.
self.assertEqual(value.replace(tzinfo=None), base)
# Conversion to UTC produces the expected offset.
UTC = FixedOffsetTimezone(0, "UTC")
value_utc = value.astimezone(UTC).replace(tzinfo=None)
self.assertEqual(base - value_utc, timedelta(seconds=offset))
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, so we get an
# error here. Check that we are generating an understandable
# error message.
try:
self.check_datetime_tz("+01:15:42", 4542)
except ValueError, exc:
self.assertEqual(exc.message, "time zone offset 4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
try:
self.check_datetime_tz("-01:15:42", -4542)
except ValueError, exc:
self.assertEqual(exc.message, "time zone offset -4542 is not a "
"whole number of minutes")
else:
self.fail("Expected ValueError")
def test_parse_time_no_timezone(self):
self.assertEqual(self.TIME("13:30:29", self.curs).tzinfo, None)
self.assertEqual(self.TIME("13:30:29.123456", self.curs).tzinfo, None)
def test_parse_datetime_no_timezone(self):
self.assertEqual(
self.DATETIME("2007-01-01 13:30:29", self.curs).tzinfo, None)
self.assertEqual(
self.DATETIME("2007-01-01 13:30:29.123456", self.curs).tzinfo, None)
def test_parse_interval(self):
value = self.INTERVAL('42 days 12:34:56.123456', None)
value = self.INTERVAL('42 days 12:34:56.123456', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.days, 42)
self.assertEqual(value.seconds, 45296)
self.assertEqual(value.microseconds, 123456)
def test_parse_negative_interval(self):
value = self.INTERVAL('-42 days -12:34:56.123456', None)
value = self.INTERVAL('-42 days -12:34:56.123456', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.days, -43)
self.assertEqual(value.seconds, 41103)
@ -163,13 +258,18 @@ class mxDateTimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
"""Tests for the mx.DateTime based date handling in psycopg2."""
def setUp(self):
self.conn = psycopg2.connect(tests.dsn)
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
def tearDown(self):
self.conn.close()
def test_parse_bc_date(self):
value = self.DATE('00042-01-01 BC', None)
value = self.DATE('00042-01-01 BC', self.curs)
self.assertNotEqual(value, None)
# mx.DateTime numbers BC dates from 0 rather than 1.
self.assertEqual(value.year, -41)
@ -177,7 +277,7 @@ class mxDateTimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
self.assertEqual(value.day, 1)
def test_parse_bc_datetime(self):
value = self.DATETIME('00042-01-01 13:30:29 BC', None)
value = self.DATETIME('00042-01-01 13:30:29 BC', self.curs)
self.assertNotEqual(value, None)
# mx.DateTime numbers BC dates from 0 rather than 1.
self.assertEqual(value.year, -41)
@ -188,19 +288,47 @@ class mxDateTimeTests(unittest.TestCase, CommonDatetimeTestsMixin):
self.assertEqual(value.second, 29)
def test_parse_time_microseconds(self):
value = self.TIME('13:30:29.123456', None)
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', None)
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.
from mx.DateTime import Time
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.
from mx.DateTime import DateTime
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', None)
value = self.INTERVAL('42 days 05:50:05', self.curs)
self.assertNotEqual(value, None)
self.assertEqual(value.day, 42)
self.assertEqual(value.hour, 5)