Skip to content

Commit

Permalink
Merge pull request #763 from cssherry/partial_minutes_582
Browse files Browse the repository at this point in the history
Closes #582: Allow partial minutes for python 3.6 or newer
  • Loading branch information
pganssle committed Jun 18, 2018
2 parents af72d0d + 41e5176 commit 6dde5d6
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 26 deletions.
1 change: 1 addition & 0 deletions changelog.d/582.bug.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for sub-minute offsets in Python 3.6+. Fixed by @cssherry (gh issue #582, pr #763)
78 changes: 57 additions & 21 deletions dateutil/test/test_tz.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))

SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6)


###
# Helper functions
Expand Down Expand Up @@ -760,6 +762,25 @@ def test_tzoffset_singleton(args):

assert tz1 is tz2


@pytest.mark.tzoffset
@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
reason='Sub-minute offsets not supported')
def test_tzoffset_sub_minute():
delta = timedelta(hours=12, seconds=30)
test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
assert test_datetime.utcoffset() == delta


@pytest.mark.tzoffset
@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
reason='Sub-minute offsets supported')
def test_tzoffset_sub_minute_rounding():
delta = timedelta(hours=12, seconds=30)
test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
assert test_date.utcoffset() == timedelta(hours=12, minutes=1)


@pytest.mark.tzlocal
class TzLocalTest(unittest.TestCase):
def testEquality(self):
Expand Down Expand Up @@ -1908,11 +1929,10 @@ def testFilestreamWithNameRepr(self):
tzc = tz.tzfile(fileobj)
self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')')

def testRoundNonFullMinutes(self):
# This timezone has an offset of 5992 seconds in 1900-01-01.
tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)),
"1900-01-01 00:00:00+01:40")





def testLeapCountDecodesProperly(self):
# This timezone has leapcnt, and failed to decode until
Expand Down Expand Up @@ -1974,6 +1994,27 @@ def testTZSetDoesntCorrupt(self):
self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00')


@pytest.mark.tzfile
@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
reason='Sub-minute offsets not supported')
def test_tzfile_sub_minute_offset():
# If user running python 3.6 or newer, exact offset is used
tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
offset = timedelta(hours=1, minutes=39, seconds=52)
assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset


@pytest.mark.tzfile
@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
reason='Sub-minute offsets supported.')
def test_sub_minute_rounding_tzfile():
# This timezone has an offset of 5992 seconds in 1900-01-01.
# For python version pre-3.6, this will be rounded
tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
offset = timedelta(hours=1, minutes=40)
assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset


@unittest.skipUnless(IS_WIN, "Requires Windows")
class TzWinTest(unittest.TestCase, TzWinFoldMixin):
def setUp(self):
Expand Down Expand Up @@ -2618,33 +2659,28 @@ def __get_kiritimati_resolve_imaginary_test():
return (tzi, ) + dates


@pytest.mark.tz_resolve_imaginary
@pytest.mark.parametrize('tzi, dt, dt_exp', [
resolve_imaginary_tests = [
(tz.gettz('Europe/London'),
datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)),
(tz.gettz('America/New_York'),
datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)),
(tz.gettz('Australia/Sydney'),
datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)),
__get_kiritimati_resolve_imaginary_test(),
])
def test_resolve_imaginary(tzi, dt, dt_exp):
dt = dt.replace(tzinfo=tzi)
dt_exp = dt_exp.replace(tzinfo=tzi)
]

dt_r = tz.resolve_imaginary(dt)
assert dt_r == dt_exp
assert dt_r.tzname() == dt_exp.tzname()
assert dt_r.utcoffset() == dt_exp.utcoffset()

if SUPPORTS_SUB_MINUTE_OFFSETS:
resolve_imaginary_tests.append(
(tz.gettz('Africa/Monrovia'),
datetime(1972, 1, 7, 0, 30), datetime(1972, 1, 7, 1, 14, 30)))


@pytest.mark.xfail
@pytest.mark.tz_resolve_imaginary
def test_resolve_imaginary_monrovia():
# See GH #582 - When that is resolved, move this into test_resolve_imaginary
tzi = tz.gettz('Africa/Monrovia')
dt = datetime(1972, 1, 7, hour=0, minute=30, second=0, tzinfo=tzi)
dt_exp = datetime(1972, 1, 7, hour=1, minute=14, second=30, tzinfo=tzi)
@pytest.mark.parametrize('tzi, dt, dt_exp', resolve_imaginary_tests)
def test_resolve_imaginary(tzi, dt, dt_exp):
dt = dt.replace(tzinfo=tzi)
dt_exp = dt_exp.replace(tzinfo=tzi)

dt_r = tz.resolve_imaginary(dt)
assert dt_r == dt_exp
Expand Down
25 changes: 20 additions & 5 deletions dateutil/tz/tz.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
except ImportError:
tzwin = tzwinlocal = None

# For warning about rounding tzinfo
from warnings import warn

ZERO = datetime.timedelta(0)
EPOCH = datetime.datetime.utcfromtimestamp(0)
EPOCHORDINAL = EPOCH.toordinal()
Expand Down Expand Up @@ -138,7 +141,8 @@ def __init__(self, name, offset):
offset = offset.total_seconds()
except (TypeError, AttributeError):
pass
self._offset = datetime.timedelta(seconds=offset)

self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))

def utcoffset(self, dt):
return self._offset
Expand Down Expand Up @@ -601,10 +605,7 @@ def _read_tzfile(self, fileobj):
out.ttinfo_list = []
for i in range(typecnt):
gmtoff, isdst, abbrind = ttinfo[i]
# Round to full-minutes if that's not the case. Python's
# datetime doesn't accept sub-minute timezones. Check
# http://python.org/sf/1447945 for some information.
gmtoff = 60 * ((gmtoff + 30) // 60)
gmtoff = _get_supported_offset(gmtoff)
tti = _ttinfo()
tti.offset = gmtoff
tti.dstoffset = datetime.timedelta(0)
Expand Down Expand Up @@ -1769,6 +1770,20 @@ def _datetime_to_timestamp(dt):
return (dt.replace(tzinfo=None) - EPOCH).total_seconds()


if sys.version_info >= (3, 6):
def _get_supported_offset(second_offset):
return second_offset
else:
def _get_supported_offset(second_offset):
# For python pre-3.6, round to full-minutes if that's not the case.
# Python's datetime doesn't accept sub-minute timezones. Check
# http://python.org/sf/1447945 or https://bugs.python.org/issue5288
# for some information.
old_offset = second_offset
calculated_offset = 60 * ((second_offset + 30) // 60)
return calculated_offset


class _ContextWrapper(object):
"""
Class for wrapping contexts so that they are passed through in a
Expand Down

0 comments on commit 6dde5d6

Please sign in to comment.