#!/usr/bin/env python # test_dates.py - unit test for dates handling # # Copyright (C) 2008-2011 James Henstridge # # 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. import math import psycopg2 from psycopg2.tz import FixedOffsetTimezone, ZERO from testutils import unittest, ConnectingTestCase class CommonDatetimeTestsMixin: def execute(self, *args): self.curs.execute(*args) return self.curs.fetchone()[0] def test_parse_date(self): value = self.DATE('2007-01-01', self.curs) self.assert_(value is not 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, self.curs) self.assertEqual(value, None) def test_parse_incomplete_date(self): 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', self.curs) self.assert_(value is not 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, self.curs) self.assertEqual(value, None) def test_parse_incomplete_time(self): 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', self.curs) self.assert_(value is not None) self.assertEqual(value.year, 2007) 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_null_datetime(self): value = self.DATETIME(None, self.curs) self.assertEqual(value, None) def test_parse_incomplete_datetime(self): self.assertRaises(psycopg2.DataError, self.DATETIME, '2007', self.curs) self.assertRaises(psycopg2.DataError, self.DATETIME, '2007-01', self.curs) self.assertRaises(psycopg2.DataError, self.DATETIME, '2007-01-01 13', self.curs) self.assertRaises(psycopg2.DataError, self.DATETIME, '2007-01-01 13:30', self.curs) def test_parse_null_interval(self): value = self.INTERVAL(None, self.curs) self.assertEqual(value, None) class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): """Tests for the datetime based date handling in psycopg2.""" def setUp(self): ConnectingTestCase.setUp(self) self.curs = self.conn.cursor() self.DATE = psycopg2.extensions.PYDATE self.TIME = psycopg2.extensions.PYTIME self.DATETIME = psycopg2.extensions.PYDATETIME self.INTERVAL = psycopg2.extensions.PYINTERVAL def test_parse_bc_date(self): # datetime does not support BC dates 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', self.curs) def test_parse_time_microseconds(self): 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', 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. # 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)) 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. # 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)) 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', 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', self.curs) self.assertNotEqual(value, None) self.assertEqual(value.days, -43) self.assertEqual(value.seconds, 41103) self.assertEqual(value.microseconds, 876544) def test_parse_infinity(self): value = self.DATETIME('-infinity', self.curs) self.assertEqual(str(value), '0001-01-01 00:00:00') value = self.DATETIME('infinity', self.curs) self.assertEqual(str(value), '9999-12-31 23:59:59.999999') value = self.DATE('infinity', self.curs) self.assertEqual(str(value), '9999-12-31') def test_adapt_date(self): from datetime import date value = self.execute('select (%s)::date::text', [date(2007, 1, 1)]) self.assertEqual(value, '2007-01-01') def test_adapt_time(self): from datetime import time value = self.execute('select (%s)::time::text', [time(13, 30, 29)]) self.assertEqual(value, '13:30:29') def test_adapt_datetime(self): from datetime import datetime value = self.execute('select (%s)::timestamp::text', [datetime(2007, 1, 1, 13, 30, 29)]) self.assertEqual(value, '2007-01-01 13:30:29') def test_adapt_timedelta(self): from datetime import timedelta value = self.execute('select extract(epoch from (%s)::interval)', [timedelta(days=42, seconds=45296, microseconds=123456)]) seconds = math.floor(value) self.assertEqual(seconds, 3674096) self.assertEqual(int(round((value - seconds) * 1000000)), 123456) def test_adapt_negative_timedelta(self): from datetime import timedelta value = self.execute('select extract(epoch from (%s)::interval)', [timedelta(days=-42, seconds=45296, microseconds=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)) return 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): from datetime import date self._test_type_roundtrip(date(2010,5,3)) def test_type_roundtrip_datetime(self): from datetime import datetime dt = self._test_type_roundtrip(datetime(2010,5,3,10,20,30)) self.assertEqual(None, dt.tzinfo) def test_type_roundtrip_datetimetz(self): from datetime import datetime import psycopg2.tz tz = psycopg2.tz.FixedOffsetTimezone(8*60) dt1 = datetime(2010,5,3,10,20,30, tzinfo=tz) dt2 = self._test_type_roundtrip(dt1) self.assertNotEqual(None, dt2.tzinfo) self.assertEqual(dt1, dt2) def test_type_roundtrip_time(self): from datetime import time tm = self._test_type_roundtrip(time(10,20,30)) self.assertEqual(None, tm.tzinfo) def test_type_roundtrip_timetz(self): from datetime import time import psycopg2.tz tz = psycopg2.tz.FixedOffsetTimezone(8*60) tm1 = time(10,20,30, tzinfo=tz) tm2 = self._test_type_roundtrip(tm1) self.assertNotEqual(None, tm2.tzinfo) self.assertEqual(tm1, tm2) def test_type_roundtrip_interval(self): from datetime import timedelta self._test_type_roundtrip(timedelta(seconds=30)) def test_type_roundtrip_date_array(self): from datetime import date self._test_type_roundtrip_array(date(2010,5,3)) def test_type_roundtrip_datetime_array(self): from datetime import datetime self._test_type_roundtrip_array(datetime(2010,5,3,10,20,30)) def test_type_roundtrip_time_array(self): from datetime import time self._test_type_roundtrip_array(time(10,20,30)) def test_type_roundtrip_interval_array(self): from datetime import timedelta self._test_type_roundtrip_array(timedelta(seconds=30)) def test_time_24(self): from datetime import time t = self.execute("select '24:00'::time;") self.assertEqual(t, time(0, 0)) t = self.execute("select '24:00+05'::timetz;") self.assertEqual(t, time(0, 0, tzinfo=FixedOffsetTimezone(300))) t = self.execute("select '24:00+05:30'::timetz;") self.assertEqual(t, time(0, 0, tzinfo=FixedOffsetTimezone(330))) # Only run the datetime tests if psycopg was compiled with support. if not hasattr(psycopg2.extensions, 'PYDATETIME'): del DatetimeTests 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. 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', 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): from mx.DateTime import Time value = self.execute('select (%s)::time::text', [Time(13, 30, 29)]) self.assertEqual(value, '13:30:29') def test_adapt_datetime(self): from mx.DateTime import DateTime 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): from mx.DateTime import DateTime 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): from mx.DateTime import DateTimeDeltaFrom 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): from mx.DateTime import DateTimeDeltaFrom 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): from mx.DateTime import Date self._test_type_roundtrip(Date(2010,5,3)) def test_type_roundtrip_datetime(self): from mx.DateTime import DateTime self._test_type_roundtrip(DateTime(2010,5,3,10,20,30)) def test_type_roundtrip_time(self): from mx.DateTime import Time self._test_type_roundtrip(Time(10,20,30)) def test_type_roundtrip_interval(self): from mx.DateTime import DateTimeDeltaFrom self._test_type_roundtrip(DateTimeDeltaFrom(seconds=30)) def test_type_roundtrip_date_array(self): from mx.DateTime import Date self._test_type_roundtrip_array(Date(2010,5,3)) def test_type_roundtrip_datetime_array(self): from mx.DateTime import DateTime self._test_type_roundtrip_array(DateTime(2010,5,3,10,20,30)) def test_type_roundtrip_time_array(self): from mx.DateTime import Time self._test_type_roundtrip_array(Time(10,20,30)) def test_type_roundtrip_interval_array(self): from mx.DateTime import DateTimeDeltaFrom self._test_type_roundtrip_array(DateTimeDeltaFrom(seconds=30)) # Only run the mx.DateTime tests if psycopg was compiled with support. try: if not hasattr(psycopg2._psycopg, 'MXDATETIME'): del mxDateTimeTests except AttributeError: del mxDateTimeTests class FromTicksTestCase(unittest.TestCase): # bug "TimestampFromTicks() throws ValueError (2-2.0.14)" # reported by Jozsef Szalay on 2010-05-06 def test_timestamp_value_error_sec_59_99(self): from datetime import datetime s = psycopg2.TimestampFromTicks(1273173119.99992) self.assertEqual(s.adapted, datetime(2010, 5, 6, 14, 11, 59, 999920, tzinfo=FixedOffsetTimezone(-5 * 60))) def test_date_value_error_sec_59_99(self): from datetime import date s = psycopg2.DateFromTicks(1273173119.99992) self.assertEqual(s.adapted, date(2010, 5, 6)) def test_time_value_error_sec_59_99(self): from datetime import time s = psycopg2.TimeFromTicks(1273173119.99992) self.assertEqual(s.adapted.replace(hour=0), time(0, 11, 59, 999920)) class FixedOffsetTimezoneTests(unittest.TestCase): def test_init_with_no_args(self): tzinfo = FixedOffsetTimezone() self.assert_(tzinfo._offset is ZERO) self.assert_(tzinfo._name is None) def test_repr_with_positive_offset(self): tzinfo = FixedOffsetTimezone(5 * 60) self.assertEqual(repr(tzinfo), "psycopg2.tz.FixedOffsetTimezone(offset=300, name=None)") def test_repr_with_negative_offset(self): tzinfo = FixedOffsetTimezone(-5 * 60) self.assertEqual(repr(tzinfo), "psycopg2.tz.FixedOffsetTimezone(offset=-300, name=None)") def test_repr_with_name(self): tzinfo = FixedOffsetTimezone(name="FOO") self.assertEqual(repr(tzinfo), "psycopg2.tz.FixedOffsetTimezone(offset=0, name='FOO')") def test_instance_caching(self): self.assert_(FixedOffsetTimezone(name="FOO") is FixedOffsetTimezone(name="FOO")) self.assert_(FixedOffsetTimezone(7 * 60) is FixedOffsetTimezone(7 * 60)) self.assert_(FixedOffsetTimezone(-9 * 60, 'FOO') is FixedOffsetTimezone(-9 * 60, 'FOO')) self.assert_(FixedOffsetTimezone(9 * 60) is not FixedOffsetTimezone(9 * 60, 'FOO')) self.assert_(FixedOffsetTimezone(name='FOO') is not FixedOffsetTimezone(9 * 60, 'FOO')) def test_pickle(self): # ticket #135 import pickle tz11 = FixedOffsetTimezone(60) tz12 = FixedOffsetTimezone(120) for proto in [-1, 0, 1, 2]: tz21, tz22 = pickle.loads(pickle.dumps([tz11, tz12], proto)) self.assertEqual(tz11, tz21) self.assertEqual(tz12, tz22) tz11 = FixedOffsetTimezone(60, name='foo') tz12 = FixedOffsetTimezone(120, name='bar') for proto in [-1, 0, 1, 2]: tz21, tz22 = pickle.loads(pickle.dumps([tz11, tz12], proto)) self.assertEqual(tz11, tz21) self.assertEqual(tz12, tz22) def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) if __name__ == "__main__": unittest.main()