Skip to content

Commit 19f6e83

Browse files
miss-islingtonserhiy-storchaka
authored andcommitted
bpo-22005: Fixed unpickling instances of datetime classes pickled by Python 2. (GH-11017) (GH-11022) (GH-11024)
encoding='latin1' should be used for successful decoding. (cherry picked from commit 8452ca1) (cherry picked from commit 0d5730e)
1 parent 1066554 commit 19f6e83

File tree

5 files changed

+328
-82
lines changed

5 files changed

+328
-82
lines changed

Doc/library/pickle.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ process more convenient:
235235
*errors* tell pickle how to decode 8-bit string instances pickled by Python
236236
2; these default to 'ASCII' and 'strict', respectively. The *encoding* can
237237
be 'bytes' to read these 8-bit string instances as bytes objects.
238+
Using ``encoding='latin1'`` is required for unpickling NumPy arrays and
239+
instances of :class:`~datetime.datetime`, :class:`~datetime.date` and
240+
:class:`~datetime.time` pickled by Python 2.
238241

239242
.. function:: loads(bytes_object, \*, fix_imports=True, encoding="ASCII", errors="strict")
240243

@@ -252,6 +255,9 @@ process more convenient:
252255
*errors* tell pickle how to decode 8-bit string instances pickled by Python
253256
2; these default to 'ASCII' and 'strict', respectively. The *encoding* can
254257
be 'bytes' to read these 8-bit string instances as bytes objects.
258+
Using ``encoding='latin1'`` is required for unpickling NumPy arrays and
259+
instances of :class:`~datetime.datetime`, :class:`~datetime.date` and
260+
:class:`~datetime.time` pickled by Python 2.
255261

256262

257263
The :mod:`pickle` module defines three exceptions:

Lib/datetime.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -693,9 +693,19 @@ def __new__(cls, year, month=None, day=None):
693693
694694
year, month, day (required, base 1)
695695
"""
696-
if month is None and isinstance(year, bytes) and len(year) == 4 and \
697-
1 <= year[2] <= 12:
696+
if (month is None and
697+
isinstance(year, (bytes, str)) and len(year) == 4 and
698+
1 <= ord(year[2:3]) <= 12):
698699
# Pickle support
700+
if isinstance(year, str):
701+
try:
702+
year = year.encode('latin1')
703+
except UnicodeEncodeError:
704+
# More informative error message.
705+
raise ValueError(
706+
"Failed to encode latin1 string when unpickling "
707+
"a date object. "
708+
"pickle.load(data, encoding='latin1') is assumed.")
699709
self = object.__new__(cls)
700710
self.__setstate(year)
701711
self._hashcode = -1
@@ -1056,8 +1066,18 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold
10561066
tzinfo (default to None)
10571067
fold (keyword only, default to zero)
10581068
"""
1059-
if isinstance(hour, bytes) and len(hour) == 6 and hour[0]&0x7F < 24:
1069+
if (isinstance(hour, (bytes, str)) and len(hour) == 6 and
1070+
ord(hour[0:1])&0x7F < 24):
10601071
# Pickle support
1072+
if isinstance(hour, str):
1073+
try:
1074+
hour = hour.encode('latin1')
1075+
except UnicodeEncodeError:
1076+
# More informative error message.
1077+
raise ValueError(
1078+
"Failed to encode latin1 string when unpickling "
1079+
"a time object. "
1080+
"pickle.load(data, encoding='latin1') is assumed.")
10611081
self = object.__new__(cls)
10621082
self.__setstate(hour, minute or None)
10631083
self._hashcode = -1
@@ -1368,8 +1388,18 @@ class datetime(date):
13681388

13691389
def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
13701390
microsecond=0, tzinfo=None, *, fold=0):
1371-
if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2]&0x7F <= 12:
1391+
if (isinstance(year, (bytes, str)) and len(year) == 10 and
1392+
1 <= ord(year[2:3])&0x7F <= 12):
13721393
# Pickle support
1394+
if isinstance(year, str):
1395+
try:
1396+
year = bytes(year, 'latin1')
1397+
except UnicodeEncodeError:
1398+
# More informative error message.
1399+
raise ValueError(
1400+
"Failed to encode latin1 string when unpickling "
1401+
"a datetime object. "
1402+
"pickle.load(data, encoding='latin1') is assumed.")
13731403
self = object.__new__(cls)
13741404
self.__setstate(year, month)
13751405
self._hashcode = -1

Lib/test/datetimetester.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import _strptime
3636
#
3737

38+
pickle_loads = {pickle.loads, pickle._loads}
3839

