Skip to content

Commit

Permalink
Merge pull request #7 from zopefoundation/issue4
Browse files Browse the repository at this point in the history
Remove fallback heuristic for guessing timezone name in large values
  • Loading branch information
jamadden committed Aug 10, 2017
2 parents f988d00 + b0267f1 commit fff8043
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 72 deletions.
15 changes: 15 additions & 0 deletions CHANGES.rst
Expand Up @@ -5,6 +5,21 @@
4.2.0 (unreleased)
==================

- Remove support for guessing the timezone name when a timestamp
exceeds the value supported by Python's ``localtime`` function. On
platforms with a 32-bit ``time_t``, this would involve parsed values
that do not specify a timezone and are past the year 2038. Now the
underlying exception will be propagated. Previously an undocumented
heuristic was used. This is not expected to be a common issue;
Windows, as one example, always uses a 64-bit ``time_t``, even on
32-bit platforms. See
https://github.com/zopefoundation/zope.datetime/issues/4

- Use true division on Python 2 to match Python 3, in case certain
parameters turn out to be integers instead of floating point values.
This is not expected to be user-visible, but it can arise in
artificial tests of internal functions.

- Add support for Python 3.5 and 3.6.

- Drop support for Python 2.6, 3.2 and 3.3.
Expand Down
59 changes: 13 additions & 46 deletions src/zope/datetime/__init__.py
Expand Up @@ -15,6 +15,8 @@
Encapsulation of date/time values
"""
from __future__ import division # We do lots of math, make sure it's consistent

import math
import re
# there is a method definition that makes just "time"
Expand Down Expand Up @@ -422,17 +424,6 @@ def _calcHMS(x, ms):
sc = x - mn * 60 + ms
return hr, mn, sc

def _calcYMDHMS(x, ms):
# x is a timezone-dependent integer of seconds.
# Produces yr,mo,dy,hr,mn,sc.
yr, mo, dy = _calendarday(x / 86400 + jd1901)
x = int(x - (x / 86400) * 86400)
hr = x / 3600
x = x - hr * 3600
mn = x / 60
sc = x - mn * 60 + ms
return yr, mo, dy, hr, mn, sc

def _julianday(y, m, d):
if m > 12:
y = y + m // 12
Expand All @@ -453,19 +444,6 @@ def _julianday(y, m, d):
b = 0
return (1461 * y - yr_correct) // 4 + 306001 * (m + 1) // 10000 + d + 1720994 + b

def _calendarday(j):
if j < 2299160:
b = j + 1525
else:
a = (4 * j - 7468861) / 146097
b = j + 1526 + a - a / 4
c = (20 * b - 2442) / 7305
d = 1461 * c / 4
e = 10000 * (b - d) / 306001
dy = int(b - d - 306001 * e / 10000)
mo = int(e - 1) if e < 14 else int(e - 13)
yr = (c - 4716) if mo > 2 else (c - 4715)
return int(yr), int(mo), int(dy)

def _tzoffset(tz, t):
try:
Expand All @@ -492,20 +470,20 @@ def _correctYear(year):
def safegmtime(t):
'''gmtime with a safety zone.'''
try:
t_int = int(t)
except OverflowError:
raise TimeError('The time %f is beyond the range '
'of this Python implementation.' % float(t))
return _time.gmtime(t_int)
return _time.gmtime(t)
except (ValueError, OverflowError): # Py2/Py3 respectively
raise TimeError('The time %r is beyond the range '
'of this Python implementation.' % t)


def safelocaltime(t):
'''localtime with a safety zone.'''
try:
t_int = int(t)
except OverflowError:
raise TimeError('The time %f is beyond the range '
'of this Python implementation.' % float(t))
return _time.localtime(t_int)
return _time.localtime(t)
except (ValueError, OverflowError): # Py2/Py3 respectively
raise TimeError('The time %r is beyond the range '
'of this Python implementation.' % t)


class DateTimeParser(object):

Expand Down Expand Up @@ -699,18 +677,7 @@ def _calcTimezoneName(self, x, ms):
fsetAtEpoch = _tzoffset(self._localzone0, 0.0)
nearTime = x - fsetAtEpoch - EPOCH + 86400 + ms
# nearTime is within an hour of being correct.
try:
ltm = safelocaltime(nearTime)
except Exception:
# We are beyond the range of Python's date support.
# Hopefully we can assume that daylight savings schedules
# repeat every 28 years. Calculate the name of the
# time zone using a supported range of years.
yr, mo, dy, hr, mn, sc = _calcYMDHMS(x, 0)
yr = ((yr - 1970) % 28) + 1970
x = _calcDependentSecond2(yr, mo, dy, hr, mn, sc)
nearTime = x - fsetAtEpoch - EPOCH + 86400 + ms
ltm = safelocaltime(nearTime)
ltm = safelocaltime(nearTime)
tz = self.localZone(ltm)
return tz

Expand Down
51 changes: 25 additions & 26 deletions src/zope/datetime/tests/test_datetime.py
Expand Up @@ -28,24 +28,19 @@ def test_error(self):
class TestFuncs(unittest.TestCase):

def test_correctYear(self):

self.assertEqual(2069, datetime._correctYear(69))
self.assertEqual(1998, datetime._correctYear(98))


def test_safegmtime_safelocaltime_overflow(self):
def i(*args):
raise OverflowError()
try:
datetime.int = i
with self.assertRaises(datetime.TimeError):
datetime.safegmtime(1)

with self.assertRaises(datetime.TimeError):
datetime.safelocaltime(1)

finally:
del datetime.int
# Use values that are practically guaranteed to overflow on all
# platforms
v = 2**64 + 1
fv = float(v)
for func in (datetime.safegmtime, datetime.safelocaltime):
for x in (v, fv):
with self.assertRaises(datetime.TimeError):
func(x)

def test_safegmtime(self):
self.assertIsNotNone(datetime.safegmtime(6000))
Expand All @@ -68,13 +63,6 @@ def test_julianday(self):
self.assertEqual(datetime._julianday(2000, -1, 1), 2451483)
self.assertEqual(datetime._julianday(0, 1, 1), 1721057)

def test_calendarday(self):
# XXX: Why do we get different things on Py2 vs Py3?
# Are the calculations wrapping around somewhere? Is it the integer
# division?
answer = (-4712, 1, 3) if str is bytes else (-4711, 2, 0)
self.assertEqual(datetime._calendarday(1), answer)

def test_findLocalTimeZoneName(self):
zmap = datetime._cache._zmap
try:
Expand Down Expand Up @@ -196,19 +184,25 @@ def test_calcTimezoneName_safelocaltime_fail(self):
dtp._localzone0 = '0'
dtp._localzone1 = '1'

called = []
class MyException(Exception):
pass

def i(_):
if not called:
called.append(1)
raise OverflowError()
return (0, 0, 0, 0, 0, 0, 0, 0, 1)
raise MyException()

orig_safelocaltime = datetime.safelocaltime
try:
datetime.safelocaltime = i
self.assertEqual('1', dtp._calcTimezoneName(9467061400, 0))
self.assertRaises(MyException,
dtp._calcTimezoneName, 9467061400, 0)
finally:
datetime.safelocaltime = orig_safelocaltime

def test_calcTimezoneName_multiple_non_fail(self):
dtp = self._makeOne()
dtp._multipleZones = True
self.assertIsNotNone(dtp._calcTimezoneName(100, 1))

def test_parse_noniso_bad_month(self):
with self.assertRaises(datetime.SyntaxError):
self._callParse("2000--31 +1")
Expand Down Expand Up @@ -290,3 +284,8 @@ def test_parse_am_pm(self):

def test_valid_date(self):
self.assertFalse(self._makeOne()._validDate(2000, 0, 12))

def test_localZone_multiple(self):
p = self._makeOne()
p._multipleZones = True
self.assertIsNotNone(p.localZone())

0 comments on commit fff8043

Please sign in to comment.