Skip to content

Commit e8cc627

Browse files
committed
Allow passing datetime.date instances as timestamp input
Fixes: #288
1 parent 65c0caa commit e8cc627

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

asyncpg/protocol/codecs/datetime.pyx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
66

77

8+
cimport cpython.datetime
89
import datetime
910

11+
cpython.datetime.import_datetime()
12+
1013
utc = datetime.timezone.utc
1114
date_from_ordinal = datetime.date.fromordinal
1215
timedelta = datetime.timedelta
@@ -56,6 +59,11 @@ cdef int32_t negative_infinity_date_ord = <int32_t>cpython.PyLong_AsLong(
5659
negative_infinity_date.toordinal())
5760

5861

62+
cdef inline _local_timezone():
63+
d = datetime.datetime.now(datetime.timezone.utc).astimezone()
64+
return datetime.timezone(d.utcoffset())
65+
66+
5967
cdef inline _encode_time(WriteBuffer buf, int64_t seconds,
6068
int32_t microseconds):
6169
# XXX: add support for double timestamps
@@ -135,6 +143,15 @@ cdef date_decode_tuple(ConnectionSettings settings, FastReadBuffer buf):
135143

136144

137145
cdef timestamp_encode(ConnectionSettings settings, WriteBuffer buf, obj):
146+
if not cpython.datetime.PyDateTime_Check(obj):
147+
if cpython.datetime.PyDate_Check(obj):
148+
obj = datetime.datetime(obj.year, obj.month, obj.day)
149+
else:
150+
raise TypeError(
151+
'expected a datetime.date or datetime.datetime instance, '
152+
'got {!r}'.format(type(obj).__name__)
153+
)
154+
138155
delta = obj - pg_epoch_datetime
139156
cdef:
140157
int64_t seconds = cpython.PyLong_AsLongLong(delta.days) * 86400 + \
@@ -186,6 +203,16 @@ cdef timestamp_decode_tuple(ConnectionSettings settings, FastReadBuffer buf):
186203

187204

188205
cdef timestamptz_encode(ConnectionSettings settings, WriteBuffer buf, obj):
206+
if not cpython.datetime.PyDateTime_Check(obj):
207+
if cpython.datetime.PyDate_Check(obj):
208+
obj = datetime.datetime(obj.year, obj.month, obj.day,
209+
tzinfo=_local_timezone())
210+
else:
211+
raise TypeError(
212+
'expected a datetime.date or datetime.datetime instance, '
213+
'got {!r}'.format(type(obj).__name__)
214+
)
215+
189216
buf.write_int32(8)
190217

191218
if obj == infinity_datetime:
@@ -195,7 +222,14 @@ cdef timestamptz_encode(ConnectionSettings settings, WriteBuffer buf, obj):
195222
buf.write_int64(pg_time64_negative_infinity)
196223
return
197224

198-
delta = obj.astimezone(utc) - pg_epoch_datetime_utc
225+
try:
226+
utc_dt = obj.astimezone(utc)
227+
except ValueError:
228+
# Python 3.5 doesn't like it when we call astimezone()
229+
# on naive datetime objects, so make it aware.
230+
utc_dt = obj.replace(tzinfo=_local_timezone()).astimezone(utc)
231+
232+
delta = utc_dt - pg_epoch_datetime_utc
199233
cdef:
200234
int64_t seconds = cpython.PyLong_AsLongLong(delta.days) * 86400 + \
201235
cpython.PyLong_AsLong(delta.seconds)

tests/test_codecs.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ def _timezone(offset):
2525
return datetime.timezone(datetime.timedelta(minutes=minutes))
2626

2727

28+
def _system_timezone():
29+
d = datetime.datetime.now(datetime.timezone.utc).astimezone()
30+
return datetime.timezone(d.utcoffset())
31+
32+
2833
infinity_datetime = datetime.datetime(
2934
datetime.MAXYEAR, 12, 31, 23, 59, 59, 999999)
3035
negative_infinity_datetime = datetime.datetime(
3136
datetime.MINYEAR, 1, 1, 0, 0, 0, 0)
3237

3338
infinity_date = datetime.date(datetime.MAXYEAR, 12, 31)
3439
negative_infinity_date = datetime.date(datetime.MINYEAR, 1, 1)
40+
current_timezone = _system_timezone()
41+
current_date = datetime.date.today()
42+
current_datetime = datetime.datetime.now()
3543

3644

3745
type_samples = [
@@ -160,6 +168,8 @@ def _timezone(offset):
160168
negative_infinity_datetime,
161169
{'textinput': 'infinity', 'output': infinity_datetime},
162170
{'textinput': '-infinity', 'output': negative_infinity_datetime},
171+
{'input': datetime.date(2000, 1, 1),
172+
'output': datetime.datetime(2000, 1, 1)}
163173
]),
164174
('date', 'date', [
165175
datetime.date(3000, 5, 20),
@@ -185,6 +195,16 @@ def _timezone(offset):
185195
datetime.datetime(2400, 1, 1, 10, 10, 0, tzinfo=_timezone(2000)),
186196
infinity_datetime,
187197
negative_infinity_datetime,
198+
{
199+
'input': current_date,
200+
'output': datetime.datetime(
201+
year=current_date.year, month=current_date.month,
202+
day=current_date.day, tzinfo=current_timezone),
203+
},
204+
{
205+
'input': current_datetime,
206+
'output': current_datetime.replace(tzinfo=current_timezone),
207+
}
188208
]),
189209
('timetz', 'timetz', [
190210
# timetz retains the offset
@@ -657,6 +677,12 @@ async def test_invalid_input(self):
657677
2 ** 32,
658678
-1,
659679
]),
680+
('timestamp', r"expected a datetime\.date.*got 'str'", [
681+
'foo'
682+
]),
683+
('timestamptz', r"expected a datetime\.date.*got 'str'", [
684+
'foo'
685+
]),
660686
]
661687

662688
for typname, errmsg, data in cases:

0 commit comments

Comments
 (0)