3940
pickle_choices = [(pickle, pickle, proto)
4041
for proto in range(pickle.HIGHEST_PROTOCOL + 1)]
@@ -1404,6 +1405,19 @@ def test_pickling(self):
14041405
self.assertEqual(orig, derived)
14051406
self.assertEqual(orig.__reduce__(), orig.__reduce_ex__(2))
14061407

1408+
def test_compat_unpickle(self):
1409+
tests = [
1410+
b"cdatetime\ndate\n(S'\\x07\\xdf\\x0b\\x1b'\ntR.",
1411+
b'cdatetime\ndate\n(U\x04\x07\xdf\x0b\x1btR.',
1412+
b'\x80\x02cdatetime\ndate\nU\x04\x07\xdf\x0b\x1b\x85R.',
1413+
]
1414+
args = 2015, 11, 27
1415+
expected = self.theclass(*args)
1416+
for data in tests:
1417+
for loads in pickle_loads:
1418+
derived = loads(data, encoding='latin1')
1419+
self.assertEqual(derived, expected)
1420+
14071421
def test_compare(self):
14081422
t1 = self.theclass(2, 3, 4)
14091423
t2 = self.theclass(2, 3, 4)
@@ -1930,6 +1944,24 @@ def test_pickling_subclass_datetime(self):
19301944
derived = unpickler.loads(green)
19311945
self.assertEqual(orig, derived)
19321946

1947+
def test_compat_unpickle(self):
1948+
tests = [
1949+
b'cdatetime\ndatetime\n('
1950+
b"S'\\x07\\xdf\\x0b\\x1b\\x14;\\x01\\x00\\x10\\x00'\ntR.",
1951+
1952+
b'cdatetime\ndatetime\n('
1953+
b'U\n\x07\xdf\x0b\x1b\x14;\x01\x00\x10\x00tR.',
1954+
1955+
b'\x80\x02cdatetime\ndatetime\n'
1956+
b'U\n\x07\xdf\x0b\x1b\x14;\x01\x00\x10\x00\x85R.',
1957+
]
1958+
args = 2015, 11, 27, 20, 59, 1, 64**2
1959+
expected = self.theclass(*args)
1960+
for data in tests:
1961+
for loads in pickle_loads:
1962+
derived = loads(data, encoding='latin1')
1963+
self.assertEqual(derived, expected)
1964+
19331965
def test_more_compare(self):
19341966
# The test_compare() inherited from TestDate covers the error cases.
19351967
# We just want to test lexicographic ordering on the members datetime
@@ -2609,6 +2641,19 @@ def test_pickling_subclass_time(self):
26092641
derived = unpickler.loads(green)
26102642
self.assertEqual(orig, derived)
26112643

2644+
def test_compat_unpickle(self):
2645+
tests = [
2646+
b"cdatetime\ntime\n(S'\\x14;\\x10\\x00\\x10\\x00'\ntR.",
2647+
b'cdatetime\ntime\n(U\x06\x14;\x10\x00\x10\x00tR.',
2648+
b'\x80\x02cdatetime\ntime\nU\x06\x14;\x10\x00\x10\x00\x85R.',
2649+
]
2650+
args = 20, 59, 16, 64**2
2651+
expected = self.theclass(*args)
2652+
for data in tests:
2653+
for loads in pickle_loads:
2654+
derived = loads(data, encoding='latin1')
2655+
self.assertEqual(derived, expected)
2656+
26122657
def test_bool(self):
26132658
# time is always True.
26142659
cls = self.theclass
@@ -2981,6 +3026,40 @@ def test_pickling(self):
29813026
self.assertEqual(derived.tzname(), 'cookie')
29823027
self.assertEqual(orig.__reduce__(), orig.__reduce_ex__(2))
29833028

3029+
def test_compat_unpickle(self):
3030+
tests = [
3031+
b"cdatetime\ntime\n(S'\\x05\\x06\\x07\\x01\\xe2@'\n"
3032+
b"ctest.datetimetester\nPicklableFixedOffset\n(tR"
3033+
b"(dS'_FixedOffset__offset'\ncdatetime\ntimedelta\n"
3034+
b"(I-1\nI68400\nI0\ntRs"
3035+
b"S'_FixedOffset__dstoffset'\nNs"
3036+
b"S'_FixedOffset__name'\nS'cookie'\nsbtR.",
3037+
3038+
b'cdatetime\ntime\n(U\x06\x05\x06\x07\x01\xe2@'
3039+
b'ctest.datetimetester\nPicklableFixedOffset\n)R'
3040+
b'}(U\x14_FixedOffset__offsetcdatetime\ntimedelta\n'
3041+
b'(J\xff\xff\xff\xffJ0\x0b\x01\x00K\x00tR'
3042+
b'U\x17_FixedOffset__dstoffsetN'
3043+
b'U\x12_FixedOffset__nameU\x06cookieubtR.',
3044+
3045+
b'\x80\x02cdatetime\ntime\nU\x06\x05\x06\x07\x01\xe2@'
3046+
b'ctest.datetimetester\nPicklableFixedOffset\n)R'
3047+
b'}(U\x14_FixedOffset__offsetcdatetime\ntimedelta\n'
3048+
b'J\xff\xff\xff\xffJ0\x0b\x01\x00K\x00\x87R'
3049+
b'U\x17_FixedOffset__dstoffsetN'
3050+
b'U\x12_FixedOffset__nameU\x06cookieub\x86R.',
3051+
]
3052+
3053+
tinfo = PicklableFixedOffset(-300, 'cookie')
3054+
expected = self.theclass(5, 6, 7, 123456, tzinfo=tinfo)
3055+
for data in tests:
3056+
for loads in pickle_loads:
3057+
derived = loads(data, encoding='latin1')
3058+
self.assertEqual(derived, expected, repr(data))
3059+
self.assertIsInstance(derived.tzinfo, PicklableFixedOffset)
3060+
self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
3061+
self.assertEqual(derived.tzname(), 'cookie')
3062+
29843063
def test_more_bool(self):
29853064
# time is always True.
29863065
cls = self.theclass
@@ -3198,6 +3277,43 @@ def test_pickling(self):
31983277
self.assertEqual(derived.tzname(), 'cookie')
31993278
self.assertEqual(orig.__reduce__(), orig.__reduce_ex__(2))
32003279

3280+
def test_compat_unpickle(self):
3281+
tests = [
3282+
b'cdatetime\ndatetime\n'
3283+
b"(S'\\x07\\xdf\\x0b\\x1b\\x14;\\x01\\x01\\xe2@'\n"
3284+
b'ctest.datetimetester\nPicklableFixedOffset\n(tR'
3285+
b"(dS'_FixedOffset__offset'\ncdatetime\ntimedelta\n"
3286+
b'(I-1\nI68400\nI0\ntRs'
3287+
b"S'_FixedOffset__dstoffset'\nNs"
3288+
b"S'_FixedOffset__name'\nS'cookie'\nsbtR.",
3289+
3290+
b'cdatetime\ndatetime\n'
3291+
b'(U\n\x07\xdf\x0b\x1b\x14;\x01\x01\xe2@'
3292+
b'ctest.datetimetester\nPicklableFixedOffset\n)R'
3293+
b'}(U\x14_FixedOffset__offsetcdatetime\ntimedelta\n'
3294+
b'(J\xff\xff\xff\xffJ0\x0b\x01\x00K\x00tR'
3295+
b'U\x17_FixedOffset__dstoffsetN'
3296+
b'U\x12_FixedOffset__nameU\x06cookieubtR.',
3297+
3298+
b'\x80\x02cdatetime\ndatetime\n'
3299+
b'U\n\x07\xdf\x0b\x1b\x14;\x01\x01\xe2@'
3300+
b'ctest.datetimetester\nPicklableFixedOffset\n)R'
3301+
b'}(U\x14_FixedOffset__offsetcdatetime\ntimedelta\n'
3302+
b'J\xff\xff\xff\xffJ0\x0b\x01\x00K\x00\x87R'
3303+
b'U\x17_FixedOffset__dstoffsetN'
3304+
b'U\x12_FixedOffset__nameU\x06cookieub\x86R.',
3305+
]
3306+
args = 2015, 11, 27, 20, 59, 1, 123456
3307+
tinfo = PicklableFixedOffset(-300, 'cookie')
3308+
expected = self.theclass(*args, **{'tzinfo': tinfo})
3309+
for data in tests:
3310+
for loads in pickle_loads:
3311+
derived = loads(data, encoding='latin1')
3312+
self.assertEqual(derived, expected)
3313+
self.assertIsInstance(derived.tzinfo, PicklableFixedOffset)
3314+
self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
3315+
self.assertEqual(derived.tzname(), 'cookie')
3316+
32013317
def test_extreme_hashes(self):
32023318
# If an attempt is made to hash these via subtracting the offset
32033319
# then hashing a datetime object, OverflowError results. The
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Implemented unpickling instances of :class:`~datetime.datetime`,
2+
:class:`~datetime.date` and :class:`~datetime.time` pickled by Python 2.
3+
``encoding='latin1'`` should be used for successful decoding.

0 commit comments

Comments
 (0)